diff --git a/example/lib/main.dart b/example/lib/main.dart index d468e0d90..28d75ace6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map_example/pages/abort_obsolete_requests.dart'; import 'package:flutter_map_example/pages/animated_map_controller.dart'; +import 'package:flutter_map_example/pages/antimeridian.dart'; import 'package:flutter_map_example/pages/bundled_offline_map.dart'; import 'package:flutter_map_example/pages/circle.dart'; import 'package:flutter_map_example/pages/debouncing_tile_update_transformer.dart'; @@ -56,6 +57,7 @@ class MyApp extends StatelessWidget { AbortObsoleteRequestsPage.route: (context) => const AbortObsoleteRequestsPage(), PolylinePage.route: (context) => const PolylinePage(), + AntimeridianPage.route: (context) => const AntimeridianPage(), SingleWorldPolysPage.route: (context) => const SingleWorldPolysPage(), PolylinePerfStressPage.route: (context) => const PolylinePerfStressPage(), diff --git a/example/lib/pages/antimeridian.dart b/example/lib/pages/antimeridian.dart new file mode 100644 index 000000000..ecfa579ae --- /dev/null +++ b/example/lib/pages/antimeridian.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_example/misc/tile_providers.dart'; +import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart'; +import 'package:latlong2/latlong.dart'; + +/// Testing if we can fling beyond the world. We can't, due to the constraint. +class AntimeridianPage extends StatelessWidget { + static const String route = '/antimeridian'; + + const AntimeridianPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Antimeridian')), + drawer: const MenuDrawer(AntimeridianPage.route), + body: FlutterMap( + options: MapOptions( + initialZoom: 2, + cameraConstraint: CameraConstraint.contain( + bounds: LatLngBounds( + const LatLng(90, -180), + const LatLng(-90, 180), + ), + ), + ), + children: [ + openStreetMapTileLayer, + ], + ), + ); + } +} diff --git a/example/lib/widgets/drawer/menu_drawer.dart b/example/lib/widgets/drawer/menu_drawer.dart index cb55795a0..53a30ca73 100644 --- a/example/lib/widgets/drawer/menu_drawer.dart +++ b/example/lib/widgets/drawer/menu_drawer.dart @@ -5,6 +5,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map_example/pages/abort_obsolete_requests.dart'; import 'package:flutter_map_example/pages/animated_map_controller.dart'; +import 'package:flutter_map_example/pages/antimeridian.dart'; import 'package:flutter_map_example/pages/bundled_offline_map.dart'; import 'package:flutter_map_example/pages/circle.dart'; import 'package:flutter_map_example/pages/debouncing_tile_update_transformer.dart'; @@ -148,6 +149,11 @@ class MenuDrawer extends StatelessWidget { routeName: PolylinePage.route, currentRoute: currentRoute, ), + MenuItemWidget( + caption: 'Antimeridian test', + routeName: AntimeridianPage.route, + currentRoute: currentRoute, + ), MenuItemWidget( caption: 'Circle Layer', routeName: CirclePage.route, diff --git a/lib/src/gestures/map_interactive_viewer.dart b/lib/src/gestures/map_interactive_viewer.dart index ab9f8f07f..e2f79011f 100644 --- a/lib/src/gestures/map_interactive_viewer.dart +++ b/lib/src/gestures/map_interactive_viewer.dart @@ -114,6 +114,7 @@ class MapInteractiveViewerState extends State late var _keyboardPanAnimationPrevZoom = _camera.zoom; // to detect changes late double _keyboardPanAnimationMaxVelocity; + double _keyboardPanAnimationMaxVelocityCalculator(double zoom) => _interactionOptions.keyboardOptions.maxPanVelocity?.call(zoom) ?? 5 * math.log(0.15 * zoom + 1) + 1; @@ -124,7 +125,9 @@ class MapInteractiveViewerState extends State // Shortcuts MapCamera get _camera => widget.controller.camera; + MapOptions get _options => widget.controller.options; + InteractionOptions get _interactionOptions => _options.interactionOptions; @override @@ -965,12 +968,15 @@ class MapInteractiveViewerState extends State newCenter = _camera.unprojectAtZoom(bestCenterPoint); } - widget.controller.moveRaw( + final moved = widget.controller.moveRaw( newCenter, _camera.zoom, hasGesture: true, source: MapEventSource.flingAnimationController, ); + if (!moved) { + _closeFlingAnimationController(MapEventSource.flingAnimationController); + } } void _resetDoubleTapHold() { diff --git a/lib/src/map/camera/camera.dart b/lib/src/map/camera/camera.dart index c9c6d980a..b68c2c4d0 100644 --- a/lib/src/map/camera/camera.dart +++ b/lib/src/map/camera/camera.dart @@ -44,6 +44,9 @@ class MapCamera { /// FlutterMap widget. final Size nonRotatedSize; + /// Is it the result of a constraint? + final bool constrained; + /// Lazily calculated field Size? _cameraSize; @@ -136,6 +139,7 @@ class MapCamera { required this.nonRotatedSize, this.minZoom, this.maxZoom, + this.constrained = false, Size? size, Rect? pixelBounds, LatLngBounds? bounds, @@ -218,6 +222,7 @@ class MapCamera { MapCamera withPosition({ LatLng? center, double? zoom, + bool constrained = false, }) => MapCamera( crs: crs, @@ -228,6 +233,7 @@ class MapCamera { rotation: rotation, nonRotatedSize: nonRotatedSize, size: _cameraSize, + constrained: constrained, ); /// Jumps camera to opposite side of the world to enable seamless scrolling diff --git a/lib/src/map/camera/camera_constraint.dart b/lib/src/map/camera/camera_constraint.dart index 423452872..75297f0a4 100644 --- a/lib/src/map/camera/camera_constraint.dart +++ b/lib/src/map/camera/camera_constraint.dart @@ -79,18 +79,18 @@ class ContainCameraCenter extends CameraConstraint { final LatLngBounds bounds; @override - MapCamera constrain(MapCamera camera) => camera.withPosition( - center: LatLng( - camera.center.latitude.clamp( - bounds.south, - bounds.north, - ), - camera.center.longitude.clamp( - bounds.west, - bounds.east, - ), - ), - ); + MapCamera constrain(MapCamera camera) { + final latitude = camera.center.latitude.clamp(bounds.south, bounds.north); + final longitude = camera.center.longitude.clamp(bounds.west, bounds.east); + if (latitude == camera.center.latitude && + longitude == camera.center.longitude) { + return camera; + } + return camera.withPosition( + center: LatLng(latitude, longitude), + constrained: true, + ); + } @override bool operator ==(Object other) { @@ -142,10 +142,13 @@ class ContainCamera extends CameraConstraint { centerPix.dy.clamp(topOkCenter, botOkCenter), ); - if (newCenterPix == centerPix) return camera; + if (newCenterPix == centerPix) { + return camera; + } return camera.withPosition( center: camera.unprojectAtZoom(newCenterPix, testZoom), + constrained: true, ); } @@ -211,6 +214,7 @@ class ContainCameraLatitude extends CameraConstraint { return camera.withPosition( center: camera.unprojectAtZoom(newCenterPix, testZoom), + constrained: true, ); } diff --git a/lib/src/map/controller/map_controller_impl.dart b/lib/src/map/controller/map_controller_impl.dart index 0ed0ed9b9..3caa70154 100644 --- a/lib/src/map/controller/map_controller_impl.dart +++ b/lib/src/map/controller/map_controller_impl.dart @@ -170,6 +170,16 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState> (newCamera.center == camera.center && newCamera.zoom == camera.zoom)) { return false; } + if (newCamera.constrained) { + if (options.cameraConstraint is ContainCamera) { + final ContainCamera containCamera = + options.cameraConstraint as ContainCamera; + if (containCamera.bounds.west == -180 && + containCamera.bounds.east == 180) { + return false; + } + } + } final oldCamera = camera; value = value.withMapCamera(newCamera); diff --git a/test/map/camera/camera_constraint_test.dart b/test/map/camera/camera_constraint_test.dart index 73d9e60a7..7bad712c5 100644 --- a/test/map/camera/camera_constraint_test.dart +++ b/test/map/camera/camera_constraint_test.dart @@ -28,6 +28,7 @@ void main() { expect(clamped.zoom, 1); expect(clamped.center.latitude, closeTo(-48.562, 0.001)); expect(clamped.center.longitude, closeTo(-55.703, 0.001)); + expect(clamped.constrained, isTrue); }); }); @@ -48,6 +49,7 @@ void main() { expect(clamped.zoom, 1); expect(clamped.center.latitude, closeTo(0, 0.001)); expect(clamped.center.longitude, closeTo(-179.9, 0.001)); + expect(clamped.constrained, isFalse); }); }); @@ -67,6 +69,7 @@ void main() { expect(clamped.zoom, 1); expect(clamped.center.latitude, closeTo(-59.534, 0.001)); expect(clamped.center.longitude, closeTo(179, 0.001)); + expect(clamped.constrained, isTrue); }); test('northern hemisphere', () { @@ -85,6 +88,7 @@ void main() { expect(clamped.zoom, 2); expect(clamped.center.latitude, closeTo(46.558, 0.001)); expect(clamped.center.longitude, closeTo(179, 0.001)); + expect(clamped.constrained, isTrue); }); test('can not translate camera within bounds', () {