From 139e9add4f02a74c5bae988d4f8af830788bb643 Mon Sep 17 00:00:00 2001 From: Mahesh Jamdade Date: Sun, 24 May 2026 18:31:17 -0400 Subject: [PATCH] Fix: Latlng is not finite exception --- lib/src/gestures/map_interactive_viewer.dart | 3 +- lib/src/map/camera/camera.dart | 40 ++++- .../map/controller/map_controller_impl.dart | 9 + .../map/camera/camera_finite_latlng_test.dart | 162 ++++++++++++++++++ 4 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 test/map/camera/camera_finite_latlng_test.dart diff --git a/lib/src/gestures/map_interactive_viewer.dart b/lib/src/gestures/map_interactive_viewer.dart index 34a9c4bf2..adf1c9f2d 100644 --- a/lib/src/gestures/map_interactive_viewer.dart +++ b/lib/src/gestures/map_interactive_viewer.dart @@ -706,7 +706,8 @@ class MapInteractiveViewerState extends State final moveDifference = _rotateOffset(_focalStartLocal - _lastFocalLocal); final newCenterPt = oldCenterPt + zoomDifference + moveDifference; - return _camera.unprojectAtZoom(newCenterPt, zoomAfterPinchZoom); + final newCenter = _camera.unprojectAtZoom(newCenterPt, zoomAfterPinchZoom); + return isFiniteMapLatLng(newCenter) ? newCenter : _camera.center; } void _handleScalePinchRotate( diff --git a/lib/src/map/camera/camera.dart b/lib/src/map/camera/camera.dart index c9c6d980a..2d0589bbf 100644 --- a/lib/src/map/camera/camera.dart +++ b/lib/src/map/camera/camera.dart @@ -6,6 +6,12 @@ import 'package:flutter_map/src/map/inherited_model.dart'; import 'package:flutter_map/src/misc/deg_rad_conversions.dart'; import 'package:flutter_map/src/misc/extensions.dart'; import 'package:latlong2/latlong.dart'; +import 'package:meta/meta.dart'; + +/// Whether [latLng] has finite coordinates (not NaN or ±infinity). +@internal +bool isFiniteMapLatLng(LatLng latLng) => + latLng.latitude.isFinite && latLng.longitude.isFinite; /// Describes the view of a map. This includes the size/zoom/position/crs as /// well as the minimum/maximum zoom. This class is mostly immutable but has @@ -418,15 +424,43 @@ class MapCamera { /// Calculate the center point which would keep the same point of the map /// visible at the given [cursorPos] with the zoom set to [zoom]. LatLng focusedZoomCenter(Offset cursorPos, double zoom) { + if (!zoom.isFinite || + !this.zoom.isFinite || + !nonRotatedSize.width.isFinite || + !nonRotatedSize.height.isFinite || + nonRotatedSize.width <= 0 || + nonRotatedSize.height <= 0) { + return center; + } + // Calculate offset of mouse cursor from viewport center final offset = (cursorPos - nonRotatedSize.center(Offset.zero)).rotate(rotationRad); // Match new center coordinate to mouse cursor position final scale = getZoomScale(zoom, this.zoom); - final newOffset = offset * (1.0 - 1.0 / scale); + // scale == 0 makes (1 - 1/scale) non-finite; 0 * infinity => NaN offset + if (!scale.isFinite || scale <= 0) { + return center; + } + + final zoomMultiplier = 1.0 - 1.0 / scale; + if (!zoomMultiplier.isFinite) { + return center; + } + + final newOffset = offset * zoomMultiplier; + if (!newOffset.dx.isFinite || !newOffset.dy.isFinite) { + return center; + } + final mapCenter = projectAtZoom(center); - final newCenter = unprojectAtZoom(mapCenter + newOffset); - return newCenter; + final projected = mapCenter + newOffset; + if (!projected.dx.isFinite || !projected.dy.isFinite) { + return center; + } + + final newCenter = unprojectAtZoom(projected); + return isFiniteMapLatLng(newCenter) ? newCenter : center; } @override diff --git a/lib/src/map/controller/map_controller_impl.dart b/lib/src/map/controller/map_controller_impl.dart index 0ed0ed9b9..13985e569 100644 --- a/lib/src/map/controller/map_controller_impl.dart +++ b/lib/src/map/controller/map_controller_impl.dart @@ -147,6 +147,10 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState> required MapEventSource source, String? id, }) { + if (!newZoom.isFinite || !isFiniteMapLatLng(newCenter)) { + return false; + } + // Algorithm thanks to https://github.com/tlserver/flutter_map_location_marker LatLng center = newCenter; if (offset != Offset.zero) { @@ -160,6 +164,10 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState> ); } + if (!isFiniteMapLatLng(center)) { + return false; + } + MapCamera? newCamera = camera.withPosition( center: center, zoom: camera.clampZoom(newZoom), @@ -167,6 +175,7 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState> newCamera = options.cameraConstraint.constrain(newCamera); if (newCamera == null || + !isFiniteMapLatLng(newCamera.center) || (newCamera.center == camera.center && newCamera.zoom == camera.zoom)) { return false; } diff --git a/test/map/camera/camera_finite_latlng_test.dart b/test/map/camera/camera_finite_latlng_test.dart new file mode 100644 index 000000000..9a570bf36 --- /dev/null +++ b/test/map/camera/camera_finite_latlng_test.dart @@ -0,0 +1,162 @@ +import 'dart:ui'; + +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_range_calculator.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:latlong2/latlong.dart'; + +import '../../test_utils/test_app.dart'; + +void main() { + group('isFiniteMapLatLng', () { + test('returns true for normal coordinates', () { + expect(isFiniteMapLatLng(const LatLng(51.5, -0.09)), isTrue); + }); + + test('returns false for NaN and infinity', () { + expect(isFiniteMapLatLng(const LatLng(double.nan, 0)), isFalse); + expect(isFiniteMapLatLng(const LatLng(0, double.nan)), isFalse); + expect(isFiniteMapLatLng(const LatLng(double.infinity, 0)), isFalse); + }); + }); + + group('MapCamera.focusedZoomCenter', () { + const center = LatLng(40.7128, -74.006); + + MapCamera camera({ + required Size size, + double zoom = 12, + }) => + MapCamera( + crs: const Epsg3857(), + center: center, + zoom: zoom, + rotation: 0, + nonRotatedSize: size, + ); + + test('returns current center before layout size is known', () { + final cam = MapCamera.initialCamera( + const MapOptions( + initialCenter: center, + initialZoom: 12, + ), + ); + + expect(cam.nonRotatedSize, MapCamera.kImpossibleSize); + expect( + cam.focusedZoomCenter(const Offset(100, 100), 5.5), + cam.center, + ); + }); + + test('returns current center for non-finite target zoom', () { + final cam = camera(size: const Size(400, 600)); + expect( + cam.focusedZoomCenter(const Offset(200, 300), double.nan), + center, + ); + }); + + test('returns current center for invalid viewport size', () { + final cam = camera(size: const Size(0, 600)); + expect( + cam.focusedZoomCenter(const Offset(0, 0), 8), + center, + ); + }); + + test('returns finite coordinates when cursor is at viewport center', () { + final cam = camera(size: const Size(400, 600)); + final result = cam.focusedZoomCenter( + const Offset(200, 300), + 12, + ); + expect(isFiniteMapLatLng(result), isTrue); + expect(result.latitude, closeTo(center.latitude, 0.0001)); + expect(result.longitude, closeTo(center.longitude, 0.0001)); + }); + + test('returns finite coordinates when zooming out toward min zoom', () { + final cam = camera(size: const Size(400, 600), zoom: 14); + final result = cam.focusedZoomCenter(const Offset(50, 80), 5.5); + expect(isFiniteMapLatLng(result), isTrue); + expect(() => cam.projectAtZoom(result, 5.5), returnsNormally); + }); + }); + + group('MapController.move', () { + testWidgets('rejects non-finite center and leaves camera unchanged', ( + tester, + ) async { + final controller = MapController(); + await tester.pumpWidget(TestApp(controller: controller)); + await tester.pump(); + + final beforeCenter = controller.camera.center; + final beforeZoom = controller.camera.zoom; + + expect( + controller.move(const LatLng(double.nan, double.nan), beforeZoom), + isFalse, + ); + expect(controller.camera.center, beforeCenter); + expect(controller.camera.zoom, beforeZoom); + expect(isFiniteMapLatLng(controller.camera.center), isTrue); + }); + + testWidgets('rejects non-finite zoom', (tester) async { + final controller = MapController(); + await tester.pumpWidget(TestApp(controller: controller)); + await tester.pump(); + + final before = controller.camera.center; + + expect( + controller.move(before, double.nan), + isFalse, + ); + expect(controller.camera.center, before); + }); + }); + + group('TileRangeCalculator', () { + test('calculate succeeds with finite map center', () { + const calculator = TileRangeCalculator(tileDimension: 256); + final camera = MapCamera( + crs: const Epsg3857(), + center: const LatLng(51.5, -0.09), + zoom: 10, + rotation: 0, + nonRotatedSize: const Size(400, 400), + ); + + expect( + () => calculator.calculate(camera: camera, tileZoom: 10), + returnsNormally, + ); + }); + + test('calculate throws when center is non-finite', () { + const calculator = TileRangeCalculator(tileDimension: 256); + final camera = MapCamera( + crs: const Epsg3857(), + center: const LatLng(double.nan, double.nan), + zoom: 10, + rotation: 0, + nonRotatedSize: const Size(400, 400), + ); + + expect( + () => calculator.calculate(camera: camera, tileZoom: 10), + throwsA( + isA().having( + (e) => e.toString(), + 'message', + contains('LatLng is not finite'), + ), + ), + ); + }); + }); +}