From 4cd53e89fef724b4332937530eb54da11f1e1d2c Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Wed, 7 Jun 2023 19:03:02 +0200 Subject: [PATCH 01/46] Split FlutterMapState in to a stateful container widget (FlutterMapStateContainer) and an immutable representation of the state of the map (FlutterMapState) --- .../lib/pages/zoombuttons_plugin_option.dart | 6 +- .../Flutter/GeneratedPluginRegistrant.swift | 2 - lib/plugin_api.dart | 6 +- lib/src/gestures/gestures.dart | 178 ++-- lib/src/gestures/map_events.dart | 158 +--- lib/src/layer/attribution_layer/rich.dart | 6 +- lib/src/layer/circle_layer.dart | 2 +- lib/src/layer/marker_layer.dart | 2 +- lib/src/layer/overlay_image_layer.dart | 2 +- lib/src/layer/polygon_layer.dart | 2 +- lib/src/layer/polyline_layer.dart | 2 +- lib/src/layer/tile_layer/tile_layer.dart | 36 +- .../tile_layer/tile_range_calculator.dart | 4 +- .../layer/tile_layer/tile_update_event.dart | 7 + lib/src/map/controller.dart | 22 +- lib/src/map/flutter_map_state.dart | 400 +++++++++ lib/src/map/flutter_map_state_container.dart | 563 ++++++++++++ .../flutter_map_state_inherited_widget.dart | 23 + lib/src/map/state.dart | 850 ------------------ lib/src/map/widget.dart | 5 +- 20 files changed, 1172 insertions(+), 1104 deletions(-) create mode 100644 lib/src/map/flutter_map_state.dart create mode 100644 lib/src/map/flutter_map_state_container.dart create mode 100644 lib/src/map/flutter_map_state_inherited_widget.dart delete mode 100644 lib/src/map/state.dart diff --git a/example/lib/pages/zoombuttons_plugin_option.dart b/example/lib/pages/zoombuttons_plugin_option.dart index 1bb102fdc..0d1ac33a6 100644 --- a/example/lib/pages/zoombuttons_plugin_option.dart +++ b/example/lib/pages/zoombuttons_plugin_option.dart @@ -54,8 +54,7 @@ class FlutterMapZoomButtons extends StatelessWidget { if (zoom > maxZoom) { zoom = maxZoom; } - map.move(centerZoom.center, zoom, - source: MapEventSource.custom); + MapController.of(context).move(centerZoom.center, zoom); }, child: Icon(zoomInIcon, color: zoomInColorIcon ?? IconTheme.of(context).color), @@ -74,8 +73,7 @@ class FlutterMapZoomButtons extends StatelessWidget { if (zoom < minZoom) { zoom = minZoom; } - map.move(centerZoom.center, zoom, - source: MapEventSource.custom); + MapController.of(context).move(centerZoom.center, zoom); }, child: Icon(zoomOutIcon, color: zoomOutColorIcon ?? IconTheme.of(context).color), diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 057e8f8a6..997e35da2 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,12 +5,10 @@ import FlutterMacOS import Foundation -import location import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - LocationPlugin.register(with: registry.registrar(forPlugin: "LocationPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/lib/plugin_api.dart b/lib/plugin_api.dart index cecfbbb40..e09f3fa65 100644 --- a/lib/plugin_api.dart +++ b/lib/plugin_api.dart @@ -1,9 +1,9 @@ library flutter_map.plugin_api; export 'package:flutter_map/flutter_map.dart'; -export 'package:flutter_map/src/map/state.dart'; +// ignore: invalid_export_of_internal_element +export 'package:flutter_map/src/map/controller.dart' show MapControllerImpl; +export 'package:flutter_map/src/map/flutter_map_state.dart'; export 'package:flutter_map/src/misc/private/bounds.dart'; export 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; export 'package:flutter_map/src/misc/private/util.dart'; -// ignore: invalid_export_of_internal_element -export 'package:flutter_map/src/map/controller.dart' show MapControllerImpl; diff --git a/lib/src/gestures/gestures.dart b/lib/src/gestures/gestures.dart index 2fcf4db17..6f718a60e 100644 --- a/lib/src/gestures/gestures.dart +++ b/lib/src/gestures/gestures.dart @@ -5,7 +5,8 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/gestures/latlng_tween.dart'; -import 'package:flutter_map/src/map/state.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/map/flutter_map_state_container.dart'; import 'package:latlong2/latlong.dart'; abstract class MapGestureMixin extends State @@ -72,7 +73,8 @@ abstract class MapGestureMixin extends State _offsetToPoint(pointerSignal.localPosition), newZoom); // Move to new center and zoom level - mapState.move(newCenterZoom[0] as LatLng, newCenterZoom[1] as double, + mapStateContainer.move( + newCenterZoom[0] as LatLng, newCenterZoom[1] as double, source: MapEventSource.scrollWheel); }); } @@ -110,6 +112,7 @@ abstract class MapGestureMixin extends State @override FlutterMap get widget; + FlutterMapStateContainer get mapStateContainer; FlutterMapState get mapState; MapController get mapController; @@ -159,10 +162,9 @@ abstract class MapGestureMixin extends State _gestureWinner = MultiFingerGesture.none; } - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventRotateEnd( - center: mapState.center, - zoom: mapState.zoom, + mapState: mapState, source: MapEventSource.interactiveFlagsChanged, ), ); @@ -199,10 +201,9 @@ abstract class MapGestureMixin extends State } if (emitMapEventMoveEnd) { - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventRotateEnd( - center: mapState.center, - zoom: mapState.zoom, + mapState: mapState, source: MapEventSource.interactiveFlagsChanged, ), ); @@ -247,9 +248,11 @@ abstract class MapGestureMixin extends State _stopListeningForAnimationInterruptions(); - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventFlingAnimationEnd( - center: mapState.center, zoom: mapState.zoom, source: source), + mapState: mapState, + source: source, + ), ); } } @@ -260,9 +263,11 @@ abstract class MapGestureMixin extends State _stopListeningForAnimationInterruptions(); - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventDoubleTapZoomEnd( - center: mapState.center, zoom: mapState.zoom, source: source), + mapState: mapState, + source: source, + ), ); } } @@ -278,8 +283,8 @@ abstract class MapGestureMixin extends State _gestureWinner = MultiFingerGesture.none; - _mapZoomStart = mapState.zoom; - _mapCenterStart = mapState.center; + _mapZoomStart = mapStateContainer.zoom; + _mapCenterStart = mapStateContainer.center; _focalStartLocal = _lastFocalLocal = details.localFocalPoint; _focalStartLatLng = _offsetToCrs(_focalStartLocal); @@ -315,25 +320,26 @@ abstract class MapGestureMixin extends State // [didUpdateWidget] will emit MapEventMoveEnd and if drag is enabled // again then this will emit the start event again. _dragStarted = true; - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventMoveStart( - center: mapState.center, - zoom: mapState.zoom, + mapState: mapState, source: eventSource, ), ); } - final oldCenterPt = mapState.project(mapState.center, mapState.zoom); + final oldCenterPt = + mapState.project(mapStateContainer.center, mapStateContainer.zoom); final localDistanceOffset = _rotateOffset(_lastFocalLocal - focalOffset); final newCenterPt = oldCenterPt + _offsetToPoint(localDistanceOffset); - final newCenter = mapState.unproject(newCenterPt, mapState.zoom); + final newCenter = + mapState.unproject(newCenterPt, mapStateContainer.zoom); - mapState.move( + mapStateContainer.move( newCenter, - mapState.zoom, + mapStateContainer.zoom, hasGesture: true, source: eventSource, ); @@ -404,10 +410,9 @@ abstract class MapGestureMixin extends State if (!_pinchMoveStarted) { // emit MoveStart event only if pinchMove hasn't started - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventMoveStart( - center: mapState.center, - zoom: mapState.zoom, + mapState: mapState, source: eventSource, ), ); @@ -415,7 +420,7 @@ abstract class MapGestureMixin extends State } } } else { - newZoom = mapState.zoom; + newZoom = mapStateContainer.zoom; } LatLng newCenter; @@ -425,10 +430,9 @@ abstract class MapGestureMixin extends State if (!_pinchZoomStarted) { // emit MoveStart event only if pinchZoom hasn't started - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventMoveStart( - center: mapState.center, - zoom: mapState.zoom, + mapState: mapState, source: eventSource, ), ); @@ -436,7 +440,8 @@ abstract class MapGestureMixin extends State } if (_pinchZoomStarted || _pinchMoveStarted) { - final oldCenterPt = mapState.project(mapState.center, newZoom); + final oldCenterPt = + mapState.project(mapStateContainer.center, newZoom); final newFocalLatLong = _offsetToCrs(_focalStartLocal, newZoom); final newFocalPt = mapState.project(newFocalLatLong, newZoom); final oldFocalPt = mapState.project(_focalStartLatLng, newZoom); @@ -449,14 +454,14 @@ abstract class MapGestureMixin extends State _offsetToPoint(moveDifference); newCenter = mapState.unproject(newCenterPt, newZoom); } else { - newCenter = mapState.center; + newCenter = mapStateContainer.center; } } else { - newCenter = mapState.center; + newCenter = mapStateContainer.center; } if (_pinchZoomStarted || _pinchMoveStarted) { - mapMoved = mapState.move( + mapMoved = mapStateContainer.move( newCenter, newZoom, hasGesture: true, @@ -468,10 +473,9 @@ abstract class MapGestureMixin extends State if (hasRotate) { if (!_rotationStarted && currentRotation != 0.0) { _rotationStarted = true; - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventRotateStart( - center: mapState.center, - zoom: mapState.zoom, + mapState: mapState, source: eventSource, ), ); @@ -479,25 +483,25 @@ abstract class MapGestureMixin extends State if (_rotationStarted) { final rotationDiff = currentRotation - _lastRotation; - final oldCenterPt = mapState.project(mapState.center); + final oldCenterPt = mapState.project(mapStateContainer.center); final rotationCenter = mapState.project(_offsetToCrs(_lastFocalLocal)); final vector = oldCenterPt - rotationCenter; final rotatedVector = vector.rotate(degToRadian(rotationDiff)); final newCenter = rotationCenter + rotatedVector; - mapMoved = mapState.move( - mapState.unproject(newCenter), mapState.zoom, + mapMoved = mapStateContainer.move( + mapState.unproject(newCenter), mapStateContainer.zoom, source: eventSource) || mapMoved; - mapRotated = mapState.rotate( - mapState.rotation + rotationDiff, + mapRotated = mapStateContainer.rotate( + mapStateContainer.rotation + rotationDiff, hasGesture: true, source: eventSource, ); } } - if (mapMoved || mapRotated) mapState.setState(() {}); + if (mapMoved || mapRotated) mapStateContainer.setState(() {}); } } } @@ -515,10 +519,9 @@ abstract class MapGestureMixin extends State if (_rotationStarted) { _rotationStarted = false; - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventRotateEnd( - center: mapState.center, - zoom: mapState.zoom, + mapState: mapState, source: eventSource, ), ); @@ -526,10 +529,9 @@ abstract class MapGestureMixin extends State if (_dragStarted || _pinchZoomStarted || _pinchMoveStarted) { _dragStarted = _pinchZoomStarted = _pinchMoveStarted = false; - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventMoveEnd( - center: mapState.center, - zoom: mapState.zoom, + mapState: mapState, source: eventSource, ), ); @@ -541,10 +543,9 @@ abstract class MapGestureMixin extends State final magnitude = details.velocity.pixelsPerSecond.distance; if (magnitude < _kMinFlingVelocity || !hasFling) { if (hasFling) { - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventFlingAnimationNotStarted( - center: mapState.center, - zoom: mapState.zoom, + mapState: mapState, source: eventSource, ), ); @@ -589,11 +590,10 @@ abstract class MapGestureMixin extends State onTap(position, latlng); } - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventTap( tapPosition: latlng, - center: mapState.center, - zoom: mapState.zoom, + mapState: mapState, source: MapEventSource.tap, ), ); @@ -613,11 +613,10 @@ abstract class MapGestureMixin extends State onSecondaryTap(position, latlng); } - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventSecondaryTap( tapPosition: latlng, - center: mapState.center, - zoom: mapState.zoom, + mapState: mapState, source: MapEventSource.secondaryTap, ), ); @@ -635,24 +634,23 @@ abstract class MapGestureMixin extends State options.onLongPress!(position, latlng); } - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventLongPress( tapPosition: latlng, - center: mapState.center, - zoom: mapState.zoom, + mapState: mapState, source: MapEventSource.longPress, ), ); } LatLng _offsetToCrs(Offset offset, [double? zoom]) { - final focalStartPt = - mapState.project(mapState.center, zoom ?? mapState.zoom); + final focalStartPt = mapState.project( + mapStateContainer.center, zoom ?? mapStateContainer.zoom); final point = (_offsetToPoint(offset) - (mapState.nonrotatedSize / 2.0)) .rotate(mapState.rotationRad); final newCenterPt = focalStartPt + point; - return mapState.unproject(newCenterPt, zoom ?? mapState.zoom); + return mapState.unproject(newCenterPt, zoom ?? mapStateContainer.zoom); } void handleDoubleTap(TapPosition tapPosition) { @@ -665,7 +663,7 @@ abstract class MapGestureMixin extends State options.interactiveFlags, InteractiveFlag.doubleTapZoom)) { final centerZoom = _getNewEventCenterZoomPosition( _offsetToPoint(tapPosition.relative!), - _getZoomForScale(mapState.zoom, 2)); + _getZoomForScale(mapStateContainer.zoom, 2)); _startDoubleTapAnimation( centerZoom[1] as double, centerZoom[0] as LatLng); } @@ -681,19 +679,21 @@ abstract class MapGestureMixin extends State final viewCenter = mapState.nonrotatedSize / 2; final offset = (cursorPos - viewCenter).rotate(mapState.rotationRad); // Match new center coordinate to mouse cursor position - final scale = mapState.getZoomScale(newZoom, mapState.zoom); + final scale = + mapStateContainer.getZoomScale(newZoom, mapStateContainer.zoom); final newOffset = offset * (1.0 - 1.0 / scale); - final mapCenter = mapState.project(mapState.center); + final mapCenter = mapState.project(mapStateContainer.center); final newCenter = mapState.unproject(mapCenter + newOffset); return [newCenter, newZoom]; } void _startDoubleTapAnimation(double newZoom, LatLng newCenter) { - _doubleTapZoomAnimation = Tween(begin: mapState.zoom, end: newZoom) - .chain(CurveTween(curve: Curves.linear)) - .animate(_doubleTapController); + _doubleTapZoomAnimation = + Tween(begin: mapStateContainer.zoom, end: newZoom) + .chain(CurveTween(curve: Curves.linear)) + .animate(_doubleTapController); _doubleTapCenterAnimation = - LatLngTween(begin: mapState.center, end: newCenter) + LatLngTween(begin: mapStateContainer.center, end: newCenter) .chain(CurveTween(curve: Curves.linear)) .animate(_doubleTapController); _doubleTapController.forward(from: 0); @@ -701,26 +701,24 @@ abstract class MapGestureMixin extends State void _doubleTapZoomStatusListener(AnimationStatus status) { if (status == AnimationStatus.forward) { - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventDoubleTapZoomStart( - center: mapState.center, - zoom: mapState.zoom, + mapState: mapState, source: MapEventSource.doubleTapZoomAnimationController), ); _startListeningForAnimationInterruptions(); } else if (status == AnimationStatus.completed) { _stopListeningForAnimationInterruptions(); - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventDoubleTapZoomEnd( - center: mapState.center, - zoom: mapState.zoom, + mapState: mapState, source: MapEventSource.doubleTapZoomAnimationController), ); } } void _handleDoubleTapZoomAnimation() { - mapState.move( + mapStateContainer.move( _doubleTapCenterAnimation.value, _doubleTapZoomAnimation.value, hasGesture: true, @@ -742,7 +740,7 @@ abstract class MapGestureMixin extends State final flags = options.interactiveFlags; if (InteractiveFlag.hasFlag(flags, InteractiveFlag.pinchZoom)) { - final zoom = mapState.zoom; + final zoom = mapStateContainer.zoom; final focalOffset = details.localFocalPoint; final verticalOffset = (_focalStartLocal - focalOffset).dy; final newZoom = _mapZoomStart - verticalOffset / 360 * zoom; @@ -750,8 +748,8 @@ abstract class MapGestureMixin extends State final max = options.maxZoom ?? double.infinity; final actualZoom = math.max(min, math.min(max, newZoom)); - mapState.move( - mapState.center, + mapStateContainer.move( + mapStateContainer.center, actualZoom, hasGesture: true, source: MapEventSource.doubleTapHold, @@ -768,11 +766,11 @@ abstract class MapGestureMixin extends State if (status == AnimationStatus.completed) { _flingAnimationStarted = false; _stopListeningForAnimationInterruptions(); - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventFlingAnimationEnd( - center: mapState.center, - zoom: mapState.zoom, - source: MapEventSource.flingAnimationController), + mapState: mapState, + source: MapEventSource.flingAnimationController, + ), ); } } @@ -780,11 +778,11 @@ abstract class MapGestureMixin extends State void _handleFlingAnimation() { if (!_flingAnimationStarted) { _flingAnimationStarted = true; - mapState.emitMapEvent( + mapStateContainer.emitMapEvent( MapEventFlingAnimationStart( - center: mapState.center, - zoom: mapState.zoom, - source: MapEventSource.flingAnimationController), + mapState: mapState, + source: MapEventSource.flingAnimationController, + ), ); _startListeningForAnimationInterruptions(); } @@ -793,9 +791,9 @@ abstract class MapGestureMixin extends State _offsetToPoint(_flingAnimation.value).rotate(mapState.rotationRad); final newCenter = mapState.unproject(newCenterPoint); - mapState.move( + mapStateContainer.move( newCenter, - mapState.zoom, + mapStateContainer.zoom, hasGesture: true, source: MapEventSource.flingAnimationController, ); @@ -825,7 +823,7 @@ abstract class MapGestureMixin extends State double _getZoomForScale(double startZoom, double scale) { final resultZoom = scale == 1.0 ? startZoom : startZoom + math.log(scale) / math.ln2; - return mapState.fitZoomToBounds(resultZoom); + return mapStateContainer.fitZoomToBounds(resultZoom); } Offset _rotateOffset(Offset offset) { diff --git a/lib/src/gestures/map_events.dart b/lib/src/gestures/map_events.dart index 4499b2cc1..52e7fdb61 100644 --- a/lib/src/gestures/map_events.dart +++ b/lib/src/gestures/map_events.dart @@ -1,4 +1,4 @@ -import 'package:flutter_map/src/misc/point.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:latlong2/latlong.dart'; /// Event sources which are used to identify different types of @@ -32,68 +32,52 @@ abstract class MapEvent { /// Who / what issued the event. final MapEventSource source; - /// Geographical coordinates related to current event. - final LatLng center; - - /// Zoom value related to current event. - final double zoom; + /// The state of the map after the event. + final FlutterMapState mapState; const MapEvent({ required this.source, - required this.center, - required this.zoom, + required this.mapState, }); } /// Base event class which is emitted by MapController instance and /// includes information about camera movement +/// TODO: Change name to reflect that this is a base class for map events +/// which are not partial (e.g start rotate, rotate, end rotate). abstract class MapEventWithMove extends MapEvent { - /// Target coordinates of point where map is being pointed to - final LatLng targetCenter; - - /// Zoom value of point where map is being pointed to - final double targetZoom; + final FlutterMapState oldMapState; const MapEventWithMove({ - required this.targetCenter, - required this.targetZoom, required super.source, - required super.center, - required super.zoom, + required this.oldMapState, + required super.mapState, }); /// Returns a subclass of [MapEventWithMove] if the [source] belongs to a /// movement event, otherwise returns null. static MapEventWithMove? fromSource({ - required LatLng targetCenter, - required double targetZoom, - required LatLng oldCenter, - required double oldZoom, + required FlutterMapState oldMapState, + required FlutterMapState mapState, required bool hasGesture, required MapEventSource source, String? id, }) => switch (source) { MapEventSource.flingAnimationController => MapEventFlingAnimation( - center: oldCenter, - zoom: oldZoom, - targetCenter: targetCenter, - targetZoom: targetZoom, + oldMapState: oldMapState, + mapState: mapState, source: source, ), MapEventSource.doubleTapZoomAnimationController => MapEventDoubleTapZoom( - center: oldCenter, - zoom: oldZoom, - targetCenter: targetCenter, - targetZoom: targetZoom, + oldMapState: oldMapState, + mapState: mapState, source: source, ), MapEventSource.scrollWheel => MapEventScrollWheelZoom( - center: oldCenter, - zoom: oldZoom, - targetCenter: targetCenter, - targetZoom: targetZoom, + oldMapState: oldMapState, + mapState: mapState, source: source, ), MapEventSource.onDrag || @@ -102,10 +86,8 @@ abstract class MapEventWithMove extends MapEvent { MapEventSource.custom => MapEventMove( id: id, - center: oldCenter, - zoom: oldZoom, - targetCenter: targetCenter, - targetZoom: targetZoom, + oldMapState: oldMapState, + mapState: mapState, source: source, ), _ => null, @@ -120,8 +102,7 @@ class MapEventTap extends MapEvent { const MapEventTap({ required this.tapPosition, required super.source, - required super.center, - required super.zoom, + required super.mapState, }); } @@ -132,8 +113,7 @@ class MapEventSecondaryTap extends MapEvent { const MapEventSecondaryTap({ required this.tapPosition, required super.source, - required super.center, - required super.zoom, + required super.mapState, }); } @@ -145,8 +125,7 @@ class MapEventLongPress extends MapEvent { const MapEventLongPress({ required this.tapPosition, required super.source, - required super.center, - required super.zoom, + required super.mapState, }); } @@ -157,23 +136,17 @@ class MapEventMove extends MapEventWithMove { const MapEventMove({ this.id, - required LatLng targetCenter, - required double targetZoom, required super.source, - required super.center, - required super.zoom, - }) : super( - targetCenter: targetCenter, - targetZoom: targetZoom, - ); + required super.oldMapState, + required super.mapState, + }); } /// Event which is fired when dragging is started class MapEventMoveStart extends MapEvent { const MapEventMoveStart({ required super.source, - required super.center, - required super.zoom, + required super.mapState, }); } @@ -181,23 +154,17 @@ class MapEventMoveStart extends MapEvent { class MapEventMoveEnd extends MapEvent { const MapEventMoveEnd({ required super.source, - required super.center, - required super.zoom, + required super.mapState, }); } /// Event which is fired when animation started by fling gesture is in progress class MapEventFlingAnimation extends MapEventWithMove { const MapEventFlingAnimation({ - required LatLng targetCenter, - required double targetZoom, required super.source, - required super.center, - required super.zoom, - }) : super( - targetCenter: targetCenter, - targetZoom: targetZoom, - ); + required super.oldMapState, + required super.mapState, + }); } /// Emits when InteractiveFlags contains fling and there wasn't enough velocity @@ -205,8 +172,7 @@ class MapEventFlingAnimation extends MapEventWithMove { class MapEventFlingAnimationNotStarted extends MapEvent { const MapEventFlingAnimationNotStarted({ required super.source, - required super.center, - required super.zoom, + required super.mapState, }); } @@ -214,8 +180,7 @@ class MapEventFlingAnimationNotStarted extends MapEvent { class MapEventFlingAnimationStart extends MapEvent { const MapEventFlingAnimationStart({ required super.source, - required super.center, - required super.zoom, + required super.mapState, }); } @@ -223,45 +188,33 @@ class MapEventFlingAnimationStart extends MapEvent { class MapEventFlingAnimationEnd extends MapEvent { const MapEventFlingAnimationEnd({ required super.source, - required super.center, - required super.zoom, + required super.mapState, }); } /// Event which is fired when map is double tapped class MapEventDoubleTapZoom extends MapEventWithMove { const MapEventDoubleTapZoom({ - required LatLng targetCenter, - required double targetZoom, required super.source, - required super.center, - required super.zoom, - }) : super( - targetCenter: targetCenter, - targetZoom: targetZoom, - ); + required super.oldMapState, + required super.mapState, + }); } /// Event which is fired when scroll wheel is used to zoom class MapEventScrollWheelZoom extends MapEventWithMove { const MapEventScrollWheelZoom({ - required LatLng targetCenter, - required double targetZoom, required super.source, - required super.center, - required super.zoom, - }) : super( - targetCenter: targetCenter, - targetZoom: targetZoom, - ); + required super.oldMapState, + required super.mapState, + }); } /// Event which is fired when animation for double tap gesture is started class MapEventDoubleTapZoomStart extends MapEvent { const MapEventDoubleTapZoomStart({ required super.source, - required super.center, - required super.zoom, + required super.mapState, }); } @@ -269,29 +222,20 @@ class MapEventDoubleTapZoomStart extends MapEvent { class MapEventDoubleTapZoomEnd extends MapEvent { const MapEventDoubleTapZoomEnd({ required super.source, - required super.center, - required super.zoom, + required super.mapState, }); } /// Event which is fired when map is being rotated -class MapEventRotate extends MapEvent { +class MapEventRotate extends MapEventWithMove { /// Custom ID to identify related object(s) final String? id; - /// Current rotation in radians - final double currentRotation; - - /// Target rotation in radians - final double targetRotation; - const MapEventRotate({ required this.id, - required this.currentRotation, - required this.targetRotation, required super.source, - required super.center, - required super.zoom, + required super.oldMapState, + required super.mapState, }); } @@ -299,25 +243,21 @@ class MapEventRotate extends MapEvent { class MapEventRotateStart extends MapEvent { const MapEventRotateStart({ required super.source, - required super.center, - required super.zoom, + required super.mapState, }); } class MapEventRotateEnd extends MapEvent { const MapEventRotateEnd({ required super.source, - required super.center, - required super.zoom, + required super.mapState, }); } -class MapEventNonRotatedSizeChange extends MapEvent { +class MapEventNonRotatedSizeChange extends MapEventWithMove { const MapEventNonRotatedSizeChange({ required super.source, - required CustomPoint previousNonRotatedSize, - required CustomPoint nonRotatedSize, - required super.center, - required super.zoom, + required super.oldMapState, + required super.mapState, }); } diff --git a/lib/src/layer/attribution_layer/rich.dart b/lib/src/layer/attribution_layer/rich.dart index 4856be294..6574c34b0 100644 --- a/lib/src/layer/attribution_layer/rich.dart +++ b/lib/src/layer/attribution_layer/rich.dart @@ -239,10 +239,8 @@ class RichAttributionWidgetState extends State { context, () { setState(() => popupExpanded = true); - mapEventSubscription = FlutterMapState.of(context) - .mapController - .mapEventStream - .listen((e) { + mapEventSubscription = + MapController.of(context).mapEventStream.listen((e) { setState(() => popupExpanded = false); mapEventSubscription?.cancel(); }); diff --git a/lib/src/layer/circle_layer.dart b/lib/src/layer/circle_layer.dart index 0bb4f2e38..a8b736c18 100644 --- a/lib/src/layer/circle_layer.dart +++ b/lib/src/layer/circle_layer.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/map/state.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:latlong2/latlong.dart' hide Path; class CircleMarker { diff --git a/lib/src/layer/marker_layer.dart b/lib/src/layer/marker_layer.dart index b94afdd89..00c273a77 100644 --- a/lib/src/layer/marker_layer.dart +++ b/lib/src/layer/marker_layer.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/state.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/src/layer/overlay_image_layer.dart b/lib/src/layer/overlay_image_layer.dart index 148a77ed4..d8e8dff21 100644 --- a/lib/src/layer/overlay_image_layer.dart +++ b/lib/src/layer/overlay_image_layer.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/state.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/src/layer/polygon_layer.dart b/lib/src/layer/polygon_layer.dart index 1fca6d92b..cf20a8217 100644 --- a/lib/src/layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer.dart @@ -3,7 +3,7 @@ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/layer/label.dart'; -import 'package:flutter_map/src/map/state.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:latlong2/latlong.dart' hide Path; // conflict with Path from UI enum PolygonLabelPlacement { diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index 6d8702b5c..e707379e7 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -3,7 +3,7 @@ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/state.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:latlong2/latlong.dart'; class Polyline { diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index c34c17a48..12a3fa4da 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -4,30 +4,15 @@ import 'dart:math' as math; import 'package:collection/collection.dart' show MapEquality; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/misc/private/bounds.dart'; -import 'package:flutter_map/src/misc/point.dart'; -import 'package:flutter_map/src/misc/private/util.dart' as util; -import 'package:flutter_map/src/geo/crs.dart'; -import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/gestures/map_events.dart'; +import 'package:flutter_map/plugin_api.dart'; import 'package:flutter_map/src/layer/tile_layer/tile.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_bounds/tile_bounds.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_bounds/tile_bounds_at_zoom.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_builder.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_display.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_image.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_image_manager.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_provider/asset_tile_provider.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_provider/base_tile_provider.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_provider/file_providers/tile_provider_stub.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_provider/network_tile_provider.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_range.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_range_calculator.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_scale_calculator.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_update_event.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_update_transformer.dart'; -import 'package:flutter_map/src/map/state.dart'; +import 'package:flutter_map/src/misc/private/util.dart' as util; import 'package:http/retry.dart'; part 'tile_layer_options.dart'; @@ -346,7 +331,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { final mapState = FlutterMapState.maybeOf(context)!; - final mapController = mapState.mapController; + final mapController = MapController.of(context); if (_mapControllerHashCode != mapController.hashCode) { _tileUpdateSubscription?.cancel(); @@ -354,7 +339,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { _tileUpdateSubscription = mapController.mapEventStream .map((mapEvent) => TileUpdateEvent(mapEvent: mapEvent)) .transform(widget.tileUpdateTransformer) - .listen((event) => _onTileUpdateEvent(mapState, event)); + .listen((event) => _onTileUpdateEvent(event)); } bool reloadTiles = false; @@ -535,15 +520,14 @@ class _TileLayerState extends State with TickerProviderStateMixin { // Load and/or prune tiles according to the visible bounds of the [event] // center/zoom, or the current center/zoom if not specified. - void _onTileUpdateEvent(FlutterMapState mapState, TileUpdateEvent event) { - final zoom = event.loadZoomOverride ?? mapState.zoom; - final center = event.loadCenterOverride ?? mapState.center; - final tileZoom = _clampToNativeZoom(zoom); + void _onTileUpdateEvent(TileUpdateEvent event) { + debugPrint('Tile update event'); + final tileZoom = _clampToNativeZoom(event.zoom); final visibleTileRange = _tileRangeCalculator.calculate( - mapState: mapState, + mapState: event.mapState, tileZoom: tileZoom, - center: center, - viewingZoom: zoom, + center: event.center, + viewingZoom: event.zoom, ); if (event.load) { diff --git a/lib/src/layer/tile_layer/tile_range_calculator.dart b/lib/src/layer/tile_layer/tile_range_calculator.dart index b823ef112..1b6b79a08 100644 --- a/lib/src/layer/tile_layer/tile_range_calculator.dart +++ b/lib/src/layer/tile_layer/tile_range_calculator.dart @@ -1,6 +1,6 @@ -import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_range.dart'; -import 'package:flutter_map/src/map/state.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; class TileRangeCalculator { diff --git a/lib/src/layer/tile_layer/tile_update_event.dart b/lib/src/layer/tile_layer/tile_update_event.dart index a8aacb3df..01425dbed 100644 --- a/lib/src/layer/tile_layer/tile_update_event.dart +++ b/lib/src/layer/tile_layer/tile_update_event.dart @@ -1,4 +1,5 @@ import 'package:flutter_map/src/gestures/map_events.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:latlong2/latlong.dart'; /// Describes whether loading and/or pruning should occur and allows overriding @@ -18,6 +19,12 @@ class TileUpdateEvent { this.loadZoomOverride, }); + double get zoom => loadZoomOverride ?? mapEvent.mapState.zoom; + + LatLng get center => loadCenterOverride ?? mapEvent.mapState.center; + + FlutterMapState get mapState => mapEvent.mapState; + /// Returns a copy of this TileUpdateEvent with only pruning enabled and the /// loadCenterOverride/loadZoomOverride removed. TileUpdateEvent pruneOnly() => TileUpdateEvent( diff --git a/lib/src/map/controller.dart b/lib/src/map/controller.dart index 9a481b4ed..0031278a0 100644 --- a/lib/src/map/controller.dart +++ b/lib/src/map/controller.dart @@ -2,7 +2,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/state.dart'; +import 'package:flutter_map/src/map/flutter_map_state_container.dart'; +import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; @@ -22,6 +23,15 @@ abstract class MapController { /// Factory constructor redirects to underlying implementation's constructor. factory MapController() = MapControllerImpl._; + static MapController? maybeOf(BuildContext context) => context + .dependOnInheritedWidgetOfExactType() + ?.mapController; + + static MapController of(BuildContext context) => + maybeOf(context) ?? + (throw StateError( + '`MapController.of()` should not be called outside a `FlutterMap` and its children')); + /// Moves and zooms the map to a [center] and [zoom] level /// /// [offset] allows a screen-based offset (in normal logical pixels) to be @@ -163,12 +173,12 @@ abstract class MapController { /// Immediately change the internal map state /// /// Not recommended for external usage. - set state(FlutterMapState state); + set state(FlutterMapStateContainer state); /// Dispose of this controller by closing the [mapEventStream]'s /// [StreamController] /// - /// Not recommended for external usage. + /// Not recommended for external usage. // TODO Why? void dispose(); } @@ -281,11 +291,11 @@ class MapControllerImpl implements MapController { @override StreamSink get mapEventSink => _mapEventStreamController.sink; - late FlutterMapState _state; + late FlutterMapStateContainer _state; @override - set state(FlutterMapState state) { - _state = state; + set state(FlutterMapStateContainer stateContainer) { + _state = stateContainer; } @override diff --git a/lib/src/map/flutter_map_state.dart b/lib/src/map/flutter_map_state.dart new file mode 100644 index 000000000..4d0b18b18 --- /dev/null +++ b/lib/src/map/flutter_map_state.dart @@ -0,0 +1,400 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter_map/src/geo/crs.dart'; +import 'package:flutter_map/src/geo/latlng_bounds.dart'; +import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; +import 'package:flutter_map/src/map/options.dart'; +import 'package:flutter_map/src/misc/center_zoom.dart'; +import 'package:flutter_map/src/misc/fit_bounds_options.dart'; +import 'package:flutter_map/src/misc/point.dart'; +import 'package:flutter_map/src/misc/private/bounds.dart'; +import 'package:latlong2/latlong.dart'; + +class FlutterMapState { + final MapOptions options; + + final LatLng center; + final double zoom; + final double rotation; + + // Original size of the map where rotation isn't calculated + final CustomPoint nonrotatedSize; + + // Extended size of the map where rotation is calculated + final CustomPoint size; + + late final CustomPoint pixelOrigin; + late final LatLngBounds bounds; + late final Bounds pixelBounds; + + final bool hasFitInitialBounds; + + FlutterMapState({ + required this.options, + required this.center, + required this.zoom, + required this.rotation, + required this.nonrotatedSize, + required this.size, + required this.hasFitInitialBounds, + }) { + pixelBounds = _getPixelBoundsStatic(options.crs, size, center, zoom); + bounds = LatLngBounds( + unproject(pixelBounds.bottomLeft), + unproject(pixelBounds.topRight), + ); + final halfSize = size / 2.0; + pixelOrigin = (project(center, zoom) - halfSize).round(); + } + + FlutterMapState copyWith({ + LatLng? center, + double? zoom, + }) => + FlutterMapState( + options: options, + center: center ?? this.center, + zoom: zoom ?? this.zoom, + rotation: rotation, + nonrotatedSize: nonrotatedSize, + size: size, + hasFitInitialBounds: hasFitInitialBounds, + ); + + FlutterMapState withNonotatedSize(CustomPoint nonrotatedSize) { + if (nonrotatedSize == this.nonrotatedSize) return this; + + return FlutterMapState( + options: options, + center: center, + zoom: zoom, + rotation: rotation, + nonrotatedSize: nonrotatedSize, + size: _calculateSize(rotation, nonrotatedSize), + hasFitInitialBounds: hasFitInitialBounds, + ); + } + + FlutterMapState withRotation(double rotation) { + if (rotation == this.rotation) return this; + + return FlutterMapState( + options: options, + center: center, + zoom: zoom, + rotation: rotation, + nonrotatedSize: nonrotatedSize, + size: _calculateSize(rotation, nonrotatedSize), + hasFitInitialBounds: hasFitInitialBounds, + ); + } + + static CustomPoint _calculateSize( + double rotation, + CustomPoint nonrotatedSize, + ) { + if (rotation == 0.0) return nonrotatedSize; + + final rotationRad = degToRadian(rotation); + final cosAngle = math.cos(rotationRad).abs(); + final sinAngle = math.sin(rotationRad).abs(); + final width = (nonrotatedSize.x * cosAngle) + (nonrotatedSize.y * sinAngle); + final height = + (nonrotatedSize.y * cosAngle) + (nonrotatedSize.x * sinAngle); + + return CustomPoint(width, height); + } + + double get rotationRad => degToRadian(rotation); + + CenterZoom centerZoomFitBounds( + LatLngBounds bounds, + FitBoundsOptions options, + ) => + getBoundsCenterZoom(bounds, options); + + double getBoundsZoom( + LatLngBounds bounds, + CustomPoint padding, { + bool inside = false, + bool forceIntegerZoomLevel = false, + }) { + final min = options.minZoom ?? 0.0; + final max = options.maxZoom ?? double.infinity; + final nw = bounds.northWest; + final se = bounds.southEast; + var size = nonrotatedSize - padding; + // Prevent negative size which results in NaN zoom value later on in the calculation + size = CustomPoint(math.max(0, size.x), math.max(0, size.y)); + var boundsSize = Bounds(project(se, zoom), project(nw, zoom)).size; + if (rotation != 0.0) { + final cosAngle = math.cos(rotationRad).abs(); + final sinAngle = math.sin(rotationRad).abs(); + boundsSize = CustomPoint( + (boundsSize.x * cosAngle) + (boundsSize.y * sinAngle), + (boundsSize.y * cosAngle) + (boundsSize.x * sinAngle), + ); + } + + final scaleX = size.x / boundsSize.x; + final scaleY = size.y / boundsSize.y; + final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); + + var boundsZoom = getScaleZoom(scale, zoom); + + if (forceIntegerZoomLevel) { + boundsZoom = + inside ? boundsZoom.ceilToDouble() : boundsZoom.floorToDouble(); + } + + return math.max(min, math.min(max, boundsZoom)); + } + + CenterZoom getBoundsCenterZoom( + LatLngBounds bounds, FitBoundsOptions options) { + final paddingTL = + CustomPoint(options.padding.left, options.padding.top); + final paddingBR = + CustomPoint(options.padding.right, options.padding.bottom); + + final paddingTotalXY = paddingTL + paddingBR; + + var zoom = getBoundsZoom( + bounds, + paddingTotalXY, + inside: options.inside, + forceIntegerZoomLevel: options.forceIntegerZoomLevel, + ); + zoom = math.min(options.maxZoom, zoom); + + final paddingOffset = (paddingBR - paddingTL) / 2; + final swPoint = project(bounds.southWest, zoom); + final nePoint = project(bounds.northEast, zoom); + + final CustomPoint projectedCenter; + if (rotation != 0.0) { + final swPointRotated = swPoint.rotate(-rotationRad); + final nePointRotated = nePoint.rotate(-rotationRad); + final centerRotated = + (swPointRotated + nePointRotated) / 2 + paddingOffset; + + projectedCenter = centerRotated.rotate(rotationRad); + } else { + projectedCenter = (swPoint + nePoint) / 2 + paddingOffset; + } + + final center = unproject(projectedCenter, zoom); + + return CenterZoom( + center: center, + zoom: zoom, + ); + } + + CustomPoint project(LatLng latlng, [double? zoom]) => + options.crs.latLngToPoint(latlng, zoom ?? this.zoom); + + LatLng unproject(CustomPoint point, [double? zoom]) => + options.crs.pointToLatLng(point, zoom ?? this.zoom); + + LatLng layerPointToLatLng(CustomPoint point) => unproject(point); + + double getZoomScale(double toZoom, double fromZoom) => + options.crs.scale(toZoom) / options.crs.scale(fromZoom); + + double getScaleZoom(double scale, double? fromZoom) { + final crs = options.crs; + fromZoom = fromZoom ?? zoom; + return crs.zoom(scale * crs.scale(fromZoom)); + } + + Bounds? getPixelWorldBounds(double? zoom) { + return options.crs.getProjectedBounds(zoom ?? this.zoom); + } + + Offset getOffsetFromOrigin(LatLng pos) { + final delta = project(pos) - pixelOrigin; + return Offset(delta.x, delta.y); + } + + CustomPoint getNewPixelOrigin(LatLng center, [double? zoom]) { + final halfSize = size / 2.0; + return (project(center, zoom) - halfSize).round(); + } + + Bounds getPixelBounds([double? zoom]) { + CustomPoint halfSize = size / 2; + if (zoom != null) { + final scale = getZoomScale(this.zoom, zoom); + halfSize = size / (scale * 2); + } + final pixelCenter = project(center, zoom).floor().toDoublePoint(); + return Bounds(pixelCenter - halfSize, pixelCenter + halfSize); + } + + static Bounds _getPixelBoundsStatic( + Crs crs, + CustomPoint size, + LatLng center, + double zoom, + ) { + final halfSize = size / 2; + final pixelCenter = crs.latLngToPoint(center, zoom).floor().toDoublePoint(); + return Bounds(pixelCenter - halfSize, pixelCenter + halfSize); + } + + // This will convert a latLng to a position that we could use with a widget + // outside of FlutterMap layer space. Eg using a Positioned Widget. + CustomPoint latLngToScreenPoint(LatLng latLng) { + final nonRotatedPixelOrigin = + (project(center, zoom) - nonrotatedSize / 2.0).round(); + + var point = options.crs.latLngToPoint(latLng, zoom); + + final mapCenter = options.crs.latLngToPoint(center, zoom); + + if (rotation != 0.0) { + point = rotatePoint(mapCenter, point, counterRotation: false); + } + + return point - nonRotatedPixelOrigin; + } + + LatLng pointToLatLng(CustomPoint localPoint) { + final localPointCenterDistance = CustomPoint( + (nonrotatedSize.x / 2) - localPoint.x, + (nonrotatedSize.y / 2) - localPoint.y, + ); + final mapCenter = options.crs.latLngToPoint(center, zoom); + + var point = mapCenter - localPointCenterDistance; + + if (rotation != 0.0) { + point = rotatePoint(mapCenter, point); + } + + return options.crs.pointToLatLng(point, zoom); + } + + // Sometimes we need to make allowances that a rotation already exists, so + // it needs to be reversed (pointToLatLng), and sometimes we want to use + // the same rotation to create a new position (latLngToScreenpoint). + // counterRotation just makes allowances this for this. + CustomPoint rotatePoint( + CustomPoint mapCenter, + CustomPoint point, { + bool counterRotation = true, + }) { + final counterRotationFactor = counterRotation ? -1 : 1; + + final m = Matrix4.identity() + ..translate(mapCenter.x, mapCenter.y) + ..rotateZ(rotationRad * counterRotationFactor) + ..translate(-mapCenter.x, -mapCenter.y); + + final tp = MatrixUtils.transformPoint(m, Offset(point.x, point.y)); + + return CustomPoint(tp.dx, tp.dy); + } + + _SafeArea? _safeAreaCache; + double? _safeAreaZoom; + + //if there is a pan boundary, do not cross + bool isOutOfBounds(LatLng center) { + if (options.adaptiveBoundaries) { + return !_safeArea!.contains(center); + } + if (options.swPanBoundary != null && options.nePanBoundary != null) { + if (center.latitude < options.swPanBoundary!.latitude || + center.latitude > options.nePanBoundary!.latitude) { + return true; + } else if (center.longitude < options.swPanBoundary!.longitude || + center.longitude > options.nePanBoundary!.longitude) { + return true; + } + } + return false; + } + + LatLng containPoint(LatLng point, LatLng fallback) { + if (options.adaptiveBoundaries) { + return _safeArea!.containPoint(point, fallback); + } else { + return LatLng( + point.latitude.clamp( + options.swPanBoundary!.latitude, options.nePanBoundary!.latitude), + point.longitude.clamp( + options.swPanBoundary!.longitude, options.nePanBoundary!.longitude), + ); + } + } + + _SafeArea? get _safeArea { + if (zoom != _safeAreaZoom || _safeAreaCache == null) { + _safeAreaZoom = zoom; + final halfScreenHeight = calculateScreenHeightInDegrees() / 2; + final halfScreenWidth = calculateScreenWidthInDegrees() / 2; + final southWestLatitude = + options.swPanBoundary!.latitude + halfScreenHeight; + final southWestLongitude = + options.swPanBoundary!.longitude + halfScreenWidth; + final northEastLatitude = + options.nePanBoundary!.latitude - halfScreenHeight; + final northEastLongitude = + options.nePanBoundary!.longitude - halfScreenWidth; + _safeAreaCache = _SafeArea( + LatLng( + southWestLatitude, + southWestLongitude, + ), + LatLng( + northEastLatitude, + northEastLongitude, + ), + ); + } + return _safeAreaCache; + } + + double calculateScreenWidthInDegrees() { + final degreesPerPixel = 360 / math.pow(2, zoom + 8); + return options.screenSize!.width * degreesPerPixel; + } + + double calculateScreenHeightInDegrees() => + options.screenSize!.height * 170.102258 / math.pow(2, zoom + 8); + + static FlutterMapState? maybeOf(BuildContext context) => context + .dependOnInheritedWidgetOfExactType() + ?.mapState; + + static FlutterMapState of(BuildContext context) => + maybeOf(context) ?? + (throw StateError( + '`FlutterMapState.of()` should not be called outside a `FlutterMap` and its children')); +} + +class _SafeArea { + final LatLngBounds bounds; + final bool isLatitudeBlocked; + final bool isLongitudeBlocked; + + _SafeArea(LatLng southWest, LatLng northEast) + : bounds = LatLngBounds(southWest, northEast), + isLatitudeBlocked = southWest.latitude > northEast.latitude, + isLongitudeBlocked = southWest.longitude > northEast.longitude; + + bool contains(LatLng point) => + isLatitudeBlocked || isLongitudeBlocked ? false : bounds.contains(point); + + LatLng containPoint(LatLng point, LatLng fallback) => LatLng( + isLatitudeBlocked + ? fallback.latitude + : point.latitude.clamp(bounds.south, bounds.north), + isLongitudeBlocked + ? fallback.longitude + : point.longitude.clamp(bounds.west, bounds.east), + ); +} diff --git a/lib/src/map/flutter_map_state_container.dart b/lib/src/map/flutter_map_state_container.dart new file mode 100644 index 000000000..d66363faa --- /dev/null +++ b/lib/src/map/flutter_map_state_container.dart @@ -0,0 +1,563 @@ +import 'dart:math' as math; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/gestures/gestures.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; +import 'package:flutter_map/src/misc/private/bounds.dart'; +import 'package:latlong2/latlong.dart'; + +class FlutterMapStateContainer extends MapGestureMixin { + static const invalidSize = CustomPoint(-1, -1); + + final _positionedTapController = PositionedTapController(); + final _gestureArenaTeam = GestureArenaTeam(); + + bool _hasFitInitialBounds = false; + + @override + FlutterMapState get mapState => _mapState; + +// TODO Should override methods like move() instead. + @override + FlutterMapStateContainer get mapStateContainer => this; + + final _localController = MapController(); + @override + MapController get mapController => widget.mapController ?? _localController; + + @override + MapOptions get options => widget.options; + + late FlutterMapState _mapState; + + LatLng get center => _mapState.center; + + LatLngBounds get bounds => _mapState.bounds; + + double get zoom => _mapState.zoom; + + double get rotation => _mapState.rotation; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance + .addPostFrameCallback((_) => options.onMapReady?.call()); + + _mapState = FlutterMapState( + options: options, + center: options.center, + zoom: options.zoom, + rotation: options.rotation, + nonrotatedSize: invalidSize, + size: invalidSize, + hasFitInitialBounds: _hasFitInitialBounds, + ); + } + + @override + void didUpdateWidget(FlutterMap oldWidget) { + super.didUpdateWidget(oldWidget); + // TODO update the map state appropriately. + } + + @override + Widget build(BuildContext context) { + final DeviceGestureSettings gestureSettings = + MediaQuery.gestureSettingsOf(context); + final Map gestures = + {}; + + gestures[TapGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(debugOwner: this), + (TapGestureRecognizer instance) { + instance + ..onTapDown = _positionedTapController.onTapDown + ..onTapUp = handleOnTapUp + ..onTap = _positionedTapController.onTap + ..onSecondaryTap = _positionedTapController.onSecondaryTap + ..onSecondaryTapDown = _positionedTapController.onTapDown; + }, + ); + + gestures[LongPressGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => LongPressGestureRecognizer(debugOwner: this), + (LongPressGestureRecognizer instance) { + instance.onLongPress = _positionedTapController.onLongPress; + }, + ); + + if (InteractiveFlag.hasFlag( + options.interactiveFlags, InteractiveFlag.drag)) { + gestures[VerticalDragGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => VerticalDragGestureRecognizer(debugOwner: this), + (VerticalDragGestureRecognizer instance) { + instance.onUpdate = (details) { + // Absorbing vertical drags + }; + instance.gestureSettings = gestureSettings; + instance.team ??= _gestureArenaTeam; + }, + ); + gestures[HorizontalDragGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => HorizontalDragGestureRecognizer(debugOwner: this), + (HorizontalDragGestureRecognizer instance) { + instance.onUpdate = (details) { + // Absorbing horizontal drags + }; + instance.gestureSettings = gestureSettings; + instance.team ??= _gestureArenaTeam; + }, + ); + } + + gestures[ScaleGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => ScaleGestureRecognizer(debugOwner: this), + (ScaleGestureRecognizer instance) { + instance + ..onStart = handleScaleStart + ..onUpdate = handleScaleUpdate + ..onEnd = handleScaleEnd; + instance.team ??= _gestureArenaTeam; + _gestureArenaTeam.captain = instance; + }, + ); + + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + _onConstraintsChange(constraints); + + return MapStateInheritedWidget( + mapController: mapController, + mapState: _mapState, + child: Listener( + onPointerDown: onPointerDown, + onPointerUp: onPointerUp, + onPointerCancel: onPointerCancel, + onPointerHover: onPointerHover, + onPointerSignal: onPointerSignal, + child: PositionedTapDetector2( + controller: _positionedTapController, + onTap: handleTap, + onSecondaryTap: handleSecondaryTap, + onLongPress: handleLongPress, + onDoubleTap: handleDoubleTap, + doubleTapDelay: InteractiveFlag.hasFlag( + options.interactiveFlags, + InteractiveFlag.doubleTapZoom, + ) + ? null + : Duration.zero, + child: RawGestureDetector( + gestures: gestures, + child: ClipRect( + child: Stack( + children: [ + OverflowBox( + minWidth: _mapState.size.x, + maxWidth: _mapState.size.x, + minHeight: _mapState.size.y, + maxHeight: _mapState.size.y, + child: Transform.rotate( + angle: _mapState.rotationRad, + child: Stack(children: widget.children), + ), + ), + Stack(children: widget.nonRotatedChildren), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } + + // No need to call setState in here as we are already running a build and the + // resulting FlutterMapState will be passed to the inherited widget which + // will trigger a build if it is different. + void _onConstraintsChange(BoxConstraints constraints) { + // Update on layout change. + _updateAndEmitSizeIfConstraintsChanged(constraints); + + // If bounds were provided set the initial center/zoom to match those + // bounds once the parent constraints are available. + if (options.bounds != null && + !_hasFitInitialBounds && + _parentConstraintsAreSet(context, constraints)) { + final target = _mapState.getBoundsCenterZoom( + options.bounds!, + options.boundsOptions, + ); + + _mapState = _mapState.copyWith(zoom: target.zoom, center: target.center); + _hasFitInitialBounds = true; + } + } + + void _updateAndEmitSizeIfConstraintsChanged(BoxConstraints constraints) { + if (_mapState.nonrotatedSize.x != constraints.maxWidth || + _mapState.nonrotatedSize.y != constraints.maxHeight) { + final oldMapState = _mapState; + _mapState = _mapState.withNonotatedSize( + CustomPoint(constraints.maxWidth, constraints.maxHeight), + ); + + if (_mapState.nonrotatedSize != invalidSize) { + emitMapEvent( + MapEventNonRotatedSizeChange( + source: MapEventSource.nonRotatedSizeChange, + oldMapState: oldMapState, + mapState: _mapState, + ), + ); + } + } + } + + // During flutter startup the native platform resolution is not immediately + // available which can cause constraints to be zero before they are updated + // in a subsequent build to the actual constraints. This check allows us to + // differentiate zero constraints caused by missing platform resolution vs + // zero constraints which were actually provided by the parent widget. + bool _parentConstraintsAreSet( + BuildContext context, BoxConstraints constraints) => + constraints.maxWidth != 0 || MediaQuery.sizeOf(context) != Size.zero; + + void emitMapEvent(MapEvent event) { + if (event.source == MapEventSource.mapController && event is MapEventMove) { + handleAnimationInterruptions(event); + } + + widget.options.onMapEvent?.call(event); + + mapController.mapEventSink.add(event); + } + + bool rotate( + double newRotation, { + bool hasGesture = false, + required MapEventSource source, + String? id, + }) { + if (newRotation != _mapState.rotation) { + final oldMapState = _mapState; + //Apply state then emit events and callbacks + setState(() { + _mapState = _mapState.withRotation(newRotation); + }); + + emitMapEvent( + MapEventRotate( + id: id, + source: source, + oldMapState: oldMapState, + mapState: _mapState, + ), + ); + return true; + } + + return false; + } + + MoveAndRotateResult rotateAroundPoint( + double degree, { + CustomPoint? point, + Offset? offset, + bool hasGesture = false, + required MapEventSource source, + String? id, + }) { + if (point != null && offset != null) { + throw ArgumentError('Only one of `point` or `offset` may be non-null'); + } + if (point == null && offset == null) { + throw ArgumentError('One of `point` or `offset` must be non-null'); + } + + if (degree == _mapState.rotation) return MoveAndRotateResult(false, false); + + if (offset == Offset.zero) { + return MoveAndRotateResult( + true, + rotate( + degree, + hasGesture: hasGesture, + source: source, + id: id, + ), + ); + } + + final rotationDiff = degree - _mapState.rotation; + final rotationCenter = _mapState.project(_mapState.center) + + (point != null + ? (point - (_mapState.nonrotatedSize / 2.0)) + : CustomPoint(offset!.dx, offset.dy)) + .rotate(_mapState.rotationRad); + + return MoveAndRotateResult( + move( + _mapState.unproject( + rotationCenter + + (_mapState.project(_mapState.center) - rotationCenter) + .rotate(degToRadian(rotationDiff)), + ), + _mapState.zoom, + hasGesture: hasGesture, + source: source, + id: id, + ), + rotate( + _mapState.rotation + rotationDiff, + hasGesture: hasGesture, + source: source, + id: id, + ), + ); + } + + MoveAndRotateResult moveAndRotate( + LatLng newCenter, + double newZoom, + double newRotation, { + Offset offset = Offset.zero, + required MapEventSource source, + String? id, + }) => + MoveAndRotateResult( + move(newCenter, newZoom, offset: offset, id: id, source: source), + rotate(newRotation, id: id, source: source), + ); + + bool move( + LatLng newCenter, + double newZoom, { + Offset offset = Offset.zero, + bool hasGesture = false, + required MapEventSource source, + String? id, + }) { + newZoom = fitZoomToBounds(newZoom); + + // Algorithm thanks to https://github.com/tlserver/flutter_map_location_marker + if (offset != Offset.zero) { + final newPoint = options.crs.latLngToPoint(newCenter, newZoom); + newCenter = options.crs.pointToLatLng( + rotatePoint( + newPoint, + newPoint - CustomPoint(offset.dx, offset.dy), + ), + newZoom, + ); + } + + if (isOutOfBounds(newCenter)) { + if (!options.slideOnBoundaries) return false; + newCenter = containPoint(newCenter, _mapState.center); + } + + if (options.maxBounds != null) { + final adjustedCenter = adjustCenterIfOutsideMaxBounds( + newCenter, + newZoom, + options.maxBounds!, + ); + + if (adjustedCenter == null) return false; + newCenter = adjustedCenter; + } + + if (newCenter == _mapState.center && newZoom == _mapState.zoom) { + return false; + } + + final oldMapState = _mapState; + setState(() { + _mapState = _mapState.copyWith(zoom: newZoom, center: newCenter); + }); + + final movementEvent = MapEventWithMove.fromSource( + oldMapState: oldMapState, + mapState: _mapState, + hasGesture: hasGesture, + source: source, + id: id, + ); + if (movementEvent != null) emitMapEvent(movementEvent); + + options.onPositionChanged?.call( + MapPosition( + center: newCenter, + bounds: _mapState.bounds, + zoom: newZoom, + hasGesture: hasGesture, + ), + hasGesture, + ); + + return true; + } + + double fitZoomToBounds(double zoom) { + // Abide to min/max zoom + if (options.maxZoom != null) { + zoom = (zoom > options.maxZoom!) ? options.maxZoom! : zoom; + } + if (options.minZoom != null) { + zoom = (zoom < options.minZoom!) ? options.minZoom! : zoom; + } + return zoom; + } + + bool fitBounds( + LatLngBounds bounds, + FitBoundsOptions options, { + Offset offset = Offset.zero, + }) { + final target = _mapState.getBoundsCenterZoom(bounds, options); + return move( + target.center, + target.zoom, + offset: offset, + source: MapEventSource.fitBounds, + ); + } + + CenterZoom centerZoomFitBounds( + LatLngBounds bounds, + FitBoundsOptions options, + ) => + _mapState.getBoundsCenterZoom(bounds, options); + + double getBoundsZoom( + LatLngBounds bounds, + CustomPoint padding, { + bool inside = false, + bool forceIntegerZoomLevel = false, + }) => + _mapState.getBoundsZoom( + bounds, + padding, + inside: inside, + forceIntegerZoomLevel: forceIntegerZoomLevel, + ); + + double getZoomScale(double toZoom, double fromZoom) => + _mapState.getZoomScale(toZoom, fromZoom); + + double getScaleZoom(double scale, double? fromZoom) => + _mapState.getScaleZoom(scale, fromZoom); + + Bounds? getPixelWorldBounds(double? zoom) => + _mapState.getPixelWorldBounds(zoom); + + Offset getOffsetFromOrigin(LatLng pos) => _mapState.getOffsetFromOrigin(pos); + + CustomPoint getNewPixelOrigin(LatLng center, [double? zoom]) => + _mapState.getNewPixelOrigin(center, zoom); + + Bounds getPixelBounds([double? zoom]) => + _mapState.getPixelBounds(zoom); + + LatLng? adjustCenterIfOutsideMaxBounds( + LatLng testCenter, double testZoom, LatLngBounds maxBounds) { + LatLng? newCenter; + + final swPixel = _mapState.project(maxBounds.southWest, testZoom); + final nePixel = _mapState.project(maxBounds.northEast, testZoom); + + final centerPix = _mapState.project(testCenter, testZoom); + + final halfSizeX = _mapState.size.x / 2; + final halfSizeY = _mapState.size.y / 2; + + // Try and find the edge value that the center could use to stay within + // the maxBounds. This should be ok for panning. If we zoom, it is possible + // there is no solution to keep all corners within the bounds. If the edges + // are still outside the bounds, don't return anything. + final leftOkCenter = math.min(swPixel.x, nePixel.x) + halfSizeX; + final rightOkCenter = math.max(swPixel.x, nePixel.x) - halfSizeX; + final topOkCenter = math.min(swPixel.y, nePixel.y) + halfSizeY; + final botOkCenter = math.max(swPixel.y, nePixel.y) - halfSizeY; + + double? newCenterX; + double? newCenterY; + + var wasAdjusted = false; + + if (centerPix.x < leftOkCenter) { + wasAdjusted = true; + newCenterX = leftOkCenter; + } else if (centerPix.x > rightOkCenter) { + wasAdjusted = true; + newCenterX = rightOkCenter; + } + + if (centerPix.y < topOkCenter) { + wasAdjusted = true; + newCenterY = topOkCenter; + } else if (centerPix.y > botOkCenter) { + wasAdjusted = true; + newCenterY = botOkCenter; + } + + if (!wasAdjusted) { + return testCenter; + } + + final newCx = newCenterX ?? centerPix.x; + final newCy = newCenterY ?? centerPix.y; + + // Have a final check, see if the adjusted center is within maxBounds. + // If not, give up. + if (newCx < leftOkCenter || + newCx > rightOkCenter || + newCy < topOkCenter || + newCy > botOkCenter) { + return null; + } else { + newCenter = _mapState.unproject(CustomPoint(newCx, newCy), testZoom); + } + + return newCenter; + } + + // This will convert a latLng to a position that we could use with a widget + // outside of FlutterMap layer space. Eg using a Positioned Widget. + CustomPoint latLngToScreenPoint(LatLng latLng) => + _mapState.latLngToScreenPoint(latLng); + + LatLng pointToLatLng(CustomPoint localPoint) => + _mapState.pointToLatLng(localPoint); + + // Sometimes we need to make allowances that a rotation already exists, so + // it needs to be reversed (pointToLatLng), and sometimes we want to use + // the same rotation to create a new position (latLngToScreenpoint). + // counterRotation just makes allowances this for this. + CustomPoint rotatePoint( + CustomPoint mapCenter, + CustomPoint point, { + bool counterRotation = true, + }) => + _mapState.rotatePoint(mapCenter, point, counterRotation: counterRotation); + + //if there is a pan boundary, do not cross + bool isOutOfBounds(LatLng center) => _mapState.isOutOfBounds(center); + + LatLng containPoint(LatLng point, LatLng fallback) => + _mapState.containPoint(point, fallback); +} diff --git a/lib/src/map/flutter_map_state_inherited_widget.dart b/lib/src/map/flutter_map_state_inherited_widget.dart new file mode 100644 index 000000000..600f7e0e0 --- /dev/null +++ b/lib/src/map/flutter_map_state_inherited_widget.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/src/map/controller.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; + +class MapStateInheritedWidget extends InheritedWidget { + const MapStateInheritedWidget({ + super.key, + required this.mapState, + required this.mapController, + required super.child, + }); + + final FlutterMapState mapState; + final MapController mapController; + + @override + bool updateShouldNotify(MapStateInheritedWidget oldWidget) { + final decision = !identical(mapState, oldWidget.mapState) || + !identical(mapController, oldWidget.mapController); + debugPrint('Decided: $decision'); + return decision; + } +} diff --git a/lib/src/map/state.dart b/lib/src/map/state.dart deleted file mode 100644 index 57d5196db..000000000 --- a/lib/src/map/state.dart +++ /dev/null @@ -1,850 +0,0 @@ -import 'dart:math' as math; - -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/misc/private/bounds.dart'; -import 'package:flutter_map/src/gestures/gestures.dart'; -import 'package:latlong2/latlong.dart'; - -class FlutterMapState extends MapGestureMixin - with AutomaticKeepAliveClientMixin { - static const invalidSize = CustomPoint(-1, -1); - - final _positionedTapController = PositionedTapController(); - final _gestureArenaTeam = GestureArenaTeam(); - - bool _hasFitInitialBounds = false; - - @override - FlutterMapState get mapState => this; - - final _localController = MapController(); - @override - MapController get mapController => widget.mapController ?? _localController; - - @override - MapOptions get options => widget.options; - - @override - void initState() { - super.initState(); - - mapController.state = this; - _rotation = options.rotation; - _center = options.center; - _zoom = options.zoom; - _pixelBounds = getPixelBounds(); - _bounds = _calculateBounds(); - - WidgetsBinding.instance - .addPostFrameCallback((_) => options.onMapReady?.call()); - } - - @override - void didUpdateWidget(FlutterMap oldWidget) { - super.didUpdateWidget(oldWidget); - mapController.state = this; - } - - @override - Widget build(BuildContext context) { - super.build(context); - - final DeviceGestureSettings gestureSettings = - MediaQuery.gestureSettingsOf(context); - final Map gestures = - {}; - - gestures[TapGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(debugOwner: this), - (TapGestureRecognizer instance) { - instance - ..onTapDown = _positionedTapController.onTapDown - ..onTapUp = handleOnTapUp - ..onTap = _positionedTapController.onTap - ..onSecondaryTap = _positionedTapController.onSecondaryTap - ..onSecondaryTapDown = _positionedTapController.onTapDown; - }, - ); - - gestures[LongPressGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => LongPressGestureRecognizer(debugOwner: this), - (LongPressGestureRecognizer instance) { - instance.onLongPress = _positionedTapController.onLongPress; - }, - ); - - if (InteractiveFlag.hasFlag( - options.interactiveFlags, InteractiveFlag.drag)) { - gestures[VerticalDragGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => VerticalDragGestureRecognizer(debugOwner: this), - (VerticalDragGestureRecognizer instance) { - instance.onUpdate = (details) { - // Absorbing vertical drags - }; - instance.gestureSettings = gestureSettings; - instance.team ??= _gestureArenaTeam; - }, - ); - gestures[HorizontalDragGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => HorizontalDragGestureRecognizer(debugOwner: this), - (HorizontalDragGestureRecognizer instance) { - instance.onUpdate = (details) { - // Absorbing horizontal drags - }; - instance.gestureSettings = gestureSettings; - instance.team ??= _gestureArenaTeam; - }, - ); - } - - gestures[ScaleGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => ScaleGestureRecognizer(debugOwner: this), - (ScaleGestureRecognizer instance) { - instance - ..onStart = handleScaleStart - ..onUpdate = handleScaleUpdate - ..onEnd = handleScaleEnd; - instance.team ??= _gestureArenaTeam; - _gestureArenaTeam.captain = instance; - }, - ); - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - // Update on layout change. - setSize(constraints.maxWidth, constraints.maxHeight); - - // If bounds were provided set the initial center/zoom to match those - // bounds once the parent constraints are available. - if (options.bounds != null && - !_hasFitInitialBounds && - _parentConstraintsAreSet(context, constraints)) { - final target = - getBoundsCenterZoom(options.bounds!, options.boundsOptions); - _zoom = target.zoom; - _center = target.center; - _hasFitInitialBounds = true; - } - - _pixelBounds = getPixelBounds(); - _bounds = _calculateBounds(); - _pixelOrigin = getNewPixelOrigin(_center); - - return _MapStateInheritedWidget( - mapState: this, - child: Listener( - onPointerDown: onPointerDown, - onPointerUp: onPointerUp, - onPointerCancel: onPointerCancel, - onPointerHover: onPointerHover, - onPointerSignal: onPointerSignal, - child: PositionedTapDetector2( - controller: _positionedTapController, - onTap: handleTap, - onSecondaryTap: handleSecondaryTap, - onLongPress: handleLongPress, - onDoubleTap: handleDoubleTap, - doubleTapDelay: InteractiveFlag.hasFlag( - options.interactiveFlags, - InteractiveFlag.doubleTapZoom, - ) - ? null - : Duration.zero, - child: RawGestureDetector( - gestures: gestures, - child: ClipRect( - child: Stack( - children: [ - OverflowBox( - minWidth: size.x, - maxWidth: size.x, - minHeight: size.y, - maxHeight: size.y, - child: Transform.rotate( - angle: rotationRad, - child: Stack(children: widget.children), - ), - ), - Stack(children: widget.nonRotatedChildren), - ], - ), - ), - ), - ), - ), - ); - }, - ); - } - - // During flutter startup the native platform resolution is not immediately - // available which can cause constraints to be zero before they are updated - // in a subsequent build to the actual constraints. This check allows us to - // differentiate zero constraints caused by missing platform resolution vs - // zero constraints which were actually provided by the parent widget. - bool _parentConstraintsAreSet( - BuildContext context, BoxConstraints constraints) => - constraints.maxWidth != 0 || MediaQuery.sizeOf(context) != Size.zero; - - @override - bool get wantKeepAlive => options.keepAlive; - - late double _zoom; - late double _rotation; - - double get zoom => _zoom; - - double get rotation => _rotation; - - double get rotationRad => degToRadian(_rotation); - - late CustomPoint _pixelOrigin; - - CustomPoint get pixelOrigin => _pixelOrigin; - - late LatLng _center; - - LatLng get center => _center; - - late LatLngBounds _bounds; - - LatLngBounds get bounds => _bounds; - - late Bounds _pixelBounds; - - Bounds get pixelBounds => _pixelBounds; - - // Original size of the map where rotation isn't calculated - CustomPoint _nonrotatedSize = invalidSize; - - CustomPoint get nonrotatedSize => _nonrotatedSize; - - void setSize(double width, double height) { - if (_nonrotatedSize.x != width || _nonrotatedSize.y != height) { - final previousNonRotatedSize = _nonrotatedSize; - - _nonrotatedSize = CustomPoint(width, height); - _updateSizeByOriginalSizeAndRotation(); - - if (previousNonRotatedSize != invalidSize) { - emitMapEvent( - MapEventNonRotatedSizeChange( - source: MapEventSource.nonRotatedSizeChange, - previousNonRotatedSize: previousNonRotatedSize, - nonRotatedSize: _nonrotatedSize, - center: center, - zoom: zoom, - ), - ); - } - } - } - - // Extended size of the map where rotation is calculated - CustomPoint _size = invalidSize; - - CustomPoint get size => _size; - - void _updateSizeByOriginalSizeAndRotation() { - final originalWidth = _nonrotatedSize.x; - final originalHeight = _nonrotatedSize.y; - - if (_rotation != 0.0) { - final cosAngle = math.cos(rotationRad).abs(); - final sinAngle = math.sin(rotationRad).abs(); - final width = (originalWidth * cosAngle) + (originalHeight * sinAngle); - final height = (originalHeight * cosAngle) + (originalWidth * sinAngle); - - _size = CustomPoint(width, height); - } else { - _size = CustomPoint(originalWidth, originalHeight); - } - - _pixelOrigin = getNewPixelOrigin(_center); - } - - void emitMapEvent(MapEvent event) { - if (event.source == MapEventSource.mapController && event is MapEventMove) { - handleAnimationInterruptions(event); - } - - widget.options.onMapEvent?.call(event); - - mapController.mapEventSink.add(event); - } - - bool rotate( - double newRotation, { - bool hasGesture = false, - required MapEventSource source, - String? id, - }) { - if (newRotation != _rotation) { - final double oldRotation = _rotation; - //Apply state then emit events and callbacks - setState(() { - _rotation = newRotation; - }); - _updateSizeByOriginalSizeAndRotation(); - - emitMapEvent( - MapEventRotate( - id: id, - currentRotation: oldRotation, - targetRotation: _rotation, - center: _center, - zoom: _zoom, - source: source, - ), - ); - return true; - } - - return false; - } - - MoveAndRotateResult rotateAroundPoint( - double degree, { - CustomPoint? point, - Offset? offset, - bool hasGesture = false, - required MapEventSource source, - String? id, - }) { - if (point != null && offset != null) { - throw ArgumentError('Only one of `point` or `offset` may be non-null'); - } - if (point == null && offset == null) { - throw ArgumentError('One of `point` or `offset` must be non-null'); - } - - if (degree == rotation) return MoveAndRotateResult(false, false); - - if (offset == Offset.zero) { - return MoveAndRotateResult( - true, - rotate( - degree, - hasGesture: hasGesture, - source: source, - id: id, - ), - ); - } - - final rotationDiff = degree - rotation; - final rotationCenter = project(center, zoom) + - (point != null - ? (point - (nonrotatedSize / 2.0)) - : CustomPoint(offset!.dx, offset.dy)) - .rotate(rotationRad); - - return MoveAndRotateResult( - move( - unproject( - rotationCenter + - (project(center) - rotationCenter) - .rotate(degToRadian(rotationDiff)), - ), - zoom, - hasGesture: hasGesture, - source: source, - id: id, - ), - rotate( - rotation + rotationDiff, - hasGesture: hasGesture, - source: source, - id: id, - ), - ); - } - - MoveAndRotateResult moveAndRotate( - LatLng newCenter, - double newZoom, - double newRotation, { - Offset offset = Offset.zero, - required MapEventSource source, - String? id, - }) => - MoveAndRotateResult( - move(newCenter, newZoom, offset: offset, id: id, source: source), - rotate(newRotation, id: id, source: source), - ); - - bool move( - LatLng newCenter, - double newZoom, { - Offset offset = Offset.zero, - bool hasGesture = false, - required MapEventSource source, - String? id, - }) { - newZoom = fitZoomToBounds(newZoom); - - // Algorithm thanks to https://github.com/tlserver/flutter_map_location_marker - if (offset != Offset.zero) { - final newPoint = options.crs.latLngToPoint(newCenter, newZoom); - newCenter = options.crs.pointToLatLng( - rotatePoint( - newPoint, - newPoint - CustomPoint(offset.dx, offset.dy), - ), - newZoom, - ); - } - - if (isOutOfBounds(newCenter)) { - if (!options.slideOnBoundaries) return false; - newCenter = containPoint(newCenter, _center); - } - - if (options.maxBounds != null) { - final adjustedCenter = adjustCenterIfOutsideMaxBounds( - newCenter, - newZoom, - options.maxBounds!, - ); - - if (adjustedCenter == null) return false; - newCenter = adjustedCenter; - } - - if (newCenter == _center && newZoom == _zoom) return false; - - final oldCenter = _center; - final oldZoom = _zoom; - - setState(() { - _zoom = newZoom; - _center = newCenter; - }); - - _pixelBounds = getPixelBounds(); - _bounds = _calculateBounds(); - _pixelOrigin = getNewPixelOrigin(newCenter); - - final movementEvent = MapEventWithMove.fromSource( - targetCenter: newCenter, - targetZoom: newZoom, - oldCenter: oldCenter, - oldZoom: oldZoom, - hasGesture: hasGesture, - source: source, - id: id, - ); - if (movementEvent != null) emitMapEvent(movementEvent); - - options.onPositionChanged?.call( - MapPosition( - center: newCenter, - bounds: _bounds, - zoom: newZoom, - hasGesture: hasGesture, - ), - hasGesture, - ); - - return true; - } - - double fitZoomToBounds(double zoom) { - // Abide to min/max zoom - if (options.maxZoom != null) { - zoom = (zoom > options.maxZoom!) ? options.maxZoom! : zoom; - } - if (options.minZoom != null) { - zoom = (zoom < options.minZoom!) ? options.minZoom! : zoom; - } - return zoom; - } - - bool fitBounds( - LatLngBounds bounds, - FitBoundsOptions options, { - Offset offset = Offset.zero, - }) { - final target = getBoundsCenterZoom(bounds, options); - return move( - target.center, - target.zoom, - offset: offset, - source: MapEventSource.fitBounds, - ); - } - - CenterZoom centerZoomFitBounds( - LatLngBounds bounds, FitBoundsOptions options) { - return getBoundsCenterZoom(bounds, options); - } - - LatLngBounds _calculateBounds() { - return LatLngBounds( - unproject(_pixelBounds.bottomLeft), - unproject(_pixelBounds.topRight), - ); - } - - CenterZoom getBoundsCenterZoom( - LatLngBounds bounds, FitBoundsOptions options) { - final paddingTL = - CustomPoint(options.padding.left, options.padding.top); - final paddingBR = - CustomPoint(options.padding.right, options.padding.bottom); - - final paddingTotalXY = paddingTL + paddingBR; - - var zoom = getBoundsZoom( - bounds, - paddingTotalXY, - inside: options.inside, - forceIntegerZoomLevel: options.forceIntegerZoomLevel, - ); - zoom = math.min(options.maxZoom, zoom); - - final paddingOffset = (paddingBR - paddingTL) / 2; - final swPoint = project(bounds.southWest, zoom); - final nePoint = project(bounds.northEast, zoom); - - final CustomPoint projectedCenter; - if (_rotation != 0.0) { - final swPointRotated = swPoint.rotate(-rotationRad); - final nePointRotated = nePoint.rotate(-rotationRad); - final centerRotated = - (swPointRotated + nePointRotated) / 2 + paddingOffset; - - projectedCenter = centerRotated.rotate(rotationRad); - } else { - projectedCenter = (swPoint + nePoint) / 2 + paddingOffset; - } - - final center = unproject(projectedCenter, zoom); - return CenterZoom( - center: center, - zoom: zoom, - ); - } - - double getBoundsZoom(LatLngBounds bounds, CustomPoint padding, - {bool inside = false, bool forceIntegerZoomLevel = false}) { - var zoom = this.zoom; - final min = options.minZoom ?? 0.0; - final max = options.maxZoom ?? double.infinity; - final nw = bounds.northWest; - final se = bounds.southEast; - var size = nonrotatedSize - padding; - // Prevent negative size which results in NaN zoom value later on in the calculation - size = CustomPoint(math.max(0, size.x), math.max(0, size.y)); - - var boundsSize = Bounds(project(se, zoom), project(nw, zoom)).size; - if (_rotation != 0.0) { - final cosAngle = math.cos(rotationRad).abs(); - final sinAngle = math.sin(rotationRad).abs(); - boundsSize = CustomPoint( - (boundsSize.x * cosAngle) + (boundsSize.y * sinAngle), - (boundsSize.y * cosAngle) + (boundsSize.x * sinAngle), - ); - } - - final scaleX = size.x / boundsSize.x; - final scaleY = size.y / boundsSize.y; - final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); - - zoom = getScaleZoom(scale, zoom); - - if (forceIntegerZoomLevel) { - zoom = inside ? zoom.ceilToDouble() : zoom.floorToDouble(); - } - - return math.max(min, math.min(max, zoom)); - } - - CustomPoint project(LatLng latlng, [double? zoom]) { - zoom ??= _zoom; - return options.crs.latLngToPoint(latlng, zoom); - } - - LatLng unproject(CustomPoint point, [double? zoom]) { - zoom ??= _zoom; - return options.crs.pointToLatLng(point, zoom); - } - - LatLng layerPointToLatLng(CustomPoint point) { - return unproject(point); - } - - double getZoomScale(double toZoom, double fromZoom) { - final crs = options.crs; - return crs.scale(toZoom) / crs.scale(fromZoom); - } - - double getScaleZoom(double scale, double? fromZoom) { - final crs = options.crs; - fromZoom = fromZoom ?? _zoom; - return crs.zoom(scale * crs.scale(fromZoom)); - } - - Bounds? getPixelWorldBounds(double? zoom) { - return options.crs.getProjectedBounds(zoom ?? _zoom); - } - - Offset getOffsetFromOrigin(LatLng pos) { - final delta = project(pos) - _pixelOrigin; - return Offset(delta.x, delta.y); - } - - CustomPoint getNewPixelOrigin(LatLng center, [double? zoom]) { - final halfSize = size / 2.0; - return (project(center, zoom) - halfSize).round(); - } - - Bounds getPixelBounds([double? zoom]) { - CustomPoint halfSize = size / 2; - if (zoom != null) { - final scale = getZoomScale(this.zoom, zoom); - halfSize = size / (scale * 2); - } - final pixelCenter = project(center, zoom).floor().toDoublePoint(); - return Bounds(pixelCenter - halfSize, pixelCenter + halfSize); - } - - LatLng? adjustCenterIfOutsideMaxBounds( - LatLng testCenter, double testZoom, LatLngBounds maxBounds) { - LatLng? newCenter; - - final swPixel = project(maxBounds.southWest, testZoom); - final nePixel = project(maxBounds.northEast, testZoom); - - final centerPix = project(testCenter, testZoom); - - final halfSizeX = size.x / 2; - final halfSizeY = size.y / 2; - - // Try and find the edge value that the center could use to stay within - // the maxBounds. This should be ok for panning. If we zoom, it is possible - // there is no solution to keep all corners within the bounds. If the edges - // are still outside the bounds, don't return anything. - final leftOkCenter = math.min(swPixel.x, nePixel.x) + halfSizeX; - final rightOkCenter = math.max(swPixel.x, nePixel.x) - halfSizeX; - final topOkCenter = math.min(swPixel.y, nePixel.y) + halfSizeY; - final botOkCenter = math.max(swPixel.y, nePixel.y) - halfSizeY; - - double? newCenterX; - double? newCenterY; - - var wasAdjusted = false; - - if (centerPix.x < leftOkCenter) { - wasAdjusted = true; - newCenterX = leftOkCenter; - } else if (centerPix.x > rightOkCenter) { - wasAdjusted = true; - newCenterX = rightOkCenter; - } - - if (centerPix.y < topOkCenter) { - wasAdjusted = true; - newCenterY = topOkCenter; - } else if (centerPix.y > botOkCenter) { - wasAdjusted = true; - newCenterY = botOkCenter; - } - - if (!wasAdjusted) { - return testCenter; - } - - final newCx = newCenterX ?? centerPix.x; - final newCy = newCenterY ?? centerPix.y; - - // Have a final check, see if the adjusted center is within maxBounds. - // If not, give up. - if (newCx < leftOkCenter || - newCx > rightOkCenter || - newCy < topOkCenter || - newCy > botOkCenter) { - return null; - } else { - newCenter = unproject(CustomPoint(newCx, newCy), testZoom); - } - - return newCenter; - } - - // This will convert a latLng to a position that we could use with a widget - // outside of FlutterMap layer space. Eg using a Positioned Widget. - CustomPoint latLngToScreenPoint(LatLng latLng) { - final nonRotatedPixelOrigin = - (project(_center, zoom) - _nonrotatedSize / 2.0).round(); - - var point = options.crs.latLngToPoint(latLng, zoom); - - final mapCenter = options.crs.latLngToPoint(center, zoom); - - if (rotation != 0.0) { - point = rotatePoint(mapCenter, point, counterRotation: false); - } - - return point - nonRotatedPixelOrigin; - } - - LatLng pointToLatLng(CustomPoint localPoint) { - final localPointCenterDistance = CustomPoint( - (_nonrotatedSize.x / 2) - localPoint.x, - (_nonrotatedSize.y / 2) - localPoint.y, - ); - final mapCenter = options.crs.latLngToPoint(center, zoom); - - var point = mapCenter - localPointCenterDistance; - - if (rotation != 0.0) { - point = rotatePoint(mapCenter, point); - } - - return options.crs.pointToLatLng(point, zoom); - } - - // Sometimes we need to make allowances that a rotation already exists, so - // it needs to be reversed (pointToLatLng), and sometimes we want to use - // the same rotation to create a new position (latLngToScreenpoint). - // counterRotation just makes allowances this for this. - CustomPoint rotatePoint( - CustomPoint mapCenter, - CustomPoint point, { - bool counterRotation = true, - }) { - final counterRotationFactor = counterRotation ? -1 : 1; - - final m = Matrix4.identity() - ..translate(mapCenter.x, mapCenter.y) - ..rotateZ(rotationRad * counterRotationFactor) - ..translate(-mapCenter.x, -mapCenter.y); - - final tp = MatrixUtils.transformPoint(m, Offset(point.x, point.y)); - - return CustomPoint(tp.dx, tp.dy); - } - - _SafeArea? _safeAreaCache; - double? _safeAreaZoom; - - //if there is a pan boundary, do not cross - bool isOutOfBounds(LatLng center) { - if (options.adaptiveBoundaries) { - return !_safeArea!.contains(center); - } - if (options.swPanBoundary != null && options.nePanBoundary != null) { - if (center.latitude < options.swPanBoundary!.latitude || - center.latitude > options.nePanBoundary!.latitude) { - return true; - } else if (center.longitude < options.swPanBoundary!.longitude || - center.longitude > options.nePanBoundary!.longitude) { - return true; - } - } - return false; - } - - LatLng containPoint(LatLng point, LatLng fallback) { - if (options.adaptiveBoundaries) { - return _safeArea!.containPoint(point, fallback); - } else { - return LatLng( - point.latitude.clamp( - options.swPanBoundary!.latitude, options.nePanBoundary!.latitude), - point.longitude.clamp( - options.swPanBoundary!.longitude, options.nePanBoundary!.longitude), - ); - } - } - - _SafeArea? get _safeArea { - final controllerZoom = _zoom; - if (controllerZoom != _safeAreaZoom || _safeAreaCache == null) { - _safeAreaZoom = controllerZoom; - final halfScreenHeight = _calculateScreenHeightInDegrees() / 2; - final halfScreenWidth = _calculateScreenWidthInDegrees() / 2; - final southWestLatitude = - options.swPanBoundary!.latitude + halfScreenHeight; - final southWestLongitude = - options.swPanBoundary!.longitude + halfScreenWidth; - final northEastLatitude = - options.nePanBoundary!.latitude - halfScreenHeight; - final northEastLongitude = - options.nePanBoundary!.longitude - halfScreenWidth; - _safeAreaCache = _SafeArea( - LatLng( - southWestLatitude, - southWestLongitude, - ), - LatLng( - northEastLatitude, - northEastLongitude, - ), - ); - } - return _safeAreaCache; - } - - double _calculateScreenWidthInDegrees() { - final degreesPerPixel = 360 / math.pow(2, zoom + 8); - return options.screenSize!.width * degreesPerPixel; - } - - double _calculateScreenHeightInDegrees() => - options.screenSize!.height * 170.102258 / math.pow(2, zoom + 8); - - static FlutterMapState? maybeOf(BuildContext context) => context - .dependOnInheritedWidgetOfExactType<_MapStateInheritedWidget>() - ?.mapState; - - static FlutterMapState of(BuildContext context) => - maybeOf(context) ?? - (throw StateError( - '`FlutterMapState.of()` should not be called outside a `FlutterMap` and its children')); -} - -class _SafeArea { - final LatLngBounds bounds; - final bool isLatitudeBlocked; - final bool isLongitudeBlocked; - - _SafeArea(LatLng southWest, LatLng northEast) - : bounds = LatLngBounds(southWest, northEast), - isLatitudeBlocked = southWest.latitude > northEast.latitude, - isLongitudeBlocked = southWest.longitude > northEast.longitude; - - bool contains(LatLng point) => - isLatitudeBlocked || isLongitudeBlocked ? false : bounds.contains(point); - - LatLng containPoint(LatLng point, LatLng fallback) => LatLng( - isLatitudeBlocked - ? fallback.latitude - : point.latitude.clamp(bounds.south, bounds.north), - isLongitudeBlocked - ? fallback.longitude - : point.longitude.clamp(bounds.west, bounds.east), - ); -} - -class _MapStateInheritedWidget extends InheritedWidget { - const _MapStateInheritedWidget({ - required this.mapState, - required super.child, - }); - - final FlutterMapState mapState; - - /// This return value does not appear to affect anything, no matter it's value - @override - bool updateShouldNotify(_MapStateInheritedWidget oldWidget) => true; -} diff --git a/lib/src/map/widget.dart b/lib/src/map/widget.dart index 9ae646296..a90e58c4c 100644 --- a/lib/src/map/widget.dart +++ b/lib/src/map/widget.dart @@ -1,7 +1,6 @@ import 'package:flutter/widgets.dart'; - import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/state.dart'; +import 'package:flutter_map/src/map/flutter_map_state_container.dart'; /// Renders an interactive geographical map as a widget /// @@ -33,5 +32,5 @@ class FlutterMap extends StatefulWidget { final MapController? mapController; @override - State createState() => FlutterMapState(); + State createState() => FlutterMapStateContainer(); } From 41a41af09b12e3146d542c8448b93fa93bef5f2b Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Fri, 9 Jun 2023 07:40:56 +0200 Subject: [PATCH 02/46] Extract interactions to InteractionDetector --- ...estures.dart => interaction_detector.dart} | 602 +++++++++--------- lib/src/layer/tile_layer/tile_layer.dart | 1 - lib/src/map/flutter_map_state.dart | 129 +++- lib/src/map/flutter_map_state_container.dart | 578 +++++++++-------- .../flutter_map_state_inherited_widget.dart | 9 +- 5 files changed, 743 insertions(+), 576 deletions(-) rename lib/src/gestures/{gestures.dart => interaction_detector.dart} (60%) diff --git a/lib/src/gestures/gestures.dart b/lib/src/gestures/interaction_detector.dart similarity index 60% rename from lib/src/gestures/gestures.dart rename to lib/src/gestures/interaction_detector.dart index 6f718a60e..2e9921cf4 100644 --- a/lib/src/gestures/gestures.dart +++ b/lib/src/gestures/interaction_detector.dart @@ -3,83 +3,99 @@ import 'dart:math' as math; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/gestures/interactive_flag.dart'; import 'package:flutter_map/src/gestures/latlng_tween.dart'; +import 'package:flutter_map/src/gestures/map_events.dart'; +import 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:flutter_map/src/map/flutter_map_state_container.dart'; +import 'package:flutter_map/src/map/options.dart'; +import 'package:flutter_map/src/misc/point.dart'; +import 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; import 'package:latlong2/latlong.dart'; -abstract class MapGestureMixin extends State +class InteractionDetector extends StatefulWidget { + final Widget child; + final FlutterMapStateContainer mapStateContainer; + + FlutterMapState get mapState => mapStateContainer.mapState; + MapOptions get options => mapState.options; + + final void Function(PointerDownEvent event) onPointerDown; + final void Function(PointerUpEvent event) onPointerUp; + final void Function(PointerCancelEvent event) onPointerCancel; + final void Function(PointerHoverEvent event) onPointerHover; + final void Function(PointerScrollEvent event) onScroll; + final void Function(MapEventSource source, double newZoom) + onOneFingerPinchZoom; + final void Function(MapEventSource source) onRotateEnd; + final void Function(MapEventSource source) onMoveEnd; + final void Function(MapEventSource source) onFlingStart; + final void Function(MapEventSource source) onFlingEnd; + final void Function(MapEventSource source) onDoubleTapZoomEnd; + final void Function(MapEventSource source) onMoveStart; + final void Function(MapEventSource source) onRotateStart; + final void Function(MapEventSource source) onFlingNotStarted; + final void Function(MapEventSource source, LatLng position) onTap; + final void Function(MapEventSource source, LatLng position) onSecondaryTap; + final void Function(MapEventSource source, LatLng position) onLongPress; + final void Function(MapEventSource source, Offset offset) onDragUpdate; + final void Function(MapEventSource source, LatLng position, double zoom) + onPinchZoomUpdate; + final void Function(MapEventSource source, LatLng position, double zoom) + onDoubleTapZoomUpdate; + final void Function( + MapEventSource source, LatLng position, double zoom, double rotation) + onRotateUpdate; + + final void Function(MapEventSource source, LatLng position) onFlingUpdate; + final void Function(MapEventSource source) onDoubleTapZoomStart; + + const InteractionDetector({ + super.key, + required this.child, + required this.mapStateContainer, + required this.onPointerDown, + required this.onPointerUp, + required this.onPointerCancel, + required this.onPointerHover, + required this.onRotateEnd, + required this.onMoveEnd, + required this.onFlingEnd, + required this.onFlingStart, + required this.onDoubleTapZoomEnd, + required this.onMoveStart, + required this.onDragUpdate, + required this.onRotateStart, + required this.onFlingNotStarted, + required this.onPinchZoomUpdate, + required this.onTap, + required this.onRotateUpdate, + required this.onSecondaryTap, + required this.onLongPress, + required this.onDoubleTapZoomStart, + required this.onScroll, + required this.onOneFingerPinchZoom, + required this.onDoubleTapZoomUpdate, + required this.onFlingUpdate, + }); + + @override + State createState() => InteractionDetectorState(); +} + +class InteractionDetectorState extends State with TickerProviderStateMixin { static const int _kMinFlingVelocity = 800; - var _dragMode = false; - var _gestureWinner = MultiFingerGesture.none; - - var _pointerCounter = 0; + final _positionedTapController = PositionedTapController(); + final _gestureArenaTeam = GestureArenaTeam(); + bool _dragMode = false; + int _gestureWinner = MultiFingerGesture.none; + int _pointerCounter = 0; bool _isListeningForInterruptions = false; - void onPointerDown(PointerDownEvent event) { - ++_pointerCounter; - if (mapState.options.onPointerDown != null) { - final latlng = _offsetToCrs(event.localPosition); - mapState.options.onPointerDown!(event, latlng); - } - } - - void onPointerUp(PointerUpEvent event) { - --_pointerCounter; - if (mapState.options.onPointerUp != null) { - final latlng = _offsetToCrs(event.localPosition); - mapState.options.onPointerUp!(event, latlng); - } - } - - void onPointerCancel(PointerCancelEvent event) { - --_pointerCounter; - if (mapState.options.onPointerCancel != null) { - final latlng = _offsetToCrs(event.localPosition); - mapState.options.onPointerCancel!(event, latlng); - } - } - - void onPointerHover(PointerHoverEvent event) { - if (mapState.options.onPointerHover != null) { - final latlng = _offsetToCrs(event.localPosition); - mapState.options.onPointerHover!(event, latlng); - } - } - - void onPointerSignal(PointerSignalEvent pointerSignal) { - // Handle mouse scroll events if the enableScrollWheel parameter is enabled - if (pointerSignal is PointerScrollEvent && - mapState.options.enableScrollWheel && - pointerSignal.scrollDelta.dy != 0) { - // Prevent scrolling of parent/child widgets simultaneously. See - // [PointerSignalResolver] documentation for more information. - GestureBinding.instance.pointerSignalResolver.register(pointerSignal, - (pointerSignal) { - pointerSignal as PointerScrollEvent; - - final minZoom = mapState.options.minZoom ?? 0.0; - final maxZoom = mapState.options.maxZoom ?? double.infinity; - final newZoom = (mapState.zoom - - pointerSignal.scrollDelta.dy * - mapState.options.scrollWheelVelocity) - .clamp(minZoom, maxZoom); - // Calculate offset of mouse cursor from viewport center - final List newCenterZoom = _getNewEventCenterZoomPosition( - _offsetToPoint(pointerSignal.localPosition), newZoom); - - // Move to new center and zoom level - mapStateContainer.move( - newCenterZoom[0] as LatLng, newCenterZoom[1] as double, - source: MapEventSource.scrollWheel); - }); - } - } - var _rotationStarted = false; var _pinchZoomStarted = false; var _pinchMoveStarted = false; @@ -89,11 +105,9 @@ abstract class MapGestureMixin extends State // Helps to reset ScaleUpdateDetails.scale back to 1.0 when a multi finger // gesture wins late double _scaleCorrector; - late double _lastRotation; late double _lastScale; late Offset _lastFocalLocal; - late LatLng _mapCenterStart; late double _mapZoomStart; late Offset _focalStartLocal; @@ -109,16 +123,6 @@ abstract class MapGestureMixin extends State int _tapUpCounter = 0; Timer? _doubleTapHoldMaxDelay; - @override - FlutterMap get widget; - - FlutterMapStateContainer get mapStateContainer; - FlutterMapState get mapState; - - MapController get mapController; - - MapOptions get options; - @override void initState() { super.initState(); @@ -132,11 +136,11 @@ abstract class MapGestureMixin extends State } @override - void didUpdateWidget(FlutterMap oldWidget) { + void didUpdateWidget(InteractionDetector oldWidget) { super.didUpdateWidget(oldWidget); final oldFlags = oldWidget.options.interactiveFlags; - final flags = options.interactiveFlags; + final flags = widget.options.interactiveFlags; final oldGestures = _getMultiFingerGestureFlags(mapOptions: oldWidget.options); @@ -162,12 +166,7 @@ abstract class MapGestureMixin extends State _gestureWinner = MultiFingerGesture.none; } - mapStateContainer.emitMapEvent( - MapEventRotateEnd( - mapState: mapState, - source: MapEventSource.interactiveFlagsChanged, - ), - ); + widget.onRotateEnd(MapEventSource.interactiveFlagsChanged); } if (_pinchZoomStarted && @@ -201,16 +200,144 @@ abstract class MapGestureMixin extends State } if (emitMapEventMoveEnd) { - mapStateContainer.emitMapEvent( - MapEventRotateEnd( - mapState: mapState, - source: MapEventSource.interactiveFlagsChanged, - ), - ); + widget.onMoveEnd(MapEventSource.interactiveFlagsChanged); } } } + @override + void dispose() { + _flingController.dispose(); + _doubleTapController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final DeviceGestureSettings gestureSettings = + MediaQuery.gestureSettingsOf(context); + final Map gestures = + {}; + + gestures[TapGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(debugOwner: this), + (TapGestureRecognizer instance) { + instance + ..onTapDown = _positionedTapController.onTapDown + ..onTapUp = handleOnTapUp + ..onTap = _positionedTapController.onTap + ..onSecondaryTap = _positionedTapController.onSecondaryTap + ..onSecondaryTapDown = _positionedTapController.onTapDown; + }, + ); + + gestures[LongPressGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => LongPressGestureRecognizer(debugOwner: this), + (LongPressGestureRecognizer instance) { + instance.onLongPress = _positionedTapController.onLongPress; + }, + ); + + if (InteractiveFlag.hasFlag( + widget.options.interactiveFlags, InteractiveFlag.drag)) { + gestures[VerticalDragGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => VerticalDragGestureRecognizer(debugOwner: this), + (VerticalDragGestureRecognizer instance) { + instance.onUpdate = (details) { + // Absorbing vertical drags + }; + instance.gestureSettings = gestureSettings; + instance.team ??= _gestureArenaTeam; + }, + ); + gestures[HorizontalDragGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => HorizontalDragGestureRecognizer(debugOwner: this), + (HorizontalDragGestureRecognizer instance) { + instance.onUpdate = (details) { + // Absorbing horizontal drags + }; + instance.gestureSettings = gestureSettings; + instance.team ??= _gestureArenaTeam; + }, + ); + } + + gestures[ScaleGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => ScaleGestureRecognizer(debugOwner: this), + (ScaleGestureRecognizer instance) { + instance + ..onStart = handleScaleStart + ..onUpdate = handleScaleUpdate + ..onEnd = handleScaleEnd; + instance.team ??= _gestureArenaTeam; + _gestureArenaTeam.captain = instance; + }, + ); + + return Listener( + onPointerDown: onPointerDown, + onPointerUp: onPointerUp, + onPointerCancel: onPointerCancel, + onPointerHover: onPointerHover, + onPointerSignal: onPointerSignal, + child: PositionedTapDetector2( + controller: _positionedTapController, + onTap: handleTap, + onSecondaryTap: handleSecondaryTap, + onLongPress: handleLongPress, + onDoubleTap: handleDoubleTap, + doubleTapDelay: InteractiveFlag.hasFlag( + widget.options.interactiveFlags, + InteractiveFlag.doubleTapZoom, + ) + ? null + : Duration.zero, + child: RawGestureDetector( + gestures: gestures, + child: widget.child, + ), + ), + ); + } + + void onPointerDown(PointerDownEvent event) { + ++_pointerCounter; + widget.onPointerDown(event); + } + + void onPointerUp(PointerUpEvent event) { + --_pointerCounter; + widget.onPointerUp(event); + } + + void onPointerCancel(PointerCancelEvent event) { + --_pointerCounter; + widget.onPointerCancel(event); + } + + void onPointerHover(PointerHoverEvent event) { + widget.onPointerHover(event); + } + + void onPointerSignal(PointerSignalEvent pointerSignal) { + // Handle mouse scroll events if the enableScrollWheel parameter is enabled + if (pointerSignal is PointerScrollEvent && + widget.options.enableScrollWheel && + pointerSignal.scrollDelta.dy != 0) { + // Prevent scrolling of parent/child widgets simultaneously. See + // [PointerSignalResolver] documentation for more information. + GestureBinding.instance.pointerSignalResolver.register( + pointerSignal, + (pointerSignal) => widget.onScroll(pointerSignal as PointerScrollEvent), + ); + } + } + void _yieldMultiFingerGestureWinner( int gestureWinner, bool resetStartVariables) { _gestureWinner = gestureWinner; @@ -224,7 +351,7 @@ abstract class MapGestureMixin extends State int _getMultiFingerGestureFlags( {int? gestureWinner, MapOptions? mapOptions}) { gestureWinner ??= _gestureWinner; - mapOptions ??= options; + mapOptions ??= widget.options; if (mapOptions.enableMultiFingerGestureRace) { if (gestureWinner == MultiFingerGesture.pinchZoom) { @@ -248,12 +375,7 @@ abstract class MapGestureMixin extends State _stopListeningForAnimationInterruptions(); - mapStateContainer.emitMapEvent( - MapEventFlingAnimationEnd( - mapState: mapState, - source: source, - ), - ); + widget.onFlingEnd(source); } } @@ -263,12 +385,7 @@ abstract class MapGestureMixin extends State _stopListeningForAnimationInterruptions(); - mapStateContainer.emitMapEvent( - MapEventDoubleTapZoomEnd( - mapState: mapState, - source: source, - ), - ); + widget.onDoubleTapZoomEnd(source); } } @@ -283,8 +400,8 @@ abstract class MapGestureMixin extends State _gestureWinner = MultiFingerGesture.none; - _mapZoomStart = mapStateContainer.zoom; - _mapCenterStart = mapStateContainer.center; + _mapZoomStart = widget.mapState.zoom; + _mapCenterStart = widget.mapState.center; _focalStartLocal = _lastFocalLocal = details.localFocalPoint; _focalStartLatLng = _offsetToCrs(_focalStartLocal); @@ -307,7 +424,7 @@ abstract class MapGestureMixin extends State final eventSource = _dragMode ? MapEventSource.onDrag : MapEventSource.onMultiFinger; - final flags = options.interactiveFlags; + final flags = widget.options.interactiveFlags; final focalOffset = details.localFocalPoint; final currentRotation = radianToDeg(details.rotation); @@ -320,29 +437,13 @@ abstract class MapGestureMixin extends State // [didUpdateWidget] will emit MapEventMoveEnd and if drag is enabled // again then this will emit the start event again. _dragStarted = true; - mapStateContainer.emitMapEvent( - MapEventMoveStart( - mapState: mapState, - source: eventSource, - ), - ); + widget.onMoveStart(eventSource); } - final oldCenterPt = - mapState.project(mapStateContainer.center, mapStateContainer.zoom); final localDistanceOffset = _rotateOffset(_lastFocalLocal - focalOffset); - final newCenterPt = oldCenterPt + _offsetToPoint(localDistanceOffset); - final newCenter = - mapState.unproject(newCenterPt, mapStateContainer.zoom); - - mapStateContainer.move( - newCenter, - mapStateContainer.zoom, - hasGesture: true, - source: eventSource, - ); + widget.onDragUpdate(eventSource, localDistanceOffset); } } else { final hasIntPinchMove = @@ -353,27 +454,27 @@ abstract class MapGestureMixin extends State InteractiveFlag.hasFlag(flags, InteractiveFlag.rotate); if (hasIntPinchMove || hasIntPinchZoom || hasIntRotate) { - final hasGestureRace = options.enableMultiFingerGestureRace; + final hasGestureRace = widget.options.enableMultiFingerGestureRace; if (hasGestureRace && _gestureWinner == MultiFingerGesture.none) { if (hasIntPinchZoom && (_getZoomForScale(_mapZoomStart, details.scale) - _mapZoomStart) .abs() >= - options.pinchZoomThreshold) { - if (options.debugMultiFingerGestureWinner) { + widget.options.pinchZoomThreshold) { + if (widget.options.debugMultiFingerGestureWinner) { debugPrint('Multi Finger Gesture winner: Pinch Zoom'); } _yieldMultiFingerGestureWinner(MultiFingerGesture.pinchZoom, true); } else if (hasIntRotate && - currentRotation.abs() >= options.rotationThreshold) { - if (options.debugMultiFingerGestureWinner) { + currentRotation.abs() >= widget.options.rotationThreshold) { + if (widget.options.debugMultiFingerGestureWinner) { debugPrint('Multi Finger Gesture winner: Rotate'); } _yieldMultiFingerGestureWinner(MultiFingerGesture.rotate, true); } else if (hasIntPinchMove && (_focalStartLocal - focalOffset).distance >= - options.pinchMoveThreshold) { - if (options.debugMultiFingerGestureWinner) { + widget.options.pinchMoveThreshold) { + if (widget.options.debugMultiFingerGestureWinner) { debugPrint('Multi Finger Gesture winner: Pinch Move'); } _yieldMultiFingerGestureWinner(MultiFingerGesture.pinchMove, true); @@ -394,8 +495,6 @@ abstract class MapGestureMixin extends State final hasZoom = hasIntPinchZoom && hasGesturePinchZoom; final hasRotate = hasIntRotate && hasGestureRotate; - var mapMoved = false; - var mapRotated = false; if (hasMove || hasZoom) { double newZoom; // checking details.scale to prevent situation whew details comes @@ -410,17 +509,12 @@ abstract class MapGestureMixin extends State if (!_pinchMoveStarted) { // emit MoveStart event only if pinchMove hasn't started - mapStateContainer.emitMapEvent( - MapEventMoveStart( - mapState: mapState, - source: eventSource, - ), - ); + widget.onMoveStart(eventSource); } } } } else { - newZoom = mapStateContainer.zoom; + newZoom = widget.mapState.zoom; } LatLng newCenter; @@ -430,21 +524,18 @@ abstract class MapGestureMixin extends State if (!_pinchZoomStarted) { // emit MoveStart event only if pinchZoom hasn't started - mapStateContainer.emitMapEvent( - MapEventMoveStart( - mapState: mapState, - source: eventSource, - ), - ); + widget.onMoveStart(eventSource); } } if (_pinchZoomStarted || _pinchMoveStarted) { final oldCenterPt = - mapState.project(mapStateContainer.center, newZoom); + widget.mapState.project(widget.mapState.center, newZoom); final newFocalLatLong = _offsetToCrs(_focalStartLocal, newZoom); - final newFocalPt = mapState.project(newFocalLatLong, newZoom); - final oldFocalPt = mapState.project(_focalStartLatLng, newZoom); + final newFocalPt = + widget.mapState.project(newFocalLatLong, newZoom); + final oldFocalPt = + widget.mapState.project(_focalStartLatLng, newZoom); final zoomDifference = oldFocalPt - newFocalPt; final moveDifference = _rotateOffset(_focalStartLocal - _lastFocalLocal); @@ -452,56 +543,43 @@ abstract class MapGestureMixin extends State final newCenterPt = oldCenterPt + zoomDifference + _offsetToPoint(moveDifference); - newCenter = mapState.unproject(newCenterPt, newZoom); + newCenter = widget.mapState.unproject(newCenterPt, newZoom); } else { - newCenter = mapStateContainer.center; + newCenter = widget.mapState.center; } } else { - newCenter = mapStateContainer.center; + newCenter = widget.mapState.center; } if (_pinchZoomStarted || _pinchMoveStarted) { - mapMoved = mapStateContainer.move( - newCenter, - newZoom, - hasGesture: true, - source: eventSource, - ); + widget.onPinchZoomUpdate(eventSource, newCenter, newZoom); } } if (hasRotate) { if (!_rotationStarted && currentRotation != 0.0) { _rotationStarted = true; - mapStateContainer.emitMapEvent( - MapEventRotateStart( - mapState: mapState, - source: eventSource, - ), - ); + widget.onRotateStart(eventSource); } if (_rotationStarted) { final rotationDiff = currentRotation - _lastRotation; - final oldCenterPt = mapState.project(mapStateContainer.center); + final oldCenterPt = + widget.mapState.project(widget.mapState.center); final rotationCenter = - mapState.project(_offsetToCrs(_lastFocalLocal)); + widget.mapState.project(_offsetToCrs(_lastFocalLocal)); final vector = oldCenterPt - rotationCenter; final rotatedVector = vector.rotate(degToRadian(rotationDiff)); final newCenter = rotationCenter + rotatedVector; - mapMoved = mapStateContainer.move( - mapState.unproject(newCenter), mapStateContainer.zoom, - source: eventSource) || - mapMoved; - mapRotated = mapStateContainer.rotate( - mapStateContainer.rotation + rotationDiff, - hasGesture: true, - source: eventSource, + + widget.onRotateUpdate( + eventSource, + widget.mapState.unproject(newCenter), + widget.mapState.zoom, + widget.mapState.rotation + rotationDiff, ); } } - - if (mapMoved || mapRotated) mapStateContainer.setState(() {}); } } } @@ -519,36 +597,21 @@ abstract class MapGestureMixin extends State if (_rotationStarted) { _rotationStarted = false; - mapStateContainer.emitMapEvent( - MapEventRotateEnd( - mapState: mapState, - source: eventSource, - ), - ); + widget.onRotateEnd(eventSource); } if (_dragStarted || _pinchZoomStarted || _pinchMoveStarted) { _dragStarted = _pinchZoomStarted = _pinchMoveStarted = false; - mapStateContainer.emitMapEvent( - MapEventMoveEnd( - mapState: mapState, - source: eventSource, - ), - ); + widget.onMoveEnd(eventSource); } final hasFling = InteractiveFlag.hasFlag( - options.interactiveFlags, InteractiveFlag.flingAnimation); + widget.options.interactiveFlags, InteractiveFlag.flingAnimation); final magnitude = details.velocity.pixelsPerSecond.distance; if (magnitude < _kMinFlingVelocity || !hasFling) { if (hasFling) { - mapStateContainer.emitMapEvent( - MapEventFlingAnimationNotStarted( - mapState: mapState, - source: eventSource, - ), - ); + widget.onFlingNotStarted(eventSource); } return; @@ -556,7 +619,8 @@ abstract class MapGestureMixin extends State final direction = details.velocity.pixelsPerSecond / magnitude; final distance = (Offset.zero & - Size(mapState.nonrotatedSize.x, mapState.nonrotatedSize.y)) + Size(widget.mapState.nonrotatedSize.x, + widget.mapState.nonrotatedSize.y)) .shortestSide; final flingOffset = _focalStartLocal - _lastFocalLocal; @@ -584,19 +648,12 @@ abstract class MapGestureMixin extends State if (relativePosition == null) return; final latlng = _offsetToCrs(relativePosition); - final onTap = options.onTap; + final onTap = widget.options.onTap; if (onTap != null) { // emit the event onTap(position, latlng); } - - mapStateContainer.emitMapEvent( - MapEventTap( - tapPosition: latlng, - mapState: mapState, - source: MapEventSource.tap, - ), - ); + widget.onTap(MapEventSource.tap, latlng); } void handleSecondaryTap(TapPosition position) { @@ -607,19 +664,13 @@ abstract class MapGestureMixin extends State if (relativePosition == null) return; final latlng = _offsetToCrs(relativePosition); - final onSecondaryTap = options.onSecondaryTap; + final onSecondaryTap = widget.options.onSecondaryTap; if (onSecondaryTap != null) { // emit the event onSecondaryTap(position, latlng); } - mapStateContainer.emitMapEvent( - MapEventSecondaryTap( - tapPosition: latlng, - mapState: mapState, - source: MapEventSource.secondaryTap, - ), - ); + widget.onSecondaryTap(MapEventSource.secondaryTap, latlng); } void handleLongPress(TapPosition position) { @@ -629,28 +680,23 @@ abstract class MapGestureMixin extends State closeDoubleTapController(MapEventSource.longPress); final latlng = _offsetToCrs(position.relative!); - if (options.onLongPress != null) { + if (widget.options.onLongPress != null) { // emit the event - options.onLongPress!(position, latlng); + widget.options.onLongPress!(position, latlng); } - mapStateContainer.emitMapEvent( - MapEventLongPress( - tapPosition: latlng, - mapState: mapState, - source: MapEventSource.longPress, - ), - ); + widget.onLongPress(MapEventSource.longPress, latlng); } LatLng _offsetToCrs(Offset offset, [double? zoom]) { - final focalStartPt = mapState.project( - mapStateContainer.center, zoom ?? mapStateContainer.zoom); - final point = (_offsetToPoint(offset) - (mapState.nonrotatedSize / 2.0)) - .rotate(mapState.rotationRad); + final focalStartPt = widget.mapState + .project(widget.mapState.center, zoom ?? widget.mapState.zoom); + final point = + (_offsetToPoint(offset) - (widget.mapState.nonrotatedSize / 2.0)) + .rotate(widget.mapState.rotationRad); final newCenterPt = focalStartPt + point; - return mapState.unproject(newCenterPt, zoom ?? mapStateContainer.zoom); + return widget.mapState.unproject(newCenterPt, zoom ?? widget.mapState.zoom); } void handleDoubleTap(TapPosition tapPosition) { @@ -660,10 +706,10 @@ abstract class MapGestureMixin extends State closeDoubleTapController(MapEventSource.doubleTap); if (InteractiveFlag.hasFlag( - options.interactiveFlags, InteractiveFlag.doubleTapZoom)) { + widget.options.interactiveFlags, InteractiveFlag.doubleTapZoom)) { final centerZoom = _getNewEventCenterZoomPosition( _offsetToPoint(tapPosition.relative!), - _getZoomForScale(mapStateContainer.zoom, 2)); + _getZoomForScale(widget.mapState.zoom, 2)); _startDoubleTapAnimation( centerZoom[1] as double, centerZoom[0] as LatLng); } @@ -676,24 +722,23 @@ abstract class MapGestureMixin extends State List _getNewEventCenterZoomPosition( CustomPoint cursorPos, double newZoom) { // Calculate offset of mouse cursor from viewport center - final viewCenter = mapState.nonrotatedSize / 2; - final offset = (cursorPos - viewCenter).rotate(mapState.rotationRad); + final viewCenter = widget.mapState.nonrotatedSize / 2; + final offset = (cursorPos - viewCenter).rotate(widget.mapState.rotationRad); // Match new center coordinate to mouse cursor position - final scale = - mapStateContainer.getZoomScale(newZoom, mapStateContainer.zoom); + final scale = widget.mapState.getZoomScale(newZoom, widget.mapState.zoom); final newOffset = offset * (1.0 - 1.0 / scale); - final mapCenter = mapState.project(mapStateContainer.center); - final newCenter = mapState.unproject(mapCenter + newOffset); + final mapCenter = widget.mapState.project(widget.mapState.center); + final newCenter = widget.mapState.unproject(mapCenter + newOffset); return [newCenter, newZoom]; } void _startDoubleTapAnimation(double newZoom, LatLng newCenter) { _doubleTapZoomAnimation = - Tween(begin: mapStateContainer.zoom, end: newZoom) + Tween(begin: widget.mapState.zoom, end: newZoom) .chain(CurveTween(curve: Curves.linear)) .animate(_doubleTapController); _doubleTapCenterAnimation = - LatLngTween(begin: mapStateContainer.center, end: newCenter) + LatLngTween(begin: widget.mapState.center, end: newCenter) .chain(CurveTween(curve: Curves.linear)) .animate(_doubleTapController); _doubleTapController.forward(from: 0); @@ -701,28 +746,21 @@ abstract class MapGestureMixin extends State void _doubleTapZoomStatusListener(AnimationStatus status) { if (status == AnimationStatus.forward) { - mapStateContainer.emitMapEvent( - MapEventDoubleTapZoomStart( - mapState: mapState, - source: MapEventSource.doubleTapZoomAnimationController), - ); + widget.onDoubleTapZoomStart( + MapEventSource.doubleTapZoomAnimationController); _startListeningForAnimationInterruptions(); } else if (status == AnimationStatus.completed) { _stopListeningForAnimationInterruptions(); - mapStateContainer.emitMapEvent( - MapEventDoubleTapZoomEnd( - mapState: mapState, - source: MapEventSource.doubleTapZoomAnimationController), - ); + widget + .onDoubleTapZoomEnd(MapEventSource.doubleTapZoomAnimationController); } } void _handleDoubleTapZoomAnimation() { - mapStateContainer.move( + widget.onDoubleTapZoomUpdate( + MapEventSource.doubleTapZoomAnimationController, _doubleTapCenterAnimation.value, _doubleTapZoomAnimation.value, - hasGesture: true, - source: MapEventSource.doubleTapZoomAnimationController, ); } @@ -738,22 +776,13 @@ abstract class MapGestureMixin extends State void _handleDoubleTapHold(ScaleUpdateDetails details) { _doubleTapHoldMaxDelay?.cancel(); - final flags = options.interactiveFlags; + final flags = widget.options.interactiveFlags; if (InteractiveFlag.hasFlag(flags, InteractiveFlag.pinchZoom)) { - final zoom = mapStateContainer.zoom; - final focalOffset = details.localFocalPoint; - final verticalOffset = (_focalStartLocal - focalOffset).dy; - final newZoom = _mapZoomStart - verticalOffset / 360 * zoom; - final min = options.minZoom ?? 0.0; - final max = options.maxZoom ?? double.infinity; - final actualZoom = math.max(min, math.min(max, newZoom)); - - mapStateContainer.move( - mapStateContainer.center, - actualZoom, - hasGesture: true, - source: MapEventSource.doubleTapHold, - ); + final verticalOffset = (_focalStartLocal - details.localFocalPoint).dy; + final newZoom = + _mapZoomStart - verticalOffset / 360 * widget.mapState.zoom; + + widget.onOneFingerPinchZoom(MapEventSource.doubleTapHold, newZoom); } } @@ -766,37 +795,23 @@ abstract class MapGestureMixin extends State if (status == AnimationStatus.completed) { _flingAnimationStarted = false; _stopListeningForAnimationInterruptions(); - mapStateContainer.emitMapEvent( - MapEventFlingAnimationEnd( - mapState: mapState, - source: MapEventSource.flingAnimationController, - ), - ); + widget.onFlingEnd(MapEventSource.flingAnimationController); } } void _handleFlingAnimation() { if (!_flingAnimationStarted) { _flingAnimationStarted = true; - mapStateContainer.emitMapEvent( - MapEventFlingAnimationStart( - mapState: mapState, - source: MapEventSource.flingAnimationController, - ), - ); + widget.onFlingStart(MapEventSource.flingAnimationController); _startListeningForAnimationInterruptions(); } - final newCenterPoint = mapState.project(_mapCenterStart) + - _offsetToPoint(_flingAnimation.value).rotate(mapState.rotationRad); - final newCenter = mapState.unproject(newCenterPoint); + final newCenterPoint = widget.mapState.project(_mapCenterStart) + + _offsetToPoint(_flingAnimation.value) + .rotate(widget.mapState.rotationRad); + final newCenter = widget.mapState.unproject(newCenterPoint); - mapStateContainer.move( - newCenter, - mapStateContainer.zoom, - hasGesture: true, - source: MapEventSource.flingAnimationController, - ); + widget.onFlingUpdate(MapEventSource.flingAnimationController, newCenter); } void _startListeningForAnimationInterruptions() { @@ -823,11 +838,11 @@ abstract class MapGestureMixin extends State double _getZoomForScale(double startZoom, double scale) { final resultZoom = scale == 1.0 ? startZoom : startZoom + math.log(scale) / math.ln2; - return mapStateContainer.fitZoomToBounds(resultZoom); + return widget.mapState.fitZoomToBounds(resultZoom); } Offset _rotateOffset(Offset offset) { - final radians = mapState.rotationRad; + final radians = widget.mapState.rotationRad; if (radians != 0.0) { final cos = math.cos(radians); final sin = math.sin(radians); @@ -839,11 +854,4 @@ abstract class MapGestureMixin extends State return offset; } - - @override - void dispose() { - _flingController.dispose(); - _doubleTapController.dispose(); - super.dispose(); - } } diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 12a3fa4da..c74dfca3d 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -521,7 +521,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { // Load and/or prune tiles according to the visible bounds of the [event] // center/zoom, or the current center/zoom if not specified. void _onTileUpdateEvent(TileUpdateEvent event) { - debugPrint('Tile update event'); final tileZoom = _clampToNativeZoom(event.zoom); final visibleTileRange = _tileRangeCalculator.calculate( mapState: event.mapState, diff --git a/lib/src/map/flutter_map_state.dart b/lib/src/map/flutter_map_state.dart index 4d0b18b18..e3f230cdb 100644 --- a/lib/src/map/flutter_map_state.dart +++ b/lib/src/map/flutter_map_state.dart @@ -30,6 +30,19 @@ class FlutterMapState { final bool hasFitInitialBounds; + FlutterMapState._({ + required this.options, + required this.center, + required this.zoom, + required this.rotation, + required this.nonrotatedSize, + required this.size, + required this.hasFitInitialBounds, + required this.pixelOrigin, + required this.bounds, + required this.pixelBounds, + }); + FlutterMapState({ required this.options, required this.center, @@ -41,8 +54,8 @@ class FlutterMapState { }) { pixelBounds = _getPixelBoundsStatic(options.crs, size, center, zoom); bounds = LatLngBounds( - unproject(pixelBounds.bottomLeft), - unproject(pixelBounds.topRight), + options.crs.pointToLatLng(pixelBounds.bottomLeft, zoom), + options.crs.pointToLatLng(pixelBounds.topRight, zoom), ); final halfSize = size / 2.0; pixelOrigin = (project(center, zoom) - halfSize).round(); @@ -90,6 +103,16 @@ class FlutterMapState { ); } + FlutterMapState withOptions(MapOptions options) => FlutterMapState( + options: options, + center: center, + zoom: zoom, + rotation: rotation, + nonrotatedSize: nonrotatedSize, + size: _calculateSize(rotation, nonrotatedSize), + hasFitInitialBounds: hasFitInitialBounds, + ); + static CustomPoint _calculateSize( double rotation, CustomPoint nonrotatedSize, @@ -298,9 +321,21 @@ class FlutterMapState { return CustomPoint(tp.dx, tp.dy); } + // TODO replicate old caching or stop doing it _SafeArea? _safeAreaCache; double? _safeAreaZoom; + double fitZoomToBounds(double zoom) { + // Abide to min/max zoom + if (options.maxZoom != null) { + zoom = (zoom > options.maxZoom!) ? options.maxZoom! : zoom; + } + if (options.minZoom != null) { + zoom = (zoom < options.minZoom!) ? options.minZoom! : zoom; + } + return zoom; + } + //if there is a pan boundary, do not cross bool isOutOfBounds(LatLng center) { if (options.adaptiveBoundaries) { @@ -366,6 +401,96 @@ class FlutterMapState { double calculateScreenHeightInDegrees() => options.screenSize!.height * 170.102258 / math.pow(2, zoom + 8); + LatLng offsetToCrs(Offset offset, [double? zoom]) { + final focalStartPt = project(center, zoom ?? this.zoom); + final point = + (offsetToPoint(offset) - (nonrotatedSize / 2.0)).rotate(rotationRad); + + final newCenterPt = focalStartPt + point; + return unproject(newCenterPt, zoom ?? this.zoom); + } + + // TODO This can be an extension on offset or a constructor on CustomPoint. + CustomPoint offsetToPoint(Offset offset) { + return CustomPoint(offset.dx, offset.dy); + } + + List getNewEventCenterZoomPosition( + CustomPoint cursorPos, double newZoom) { + // Calculate offset of mouse cursor from viewport center + final viewCenter = nonrotatedSize / 2; + final offset = (cursorPos - viewCenter).rotate(rotationRad); + // Match new center coordinate to mouse cursor position + final scale = getZoomScale(newZoom, zoom); + final newOffset = offset * (1.0 - 1.0 / scale); + final mapCenter = project(center); + final newCenter = unproject(mapCenter + newOffset); + return [newCenter, newZoom]; + } + + LatLng? adjustCenterIfOutsideMaxBounds( + LatLng testCenter, double testZoom, LatLngBounds maxBounds) { + LatLng? newCenter; + + final swPixel = project(maxBounds.southWest, testZoom); + final nePixel = project(maxBounds.northEast, testZoom); + + final centerPix = project(testCenter, testZoom); + + final halfSizeX = size.x / 2; + final halfSizeY = size.y / 2; + + // Try and find the edge value that the center could use to stay within + // the maxBounds. This should be ok for panning. If we zoom, it is possible + // there is no solution to keep all corners within the bounds. If the edges + // are still outside the bounds, don't return anything. + final leftOkCenter = math.min(swPixel.x, nePixel.x) + halfSizeX; + final rightOkCenter = math.max(swPixel.x, nePixel.x) - halfSizeX; + final topOkCenter = math.min(swPixel.y, nePixel.y) + halfSizeY; + final botOkCenter = math.max(swPixel.y, nePixel.y) - halfSizeY; + + double? newCenterX; + double? newCenterY; + + var wasAdjusted = false; + + if (centerPix.x < leftOkCenter) { + wasAdjusted = true; + newCenterX = leftOkCenter; + } else if (centerPix.x > rightOkCenter) { + wasAdjusted = true; + newCenterX = rightOkCenter; + } + + if (centerPix.y < topOkCenter) { + wasAdjusted = true; + newCenterY = topOkCenter; + } else if (centerPix.y > botOkCenter) { + wasAdjusted = true; + newCenterY = botOkCenter; + } + + if (!wasAdjusted) { + return testCenter; + } + + final newCx = newCenterX ?? centerPix.x; + final newCy = newCenterY ?? centerPix.y; + + // Have a final check, see if the adjusted center is within maxBounds. + // If not, give up. + if (newCx < leftOkCenter || + newCx > rightOkCenter || + newCy < topOkCenter || + newCy > botOkCenter) { + return null; + } else { + newCenter = unproject(CustomPoint(newCx, newCy), testZoom); + } + + return newCenter; + } + static FlutterMapState? maybeOf(BuildContext context) => context .dependOnInheritedWidgetOfExactType() ?.mapState; diff --git a/lib/src/map/flutter_map_state_container.dart b/lib/src/map/flutter_map_state_container.dart index d66363faa..eb6ee5f1d 100644 --- a/lib/src/map/flutter_map_state_container.dart +++ b/lib/src/map/flutter_map_state_container.dart @@ -4,36 +4,24 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/gestures/gestures.dart'; +import 'package:flutter_map/src/gestures/interaction_detector.dart'; import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; -import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; -class FlutterMapStateContainer extends MapGestureMixin { +class FlutterMapStateContainer extends State { static const invalidSize = CustomPoint(-1, -1); - - final _positionedTapController = PositionedTapController(); - final _gestureArenaTeam = GestureArenaTeam(); + final _flutterMapGestureDetectorKey = GlobalKey(); bool _hasFitInitialBounds = false; - @override - FlutterMapState get mapState => _mapState; - -// TODO Should override methods like move() instead. - @override - FlutterMapStateContainer get mapStateContainer => this; - final _localController = MapController(); - @override MapController get mapController => widget.mapController ?? _localController; - @override - MapOptions get options => widget.options; - late FlutterMapState _mapState; + FlutterMapState get mapState => _mapState; + LatLng get center => _mapState.center; LatLngBounds get bounds => _mapState.bounds; @@ -47,13 +35,13 @@ class FlutterMapStateContainer extends MapGestureMixin { super.initState(); WidgetsBinding.instance - .addPostFrameCallback((_) => options.onMapReady?.call()); + .addPostFrameCallback((_) => widget.options.onMapReady?.call()); _mapState = FlutterMapState( - options: options, - center: options.center, - zoom: options.zoom, - rotation: options.rotation, + options: widget.options, + center: widget.options.center, + zoom: widget.options.zoom, + rotation: widget.options.rotation, nonrotatedSize: invalidSize, size: invalidSize, hasFitInitialBounds: _hasFitInitialBounds, @@ -62,77 +50,15 @@ class FlutterMapStateContainer extends MapGestureMixin { @override void didUpdateWidget(FlutterMap oldWidget) { + if (oldWidget.options != widget.options) { + _mapState = _mapState.withOptions(widget.options); + } super.didUpdateWidget(oldWidget); // TODO update the map state appropriately. } @override Widget build(BuildContext context) { - final DeviceGestureSettings gestureSettings = - MediaQuery.gestureSettingsOf(context); - final Map gestures = - {}; - - gestures[TapGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(debugOwner: this), - (TapGestureRecognizer instance) { - instance - ..onTapDown = _positionedTapController.onTapDown - ..onTapUp = handleOnTapUp - ..onTap = _positionedTapController.onTap - ..onSecondaryTap = _positionedTapController.onSecondaryTap - ..onSecondaryTapDown = _positionedTapController.onTapDown; - }, - ); - - gestures[LongPressGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => LongPressGestureRecognizer(debugOwner: this), - (LongPressGestureRecognizer instance) { - instance.onLongPress = _positionedTapController.onLongPress; - }, - ); - - if (InteractiveFlag.hasFlag( - options.interactiveFlags, InteractiveFlag.drag)) { - gestures[VerticalDragGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => VerticalDragGestureRecognizer(debugOwner: this), - (VerticalDragGestureRecognizer instance) { - instance.onUpdate = (details) { - // Absorbing vertical drags - }; - instance.gestureSettings = gestureSettings; - instance.team ??= _gestureArenaTeam; - }, - ); - gestures[HorizontalDragGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => HorizontalDragGestureRecognizer(debugOwner: this), - (HorizontalDragGestureRecognizer instance) { - instance.onUpdate = (details) { - // Absorbing horizontal drags - }; - instance.gestureSettings = gestureSettings; - instance.team ??= _gestureArenaTeam; - }, - ); - } - - gestures[ScaleGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => ScaleGestureRecognizer(debugOwner: this), - (ScaleGestureRecognizer instance) { - instance - ..onStart = handleScaleStart - ..onUpdate = handleScaleUpdate - ..onEnd = handleScaleEnd; - instance.team ??= _gestureArenaTeam; - _gestureArenaTeam.captain = instance; - }, - ); - return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { _onConstraintsChange(constraints); @@ -140,43 +66,47 @@ class FlutterMapStateContainer extends MapGestureMixin { return MapStateInheritedWidget( mapController: mapController, mapState: _mapState, - child: Listener( - onPointerDown: onPointerDown, - onPointerUp: onPointerUp, - onPointerCancel: onPointerCancel, - onPointerHover: onPointerHover, - onPointerSignal: onPointerSignal, - child: PositionedTapDetector2( - controller: _positionedTapController, - onTap: handleTap, - onSecondaryTap: handleSecondaryTap, - onLongPress: handleLongPress, - onDoubleTap: handleDoubleTap, - doubleTapDelay: InteractiveFlag.hasFlag( - options.interactiveFlags, - InteractiveFlag.doubleTapZoom, - ) - ? null - : Duration.zero, - child: RawGestureDetector( - gestures: gestures, - child: ClipRect( - child: Stack( - children: [ - OverflowBox( - minWidth: _mapState.size.x, - maxWidth: _mapState.size.x, - minHeight: _mapState.size.y, - maxHeight: _mapState.size.y, - child: Transform.rotate( - angle: _mapState.rotationRad, - child: Stack(children: widget.children), - ), - ), - Stack(children: widget.nonRotatedChildren), - ], + child: InteractionDetector( + key: _flutterMapGestureDetectorKey, + mapStateContainer: this, + onPointerDown: _onPointerDown, + onPointerUp: _onPointerUp, + onPointerCancel: _onPointerCancel, + onPointerHover: _onPointerHover, + onRotateEnd: _onRotateEnd, + onFlingStart: _onFlingStart, + onFlingEnd: _onFlingEnd, + onMoveEnd: _onMoveEnd, + onOneFingerPinchZoom: _onOneFingerPinchZoom, + onDoubleTapZoomEnd: _onDoubleTapZoomEnd, + onMoveStart: _onMoveStart, + onRotateStart: _onRotateStart, + onFlingNotStarted: _onFlingNotStarted, + onPinchZoomUpdate: _onPinchZoomUpdate, + onRotateUpdate: _onRotateUpdate, + onTap: _onTap, + onDragUpdate: _onDragUpdate, + onSecondaryTap: _onSecondaryTap, + onLongPress: _onLongPress, + onDoubleTapZoomStart: _onDoubleTapZoomStart, + onScroll: _onScroll, + onDoubleTapZoomUpdate: _onDoubleTapZoomUpdate, + onFlingUpdate: _onFlingUpdate, + child: ClipRect( + child: Stack( + children: [ + OverflowBox( + minWidth: _mapState.size.x, + maxWidth: _mapState.size.x, + minHeight: _mapState.size.y, + maxHeight: _mapState.size.y, + child: Transform.rotate( + angle: _mapState.rotationRad, + child: Stack(children: widget.children), + ), ), - ), + Stack(children: widget.nonRotatedChildren), + ], ), ), ), @@ -185,6 +115,219 @@ class FlutterMapStateContainer extends MapGestureMixin { ); } + void _onDoubleTapZoomUpdate( + MapEventSource source, + LatLng position, + double zoom, + ) { + move( + position, + zoom, + hasGesture: true, + source: source, + ); + } + + void _onFlingUpdate(MapEventSource source, LatLng position) { + move( + position, + _mapState.zoom, + hasGesture: true, + source: source, + ); + } + + void _onRotateUpdate( + MapEventSource source, + LatLng position, + double zoom, + double rotation, + ) { + moveAndRotate(position, zoom, rotation, source: source, hasGesture: true); + } + + void _onOneFingerPinchZoom(MapEventSource source, double newZoom) { + final min = widget.options.minZoom ?? 0.0; + final max = widget.options.maxZoom ?? double.infinity; + final actualZoom = math.max(min, math.min(max, newZoom)); + + move( + _mapState.center, + actualZoom, + hasGesture: true, + source: source, + ); + } + + void _onScroll(PointerScrollEvent event) { + final minZoom = widget.options.minZoom ?? 0.0; + final maxZoom = widget.options.maxZoom ?? double.infinity; + final newZoom = (_mapState.zoom - + event.scrollDelta.dy * widget.options.scrollWheelVelocity) + .clamp(minZoom, maxZoom); + // Calculate offset of mouse cursor from viewport center + final List newCenterZoom = _mapState.getNewEventCenterZoomPosition( + _mapState.offsetToPoint(event.localPosition), newZoom); + + // Move to new center and zoom level + move( + newCenterZoom[0] as LatLng, + newCenterZoom[1] as double, + source: MapEventSource.scrollWheel, + ); + } + + void _onPointerDown(PointerDownEvent event) { + if (widget.options.onPointerDown != null) { + final latlng = _mapState.offsetToCrs(event.localPosition); + widget.options.onPointerDown!(event, latlng); + } + } + + void _onPointerUp(PointerUpEvent event) { + if (widget.options.onPointerUp != null) { + final latlng = _mapState.offsetToCrs(event.localPosition); + widget.options.onPointerUp!(event, latlng); + } + } + + void _onPointerCancel(PointerCancelEvent event) { + if (widget.options.onPointerCancel != null) { + final latlng = _mapState.offsetToCrs(event.localPosition); + widget.options.onPointerCancel!(event, latlng); + } + } + + void _onPointerHover(PointerHoverEvent event) { + if (widget.options.onPointerHover != null) { + final latlng = _mapState.offsetToCrs(event.localPosition); + widget.options.onPointerHover!(event, latlng); + } + } + + void _onTap(MapEventSource source, LatLng position) { + _emitMapEvent( + MapEventTap( + tapPosition: position, + mapState: _mapState, + source: source, + ), + ); + } + + void _onSecondaryTap(MapEventSource source, LatLng position) { + _emitMapEvent( + MapEventSecondaryTap( + tapPosition: position, + mapState: _mapState, + source: source, + ), + ); + } + + void _onDragUpdate(MapEventSource source, Offset offset) { + final oldCenterPt = _mapState.project(_mapState.center); + + final newCenterPt = oldCenterPt + _mapState.offsetToPoint(offset); + final newCenter = _mapState.unproject(newCenterPt); + + move(newCenter, _mapState.zoom, hasGesture: true, source: source); + } + + void _onPinchZoomUpdate(MapEventSource source, LatLng position, double zoom) { + move(position, zoom, hasGesture: true, source: source); + } + + void _onLongPress(MapEventSource source, LatLng position) { + _emitMapEvent( + MapEventLongPress( + tapPosition: position, + mapState: _mapState, + source: source, + ), + ); + } + + void _onMoveStart(MapEventSource source) { + _emitMapEvent( + MapEventMoveStart( + mapState: _mapState, + source: source, + ), + ); + } + + void _onRotateStart(MapEventSource source) { + _emitMapEvent( + MapEventRotateStart( + mapState: _mapState, + source: source, + ), + ); + } + + void _onRotateEnd(MapEventSource source) { + _emitMapEvent( + MapEventRotateEnd( + mapState: _mapState, + source: source, + ), + ); + } + + void _onMoveEnd(MapEventSource source) { + _emitMapEvent( + MapEventRotateEnd( + mapState: _mapState, + source: source, + ), + ); + } + + void _onFlingStart(MapEventSource source) { + _emitMapEvent( + MapEventFlingAnimationStart( + mapState: _mapState, + source: MapEventSource.flingAnimationController, + ), + ); + } + + void _onFlingEnd(MapEventSource source) { + _emitMapEvent( + MapEventFlingAnimationEnd( + mapState: _mapState, + source: source, + ), + ); + } + + void _onDoubleTapZoomEnd(MapEventSource source) { + _emitMapEvent( + MapEventDoubleTapZoomEnd( + mapState: _mapState, + source: source, + ), + ); + } + + void _onDoubleTapZoomStart(MapEventSource source) { + _emitMapEvent( + MapEventDoubleTapZoomStart( + mapState: _mapState, + source: MapEventSource.doubleTapZoomAnimationController), + ); + } + + void _onFlingNotStarted(MapEventSource source) { + _emitMapEvent( + MapEventFlingAnimationNotStarted( + mapState: _mapState, + source: source, + ), + ); + } + // No need to call setState in here as we are already running a build and the // resulting FlutterMapState will be passed to the inherited widget which // will trigger a build if it is different. @@ -194,12 +337,12 @@ class FlutterMapStateContainer extends MapGestureMixin { // If bounds were provided set the initial center/zoom to match those // bounds once the parent constraints are available. - if (options.bounds != null && + if (widget.options.bounds != null && !_hasFitInitialBounds && _parentConstraintsAreSet(context, constraints)) { final target = _mapState.getBoundsCenterZoom( - options.bounds!, - options.boundsOptions, + widget.options.bounds!, + widget.options.boundsOptions, ); _mapState = _mapState.copyWith(zoom: target.zoom, center: target.center); @@ -216,7 +359,7 @@ class FlutterMapStateContainer extends MapGestureMixin { ); if (_mapState.nonrotatedSize != invalidSize) { - emitMapEvent( + _emitMapEvent( MapEventNonRotatedSizeChange( source: MapEventSource.nonRotatedSizeChange, oldMapState: oldMapState, @@ -236,9 +379,10 @@ class FlutterMapStateContainer extends MapGestureMixin { BuildContext context, BoxConstraints constraints) => constraints.maxWidth != 0 || MediaQuery.sizeOf(context) != Size.zero; - void emitMapEvent(MapEvent event) { + void _emitMapEvent(MapEvent event) { if (event.source == MapEventSource.mapController && event is MapEventMove) { - handleAnimationInterruptions(event); + _flutterMapGestureDetectorKey.currentState + ?.handleAnimationInterruptions(event); } widget.options.onMapEvent?.call(event); @@ -259,7 +403,7 @@ class FlutterMapStateContainer extends MapGestureMixin { _mapState = _mapState.withRotation(newRotation); }); - emitMapEvent( + _emitMapEvent( MapEventRotate( id: id, source: source, @@ -337,10 +481,18 @@ class FlutterMapStateContainer extends MapGestureMixin { Offset offset = Offset.zero, required MapEventSource source, String? id, + bool hasGesture = false, }) => MoveAndRotateResult( - move(newCenter, newZoom, offset: offset, id: id, source: source), - rotate(newRotation, id: id, source: source), + move( + newCenter, + newZoom, + offset: offset, + id: id, + source: source, + hasGesture: hasGesture, + ), + rotate(newRotation, id: id, source: source, hasGesture: hasGesture), ); bool move( @@ -351,13 +503,13 @@ class FlutterMapStateContainer extends MapGestureMixin { required MapEventSource source, String? id, }) { - newZoom = fitZoomToBounds(newZoom); + newZoom = _mapState.fitZoomToBounds(newZoom); // Algorithm thanks to https://github.com/tlserver/flutter_map_location_marker if (offset != Offset.zero) { - final newPoint = options.crs.latLngToPoint(newCenter, newZoom); - newCenter = options.crs.pointToLatLng( - rotatePoint( + final newPoint = widget.options.crs.latLngToPoint(newCenter, newZoom); + newCenter = widget.options.crs.pointToLatLng( + _mapState.rotatePoint( newPoint, newPoint - CustomPoint(offset.dx, offset.dy), ), @@ -365,16 +517,16 @@ class FlutterMapStateContainer extends MapGestureMixin { ); } - if (isOutOfBounds(newCenter)) { - if (!options.slideOnBoundaries) return false; - newCenter = containPoint(newCenter, _mapState.center); + if (_mapState.isOutOfBounds(newCenter)) { + if (!widget.options.slideOnBoundaries) return false; + newCenter = _mapState.containPoint(newCenter, _mapState.center); } - if (options.maxBounds != null) { - final adjustedCenter = adjustCenterIfOutsideMaxBounds( + if (widget.options.maxBounds != null) { + final adjustedCenter = _mapState.adjustCenterIfOutsideMaxBounds( newCenter, newZoom, - options.maxBounds!, + widget.options.maxBounds!, ); if (adjustedCenter == null) return false; @@ -397,9 +549,9 @@ class FlutterMapStateContainer extends MapGestureMixin { source: source, id: id, ); - if (movementEvent != null) emitMapEvent(movementEvent); + if (movementEvent != null) _emitMapEvent(movementEvent); - options.onPositionChanged?.call( + widget.options.onPositionChanged?.call( MapPosition( center: newCenter, bounds: _mapState.bounds, @@ -412,17 +564,6 @@ class FlutterMapStateContainer extends MapGestureMixin { return true; } - double fitZoomToBounds(double zoom) { - // Abide to min/max zoom - if (options.maxZoom != null) { - zoom = (zoom > options.maxZoom!) ? options.maxZoom! : zoom; - } - if (options.minZoom != null) { - zoom = (zoom < options.minZoom!) ? options.minZoom! : zoom; - } - return zoom; - } - bool fitBounds( LatLngBounds bounds, FitBoundsOptions options, { @@ -437,127 +578,24 @@ class FlutterMapStateContainer extends MapGestureMixin { ); } - CenterZoom centerZoomFitBounds( - LatLngBounds bounds, - FitBoundsOptions options, - ) => - _mapState.getBoundsCenterZoom(bounds, options); - - double getBoundsZoom( - LatLngBounds bounds, - CustomPoint padding, { - bool inside = false, - bool forceIntegerZoomLevel = false, - }) => - _mapState.getBoundsZoom( - bounds, - padding, - inside: inside, - forceIntegerZoomLevel: forceIntegerZoomLevel, - ); - - double getZoomScale(double toZoom, double fromZoom) => - _mapState.getZoomScale(toZoom, fromZoom); - - double getScaleZoom(double scale, double? fromZoom) => - _mapState.getScaleZoom(scale, fromZoom); - - Bounds? getPixelWorldBounds(double? zoom) => - _mapState.getPixelWorldBounds(zoom); - - Offset getOffsetFromOrigin(LatLng pos) => _mapState.getOffsetFromOrigin(pos); - - CustomPoint getNewPixelOrigin(LatLng center, [double? zoom]) => - _mapState.getNewPixelOrigin(center, zoom); - - Bounds getPixelBounds([double? zoom]) => - _mapState.getPixelBounds(zoom); - - LatLng? adjustCenterIfOutsideMaxBounds( - LatLng testCenter, double testZoom, LatLngBounds maxBounds) { - LatLng? newCenter; - - final swPixel = _mapState.project(maxBounds.southWest, testZoom); - final nePixel = _mapState.project(maxBounds.northEast, testZoom); - - final centerPix = _mapState.project(testCenter, testZoom); - - final halfSizeX = _mapState.size.x / 2; - final halfSizeY = _mapState.size.y / 2; - - // Try and find the edge value that the center could use to stay within - // the maxBounds. This should be ok for panning. If we zoom, it is possible - // there is no solution to keep all corners within the bounds. If the edges - // are still outside the bounds, don't return anything. - final leftOkCenter = math.min(swPixel.x, nePixel.x) + halfSizeX; - final rightOkCenter = math.max(swPixel.x, nePixel.x) - halfSizeX; - final topOkCenter = math.min(swPixel.y, nePixel.y) + halfSizeY; - final botOkCenter = math.max(swPixel.y, nePixel.y) - halfSizeY; - - double? newCenterX; - double? newCenterY; - - var wasAdjusted = false; - - if (centerPix.x < leftOkCenter) { - wasAdjusted = true; - newCenterX = leftOkCenter; - } else if (centerPix.x > rightOkCenter) { - wasAdjusted = true; - newCenterX = rightOkCenter; - } - - if (centerPix.y < topOkCenter) { - wasAdjusted = true; - newCenterY = topOkCenter; - } else if (centerPix.y > botOkCenter) { - wasAdjusted = true; - newCenterY = botOkCenter; - } - - if (!wasAdjusted) { - return testCenter; - } - - final newCx = newCenterX ?? centerPix.x; - final newCy = newCenterY ?? centerPix.y; - - // Have a final check, see if the adjusted center is within maxBounds. - // If not, give up. - if (newCx < leftOkCenter || - newCx > rightOkCenter || - newCy < topOkCenter || - newCy > botOkCenter) { - return null; - } else { - newCenter = _mapState.unproject(CustomPoint(newCx, newCy), testZoom); - } + LatLng pointToLatLng(CustomPoint localPoint) => + _mapState.pointToLatLng(localPoint); - return newCenter; - } + CenterZoom centerZoomFitBounds( + LatLngBounds bounds, FitBoundsOptions options) => + _mapState.centerZoomFitBounds(bounds, options); - // This will convert a latLng to a position that we could use with a widget - // outside of FlutterMap layer space. Eg using a Positioned Widget. CustomPoint latLngToScreenPoint(LatLng latLng) => _mapState.latLngToScreenPoint(latLng); - LatLng pointToLatLng(CustomPoint localPoint) => - _mapState.pointToLatLng(localPoint); - - // Sometimes we need to make allowances that a rotation already exists, so - // it needs to be reversed (pointToLatLng), and sometimes we want to use - // the same rotation to create a new position (latLngToScreenpoint). - // counterRotation just makes allowances this for this. CustomPoint rotatePoint( - CustomPoint mapCenter, - CustomPoint point, { + CustomPoint mapCenter, + CustomPoint point, { bool counterRotation = true, }) => - _mapState.rotatePoint(mapCenter, point, counterRotation: counterRotation); - - //if there is a pan boundary, do not cross - bool isOutOfBounds(LatLng center) => _mapState.isOutOfBounds(center); - - LatLng containPoint(LatLng point, LatLng fallback) => - _mapState.containPoint(point, fallback); + _mapState.rotatePoint( + mapCenter.toDoublePoint(), + point.toDoublePoint(), + counterRotation: counterRotation, + ); } diff --git a/lib/src/map/flutter_map_state_inherited_widget.dart b/lib/src/map/flutter_map_state_inherited_widget.dart index 600f7e0e0..c2e772409 100644 --- a/lib/src/map/flutter_map_state_inherited_widget.dart +++ b/lib/src/map/flutter_map_state_inherited_widget.dart @@ -14,10 +14,7 @@ class MapStateInheritedWidget extends InheritedWidget { final MapController mapController; @override - bool updateShouldNotify(MapStateInheritedWidget oldWidget) { - final decision = !identical(mapState, oldWidget.mapState) || - !identical(mapController, oldWidget.mapController); - debugPrint('Decided: $decision'); - return decision; - } + bool updateShouldNotify(MapStateInheritedWidget oldWidget) => + !identical(mapState, oldWidget.mapState) || + !identical(mapController, oldWidget.mapController); } From afa3c7833a6fb07b73fc1ffa20687ea459d764ee Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Fri, 9 Jun 2023 10:26:08 +0200 Subject: [PATCH 03/46] Move gesture initialisation out of builder and stop passing the whole FlutterMapStateContainer to InteractionDetector --- lib/src/gestures/interaction_detector.dart | 265 +++++++++++-------- lib/src/map/flutter_map_state_container.dart | 4 +- 2 files changed, 151 insertions(+), 118 deletions(-) diff --git a/lib/src/gestures/interaction_detector.dart b/lib/src/gestures/interaction_detector.dart index 2e9921cf4..946f7e975 100644 --- a/lib/src/gestures/interaction_detector.dart +++ b/lib/src/gestures/interaction_detector.dart @@ -8,7 +8,6 @@ import 'package:flutter_map/src/gestures/latlng_tween.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; import 'package:flutter_map/src/map/flutter_map_state.dart'; -import 'package:flutter_map/src/map/flutter_map_state_container.dart'; import 'package:flutter_map/src/map/options.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; @@ -16,11 +15,10 @@ import 'package:latlong2/latlong.dart'; class InteractionDetector extends StatefulWidget { final Widget child; - final FlutterMapStateContainer mapStateContainer; - - FlutterMapState get mapState => mapStateContainer.mapState; - MapOptions get options => mapState.options; - + final MapOptions options; + // This needs to be passed as a callback rather than just passing the state + // otherwise the map lags significantly. + final FlutterMapState Function() currentMapState; final void Function(PointerDownEvent event) onPointerDown; final void Function(PointerUpEvent event) onPointerUp; final void Function(PointerCancelEvent event) onPointerCancel; @@ -54,7 +52,8 @@ class InteractionDetector extends StatefulWidget { const InteractionDetector({ super.key, required this.child, - required this.mapStateContainer, + required this.options, + required this.currentMapState, required this.onPointerDown, required this.onPointerUp, required this.onPointerCancel, @@ -90,6 +89,7 @@ class InteractionDetectorState extends State final _positionedTapController = PositionedTapController(); final _gestureArenaTeam = GestureArenaTeam(); + late Map _gestures; bool _dragMode = false; int _gestureWinner = MultiFingerGesture.none; @@ -135,6 +135,85 @@ class InteractionDetectorState extends State ..addStatusListener(_doubleTapZoomStatusListener); } + @override + void didChangeDependencies() { + _gestures = _initializeGestures( + MediaQuery.gestureSettingsOf(context), + dragEnabled: InteractiveFlag.hasFlag( + widget.options.interactiveFlags, InteractiveFlag.drag), + ); + super.didChangeDependencies(); + } + + Map _initializeGestures( + DeviceGestureSettings gestureSettings, { + required bool dragEnabled, + }) { + final Map gestures = + {}; + + gestures[TapGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(debugOwner: this), + (TapGestureRecognizer instance) { + instance + ..onTapDown = _positionedTapController.onTapDown + ..onTapUp = handleOnTapUp + ..onTap = _positionedTapController.onTap + ..onSecondaryTap = _positionedTapController.onSecondaryTap + ..onSecondaryTapDown = _positionedTapController.onTapDown; + }, + ); + + gestures[LongPressGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => LongPressGestureRecognizer(debugOwner: this), + (LongPressGestureRecognizer instance) { + instance.onLongPress = _positionedTapController.onLongPress; + }, + ); + + if (dragEnabled) { + gestures[VerticalDragGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => VerticalDragGestureRecognizer(debugOwner: this), + (VerticalDragGestureRecognizer instance) { + instance.onUpdate = (details) { + // Absorbing vertical drags + }; + instance.gestureSettings = gestureSettings; + instance.team ??= _gestureArenaTeam; + }, + ); + gestures[HorizontalDragGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => HorizontalDragGestureRecognizer(debugOwner: this), + (HorizontalDragGestureRecognizer instance) { + instance.onUpdate = (details) { + // Absorbing horizontal drags + }; + instance.gestureSettings = gestureSettings; + instance.team ??= _gestureArenaTeam; + }, + ); + } + + gestures[ScaleGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => ScaleGestureRecognizer(debugOwner: this), + (ScaleGestureRecognizer instance) { + instance + ..onStart = _handleScaleStart + ..onUpdate = _handleScaleUpdate + ..onEnd = _handleScaleEnd; + instance.team ??= _gestureArenaTeam; + _gestureArenaTeam.captain = instance; + }, + ); + + return gestures; + } + @override void didUpdateWidget(InteractionDetector oldWidget) { super.didUpdateWidget(oldWidget); @@ -146,6 +225,14 @@ class InteractionDetectorState extends State _getMultiFingerGestureFlags(mapOptions: oldWidget.options); final gestures = _getMultiFingerGestureFlags(); + if (flags != oldFlags) { + _gestures = _initializeGestures( + MediaQuery.gestureSettingsOf(context), + dragEnabled: InteractiveFlag.hasFlag( + widget.options.interactiveFlags, InteractiveFlag.drag), + ); + } + if (flags != oldFlags || gestures != oldGestures) { var emitMapEventMoveEnd = false; @@ -214,71 +301,6 @@ class InteractionDetectorState extends State @override Widget build(BuildContext context) { - final DeviceGestureSettings gestureSettings = - MediaQuery.gestureSettingsOf(context); - final Map gestures = - {}; - - gestures[TapGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(debugOwner: this), - (TapGestureRecognizer instance) { - instance - ..onTapDown = _positionedTapController.onTapDown - ..onTapUp = handleOnTapUp - ..onTap = _positionedTapController.onTap - ..onSecondaryTap = _positionedTapController.onSecondaryTap - ..onSecondaryTapDown = _positionedTapController.onTapDown; - }, - ); - - gestures[LongPressGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => LongPressGestureRecognizer(debugOwner: this), - (LongPressGestureRecognizer instance) { - instance.onLongPress = _positionedTapController.onLongPress; - }, - ); - - if (InteractiveFlag.hasFlag( - widget.options.interactiveFlags, InteractiveFlag.drag)) { - gestures[VerticalDragGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => VerticalDragGestureRecognizer(debugOwner: this), - (VerticalDragGestureRecognizer instance) { - instance.onUpdate = (details) { - // Absorbing vertical drags - }; - instance.gestureSettings = gestureSettings; - instance.team ??= _gestureArenaTeam; - }, - ); - gestures[HorizontalDragGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => HorizontalDragGestureRecognizer(debugOwner: this), - (HorizontalDragGestureRecognizer instance) { - instance.onUpdate = (details) { - // Absorbing horizontal drags - }; - instance.gestureSettings = gestureSettings; - instance.team ??= _gestureArenaTeam; - }, - ); - } - - gestures[ScaleGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => ScaleGestureRecognizer(debugOwner: this), - (ScaleGestureRecognizer instance) { - instance - ..onStart = handleScaleStart - ..onUpdate = handleScaleUpdate - ..onEnd = handleScaleEnd; - instance.team ??= _gestureArenaTeam; - _gestureArenaTeam.captain = instance; - }, - ); - return Listener( onPointerDown: onPointerDown, onPointerUp: onPointerUp, @@ -298,7 +320,7 @@ class InteractionDetectorState extends State ? null : Duration.zero, child: RawGestureDetector( - gestures: gestures, + gestures: _gestures, child: widget.child, ), ), @@ -389,7 +411,7 @@ class InteractionDetectorState extends State } } - void handleScaleStart(ScaleStartDetails details) { + void _handleScaleStart(ScaleStartDetails details) { _dragMode = _pointerCounter == 1; final eventSource = _dragMode @@ -400,8 +422,8 @@ class InteractionDetectorState extends State _gestureWinner = MultiFingerGesture.none; - _mapZoomStart = widget.mapState.zoom; - _mapCenterStart = widget.mapState.center; + _mapZoomStart = widget.currentMapState().zoom; + _mapCenterStart = widget.currentMapState().center; _focalStartLocal = _lastFocalLocal = details.localFocalPoint; _focalStartLatLng = _offsetToCrs(_focalStartLocal); @@ -415,7 +437,7 @@ class InteractionDetectorState extends State _lastScale = 1.0; } - void handleScaleUpdate(ScaleUpdateDetails details) { + void _handleScaleUpdate(ScaleUpdateDetails details) { if (_tapUpCounter == 1) { _handleDoubleTapHold(details); return; @@ -514,7 +536,7 @@ class InteractionDetectorState extends State } } } else { - newZoom = widget.mapState.zoom; + newZoom = widget.currentMapState().zoom; } LatLng newCenter; @@ -529,13 +551,15 @@ class InteractionDetectorState extends State } if (_pinchZoomStarted || _pinchMoveStarted) { - final oldCenterPt = - widget.mapState.project(widget.mapState.center, newZoom); + final oldCenterPt = widget + .currentMapState() + .project(widget.currentMapState().center, newZoom); final newFocalLatLong = _offsetToCrs(_focalStartLocal, newZoom); final newFocalPt = - widget.mapState.project(newFocalLatLong, newZoom); - final oldFocalPt = - widget.mapState.project(_focalStartLatLng, newZoom); + widget.currentMapState().project(newFocalLatLong, newZoom); + final oldFocalPt = widget + .currentMapState() + .project(_focalStartLatLng, newZoom); final zoomDifference = oldFocalPt - newFocalPt; final moveDifference = _rotateOffset(_focalStartLocal - _lastFocalLocal); @@ -543,12 +567,13 @@ class InteractionDetectorState extends State final newCenterPt = oldCenterPt + zoomDifference + _offsetToPoint(moveDifference); - newCenter = widget.mapState.unproject(newCenterPt, newZoom); + newCenter = + widget.currentMapState().unproject(newCenterPt, newZoom); } else { - newCenter = widget.mapState.center; + newCenter = widget.currentMapState().center; } } else { - newCenter = widget.mapState.center; + newCenter = widget.currentMapState().center; } if (_pinchZoomStarted || _pinchMoveStarted) { @@ -564,19 +589,21 @@ class InteractionDetectorState extends State if (_rotationStarted) { final rotationDiff = currentRotation - _lastRotation; - final oldCenterPt = - widget.mapState.project(widget.mapState.center); - final rotationCenter = - widget.mapState.project(_offsetToCrs(_lastFocalLocal)); + final oldCenterPt = widget + .currentMapState() + .project(widget.currentMapState().center); + final rotationCenter = widget + .currentMapState() + .project(_offsetToCrs(_lastFocalLocal)); final vector = oldCenterPt - rotationCenter; final rotatedVector = vector.rotate(degToRadian(rotationDiff)); final newCenter = rotationCenter + rotatedVector; widget.onRotateUpdate( eventSource, - widget.mapState.unproject(newCenter), - widget.mapState.zoom, - widget.mapState.rotation + rotationDiff, + widget.currentMapState().unproject(newCenter), + widget.currentMapState().zoom, + widget.currentMapState().rotation + rotationDiff, ); } } @@ -589,7 +616,7 @@ class InteractionDetectorState extends State _lastFocalLocal = focalOffset; } - void handleScaleEnd(ScaleEndDetails details) { + void _handleScaleEnd(ScaleEndDetails details) { _resetDoubleTapHold(); final eventSource = @@ -619,8 +646,8 @@ class InteractionDetectorState extends State final direction = details.velocity.pixelsPerSecond / magnitude; final distance = (Offset.zero & - Size(widget.mapState.nonrotatedSize.x, - widget.mapState.nonrotatedSize.y)) + Size(widget.currentMapState().nonrotatedSize.x, + widget.currentMapState().nonrotatedSize.y)) .shortestSide; final flingOffset = _focalStartLocal - _lastFocalLocal; @@ -689,14 +716,16 @@ class InteractionDetectorState extends State } LatLng _offsetToCrs(Offset offset, [double? zoom]) { - final focalStartPt = widget.mapState - .project(widget.mapState.center, zoom ?? widget.mapState.zoom); - final point = - (_offsetToPoint(offset) - (widget.mapState.nonrotatedSize / 2.0)) - .rotate(widget.mapState.rotationRad); + final focalStartPt = widget.currentMapState().project( + widget.currentMapState().center, zoom ?? widget.currentMapState().zoom); + final point = (_offsetToPoint(offset) - + (widget.currentMapState().nonrotatedSize / 2.0)) + .rotate(widget.currentMapState().rotationRad); final newCenterPt = focalStartPt + point; - return widget.mapState.unproject(newCenterPt, zoom ?? widget.mapState.zoom); + return widget + .currentMapState() + .unproject(newCenterPt, zoom ?? widget.currentMapState().zoom); } void handleDoubleTap(TapPosition tapPosition) { @@ -709,7 +738,7 @@ class InteractionDetectorState extends State widget.options.interactiveFlags, InteractiveFlag.doubleTapZoom)) { final centerZoom = _getNewEventCenterZoomPosition( _offsetToPoint(tapPosition.relative!), - _getZoomForScale(widget.mapState.zoom, 2)); + _getZoomForScale(widget.currentMapState().zoom, 2)); _startDoubleTapAnimation( centerZoom[1] as double, centerZoom[0] as LatLng); } @@ -722,23 +751,27 @@ class InteractionDetectorState extends State List _getNewEventCenterZoomPosition( CustomPoint cursorPos, double newZoom) { // Calculate offset of mouse cursor from viewport center - final viewCenter = widget.mapState.nonrotatedSize / 2; - final offset = (cursorPos - viewCenter).rotate(widget.mapState.rotationRad); + final viewCenter = widget.currentMapState().nonrotatedSize / 2; + final offset = + (cursorPos - viewCenter).rotate(widget.currentMapState().rotationRad); // Match new center coordinate to mouse cursor position - final scale = widget.mapState.getZoomScale(newZoom, widget.mapState.zoom); + final scale = widget + .currentMapState() + .getZoomScale(newZoom, widget.currentMapState().zoom); final newOffset = offset * (1.0 - 1.0 / scale); - final mapCenter = widget.mapState.project(widget.mapState.center); - final newCenter = widget.mapState.unproject(mapCenter + newOffset); + final mapCenter = + widget.currentMapState().project(widget.currentMapState().center); + final newCenter = widget.currentMapState().unproject(mapCenter + newOffset); return [newCenter, newZoom]; } void _startDoubleTapAnimation(double newZoom, LatLng newCenter) { _doubleTapZoomAnimation = - Tween(begin: widget.mapState.zoom, end: newZoom) + Tween(begin: widget.currentMapState().zoom, end: newZoom) .chain(CurveTween(curve: Curves.linear)) .animate(_doubleTapController); _doubleTapCenterAnimation = - LatLngTween(begin: widget.mapState.center, end: newCenter) + LatLngTween(begin: widget.currentMapState().center, end: newCenter) .chain(CurveTween(curve: Curves.linear)) .animate(_doubleTapController); _doubleTapController.forward(from: 0); @@ -780,7 +813,7 @@ class InteractionDetectorState extends State if (InteractiveFlag.hasFlag(flags, InteractiveFlag.pinchZoom)) { final verticalOffset = (_focalStartLocal - details.localFocalPoint).dy; final newZoom = - _mapZoomStart - verticalOffset / 360 * widget.mapState.zoom; + _mapZoomStart - verticalOffset / 360 * widget.currentMapState().zoom; widget.onOneFingerPinchZoom(MapEventSource.doubleTapHold, newZoom); } @@ -806,10 +839,10 @@ class InteractionDetectorState extends State _startListeningForAnimationInterruptions(); } - final newCenterPoint = widget.mapState.project(_mapCenterStart) + + final newCenterPoint = widget.currentMapState().project(_mapCenterStart) + _offsetToPoint(_flingAnimation.value) - .rotate(widget.mapState.rotationRad); - final newCenter = widget.mapState.unproject(newCenterPoint); + .rotate(widget.currentMapState().rotationRad); + final newCenter = widget.currentMapState().unproject(newCenterPoint); widget.onFlingUpdate(MapEventSource.flingAnimationController, newCenter); } @@ -838,11 +871,11 @@ class InteractionDetectorState extends State double _getZoomForScale(double startZoom, double scale) { final resultZoom = scale == 1.0 ? startZoom : startZoom + math.log(scale) / math.ln2; - return widget.mapState.fitZoomToBounds(resultZoom); + return widget.currentMapState().fitZoomToBounds(resultZoom); } Offset _rotateOffset(Offset offset) { - final radians = widget.mapState.rotationRad; + final radians = widget.currentMapState().rotationRad; if (radians != 0.0) { final cos = math.cos(radians); final sin = math.sin(radians); diff --git a/lib/src/map/flutter_map_state_container.dart b/lib/src/map/flutter_map_state_container.dart index eb6ee5f1d..73bc12bd1 100644 --- a/lib/src/map/flutter_map_state_container.dart +++ b/lib/src/map/flutter_map_state_container.dart @@ -54,7 +54,6 @@ class FlutterMapStateContainer extends State { _mapState = _mapState.withOptions(widget.options); } super.didUpdateWidget(oldWidget); - // TODO update the map state appropriately. } @override @@ -68,7 +67,8 @@ class FlutterMapStateContainer extends State { mapState: _mapState, child: InteractionDetector( key: _flutterMapGestureDetectorKey, - mapStateContainer: this, + options: widget.options, + currentMapState: () => _mapState, onPointerDown: _onPointerDown, onPointerUp: _onPointerUp, onPointerCancel: _onPointerCancel, From f389195944d74ec66a3bce9f13edc3a7f0e2b666 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Fri, 9 Jun 2023 11:31:51 +0200 Subject: [PATCH 04/46] Minor tidy-ups --- lib/src/gestures/interaction_detector.dart | 279 +++++++++---------- lib/src/gestures/map_events.dart | 1 - lib/src/map/controller.dart | 5 +- lib/src/map/flutter_map_state.dart | 23 +- lib/src/map/flutter_map_state_container.dart | 12 +- lib/src/misc/point.dart | 5 + 6 files changed, 148 insertions(+), 177 deletions(-) diff --git a/lib/src/gestures/interaction_detector.dart b/lib/src/gestures/interaction_detector.dart index 946f7e975..e4dffcd4f 100644 --- a/lib/src/gestures/interaction_detector.dart +++ b/lib/src/gestures/interaction_detector.dart @@ -86,6 +86,7 @@ class InteractionDetector extends StatefulWidget { class InteractionDetectorState extends State with TickerProviderStateMixin { static const int _kMinFlingVelocity = 800; + static const _kDoubleTapZoomDuration = 200; final _positionedTapController = PositionedTapController(); final _gestureArenaTeam = GestureArenaTeam(); @@ -130,7 +131,11 @@ class InteractionDetectorState extends State ..addListener(_handleFlingAnimation) ..addStatusListener(_flingAnimationStatusListener); _doubleTapController = AnimationController( - vsync: this, duration: const Duration(milliseconds: 200)) + vsync: this, + duration: const Duration( + milliseconds: _kDoubleTapZoomDuration, + ), + ) ..addListener(_handleDoubleTapZoomAnimation) ..addStatusListener(_doubleTapZoomStatusListener); } @@ -145,75 +150,6 @@ class InteractionDetectorState extends State super.didChangeDependencies(); } - Map _initializeGestures( - DeviceGestureSettings gestureSettings, { - required bool dragEnabled, - }) { - final Map gestures = - {}; - - gestures[TapGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(debugOwner: this), - (TapGestureRecognizer instance) { - instance - ..onTapDown = _positionedTapController.onTapDown - ..onTapUp = handleOnTapUp - ..onTap = _positionedTapController.onTap - ..onSecondaryTap = _positionedTapController.onSecondaryTap - ..onSecondaryTapDown = _positionedTapController.onTapDown; - }, - ); - - gestures[LongPressGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => LongPressGestureRecognizer(debugOwner: this), - (LongPressGestureRecognizer instance) { - instance.onLongPress = _positionedTapController.onLongPress; - }, - ); - - if (dragEnabled) { - gestures[VerticalDragGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => VerticalDragGestureRecognizer(debugOwner: this), - (VerticalDragGestureRecognizer instance) { - instance.onUpdate = (details) { - // Absorbing vertical drags - }; - instance.gestureSettings = gestureSettings; - instance.team ??= _gestureArenaTeam; - }, - ); - gestures[HorizontalDragGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => HorizontalDragGestureRecognizer(debugOwner: this), - (HorizontalDragGestureRecognizer instance) { - instance.onUpdate = (details) { - // Absorbing horizontal drags - }; - instance.gestureSettings = gestureSettings; - instance.team ??= _gestureArenaTeam; - }, - ); - } - - gestures[ScaleGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => ScaleGestureRecognizer(debugOwner: this), - (ScaleGestureRecognizer instance) { - instance - ..onStart = _handleScaleStart - ..onUpdate = _handleScaleUpdate - ..onEnd = _handleScaleEnd; - instance.team ??= _gestureArenaTeam; - _gestureArenaTeam.captain = instance; - }, - ); - - return gestures; - } - @override void didUpdateWidget(InteractionDetector oldWidget) { super.didUpdateWidget(oldWidget); @@ -237,10 +173,10 @@ class InteractionDetectorState extends State var emitMapEventMoveEnd = false; if (!InteractiveFlag.hasFlag(flags, InteractiveFlag.flingAnimation)) { - closeFlingAnimationController(MapEventSource.interactiveFlagsChanged); + _closeFlingAnimationController(MapEventSource.interactiveFlagsChanged); } if (!InteractiveFlag.hasFlag(flags, InteractiveFlag.doubleTapZoom)) { - closeDoubleTapController(MapEventSource.interactiveFlagsChanged); + _closeDoubleTapController(MapEventSource.interactiveFlagsChanged); } if (_rotationStarted && @@ -299,20 +235,89 @@ class InteractionDetectorState extends State super.dispose(); } + Map _initializeGestures( + DeviceGestureSettings gestureSettings, { + required bool dragEnabled, + }) { + final Map gestures = + {}; + + gestures[TapGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(debugOwner: this), + (TapGestureRecognizer instance) { + instance + ..onTapDown = _positionedTapController.onTapDown + ..onTapUp = _handleOnTapUp + ..onTap = _positionedTapController.onTap + ..onSecondaryTap = _positionedTapController.onSecondaryTap + ..onSecondaryTapDown = _positionedTapController.onTapDown; + }, + ); + + gestures[LongPressGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => LongPressGestureRecognizer(debugOwner: this), + (LongPressGestureRecognizer instance) { + instance.onLongPress = _positionedTapController.onLongPress; + }, + ); + + if (dragEnabled) { + gestures[VerticalDragGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => VerticalDragGestureRecognizer(debugOwner: this), + (VerticalDragGestureRecognizer instance) { + instance.onUpdate = (details) { + // Absorbing vertical drags + }; + instance.gestureSettings = gestureSettings; + instance.team ??= _gestureArenaTeam; + }, + ); + gestures[HorizontalDragGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => HorizontalDragGestureRecognizer(debugOwner: this), + (HorizontalDragGestureRecognizer instance) { + instance.onUpdate = (details) { + // Absorbing horizontal drags + }; + instance.gestureSettings = gestureSettings; + instance.team ??= _gestureArenaTeam; + }, + ); + } + + gestures[ScaleGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => ScaleGestureRecognizer(debugOwner: this), + (ScaleGestureRecognizer instance) { + instance + ..onStart = _handleScaleStart + ..onUpdate = _handleScaleUpdate + ..onEnd = _handleScaleEnd; + instance.team ??= _gestureArenaTeam; + _gestureArenaTeam.captain = instance; + }, + ); + + return gestures; + } + @override Widget build(BuildContext context) { return Listener( - onPointerDown: onPointerDown, - onPointerUp: onPointerUp, - onPointerCancel: onPointerCancel, - onPointerHover: onPointerHover, - onPointerSignal: onPointerSignal, + onPointerDown: _onPointerDown, + onPointerUp: _onPointerUp, + onPointerCancel: _onPointerCancel, + onPointerHover: _onPointerHover, + onPointerSignal: _onPointerSignal, child: PositionedTapDetector2( controller: _positionedTapController, - onTap: handleTap, - onSecondaryTap: handleSecondaryTap, + onTap: _handleTap, + onSecondaryTap: _handleSecondaryTap, onLongPress: handleLongPress, - onDoubleTap: handleDoubleTap, + onDoubleTap: _handleDoubleTap, doubleTapDelay: InteractiveFlag.hasFlag( widget.options.interactiveFlags, InteractiveFlag.doubleTapZoom, @@ -327,26 +332,26 @@ class InteractionDetectorState extends State ); } - void onPointerDown(PointerDownEvent event) { + void _onPointerDown(PointerDownEvent event) { ++_pointerCounter; widget.onPointerDown(event); } - void onPointerUp(PointerUpEvent event) { + void _onPointerUp(PointerUpEvent event) { --_pointerCounter; widget.onPointerUp(event); } - void onPointerCancel(PointerCancelEvent event) { + void _onPointerCancel(PointerCancelEvent event) { --_pointerCounter; widget.onPointerCancel(event); } - void onPointerHover(PointerHoverEvent event) { + void _onPointerHover(PointerHoverEvent event) { widget.onPointerHover(event); } - void onPointerSignal(PointerSignalEvent pointerSignal) { + void _onPointerSignal(PointerSignalEvent pointerSignal) { // Handle mouse scroll events if the enableScrollWheel parameter is enabled if (pointerSignal is PointerScrollEvent && widget.options.enableScrollWheel && @@ -390,7 +395,7 @@ class InteractionDetectorState extends State } } - void closeFlingAnimationController(MapEventSource source) { + void _closeFlingAnimationController(MapEventSource source) { _flingAnimationStarted = false; if (_flingController.isAnimating) { _flingController.stop(); @@ -401,7 +406,7 @@ class InteractionDetectorState extends State } } - void closeDoubleTapController(MapEventSource source) { + void _closeDoubleTapController(MapEventSource source) { if (_doubleTapController.isAnimating) { _doubleTapController.stop(); @@ -417,15 +422,15 @@ class InteractionDetectorState extends State final eventSource = _dragMode ? MapEventSource.dragStart : MapEventSource.multiFingerGestureStart; - closeFlingAnimationController(eventSource); - closeDoubleTapController(eventSource); + _closeFlingAnimationController(eventSource); + _closeDoubleTapController(eventSource); _gestureWinner = MultiFingerGesture.none; _mapZoomStart = widget.currentMapState().zoom; _mapCenterStart = widget.currentMapState().center; _focalStartLocal = _lastFocalLocal = details.localFocalPoint; - _focalStartLatLng = _offsetToCrs(_focalStartLocal); + _focalStartLatLng = widget.currentMapState().offsetToCrs(_focalStartLocal); _dragStarted = false; _pinchZoomStarted = false; @@ -554,7 +559,9 @@ class InteractionDetectorState extends State final oldCenterPt = widget .currentMapState() .project(widget.currentMapState().center, newZoom); - final newFocalLatLong = _offsetToCrs(_focalStartLocal, newZoom); + final newFocalLatLong = widget + .currentMapState() + .offsetToCrs(_focalStartLocal, newZoom); final newFocalPt = widget.currentMapState().project(newFocalLatLong, newZoom); final oldFocalPt = widget @@ -566,7 +573,7 @@ class InteractionDetectorState extends State final newCenterPt = oldCenterPt + zoomDifference + - _offsetToPoint(moveDifference); + moveDifference.toCustomPoint(); newCenter = widget.currentMapState().unproject(newCenterPt, newZoom); } else { @@ -592,9 +599,8 @@ class InteractionDetectorState extends State final oldCenterPt = widget .currentMapState() .project(widget.currentMapState().center); - final rotationCenter = widget - .currentMapState() - .project(_offsetToCrs(_lastFocalLocal)); + final rotationCenter = widget.currentMapState().project( + widget.currentMapState().offsetToCrs(_lastFocalLocal)); final vector = oldCenterPt - rotationCenter; final rotatedVector = vector.rotate(degToRadian(rotationDiff)); final newCenter = rotationCenter + rotatedVector; @@ -667,14 +673,14 @@ class InteractionDetectorState extends State )); } - void handleTap(TapPosition position) { - closeFlingAnimationController(MapEventSource.tap); - closeDoubleTapController(MapEventSource.tap); + void _handleTap(TapPosition position) { + _closeFlingAnimationController(MapEventSource.tap); + _closeDoubleTapController(MapEventSource.tap); final relativePosition = position.relative; if (relativePosition == null) return; - final latlng = _offsetToCrs(relativePosition); + final latlng = widget.currentMapState().offsetToCrs(relativePosition); final onTap = widget.options.onTap; if (onTap != null) { // emit the event @@ -683,14 +689,14 @@ class InteractionDetectorState extends State widget.onTap(MapEventSource.tap, latlng); } - void handleSecondaryTap(TapPosition position) { - closeFlingAnimationController(MapEventSource.secondaryTap); - closeDoubleTapController(MapEventSource.secondaryTap); + void _handleSecondaryTap(TapPosition position) { + _closeFlingAnimationController(MapEventSource.secondaryTap); + _closeDoubleTapController(MapEventSource.secondaryTap); final relativePosition = position.relative; if (relativePosition == null) return; - final latlng = _offsetToCrs(relativePosition); + final latlng = widget.currentMapState().offsetToCrs(relativePosition); final onSecondaryTap = widget.options.onSecondaryTap; if (onSecondaryTap != null) { // emit the event @@ -703,10 +709,10 @@ class InteractionDetectorState extends State void handleLongPress(TapPosition position) { _resetDoubleTapHold(); - closeFlingAnimationController(MapEventSource.longPress); - closeDoubleTapController(MapEventSource.longPress); + _closeFlingAnimationController(MapEventSource.longPress); + _closeDoubleTapController(MapEventSource.longPress); - final latlng = _offsetToCrs(position.relative!); + final latlng = widget.currentMapState().offsetToCrs(position.relative!); if (widget.options.onLongPress != null) { // emit the event widget.options.onLongPress!(position, latlng); @@ -715,56 +721,21 @@ class InteractionDetectorState extends State widget.onLongPress(MapEventSource.longPress, latlng); } - LatLng _offsetToCrs(Offset offset, [double? zoom]) { - final focalStartPt = widget.currentMapState().project( - widget.currentMapState().center, zoom ?? widget.currentMapState().zoom); - final point = (_offsetToPoint(offset) - - (widget.currentMapState().nonrotatedSize / 2.0)) - .rotate(widget.currentMapState().rotationRad); - - final newCenterPt = focalStartPt + point; - return widget - .currentMapState() - .unproject(newCenterPt, zoom ?? widget.currentMapState().zoom); - } - - void handleDoubleTap(TapPosition tapPosition) { + void _handleDoubleTap(TapPosition tapPosition) { _resetDoubleTapHold(); - closeFlingAnimationController(MapEventSource.doubleTap); - closeDoubleTapController(MapEventSource.doubleTap); + _closeFlingAnimationController(MapEventSource.doubleTap); + _closeDoubleTapController(MapEventSource.doubleTap); if (InteractiveFlag.hasFlag( widget.options.interactiveFlags, InteractiveFlag.doubleTapZoom)) { - final centerZoom = _getNewEventCenterZoomPosition( - _offsetToPoint(tapPosition.relative!), + final centerZoom = widget.currentMapState().getNewEventCenterZoomPosition( + tapPosition.relative!.toCustomPoint(), _getZoomForScale(widget.currentMapState().zoom, 2)); - _startDoubleTapAnimation( - centerZoom[1] as double, centerZoom[0] as LatLng); + _startDoubleTapAnimation(centerZoom.$2, centerZoom.$1); } } - // If we double click in the corner of a map, calculate the new - // center of the whole map after a zoom, to retain that offset position - // so that the same event LatLng is still under the cursor. - - List _getNewEventCenterZoomPosition( - CustomPoint cursorPos, double newZoom) { - // Calculate offset of mouse cursor from viewport center - final viewCenter = widget.currentMapState().nonrotatedSize / 2; - final offset = - (cursorPos - viewCenter).rotate(widget.currentMapState().rotationRad); - // Match new center coordinate to mouse cursor position - final scale = widget - .currentMapState() - .getZoomScale(newZoom, widget.currentMapState().zoom); - final newOffset = offset * (1.0 - 1.0 / scale); - final mapCenter = - widget.currentMapState().project(widget.currentMapState().center); - final newCenter = widget.currentMapState().unproject(mapCenter + newOffset); - return [newCenter, newZoom]; - } - void _startDoubleTapAnimation(double newZoom, LatLng newCenter) { _doubleTapZoomAnimation = Tween(begin: widget.currentMapState().zoom, end: newZoom) @@ -797,7 +768,7 @@ class InteractionDetectorState extends State ); } - void handleOnTapUp(TapUpDetails details) { + void _handleOnTapUp(TapUpDetails details) { _doubleTapHoldMaxDelay?.cancel(); if (++_tapUpCounter == 1) { @@ -840,7 +811,8 @@ class InteractionDetectorState extends State } final newCenterPoint = widget.currentMapState().project(_mapCenterStart) + - _offsetToPoint(_flingAnimation.value) + _flingAnimation.value + .toCustomPoint() .rotate(widget.currentMapState().rotationRad); final newCenter = widget.currentMapState().unproject(newCenterPoint); @@ -855,17 +827,14 @@ class InteractionDetectorState extends State _isListeningForInterruptions = false; } + // TODO Expose via controller. void handleAnimationInterruptions(MapEvent event) { if (_isListeningForInterruptions == false) { //Do not handle animation interruptions if not listening return; } - closeDoubleTapController(event.source); - closeFlingAnimationController(event.source); - } - - CustomPoint _offsetToPoint(Offset offset) { - return CustomPoint(offset.dx, offset.dy); + _closeDoubleTapController(event.source); + _closeFlingAnimationController(event.source); } double _getZoomForScale(double startZoom, double scale) { diff --git a/lib/src/gestures/map_events.dart b/lib/src/gestures/map_events.dart index 52e7fdb61..09a640ca2 100644 --- a/lib/src/gestures/map_events.dart +++ b/lib/src/gestures/map_events.dart @@ -43,7 +43,6 @@ abstract class MapEvent { /// Base event class which is emitted by MapController instance and /// includes information about camera movement -/// TODO: Change name to reflect that this is a base class for map events /// which are not partial (e.g start rotate, rotate, end rotate). abstract class MapEventWithMove extends MapEvent { final FlutterMapState oldMapState; diff --git a/lib/src/map/controller.dart b/lib/src/map/controller.dart index 0031278a0..f01827059 100644 --- a/lib/src/map/controller.dart +++ b/lib/src/map/controller.dart @@ -175,10 +175,7 @@ abstract class MapController { /// Not recommended for external usage. set state(FlutterMapStateContainer state); - /// Dispose of this controller by closing the [mapEventStream]'s - /// [StreamController] - /// - /// Not recommended for external usage. // TODO Why? + /// Dispose of this controller. void dispose(); } diff --git a/lib/src/map/flutter_map_state.dart b/lib/src/map/flutter_map_state.dart index e3f230cdb..1c62869aa 100644 --- a/lib/src/map/flutter_map_state.dart +++ b/lib/src/map/flutter_map_state.dart @@ -369,8 +369,8 @@ class FlutterMapState { _SafeArea? get _safeArea { if (zoom != _safeAreaZoom || _safeAreaCache == null) { _safeAreaZoom = zoom; - final halfScreenHeight = calculateScreenHeightInDegrees() / 2; - final halfScreenWidth = calculateScreenWidthInDegrees() / 2; + final halfScreenHeight = _calculateScreenHeightInDegrees() / 2; + final halfScreenWidth = _calculateScreenWidthInDegrees() / 2; final southWestLatitude = options.swPanBoundary!.latitude + halfScreenHeight; final southWestLongitude = @@ -393,29 +393,28 @@ class FlutterMapState { return _safeAreaCache; } - double calculateScreenWidthInDegrees() { + double _calculateScreenWidthInDegrees() { final degreesPerPixel = 360 / math.pow(2, zoom + 8); return options.screenSize!.width * degreesPerPixel; } - double calculateScreenHeightInDegrees() => + double _calculateScreenHeightInDegrees() => options.screenSize!.height * 170.102258 / math.pow(2, zoom + 8); LatLng offsetToCrs(Offset offset, [double? zoom]) { final focalStartPt = project(center, zoom ?? this.zoom); final point = - (offsetToPoint(offset) - (nonrotatedSize / 2.0)).rotate(rotationRad); + (offset.toCustomPoint() - (nonrotatedSize / 2.0)).rotate(rotationRad); final newCenterPt = focalStartPt + point; return unproject(newCenterPt, zoom ?? this.zoom); } - // TODO This can be an extension on offset or a constructor on CustomPoint. - CustomPoint offsetToPoint(Offset offset) { - return CustomPoint(offset.dx, offset.dy); - } - - List getNewEventCenterZoomPosition( + // TODO better description + // If we double click in the corner of a map, calculate the new + // center of the whole map after a zoom, to retain that offset position + // so that the same event LatLng is still under the cursor. + (LatLng, double) getNewEventCenterZoomPosition( CustomPoint cursorPos, double newZoom) { // Calculate offset of mouse cursor from viewport center final viewCenter = nonrotatedSize / 2; @@ -425,7 +424,7 @@ class FlutterMapState { final newOffset = offset * (1.0 - 1.0 / scale); final mapCenter = project(center); final newCenter = unproject(mapCenter + newOffset); - return [newCenter, newZoom]; + return (newCenter, newZoom); } LatLng? adjustCenterIfOutsideMaxBounds( diff --git a/lib/src/map/flutter_map_state_container.dart b/lib/src/map/flutter_map_state_container.dart index 73bc12bd1..d83d0f51d 100644 --- a/lib/src/map/flutter_map_state_container.dart +++ b/lib/src/map/flutter_map_state_container.dart @@ -166,13 +166,15 @@ class FlutterMapStateContainer extends State { event.scrollDelta.dy * widget.options.scrollWheelVelocity) .clamp(minZoom, maxZoom); // Calculate offset of mouse cursor from viewport center - final List newCenterZoom = _mapState.getNewEventCenterZoomPosition( - _mapState.offsetToPoint(event.localPosition), newZoom); + final newCenterZoom = _mapState.getNewEventCenterZoomPosition( + event.localPosition.toCustomPoint(), + newZoom, + ); // Move to new center and zoom level move( - newCenterZoom[0] as LatLng, - newCenterZoom[1] as double, + newCenterZoom.$1, + newCenterZoom.$2, source: MapEventSource.scrollWheel, ); } @@ -228,7 +230,7 @@ class FlutterMapStateContainer extends State { void _onDragUpdate(MapEventSource source, Offset offset) { final oldCenterPt = _mapState.project(_mapState.center); - final newCenterPt = oldCenterPt + _mapState.offsetToPoint(offset); + final newCenterPt = oldCenterPt + offset.toCustomPoint(); final newCenter = _mapState.unproject(newCenterPt); move(newCenter, _mapState.zoom, hasGesture: true, source: source); diff --git a/lib/src/misc/point.dart b/lib/src/misc/point.dart index c66b436be..2077bb6ee 100644 --- a/lib/src/misc/point.dart +++ b/lib/src/misc/point.dart @@ -1,4 +1,5 @@ import 'dart:math' as math; +import 'dart:ui'; /// Data represenation of point located on map instance /// where [x] is horizontal and [y] is vertical pixel value @@ -81,3 +82,7 @@ class CustomPoint extends math.Point { @override String toString() => 'CustomPoint ($x, $y)'; } + +extension OffsetToCustomPointExtension on Offset { + CustomPoint toCustomPoint() => CustomPoint(dx, dy); +} From ff484a1ea161882277a3bb69c7caa64ced8e876a Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Fri, 9 Jun 2023 12:39:26 +0200 Subject: [PATCH 05/46] Re-instate linking of MapController to map state --- .../ineraction_detector_controller.dart | 6 +++++ lib/src/map/flutter_map_state_container.dart | 25 ++++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 lib/src/gestures/ineraction_detector_controller.dart diff --git a/lib/src/gestures/ineraction_detector_controller.dart b/lib/src/gestures/ineraction_detector_controller.dart new file mode 100644 index 000000000..9c42e96db --- /dev/null +++ b/lib/src/gestures/ineraction_detector_controller.dart @@ -0,0 +1,6 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; + +class InteractionDetectorController extends ValueNotifier { + InteractionDetectorController(super.value); +} diff --git a/lib/src/map/flutter_map_state_container.dart b/lib/src/map/flutter_map_state_container.dart index d83d0f51d..67e9212b3 100644 --- a/lib/src/map/flutter_map_state_container.dart +++ b/lib/src/map/flutter_map_state_container.dart @@ -15,8 +15,8 @@ class FlutterMapStateContainer extends State { bool _hasFitInitialBounds = false; - final _localController = MapController(); - MapController get mapController => widget.mapController ?? _localController; + late bool _mapControllerCreatedInternally; + late MapController _mapController; late FlutterMapState _mapState; @@ -30,9 +30,16 @@ class FlutterMapStateContainer extends State { double get rotation => _mapState.rotation; + void _initializeMapController() { + _mapController = widget.mapController ?? MapController(); + _mapControllerCreatedInternally = widget.mapController == null; + } + @override void initState() { super.initState(); + _initializeMapController(); + _mapController.state = this; WidgetsBinding.instance .addPostFrameCallback((_) => widget.options.onMapReady?.call()); @@ -53,9 +60,19 @@ class FlutterMapStateContainer extends State { if (oldWidget.options != widget.options) { _mapState = _mapState.withOptions(widget.options); } + if (oldWidget.mapController != widget.mapController) { + _initializeMapController(); + _mapController.state = this; + } super.didUpdateWidget(oldWidget); } + @override + void dispose() { + if (_mapControllerCreatedInternally) _mapController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return LayoutBuilder( @@ -63,7 +80,7 @@ class FlutterMapStateContainer extends State { _onConstraintsChange(constraints); return MapStateInheritedWidget( - mapController: mapController, + mapController: _mapController, mapState: _mapState, child: InteractionDetector( key: _flutterMapGestureDetectorKey, @@ -389,7 +406,7 @@ class FlutterMapStateContainer extends State { widget.options.onMapEvent?.call(event); - mapController.mapEventSink.add(event); + _mapController.mapEventSink.add(event); } bool rotate( From 0746a0b91f4152176e57d8cf6665ebd39d54af5e Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Sat, 10 Jun 2023 01:28:58 +0200 Subject: [PATCH 06/46] Trigger all FlutterMapState manipulations via FlutterMapStateController --- example/lib/pages/epsg4326_crs.dart | 2 +- example/lib/pages/latlng_to_screen_point.dart | 13 +- example/lib/pages/plugin_scalebar.dart | 15 +- example/lib/pages/plugin_zoombuttons.dart | 39 +- example/lib/pages/point_to_latlng.dart | 2 +- example/lib/pages/wms_tile_layer.dart | 2 +- lib/flutter_map.dart | 16 +- lib/plugin_api.dart | 3 +- ...rt => flutter_map_interactive_viewer.dart} | 375 +++++------ .../flutter_map_state_controller.dart | 479 ++++++++++++++ .../ineraction_detector_controller.dart | 6 - lib/src/map/flutter_map_state.dart | 249 +++----- lib/src/map/flutter_map_state_container.dart | 601 ++---------------- ...lutter_map_state_controller_interface.dart | 73 +++ .../flutter_map_state_inherited_widget.dart | 2 +- .../{controller.dart => map_controller.dart} | 137 +--- lib/src/map/map_controller_impl.dart | 144 +++++ lib/src/map/map_safe_area.dart | 89 +++ lib/src/map/options.dart | 15 +- test/flutter_map_test.dart | 4 +- 20 files changed, 1196 insertions(+), 1070 deletions(-) rename lib/src/gestures/{interaction_detector.dart => flutter_map_interactive_viewer.dart} (72%) create mode 100644 lib/src/gestures/flutter_map_state_controller.dart delete mode 100644 lib/src/gestures/ineraction_detector_controller.dart create mode 100644 lib/src/map/flutter_map_state_controller_interface.dart rename lib/src/map/{controller.dart => map_controller.dart} (66%) create mode 100644 lib/src/map/map_controller_impl.dart create mode 100644 lib/src/map/map_safe_area.dart diff --git a/example/lib/pages/epsg4326_crs.dart b/example/lib/pages/epsg4326_crs.dart index 544967441..c13d6d6ad 100644 --- a/example/lib/pages/epsg4326_crs.dart +++ b/example/lib/pages/epsg4326_crs.dart @@ -37,7 +37,7 @@ class EPSG4326Page extends StatelessWidget { layers: ['TOPO-OSM-WMS'], ), userAgentPackageName: 'dev.fleaflet.flutter_map.example', - ) + ), ], ), ), diff --git a/example/lib/pages/latlng_to_screen_point.dart b/example/lib/pages/latlng_to_screen_point.dart index 7e6497576..ce9799f04 100644 --- a/example/lib/pages/latlng_to_screen_point.dart +++ b/example/lib/pages/latlng_to_screen_point.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map_example/widgets/drawer.dart'; import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map_example/widgets/drawer.dart'; import 'package:latlong2/latlong.dart'; class LatLngScreenPointTestPage extends StatefulWidget { @@ -36,9 +36,10 @@ class _LatLngScreenPointTestPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('LatLng To Screen Point')), - drawer: buildDrawer(context, LatLngScreenPointTestPage.route), - body: Stack(children: [ + appBar: AppBar(title: const Text('LatLng To Screen Point')), + drawer: buildDrawer(context, LatLngScreenPointTestPage.route), + body: Stack( + children: [ Padding( padding: const EdgeInsets.all(8), child: FlutterMap( @@ -68,6 +69,8 @@ class _LatLngScreenPointTestPageState extends State { width: 20, height: 20, child: const FlutterLogo()) - ])); + ], + ), + ); } } diff --git a/example/lib/pages/plugin_scalebar.dart b/example/lib/pages/plugin_scalebar.dart index edbe74049..d3d72ab83 100644 --- a/example/lib/pages/plugin_scalebar.dart +++ b/example/lib/pages/plugin_scalebar.dart @@ -26,13 +26,14 @@ class PluginScaleBar extends StatelessWidget { ), nonRotatedChildren: [ ScaleLayerWidget( - options: ScaleLayerPluginOption( - lineColor: Colors.blue, - lineWidth: 2, - textStyle: - const TextStyle(color: Colors.blue, fontSize: 12), - padding: const EdgeInsets.all(10), - )), + options: ScaleLayerPluginOption( + lineColor: Colors.blue, + lineWidth: 2, + textStyle: + const TextStyle(color: Colors.blue, fontSize: 12), + padding: const EdgeInsets.all(10), + ), + ), ], children: [ TileLayer( diff --git a/example/lib/pages/plugin_zoombuttons.dart b/example/lib/pages/plugin_zoombuttons.dart index 3c95f1fe2..46aa917b6 100644 --- a/example/lib/pages/plugin_zoombuttons.dart +++ b/example/lib/pages/plugin_zoombuttons.dart @@ -20,26 +20,27 @@ class PluginZoomButtons extends StatelessWidget { children: [ Flexible( child: FlutterMap( - options: MapOptions( - center: const LatLng(51.5, -0.09), - zoom: 5, + options: MapOptions( + center: const LatLng(51.5, -0.09), + zoom: 5, + ), + nonRotatedChildren: const [ + FlutterMapZoomButtons( + minZoom: 4, + maxZoom: 19, + mini: true, + padding: 10, + alignment: Alignment.bottomRight, ), - nonRotatedChildren: const [ - FlutterMapZoomButtons( - minZoom: 4, - maxZoom: 19, - mini: true, - padding: 10, - alignment: Alignment.bottomRight, - ), - ], - children: [ - TileLayer( - urlTemplate: - 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'dev.fleaflet.flutter_map.example', - ), - ]), + ], + children: [ + TileLayer( + urlTemplate: + 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'dev.fleaflet.flutter_map.example', + ) + ], + ), ), ], ), diff --git a/example/lib/pages/point_to_latlng.dart b/example/lib/pages/point_to_latlng.dart index c00c62e0e..10f3ac215 100644 --- a/example/lib/pages/point_to_latlng.dart +++ b/example/lib/pages/point_to_latlng.dart @@ -78,7 +78,7 @@ class PointToLatlngPage extends State { builder: (ctx) => const FlutterLogo(), ) ], - ) + ), ], ), Container( diff --git a/example/lib/pages/wms_tile_layer.dart b/example/lib/pages/wms_tile_layer.dart index 1459e3598..fed2db762 100644 --- a/example/lib/pages/wms_tile_layer.dart +++ b/example/lib/pages/wms_tile_layer.dart @@ -58,7 +58,7 @@ class WMSLayerPage extends StatelessWidget { ), subdomains: const ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', - ) + ), ], ), ), diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index 038466ee6..949a792b1 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -1,11 +1,5 @@ library flutter_map; -export 'package:flutter_map/src/misc/center_zoom.dart'; -export 'package:flutter_map/src/misc/fit_bounds_options.dart'; -export 'package:flutter_map/src/misc/move_and_rotate_result.dart'; -export 'package:flutter_map/src/misc/point.dart'; -export 'package:flutter_map/src/misc/position.dart'; -export 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; export 'package:flutter_map/src/geo/crs.dart'; export 'package:flutter_map/src/geo/latlng_bounds.dart'; export 'package:flutter_map/src/gestures/interactive_flag.dart'; @@ -32,6 +26,12 @@ export 'package:flutter_map/src/layer/tile_layer/tile_provider/file_providers/ti export 'package:flutter_map/src/layer/tile_layer/tile_provider/network_tile_provider.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_update_event.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_update_transformer.dart'; -export 'package:flutter_map/src/map/controller.dart' hide MapControllerImpl; -export 'package:flutter_map/src/map/widget.dart'; +export 'package:flutter_map/src/map/map_controller.dart'; export 'package:flutter_map/src/map/options.dart'; +export 'package:flutter_map/src/map/widget.dart'; +export 'package:flutter_map/src/misc/center_zoom.dart'; +export 'package:flutter_map/src/misc/fit_bounds_options.dart'; +export 'package:flutter_map/src/misc/move_and_rotate_result.dart'; +export 'package:flutter_map/src/misc/point.dart'; +export 'package:flutter_map/src/misc/position.dart'; +export 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; diff --git a/lib/plugin_api.dart b/lib/plugin_api.dart index e09f3fa65..e1fbcaa87 100644 --- a/lib/plugin_api.dart +++ b/lib/plugin_api.dart @@ -1,9 +1,8 @@ library flutter_map.plugin_api; export 'package:flutter_map/flutter_map.dart'; -// ignore: invalid_export_of_internal_element -export 'package:flutter_map/src/map/controller.dart' show MapControllerImpl; export 'package:flutter_map/src/map/flutter_map_state.dart'; +export 'package:flutter_map/src/map/map_controller.dart'; export 'package:flutter_map/src/misc/private/bounds.dart'; export 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; export 'package:flutter_map/src/misc/private/util.dart'; diff --git a/lib/src/gestures/interaction_detector.dart b/lib/src/gestures/flutter_map_interactive_viewer.dart similarity index 72% rename from lib/src/gestures/interaction_detector.dart rename to lib/src/gestures/flutter_map_interactive_viewer.dart index e4dffcd4f..43b3434b6 100644 --- a/lib/src/gestures/interaction_detector.dart +++ b/lib/src/gestures/flutter_map_interactive_viewer.dart @@ -3,6 +3,7 @@ import 'dart:math' as math; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_map/src/gestures/flutter_map_state_controller.dart'; import 'package:flutter_map/src/gestures/interactive_flag.dart'; import 'package:flutter_map/src/gestures/latlng_tween.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; @@ -13,78 +14,25 @@ import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; import 'package:latlong2/latlong.dart'; -class InteractionDetector extends StatefulWidget { - final Widget child; +class FlutterMapInteractiveViewer extends StatefulWidget { + final Widget Function(BuildContext context, FlutterMapState mapState) builder; final MapOptions options; - // This needs to be passed as a callback rather than just passing the state - // otherwise the map lags significantly. - final FlutterMapState Function() currentMapState; - final void Function(PointerDownEvent event) onPointerDown; - final void Function(PointerUpEvent event) onPointerUp; - final void Function(PointerCancelEvent event) onPointerCancel; - final void Function(PointerHoverEvent event) onPointerHover; - final void Function(PointerScrollEvent event) onScroll; - final void Function(MapEventSource source, double newZoom) - onOneFingerPinchZoom; - final void Function(MapEventSource source) onRotateEnd; - final void Function(MapEventSource source) onMoveEnd; - final void Function(MapEventSource source) onFlingStart; - final void Function(MapEventSource source) onFlingEnd; - final void Function(MapEventSource source) onDoubleTapZoomEnd; - final void Function(MapEventSource source) onMoveStart; - final void Function(MapEventSource source) onRotateStart; - final void Function(MapEventSource source) onFlingNotStarted; - final void Function(MapEventSource source, LatLng position) onTap; - final void Function(MapEventSource source, LatLng position) onSecondaryTap; - final void Function(MapEventSource source, LatLng position) onLongPress; - final void Function(MapEventSource source, Offset offset) onDragUpdate; - final void Function(MapEventSource source, LatLng position, double zoom) - onPinchZoomUpdate; - final void Function(MapEventSource source, LatLng position, double zoom) - onDoubleTapZoomUpdate; - final void Function( - MapEventSource source, LatLng position, double zoom, double rotation) - onRotateUpdate; - - final void Function(MapEventSource source, LatLng position) onFlingUpdate; - final void Function(MapEventSource source) onDoubleTapZoomStart; - - const InteractionDetector({ + final FlutterMapStateController controller; + + const FlutterMapInteractiveViewer({ super.key, - required this.child, + required this.builder, required this.options, - required this.currentMapState, - required this.onPointerDown, - required this.onPointerUp, - required this.onPointerCancel, - required this.onPointerHover, - required this.onRotateEnd, - required this.onMoveEnd, - required this.onFlingEnd, - required this.onFlingStart, - required this.onDoubleTapZoomEnd, - required this.onMoveStart, - required this.onDragUpdate, - required this.onRotateStart, - required this.onFlingNotStarted, - required this.onPinchZoomUpdate, - required this.onTap, - required this.onRotateUpdate, - required this.onSecondaryTap, - required this.onLongPress, - required this.onDoubleTapZoomStart, - required this.onScroll, - required this.onOneFingerPinchZoom, - required this.onDoubleTapZoomUpdate, - required this.onFlingUpdate, + required this.controller, }); @override - State createState() => InteractionDetectorState(); + State createState() => + FlutterMapInteractiveViewerState(); } -class InteractionDetectorState extends State - with TickerProviderStateMixin { +class FlutterMapInteractiveViewerState + extends State with TickerProviderStateMixin { static const int _kMinFlingVelocity = 800; static const _kDoubleTapZoomDuration = 200; @@ -127,6 +75,8 @@ class InteractionDetectorState extends State @override void initState() { super.initState(); + widget.controller.interactiveViewerState = this; + widget.controller.addListener(_onMapStateChange); _flingController = AnimationController(vsync: this) ..addListener(_handleFlingAnimation) ..addStatusListener(_flingAnimationStatusListener); @@ -140,6 +90,10 @@ class InteractionDetectorState extends State ..addStatusListener(_doubleTapZoomStatusListener); } + void _onMapStateChange() { + setState(() {}); + } + @override void didChangeDependencies() { _gestures = _initializeGestures( @@ -151,7 +105,7 @@ class InteractionDetectorState extends State } @override - void didUpdateWidget(InteractionDetector oldWidget) { + void didUpdateWidget(FlutterMapInteractiveViewer oldWidget) { super.didUpdateWidget(oldWidget); final oldFlags = oldWidget.options.interactiveFlags; @@ -189,7 +143,7 @@ class InteractionDetectorState extends State _gestureWinner = MultiFingerGesture.none; } - widget.onRotateEnd(MapEventSource.interactiveFlagsChanged); + widget.controller.rotateEnded(MapEventSource.interactiveFlagsChanged); } if (_pinchZoomStarted && @@ -223,15 +177,17 @@ class InteractionDetectorState extends State } if (emitMapEventMoveEnd) { - widget.onMoveEnd(MapEventSource.interactiveFlagsChanged); + widget.controller.moveEnded(MapEventSource.interactiveFlagsChanged); } } } @override void dispose() { + widget.controller.removeListener(_onMapStateChange); _flingController.dispose(); _doubleTapController.dispose(); + super.dispose(); } @@ -316,7 +272,7 @@ class InteractionDetectorState extends State controller: _positionedTapController, onTap: _handleTap, onSecondaryTap: _handleSecondaryTap, - onLongPress: handleLongPress, + onLongPress: _handleLongPress, onDoubleTap: _handleDoubleTap, doubleTapDelay: InteractiveFlag.hasFlag( widget.options.interactiveFlags, @@ -326,7 +282,7 @@ class InteractionDetectorState extends State : Duration.zero, child: RawGestureDetector( gestures: _gestures, - child: widget.child, + child: widget.builder(context, widget.controller.value), ), ), ); @@ -334,21 +290,36 @@ class InteractionDetectorState extends State void _onPointerDown(PointerDownEvent event) { ++_pointerCounter; - widget.onPointerDown(event); + + if (widget.options.onPointerDown != null) { + final latlng = widget.controller.value.offsetToCrs(event.localPosition); + widget.options.onPointerDown!(event, latlng); + } } void _onPointerUp(PointerUpEvent event) { --_pointerCounter; - widget.onPointerUp(event); + + if (widget.options.onPointerUp != null) { + final latlng = widget.controller.value.offsetToCrs(event.localPosition); + widget.options.onPointerUp!(event, latlng); + } } void _onPointerCancel(PointerCancelEvent event) { --_pointerCounter; - widget.onPointerCancel(event); + + if (widget.options.onPointerCancel != null) { + final latlng = widget.controller.value.offsetToCrs(event.localPosition); + widget.options.onPointerCancel!(event, latlng); + } } void _onPointerHover(PointerHoverEvent event) { - widget.onPointerHover(event); + if (widget.options.onPointerHover != null) { + final latlng = widget.controller.value.offsetToCrs(event.localPosition); + widget.options.onPointerHover!(event, latlng); + } } void _onPointerSignal(PointerSignalEvent pointerSignal) { @@ -360,7 +331,28 @@ class InteractionDetectorState extends State // [PointerSignalResolver] documentation for more information. GestureBinding.instance.pointerSignalResolver.register( pointerSignal, - (pointerSignal) => widget.onScroll(pointerSignal as PointerScrollEvent), + (pointerSignal) { + pointerSignal as PointerScrollEvent; + final minZoom = widget.options.minZoom ?? 0.0; + final maxZoom = widget.options.maxZoom ?? double.infinity; + final newZoom = (widget.controller.value.zoom - + pointerSignal.scrollDelta.dy * + widget.options.scrollWheelVelocity) + .clamp(minZoom, maxZoom); + // Calculate offset of mouse cursor from viewport center + final newCenter = widget.controller.value.focusedZoomCenter( + pointerSignal.localPosition.toCustomPoint(), + newZoom, + ); + widget.controller.move( + newCenter, + newZoom, + offset: Offset.zero, + hasGesture: false, + source: MapEventSource.scrollWheel, + id: null, + ); + }, ); } } @@ -375,8 +367,10 @@ class InteractionDetectorState extends State } } - int _getMultiFingerGestureFlags( - {int? gestureWinner, MapOptions? mapOptions}) { + int _getMultiFingerGestureFlags({ + int? gestureWinner, + MapOptions? mapOptions, + }) { gestureWinner ??= _gestureWinner; mapOptions ??= widget.options; @@ -402,7 +396,7 @@ class InteractionDetectorState extends State _stopListeningForAnimationInterruptions(); - widget.onFlingEnd(source); + widget.controller.flingEnded(source); } } @@ -412,7 +406,7 @@ class InteractionDetectorState extends State _stopListeningForAnimationInterruptions(); - widget.onDoubleTapZoomEnd(source); + widget.controller.doubleTapZoomEnded(source); } } @@ -427,10 +421,10 @@ class InteractionDetectorState extends State _gestureWinner = MultiFingerGesture.none; - _mapZoomStart = widget.currentMapState().zoom; - _mapCenterStart = widget.currentMapState().center; + _mapZoomStart = widget.controller.value.zoom; + _mapCenterStart = widget.controller.value.center; _focalStartLocal = _lastFocalLocal = details.localFocalPoint; - _focalStartLatLng = widget.currentMapState().offsetToCrs(_focalStartLocal); + _focalStartLatLng = widget.controller.value.offsetToCrs(_focalStartLocal); _dragStarted = false; _pinchZoomStarted = false; @@ -464,13 +458,13 @@ class InteractionDetectorState extends State // [didUpdateWidget] will emit MapEventMoveEnd and if drag is enabled // again then this will emit the start event again. _dragStarted = true; - widget.onMoveStart(eventSource); + widget.controller.moveStarted(eventSource); } final localDistanceOffset = _rotateOffset(_lastFocalLocal - focalOffset); - widget.onDragUpdate(eventSource, localDistanceOffset); + widget.controller.dragUpdated(eventSource, localDistanceOffset); } } else { final hasIntPinchMove = @@ -536,12 +530,12 @@ class InteractionDetectorState extends State if (!_pinchMoveStarted) { // emit MoveStart event only if pinchMove hasn't started - widget.onMoveStart(eventSource); + widget.controller.moveStarted(eventSource); } } } } else { - newZoom = widget.currentMapState().zoom; + newZoom = widget.controller.value.zoom; } LatLng newCenter; @@ -551,22 +545,19 @@ class InteractionDetectorState extends State if (!_pinchZoomStarted) { // emit MoveStart event only if pinchZoom hasn't started - widget.onMoveStart(eventSource); + widget.controller.moveStarted(eventSource); } } if (_pinchZoomStarted || _pinchMoveStarted) { - final oldCenterPt = widget - .currentMapState() - .project(widget.currentMapState().center, newZoom); - final newFocalLatLong = widget - .currentMapState() + final oldCenterPt = widget.controller.value + .project(widget.controller.value.center, newZoom); + final newFocalLatLong = widget.controller.value .offsetToCrs(_focalStartLocal, newZoom); final newFocalPt = - widget.currentMapState().project(newFocalLatLong, newZoom); - final oldFocalPt = widget - .currentMapState() - .project(_focalStartLatLng, newZoom); + widget.controller.value.project(newFocalLatLong, newZoom); + final oldFocalPt = + widget.controller.value.project(_focalStartLatLng, newZoom); final zoomDifference = oldFocalPt - newFocalPt; final moveDifference = _rotateOffset(_focalStartLocal - _lastFocalLocal); @@ -575,41 +566,50 @@ class InteractionDetectorState extends State zoomDifference + moveDifference.toCustomPoint(); newCenter = - widget.currentMapState().unproject(newCenterPt, newZoom); + widget.controller.value.unproject(newCenterPt, newZoom); } else { - newCenter = widget.currentMapState().center; + newCenter = widget.controller.value.center; } } else { - newCenter = widget.currentMapState().center; + newCenter = widget.controller.value.center; } if (_pinchZoomStarted || _pinchMoveStarted) { - widget.onPinchZoomUpdate(eventSource, newCenter, newZoom); + widget.controller.move( + newCenter, + newZoom, + offset: Offset.zero, + hasGesture: true, + source: eventSource, + id: null, + ); } } if (hasRotate) { if (!_rotationStarted && currentRotation != 0.0) { _rotationStarted = true; - widget.onRotateStart(eventSource); + widget.controller.rotateStarted(eventSource); } if (_rotationStarted) { final rotationDiff = currentRotation - _lastRotation; - final oldCenterPt = widget - .currentMapState() - .project(widget.currentMapState().center); - final rotationCenter = widget.currentMapState().project( - widget.currentMapState().offsetToCrs(_lastFocalLocal)); + final oldCenterPt = widget.controller.value + .project(widget.controller.value.center); + final rotationCenter = widget.controller.value.project( + widget.controller.value.offsetToCrs(_lastFocalLocal)); final vector = oldCenterPt - rotationCenter; final rotatedVector = vector.rotate(degToRadian(rotationDiff)); final newCenter = rotationCenter + rotatedVector; - widget.onRotateUpdate( - eventSource, - widget.currentMapState().unproject(newCenter), - widget.currentMapState().zoom, - widget.currentMapState().rotation + rotationDiff, + widget.controller.moveAndRotate( + widget.controller.value.unproject(newCenter), + widget.controller.value.zoom, + widget.controller.value.rotation + rotationDiff, + offset: Offset.zero, + hasGesture: true, + source: eventSource, + id: null, ); } } @@ -630,12 +630,12 @@ class InteractionDetectorState extends State if (_rotationStarted) { _rotationStarted = false; - widget.onRotateEnd(eventSource); + widget.controller.rotateEnded(eventSource); } if (_dragStarted || _pinchZoomStarted || _pinchMoveStarted) { _dragStarted = _pinchZoomStarted = _pinchMoveStarted = false; - widget.onMoveEnd(eventSource); + widget.controller.moveEnded(eventSource); } final hasFling = InteractiveFlag.hasFlag( @@ -643,17 +643,14 @@ class InteractionDetectorState extends State final magnitude = details.velocity.pixelsPerSecond.distance; if (magnitude < _kMinFlingVelocity || !hasFling) { - if (hasFling) { - widget.onFlingNotStarted(eventSource); - } - + if (hasFling) widget.controller.flingNotStarted(eventSource); return; } final direction = details.velocity.pixelsPerSecond / magnitude; final distance = (Offset.zero & - Size(widget.currentMapState().nonrotatedSize.x, - widget.currentMapState().nonrotatedSize.y)) + Size(widget.controller.value.nonRotatedSize.x, + widget.controller.value.nonRotatedSize.y)) .shortestSide; final flingOffset = _focalStartLocal - _lastFocalLocal; @@ -680,13 +677,11 @@ class InteractionDetectorState extends State final relativePosition = position.relative; if (relativePosition == null) return; - final latlng = widget.currentMapState().offsetToCrs(relativePosition); - final onTap = widget.options.onTap; - if (onTap != null) { - // emit the event - onTap(position, latlng); - } - widget.onTap(MapEventSource.tap, latlng); + widget.controller.tapped( + MapEventSource.tap, + position, + widget.controller.value.offsetToCrs(relativePosition), + ); } void _handleSecondaryTap(TapPosition position) { @@ -696,29 +691,24 @@ class InteractionDetectorState extends State final relativePosition = position.relative; if (relativePosition == null) return; - final latlng = widget.currentMapState().offsetToCrs(relativePosition); - final onSecondaryTap = widget.options.onSecondaryTap; - if (onSecondaryTap != null) { - // emit the event - onSecondaryTap(position, latlng); - } - - widget.onSecondaryTap(MapEventSource.secondaryTap, latlng); + widget.controller.secondaryTapped( + MapEventSource.secondaryTap, + position, + widget.controller.value.offsetToCrs(relativePosition), + ); } - void handleLongPress(TapPosition position) { + void _handleLongPress(TapPosition position) { _resetDoubleTapHold(); _closeFlingAnimationController(MapEventSource.longPress); _closeDoubleTapController(MapEventSource.longPress); - final latlng = widget.currentMapState().offsetToCrs(position.relative!); - if (widget.options.onLongPress != null) { - // emit the event - widget.options.onLongPress!(position, latlng); - } - - widget.onLongPress(MapEventSource.longPress, latlng); + widget.controller.longPressed( + MapEventSource.longPress, + position, + widget.controller.value.offsetToCrs(position.relative!), + ); } void _handleDoubleTap(TapPosition tapPosition) { @@ -729,20 +719,22 @@ class InteractionDetectorState extends State if (InteractiveFlag.hasFlag( widget.options.interactiveFlags, InteractiveFlag.doubleTapZoom)) { - final centerZoom = widget.currentMapState().getNewEventCenterZoomPosition( - tapPosition.relative!.toCustomPoint(), - _getZoomForScale(widget.currentMapState().zoom, 2)); - _startDoubleTapAnimation(centerZoom.$2, centerZoom.$1); + final newZoom = _getZoomForScale(widget.controller.zoom, 2); + final newCenter = widget.controller.value.focusedZoomCenter( + tapPosition.relative!.toCustomPoint(), + newZoom, + ); + _startDoubleTapAnimation(newZoom, newCenter); } } void _startDoubleTapAnimation(double newZoom, LatLng newCenter) { _doubleTapZoomAnimation = - Tween(begin: widget.currentMapState().zoom, end: newZoom) + Tween(begin: widget.controller.value.zoom, end: newZoom) .chain(CurveTween(curve: Curves.linear)) .animate(_doubleTapController); _doubleTapCenterAnimation = - LatLngTween(begin: widget.currentMapState().center, end: newCenter) + LatLngTween(begin: widget.controller.value.center, end: newCenter) .chain(CurveTween(curve: Curves.linear)) .animate(_doubleTapController); _doubleTapController.forward(from: 0); @@ -750,21 +742,27 @@ class InteractionDetectorState extends State void _doubleTapZoomStatusListener(AnimationStatus status) { if (status == AnimationStatus.forward) { - widget.onDoubleTapZoomStart( - MapEventSource.doubleTapZoomAnimationController); + widget.controller.doubleTapZoomStarted( + MapEventSource.doubleTapZoomAnimationController, + ); _startListeningForAnimationInterruptions(); } else if (status == AnimationStatus.completed) { _stopListeningForAnimationInterruptions(); - widget - .onDoubleTapZoomEnd(MapEventSource.doubleTapZoomAnimationController); + + widget.controller.doubleTapZoomEnded( + MapEventSource.doubleTapZoomAnimationController, + ); } } void _handleDoubleTapZoomAnimation() { - widget.onDoubleTapZoomUpdate( - MapEventSource.doubleTapZoomAnimationController, + widget.controller.move( _doubleTapCenterAnimation.value, _doubleTapZoomAnimation.value, + offset: Offset.zero, + hasGesture: true, + source: MapEventSource.doubleTapZoomAnimationController, + id: null, ); } @@ -784,10 +782,44 @@ class InteractionDetectorState extends State if (InteractiveFlag.hasFlag(flags, InteractiveFlag.pinchZoom)) { final verticalOffset = (_focalStartLocal - details.localFocalPoint).dy; final newZoom = - _mapZoomStart - verticalOffset / 360 * widget.currentMapState().zoom; + _mapZoomStart - verticalOffset / 360 * widget.controller.value.zoom; + + final min = widget.options.minZoom ?? 0.0; + final max = widget.options.maxZoom ?? double.infinity; + final actualZoom = math.max(min, math.min(max, newZoom)); + + widget.controller.move( + widget.controller.value.center, + actualZoom, + offset: Offset.zero, + hasGesture: true, + source: MapEventSource.doubleTapHold, + id: null, + ); + } + } - widget.onOneFingerPinchZoom(MapEventSource.doubleTapHold, newZoom); + void _handleFlingAnimation() { + if (!_flingAnimationStarted) { + _flingAnimationStarted = true; + widget.controller.flingStarted(MapEventSource.flingAnimationController); + _startListeningForAnimationInterruptions(); } + + final newCenterPoint = widget.controller.value.project(_mapCenterStart) + + _flingAnimation.value + .toCustomPoint() + .rotate(widget.controller.value.rotationRad); + final newCenter = widget.controller.value.unproject(newCenterPoint); + + widget.controller.move( + newCenter, + widget.controller.value.zoom, + offset: Offset.zero, + hasGesture: true, + source: MapEventSource.flingAnimationController, + id: null, + ); } void _resetDoubleTapHold() { @@ -799,26 +831,10 @@ class InteractionDetectorState extends State if (status == AnimationStatus.completed) { _flingAnimationStarted = false; _stopListeningForAnimationInterruptions(); - widget.onFlingEnd(MapEventSource.flingAnimationController); + widget.controller.flingEnded(MapEventSource.flingAnimationController); } } - void _handleFlingAnimation() { - if (!_flingAnimationStarted) { - _flingAnimationStarted = true; - widget.onFlingStart(MapEventSource.flingAnimationController); - _startListeningForAnimationInterruptions(); - } - - final newCenterPoint = widget.currentMapState().project(_mapCenterStart) + - _flingAnimation.value - .toCustomPoint() - .rotate(widget.currentMapState().rotationRad); - final newCenter = widget.currentMapState().unproject(newCenterPoint); - - widget.onFlingUpdate(MapEventSource.flingAnimationController, newCenter); - } - void _startListeningForAnimationInterruptions() { _isListeningForInterruptions = true; } @@ -827,24 +843,21 @@ class InteractionDetectorState extends State _isListeningForInterruptions = false; } - // TODO Expose via controller. - void handleAnimationInterruptions(MapEvent event) { - if (_isListeningForInterruptions == false) { - //Do not handle animation interruptions if not listening - return; + void interruptAnimatedMovement(MapEvent event) { + if (_isListeningForInterruptions) { + _closeDoubleTapController(event.source); + _closeFlingAnimationController(event.source); } - _closeDoubleTapController(event.source); - _closeFlingAnimationController(event.source); } double _getZoomForScale(double startZoom, double scale) { final resultZoom = scale == 1.0 ? startZoom : startZoom + math.log(scale) / math.ln2; - return widget.currentMapState().fitZoomToBounds(resultZoom); + return widget.controller.value.fitZoomToBounds(resultZoom); } Offset _rotateOffset(Offset offset) { - final radians = widget.currentMapState().rotationRad; + final radians = widget.controller.value.rotationRad; if (radians != 0.0) { final cos = math.cos(radians); final sin = math.sin(radians); diff --git a/lib/src/gestures/flutter_map_state_controller.dart b/lib/src/gestures/flutter_map_state_controller.dart new file mode 100644 index 000000000..b56ace710 --- /dev/null +++ b/lib/src/gestures/flutter_map_state_controller.dart @@ -0,0 +1,479 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/src/gestures/flutter_map_interactive_viewer.dart'; +import 'package:flutter_map/src/map/flutter_map_state_controller_interface.dart'; +import 'package:flutter_map/src/map/map_controller_impl.dart'; +import 'package:latlong2/latlong.dart'; + +// This controller is for internal use. All updates to the state should be done +// by calling methods of this class to ensure consistency. +class FlutterMapStateController extends ValueNotifier + implements FlutterMapStateControllerInterface { + late final FlutterMapInteractiveViewerState _interactiveViewerState; + late MapControllerImpl _mapControllerImpl; + + FlutterMapStateController(MapOptions options) + : super(FlutterMapState.initialState(options)); + + // Link the viewer state with the controller. This should be done once when + // the FlutterMapInteractiveViewerState is initialized. + set interactiveViewerState( + FlutterMapInteractiveViewerState interactiveViewerState, + ) => + _interactiveViewerState = interactiveViewerState; + + void linkMapController(MapControllerImpl mapControllerImpl) { + _mapControllerImpl = mapControllerImpl; + _mapControllerImpl.stateController = this; + } + + /// This setter should only be called in this class or within tests. Changes + /// to the FlutterMapState should be done via methods in this class. + @visibleForTesting + @override + set value(FlutterMapState value) => super.value = value; + + // Note: All named parameters are required to prevent inconsistent default + // values since this method can be called by MapController which declares + // defaults. + @override + bool move( + LatLng newCenter, + double newZoom, { + required Offset offset, + required bool hasGesture, + required MapEventSource source, + required String? id, + }) { + newZoom = value.fitZoomToBounds(newZoom); + + // Algorithm thanks to https://github.com/tlserver/flutter_map_location_marker + if (offset != Offset.zero) { + final newPoint = value.project(newCenter, newZoom); + newCenter = value.unproject( + value.rotatePoint( + newPoint, + newPoint - CustomPoint(offset.dx, offset.dy), + ), + newZoom, + ); + } + + if (value.isOutOfBounds(newCenter)) { + if (!value.options.slideOnBoundaries) return false; + newCenter = value.containPoint(newCenter, value.center); + } + + if (value.options.maxBounds != null) { + final adjustedCenter = value.adjustCenterIfOutsideMaxBounds( + newCenter, + newZoom, + value.options.maxBounds!, + ); + + if (adjustedCenter == null) return false; + newCenter = adjustedCenter; + } + + if (newCenter == value.center && newZoom == value.zoom) { + return false; + } + + final oldMapState = value; + value = value.copyWith(zoom: newZoom, center: newCenter); + + final movementEvent = MapEventWithMove.fromSource( + oldMapState: oldMapState, + mapState: value, + hasGesture: hasGesture, + source: source, + id: id, + ); + if (movementEvent != null) _emitMapEvent(movementEvent); + + value.options.onPositionChanged?.call( + MapPosition( + center: newCenter, + bounds: value.bounds, + zoom: newZoom, + hasGesture: hasGesture, + ), + hasGesture, + ); + + return true; + } + + // Note: All named parameters are required to prevent inconsistent default + // values since this method can be called by MapController which declares + // defaults. + @override + bool rotate( + double newRotation, { + required bool hasGesture, + required MapEventSource source, + required String? id, + }) { + if (newRotation != value.rotation) { + final oldMapState = value; + //Apply state then emit events and callbacks + value = value.withRotation(newRotation); + + _emitMapEvent( + MapEventRotate( + id: id, + source: source, + oldMapState: oldMapState, + mapState: value, + ), + ); + return true; + } + + return false; + } + + // Note: All named parameters are required to prevent inconsistent default + // values since this method can be called by MapController which declares + // defaults. + @override + MoveAndRotateResult rotateAroundPoint( + double degree, { + required CustomPoint? point, + required Offset? offset, + required bool hasGesture, + required MapEventSource source, + required String? id, + }) { + if (point != null && offset != null) { + throw ArgumentError('Only one of `point` or `offset` may be non-null'); + } + if (point == null && offset == null) { + throw ArgumentError('One of `point` or `offset` must be non-null'); + } + + if (degree == value.rotation) { + return MoveAndRotateResult(false, false); + } + + if (offset == Offset.zero) { + return MoveAndRotateResult( + true, + rotate( + degree, + hasGesture: hasGesture, + source: source, + id: id, + ), + ); + } + + final rotationDiff = degree - value.rotation; + final rotationCenter = value.project(value.center) + + (point != null + ? (point - (value.nonRotatedSize / 2.0)) + : CustomPoint(offset!.dx, offset.dy)) + .rotate(value.rotationRad); + + return MoveAndRotateResult( + move( + value.unproject( + rotationCenter + + (value.project(value.center) - rotationCenter) + .rotate(degToRadian(rotationDiff)), + ), + value.zoom, + offset: Offset.zero, + hasGesture: hasGesture, + source: source, + id: id, + ), + rotate( + value.rotation + rotationDiff, + hasGesture: hasGesture, + source: source, + id: id, + ), + ); + } + + // Note: All named parameters are required to prevent inconsistent default + // values since this method can be called by MapController which declares + // defaults. + @override + MoveAndRotateResult moveAndRotate( + LatLng newCenter, + double newZoom, + double newRotation, { + required Offset offset, + required bool hasGesture, + required MapEventSource source, + required String? id, + }) => + MoveAndRotateResult( + move( + newCenter, + newZoom, + offset: offset, + hasGesture: hasGesture, + source: source, + id: id, + ), + rotate(newRotation, id: id, source: source, hasGesture: hasGesture), + ); + + // Note: All named parameters are required to prevent inconsistent default + // values since this method can be called by MapController which declares + // defaults. + @override + bool fitBounds( + LatLngBounds bounds, + FitBoundsOptions options, { + required Offset offset, + }) { + final target = value.getBoundsCenterZoom(bounds, options); + return move( + target.center, + target.zoom, + offset: offset, + hasGesture: false, + source: MapEventSource.fitBounds, + id: null, + ); + } + + @override + LatLng get center => value.center; + + @override + double get zoom => value.zoom; + + @override + double get rotation => value.rotation; + + @override + LatLngBounds? get bounds => value.bounds; + + @override + CenterZoom centerZoomFitBounds( + LatLngBounds bounds, + FitBoundsOptions options, + ) => + value.centerZoomFitBounds(bounds, options); + + @override + LatLng pointToLatLng(CustomPoint localPoint) => + value.pointToLatLng(localPoint); + + @override + CustomPoint latLngToScreenPoint(LatLng latLng) => + value.latLngToScreenPoint(latLng); + + @override + CustomPoint rotatePoint( + CustomPoint mapCenter, + CustomPoint point, { + required bool counterRotation, + }) => + value.rotatePoint( + mapCenter.toDoublePoint(), + point.toDoublePoint(), + counterRotation: counterRotation, + ); + + bool setNonRotatedSizeWithoutEmittingEvent( + CustomPoint nonRotatedSize, + ) { + if (nonRotatedSize != FlutterMapState.kImpossibleSize && + nonRotatedSize != value.nonRotatedSize) { + value = value.withNonRotatedSize(nonRotatedSize); + return true; + } + + return false; + } + + void setOptions(MapOptions options) { + if (value.options != options) { + value = value.withOptions(options); + } + } + + // To be called when a gesture that causes movement starts. + void moveStarted(MapEventSource source) { + _emitMapEvent( + MapEventMoveStart( + mapState: value, + source: source, + ), + ); + } + + // To be called when an ongoing drag movement updates. + void dragUpdated(MapEventSource source, Offset offset) { + final oldCenterPt = value.project(value.center); + + final newCenterPt = oldCenterPt + offset.toCustomPoint(); + final newCenter = value.unproject(newCenterPt); + + move( + newCenter, + value.zoom, + offset: Offset.zero, + hasGesture: true, + source: source, + id: null, + ); + } + + // To be called when a drag gesture ends. + void moveEnded(MapEventSource source) { + _emitMapEvent( + MapEventMoveEnd( + mapState: value, + source: source, + ), + ); + } + + // To be called when a rotation gesture starts. + void rotateStarted(MapEventSource source) { + _emitMapEvent( + MapEventRotateStart( + mapState: value, + source: source, + ), + ); + } + + // To be called when a rotation gesture ends. + void rotateEnded(MapEventSource source) { + _emitMapEvent( + MapEventRotateEnd( + mapState: value, + source: source, + ), + ); + } + + // To be called when a fling gesture starts. + void flingStarted(MapEventSource source) { + _emitMapEvent( + MapEventFlingAnimationStart( + mapState: value, + source: MapEventSource.flingAnimationController, + ), + ); + } + + // To be called when a fling gesture ends. + void flingEnded(MapEventSource source) { + _emitMapEvent( + MapEventFlingAnimationEnd( + mapState: value, + source: source, + ), + ); + } + + // To be called when a fling gesture does not start. + void flingNotStarted(MapEventSource source) { + _emitMapEvent( + MapEventFlingAnimationNotStarted( + mapState: value, + source: source, + ), + ); + } + + // To be called when a double tap zoom starts. + void doubleTapZoomStarted(MapEventSource source) { + _emitMapEvent( + MapEventDoubleTapZoomStart( + mapState: value, + source: source, + ), + ); + } + + // To be called when a double tap zoom ends. + void doubleTapZoomEnded(MapEventSource source) { + _emitMapEvent( + MapEventDoubleTapZoomEnd( + mapState: value, + source: source, + ), + ); + } + + void tapped( + MapEventSource source, + TapPosition tapPosition, + LatLng position, + ) { + value.options.onTap?.call(tapPosition, position); + _emitMapEvent( + MapEventTap( + tapPosition: position, + mapState: value, + source: source, + ), + ); + } + + void secondaryTapped( + MapEventSource source, + TapPosition tapPosition, + LatLng position, + ) { + value.options.onSecondaryTap?.call(tapPosition, position); + _emitMapEvent( + MapEventSecondaryTap( + tapPosition: position, + mapState: value, + source: source, + ), + ); + } + + void longPressed( + MapEventSource source, + TapPosition tapPosition, + LatLng position, + ) { + value.options.onLongPress?.call(tapPosition, position); + _emitMapEvent( + MapEventLongPress( + tapPosition: position, + mapState: value, + source: MapEventSource.longPress, + ), + ); + } + + // To be called when the map's size constraints change. + void nonRotatedSizeChange( + MapEventSource source, + FlutterMapState oldMapState, + FlutterMapState newMapState, + ) { + _emitMapEvent( + MapEventNonRotatedSizeChange( + source: MapEventSource.nonRotatedSizeChange, + oldMapState: oldMapState, + mapState: newMapState, + ), + ); + } + + void _emitMapEvent(MapEvent event) { + if (event.source == MapEventSource.mapController && event is MapEventMove) { + _interactiveViewerState.interruptAnimatedMovement(event); + } + + value.options.onMapEvent?.call(event); + + _mapControllerImpl.mapEventSink.add(event); + } +} diff --git a/lib/src/gestures/ineraction_detector_controller.dart b/lib/src/gestures/ineraction_detector_controller.dart deleted file mode 100644 index 9c42e96db..000000000 --- a/lib/src/gestures/ineraction_detector_controller.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_map/src/map/flutter_map_state.dart'; - -class InteractionDetectorController extends ValueNotifier { - InteractionDetectorController(super.value); -} diff --git a/lib/src/map/flutter_map_state.dart b/lib/src/map/flutter_map_state.dart index 1c62869aa..577e9bfa9 100644 --- a/lib/src/map/flutter_map_state.dart +++ b/lib/src/map/flutter_map_state.dart @@ -1,9 +1,9 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; -import 'package:flutter_map/src/geo/crs.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; +import 'package:flutter_map/src/map/map_safe_area.dart'; import 'package:flutter_map/src/map/options.dart'; import 'package:flutter_map/src/misc/center_zoom.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; @@ -12,6 +12,13 @@ import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; class FlutterMapState { + // During Flutter startup the native platform resolution is not immediately + // available which can cause constraints to be zero before they are updated + // in a subsequent build to the actual constraints. We set the size to this + // impossible (negative) value initially and only change it once Flutter + // provides real constraints. + static const kImpossibleSize = CustomPoint(-1, -1); + final MapOptions options; final LatLng center; @@ -19,73 +26,63 @@ class FlutterMapState { final double rotation; // Original size of the map where rotation isn't calculated - final CustomPoint nonrotatedSize; + final CustomPoint nonRotatedSize; // Extended size of the map where rotation is calculated final CustomPoint size; - late final CustomPoint pixelOrigin; - late final LatLngBounds bounds; - late final Bounds pixelBounds; + // Lazily calculated fields. + Bounds? _pixelBounds; + LatLngBounds? _bounds; + CustomPoint? _pixelOrigin; - final bool hasFitInitialBounds; + MapSafeArea? _mapSafeAreaCache; - FlutterMapState._({ - required this.options, - required this.center, - required this.zoom, - required this.rotation, - required this.nonrotatedSize, - required this.size, - required this.hasFitInitialBounds, - required this.pixelOrigin, - required this.bounds, - required this.pixelBounds, - }); + static FlutterMapState? maybeOf(BuildContext context) => context + .dependOnInheritedWidgetOfExactType() + ?.mapState; + static FlutterMapState of(BuildContext context) => + maybeOf(context) ?? + (throw StateError( + '`FlutterMapState.of()` should not be called outside a `FlutterMap` and its descendants')); + + /// Initializes FlutterMapState from the given [options] and with the + /// [nonRotatedSize] and [size] both set to [kImpossibleSize]. + FlutterMapState.initialState(this.options) + : center = options.center, + zoom = options.zoom, + rotation = options.rotation, + nonRotatedSize = kImpossibleSize, + size = kImpossibleSize; + + // Create an instance of FlutterMapState. The [pixelOrigin], [bounds], and + // [pixelBounds] may be set if they are known already. Otherwise if left + // null they will be calculated lazily when they are used. FlutterMapState({ required this.options, required this.center, required this.zoom, required this.rotation, - required this.nonrotatedSize, + required this.nonRotatedSize, required this.size, - required this.hasFitInitialBounds, - }) { - pixelBounds = _getPixelBoundsStatic(options.crs, size, center, zoom); - bounds = LatLngBounds( - options.crs.pointToLatLng(pixelBounds.bottomLeft, zoom), - options.crs.pointToLatLng(pixelBounds.topRight, zoom), - ); - final halfSize = size / 2.0; - pixelOrigin = (project(center, zoom) - halfSize).round(); - } + Bounds? pixelBounds, + LatLngBounds? bounds, + CustomPoint? pixelOrigin, + }) : _pixelBounds = pixelBounds, + _bounds = bounds, + _pixelOrigin = pixelOrigin; - FlutterMapState copyWith({ - LatLng? center, - double? zoom, - }) => - FlutterMapState( - options: options, - center: center ?? this.center, - zoom: zoom ?? this.zoom, - rotation: rotation, - nonrotatedSize: nonrotatedSize, - size: size, - hasFitInitialBounds: hasFitInitialBounds, - ); - - FlutterMapState withNonotatedSize(CustomPoint nonrotatedSize) { - if (nonrotatedSize == this.nonrotatedSize) return this; + FlutterMapState withNonRotatedSize(CustomPoint nonRotatedSize) { + if (nonRotatedSize == this.nonRotatedSize) return this; return FlutterMapState( options: options, center: center, zoom: zoom, rotation: rotation, - nonrotatedSize: nonrotatedSize, - size: _calculateSize(rotation, nonrotatedSize), - hasFitInitialBounds: hasFitInitialBounds, + nonRotatedSize: nonRotatedSize, + size: _calculateSize(rotation, nonRotatedSize), ); } @@ -97,9 +94,8 @@ class FlutterMapState { center: center, zoom: zoom, rotation: rotation, - nonrotatedSize: nonrotatedSize, - size: _calculateSize(rotation, nonrotatedSize), - hasFitInitialBounds: hasFitInitialBounds, + nonRotatedSize: nonRotatedSize, + size: _calculateSize(rotation, nonRotatedSize), ); } @@ -108,23 +104,49 @@ class FlutterMapState { center: center, zoom: zoom, rotation: rotation, - nonrotatedSize: nonrotatedSize, - size: _calculateSize(rotation, nonrotatedSize), - hasFitInitialBounds: hasFitInitialBounds, + nonRotatedSize: nonRotatedSize, + size: _calculateSize(rotation, nonRotatedSize), + ); + + Bounds get pixelBounds => + _pixelBounds ?? (_pixelBounds = getPixelBounds()); + + LatLngBounds get bounds => + _bounds ?? + (_bounds = LatLngBounds( + unproject(pixelBounds.bottomLeft, zoom), + unproject(pixelBounds.topRight, zoom), + )); + + CustomPoint get pixelOrigin => + _pixelOrigin ?? + (_pixelOrigin = (project(center, zoom) - size / 2.0).round()); + + FlutterMapState copyWith({ + LatLng? center, + double? zoom, + }) => + FlutterMapState( + options: options, + center: center ?? this.center, + zoom: zoom ?? this.zoom, + rotation: rotation, + nonRotatedSize: nonRotatedSize, + size: size, ); static CustomPoint _calculateSize( double rotation, - CustomPoint nonrotatedSize, + CustomPoint nonRotatedSize, ) { - if (rotation == 0.0) return nonrotatedSize; + if (rotation == 0.0) return nonRotatedSize; final rotationRad = degToRadian(rotation); final cosAngle = math.cos(rotationRad).abs(); final sinAngle = math.sin(rotationRad).abs(); - final width = (nonrotatedSize.x * cosAngle) + (nonrotatedSize.y * sinAngle); + final width = (nonRotatedSize.x * cosAngle) + (nonRotatedSize.y * sinAngle); final height = - (nonrotatedSize.y * cosAngle) + (nonrotatedSize.x * sinAngle); + (nonRotatedSize.y * cosAngle) + (nonRotatedSize.x * sinAngle); return CustomPoint(width, height); } @@ -256,22 +278,11 @@ class FlutterMapState { return Bounds(pixelCenter - halfSize, pixelCenter + halfSize); } - static Bounds _getPixelBoundsStatic( - Crs crs, - CustomPoint size, - LatLng center, - double zoom, - ) { - final halfSize = size / 2; - final pixelCenter = crs.latLngToPoint(center, zoom).floor().toDoublePoint(); - return Bounds(pixelCenter - halfSize, pixelCenter + halfSize); - } - // This will convert a latLng to a position that we could use with a widget // outside of FlutterMap layer space. Eg using a Positioned Widget. CustomPoint latLngToScreenPoint(LatLng latLng) { final nonRotatedPixelOrigin = - (project(center, zoom) - nonrotatedSize / 2.0).round(); + (project(center, zoom) - nonRotatedSize / 2.0).round(); var point = options.crs.latLngToPoint(latLng, zoom); @@ -286,8 +297,8 @@ class FlutterMapState { LatLng pointToLatLng(CustomPoint localPoint) { final localPointCenterDistance = CustomPoint( - (nonrotatedSize.x / 2) - localPoint.x, - (nonrotatedSize.y / 2) - localPoint.y, + (nonRotatedSize.x / 2) - localPoint.x, + (nonRotatedSize.y / 2) - localPoint.y, ); final mapCenter = options.crs.latLngToPoint(center, zoom); @@ -321,10 +332,6 @@ class FlutterMapState { return CustomPoint(tp.dx, tp.dy); } - // TODO replicate old caching or stop doing it - _SafeArea? _safeAreaCache; - double? _safeAreaZoom; - double fitZoomToBounds(double zoom) { // Abide to min/max zoom if (options.maxZoom != null) { @@ -336,7 +343,7 @@ class FlutterMapState { return zoom; } - //if there is a pan boundary, do not cross + // Returns true if given [center] is outside of the allowed bounds. bool isOutOfBounds(LatLng center) { if (options.adaptiveBoundaries) { return !_safeArea!.contains(center); @@ -366,65 +373,35 @@ class FlutterMapState { } } - _SafeArea? get _safeArea { - if (zoom != _safeAreaZoom || _safeAreaCache == null) { - _safeAreaZoom = zoom; - final halfScreenHeight = _calculateScreenHeightInDegrees() / 2; - final halfScreenWidth = _calculateScreenWidthInDegrees() / 2; - final southWestLatitude = - options.swPanBoundary!.latitude + halfScreenHeight; - final southWestLongitude = - options.swPanBoundary!.longitude + halfScreenWidth; - final northEastLatitude = - options.nePanBoundary!.latitude - halfScreenHeight; - final northEastLongitude = - options.nePanBoundary!.longitude - halfScreenWidth; - _safeAreaCache = _SafeArea( - LatLng( - southWestLatitude, - southWestLongitude, - ), - LatLng( - northEastLatitude, - northEastLongitude, - ), + MapSafeArea? get _safeArea => MapSafeArea.createUnlessMatching( + previous: _mapSafeAreaCache, + zoom: zoom, + screenSize: options.screenSize!, + swPanBoundary: options.swPanBoundary!, + nePanBoundary: options.nePanBoundary!, ); - } - return _safeAreaCache; - } - - double _calculateScreenWidthInDegrees() { - final degreesPerPixel = 360 / math.pow(2, zoom + 8); - return options.screenSize!.width * degreesPerPixel; - } - - double _calculateScreenHeightInDegrees() => - options.screenSize!.height * 170.102258 / math.pow(2, zoom + 8); LatLng offsetToCrs(Offset offset, [double? zoom]) { final focalStartPt = project(center, zoom ?? this.zoom); final point = - (offset.toCustomPoint() - (nonrotatedSize / 2.0)).rotate(rotationRad); + (offset.toCustomPoint() - (nonRotatedSize / 2.0)).rotate(rotationRad); final newCenterPt = focalStartPt + point; return unproject(newCenterPt, zoom ?? this.zoom); } - // TODO better description - // If we double click in the corner of a map, calculate the new - // center of the whole map after a zoom, to retain that offset position - // so that the same event LatLng is still under the cursor. - (LatLng, double) getNewEventCenterZoomPosition( - CustomPoint cursorPos, double newZoom) { + // 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(CustomPoint cursorPos, double zoom) { // Calculate offset of mouse cursor from viewport center - final viewCenter = nonrotatedSize / 2; + final viewCenter = nonRotatedSize / 2; final offset = (cursorPos - viewCenter).rotate(rotationRad); // Match new center coordinate to mouse cursor position - final scale = getZoomScale(newZoom, zoom); + final scale = getZoomScale(zoom, this.zoom); final newOffset = offset * (1.0 - 1.0 / scale); final mapCenter = project(center); final newCenter = unproject(mapCenter + newOffset); - return (newCenter, newZoom); + return newCenter; } LatLng? adjustCenterIfOutsideMaxBounds( @@ -489,36 +466,4 @@ class FlutterMapState { return newCenter; } - - static FlutterMapState? maybeOf(BuildContext context) => context - .dependOnInheritedWidgetOfExactType() - ?.mapState; - - static FlutterMapState of(BuildContext context) => - maybeOf(context) ?? - (throw StateError( - '`FlutterMapState.of()` should not be called outside a `FlutterMap` and its children')); -} - -class _SafeArea { - final LatLngBounds bounds; - final bool isLatitudeBlocked; - final bool isLongitudeBlocked; - - _SafeArea(LatLng southWest, LatLng northEast) - : bounds = LatLngBounds(southWest, northEast), - isLatitudeBlocked = southWest.latitude > northEast.latitude, - isLongitudeBlocked = southWest.longitude > northEast.longitude; - - bool contains(LatLng point) => - isLatitudeBlocked || isLongitudeBlocked ? false : bounds.contains(point); - - LatLng containPoint(LatLng point, LatLng fallback) => LatLng( - isLatitudeBlocked - ? fallback.latitude - : point.latitude.clamp(bounds.south, bounds.north), - isLongitudeBlocked - ? fallback.longitude - : point.longitude.clamp(bounds.west, bounds.east), - ); } diff --git a/lib/src/map/flutter_map_state_container.dart b/lib/src/map/flutter_map_state_container.dart index 67e9212b3..88c219b79 100644 --- a/lib/src/map/flutter_map_state_container.dart +++ b/lib/src/map/flutter_map_state_container.dart @@ -1,68 +1,33 @@ -import 'dart:math' as math; - -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/gestures/interaction_detector.dart'; -import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/gestures/flutter_map_interactive_viewer.dart'; +import 'package:flutter_map/src/gestures/flutter_map_state_controller.dart'; import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; -import 'package:latlong2/latlong.dart'; +import 'package:flutter_map/src/map/map_controller_impl.dart'; class FlutterMapStateContainer extends State { - static const invalidSize = CustomPoint(-1, -1); - final _flutterMapGestureDetectorKey = GlobalKey(); - bool _hasFitInitialBounds = false; + late final FlutterMapStateController _flutterMapStateController; + late MapControllerImpl _mapController; late bool _mapControllerCreatedInternally; - late MapController _mapController; - - late FlutterMapState _mapState; - - FlutterMapState get mapState => _mapState; - - LatLng get center => _mapState.center; - - LatLngBounds get bounds => _mapState.bounds; - - double get zoom => _mapState.zoom; - - double get rotation => _mapState.rotation; - - void _initializeMapController() { - _mapController = widget.mapController ?? MapController(); - _mapControllerCreatedInternally = widget.mapController == null; - } @override void initState() { super.initState(); - _initializeMapController(); - _mapController.state = this; + _flutterMapStateController = FlutterMapStateController(widget.options); + _initializeAndLinkMapController(); WidgetsBinding.instance .addPostFrameCallback((_) => widget.options.onMapReady?.call()); - - _mapState = FlutterMapState( - options: widget.options, - center: widget.options.center, - zoom: widget.options.zoom, - rotation: widget.options.rotation, - nonrotatedSize: invalidSize, - size: invalidSize, - hasFitInitialBounds: _hasFitInitialBounds, - ); } @override void didUpdateWidget(FlutterMap oldWidget) { - if (oldWidget.options != widget.options) { - _mapState = _mapState.withOptions(widget.options); - } + _flutterMapStateController.setOptions(widget.options); if (oldWidget.mapController != widget.mapController) { - _initializeMapController(); - _mapController.state = this; + _initializeAndLinkMapController(); } super.didUpdateWidget(oldWidget); } @@ -70,55 +35,40 @@ class FlutterMapStateContainer extends State { @override void dispose() { if (_mapControllerCreatedInternally) _mapController.dispose(); + _flutterMapStateController.dispose(); super.dispose(); } + void _initializeAndLinkMapController() { + _mapController = + (widget.mapController ?? MapController()) as MapControllerImpl; + _mapControllerCreatedInternally = widget.mapController == null; + _flutterMapStateController.linkMapController(_mapController); + } + @override Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - _onConstraintsChange(constraints); - - return MapStateInheritedWidget( - mapController: _mapController, - mapState: _mapState, - child: InteractionDetector( - key: _flutterMapGestureDetectorKey, - options: widget.options, - currentMapState: () => _mapState, - onPointerDown: _onPointerDown, - onPointerUp: _onPointerUp, - onPointerCancel: _onPointerCancel, - onPointerHover: _onPointerHover, - onRotateEnd: _onRotateEnd, - onFlingStart: _onFlingStart, - onFlingEnd: _onFlingEnd, - onMoveEnd: _onMoveEnd, - onOneFingerPinchZoom: _onOneFingerPinchZoom, - onDoubleTapZoomEnd: _onDoubleTapZoomEnd, - onMoveStart: _onMoveStart, - onRotateStart: _onRotateStart, - onFlingNotStarted: _onFlingNotStarted, - onPinchZoomUpdate: _onPinchZoomUpdate, - onRotateUpdate: _onRotateUpdate, - onTap: _onTap, - onDragUpdate: _onDragUpdate, - onSecondaryTap: _onSecondaryTap, - onLongPress: _onLongPress, - onDoubleTapZoomStart: _onDoubleTapZoomStart, - onScroll: _onScroll, - onDoubleTapZoomUpdate: _onDoubleTapZoomUpdate, - onFlingUpdate: _onFlingUpdate, + _updateAndEmitSizeIfConstraintsChanged(constraints); + _setInitialFitBounds(constraints); + + return FlutterMapInteractiveViewer( + controller: _flutterMapStateController, + options: widget.options, + builder: (context, mapState) => MapStateInheritedWidget( + mapController: _mapController, + mapState: mapState, child: ClipRect( child: Stack( children: [ OverflowBox( - minWidth: _mapState.size.x, - maxWidth: _mapState.size.x, - minHeight: _mapState.size.y, - maxHeight: _mapState.size.y, + minWidth: mapState.size.x, + maxWidth: mapState.size.x, + minHeight: mapState.size.y, + maxHeight: mapState.size.y, child: Transform.rotate( - angle: _mapState.rotationRad, + angle: mapState.rotationRad, child: Stack(children: widget.children), ), ), @@ -132,264 +82,47 @@ class FlutterMapStateContainer extends State { ); } - void _onDoubleTapZoomUpdate( - MapEventSource source, - LatLng position, - double zoom, - ) { - move( - position, - zoom, - hasGesture: true, - source: source, - ); - } - - void _onFlingUpdate(MapEventSource source, LatLng position) { - move( - position, - _mapState.zoom, - hasGesture: true, - source: source, - ); - } - - void _onRotateUpdate( - MapEventSource source, - LatLng position, - double zoom, - double rotation, - ) { - moveAndRotate(position, zoom, rotation, source: source, hasGesture: true); - } - - void _onOneFingerPinchZoom(MapEventSource source, double newZoom) { - final min = widget.options.minZoom ?? 0.0; - final max = widget.options.maxZoom ?? double.infinity; - final actualZoom = math.max(min, math.min(max, newZoom)); - - move( - _mapState.center, - actualZoom, - hasGesture: true, - source: source, - ); - } - - void _onScroll(PointerScrollEvent event) { - final minZoom = widget.options.minZoom ?? 0.0; - final maxZoom = widget.options.maxZoom ?? double.infinity; - final newZoom = (_mapState.zoom - - event.scrollDelta.dy * widget.options.scrollWheelVelocity) - .clamp(minZoom, maxZoom); - // Calculate offset of mouse cursor from viewport center - final newCenterZoom = _mapState.getNewEventCenterZoomPosition( - event.localPosition.toCustomPoint(), - newZoom, - ); - - // Move to new center and zoom level - move( - newCenterZoom.$1, - newCenterZoom.$2, - source: MapEventSource.scrollWheel, - ); - } - - void _onPointerDown(PointerDownEvent event) { - if (widget.options.onPointerDown != null) { - final latlng = _mapState.offsetToCrs(event.localPosition); - widget.options.onPointerDown!(event, latlng); - } - } - - void _onPointerUp(PointerUpEvent event) { - if (widget.options.onPointerUp != null) { - final latlng = _mapState.offsetToCrs(event.localPosition); - widget.options.onPointerUp!(event, latlng); - } - } - - void _onPointerCancel(PointerCancelEvent event) { - if (widget.options.onPointerCancel != null) { - final latlng = _mapState.offsetToCrs(event.localPosition); - widget.options.onPointerCancel!(event, latlng); - } - } - - void _onPointerHover(PointerHoverEvent event) { - if (widget.options.onPointerHover != null) { - final latlng = _mapState.offsetToCrs(event.localPosition); - widget.options.onPointerHover!(event, latlng); - } - } - - void _onTap(MapEventSource source, LatLng position) { - _emitMapEvent( - MapEventTap( - tapPosition: position, - mapState: _mapState, - source: source, - ), - ); - } - - void _onSecondaryTap(MapEventSource source, LatLng position) { - _emitMapEvent( - MapEventSecondaryTap( - tapPosition: position, - mapState: _mapState, - source: source, - ), - ); - } - - void _onDragUpdate(MapEventSource source, Offset offset) { - final oldCenterPt = _mapState.project(_mapState.center); - - final newCenterPt = oldCenterPt + offset.toCustomPoint(); - final newCenter = _mapState.unproject(newCenterPt); - - move(newCenter, _mapState.zoom, hasGesture: true, source: source); - } - - void _onPinchZoomUpdate(MapEventSource source, LatLng position, double zoom) { - move(position, zoom, hasGesture: true, source: source); - } - - void _onLongPress(MapEventSource source, LatLng position) { - _emitMapEvent( - MapEventLongPress( - tapPosition: position, - mapState: _mapState, - source: source, - ), - ); - } - - void _onMoveStart(MapEventSource source) { - _emitMapEvent( - MapEventMoveStart( - mapState: _mapState, - source: source, - ), - ); - } - - void _onRotateStart(MapEventSource source) { - _emitMapEvent( - MapEventRotateStart( - mapState: _mapState, - source: source, - ), - ); - } - - void _onRotateEnd(MapEventSource source) { - _emitMapEvent( - MapEventRotateEnd( - mapState: _mapState, - source: source, - ), - ); - } - - void _onMoveEnd(MapEventSource source) { - _emitMapEvent( - MapEventRotateEnd( - mapState: _mapState, - source: source, - ), - ); - } - - void _onFlingStart(MapEventSource source) { - _emitMapEvent( - MapEventFlingAnimationStart( - mapState: _mapState, - source: MapEventSource.flingAnimationController, - ), - ); - } - - void _onFlingEnd(MapEventSource source) { - _emitMapEvent( - MapEventFlingAnimationEnd( - mapState: _mapState, - source: source, - ), - ); - } - - void _onDoubleTapZoomEnd(MapEventSource source) { - _emitMapEvent( - MapEventDoubleTapZoomEnd( - mapState: _mapState, - source: source, - ), - ); - } - - void _onDoubleTapZoomStart(MapEventSource source) { - _emitMapEvent( - MapEventDoubleTapZoomStart( - mapState: _mapState, - source: MapEventSource.doubleTapZoomAnimationController), - ); - } - - void _onFlingNotStarted(MapEventSource source) { - _emitMapEvent( - MapEventFlingAnimationNotStarted( - mapState: _mapState, - source: source, - ), - ); - } - - // No need to call setState in here as we are already running a build and the - // resulting FlutterMapState will be passed to the inherited widget which - // will trigger a build if it is different. - void _onConstraintsChange(BoxConstraints constraints) { - // Update on layout change. - _updateAndEmitSizeIfConstraintsChanged(constraints); - + void _setInitialFitBounds(BoxConstraints constraints) { // If bounds were provided set the initial center/zoom to match those // bounds once the parent constraints are available. if (widget.options.bounds != null && !_hasFitInitialBounds && _parentConstraintsAreSet(context, constraints)) { - final target = _mapState.getBoundsCenterZoom( + _hasFitInitialBounds = true; + + _flutterMapStateController.fitBounds( widget.options.bounds!, widget.options.boundsOptions, + offset: Offset.zero, ); - - _mapState = _mapState.copyWith(zoom: target.zoom, center: target.center); - _hasFitInitialBounds = true; } } void _updateAndEmitSizeIfConstraintsChanged(BoxConstraints constraints) { - if (_mapState.nonrotatedSize.x != constraints.maxWidth || - _mapState.nonrotatedSize.y != constraints.maxHeight) { - final oldMapState = _mapState; - _mapState = _mapState.withNonotatedSize( - CustomPoint(constraints.maxWidth, constraints.maxHeight), - ); - - if (_mapState.nonrotatedSize != invalidSize) { - _emitMapEvent( - MapEventNonRotatedSizeChange( - source: MapEventSource.nonRotatedSizeChange, - oldMapState: oldMapState, - mapState: _mapState, - ), - ); - } + final nonRotatedSize = CustomPoint( + constraints.maxWidth, + constraints.maxHeight, + ); + final oldMapState = _flutterMapStateController.value; + if (_flutterMapStateController + .setNonRotatedSizeWithoutEmittingEvent(nonRotatedSize)) { + final newMapState = _flutterMapStateController.value; + + // Avoid emitting the event during build otherwise if the user calls + // setState in the onMapEvent callback it will throw. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _flutterMapStateController.nonRotatedSizeChange( + MapEventSource.nonRotatedSizeChange, + oldMapState, + newMapState, + ); + } + }); } } - // During flutter startup the native platform resolution is not immediately + // During Flutter startup the native platform resolution is not immediately // available which can cause constraints to be zero before they are updated // in a subsequent build to the actual constraints. This check allows us to // differentiate zero constraints caused by missing platform resolution vs @@ -397,224 +130,4 @@ class FlutterMapStateContainer extends State { bool _parentConstraintsAreSet( BuildContext context, BoxConstraints constraints) => constraints.maxWidth != 0 || MediaQuery.sizeOf(context) != Size.zero; - - void _emitMapEvent(MapEvent event) { - if (event.source == MapEventSource.mapController && event is MapEventMove) { - _flutterMapGestureDetectorKey.currentState - ?.handleAnimationInterruptions(event); - } - - widget.options.onMapEvent?.call(event); - - _mapController.mapEventSink.add(event); - } - - bool rotate( - double newRotation, { - bool hasGesture = false, - required MapEventSource source, - String? id, - }) { - if (newRotation != _mapState.rotation) { - final oldMapState = _mapState; - //Apply state then emit events and callbacks - setState(() { - _mapState = _mapState.withRotation(newRotation); - }); - - _emitMapEvent( - MapEventRotate( - id: id, - source: source, - oldMapState: oldMapState, - mapState: _mapState, - ), - ); - return true; - } - - return false; - } - - MoveAndRotateResult rotateAroundPoint( - double degree, { - CustomPoint? point, - Offset? offset, - bool hasGesture = false, - required MapEventSource source, - String? id, - }) { - if (point != null && offset != null) { - throw ArgumentError('Only one of `point` or `offset` may be non-null'); - } - if (point == null && offset == null) { - throw ArgumentError('One of `point` or `offset` must be non-null'); - } - - if (degree == _mapState.rotation) return MoveAndRotateResult(false, false); - - if (offset == Offset.zero) { - return MoveAndRotateResult( - true, - rotate( - degree, - hasGesture: hasGesture, - source: source, - id: id, - ), - ); - } - - final rotationDiff = degree - _mapState.rotation; - final rotationCenter = _mapState.project(_mapState.center) + - (point != null - ? (point - (_mapState.nonrotatedSize / 2.0)) - : CustomPoint(offset!.dx, offset.dy)) - .rotate(_mapState.rotationRad); - - return MoveAndRotateResult( - move( - _mapState.unproject( - rotationCenter + - (_mapState.project(_mapState.center) - rotationCenter) - .rotate(degToRadian(rotationDiff)), - ), - _mapState.zoom, - hasGesture: hasGesture, - source: source, - id: id, - ), - rotate( - _mapState.rotation + rotationDiff, - hasGesture: hasGesture, - source: source, - id: id, - ), - ); - } - - MoveAndRotateResult moveAndRotate( - LatLng newCenter, - double newZoom, - double newRotation, { - Offset offset = Offset.zero, - required MapEventSource source, - String? id, - bool hasGesture = false, - }) => - MoveAndRotateResult( - move( - newCenter, - newZoom, - offset: offset, - id: id, - source: source, - hasGesture: hasGesture, - ), - rotate(newRotation, id: id, source: source, hasGesture: hasGesture), - ); - - bool move( - LatLng newCenter, - double newZoom, { - Offset offset = Offset.zero, - bool hasGesture = false, - required MapEventSource source, - String? id, - }) { - newZoom = _mapState.fitZoomToBounds(newZoom); - - // Algorithm thanks to https://github.com/tlserver/flutter_map_location_marker - if (offset != Offset.zero) { - final newPoint = widget.options.crs.latLngToPoint(newCenter, newZoom); - newCenter = widget.options.crs.pointToLatLng( - _mapState.rotatePoint( - newPoint, - newPoint - CustomPoint(offset.dx, offset.dy), - ), - newZoom, - ); - } - - if (_mapState.isOutOfBounds(newCenter)) { - if (!widget.options.slideOnBoundaries) return false; - newCenter = _mapState.containPoint(newCenter, _mapState.center); - } - - if (widget.options.maxBounds != null) { - final adjustedCenter = _mapState.adjustCenterIfOutsideMaxBounds( - newCenter, - newZoom, - widget.options.maxBounds!, - ); - - if (adjustedCenter == null) return false; - newCenter = adjustedCenter; - } - - if (newCenter == _mapState.center && newZoom == _mapState.zoom) { - return false; - } - - final oldMapState = _mapState; - setState(() { - _mapState = _mapState.copyWith(zoom: newZoom, center: newCenter); - }); - - final movementEvent = MapEventWithMove.fromSource( - oldMapState: oldMapState, - mapState: _mapState, - hasGesture: hasGesture, - source: source, - id: id, - ); - if (movementEvent != null) _emitMapEvent(movementEvent); - - widget.options.onPositionChanged?.call( - MapPosition( - center: newCenter, - bounds: _mapState.bounds, - zoom: newZoom, - hasGesture: hasGesture, - ), - hasGesture, - ); - - return true; - } - - bool fitBounds( - LatLngBounds bounds, - FitBoundsOptions options, { - Offset offset = Offset.zero, - }) { - final target = _mapState.getBoundsCenterZoom(bounds, options); - return move( - target.center, - target.zoom, - offset: offset, - source: MapEventSource.fitBounds, - ); - } - - LatLng pointToLatLng(CustomPoint localPoint) => - _mapState.pointToLatLng(localPoint); - - CenterZoom centerZoomFitBounds( - LatLngBounds bounds, FitBoundsOptions options) => - _mapState.centerZoomFitBounds(bounds, options); - - CustomPoint latLngToScreenPoint(LatLng latLng) => - _mapState.latLngToScreenPoint(latLng); - - CustomPoint rotatePoint( - CustomPoint mapCenter, - CustomPoint point, { - bool counterRotation = true, - }) => - _mapState.rotatePoint( - mapCenter.toDoublePoint(), - point.toDoublePoint(), - counterRotation: counterRotation, - ); } diff --git a/lib/src/map/flutter_map_state_controller_interface.dart b/lib/src/map/flutter_map_state_controller_interface.dart new file mode 100644 index 000000000..5c6ce6c2e --- /dev/null +++ b/lib/src/map/flutter_map_state_controller_interface.dart @@ -0,0 +1,73 @@ +import 'package:flutter/rendering.dart'; +import 'package:flutter_map/src/geo/latlng_bounds.dart'; +import 'package:flutter_map/src/gestures/map_events.dart'; +import 'package:flutter_map/src/misc/center_zoom.dart'; +import 'package:flutter_map/src/misc/fit_bounds_options.dart'; +import 'package:flutter_map/src/misc/move_and_rotate_result.dart'; +import 'package:flutter_map/src/misc/point.dart'; +import 'package:latlong2/latlong.dart'; + +/// Defines the methods that the MapController may call on the +/// FlutterMapStateController. This clarifies the link between the two classes. +abstract interface class FlutterMapStateControllerInterface { + LatLng get center; + double get zoom; + double get rotation; + LatLngBounds? get bounds; + + bool move( + LatLng newCenter, + double newZoom, { + required Offset offset, + required bool hasGesture, + required MapEventSource source, + required String? id, + }); + + bool rotate( + double newRotation, { + required bool hasGesture, + required MapEventSource source, + required String? id, + }); + + MoveAndRotateResult rotateAroundPoint( + double degree, { + required CustomPoint? point, + required Offset? offset, + required bool hasGesture, + required MapEventSource source, + required String? id, + }); + + MoveAndRotateResult moveAndRotate( + LatLng newCenter, + double newZoom, + double newRotation, { + required Offset offset, + required bool hasGesture, + required MapEventSource source, + required String? id, + }); + + bool fitBounds( + LatLngBounds bounds, + FitBoundsOptions options, { + required Offset offset, + }); + + CenterZoom centerZoomFitBounds( + LatLngBounds bounds, + FitBoundsOptions options, + ); + + LatLng pointToLatLng(CustomPoint localPoint); + + CustomPoint latLngToScreenPoint(LatLng latLng); + + CustomPoint rotatePoint( + CustomPoint mapCenter, + CustomPoint point, { + required bool counterRotation, + }); +} diff --git a/lib/src/map/flutter_map_state_inherited_widget.dart b/lib/src/map/flutter_map_state_inherited_widget.dart index c2e772409..169b8aff0 100644 --- a/lib/src/map/flutter_map_state_inherited_widget.dart +++ b/lib/src/map/flutter_map_state_inherited_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/src/map/controller.dart'; import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/map/map_controller.dart'; class MapStateInheritedWidget extends InheritedWidget { const MapStateInheritedWidget({ diff --git a/lib/src/map/controller.dart b/lib/src/map/map_controller.dart similarity index 66% rename from lib/src/map/controller.dart rename to lib/src/map/map_controller.dart index f01827059..ede173ae9 100644 --- a/lib/src/map/controller.dart +++ b/lib/src/map/map_controller.dart @@ -2,10 +2,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/flutter_map_state_container.dart'; import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; +import 'package:flutter_map/src/map/map_controller_impl.dart'; import 'package:latlong2/latlong.dart'; -import 'package:meta/meta.dart'; /// Controller to programmatically interact with [FlutterMap], such as /// controlling it and accessing some of its properties. @@ -21,7 +20,7 @@ abstract class MapController { /// instance. /// /// Factory constructor redirects to underlying implementation's constructor. - factory MapController() = MapControllerImpl._; + factory MapController() = MapControllerImpl; static MapController? maybeOf(BuildContext context) => context .dependOnInheritedWidgetOfExactType() @@ -165,138 +164,6 @@ abstract class MapController { /// [Stream] of all emitted [MapEvent]s Stream get mapEventStream; - /// Underlying [StreamSink] of [mapEventStream] - /// - /// Usually prefer to use [mapEventStream]. - StreamSink get mapEventSink; - - /// Immediately change the internal map state - /// - /// Not recommended for external usage. - set state(FlutterMapStateContainer state); - /// Dispose of this controller. void dispose(); } - -@internal -class MapControllerImpl implements MapController { - MapControllerImpl._(); - - @override - bool move( - LatLng center, - double zoom, { - Offset offset = Offset.zero, - String? id, - }) => - _state.move( - center, - zoom, - offset: offset, - id: id, - source: MapEventSource.mapController, - ); - - @override - bool rotate(double degree, {String? id}) => - _state.rotate(degree, id: id, source: MapEventSource.mapController); - - @override - MoveAndRotateResult rotateAroundPoint( - double degree, { - CustomPoint? point, - Offset? offset, - String? id, - }) => - _state.rotateAroundPoint( - degree, - point: point, - offset: offset, - id: id, - source: MapEventSource.mapController, - ); - - @override - MoveAndRotateResult moveAndRotate( - LatLng center, - double zoom, - double degree, { - String? id, - }) => - _state.moveAndRotate( - center, - zoom, - degree, - source: MapEventSource.mapController, - id: id, - ); - - @override - bool fitBounds( - LatLngBounds bounds, { - FitBoundsOptions? options = - const FitBoundsOptions(padding: EdgeInsets.all(12)), - }) => - _state.fitBounds(bounds, options!); - - @override - CenterZoom centerZoomFitBounds( - LatLngBounds bounds, { - FitBoundsOptions? options = - const FitBoundsOptions(padding: EdgeInsets.all(12)), - }) => - _state.centerZoomFitBounds(bounds, options!); - - @override - LatLng pointToLatLng(CustomPoint localPoint) => - _state.pointToLatLng(localPoint); - - @override - CustomPoint latLngToScreenPoint(LatLng latLng) => - _state.latLngToScreenPoint(latLng); - - @override - CustomPoint rotatePoint( - CustomPoint mapCenter, - CustomPoint point, { - bool counterRotation = true, - }) => - _state.rotatePoint( - mapCenter.toDoublePoint(), - point.toDoublePoint(), - counterRotation: counterRotation, - ); - - @override - LatLng get center => _state.center; - - @override - LatLngBounds? get bounds => _state.bounds; - - @override - double get zoom => _state.zoom; - - @override - double get rotation => _state.rotation; - - final _mapEventStreamController = StreamController.broadcast(); - - @override - Stream get mapEventStream => _mapEventStreamController.stream; - - @override - StreamSink get mapEventSink => _mapEventStreamController.sink; - - late FlutterMapStateContainer _state; - - @override - set state(FlutterMapStateContainer stateContainer) { - _state = stateContainer; - } - - @override - void dispose() { - _mapEventStreamController.close(); - } -} diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/map_controller_impl.dart new file mode 100644 index 000000000..79f05f346 --- /dev/null +++ b/lib/src/map/map_controller_impl.dart @@ -0,0 +1,144 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/src/geo/latlng_bounds.dart'; +import 'package:flutter_map/src/gestures/flutter_map_state_controller.dart'; +import 'package:flutter_map/src/gestures/map_events.dart'; +import 'package:flutter_map/src/map/flutter_map_state_controller_interface.dart'; +import 'package:flutter_map/src/map/map_controller.dart'; +import 'package:flutter_map/src/misc/center_zoom.dart'; +import 'package:flutter_map/src/misc/fit_bounds_options.dart'; +import 'package:flutter_map/src/misc/move_and_rotate_result.dart'; +import 'package:flutter_map/src/misc/point.dart'; +import 'package:latlong2/latlong.dart'; + +class MapControllerImpl implements MapController { + MapControllerImpl(); + + @override + bool move( + LatLng center, + double zoom, { + Offset offset = Offset.zero, + String? id, + }) => + _stateController.move( + center, + zoom, + offset: offset, + hasGesture: false, + source: MapEventSource.mapController, + id: id, + ); + + @override + bool rotate(double degree, {String? id}) => _stateController.rotate( + degree, + hasGesture: false, + source: MapEventSource.mapController, + id: id, + ); + + @override + MoveAndRotateResult rotateAroundPoint( + double degree, { + CustomPoint? point, + Offset? offset, + String? id, + }) => + _stateController.rotateAroundPoint( + degree, + point: point, + offset: offset, + hasGesture: false, + source: MapEventSource.mapController, + id: id, + ); + + @override + MoveAndRotateResult moveAndRotate( + LatLng center, + double zoom, + double degree, { + String? id, + }) => + _stateController.moveAndRotate( + center, + zoom, + degree, + offset: Offset.zero, + hasGesture: false, + source: MapEventSource.mapController, + id: id, + ); + + @override + bool fitBounds( + LatLngBounds bounds, { + FitBoundsOptions? options = + const FitBoundsOptions(padding: EdgeInsets.all(12)), + }) => + _stateController.fitBounds( + bounds, + options!, + offset: Offset.zero, + ); + + @override + CenterZoom centerZoomFitBounds( + LatLngBounds bounds, { + FitBoundsOptions? options = + const FitBoundsOptions(padding: EdgeInsets.all(12)), + }) => + _stateController.centerZoomFitBounds(bounds, options!); + + @override + LatLng pointToLatLng(CustomPoint localPoint) => + _stateController.pointToLatLng(localPoint); + + @override + CustomPoint latLngToScreenPoint(LatLng latLng) => + _stateController.latLngToScreenPoint(latLng); + + @override + CustomPoint rotatePoint( + CustomPoint mapCenter, + CustomPoint point, { + bool counterRotation = true, + }) => + _stateController.rotatePoint( + mapCenter.toDoublePoint(), + point.toDoublePoint(), + counterRotation: counterRotation, + ); + + @override + LatLng get center => _stateController.center; + + @override + LatLngBounds? get bounds => _stateController.bounds; + + @override + double get zoom => _stateController.zoom; + + @override + double get rotation => _stateController.rotation; + + final _mapEventStreamController = StreamController.broadcast(); + + @override + Stream get mapEventStream => _mapEventStreamController.stream; + + StreamSink get mapEventSink => _mapEventStreamController.sink; + + late FlutterMapStateControllerInterface _stateController; + + set stateController(FlutterMapStateController stateController) { + _stateController = stateController; + } + + @override + void dispose() { + _mapEventStreamController.close(); + } +} diff --git a/lib/src/map/map_safe_area.dart b/lib/src/map/map_safe_area.dart new file mode 100644 index 000000000..db691a24f --- /dev/null +++ b/lib/src/map/map_safe_area.dart @@ -0,0 +1,89 @@ +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flutter_map/src/geo/latlng_bounds.dart'; +import 'package:latlong2/latlong.dart'; + +class MapSafeArea { + final LatLngBounds bounds; + final bool isLatitudeBlocked; + final bool isLongitudeBlocked; + + final double _zoom; + final Size _screenSize; + final LatLng _swPanBoundary; + final LatLng _nePanBoundary; + + MapSafeArea({ + required LatLng southWest, + required LatLng northEast, + required double zoom, + required Size screenSize, + required LatLng swPanBoundary, + required LatLng nePanBoundary, + }) : bounds = LatLngBounds(southWest, northEast), + isLatitudeBlocked = southWest.latitude > northEast.latitude, + isLongitudeBlocked = southWest.longitude > northEast.longitude, + _zoom = zoom, + _screenSize = screenSize, + _swPanBoundary = swPanBoundary, + _nePanBoundary = nePanBoundary; + + factory MapSafeArea.createUnlessMatching({ + MapSafeArea? previous, + required double zoom, + required Size screenSize, + required LatLng swPanBoundary, + required LatLng nePanBoundary, + }) { + if (previous == null || + previous._zoom != zoom || + previous._screenSize != screenSize || + previous._swPanBoundary != swPanBoundary || + previous._nePanBoundary != nePanBoundary) { + final halfScreenHeightDeg = _halfScreenHeightDegrees(screenSize, zoom); + final halfScreenWidthDeg = _halfScreenWidthDegrees(screenSize, zoom); + + final southWestLatitude = swPanBoundary.latitude + halfScreenHeightDeg; + final southWestLongitude = swPanBoundary.longitude + halfScreenWidthDeg; + final northEastLatitude = nePanBoundary.latitude - halfScreenHeightDeg; + final northEastLongitude = nePanBoundary.longitude - halfScreenWidthDeg; + + return MapSafeArea( + southWest: LatLng(southWestLatitude, southWestLongitude), + northEast: LatLng(northEastLatitude, northEastLongitude), + zoom: zoom, + screenSize: screenSize, + swPanBoundary: swPanBoundary, + nePanBoundary: nePanBoundary, + ); + } + return previous; + } + + bool contains(LatLng point) => + isLatitudeBlocked || isLongitudeBlocked ? false : bounds.contains(point); + + LatLng containPoint(LatLng point, LatLng fallback) => LatLng( + isLatitudeBlocked + ? fallback.latitude + : point.latitude.clamp(bounds.south, bounds.north), + isLongitudeBlocked + ? fallback.longitude + : point.longitude.clamp(bounds.west, bounds.east), + ); + + static double _halfScreenWidthDegrees( + Size screenSize, + double zoom, + ) { + final degreesPerPixel = 360 / math.pow(2, zoom + 8); + return (screenSize.width * degreesPerPixel) / 2; + } + + static double _halfScreenHeightDegrees( + Size screenSize, + double zoom, + ) => + (screenSize.height * 170.102258 / math.pow(2, zoom + 8)) / 2; +} diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index e5a6ae14f..129b1c287 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -19,9 +19,10 @@ import 'package:latlong2/latlong.dart'; /// defined boundaries. /// /// If you download offline tiles dynamically, you can set [adaptiveBoundaries] -/// to true (make sure to pass [screenSize] and an external [controller]), which -/// will enforce panning/zooming to ensure there is never a need to display -/// tiles outside the boundaries set by [swPanBoundary] and [nePanBoundary]. +/// to true (make sure to also set [screenSize], [nePanBoundary], +/// [swPanBoundary] and an external [controller]), which will enforce +/// panning/zooming to ensure there is never a need to display tiles outside the +/// boundaries set by [swPanBoundary] and [nePanBoundary]. class MapOptions { final Crs crs; final double zoom; @@ -172,8 +173,12 @@ class MapOptions { assert(rotationThreshold >= 0.0), assert(pinchZoomThreshold >= 0.0), assert(pinchMoveThreshold >= 0.0) { - assert(!adaptiveBoundaries || screenSize != null, - 'screenSize must be set in order to enable adaptive boundaries.'); + assert( + !adaptiveBoundaries || + (screenSize != null && + swPanBoundary != null && + nePanBoundary != null), + 'screenSize, swPanBoundary and nePanBoundary must be set in order to enable adaptive boundaries.'); } } diff --git a/test/flutter_map_test.dart b/test/flutter_map_test.dart index 3f2884cfd..a321cdc98 100644 --- a/test/flutter_map_test.dart +++ b/test/flutter_map_test.dart @@ -45,10 +45,10 @@ void main() { ), children: [ Builder( - builder: (BuildContext context) { + builder: (context) { final _ = FlutterMapState.of(context); builds++; - return Container(); + return const SizedBox.shrink(); }, ), ], From 8be935bc55b0624f5a79b9de863fc34e7778e9d2 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Sat, 10 Jun 2023 12:44:23 +0200 Subject: [PATCH 07/46] Reduce MapController API size and simplify gesture code - Replaced mapState getter with the various getters which were just proxied to MapState. - Heavy refactoring (hopefully without changing behaviour) of gesture code. In passing I have simplified --- .../lib/pages/animated_map_controller.dart | 9 +- example/lib/pages/latlng_to_screen_point.dart | 3 +- example/lib/pages/map_controller.dart | 2 +- example/lib/pages/point_to_latlng.dart | 3 +- .../flutter_map_interactive_viewer.dart | 664 +++++++++--------- .../flutter_map_state_controller.dart | 39 - lib/src/gestures/interactive_flag.dart | 36 +- lib/src/gestures/multi_finger_gesture.dart | 4 + lib/src/map/flutter_map_state.dart | 11 +- ...lutter_map_state_controller_interface.dart | 22 +- lib/src/map/map_controller.dart | 37 +- lib/src/map/map_controller_impl.dart | 41 +- test/flutter_map_controller_test.dart | 70 +- 13 files changed, 430 insertions(+), 511 deletions(-) diff --git a/example/lib/pages/animated_map_controller.dart b/example/lib/pages/animated_map_controller.dart index 0f0b1529f..36ab36047 100644 --- a/example/lib/pages/animated_map_controller.dart +++ b/example/lib/pages/animated_map_controller.dart @@ -35,11 +35,12 @@ class AnimatedMapControllerPageState extends State void _animatedMapMove(LatLng destLocation, double destZoom) { // Create some tweens. These serve to split up the transition from one location to another. // In our case, we want to split the transition be our current map center and the destination. + final mapState = mapController.mapState; final latTween = Tween( - begin: mapController.center.latitude, end: destLocation.latitude); + begin: mapState.center.latitude, end: destLocation.latitude); final lngTween = Tween( - begin: mapController.center.longitude, end: destLocation.longitude); - final zoomTween = Tween(begin: mapController.zoom, end: destZoom); + begin: mapState.center.longitude, end: destLocation.longitude); + final zoomTween = Tween(begin: mapState.zoom, end: destZoom); // Create a animation controller that has a duration and a TickerProvider. final controller = AnimationController( @@ -179,7 +180,7 @@ class AnimatedMapControllerPageState extends State ]); final centerZoom = - mapController.centerZoomFitBounds(bounds); + mapController.mapState.centerZoomFitBounds(bounds); _animatedMapMove(centerZoom.center, centerZoom.zoom); }, child: const Text('Fit Bounds animated'), diff --git a/example/lib/pages/latlng_to_screen_point.dart b/example/lib/pages/latlng_to_screen_point.dart index ce9799f04..8b28a120f 100644 --- a/example/lib/pages/latlng_to_screen_point.dart +++ b/example/lib/pages/latlng_to_screen_point.dart @@ -47,7 +47,8 @@ class _LatLngScreenPointTestPageState extends State { options: MapOptions( onMapEvent: onMapEvent, onTap: (tapPos, latLng) { - final pt1 = _mapController.latLngToScreenPoint(latLng); + final pt1 = + _mapController.mapState.latLngToScreenPoint(latLng); _textPos = CustomPoint(pt1.x, pt1.y); setState(() {}); }, diff --git a/example/lib/pages/map_controller.dart b/example/lib/pages/map_controller.dart index 1aa7086ff..5ba3ae171 100644 --- a/example/lib/pages/map_controller.dart +++ b/example/lib/pages/map_controller.dart @@ -116,7 +116,7 @@ class MapControllerPageState extends State { Builder(builder: (BuildContext context) { return MaterialButton( onPressed: () { - final bounds = _mapController.bounds!; + final bounds = _mapController.mapState.bounds; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( diff --git a/example/lib/pages/point_to_latlng.dart b/example/lib/pages/point_to_latlng.dart index 10f3ac215..fb6230695 100644 --- a/example/lib/pages/point_to_latlng.dart +++ b/example/lib/pages/point_to_latlng.dart @@ -106,7 +106,8 @@ class PointToLatlngPage extends State { void updatePoint(MapEvent? event, BuildContext context) { final pointX = _getPointX(context); setState(() { - latLng = mapController.pointToLatLng(CustomPoint(pointX, pointY)); + latLng = + mapController.mapState.pointToLatLng(CustomPoint(pointX, pointY)); }); } diff --git a/lib/src/gestures/flutter_map_interactive_viewer.dart b/lib/src/gestures/flutter_map_interactive_viewer.dart index 43b3434b6..51dfcb0dc 100644 --- a/lib/src/gestures/flutter_map_interactive_viewer.dart +++ b/lib/src/gestures/flutter_map_interactive_viewer.dart @@ -72,6 +72,10 @@ class FlutterMapInteractiveViewerState int _tapUpCounter = 0; Timer? _doubleTapHoldMaxDelay; + FlutterMapState get _mapState => widget.controller.value; + + MapOptions get _options => widget.options; + @override void initState() { super.initState(); @@ -96,10 +100,9 @@ class FlutterMapInteractiveViewerState @override void didChangeDependencies() { - _gestures = _initializeGestures( + _gestures = _createGestures( MediaQuery.gestureSettingsOf(context), - dragEnabled: InteractiveFlag.hasFlag( - widget.options.interactiveFlags, InteractiveFlag.drag), + dragEnabled: InteractiveFlag.hasDrag(_options.interactiveFlags), ); super.didChangeDependencies(); } @@ -111,32 +114,29 @@ class FlutterMapInteractiveViewerState final oldFlags = oldWidget.options.interactiveFlags; final flags = widget.options.interactiveFlags; - final oldGestures = - _getMultiFingerGestureFlags(mapOptions: oldWidget.options); + final oldGestures = _getMultiFingerGestureFlags(options: oldWidget.options); final gestures = _getMultiFingerGestureFlags(); if (flags != oldFlags) { - _gestures = _initializeGestures( + _gestures = _createGestures( MediaQuery.gestureSettingsOf(context), - dragEnabled: InteractiveFlag.hasFlag( - widget.options.interactiveFlags, InteractiveFlag.drag), + dragEnabled: InteractiveFlag.hasDrag(widget.options.interactiveFlags), ); } if (flags != oldFlags || gestures != oldGestures) { var emitMapEventMoveEnd = false; - if (!InteractiveFlag.hasFlag(flags, InteractiveFlag.flingAnimation)) { + if (!InteractiveFlag.hasFlingAnimation(flags)) { _closeFlingAnimationController(MapEventSource.interactiveFlagsChanged); } - if (!InteractiveFlag.hasFlag(flags, InteractiveFlag.doubleTapZoom)) { + if (!InteractiveFlag.hasDoubleTapZoom(flags)) { _closeDoubleTapController(MapEventSource.interactiveFlagsChanged); } if (_rotationStarted && - !(InteractiveFlag.hasFlag(flags, InteractiveFlag.rotate) && - MultiFingerGesture.hasFlag( - gestures, MultiFingerGesture.rotate))) { + !(InteractiveFlag.hasRotate(flags) && + MultiFingerGesture.hasRotate(gestures))) { _rotationStarted = false; if (_gestureWinner == MultiFingerGesture.rotate) { @@ -147,9 +147,8 @@ class FlutterMapInteractiveViewerState } if (_pinchZoomStarted && - !(InteractiveFlag.hasFlag(flags, InteractiveFlag.pinchZoom) && - MultiFingerGesture.hasFlag( - gestures, MultiFingerGesture.pinchZoom))) { + !(InteractiveFlag.hasPinchZoom(flags) && + MultiFingerGesture.hasPinchZoom(gestures))) { _pinchZoomStarted = false; emitMapEventMoveEnd = true; @@ -159,9 +158,8 @@ class FlutterMapInteractiveViewerState } if (_pinchMoveStarted && - !(InteractiveFlag.hasFlag(flags, InteractiveFlag.pinchMove) && - MultiFingerGesture.hasFlag( - gestures, MultiFingerGesture.pinchMove))) { + !(InteractiveFlag.hasPinchMove(flags) && + MultiFingerGesture.hasPinchMove(gestures))) { _pinchMoveStarted = false; emitMapEventMoveEnd = true; @@ -170,8 +168,7 @@ class FlutterMapInteractiveViewerState } } - if (_dragStarted && - !InteractiveFlag.hasFlag(flags, InteractiveFlag.drag)) { + if (_dragStarted && !InteractiveFlag.hasDrag(flags)) { _dragStarted = false; emitMapEventMoveEnd = true; } @@ -191,74 +188,67 @@ class FlutterMapInteractiveViewerState super.dispose(); } - Map _initializeGestures( + Map _createGestures( DeviceGestureSettings gestureSettings, { required bool dragEnabled, - }) { - final Map gestures = - {}; - - gestures[TapGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(debugOwner: this), - (TapGestureRecognizer instance) { - instance - ..onTapDown = _positionedTapController.onTapDown - ..onTapUp = _handleOnTapUp - ..onTap = _positionedTapController.onTap - ..onSecondaryTap = _positionedTapController.onSecondaryTap - ..onSecondaryTapDown = _positionedTapController.onTapDown; - }, - ); - - gestures[LongPressGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => LongPressGestureRecognizer(debugOwner: this), - (LongPressGestureRecognizer instance) { - instance.onLongPress = _positionedTapController.onLongPress; - }, - ); - - if (dragEnabled) { - gestures[VerticalDragGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => VerticalDragGestureRecognizer(debugOwner: this), - (VerticalDragGestureRecognizer instance) { - instance.onUpdate = (details) { - // Absorbing vertical drags - }; - instance.gestureSettings = gestureSettings; - instance.team ??= _gestureArenaTeam; - }, - ); - gestures[HorizontalDragGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => HorizontalDragGestureRecognizer(debugOwner: this), - (HorizontalDragGestureRecognizer instance) { - instance.onUpdate = (details) { - // Absorbing horizontal drags - }; - instance.gestureSettings = gestureSettings; - instance.team ??= _gestureArenaTeam; - }, - ); - } - - gestures[ScaleGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => ScaleGestureRecognizer(debugOwner: this), - (ScaleGestureRecognizer instance) { - instance - ..onStart = _handleScaleStart - ..onUpdate = _handleScaleUpdate - ..onEnd = _handleScaleEnd; - instance.team ??= _gestureArenaTeam; - _gestureArenaTeam.captain = instance; - }, - ); - - return gestures; - } + }) => + { + TapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(debugOwner: this), + (TapGestureRecognizer instance) { + instance + ..onTapDown = _positionedTapController.onTapDown + ..onTapUp = _handleOnTapUp + ..onTap = _positionedTapController.onTap + ..onSecondaryTap = _positionedTapController.onSecondaryTap + ..onSecondaryTapDown = _positionedTapController.onTapDown; + }, + ), + LongPressGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => LongPressGestureRecognizer(debugOwner: this), + (LongPressGestureRecognizer instance) { + instance.onLongPress = _positionedTapController.onLongPress; + }, + ), + if (dragEnabled) + VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers< + VerticalDragGestureRecognizer>( + () => VerticalDragGestureRecognizer(debugOwner: this), + (VerticalDragGestureRecognizer instance) { + instance.onUpdate = (details) { + // Absorbing vertical drags + }; + instance.gestureSettings = gestureSettings; + instance.team ??= _gestureArenaTeam; + }, + ), + if (dragEnabled) + HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers< + HorizontalDragGestureRecognizer>( + () => HorizontalDragGestureRecognizer(debugOwner: this), + (HorizontalDragGestureRecognizer instance) { + instance.onUpdate = (details) { + // Absorbing horizontal drags + }; + instance.gestureSettings = gestureSettings; + instance.team ??= _gestureArenaTeam; + }, + ), + ScaleGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => ScaleGestureRecognizer(debugOwner: this), + (ScaleGestureRecognizer instance) { + instance + ..onStart = _handleScaleStart + ..onUpdate = _handleScaleUpdate + ..onEnd = _handleScaleEnd; + instance.team ??= _gestureArenaTeam; + _gestureArenaTeam.captain = instance; + }, + ), + }; @override Widget build(BuildContext context) { @@ -274,15 +264,12 @@ class FlutterMapInteractiveViewerState onSecondaryTap: _handleSecondaryTap, onLongPress: _handleLongPress, onDoubleTap: _handleDoubleTap, - doubleTapDelay: InteractiveFlag.hasFlag( - widget.options.interactiveFlags, - InteractiveFlag.doubleTapZoom, - ) + doubleTapDelay: InteractiveFlag.hasDrag(_options.interactiveFlags) ? null : Duration.zero, child: RawGestureDetector( gestures: _gestures, - child: widget.builder(context, widget.controller.value), + child: widget.builder(context, _mapState), ), ), ); @@ -291,41 +278,41 @@ class FlutterMapInteractiveViewerState void _onPointerDown(PointerDownEvent event) { ++_pointerCounter; - if (widget.options.onPointerDown != null) { - final latlng = widget.controller.value.offsetToCrs(event.localPosition); - widget.options.onPointerDown!(event, latlng); + if (_options.onPointerDown != null) { + final latlng = _mapState.offsetToCrs(event.localPosition); + _options.onPointerDown!(event, latlng); } } void _onPointerUp(PointerUpEvent event) { --_pointerCounter; - if (widget.options.onPointerUp != null) { - final latlng = widget.controller.value.offsetToCrs(event.localPosition); - widget.options.onPointerUp!(event, latlng); + if (_options.onPointerUp != null) { + final latlng = _mapState.offsetToCrs(event.localPosition); + _options.onPointerUp!(event, latlng); } } void _onPointerCancel(PointerCancelEvent event) { --_pointerCounter; - if (widget.options.onPointerCancel != null) { - final latlng = widget.controller.value.offsetToCrs(event.localPosition); - widget.options.onPointerCancel!(event, latlng); + if (_options.onPointerCancel != null) { + final latlng = _mapState.offsetToCrs(event.localPosition); + _options.onPointerCancel!(event, latlng); } } void _onPointerHover(PointerHoverEvent event) { - if (widget.options.onPointerHover != null) { - final latlng = widget.controller.value.offsetToCrs(event.localPosition); - widget.options.onPointerHover!(event, latlng); + if (_options.onPointerHover != null) { + final latlng = _mapState.offsetToCrs(event.localPosition); + _options.onPointerHover!(event, latlng); } } void _onPointerSignal(PointerSignalEvent pointerSignal) { // Handle mouse scroll events if the enableScrollWheel parameter is enabled if (pointerSignal is PointerScrollEvent && - widget.options.enableScrollWheel && + _options.enableScrollWheel && pointerSignal.scrollDelta.dy != 0) { // Prevent scrolling of parent/child widgets simultaneously. See // [PointerSignalResolver] documentation for more information. @@ -333,14 +320,13 @@ class FlutterMapInteractiveViewerState pointerSignal, (pointerSignal) { pointerSignal as PointerScrollEvent; - final minZoom = widget.options.minZoom ?? 0.0; - final maxZoom = widget.options.maxZoom ?? double.infinity; - final newZoom = (widget.controller.value.zoom - - pointerSignal.scrollDelta.dy * - widget.options.scrollWheelVelocity) + final minZoom = _options.minZoom ?? 0.0; + final maxZoom = _options.maxZoom ?? double.infinity; + final newZoom = (_mapState.zoom - + pointerSignal.scrollDelta.dy * _options.scrollWheelVelocity) .clamp(minZoom, maxZoom); // Calculate offset of mouse cursor from viewport center - final newCenter = widget.controller.value.focusedZoomCenter( + final newCenter = _mapState.focusedZoomCenter( pointerSignal.localPosition.toCustomPoint(), newZoom, ); @@ -357,30 +343,16 @@ class FlutterMapInteractiveViewerState } } - void _yieldMultiFingerGestureWinner( - int gestureWinner, bool resetStartVariables) { - _gestureWinner = gestureWinner; + int _getMultiFingerGestureFlags({MapOptions? options}) { + options ??= _options; - if (resetStartVariables) { - // note: here we could reset to current values instead of last values - _scaleCorrector = 1.0 - _lastScale; - } - } - - int _getMultiFingerGestureFlags({ - int? gestureWinner, - MapOptions? mapOptions, - }) { - gestureWinner ??= _gestureWinner; - mapOptions ??= widget.options; - - if (mapOptions.enableMultiFingerGestureRace) { - if (gestureWinner == MultiFingerGesture.pinchZoom) { - return mapOptions.pinchZoomWinGestures; - } else if (gestureWinner == MultiFingerGesture.rotate) { - return mapOptions.rotationWinGestures; - } else if (gestureWinner == MultiFingerGesture.pinchMove) { - return mapOptions.pinchMoveWinGestures; + if (options.enableMultiFingerGestureRace) { + if (_gestureWinner == MultiFingerGesture.pinchZoom) { + return options.pinchZoomWinGestures; + } else if (_gestureWinner == MultiFingerGesture.rotate) { + return options.rotationWinGestures; + } else if (_gestureWinner == MultiFingerGesture.pinchMove) { + return options.pinchMoveWinGestures; } return MultiFingerGesture.none; @@ -421,10 +393,10 @@ class FlutterMapInteractiveViewerState _gestureWinner = MultiFingerGesture.none; - _mapZoomStart = widget.controller.value.zoom; - _mapCenterStart = widget.controller.value.center; + _mapZoomStart = _mapState.zoom; + _mapCenterStart = _mapState.center; _focalStartLocal = _lastFocalLocal = details.localFocalPoint; - _focalStartLatLng = widget.controller.value.offsetToCrs(_focalStartLocal); + _focalStartLatLng = _mapState.offsetToCrs(_focalStartLocal); _dragStarted = false; _pinchZoomStarted = false; @@ -442,184 +414,214 @@ class FlutterMapInteractiveViewerState return; } - final eventSource = - _dragMode ? MapEventSource.onDrag : MapEventSource.onMultiFinger; + final currentRotation = radianToDeg(details.rotation); + if (_dragMode) { + _handleScaleDragUpdate(details); + } else if (_multiFingerEnabled) { + _handleScalePinchUpdate(details, currentRotation); + } - final flags = widget.options.interactiveFlags; - final focalOffset = details.localFocalPoint; + _lastRotation = currentRotation; + _lastScale = details.scale; + _lastFocalLocal = details.localFocalPoint; + } - final currentRotation = radianToDeg(details.rotation); + bool get _multiFingerEnabled => + InteractiveFlag.hasMultiFinger(_options.interactiveFlags); - if (_dragMode) { - if (InteractiveFlag.hasFlag(flags, InteractiveFlag.drag)) { - if (!_dragStarted) { - // We could emit start event at [handleScaleStart], however it is - // possible drag will be disabled during ongoing drag then - // [didUpdateWidget] will emit MapEventMoveEnd and if drag is enabled - // again then this will emit the start event again. - _dragStarted = true; - widget.controller.moveStarted(eventSource); - } + bool get _pinchZoomEnabled => + InteractiveFlag.hasPinchZoom(_options.interactiveFlags); + + bool get _rotateEnabled => + InteractiveFlag.hasRotate(_options.interactiveFlags); - final localDistanceOffset = - _rotateOffset(_lastFocalLocal - focalOffset); + bool get _pinchMoveEnabled => + InteractiveFlag.hasPinchMove(_options.interactiveFlags); - widget.controller.dragUpdated(eventSource, localDistanceOffset); + void _handleScaleDragUpdate(ScaleUpdateDetails details) { + const eventSource = MapEventSource.onDrag; + + if (InteractiveFlag.hasDrag(_options.interactiveFlags)) { + if (!_dragStarted) { + // We could emit start event at [handleScaleStart], however it is + // possible drag will be disabled during ongoing drag then + // [didUpdateWidget] will emit MapEventMoveEnd and if drag is enabled + // again then this will emit the start event again. + _dragStarted = true; + widget.controller.moveStarted(eventSource); } - } else { - final hasIntPinchMove = - InteractiveFlag.hasFlag(flags, InteractiveFlag.pinchMove); - final hasIntPinchZoom = - InteractiveFlag.hasFlag(flags, InteractiveFlag.pinchZoom); - final hasIntRotate = - InteractiveFlag.hasFlag(flags, InteractiveFlag.rotate); - - if (hasIntPinchMove || hasIntPinchZoom || hasIntRotate) { - final hasGestureRace = widget.options.enableMultiFingerGestureRace; - - if (hasGestureRace && _gestureWinner == MultiFingerGesture.none) { - if (hasIntPinchZoom && - (_getZoomForScale(_mapZoomStart, details.scale) - _mapZoomStart) - .abs() >= - widget.options.pinchZoomThreshold) { - if (widget.options.debugMultiFingerGestureWinner) { - debugPrint('Multi Finger Gesture winner: Pinch Zoom'); - } - _yieldMultiFingerGestureWinner(MultiFingerGesture.pinchZoom, true); - } else if (hasIntRotate && - currentRotation.abs() >= widget.options.rotationThreshold) { - if (widget.options.debugMultiFingerGestureWinner) { - debugPrint('Multi Finger Gesture winner: Rotate'); - } - _yieldMultiFingerGestureWinner(MultiFingerGesture.rotate, true); - } else if (hasIntPinchMove && - (_focalStartLocal - focalOffset).distance >= - widget.options.pinchMoveThreshold) { - if (widget.options.debugMultiFingerGestureWinner) { - debugPrint('Multi Finger Gesture winner: Pinch Move'); - } - _yieldMultiFingerGestureWinner(MultiFingerGesture.pinchMove, true); - } + + final localDistanceOffset = _rotateOffset( + _lastFocalLocal - details.localFocalPoint, + ); + + widget.controller.dragUpdated(eventSource, localDistanceOffset); + } + } + + void _handleScalePinchUpdate( + ScaleUpdateDetails details, + double currentRotation, + ) { + final hasGestureRace = _options.enableMultiFingerGestureRace; + + if (hasGestureRace && _gestureWinner == MultiFingerGesture.none) { + final gestureWinner = _determineMultiFingerGestureWinner( + _options.rotationThreshold, + currentRotation, + details.scale, + details.localFocalPoint, + ); + if (gestureWinner != null) { + _gestureWinner = gestureWinner; + // note: here we could reset to current values instead of last values + _scaleCorrector = 1.0 - _lastScale; + } + } + + if (!hasGestureRace || _gestureWinner != MultiFingerGesture.none) { + final gestures = _getMultiFingerGestureFlags(); + + final hasPinchZoom = + _pinchZoomEnabled && MultiFingerGesture.hasPinchZoom(gestures); + final hasPinchMove = + _pinchMoveEnabled && MultiFingerGesture.hasPinchMove(gestures); + if (hasPinchZoom || hasPinchMove) { + _handleScalePinchZoomAndMove(details, hasPinchMove, hasPinchZoom); + } + + if (_rotateEnabled && MultiFingerGesture.hasRotate(gestures)) { + _handleScalePinchRotate(details, currentRotation); + } + } + } + + void _handleScalePinchZoomAndMove( + ScaleUpdateDetails details, + bool hasPinchMove, + bool hasPinchZoom, + ) { + double? zoomAfterPinchZoom; + + // Calculate zoom change and handle starting of pinch zoom. + if (hasPinchZoom && details.scale > 0.0) { + zoomAfterPinchZoom = _getZoomForScale( + _mapZoomStart, + details.scale + _scaleCorrector, + ); + + // Handle starting of pinch zoom. + if (!_pinchZoomStarted && zoomAfterPinchZoom != _mapZoomStart) { + _pinchZoomStarted = true; + + if (!_pinchMoveStarted) { + // We want to call moveStart only once for a movement so don't call + // it if a pinch move is already underway. + widget.controller.moveStarted(MapEventSource.onMultiFinger); } + } + } + zoomAfterPinchZoom ??= _mapState.zoom; - if (!hasGestureRace || _gestureWinner != MultiFingerGesture.none) { - final gestures = _getMultiFingerGestureFlags(); - - final hasGesturePinchMove = MultiFingerGesture.hasFlag( - gestures, MultiFingerGesture.pinchMove); - final hasGesturePinchZoom = MultiFingerGesture.hasFlag( - gestures, MultiFingerGesture.pinchZoom); - final hasGestureRotate = - MultiFingerGesture.hasFlag(gestures, MultiFingerGesture.rotate); - - final hasMove = hasIntPinchMove && hasGesturePinchMove; - final hasZoom = hasIntPinchZoom && hasGesturePinchZoom; - final hasRotate = hasIntRotate && hasGestureRotate; - - if (hasMove || hasZoom) { - double newZoom; - // checking details.scale to prevent situation whew details comes - // with zero scale - if (hasZoom && details.scale > 0.0) { - newZoom = _getZoomForScale( - _mapZoomStart, details.scale + _scaleCorrector); - - if (!_pinchZoomStarted) { - if (newZoom != _mapZoomStart) { - _pinchZoomStarted = true; - - if (!_pinchMoveStarted) { - // emit MoveStart event only if pinchMove hasn't started - widget.controller.moveStarted(eventSource); - } - } - } - } else { - newZoom = widget.controller.value.zoom; - } - - LatLng newCenter; - if (hasMove) { - if (!_pinchMoveStarted && _lastFocalLocal != focalOffset) { - _pinchMoveStarted = true; - - if (!_pinchZoomStarted) { - // emit MoveStart event only if pinchZoom hasn't started - widget.controller.moveStarted(eventSource); - } - } - - if (_pinchZoomStarted || _pinchMoveStarted) { - final oldCenterPt = widget.controller.value - .project(widget.controller.value.center, newZoom); - final newFocalLatLong = widget.controller.value - .offsetToCrs(_focalStartLocal, newZoom); - final newFocalPt = - widget.controller.value.project(newFocalLatLong, newZoom); - final oldFocalPt = - widget.controller.value.project(_focalStartLatLng, newZoom); - final zoomDifference = oldFocalPt - newFocalPt; - final moveDifference = - _rotateOffset(_focalStartLocal - _lastFocalLocal); - - final newCenterPt = oldCenterPt + - zoomDifference + - moveDifference.toCustomPoint(); - newCenter = - widget.controller.value.unproject(newCenterPt, newZoom); - } else { - newCenter = widget.controller.value.center; - } - } else { - newCenter = widget.controller.value.center; - } - - if (_pinchZoomStarted || _pinchMoveStarted) { - widget.controller.move( - newCenter, - newZoom, - offset: Offset.zero, - hasGesture: true, - source: eventSource, - id: null, - ); - } - } - - if (hasRotate) { - if (!_rotationStarted && currentRotation != 0.0) { - _rotationStarted = true; - widget.controller.rotateStarted(eventSource); - } - - if (_rotationStarted) { - final rotationDiff = currentRotation - _lastRotation; - final oldCenterPt = widget.controller.value - .project(widget.controller.value.center); - final rotationCenter = widget.controller.value.project( - widget.controller.value.offsetToCrs(_lastFocalLocal)); - final vector = oldCenterPt - rotationCenter; - final rotatedVector = vector.rotate(degToRadian(rotationDiff)); - final newCenter = rotationCenter + rotatedVector; - - widget.controller.moveAndRotate( - widget.controller.value.unproject(newCenter), - widget.controller.value.zoom, - widget.controller.value.rotation + rotationDiff, - offset: Offset.zero, - hasGesture: true, - source: eventSource, - id: null, - ); - } - } + // Handle starting of pinch move. + if (hasPinchMove) { + if (!_pinchMoveStarted && _lastFocalLocal != details.localFocalPoint) { + _pinchMoveStarted = true; + + if (!_pinchZoomStarted) { + // We want to call moveStart only once for a movement so don't call + // it if a pinch zoom is already underway. + widget.controller.moveStarted(MapEventSource.onMultiFinger); } } } - _lastRotation = currentRotation; - _lastScale = details.scale; - _lastFocalLocal = focalOffset; + if (_pinchZoomStarted || _pinchMoveStarted) { + widget.controller.move( + _calculatePinchZoomAndMove(details, zoomAfterPinchZoom), + zoomAfterPinchZoom, + offset: Offset.zero, + hasGesture: true, + source: MapEventSource.onMultiFinger, + id: null, + ); + } + } + + LatLng _calculatePinchZoomAndMove( + ScaleUpdateDetails details, + double zoomAfterPinchZoom, + ) { + final oldCenterPt = _mapState.project(_mapState.center, zoomAfterPinchZoom); + final newFocalLatLong = + _mapState.offsetToCrs(_focalStartLocal, zoomAfterPinchZoom); + final newFocalPt = _mapState.project(newFocalLatLong, zoomAfterPinchZoom); + final oldFocalPt = _mapState.project(_focalStartLatLng, zoomAfterPinchZoom); + final zoomDifference = oldFocalPt - newFocalPt; + final moveDifference = _rotateOffset(_focalStartLocal - _lastFocalLocal); + + final newCenterPt = + oldCenterPt + zoomDifference + moveDifference.toCustomPoint(); + return _mapState.unproject(newCenterPt, zoomAfterPinchZoom); + } + + void _handleScalePinchRotate( + ScaleUpdateDetails details, + double currentRotation, + ) { + if (!_rotationStarted && currentRotation != 0.0) { + _rotationStarted = true; + widget.controller.rotateStarted(MapEventSource.onMultiFinger); + } + + if (_rotationStarted) { + final rotationDiff = currentRotation - _lastRotation; + final oldCenterPt = _mapState.project(_mapState.center); + final rotationCenter = + _mapState.project(_mapState.offsetToCrs(_lastFocalLocal)); + final vector = oldCenterPt - rotationCenter; + final rotatedVector = vector.rotate(degToRadian(rotationDiff)); + final newCenter = rotationCenter + rotatedVector; + + widget.controller.moveAndRotate( + _mapState.unproject(newCenter), + _mapState.zoom, + _mapState.rotation + rotationDiff, + offset: Offset.zero, + hasGesture: true, + source: MapEventSource.onMultiFinger, + id: null, + ); + } + } + + int? _determineMultiFingerGestureWinner(double rotationThreshold, + double currentRotation, double scale, Offset focalOffset) { + final int winner; + if (_pinchZoomEnabled && + (_getZoomForScale(_mapZoomStart, scale) - _mapZoomStart).abs() >= + _options.pinchZoomThreshold) { + if (_options.debugMultiFingerGestureWinner) { + debugPrint('Multi Finger Gesture winner: Pinch Zoom'); + } + winner = MultiFingerGesture.pinchZoom; + } else if (_rotateEnabled && currentRotation.abs() >= rotationThreshold) { + if (_options.debugMultiFingerGestureWinner) { + debugPrint('Multi Finger Gesture winner: Rotate'); + } + winner = MultiFingerGesture.rotate; + } else if (_pinchMoveEnabled && + (_focalStartLocal - focalOffset).distance >= + _options.pinchMoveThreshold) { + if (_options.debugMultiFingerGestureWinner) { + debugPrint('Multi Finger Gesture winner: Pinch Move'); + } + winner = MultiFingerGesture.pinchMove; + } else { + return null; + } + + return winner; } void _handleScaleEnd(ScaleEndDetails details) { @@ -638,8 +640,8 @@ class FlutterMapInteractiveViewerState widget.controller.moveEnded(eventSource); } - final hasFling = InteractiveFlag.hasFlag( - widget.options.interactiveFlags, InteractiveFlag.flingAnimation); + final hasFling = + InteractiveFlag.hasFlingAnimation(_options.interactiveFlags); final magnitude = details.velocity.pixelsPerSecond.distance; if (magnitude < _kMinFlingVelocity || !hasFling) { @@ -649,8 +651,7 @@ class FlutterMapInteractiveViewerState final direction = details.velocity.pixelsPerSecond / magnitude; final distance = (Offset.zero & - Size(widget.controller.value.nonRotatedSize.x, - widget.controller.value.nonRotatedSize.y)) + Size(_mapState.nonRotatedSize.x, _mapState.nonRotatedSize.y)) .shortestSide; final flingOffset = _focalStartLocal - _lastFocalLocal; @@ -680,7 +681,7 @@ class FlutterMapInteractiveViewerState widget.controller.tapped( MapEventSource.tap, position, - widget.controller.value.offsetToCrs(relativePosition), + _mapState.offsetToCrs(relativePosition), ); } @@ -694,7 +695,7 @@ class FlutterMapInteractiveViewerState widget.controller.secondaryTapped( MapEventSource.secondaryTap, position, - widget.controller.value.offsetToCrs(relativePosition), + _mapState.offsetToCrs(relativePosition), ); } @@ -707,7 +708,7 @@ class FlutterMapInteractiveViewerState widget.controller.longPressed( MapEventSource.longPress, position, - widget.controller.value.offsetToCrs(position.relative!), + _mapState.offsetToCrs(position.relative!), ); } @@ -717,10 +718,9 @@ class FlutterMapInteractiveViewerState _closeFlingAnimationController(MapEventSource.doubleTap); _closeDoubleTapController(MapEventSource.doubleTap); - if (InteractiveFlag.hasFlag( - widget.options.interactiveFlags, InteractiveFlag.doubleTapZoom)) { - final newZoom = _getZoomForScale(widget.controller.zoom, 2); - final newCenter = widget.controller.value.focusedZoomCenter( + if (InteractiveFlag.hasDoubleTapZoom(_options.interactiveFlags)) { + final newZoom = _getZoomForScale(_mapState.zoom, 2); + final newCenter = _mapState.focusedZoomCenter( tapPosition.relative!.toCustomPoint(), newZoom, ); @@ -729,12 +729,11 @@ class FlutterMapInteractiveViewerState } void _startDoubleTapAnimation(double newZoom, LatLng newCenter) { - _doubleTapZoomAnimation = - Tween(begin: widget.controller.value.zoom, end: newZoom) - .chain(CurveTween(curve: Curves.linear)) - .animate(_doubleTapController); + _doubleTapZoomAnimation = Tween(begin: _mapState.zoom, end: newZoom) + .chain(CurveTween(curve: Curves.linear)) + .animate(_doubleTapController); _doubleTapCenterAnimation = - LatLngTween(begin: widget.controller.value.center, end: newCenter) + LatLngTween(begin: _mapState.center, end: newCenter) .chain(CurveTween(curve: Curves.linear)) .animate(_doubleTapController); _doubleTapController.forward(from: 0); @@ -778,18 +777,17 @@ class FlutterMapInteractiveViewerState void _handleDoubleTapHold(ScaleUpdateDetails details) { _doubleTapHoldMaxDelay?.cancel(); - final flags = widget.options.interactiveFlags; - if (InteractiveFlag.hasFlag(flags, InteractiveFlag.pinchZoom)) { + final flags = _options.interactiveFlags; + if (InteractiveFlag.hasPinchZoom(flags)) { final verticalOffset = (_focalStartLocal - details.localFocalPoint).dy; - final newZoom = - _mapZoomStart - verticalOffset / 360 * widget.controller.value.zoom; + final newZoom = _mapZoomStart - verticalOffset / 360 * _mapState.zoom; - final min = widget.options.minZoom ?? 0.0; - final max = widget.options.maxZoom ?? double.infinity; + final min = _options.minZoom ?? 0.0; + final max = _options.maxZoom ?? double.infinity; final actualZoom = math.max(min, math.min(max, newZoom)); widget.controller.move( - widget.controller.value.center, + _mapState.center, actualZoom, offset: Offset.zero, hasGesture: true, @@ -806,15 +804,13 @@ class FlutterMapInteractiveViewerState _startListeningForAnimationInterruptions(); } - final newCenterPoint = widget.controller.value.project(_mapCenterStart) + - _flingAnimation.value - .toCustomPoint() - .rotate(widget.controller.value.rotationRad); - final newCenter = widget.controller.value.unproject(newCenterPoint); + final newCenterPoint = _mapState.project(_mapCenterStart) + + _flingAnimation.value.toCustomPoint().rotate(_mapState.rotationRad); + final newCenter = _mapState.unproject(newCenterPoint); widget.controller.move( newCenter, - widget.controller.value.zoom, + _mapState.zoom, offset: Offset.zero, hasGesture: true, source: MapEventSource.flingAnimationController, @@ -853,11 +849,11 @@ class FlutterMapInteractiveViewerState double _getZoomForScale(double startZoom, double scale) { final resultZoom = scale == 1.0 ? startZoom : startZoom + math.log(scale) / math.ln2; - return widget.controller.value.fitZoomToBounds(resultZoom); + return _mapState.fitZoomToBounds(resultZoom); } Offset _rotateOffset(Offset offset) { - final radians = widget.controller.value.rotationRad; + final radians = _mapState.rotationRad; if (radians != 0.0) { final cos = math.cos(radians); final sin = math.sin(radians); diff --git a/lib/src/gestures/flutter_map_state_controller.dart b/lib/src/gestures/flutter_map_state_controller.dart index b56ace710..0963422c7 100644 --- a/lib/src/gestures/flutter_map_state_controller.dart +++ b/lib/src/gestures/flutter_map_state_controller.dart @@ -243,45 +243,6 @@ class FlutterMapStateController extends ValueNotifier ); } - @override - LatLng get center => value.center; - - @override - double get zoom => value.zoom; - - @override - double get rotation => value.rotation; - - @override - LatLngBounds? get bounds => value.bounds; - - @override - CenterZoom centerZoomFitBounds( - LatLngBounds bounds, - FitBoundsOptions options, - ) => - value.centerZoomFitBounds(bounds, options); - - @override - LatLng pointToLatLng(CustomPoint localPoint) => - value.pointToLatLng(localPoint); - - @override - CustomPoint latLngToScreenPoint(LatLng latLng) => - value.latLngToScreenPoint(latLng); - - @override - CustomPoint rotatePoint( - CustomPoint mapCenter, - CustomPoint point, { - required bool counterRotation, - }) => - value.rotatePoint( - mapCenter.toDoublePoint(), - point.toDoublePoint(), - counterRotation: counterRotation, - ); - bool setNonRotatedSizeWithoutEmittingEvent( CustomPoint nonRotatedSize, ) { diff --git a/lib/src/gestures/interactive_flag.dart b/lib/src/gestures/interactive_flag.dart index a9e60813a..561557932 100644 --- a/lib/src/gestures/interactive_flag.dart +++ b/lib/src/gestures/interactive_flag.dart @@ -13,24 +13,27 @@ class InteractiveFlag { drag | flingAnimation | pinchMove | pinchZoom | doubleTapZoom | rotate; static const int none = 0; - // enable move with one finger + // Enable move with one finger. static const int drag = 1 << 0; - // enable fling animation when drag or pinchMove have enough Fling Velocity + // Enable fling animation when drag or pinchMove have enough Fling Velocity. static const int flingAnimation = 1 << 1; - // enable move with two or more fingers + // Enable move with two or more fingers. static const int pinchMove = 1 << 2; - // enable pinch zoom + // Enable pinch zoom. static const int pinchZoom = 1 << 3; - // enable double tap zoom animation + // Enable double tap zoom animation. static const int doubleTapZoom = 1 << 4; - // enable map rotate + /// Enable map rotate. static const int rotate = 1 << 5; + /// Flags pertaining to gestures which require multiple fingers. + static const _multiFingerFlags = pinchMove | pinchZoom | rotate; + /// Returns `true` if [leftFlags] has at least one member in [rightFlags] /// (intersection) for example [leftFlags]= [InteractiveFlag.drag] | /// [InteractiveFlag.rotate] and [rightFlags]= [InteractiveFlag.rotate] | @@ -39,4 +42,25 @@ class InteractiveFlag { static bool hasFlag(int leftFlags, int rightFlags) { return leftFlags & rightFlags != 0; } + + /// True if any multi-finger gesture flags are enabled. + static bool hasMultiFinger(int flags) => hasFlag(flags, _multiFingerFlags); + + /// True if the [drag] interactive flag is enabled. + static bool hasDrag(int flags) => hasFlag(flags, drag); + + /// True if the [flingAnimation] interactive flag is enabled. + static bool hasFlingAnimation(int flags) => hasFlag(flags, flingAnimation); + + /// True if the [pinchMove] interactive flag is enabled. + static bool hasPinchMove(int flags) => hasFlag(flags, pinchMove); + + /// True if the [pinchZoom] interactive flag is enabled. + static bool hasPinchZoom(int flags) => hasFlag(flags, pinchZoom); + + /// True if the [doubleTapZoom] interactive flag is enabled. + static bool hasDoubleTapZoom(int flags) => hasFlag(flags, doubleTapZoom); + + /// True if the [rotate] interactive flag is enabled. + static bool hasRotate(int flags) => hasFlag(flags, rotate); } diff --git a/lib/src/gestures/multi_finger_gesture.dart b/lib/src/gestures/multi_finger_gesture.dart index fa7d66bfa..5fa8a7dca 100644 --- a/lib/src/gestures/multi_finger_gesture.dart +++ b/lib/src/gestures/multi_finger_gesture.dart @@ -26,4 +26,8 @@ class MultiFingerGesture { static bool hasFlag(int leftFlags, int rightFlags) { return leftFlags & rightFlags != 0; } + + static bool hasPinchMove(int gestures) => hasFlag(gestures, pinchMove); + static bool hasPinchZoom(int gestures) => hasFlag(gestures, pinchZoom); + static bool hasRotate(int gestures) => hasFlag(gestures, rotate); } diff --git a/lib/src/map/flutter_map_state.dart b/lib/src/map/flutter_map_state.dart index 577e9bfa9..b77670c0a 100644 --- a/lib/src/map/flutter_map_state.dart +++ b/lib/src/map/flutter_map_state.dart @@ -154,9 +154,10 @@ class FlutterMapState { double get rotationRad => degToRadian(rotation); CenterZoom centerZoomFitBounds( - LatLngBounds bounds, - FitBoundsOptions options, - ) => + LatLngBounds bounds, { + FitBoundsOptions options = + const FitBoundsOptions(padding: EdgeInsets.all(12)), + }) => getBoundsCenterZoom(bounds, options); double getBoundsZoom( @@ -197,7 +198,9 @@ class FlutterMapState { } CenterZoom getBoundsCenterZoom( - LatLngBounds bounds, FitBoundsOptions options) { + LatLngBounds bounds, + FitBoundsOptions options, + ) { final paddingTL = CustomPoint(options.padding.left, options.padding.top); final paddingBR = diff --git a/lib/src/map/flutter_map_state_controller_interface.dart b/lib/src/map/flutter_map_state_controller_interface.dart index 5c6ce6c2e..ba597a83b 100644 --- a/lib/src/map/flutter_map_state_controller_interface.dart +++ b/lib/src/map/flutter_map_state_controller_interface.dart @@ -1,7 +1,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; -import 'package:flutter_map/src/misc/center_zoom.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; import 'package:flutter_map/src/misc/move_and_rotate_result.dart'; import 'package:flutter_map/src/misc/point.dart'; @@ -10,10 +10,7 @@ import 'package:latlong2/latlong.dart'; /// Defines the methods that the MapController may call on the /// FlutterMapStateController. This clarifies the link between the two classes. abstract interface class FlutterMapStateControllerInterface { - LatLng get center; - double get zoom; - double get rotation; - LatLngBounds? get bounds; + FlutterMapState get value; bool move( LatLng newCenter, @@ -55,19 +52,4 @@ abstract interface class FlutterMapStateControllerInterface { FitBoundsOptions options, { required Offset offset, }); - - CenterZoom centerZoomFitBounds( - LatLngBounds bounds, - FitBoundsOptions options, - ); - - LatLng pointToLatLng(CustomPoint localPoint); - - CustomPoint latLngToScreenPoint(LatLng latLng); - - CustomPoint rotatePoint( - CustomPoint mapCenter, - CustomPoint point, { - required bool counterRotation, - }); } diff --git a/lib/src/map/map_controller.dart b/lib/src/map/map_controller.dart index ede173ae9..d8ea6fee6 100644 --- a/lib/src/map/map_controller.dart +++ b/lib/src/map/map_controller.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; import 'package:latlong2/latlong.dart'; @@ -126,40 +127,8 @@ abstract class MapController { /// documentation. bool fitBounds(LatLngBounds bounds, {FitBoundsOptions? options}); - /// Calculates the appropriate center and zoom level for the map to perfectly - /// fit [bounds], with additional configurable [options] - /// - /// Does not move/zoom the map: see [fitBounds]. - CenterZoom centerZoomFitBounds( - LatLngBounds bounds, { - FitBoundsOptions? options, - }); - - /// Convert a screen point (x/y) to its corresponding map coordinate (lat/lng), - /// based on the map's current properties - LatLng pointToLatLng(CustomPoint screenPoint); - - /// Convert a map coordinate (lat/lng) to its corresponding screen point (x/y), - /// based on the map's current screen positioning - CustomPoint latLngToScreenPoint(LatLng mapCoordinate); - - CustomPoint rotatePoint( - CustomPoint mapCenter, - CustomPoint point, { - bool counterRotation = true, - }); - - /// Current center coordinates - LatLng get center; - - /// Current outer points/boundaries coordinates - LatLngBounds? get bounds; - - /// Current zoom level - double get zoom; - - /// Current rotation in degrees, where 0° is North - double get rotation; + /// Current FlutterMapState. + FlutterMapState get mapState; /// [Stream] of all emitted [MapEvent]s Stream get mapEventStream; diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/map_controller_impl.dart index 79f05f346..955af084d 100644 --- a/lib/src/map/map_controller_impl.dart +++ b/lib/src/map/map_controller_impl.dart @@ -4,9 +4,9 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/gestures/flutter_map_state_controller.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:flutter_map/src/map/flutter_map_state_controller_interface.dart'; import 'package:flutter_map/src/map/map_controller.dart'; -import 'package:flutter_map/src/misc/center_zoom.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; import 'package:flutter_map/src/misc/move_and_rotate_result.dart'; import 'package:flutter_map/src/misc/point.dart'; @@ -85,44 +85,7 @@ class MapControllerImpl implements MapController { ); @override - CenterZoom centerZoomFitBounds( - LatLngBounds bounds, { - FitBoundsOptions? options = - const FitBoundsOptions(padding: EdgeInsets.all(12)), - }) => - _stateController.centerZoomFitBounds(bounds, options!); - - @override - LatLng pointToLatLng(CustomPoint localPoint) => - _stateController.pointToLatLng(localPoint); - - @override - CustomPoint latLngToScreenPoint(LatLng latLng) => - _stateController.latLngToScreenPoint(latLng); - - @override - CustomPoint rotatePoint( - CustomPoint mapCenter, - CustomPoint point, { - bool counterRotation = true, - }) => - _stateController.rotatePoint( - mapCenter.toDoublePoint(), - point.toDoublePoint(), - counterRotation: counterRotation, - ); - - @override - LatLng get center => _stateController.center; - - @override - LatLngBounds? get bounds => _stateController.bounds; - - @override - double get zoom => _stateController.zoom; - - @override - double get rotation => _stateController.rotation; + FlutterMapState get mapState => _stateController.value; final _mapEventStreamController = StreamController.broadcast(); diff --git a/test/flutter_map_controller_test.dart b/test/flutter_map_controller_test.dart index 037668f84..5d12e92cd 100644 --- a/test/flutter_map_controller_test.dart +++ b/test/flutter_map_controller_test.dart @@ -28,18 +28,21 @@ void main() { ); const expectedZoom = 7.451812751543818; - final fit = controller.centerZoomFitBounds(bounds, options: fitOptions); + var mapState = controller.mapState; + final fit = mapState.centerZoomFitBounds(bounds, options: fitOptions); controller.move(fit.center, fit.zoom); await tester.pump(); - expect(controller.bounds, equals(expectedBounds)); - expect(controller.center, equals(expectedCenter)); - expect(controller.zoom, equals(expectedZoom)); + mapState = controller.mapState; + expect(mapState.bounds, equals(expectedBounds)); + expect(mapState.center, equals(expectedCenter)); + expect(mapState.zoom, equals(expectedZoom)); controller.fitBounds(bounds, options: fitOptions); await tester.pump(); - expect(controller.bounds, equals(expectedBounds)); - expect(controller.center, equals(expectedCenter)); - expect(controller.zoom, equals(expectedZoom)); + mapState = controller.mapState; + expect(mapState.bounds, equals(expectedBounds)); + expect(mapState.center, equals(expectedCenter)); + expect(mapState.zoom, equals(expectedZoom)); } { @@ -53,18 +56,21 @@ void main() { ); const expectedZoom = 7; - final fit = controller.centerZoomFitBounds(bounds, options: fitOptions); + var mapState = controller.mapState; + final fit = mapState.centerZoomFitBounds(bounds, options: fitOptions); controller.move(fit.center, fit.zoom); await tester.pump(); - expect(controller.bounds, equals(expectedBounds)); - expect(controller.center, equals(expectedCenter)); - expect(controller.zoom, equals(expectedZoom)); + mapState = controller.mapState; + expect(mapState.bounds, equals(expectedBounds)); + expect(mapState.center, equals(expectedCenter)); + expect(mapState.zoom, equals(expectedZoom)); controller.fitBounds(bounds, options: fitOptions); await tester.pump(); - expect(controller.bounds, equals(expectedBounds)); - expect(controller.center, equals(expectedCenter)); - expect(controller.zoom, equals(expectedZoom)); + mapState = controller.mapState; + expect(mapState.bounds, equals(expectedBounds)); + expect(mapState.center, equals(expectedCenter)); + expect(mapState.zoom, equals(expectedZoom)); } { @@ -78,19 +84,23 @@ void main() { ); const expectedZoom = 8.135709286104404; - final fit = controller.centerZoomFitBounds(bounds, options: fitOptions); + var mapState = controller.mapState; + final fit = mapState.centerZoomFitBounds(bounds, options: fitOptions); controller.move(fit.center, fit.zoom); await tester.pump(); - expect(controller.bounds, equals(expectedBounds)); - expect(controller.center, equals(expectedCenter)); - expect(controller.zoom, equals(expectedZoom)); + mapState = controller.mapState; + expect(mapState.bounds, equals(expectedBounds)); + expect(mapState.center, equals(expectedCenter)); + expect(mapState.zoom, equals(expectedZoom)); controller.fitBounds(bounds, options: fitOptions); await tester.pump(); - expect(controller.bounds, equals(expectedBounds)); - expect(controller.center, equals(expectedCenter)); - expect(controller.zoom, equals(expectedZoom)); + + mapState = controller.mapState; + expect(mapState.bounds, equals(expectedBounds)); + expect(mapState.center, equals(expectedCenter)); + expect(mapState.zoom, equals(expectedZoom)); } { @@ -105,18 +115,22 @@ void main() { ); const expectedZoom = 9; - final fit = controller.centerZoomFitBounds(bounds, options: fitOptions); + var mapState = controller.mapState; + final fit = mapState.centerZoomFitBounds(bounds, options: fitOptions); controller.move(fit.center, fit.zoom); await tester.pump(); - expect(controller.bounds, equals(expectedBounds)); - expect(controller.center, equals(expectedCenter)); - expect(controller.zoom, equals(expectedZoom)); + mapState = controller.mapState; + expect(mapState.bounds, equals(expectedBounds)); + expect(mapState.center, equals(expectedCenter)); + expect(mapState.zoom, equals(expectedZoom)); controller.fitBounds(bounds, options: fitOptions); await tester.pump(); - expect(controller.bounds, equals(expectedBounds)); - expect(controller.center, equals(expectedCenter)); - expect(controller.zoom, equals(expectedZoom)); + + mapState = controller.mapState; + expect(mapState.bounds, equals(expectedBounds)); + expect(mapState.center, equals(expectedCenter)); + expect(mapState.zoom, equals(expectedZoom)); } }); testWidgets('test fit bounds methods with rotation', (tester) async { From 21ae26579b656b5d5036da08ff55668d029d0e30 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Sat, 10 Jun 2023 12:56:27 +0200 Subject: [PATCH 08/46] Remove unnecessary getters now that InteractiveFlags defines convenience methods for checking single flags --- .../flutter_map_interactive_viewer.dart | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/lib/src/gestures/flutter_map_interactive_viewer.dart b/lib/src/gestures/flutter_map_interactive_viewer.dart index 51dfcb0dc..f8a2a62b1 100644 --- a/lib/src/gestures/flutter_map_interactive_viewer.dart +++ b/lib/src/gestures/flutter_map_interactive_viewer.dart @@ -417,7 +417,7 @@ class FlutterMapInteractiveViewerState final currentRotation = radianToDeg(details.rotation); if (_dragMode) { _handleScaleDragUpdate(details); - } else if (_multiFingerEnabled) { + } else if (InteractiveFlag.hasMultiFinger(_options.interactiveFlags)) { _handleScalePinchUpdate(details, currentRotation); } @@ -426,18 +426,6 @@ class FlutterMapInteractiveViewerState _lastFocalLocal = details.localFocalPoint; } - bool get _multiFingerEnabled => - InteractiveFlag.hasMultiFinger(_options.interactiveFlags); - - bool get _pinchZoomEnabled => - InteractiveFlag.hasPinchZoom(_options.interactiveFlags); - - bool get _rotateEnabled => - InteractiveFlag.hasRotate(_options.interactiveFlags); - - bool get _pinchMoveEnabled => - InteractiveFlag.hasPinchMove(_options.interactiveFlags); - void _handleScaleDragUpdate(ScaleUpdateDetails details) { const eventSource = MapEventSource.onDrag; @@ -483,14 +471,17 @@ class FlutterMapInteractiveViewerState final gestures = _getMultiFingerGestureFlags(); final hasPinchZoom = - _pinchZoomEnabled && MultiFingerGesture.hasPinchZoom(gestures); + InteractiveFlag.hasPinchZoom(_options.interactiveFlags) && + MultiFingerGesture.hasPinchZoom(gestures); final hasPinchMove = - _pinchMoveEnabled && MultiFingerGesture.hasPinchMove(gestures); + InteractiveFlag.hasPinchMove(_options.interactiveFlags) && + MultiFingerGesture.hasPinchMove(gestures); if (hasPinchZoom || hasPinchMove) { _handleScalePinchZoomAndMove(details, hasPinchMove, hasPinchZoom); } - if (_rotateEnabled && MultiFingerGesture.hasRotate(gestures)) { + if (InteractiveFlag.hasRotate(_options.interactiveFlags) && + MultiFingerGesture.hasRotate(gestures)) { _handleScalePinchRotate(details, currentRotation); } } @@ -598,19 +589,20 @@ class FlutterMapInteractiveViewerState int? _determineMultiFingerGestureWinner(double rotationThreshold, double currentRotation, double scale, Offset focalOffset) { final int winner; - if (_pinchZoomEnabled && + if (InteractiveFlag.hasPinchZoom(_options.interactiveFlags) && (_getZoomForScale(_mapZoomStart, scale) - _mapZoomStart).abs() >= _options.pinchZoomThreshold) { if (_options.debugMultiFingerGestureWinner) { debugPrint('Multi Finger Gesture winner: Pinch Zoom'); } winner = MultiFingerGesture.pinchZoom; - } else if (_rotateEnabled && currentRotation.abs() >= rotationThreshold) { + } else if (InteractiveFlag.hasRotate(_options.interactiveFlags) && + currentRotation.abs() >= rotationThreshold) { if (_options.debugMultiFingerGestureWinner) { debugPrint('Multi Finger Gesture winner: Rotate'); } winner = MultiFingerGesture.rotate; - } else if (_pinchMoveEnabled && + } else if (InteractiveFlag.hasPinchMove(_options.interactiveFlags) && (_focalStartLocal - focalOffset).distance >= _options.pinchMoveThreshold) { if (_options.debugMultiFingerGestureWinner) { From 5d4298c0843f0e4883acd09d8ab3d1884567c12a Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Sat, 10 Jun 2023 13:11:34 +0200 Subject: [PATCH 09/46] Fix double tap zoom not working when drag was enabled and prevent pinch move when only pinch zoom is enabled --- .../flutter_map_interactive_viewer.dart | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/lib/src/gestures/flutter_map_interactive_viewer.dart b/lib/src/gestures/flutter_map_interactive_viewer.dart index f8a2a62b1..bfd751c46 100644 --- a/lib/src/gestures/flutter_map_interactive_viewer.dart +++ b/lib/src/gestures/flutter_map_interactive_viewer.dart @@ -264,9 +264,10 @@ class FlutterMapInteractiveViewerState onSecondaryTap: _handleSecondaryTap, onLongPress: _handleLongPress, onDoubleTap: _handleDoubleTap, - doubleTapDelay: InteractiveFlag.hasDrag(_options.interactiveFlags) - ? null - : Duration.zero, + doubleTapDelay: + InteractiveFlag.hasDoubleTapZoom(_options.interactiveFlags) + ? null + : Duration.zero, child: RawGestureDetector( gestures: _gestures, child: widget.builder(context, _mapState), @@ -418,7 +419,7 @@ class FlutterMapInteractiveViewerState if (_dragMode) { _handleScaleDragUpdate(details); } else if (InteractiveFlag.hasMultiFinger(_options.interactiveFlags)) { - _handleScalePinchUpdate(details, currentRotation); + _handleScaleMultiFingerUpdate(details, currentRotation); } _lastRotation = currentRotation; @@ -447,7 +448,7 @@ class FlutterMapInteractiveViewerState } } - void _handleScalePinchUpdate( + void _handleScaleMultiFingerUpdate( ScaleUpdateDetails details, double currentRotation, ) { @@ -477,7 +478,7 @@ class FlutterMapInteractiveViewerState InteractiveFlag.hasPinchMove(_options.interactiveFlags) && MultiFingerGesture.hasPinchMove(gestures); if (hasPinchZoom || hasPinchMove) { - _handleScalePinchZoomAndMove(details, hasPinchMove, hasPinchZoom); + _handleScalePinchZoomAndMove(details, hasPinchZoom, hasPinchMove); } if (InteractiveFlag.hasRotate(_options.interactiveFlags) && @@ -489,20 +490,21 @@ class FlutterMapInteractiveViewerState void _handleScalePinchZoomAndMove( ScaleUpdateDetails details, - bool hasPinchMove, bool hasPinchZoom, + bool hasPinchMove, ) { - double? zoomAfterPinchZoom; + LatLng newCenter = _mapState.center; + double newZoom = _mapState.zoom; - // Calculate zoom change and handle starting of pinch zoom. + // Handle pinch zoom. if (hasPinchZoom && details.scale > 0.0) { - zoomAfterPinchZoom = _getZoomForScale( + newZoom = _getZoomForScale( _mapZoomStart, details.scale + _scaleCorrector, ); // Handle starting of pinch zoom. - if (!_pinchZoomStarted && zoomAfterPinchZoom != _mapZoomStart) { + if (!_pinchZoomStarted && newZoom != _mapZoomStart) { _pinchZoomStarted = true; if (!_pinchMoveStarted) { @@ -512,10 +514,11 @@ class FlutterMapInteractiveViewerState } } } - zoomAfterPinchZoom ??= _mapState.zoom; - // Handle starting of pinch move. + // Handle pinch move. if (hasPinchMove) { + newCenter = _calculatePinchZoomAndMove(details, newZoom); + if (!_pinchMoveStarted && _lastFocalLocal != details.localFocalPoint) { _pinchMoveStarted = true; @@ -529,8 +532,8 @@ class FlutterMapInteractiveViewerState if (_pinchZoomStarted || _pinchMoveStarted) { widget.controller.move( - _calculatePinchZoomAndMove(details, zoomAfterPinchZoom), - zoomAfterPinchZoom, + newCenter, + newZoom, offset: Offset.zero, hasGesture: true, source: MapEventSource.onMultiFinger, @@ -710,7 +713,9 @@ class FlutterMapInteractiveViewerState _closeFlingAnimationController(MapEventSource.doubleTap); _closeDoubleTapController(MapEventSource.doubleTap); + debugPrint('CHECKING'); if (InteractiveFlag.hasDoubleTapZoom(_options.interactiveFlags)) { + debugPrint('YES'); final newZoom = _getZoomForScale(_mapState.zoom, 2); final newCenter = _mapState.focusedZoomCenter( tapPosition.relative!.toCustomPoint(), From b1fba183cece6648029564b7ad5418d61a424896 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Sat, 10 Jun 2023 13:11:49 +0200 Subject: [PATCH 10/46] Use new InteractiveFlag convenience methods --- example/lib/pages/interactive_test_page.dart | 24 ++++++++------------ 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/example/lib/pages/interactive_test_page.dart b/example/lib/pages/interactive_test_page.dart index 9a8283236..5b9997291 100644 --- a/example/lib/pages/interactive_test_page.dart +++ b/example/lib/pages/interactive_test_page.dart @@ -59,7 +59,7 @@ class _InteractiveTestPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ MaterialButton( - color: InteractiveFlag.hasFlag(flags, InteractiveFlag.drag) + color: InteractiveFlag.hasDrag(flags) ? Colors.greenAccent : Colors.redAccent, onPressed: () { @@ -70,8 +70,7 @@ class _InteractiveTestPageState extends State { child: const Text('Drag'), ), MaterialButton( - color: InteractiveFlag.hasFlag( - flags, InteractiveFlag.flingAnimation) + color: InteractiveFlag.hasFlingAnimation(flags) ? Colors.greenAccent : Colors.redAccent, onPressed: () { @@ -82,10 +81,9 @@ class _InteractiveTestPageState extends State { child: const Text('Fling'), ), MaterialButton( - color: - InteractiveFlag.hasFlag(flags, InteractiveFlag.pinchMove) - ? Colors.greenAccent - : Colors.redAccent, + color: InteractiveFlag.hasPinchMove(flags) + ? Colors.greenAccent + : Colors.redAccent, onPressed: () { setState(() { updateFlags(InteractiveFlag.pinchMove); @@ -99,8 +97,7 @@ class _InteractiveTestPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ MaterialButton( - color: InteractiveFlag.hasFlag( - flags, InteractiveFlag.doubleTapZoom) + color: InteractiveFlag.hasDoubleTapZoom(flags) ? Colors.greenAccent : Colors.redAccent, onPressed: () { @@ -111,7 +108,7 @@ class _InteractiveTestPageState extends State { child: const Text('Double tap zoom'), ), MaterialButton( - color: InteractiveFlag.hasFlag(flags, InteractiveFlag.rotate) + color: InteractiveFlag.hasRotate(flags) ? Colors.greenAccent : Colors.redAccent, onPressed: () { @@ -122,10 +119,9 @@ class _InteractiveTestPageState extends State { child: const Text('Rotate'), ), MaterialButton( - color: - InteractiveFlag.hasFlag(flags, InteractiveFlag.pinchZoom) - ? Colors.greenAccent - : Colors.redAccent, + color: InteractiveFlag.hasPinchZoom(flags) + ? Colors.greenAccent + : Colors.redAccent, onPressed: () { setState(() { updateFlags(InteractiveFlag.pinchZoom); From 32d57e034251770f6ae70e454687580a2685b6cc Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Sun, 11 Jun 2023 11:28:03 +0200 Subject: [PATCH 11/46] Combine getBoundsCenterZoom and centerZoomFitBounds --- .../lib/pages/zoombuttons_plugin_option.dart | 8 +- .../flutter_map_state_controller.dart | 2 +- lib/src/map/flutter_map_state.dart | 80 +++++++++---------- 3 files changed, 44 insertions(+), 46 deletions(-) diff --git a/example/lib/pages/zoombuttons_plugin_option.dart b/example/lib/pages/zoombuttons_plugin_option.dart index 0d1ac33a6..e51b3083c 100644 --- a/example/lib/pages/zoombuttons_plugin_option.dart +++ b/example/lib/pages/zoombuttons_plugin_option.dart @@ -49,7 +49,8 @@ class FlutterMapZoomButtons extends StatelessWidget { backgroundColor: zoomInColor ?? Theme.of(context).primaryColor, onPressed: () { final bounds = map.bounds; - final centerZoom = map.getBoundsCenterZoom(bounds, options); + final centerZoom = + map.centerZoomFitBounds(bounds, options: options); var zoom = centerZoom.zoom + 1; if (zoom > maxZoom) { zoom = maxZoom; @@ -68,7 +69,10 @@ class FlutterMapZoomButtons extends StatelessWidget { backgroundColor: zoomOutColor ?? Theme.of(context).primaryColor, onPressed: () { final bounds = map.bounds; - final centerZoom = map.getBoundsCenterZoom(bounds, options); + final centerZoom = map.centerZoomFitBounds( + bounds, + options: options, + ); var zoom = centerZoom.zoom - 1; if (zoom < minZoom) { zoom = minZoom; diff --git a/lib/src/gestures/flutter_map_state_controller.dart b/lib/src/gestures/flutter_map_state_controller.dart index 0963422c7..d9b8bc5be 100644 --- a/lib/src/gestures/flutter_map_state_controller.dart +++ b/lib/src/gestures/flutter_map_state_controller.dart @@ -232,7 +232,7 @@ class FlutterMapStateController extends ValueNotifier FitBoundsOptions options, { required Offset offset, }) { - final target = value.getBoundsCenterZoom(bounds, options); + final target = value.centerZoomFitBounds(bounds, options: options); return move( target.center, target.zoom, diff --git a/lib/src/map/flutter_map_state.dart b/lib/src/map/flutter_map_state.dart index b77670c0a..c45f259a3 100644 --- a/lib/src/map/flutter_map_state.dart +++ b/lib/src/map/flutter_map_state.dart @@ -157,50 +157,7 @@ class FlutterMapState { LatLngBounds bounds, { FitBoundsOptions options = const FitBoundsOptions(padding: EdgeInsets.all(12)), - }) => - getBoundsCenterZoom(bounds, options); - - double getBoundsZoom( - LatLngBounds bounds, - CustomPoint padding, { - bool inside = false, - bool forceIntegerZoomLevel = false, }) { - final min = options.minZoom ?? 0.0; - final max = options.maxZoom ?? double.infinity; - final nw = bounds.northWest; - final se = bounds.southEast; - var size = nonrotatedSize - padding; - // Prevent negative size which results in NaN zoom value later on in the calculation - size = CustomPoint(math.max(0, size.x), math.max(0, size.y)); - var boundsSize = Bounds(project(se, zoom), project(nw, zoom)).size; - if (rotation != 0.0) { - final cosAngle = math.cos(rotationRad).abs(); - final sinAngle = math.sin(rotationRad).abs(); - boundsSize = CustomPoint( - (boundsSize.x * cosAngle) + (boundsSize.y * sinAngle), - (boundsSize.y * cosAngle) + (boundsSize.x * sinAngle), - ); - } - - final scaleX = size.x / boundsSize.x; - final scaleY = size.y / boundsSize.y; - final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); - - var boundsZoom = getScaleZoom(scale, zoom); - - if (forceIntegerZoomLevel) { - boundsZoom = - inside ? boundsZoom.ceilToDouble() : boundsZoom.floorToDouble(); - } - - return math.max(min, math.min(max, boundsZoom)); - } - - CenterZoom getBoundsCenterZoom( - LatLngBounds bounds, - FitBoundsOptions options, - ) { final paddingTL = CustomPoint(options.padding.left, options.padding.top); final paddingBR = @@ -240,6 +197,43 @@ class FlutterMapState { ); } + double getBoundsZoom( + LatLngBounds bounds, + CustomPoint padding, { + bool inside = false, + bool forceIntegerZoomLevel = false, + }) { + final min = options.minZoom ?? 0.0; + final max = options.maxZoom ?? double.infinity; + final nw = bounds.northWest; + final se = bounds.southEast; + var size = nonRotatedSize - padding; + // Prevent negative size which results in NaN zoom value later on in the calculation + size = CustomPoint(math.max(0, size.x), math.max(0, size.y)); + var boundsSize = Bounds(project(se, zoom), project(nw, zoom)).size; + if (rotation != 0.0) { + final cosAngle = math.cos(rotationRad).abs(); + final sinAngle = math.sin(rotationRad).abs(); + boundsSize = CustomPoint( + (boundsSize.x * cosAngle) + (boundsSize.y * sinAngle), + (boundsSize.y * cosAngle) + (boundsSize.x * sinAngle), + ); + } + + final scaleX = size.x / boundsSize.x; + final scaleY = size.y / boundsSize.y; + final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); + + var boundsZoom = getScaleZoom(scale, zoom); + + if (forceIntegerZoomLevel) { + boundsZoom = + inside ? boundsZoom.ceilToDouble() : boundsZoom.floorToDouble(); + } + + return math.max(min, math.min(max, boundsZoom)); + } + CustomPoint project(LatLng latlng, [double? zoom]) => options.crs.latLngToPoint(latlng, zoom ?? this.zoom); From 3bc6ca459121047aa70cca93f3a136c7b64a41f8 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Sun, 11 Jun 2023 12:05:30 +0200 Subject: [PATCH 12/46] Replace http stubbing with an in-memory TileProvider in tests This stops the following message from being spammed in tests which was caused by a problem with the http mocking: type 'Null' is not a subtype of type 'Future' --- test/flutter_map_controller_test.dart | 3 -- test/flutter_map_test.dart | 3 -- test/layer/circle_layer_test.dart | 3 -- test/layer/marker_layer_test.dart | 3 -- test/layer/polygon_layer_test.dart | 3 -- test/layer/polyline_layer_test.dart | 3 -- test/test_utils/mocks.dart | 60 --------------------------- test/test_utils/test_app.dart | 14 +++++++ 8 files changed, 14 insertions(+), 78 deletions(-) delete mode 100644 test/test_utils/mocks.dart diff --git a/test/flutter_map_controller_test.dart b/test/flutter_map_controller_test.dart index 5d12e92cd..83b768d74 100644 --- a/test/flutter_map_controller_test.dart +++ b/test/flutter_map_controller_test.dart @@ -3,12 +3,9 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; -import 'test_utils/mocks.dart'; import 'test_utils/test_app.dart'; void main() { - setupMocks(); - testWidgets('test fit bounds methods', (tester) async { final controller = MapController(); final bounds = LatLngBounds( diff --git a/test/flutter_map_test.dart b/test/flutter_map_test.dart index a321cdc98..f2e99635a 100644 --- a/test/flutter_map_test.dart +++ b/test/flutter_map_test.dart @@ -3,12 +3,9 @@ import 'package:flutter_map/plugin_api.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; -import 'test_utils/mocks.dart'; import 'test_utils/test_app.dart'; void main() { - setupMocks(); - testWidgets('flutter_map', (tester) async { final markers = [ Marker( diff --git a/test/layer/circle_layer_test.dart b/test/layer/circle_layer_test.dart index e69ba0906..ac8194dfc 100644 --- a/test/layer/circle_layer_test.dart +++ b/test/layer/circle_layer_test.dart @@ -3,12 +3,9 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; -import '../test_utils/mocks.dart'; import '../test_utils/test_app.dart'; void main() { - setupMocks(); - testWidgets('test circle marker key', (tester) async { const key = Key('c-1'); diff --git a/test/layer/marker_layer_test.dart b/test/layer/marker_layer_test.dart index db0fb1eef..825b5294a 100644 --- a/test/layer/marker_layer_test.dart +++ b/test/layer/marker_layer_test.dart @@ -3,12 +3,9 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; -import '../test_utils/mocks.dart'; import '../test_utils/test_app.dart'; void main() { - setupMocks(); - testWidgets('test marker key', (tester) async { const key = Key('m-1'); diff --git a/test/layer/polygon_layer_test.dart b/test/layer/polygon_layer_test.dart index ef5e46c78..ac55b4684 100644 --- a/test/layer/polygon_layer_test.dart +++ b/test/layer/polygon_layer_test.dart @@ -3,12 +3,9 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; -import '../test_utils/mocks.dart'; import '../test_utils/test_app.dart'; void main() { - setupMocks(); - testWidgets('test polygon layer', (tester) async { final polygons = [ for (int i = 0; i < 1; ++i) diff --git a/test/layer/polyline_layer_test.dart b/test/layer/polyline_layer_test.dart index a42816161..9fdf43f44 100644 --- a/test/layer/polyline_layer_test.dart +++ b/test/layer/polyline_layer_test.dart @@ -3,12 +3,9 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; -import '../test_utils/mocks.dart'; import '../test_utils/test_app.dart'; void main() { - setupMocks(); - testWidgets('test polyline layer', (tester) async { final polylines = [ for (int i = 0; i < 10; i++) diff --git a/test/test_utils/mocks.dart b/test/test_utils/mocks.dart deleted file mode 100644 index 8a25202e3..000000000 --- a/test/test_utils/mocks.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockHttpClientResponse extends Mock implements HttpClientResponse { - final _stream = readFile(); - - @override - int get statusCode => HttpStatus.ok; - - @override - int get contentLength => File('test/res/map.png').lengthSync(); - - @override - HttpClientResponseCompressionState get compressionState => - HttpClientResponseCompressionState.notCompressed; - - @override - StreamSubscription> listen(void Function(List event)? onData, - {Function? onError, void Function()? onDone, bool? cancelOnError}) { - return _stream.listen( - onData, - onError: onError, - onDone: onDone, - cancelOnError: cancelOnError, - ); - } - - static Stream> readFile() => File('test/res/map.png').openRead(); -} - -class MockHttpHeaders extends Mock implements HttpHeaders {} - -class MockHttpClientRequest extends Mock implements HttpClientRequest { - @override - HttpHeaders get headers => MockHttpHeaders(); - - @override - Future close() => Future.value(MockHttpClientResponse()); -} - -class MockClient extends Mock implements HttpClient { - @override - Future getUrl(Uri url) { - return Future.value(MockHttpClientRequest()); - } -} - -class MockHttpOverrides extends HttpOverrides { - @override - HttpClient createHttpClient(SecurityContext? securityContext) => MockClient(); -} - -void setupMocks() { - setUpAll(() { - HttpOverrides.global = MockHttpOverrides(); - }); -} diff --git a/test/test_utils/test_app.dart b/test/test_utils/test_app.dart index 5945ff855..4448dbca4 100644 --- a/test/test_utils/test_app.dart +++ b/test/test_utils/test_app.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; @@ -36,6 +38,7 @@ class TestApp extends StatelessWidget { children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + tileProvider: TestTileProvider(), ), if (polylines.isNotEmpty) PolylineLayer(polylines: polylines), if (polygons.isNotEmpty) PolygonLayer(polygons: polygons), @@ -49,3 +52,14 @@ class TestApp extends StatelessWidget { ); } } + +class TestTileProvider extends TileProvider { + // Base 64 encoded 256x256 white tile. + static const _whiteTile = + 'iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRF////p8QbyAAAAB9JREFUeJztwQENAAAAwqD3T20ON6AAAAAAAAAAAL4NIQAAAfFnIe4AAAAASUVORK5CYII='; + + @override + ImageProvider getImage( + TileCoordinates coordinates, TileLayer options) => + MemoryImage(base64Decode(_whiteTile)); +} From 0a9a05f950a16dc694c9fe6620833f9c5f886d1e Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Sun, 11 Jun 2023 17:27:36 +0200 Subject: [PATCH 13/46] Separate MapOptions from FlutterMapState In doing so I noticed that the adaptive boundary options could use a refactor and so placed them in a dedicated class which led to a tidy up to the boundary code in FlutterMapState. --- .../lib/pages/animated_map_controller.dart | 4 +- example/lib/pages/circle.dart | 4 +- example/lib/pages/epsg4326_crs.dart | 6 +- example/lib/pages/many_markers.dart | 4 +- example/lib/pages/map_controller.dart | 6 +- example/lib/pages/map_inside_listview.dart | 4 +- example/lib/pages/moving_markers.dart | 4 +- example/lib/pages/offline_map.dart | 8 +- example/lib/pages/overlay_image.dart | 4 +- example/lib/pages/plugin_scalebar.dart | 4 +- example/lib/pages/plugin_zoombuttons.dart | 4 +- example/lib/pages/polygon.dart | 4 +- example/lib/pages/polyline.dart | 4 +- example/lib/pages/reset_tile_layer.dart | 4 +- example/lib/pages/sliding_map.dart | 10 +- example/lib/pages/stateful_markers.dart | 4 +- example/lib/pages/tile_builder_example.dart | 4 +- example/lib/pages/wms_tile_layer.dart | 4 +- .../lib/pages/zoombuttons_plugin_option.dart | 4 +- lib/flutter_map.dart | 1 + .../flutter_map_interactive_viewer.dart | 6 +- lib/src/layer/polygon_layer.dart | 4 +- lib/src/layer/polyline_layer.dart | 4 +- lib/src/layer/tile_layer/tile_layer.dart | 13 +- .../flutter_map_internal_controller.dart} | 136 ++++++------- lib/src/map/flutter_map_internal_state.dart | 17 ++ lib/src/map/flutter_map_state.dart | 182 ++++++++++-------- lib/src/map/flutter_map_state_container.dart | 19 +- ...lutter_map_state_controller_interface.dart | 55 ------ .../flutter_map_state_inherited_widget.dart | 16 +- lib/src/map/map_controller.dart | 2 +- lib/src/map/map_controller_impl.dart | 9 +- lib/src/map/map_safe_area.dart | 80 +++----- lib/src/map/options.dart | 50 +++-- lib/src/misc/map_boundary.dart | 64 ++++++ test/flutter_map_controller_test.dart | 16 +- test/flutter_map_test.dart | 4 +- test/test_utils/test_app.dart | 4 +- 38 files changed, 405 insertions(+), 367 deletions(-) rename lib/src/{gestures/flutter_map_state_controller.dart => map/flutter_map_internal_controller.dart} (74%) create mode 100644 lib/src/map/flutter_map_internal_state.dart delete mode 100644 lib/src/map/flutter_map_state_controller_interface.dart create mode 100644 lib/src/misc/map_boundary.dart diff --git a/example/lib/pages/animated_map_controller.dart b/example/lib/pages/animated_map_controller.dart index 36ab36047..67ee6c194 100644 --- a/example/lib/pages/animated_map_controller.dart +++ b/example/lib/pages/animated_map_controller.dart @@ -191,8 +191,8 @@ class AnimatedMapControllerPageState extends State Flexible( child: FlutterMap( mapController: mapController, - options: MapOptions( - center: const LatLng(51.5, -0.09), + options: const MapOptions( + center: LatLng(51.5, -0.09), zoom: 5, maxZoom: 10, minZoom: 3), diff --git a/example/lib/pages/circle.dart b/example/lib/pages/circle.dart index f3a332241..2f6c75f78 100644 --- a/example/lib/pages/circle.dart +++ b/example/lib/pages/circle.dart @@ -33,8 +33,8 @@ class CirclePage extends StatelessWidget { ), Flexible( child: FlutterMap( - options: MapOptions( - center: const LatLng(51.5, -0.09), + options: const MapOptions( + center: LatLng(51.5, -0.09), zoom: 11, ), children: [ diff --git a/example/lib/pages/epsg4326_crs.dart b/example/lib/pages/epsg4326_crs.dart index c13d6d6ad..d3a6488fe 100644 --- a/example/lib/pages/epsg4326_crs.dart +++ b/example/lib/pages/epsg4326_crs.dart @@ -23,10 +23,10 @@ class EPSG4326Page extends StatelessWidget { ), Flexible( child: FlutterMap( - options: MapOptions( + options: const MapOptions( minZoom: 0, - crs: const Epsg4326(), - center: const LatLng(0, 0), + crs: Epsg4326(), + center: LatLng(0, 0), zoom: 0, ), children: [ diff --git a/example/lib/pages/many_markers.dart b/example/lib/pages/many_markers.dart index b0ad1bf42..e2da101fd 100644 --- a/example/lib/pages/many_markers.dart +++ b/example/lib/pages/many_markers.dart @@ -72,8 +72,8 @@ class _ManyMarkersPageState extends State { Text('$_sliderVal markers'), Flexible( child: FlutterMap( - options: MapOptions( - center: const LatLng(50, 20), + options: const MapOptions( + center: LatLng(50, 20), zoom: 5, interactiveFlags: InteractiveFlag.all - InteractiveFlag.rotate, ), diff --git a/example/lib/pages/map_controller.dart b/example/lib/pages/map_controller.dart index 5ba3ae171..4fbea3d55 100644 --- a/example/lib/pages/map_controller.dart +++ b/example/lib/pages/map_controller.dart @@ -116,7 +116,7 @@ class MapControllerPageState extends State { Builder(builder: (BuildContext context) { return MaterialButton( onPressed: () { - final bounds = _mapController.mapState.bounds; + final bounds = _mapController.mapState.visibleBounds; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( @@ -151,8 +151,8 @@ class MapControllerPageState extends State { Flexible( child: FlutterMap( mapController: _mapController, - options: MapOptions( - center: const LatLng(51.5, -0.09), + options: const MapOptions( + center: LatLng(51.5, -0.09), zoom: 5, maxZoom: 5, minZoom: 3, diff --git a/example/lib/pages/map_inside_listview.dart b/example/lib/pages/map_inside_listview.dart index 50734e64a..9298ba634 100644 --- a/example/lib/pages/map_inside_listview.dart +++ b/example/lib/pages/map_inside_listview.dart @@ -22,8 +22,8 @@ class MapInsideListViewPage extends StatelessWidget { SizedBox( height: 300, child: FlutterMap( - options: MapOptions( - center: const LatLng(51.5, -0.09), + options: const MapOptions( + center: LatLng(51.5, -0.09), zoom: 5, ), nonRotatedChildren: const [ diff --git a/example/lib/pages/moving_markers.dart b/example/lib/pages/moving_markers.dart index 6b8d75efe..b65efaca2 100644 --- a/example/lib/pages/moving_markers.dart +++ b/example/lib/pages/moving_markers.dart @@ -54,8 +54,8 @@ class _MovingMarkersPageState extends State { ), Flexible( child: FlutterMap( - options: MapOptions( - center: const LatLng(51.5, -0.09), + options: const MapOptions( + center: LatLng(51.5, -0.09), zoom: 5, ), children: [ diff --git a/example/lib/pages/offline_map.dart b/example/lib/pages/offline_map.dart index 91c9ca41b..a156e212d 100644 --- a/example/lib/pages/offline_map.dart +++ b/example/lib/pages/offline_map.dart @@ -28,8 +28,12 @@ class OfflineMapPage extends StatelessWidget { center: const LatLng(56.704173, 11.543808), minZoom: 12, maxZoom: 14, - swPanBoundary: const LatLng(56.6877, 11.5089), - nePanBoundary: const LatLng(56.7378, 11.6644), + boundary: FixedBoundary( + latLngBounds: LatLngBounds( + const LatLng(56.7378, 11.6644), + const LatLng(56.6877, 11.5089), + ), + ), ), children: [ TileLayer( diff --git a/example/lib/pages/overlay_image.dart b/example/lib/pages/overlay_image.dart index 2fb86489b..203a47862 100644 --- a/example/lib/pages/overlay_image.dart +++ b/example/lib/pages/overlay_image.dart @@ -45,8 +45,8 @@ class OverlayImagePage extends StatelessWidget { ), Flexible( child: FlutterMap( - options: MapOptions( - center: const LatLng(51.5, -0.09), + options: const MapOptions( + center: LatLng(51.5, -0.09), zoom: 6, ), children: [ diff --git a/example/lib/pages/plugin_scalebar.dart b/example/lib/pages/plugin_scalebar.dart index d3d72ab83..744dd8c39 100644 --- a/example/lib/pages/plugin_scalebar.dart +++ b/example/lib/pages/plugin_scalebar.dart @@ -20,8 +20,8 @@ class PluginScaleBar extends StatelessWidget { children: [ Flexible( child: FlutterMap( - options: MapOptions( - center: const LatLng(51.5, -0.09), + options: const MapOptions( + center: LatLng(51.5, -0.09), zoom: 5, ), nonRotatedChildren: [ diff --git a/example/lib/pages/plugin_zoombuttons.dart b/example/lib/pages/plugin_zoombuttons.dart index 46aa917b6..a324e8bf4 100644 --- a/example/lib/pages/plugin_zoombuttons.dart +++ b/example/lib/pages/plugin_zoombuttons.dart @@ -20,8 +20,8 @@ class PluginZoomButtons extends StatelessWidget { children: [ Flexible( child: FlutterMap( - options: MapOptions( - center: const LatLng(51.5, -0.09), + options: const MapOptions( + center: LatLng(51.5, -0.09), zoom: 5, ), nonRotatedChildren: const [ diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index 9bffd1423..345ad0f11 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -77,8 +77,8 @@ class PolygonPage extends StatelessWidget { ), Flexible( child: FlutterMap( - options: MapOptions( - center: const LatLng(51.5, -0.09), + options: const MapOptions( + center: LatLng(51.5, -0.09), zoom: 5, ), children: [ diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 0fb9ee465..a3f178ff9 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -28,8 +28,8 @@ class _PolylinePageState extends State { ), Flexible( child: FlutterMap( - options: MapOptions( - center: const LatLng(51.5, -0.09), + options: const MapOptions( + center: LatLng(51.5, -0.09), zoom: 5, ), children: [ diff --git a/example/lib/pages/reset_tile_layer.dart b/example/lib/pages/reset_tile_layer.dart index 9c9e35528..930000dd3 100644 --- a/example/lib/pages/reset_tile_layer.dart +++ b/example/lib/pages/reset_tile_layer.dart @@ -71,8 +71,8 @@ class ResetTileLayerPageState extends State { ), Flexible( child: FlutterMap( - options: MapOptions( - center: const LatLng(51.5, -0.09), + options: const MapOptions( + center: LatLng(51.5, -0.09), zoom: 5, ), children: [ diff --git a/example/lib/pages/sliding_map.dart b/example/lib/pages/sliding_map.dart index 09bf7eb77..83c512096 100644 --- a/example/lib/pages/sliding_map.dart +++ b/example/lib/pages/sliding_map.dart @@ -29,10 +29,14 @@ class SlidingMapPage extends StatelessWidget { minZoom: 12, maxZoom: 14, zoom: 13, - swPanBoundary: const LatLng(56.6877, 11.5089), - nePanBoundary: const LatLng(56.7378, 11.6644), + boundary: AdaptiveBoundary( + screenSize: MediaQuery.of(context).size, + latLngBounds: LatLngBounds( + const LatLng(56.7378, 11.6644), + const LatLng(56.6877, 11.5089), + ), + ), slideOnBoundaries: true, - screenSize: MediaQuery.of(context).size, ), children: [ TileLayer( diff --git a/example/lib/pages/stateful_markers.dart b/example/lib/pages/stateful_markers.dart index 4b93f7f69..2021f7d0f 100644 --- a/example/lib/pages/stateful_markers.dart +++ b/example/lib/pages/stateful_markers.dart @@ -59,8 +59,8 @@ class _StatefulMarkersPageState extends State { ), Flexible( child: FlutterMap( - options: MapOptions( - center: const LatLng(51.5, -0.09), + options: const MapOptions( + center: LatLng(51.5, -0.09), zoom: 5, ), children: [ diff --git a/example/lib/pages/tile_builder_example.dart b/example/lib/pages/tile_builder_example.dart index d080f513d..ca2b9bda0 100644 --- a/example/lib/pages/tile_builder_example.dart +++ b/example/lib/pages/tile_builder_example.dart @@ -120,8 +120,8 @@ class _TileBuilderPageState extends State { body: Padding( padding: const EdgeInsets.all(8), child: FlutterMap( - options: MapOptions( - center: const LatLng(51.5, -0.09), + options: const MapOptions( + center: LatLng(51.5, -0.09), zoom: 5, ), children: [ diff --git a/example/lib/pages/wms_tile_layer.dart b/example/lib/pages/wms_tile_layer.dart index fed2db762..a035222ef 100644 --- a/example/lib/pages/wms_tile_layer.dart +++ b/example/lib/pages/wms_tile_layer.dart @@ -24,8 +24,8 @@ class WMSLayerPage extends StatelessWidget { ), Flexible( child: FlutterMap( - options: MapOptions( - center: const LatLng(42.58, 12.43), + options: const MapOptions( + center: LatLng(42.58, 12.43), zoom: 6, ), nonRotatedChildren: [ diff --git a/example/lib/pages/zoombuttons_plugin_option.dart b/example/lib/pages/zoombuttons_plugin_option.dart index e51b3083c..82bcfc91c 100644 --- a/example/lib/pages/zoombuttons_plugin_option.dart +++ b/example/lib/pages/zoombuttons_plugin_option.dart @@ -48,7 +48,7 @@ class FlutterMapZoomButtons extends StatelessWidget { mini: mini, backgroundColor: zoomInColor ?? Theme.of(context).primaryColor, onPressed: () { - final bounds = map.bounds; + final bounds = map.visibleBounds; final centerZoom = map.centerZoomFitBounds(bounds, options: options); var zoom = centerZoom.zoom + 1; @@ -68,7 +68,7 @@ class FlutterMapZoomButtons extends StatelessWidget { mini: mini, backgroundColor: zoomOutColor ?? Theme.of(context).primaryColor, onPressed: () { - final bounds = map.bounds; + final bounds = map.visibleBounds; final centerZoom = map.centerZoomFitBounds( bounds, options: options, diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index 949a792b1..69865ecd0 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -31,6 +31,7 @@ export 'package:flutter_map/src/map/options.dart'; export 'package:flutter_map/src/map/widget.dart'; export 'package:flutter_map/src/misc/center_zoom.dart'; export 'package:flutter_map/src/misc/fit_bounds_options.dart'; +export 'package:flutter_map/src/misc/map_boundary.dart'; export 'package:flutter_map/src/misc/move_and_rotate_result.dart'; export 'package:flutter_map/src/misc/point.dart'; export 'package:flutter_map/src/misc/position.dart'; diff --git a/lib/src/gestures/flutter_map_interactive_viewer.dart b/lib/src/gestures/flutter_map_interactive_viewer.dart index bfd751c46..7f5f4a3a0 100644 --- a/lib/src/gestures/flutter_map_interactive_viewer.dart +++ b/lib/src/gestures/flutter_map_interactive_viewer.dart @@ -3,11 +3,11 @@ import 'dart:math' as math; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_map/src/gestures/flutter_map_state_controller.dart'; import 'package:flutter_map/src/gestures/interactive_flag.dart'; import 'package:flutter_map/src/gestures/latlng_tween.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; +import 'package:flutter_map/src/map/flutter_map_internal_controller.dart'; import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:flutter_map/src/map/options.dart'; import 'package:flutter_map/src/misc/point.dart'; @@ -17,7 +17,7 @@ import 'package:latlong2/latlong.dart'; class FlutterMapInteractiveViewer extends StatefulWidget { final Widget Function(BuildContext context, FlutterMapState mapState) builder; final MapOptions options; - final FlutterMapStateController controller; + final FlutterMapInternalController controller; const FlutterMapInteractiveViewer({ super.key, @@ -72,7 +72,7 @@ class FlutterMapInteractiveViewerState int _tapUpCounter = 0; Timer? _doubleTapHoldMaxDelay; - FlutterMapState get _mapState => widget.controller.value; + FlutterMapState get _mapState => widget.controller.mapState; MapOptions get _options => widget.options; diff --git a/lib/src/layer/polygon_layer.dart b/lib/src/layer/polygon_layer.dart index cf20a8217..6f970ae7a 100644 --- a/lib/src/layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer.dart @@ -83,7 +83,7 @@ class PolygonLayer extends StatelessWidget { final List pgons = polygonCulling ? polygons.where((p) { - return p.boundingBox.isOverlapping(map.bounds); + return p.boundingBox.isOverlapping(map.visibleBounds); }).toList() : polygons; @@ -100,7 +100,7 @@ class PolygonPainter extends CustomPainter { final FlutterMapState map; final LatLngBounds bounds; - PolygonPainter(this.polygons, this.map) : bounds = map.bounds; + PolygonPainter(this.polygons, this.map) : bounds = map.visibleBounds; int get hash { _hash ??= Object.hashAll(polygons); diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index e707379e7..7c09bf32c 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -72,7 +72,7 @@ class PolylineLayer extends StatelessWidget { painter: PolylinePainter( polylineCulling ? polylines - .where((p) => p.boundingBox.isOverlapping(map.bounds)) + .where((p) => p.boundingBox.isOverlapping(map.visibleBounds)) .toList() : polylines, map, @@ -89,7 +89,7 @@ class PolylinePainter extends CustomPainter { final FlutterMapState map; final LatLngBounds bounds; - PolylinePainter(this.polylines, this.map) : bounds = map.bounds; + PolylinePainter(this.polylines, this.map) : bounds = map.visibleBounds; int get hash { _hash ??= Object.hashAll(polylines); diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index c74dfca3d..3d8efc037 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -329,7 +329,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { void didChangeDependencies() { super.didChangeDependencies(); - final mapState = FlutterMapState.maybeOf(context)!; + final mapOptions = MapOptions.of(context); final mapController = MapController.of(context); if (_mapControllerHashCode != mapController.hashCode) { @@ -345,26 +345,25 @@ class _TileLayerState extends State with TickerProviderStateMixin { bool reloadTiles = false; if (!_initializedFromMapState || _tileBounds.shouldReplace( - mapState.options.crs, widget.tileSize, widget.tileBounds)) { + mapOptions.crs, widget.tileSize, widget.tileBounds)) { reloadTiles = true; _tileBounds = TileBounds( - crs: mapState.options.crs, + crs: mapOptions.crs, tileSize: widget.tileSize, latLngBounds: widget.tileBounds, ); } if (!_initializedFromMapState || - _tileScaleCalculator.shouldReplace( - mapState.options.crs, widget.tileSize)) { + _tileScaleCalculator.shouldReplace(mapOptions.crs, widget.tileSize)) { reloadTiles = true; _tileScaleCalculator = TileScaleCalculator( - crs: mapState.options.crs, + crs: mapOptions.crs, tileSize: widget.tileSize, ); } - if (reloadTiles) _loadAndPruneInVisibleBounds(mapState); + if (reloadTiles) _loadAndPruneInVisibleBounds(FlutterMapState.of(context)); _initializedFromMapState = true; } diff --git a/lib/src/gestures/flutter_map_state_controller.dart b/lib/src/map/flutter_map_internal_controller.dart similarity index 74% rename from lib/src/gestures/flutter_map_state_controller.dart rename to lib/src/map/flutter_map_internal_controller.dart index d9b8bc5be..a24867c14 100644 --- a/lib/src/gestures/flutter_map_state_controller.dart +++ b/lib/src/map/flutter_map_internal_controller.dart @@ -2,19 +2,24 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_map/plugin_api.dart'; import 'package:flutter_map/src/gestures/flutter_map_interactive_viewer.dart'; -import 'package:flutter_map/src/map/flutter_map_state_controller_interface.dart'; +import 'package:flutter_map/src/map/flutter_map_internal_state.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; import 'package:latlong2/latlong.dart'; // This controller is for internal use. All updates to the state should be done // by calling methods of this class to ensure consistency. -class FlutterMapStateController extends ValueNotifier - implements FlutterMapStateControllerInterface { +class FlutterMapInternalController + extends ValueNotifier { late final FlutterMapInteractiveViewerState _interactiveViewerState; late MapControllerImpl _mapControllerImpl; - FlutterMapStateController(MapOptions options) - : super(FlutterMapState.initialState(options)); + FlutterMapInternalController(MapOptions options) + : super( + FlutterMapInternalState( + options: options, + mapState: FlutterMapState.initialState(options), + ), + ); // Link the viewer state with the controller. This should be done once when // the FlutterMapInteractiveViewerState is initialized. @@ -23,6 +28,9 @@ class FlutterMapStateController extends ValueNotifier ) => _interactiveViewerState = interactiveViewerState; + MapOptions get options => value.options; + FlutterMapState get mapState => value.mapState; + void linkMapController(MapControllerImpl mapControllerImpl) { _mapControllerImpl = mapControllerImpl; _mapControllerImpl.stateController = this; @@ -32,12 +40,11 @@ class FlutterMapStateController extends ValueNotifier /// to the FlutterMapState should be done via methods in this class. @visibleForTesting @override - set value(FlutterMapState value) => super.value = value; + set value(FlutterMapInternalState value) => super.value = value; // Note: All named parameters are required to prevent inconsistent default // values since this method can be called by MapController which declares // defaults. - @override bool move( LatLng newCenter, double newZoom, { @@ -46,13 +53,13 @@ class FlutterMapStateController extends ValueNotifier required MapEventSource source, required String? id, }) { - newZoom = value.fitZoomToBounds(newZoom); + newZoom = mapState.fitZoomToBounds(newZoom); // Algorithm thanks to https://github.com/tlserver/flutter_map_location_marker if (offset != Offset.zero) { - final newPoint = value.project(newCenter, newZoom); - newCenter = value.unproject( - value.rotatePoint( + final newPoint = mapState.project(newCenter, newZoom); + newCenter = mapState.unproject( + mapState.rotatePoint( newPoint, newPoint - CustomPoint(offset.dx, offset.dy), ), @@ -60,42 +67,45 @@ class FlutterMapStateController extends ValueNotifier ); } - if (value.isOutOfBounds(newCenter)) { - if (!value.options.slideOnBoundaries) return false; - newCenter = value.containPoint(newCenter, value.center); + // TODO: Why do we have two separate methods of adjusting the bounds? + if (mapState.isOutOfBounds(newCenter)) { + if (!options.slideOnBoundaries) return false; + newCenter = mapState.clampWithFallback(newCenter, mapState.center); } - if (value.options.maxBounds != null) { - final adjustedCenter = value.adjustCenterIfOutsideMaxBounds( + if (options.maxBounds != null) { + final adjustedCenter = mapState.adjustCenterIfOutsideMaxBounds( newCenter, newZoom, - value.options.maxBounds!, + options.maxBounds!, ); if (adjustedCenter == null) return false; newCenter = adjustedCenter; } - if (newCenter == value.center && newZoom == value.zoom) { + if (newCenter == mapState.center && newZoom == mapState.zoom) { return false; } - final oldMapState = value; - value = value.copyWith(zoom: newZoom, center: newCenter); + final oldMapState = mapState; + value = value.withMapState( + mapState.withPosition(zoom: newZoom, center: newCenter), + ); final movementEvent = MapEventWithMove.fromSource( oldMapState: oldMapState, - mapState: value, + mapState: mapState, hasGesture: hasGesture, source: source, id: id, ); if (movementEvent != null) _emitMapEvent(movementEvent); - value.options.onPositionChanged?.call( + options.onPositionChanged?.call( MapPosition( center: newCenter, - bounds: value.bounds, + bounds: mapState.visibleBounds, zoom: newZoom, hasGesture: hasGesture, ), @@ -108,24 +118,23 @@ class FlutterMapStateController extends ValueNotifier // Note: All named parameters are required to prevent inconsistent default // values since this method can be called by MapController which declares // defaults. - @override bool rotate( double newRotation, { required bool hasGesture, required MapEventSource source, required String? id, }) { - if (newRotation != value.rotation) { - final oldMapState = value; - //Apply state then emit events and callbacks - value = value.withRotation(newRotation); + if (newRotation != mapState.rotation) { + final oldMapState = mapState; + // Apply state then emit events and callbacks + value = value.withMapState(mapState.withRotation(newRotation)); _emitMapEvent( MapEventRotate( id: id, source: source, oldMapState: oldMapState, - mapState: value, + mapState: mapState, ), ); return true; @@ -137,7 +146,6 @@ class FlutterMapStateController extends ValueNotifier // Note: All named parameters are required to prevent inconsistent default // values since this method can be called by MapController which declares // defaults. - @override MoveAndRotateResult rotateAroundPoint( double degree, { required CustomPoint? point, @@ -153,7 +161,7 @@ class FlutterMapStateController extends ValueNotifier throw ArgumentError('One of `point` or `offset` must be non-null'); } - if (degree == value.rotation) { + if (degree == mapState.rotation) { return MoveAndRotateResult(false, false); } @@ -169,28 +177,28 @@ class FlutterMapStateController extends ValueNotifier ); } - final rotationDiff = degree - value.rotation; - final rotationCenter = value.project(value.center) + + final rotationDiff = degree - mapState.rotation; + final rotationCenter = mapState.project(mapState.center) + (point != null - ? (point - (value.nonRotatedSize / 2.0)) + ? (point - (mapState.nonRotatedSize / 2.0)) : CustomPoint(offset!.dx, offset.dy)) - .rotate(value.rotationRad); + .rotate(mapState.rotationRad); return MoveAndRotateResult( move( - value.unproject( + mapState.unproject( rotationCenter + - (value.project(value.center) - rotationCenter) + (mapState.project(mapState.center) - rotationCenter) .rotate(degToRadian(rotationDiff)), ), - value.zoom, + mapState.zoom, offset: Offset.zero, hasGesture: hasGesture, source: source, id: id, ), rotate( - value.rotation + rotationDiff, + mapState.rotation + rotationDiff, hasGesture: hasGesture, source: source, id: id, @@ -201,7 +209,6 @@ class FlutterMapStateController extends ValueNotifier // Note: All named parameters are required to prevent inconsistent default // values since this method can be called by MapController which declares // defaults. - @override MoveAndRotateResult moveAndRotate( LatLng newCenter, double newZoom, @@ -226,13 +233,12 @@ class FlutterMapStateController extends ValueNotifier // Note: All named parameters are required to prevent inconsistent default // values since this method can be called by MapController which declares // defaults. - @override bool fitBounds( LatLngBounds bounds, FitBoundsOptions options, { required Offset offset, }) { - final target = value.centerZoomFitBounds(bounds, options: options); + final target = mapState.centerZoomFitBounds(bounds, options: options); return move( target.center, target.zoom, @@ -247,8 +253,8 @@ class FlutterMapStateController extends ValueNotifier CustomPoint nonRotatedSize, ) { if (nonRotatedSize != FlutterMapState.kImpossibleSize && - nonRotatedSize != value.nonRotatedSize) { - value = value.withNonRotatedSize(nonRotatedSize); + nonRotatedSize != mapState.nonRotatedSize) { + value = value.withMapState(mapState.withNonRotatedSize(nonRotatedSize)); return true; } @@ -256,8 +262,8 @@ class FlutterMapStateController extends ValueNotifier } void setOptions(MapOptions options) { - if (value.options != options) { - value = value.withOptions(options); + if (options != this.options) { + value = value.withMapState(mapState.withOptions(options)); } } @@ -265,7 +271,7 @@ class FlutterMapStateController extends ValueNotifier void moveStarted(MapEventSource source) { _emitMapEvent( MapEventMoveStart( - mapState: value, + mapState: mapState, source: source, ), ); @@ -273,14 +279,14 @@ class FlutterMapStateController extends ValueNotifier // To be called when an ongoing drag movement updates. void dragUpdated(MapEventSource source, Offset offset) { - final oldCenterPt = value.project(value.center); + final oldCenterPt = mapState.project(mapState.center); final newCenterPt = oldCenterPt + offset.toCustomPoint(); - final newCenter = value.unproject(newCenterPt); + final newCenter = mapState.unproject(newCenterPt); move( newCenter, - value.zoom, + mapState.zoom, offset: Offset.zero, hasGesture: true, source: source, @@ -292,7 +298,7 @@ class FlutterMapStateController extends ValueNotifier void moveEnded(MapEventSource source) { _emitMapEvent( MapEventMoveEnd( - mapState: value, + mapState: mapState, source: source, ), ); @@ -302,7 +308,7 @@ class FlutterMapStateController extends ValueNotifier void rotateStarted(MapEventSource source) { _emitMapEvent( MapEventRotateStart( - mapState: value, + mapState: mapState, source: source, ), ); @@ -312,7 +318,7 @@ class FlutterMapStateController extends ValueNotifier void rotateEnded(MapEventSource source) { _emitMapEvent( MapEventRotateEnd( - mapState: value, + mapState: mapState, source: source, ), ); @@ -322,7 +328,7 @@ class FlutterMapStateController extends ValueNotifier void flingStarted(MapEventSource source) { _emitMapEvent( MapEventFlingAnimationStart( - mapState: value, + mapState: mapState, source: MapEventSource.flingAnimationController, ), ); @@ -332,7 +338,7 @@ class FlutterMapStateController extends ValueNotifier void flingEnded(MapEventSource source) { _emitMapEvent( MapEventFlingAnimationEnd( - mapState: value, + mapState: mapState, source: source, ), ); @@ -342,7 +348,7 @@ class FlutterMapStateController extends ValueNotifier void flingNotStarted(MapEventSource source) { _emitMapEvent( MapEventFlingAnimationNotStarted( - mapState: value, + mapState: mapState, source: source, ), ); @@ -352,7 +358,7 @@ class FlutterMapStateController extends ValueNotifier void doubleTapZoomStarted(MapEventSource source) { _emitMapEvent( MapEventDoubleTapZoomStart( - mapState: value, + mapState: mapState, source: source, ), ); @@ -362,7 +368,7 @@ class FlutterMapStateController extends ValueNotifier void doubleTapZoomEnded(MapEventSource source) { _emitMapEvent( MapEventDoubleTapZoomEnd( - mapState: value, + mapState: mapState, source: source, ), ); @@ -373,11 +379,11 @@ class FlutterMapStateController extends ValueNotifier TapPosition tapPosition, LatLng position, ) { - value.options.onTap?.call(tapPosition, position); + options.onTap?.call(tapPosition, position); _emitMapEvent( MapEventTap( tapPosition: position, - mapState: value, + mapState: mapState, source: source, ), ); @@ -388,11 +394,11 @@ class FlutterMapStateController extends ValueNotifier TapPosition tapPosition, LatLng position, ) { - value.options.onSecondaryTap?.call(tapPosition, position); + options.onSecondaryTap?.call(tapPosition, position); _emitMapEvent( MapEventSecondaryTap( tapPosition: position, - mapState: value, + mapState: mapState, source: source, ), ); @@ -403,11 +409,11 @@ class FlutterMapStateController extends ValueNotifier TapPosition tapPosition, LatLng position, ) { - value.options.onLongPress?.call(tapPosition, position); + options.onLongPress?.call(tapPosition, position); _emitMapEvent( MapEventLongPress( tapPosition: position, - mapState: value, + mapState: mapState, source: MapEventSource.longPress, ), ); @@ -433,7 +439,7 @@ class FlutterMapStateController extends ValueNotifier _interactiveViewerState.interruptAnimatedMovement(event); } - value.options.onMapEvent?.call(event); + options.onMapEvent?.call(event); _mapControllerImpl.mapEventSink.add(event); } diff --git a/lib/src/map/flutter_map_internal_state.dart b/lib/src/map/flutter_map_internal_state.dart new file mode 100644 index 000000000..2092ed72e --- /dev/null +++ b/lib/src/map/flutter_map_internal_state.dart @@ -0,0 +1,17 @@ +import 'package:flutter_map/plugin_api.dart'; + +class FlutterMapInternalState { + final FlutterMapState mapState; + final MapOptions options; + + const FlutterMapInternalState({ + required this.options, + required this.mapState, + }); + + FlutterMapInternalState withMapState(FlutterMapState mapState) => + FlutterMapInternalState( + options: options, + mapState: mapState, + ); +} diff --git a/lib/src/map/flutter_map_state.dart b/lib/src/map/flutter_map_state.dart index c45f259a3..be999ec54 100644 --- a/lib/src/map/flutter_map_state.dart +++ b/lib/src/map/flutter_map_state.dart @@ -1,12 +1,13 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; +import 'package:flutter_map/src/geo/crs.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; -import 'package:flutter_map/src/map/map_safe_area.dart'; import 'package:flutter_map/src/map/options.dart'; import 'package:flutter_map/src/misc/center_zoom.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; +import 'package:flutter_map/src/misc/map_boundary.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; @@ -19,7 +20,10 @@ class FlutterMapState { // provides real constraints. static const kImpossibleSize = CustomPoint(-1, -1); - final MapOptions options; + final Crs crs; + final double? minZoom; + final double? maxZoom; + final MapBoundary? boundary; final LatLng center; final double zoom; @@ -36,11 +40,9 @@ class FlutterMapState { LatLngBounds? _bounds; CustomPoint? _pixelOrigin; - MapSafeArea? _mapSafeAreaCache; - static FlutterMapState? maybeOf(BuildContext context) => context .dependOnInheritedWidgetOfExactType() - ?.mapState; + ?.state; static FlutterMapState of(BuildContext context) => maybeOf(context) ?? @@ -49,8 +51,12 @@ class FlutterMapState { /// Initializes FlutterMapState from the given [options] and with the /// [nonRotatedSize] and [size] both set to [kImpossibleSize]. - FlutterMapState.initialState(this.options) - : center = options.center, + FlutterMapState.initialState(MapOptions options) + : crs = options.crs, + minZoom = options.minZoom, + maxZoom = options.maxZoom, + boundary = options.boundary, + center = options.center, zoom = options.zoom, rotation = options.rotation, nonRotatedSize = kImpossibleSize, @@ -60,7 +66,10 @@ class FlutterMapState { // [pixelBounds] may be set if they are known already. Otherwise if left // null they will be calculated lazily when they are used. FlutterMapState({ - required this.options, + required this.crs, + required this.minZoom, + required this.maxZoom, + required this.boundary, required this.center, required this.zoom, required this.rotation, @@ -77,7 +86,10 @@ class FlutterMapState { if (nonRotatedSize == this.nonRotatedSize) return this; return FlutterMapState( - options: options, + crs: crs, + minZoom: minZoom, + maxZoom: maxZoom, + boundary: boundary, center: center, zoom: zoom, rotation: rotation, @@ -90,7 +102,31 @@ class FlutterMapState { if (rotation == this.rotation) return this; return FlutterMapState( - options: options, + crs: crs, + minZoom: minZoom, + maxZoom: maxZoom, + boundary: boundary, + center: center, + zoom: zoom, + rotation: rotation, + nonRotatedSize: nonRotatedSize, + size: _calculateSize(rotation, nonRotatedSize), + ); + } + + FlutterMapState withOptions(MapOptions options) { + if (options.crs == crs && + options.minZoom == minZoom && + options.maxZoom == maxZoom && + options.boundary == boundary) { + return this; + } + + return FlutterMapState( + crs: options.crs, + minZoom: options.minZoom, + maxZoom: options.maxZoom, + boundary: options.boundary, center: center, zoom: zoom, rotation: rotation, @@ -99,19 +135,29 @@ class FlutterMapState { ); } - FlutterMapState withOptions(MapOptions options) => FlutterMapState( - options: options, - center: center, - zoom: zoom, + FlutterMapState withPosition({ + LatLng? center, + double? zoom, + }) => + FlutterMapState( + crs: crs, + minZoom: minZoom, + maxZoom: maxZoom, + boundary: boundary, + center: center ?? this.center, + zoom: zoom ?? this.zoom, rotation: rotation, nonRotatedSize: nonRotatedSize, - size: _calculateSize(rotation, nonRotatedSize), + size: size, ); Bounds get pixelBounds => _pixelBounds ?? (_pixelBounds = getPixelBounds()); - LatLngBounds get bounds => + @Deprecated('Use visibleBounds instead.') + LatLngBounds get bounds => visibleBounds; + + LatLngBounds get visibleBounds => _bounds ?? (_bounds = LatLngBounds( unproject(pixelBounds.bottomLeft, zoom), @@ -122,19 +168,6 @@ class FlutterMapState { _pixelOrigin ?? (_pixelOrigin = (project(center, zoom) - size / 2.0).round()); - FlutterMapState copyWith({ - LatLng? center, - double? zoom, - }) => - FlutterMapState( - options: options, - center: center ?? this.center, - zoom: zoom ?? this.zoom, - rotation: rotation, - nonRotatedSize: nonRotatedSize, - size: size, - ); - static CustomPoint _calculateSize( double rotation, CustomPoint nonRotatedSize, @@ -203,8 +236,8 @@ class FlutterMapState { bool inside = false, bool forceIntegerZoomLevel = false, }) { - final min = options.minZoom ?? 0.0; - final max = options.maxZoom ?? double.infinity; + final min = minZoom ?? 0.0; + final max = maxZoom ?? double.infinity; final nw = bounds.northWest; final se = bounds.southEast; var size = nonRotatedSize - padding; @@ -235,25 +268,21 @@ class FlutterMapState { } CustomPoint project(LatLng latlng, [double? zoom]) => - options.crs.latLngToPoint(latlng, zoom ?? this.zoom); + crs.latLngToPoint(latlng, zoom ?? this.zoom); LatLng unproject(CustomPoint point, [double? zoom]) => - options.crs.pointToLatLng(point, zoom ?? this.zoom); + crs.pointToLatLng(point, zoom ?? this.zoom); LatLng layerPointToLatLng(CustomPoint point) => unproject(point); double getZoomScale(double toZoom, double fromZoom) => - options.crs.scale(toZoom) / options.crs.scale(fromZoom); + crs.scale(toZoom) / crs.scale(fromZoom); - double getScaleZoom(double scale, double? fromZoom) { - final crs = options.crs; - fromZoom = fromZoom ?? zoom; - return crs.zoom(scale * crs.scale(fromZoom)); - } + double getScaleZoom(double scale, double? fromZoom) => + crs.zoom(scale * crs.scale(fromZoom ?? zoom)); - Bounds? getPixelWorldBounds(double? zoom) { - return options.crs.getProjectedBounds(zoom ?? this.zoom); - } + Bounds? getPixelWorldBounds(double? zoom) => + crs.getProjectedBounds(zoom ?? this.zoom); Offset getOffsetFromOrigin(LatLng pos) { final delta = project(pos) - pixelOrigin; @@ -281,9 +310,9 @@ class FlutterMapState { final nonRotatedPixelOrigin = (project(center, zoom) - nonRotatedSize / 2.0).round(); - var point = options.crs.latLngToPoint(latLng, zoom); + var point = crs.latLngToPoint(latLng, zoom); - final mapCenter = options.crs.latLngToPoint(center, zoom); + final mapCenter = crs.latLngToPoint(center, zoom); if (rotation != 0.0) { point = rotatePoint(mapCenter, point, counterRotation: false); @@ -297,7 +326,7 @@ class FlutterMapState { (nonRotatedSize.x / 2) - localPoint.x, (nonRotatedSize.y / 2) - localPoint.y, ); - final mapCenter = options.crs.latLngToPoint(center, zoom); + final mapCenter = crs.latLngToPoint(center, zoom); var point = mapCenter - localPointCenterDistance; @@ -305,7 +334,7 @@ class FlutterMapState { point = rotatePoint(mapCenter, point); } - return options.crs.pointToLatLng(point, zoom); + return crs.pointToLatLng(point, zoom); } // Sometimes we need to make allowances that a rotation already exists, so @@ -331,53 +360,39 @@ class FlutterMapState { double fitZoomToBounds(double zoom) { // Abide to min/max zoom - if (options.maxZoom != null) { - zoom = (zoom > options.maxZoom!) ? options.maxZoom! : zoom; + if (maxZoom != null) { + zoom = (zoom > maxZoom!) ? maxZoom! : zoom; } - if (options.minZoom != null) { - zoom = (zoom < options.minZoom!) ? options.minZoom! : zoom; + if (minZoom != null) { + zoom = (zoom < minZoom!) ? minZoom! : zoom; } return zoom; } // Returns true if given [center] is outside of the allowed bounds. - bool isOutOfBounds(LatLng center) { - if (options.adaptiveBoundaries) { - return !_safeArea!.contains(center); + bool isOutOfBounds(LatLng latLng) { + switch (boundary) { + case FixedBoundary(): + return !(boundary as FixedBoundary).contains(latLng); + case AdaptiveBoundary(): + return !(boundary as AdaptiveBoundary).contains(latLng, zoom); + case null: + return false; } - if (options.swPanBoundary != null && options.nePanBoundary != null) { - if (center.latitude < options.swPanBoundary!.latitude || - center.latitude > options.nePanBoundary!.latitude) { - return true; - } else if (center.longitude < options.swPanBoundary!.longitude || - center.longitude > options.nePanBoundary!.longitude) { - return true; - } - } - return false; } - LatLng containPoint(LatLng point, LatLng fallback) { - if (options.adaptiveBoundaries) { - return _safeArea!.containPoint(point, fallback); - } else { - return LatLng( - point.latitude.clamp( - options.swPanBoundary!.latitude, options.nePanBoundary!.latitude), - point.longitude.clamp( - options.swPanBoundary!.longitude, options.nePanBoundary!.longitude), - ); + LatLng clampWithFallback(LatLng point, LatLng fallback) { + switch (boundary) { + case FixedBoundary(): + return (boundary as FixedBoundary).clamp(point); + case AdaptiveBoundary(): + return (boundary as AdaptiveBoundary) + .clampWithFallback(point, fallback, zoom); + case null: + return point; } } - MapSafeArea? get _safeArea => MapSafeArea.createUnlessMatching( - previous: _mapSafeAreaCache, - zoom: zoom, - screenSize: options.screenSize!, - swPanBoundary: options.swPanBoundary!, - nePanBoundary: options.nePanBoundary!, - ); - LatLng offsetToCrs(Offset offset, [double? zoom]) { final focalStartPt = project(center, zoom ?? this.zoom); final point = @@ -402,7 +417,10 @@ class FlutterMapState { } LatLng? adjustCenterIfOutsideMaxBounds( - LatLng testCenter, double testZoom, LatLngBounds maxBounds) { + LatLng testCenter, + double testZoom, + LatLngBounds maxBounds, + ) { LatLng? newCenter; final swPixel = project(maxBounds.southWest, testZoom); diff --git a/lib/src/map/flutter_map_state_container.dart b/lib/src/map/flutter_map_state_container.dart index 88c219b79..9c07e823b 100644 --- a/lib/src/map/flutter_map_state_container.dart +++ b/lib/src/map/flutter_map_state_container.dart @@ -2,21 +2,21 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/gestures/flutter_map_interactive_viewer.dart'; -import 'package:flutter_map/src/gestures/flutter_map_state_controller.dart'; +import 'package:flutter_map/src/map/flutter_map_internal_controller.dart'; import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; class FlutterMapStateContainer extends State { bool _hasFitInitialBounds = false; - late final FlutterMapStateController _flutterMapStateController; + late final FlutterMapInternalController _flutterMapStateController; late MapControllerImpl _mapController; late bool _mapControllerCreatedInternally; @override void initState() { super.initState(); - _flutterMapStateController = FlutterMapStateController(widget.options); + _flutterMapStateController = FlutterMapInternalController(widget.options); _initializeAndLinkMapController(); WidgetsBinding.instance @@ -57,8 +57,9 @@ class FlutterMapStateContainer extends State { controller: _flutterMapStateController, options: widget.options, builder: (context, mapState) => MapStateInheritedWidget( - mapController: _mapController, - mapState: mapState, + controller: _mapController, + options: widget.options, + state: mapState, child: ClipRect( child: Stack( children: [ @@ -85,13 +86,13 @@ class FlutterMapStateContainer extends State { void _setInitialFitBounds(BoxConstraints constraints) { // If bounds were provided set the initial center/zoom to match those // bounds once the parent constraints are available. - if (widget.options.bounds != null && + if (widget.options.initialBounds != null && !_hasFitInitialBounds && _parentConstraintsAreSet(context, constraints)) { _hasFitInitialBounds = true; _flutterMapStateController.fitBounds( - widget.options.bounds!, + widget.options.initialBounds!, widget.options.boundsOptions, offset: Offset.zero, ); @@ -103,10 +104,10 @@ class FlutterMapStateContainer extends State { constraints.maxWidth, constraints.maxHeight, ); - final oldMapState = _flutterMapStateController.value; + final oldMapState = _flutterMapStateController.mapState; if (_flutterMapStateController .setNonRotatedSizeWithoutEmittingEvent(nonRotatedSize)) { - final newMapState = _flutterMapStateController.value; + final newMapState = _flutterMapStateController.mapState; // Avoid emitting the event during build otherwise if the user calls // setState in the onMapEvent callback it will throw. diff --git a/lib/src/map/flutter_map_state_controller_interface.dart b/lib/src/map/flutter_map_state_controller_interface.dart deleted file mode 100644 index ba597a83b..000000000 --- a/lib/src/map/flutter_map_state_controller_interface.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/rendering.dart'; -import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/gestures/map_events.dart'; -import 'package:flutter_map/src/map/flutter_map_state.dart'; -import 'package:flutter_map/src/misc/fit_bounds_options.dart'; -import 'package:flutter_map/src/misc/move_and_rotate_result.dart'; -import 'package:flutter_map/src/misc/point.dart'; -import 'package:latlong2/latlong.dart'; - -/// Defines the methods that the MapController may call on the -/// FlutterMapStateController. This clarifies the link between the two classes. -abstract interface class FlutterMapStateControllerInterface { - FlutterMapState get value; - - bool move( - LatLng newCenter, - double newZoom, { - required Offset offset, - required bool hasGesture, - required MapEventSource source, - required String? id, - }); - - bool rotate( - double newRotation, { - required bool hasGesture, - required MapEventSource source, - required String? id, - }); - - MoveAndRotateResult rotateAroundPoint( - double degree, { - required CustomPoint? point, - required Offset? offset, - required bool hasGesture, - required MapEventSource source, - required String? id, - }); - - MoveAndRotateResult moveAndRotate( - LatLng newCenter, - double newZoom, - double newRotation, { - required Offset offset, - required bool hasGesture, - required MapEventSource source, - required String? id, - }); - - bool fitBounds( - LatLngBounds bounds, - FitBoundsOptions options, { - required Offset offset, - }); -} diff --git a/lib/src/map/flutter_map_state_inherited_widget.dart b/lib/src/map/flutter_map_state_inherited_widget.dart index 169b8aff0..3b8b35851 100644 --- a/lib/src/map/flutter_map_state_inherited_widget.dart +++ b/lib/src/map/flutter_map_state_inherited_widget.dart @@ -1,20 +1,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:flutter_map/src/map/map_controller.dart'; +import 'package:flutter_map/src/map/options.dart'; class MapStateInheritedWidget extends InheritedWidget { const MapStateInheritedWidget({ super.key, - required this.mapState, - required this.mapController, + required this.state, + required this.controller, + required this.options, required super.child, }); - final FlutterMapState mapState; - final MapController mapController; + final FlutterMapState state; + final MapController controller; + final MapOptions options; @override bool updateShouldNotify(MapStateInheritedWidget oldWidget) => - !identical(mapState, oldWidget.mapState) || - !identical(mapController, oldWidget.mapController); + !identical(state, oldWidget.state) || + !identical(controller, oldWidget.controller) || + !identical(options, oldWidget.options); } diff --git a/lib/src/map/map_controller.dart b/lib/src/map/map_controller.dart index d8ea6fee6..4dc925f2e 100644 --- a/lib/src/map/map_controller.dart +++ b/lib/src/map/map_controller.dart @@ -25,7 +25,7 @@ abstract class MapController { static MapController? maybeOf(BuildContext context) => context .dependOnInheritedWidgetOfExactType() - ?.mapController; + ?.controller; static MapController of(BuildContext context) => maybeOf(context) ?? diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/map_controller_impl.dart index 955af084d..aeb076542 100644 --- a/lib/src/map/map_controller_impl.dart +++ b/lib/src/map/map_controller_impl.dart @@ -2,10 +2,9 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/gestures/flutter_map_state_controller.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; +import 'package:flutter_map/src/map/flutter_map_internal_controller.dart'; import 'package:flutter_map/src/map/flutter_map_state.dart'; -import 'package:flutter_map/src/map/flutter_map_state_controller_interface.dart'; import 'package:flutter_map/src/map/map_controller.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; import 'package:flutter_map/src/misc/move_and_rotate_result.dart'; @@ -85,7 +84,7 @@ class MapControllerImpl implements MapController { ); @override - FlutterMapState get mapState => _stateController.value; + FlutterMapState get mapState => _stateController.mapState; final _mapEventStreamController = StreamController.broadcast(); @@ -94,9 +93,9 @@ class MapControllerImpl implements MapController { StreamSink get mapEventSink => _mapEventStreamController.sink; - late FlutterMapStateControllerInterface _stateController; + late FlutterMapInternalController _stateController; - set stateController(FlutterMapStateController stateController) { + set stateController(FlutterMapInternalController stateController) { _stateController = stateController; } diff --git a/lib/src/map/map_safe_area.dart b/lib/src/map/map_safe_area.dart index db691a24f..340cf0bca 100644 --- a/lib/src/map/map_safe_area.dart +++ b/lib/src/map/map_safe_area.dart @@ -6,69 +6,49 @@ import 'package:latlong2/latlong.dart'; class MapSafeArea { final LatLngBounds bounds; - final bool isLatitudeBlocked; - final bool isLongitudeBlocked; + final double zoom; + final bool _isLatitudeBlocked; + final bool _isLongitudeBlocked; - final double _zoom; - final Size _screenSize; - final LatLng _swPanBoundary; - final LatLng _nePanBoundary; + MapSafeArea._({ + required this.bounds, + required this.zoom, + }) : _isLatitudeBlocked = bounds.south > bounds.north, + _isLongitudeBlocked = bounds.west > bounds.east; - MapSafeArea({ - required LatLng southWest, - required LatLng northEast, - required double zoom, + factory MapSafeArea({ required Size screenSize, - required LatLng swPanBoundary, - required LatLng nePanBoundary, - }) : bounds = LatLngBounds(southWest, northEast), - isLatitudeBlocked = southWest.latitude > northEast.latitude, - isLongitudeBlocked = southWest.longitude > northEast.longitude, - _zoom = zoom, - _screenSize = screenSize, - _swPanBoundary = swPanBoundary, - _nePanBoundary = nePanBoundary; - - factory MapSafeArea.createUnlessMatching({ - MapSafeArea? previous, + required LatLngBounds bounds, required double zoom, - required Size screenSize, - required LatLng swPanBoundary, - required LatLng nePanBoundary, }) { - if (previous == null || - previous._zoom != zoom || - previous._screenSize != screenSize || - previous._swPanBoundary != swPanBoundary || - previous._nePanBoundary != nePanBoundary) { - final halfScreenHeightDeg = _halfScreenHeightDegrees(screenSize, zoom); - final halfScreenWidthDeg = _halfScreenWidthDegrees(screenSize, zoom); + final halfScreenHeightDeg = _halfScreenHeightDegrees(screenSize, zoom); + final halfScreenWidthDeg = _halfScreenWidthDegrees(screenSize, zoom); - final southWestLatitude = swPanBoundary.latitude + halfScreenHeightDeg; - final southWestLongitude = swPanBoundary.longitude + halfScreenWidthDeg; - final northEastLatitude = nePanBoundary.latitude - halfScreenHeightDeg; - final northEastLongitude = nePanBoundary.longitude - halfScreenWidthDeg; + final safeBounds = LatLngBounds( + LatLng( + bounds.north - halfScreenHeightDeg, + bounds.east - halfScreenWidthDeg, + ), + LatLng( + bounds.south + halfScreenHeightDeg, + bounds.west + halfScreenWidthDeg, + ), + ); - return MapSafeArea( - southWest: LatLng(southWestLatitude, southWestLongitude), - northEast: LatLng(northEastLatitude, northEastLongitude), - zoom: zoom, - screenSize: screenSize, - swPanBoundary: swPanBoundary, - nePanBoundary: nePanBoundary, - ); - } - return previous; + return MapSafeArea._( + bounds: safeBounds, + zoom: zoom, + ); } bool contains(LatLng point) => - isLatitudeBlocked || isLongitudeBlocked ? false : bounds.contains(point); + _isLatitudeBlocked || _isLongitudeBlocked ? false : bounds.contains(point); - LatLng containPoint(LatLng point, LatLng fallback) => LatLng( - isLatitudeBlocked + LatLng clampWithFallback(LatLng point, LatLng fallback) => LatLng( + _isLatitudeBlocked ? fallback.latitude : point.latitude.clamp(bounds.south, bounds.north), - isLongitudeBlocked + _isLongitudeBlocked ? fallback.longitude : point.longitude.clamp(bounds.west, bounds.east), ); diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index 129b1c287..0b49efeed 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -1,12 +1,14 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; import 'package:latlong2/latlong.dart'; /// Allows you to provide your map's starting properties for [zoom], [rotation] -/// and [center]. Alternatively you can provide [bounds] instead of [center]. -/// If both, [center] and [bounds] are provided, bounds will take preference -/// over [center]. +/// and [center]. Alternatively you can provide [initialBounds] instead of +/// [center]. If both [center] and [initialBounds] are provided, initialBounds +/// will take preference over [center]. +/// /// Zoom, pan boundary and interactivity constraints can be specified here too. /// /// Callbacks for [onTap], [onSecondaryTap], [onLongPress] and @@ -19,8 +21,7 @@ import 'package:latlong2/latlong.dart'; /// defined boundaries. /// /// If you download offline tiles dynamically, you can set [adaptiveBoundaries] -/// to true (make sure to also set [screenSize], [nePanBoundary], -/// [swPanBoundary] and an external [controller]), which will enforce +/// (make sure to also set an external [controller]), which will enforce /// panning/zooming to ensure there is never a need to display tiles outside the /// boundaries set by [swPanBoundary] and [nePanBoundary]. class MapOptions { @@ -101,13 +102,10 @@ class MapOptions { final PositionCallback? onPositionChanged; final MapEventCallback? onMapEvent; final bool slideOnBoundaries; - final Size? screenSize; - final bool adaptiveBoundaries; + final MapBoundary? boundary; final LatLng center; - final LatLngBounds? bounds; + final LatLngBounds? initialBounds; final FitBoundsOptions boundsOptions; - final LatLng? swPanBoundary; - final LatLng? nePanBoundary; /// OnMapReady is called after the map runs it's initState. /// At that point the map has assigned its state to the controller @@ -130,10 +128,10 @@ class MapOptions { /// widget from rebuilding. final bool keepAlive; - MapOptions({ + const MapOptions({ this.crs = const Epsg3857(), - LatLng? center, - this.bounds, + this.center = const LatLng(50.5, 30.51), + this.initialBounds, this.boundsOptions = const FitBoundsOptions(), this.zoom = 13.0, this.rotation = 0.0, @@ -163,23 +161,21 @@ class MapOptions { this.onMapEvent, this.onMapReady, this.slideOnBoundaries = false, - this.adaptiveBoundaries = false, - this.screenSize, - this.swPanBoundary, - this.nePanBoundary, + this.boundary, this.maxBounds, this.keepAlive = false, - }) : center = center ?? const LatLng(50.5, 30.51), - assert(rotationThreshold >= 0.0), + }) : assert(rotationThreshold >= 0.0), assert(pinchZoomThreshold >= 0.0), - assert(pinchMoveThreshold >= 0.0) { - assert( - !adaptiveBoundaries || - (screenSize != null && - swPanBoundary != null && - nePanBoundary != null), - 'screenSize, swPanBoundary and nePanBoundary must be set in order to enable adaptive boundaries.'); - } + assert(pinchMoveThreshold >= 0.0); + + static MapOptions? maybeOf(BuildContext context) => context + .dependOnInheritedWidgetOfExactType() + ?.options; + + static MapOptions of(BuildContext context) => + maybeOf(context) ?? + (throw StateError( + '`MapOptions.of()` should not be called outside a `FlutterMap` and its descendants')); } typedef MapEventCallback = void Function(MapEvent); diff --git a/lib/src/misc/map_boundary.dart b/lib/src/misc/map_boundary.dart new file mode 100644 index 000000000..da6fcd961 --- /dev/null +++ b/lib/src/misc/map_boundary.dart @@ -0,0 +1,64 @@ +import 'dart:ui'; + +import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/src/map/map_safe_area.dart'; +import 'package:latlong2/latlong.dart'; + +sealed class MapBoundary { + const MapBoundary(); +} + +class FixedBoundary extends MapBoundary { + final LatLngBounds latLngBounds; + + const FixedBoundary({ + required this.latLngBounds, + }); + + bool contains(LatLng latLng) => latLngBounds.contains(latLng); + + LatLng clamp(LatLng latLng) => LatLng( + latLng.latitude.clamp(latLngBounds.south, latLngBounds.north), + latLng.longitude.clamp(latLngBounds.west, latLngBounds.east), + ); +} + +class AdaptiveBoundary extends MapBoundary { + final Size screenSize; + final LatLngBounds latLngBounds; + MapSafeArea? _mapSafeAreaCache; + + /// Tiles outside of these bounds will never be displayed. + AdaptiveBoundary({ + required this.screenSize, + required this.latLngBounds, + }); + + bool contains(LatLng latLng, double zoom) => + _mapSafeArea(zoom).contains(latLng); + + LatLng clampWithFallback(LatLng latLng, LatLng fallback, double zoom) => + _mapSafeArea(zoom).clampWithFallback(latLng, fallback); + + MapSafeArea _mapSafeArea(double zoom) { + if (_mapSafeAreaCache?.zoom != zoom) { + return MapSafeArea( + screenSize: screenSize, + bounds: latLngBounds, + zoom: zoom, + ); + } + + return _mapSafeAreaCache!; + } + + @override + bool operator ==(Object other) { + return other is AdaptiveBoundary && + other.screenSize == screenSize && + other.latLngBounds == latLngBounds; + } + + @override + int get hashCode => Object.hash(screenSize, latLngBounds); +} diff --git a/test/flutter_map_controller_test.dart b/test/flutter_map_controller_test.dart index 83b768d74..2e2683971 100644 --- a/test/flutter_map_controller_test.dart +++ b/test/flutter_map_controller_test.dart @@ -30,14 +30,14 @@ void main() { controller.move(fit.center, fit.zoom); await tester.pump(); mapState = controller.mapState; - expect(mapState.bounds, equals(expectedBounds)); + expect(mapState.visibleBounds, equals(expectedBounds)); expect(mapState.center, equals(expectedCenter)); expect(mapState.zoom, equals(expectedZoom)); controller.fitBounds(bounds, options: fitOptions); await tester.pump(); mapState = controller.mapState; - expect(mapState.bounds, equals(expectedBounds)); + expect(mapState.visibleBounds, equals(expectedBounds)); expect(mapState.center, equals(expectedCenter)); expect(mapState.zoom, equals(expectedZoom)); } @@ -58,14 +58,14 @@ void main() { controller.move(fit.center, fit.zoom); await tester.pump(); mapState = controller.mapState; - expect(mapState.bounds, equals(expectedBounds)); + expect(mapState.visibleBounds, equals(expectedBounds)); expect(mapState.center, equals(expectedCenter)); expect(mapState.zoom, equals(expectedZoom)); controller.fitBounds(bounds, options: fitOptions); await tester.pump(); mapState = controller.mapState; - expect(mapState.bounds, equals(expectedBounds)); + expect(mapState.visibleBounds, equals(expectedBounds)); expect(mapState.center, equals(expectedCenter)); expect(mapState.zoom, equals(expectedZoom)); } @@ -87,7 +87,7 @@ void main() { await tester.pump(); mapState = controller.mapState; - expect(mapState.bounds, equals(expectedBounds)); + expect(mapState.visibleBounds, equals(expectedBounds)); expect(mapState.center, equals(expectedCenter)); expect(mapState.zoom, equals(expectedZoom)); @@ -95,7 +95,7 @@ void main() { await tester.pump(); mapState = controller.mapState; - expect(mapState.bounds, equals(expectedBounds)); + expect(mapState.visibleBounds, equals(expectedBounds)); expect(mapState.center, equals(expectedCenter)); expect(mapState.zoom, equals(expectedZoom)); } @@ -117,7 +117,7 @@ void main() { controller.move(fit.center, fit.zoom); await tester.pump(); mapState = controller.mapState; - expect(mapState.bounds, equals(expectedBounds)); + expect(mapState.visibleBounds, equals(expectedBounds)); expect(mapState.center, equals(expectedCenter)); expect(mapState.zoom, equals(expectedZoom)); @@ -125,7 +125,7 @@ void main() { await tester.pump(); mapState = controller.mapState; - expect(mapState.bounds, equals(expectedBounds)); + expect(mapState.visibleBounds, equals(expectedBounds)); expect(mapState.center, equals(expectedCenter)); expect(mapState.zoom, equals(expectedZoom)); } diff --git a/test/flutter_map_test.dart b/test/flutter_map_test.dart index f2e99635a..608cee7d4 100644 --- a/test/flutter_map_test.dart +++ b/test/flutter_map_test.dart @@ -36,8 +36,8 @@ void main() { int builds = 0; final map = FlutterMap( - options: MapOptions( - center: const LatLng(45.5231, -122.6765), + options: const MapOptions( + center: LatLng(45.5231, -122.6765), zoom: 13, ), children: [ diff --git a/test/test_utils/test_app.dart b/test/test_utils/test_app.dart index 4448dbca4..1aa11496f 100644 --- a/test/test_utils/test_app.dart +++ b/test/test_utils/test_app.dart @@ -31,8 +31,8 @@ class TestApp extends StatelessWidget { height: 200, child: FlutterMap( mapController: controller, - options: MapOptions( - center: const LatLng(45.5231, -122.6765), + options: const MapOptions( + center: LatLng(45.5231, -122.6765), zoom: 13, ), children: [ From d5a8acb49a86c8328cf1a236cdc24233d96b7a25 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Tue, 13 Jun 2023 13:53:51 +0200 Subject: [PATCH 14/46] Combine adaptive bounds, max bounds and sw/ne pan bounds in to a single MapBounds class --- .../lib/pages/animated_map_controller.dart | 10 +- example/lib/pages/circle.dart | 4 +- example/lib/pages/custom_crs/custom_crs.dart | 4 +- example/lib/pages/epsg3413_crs.dart | 4 +- example/lib/pages/epsg4326_crs.dart | 4 +- example/lib/pages/fallback_url.dart | 4 +- example/lib/pages/home.dart | 12 +- example/lib/pages/interactive_test_page.dart | 4 +- example/lib/pages/latlng_to_screen_point.dart | 6 +- example/lib/pages/many_markers.dart | 4 +- example/lib/pages/map_controller.dart | 4 +- example/lib/pages/map_inside_listview.dart | 4 +- example/lib/pages/markers.dart | 4 +- example/lib/pages/moving_markers.dart | 4 +- example/lib/pages/offline_map.dart | 4 +- example/lib/pages/overlay_image.dart | 4 +- example/lib/pages/plugin_scalebar.dart | 4 +- example/lib/pages/plugin_zoombuttons.dart | 4 +- example/lib/pages/point_to_latlng.dart | 4 +- example/lib/pages/polygon.dart | 4 +- example/lib/pages/polyline.dart | 4 +- example/lib/pages/reset_tile_layer.dart | 4 +- example/lib/pages/secondary_tap.dart | 4 +- example/lib/pages/sliding_map.dart | 7 +- example/lib/pages/stateful_markers.dart | 4 +- example/lib/pages/tile_builder_example.dart | 4 +- .../lib/pages/tile_loading_error_handle.dart | 4 +- example/lib/pages/wms_tile_layer.dart | 4 +- .../flutter_map_interactive_viewer.dart | 4 +- .../map/flutter_map_internal_controller.dart | 30 +-- lib/src/map/flutter_map_state.dart | 220 +++--------------- lib/src/map/flutter_map_state_container.dart | 25 +- lib/src/map/map_controller.dart | 14 +- lib/src/map/map_controller_impl.dart | 37 +-- lib/src/map/map_safe_area.dart | 69 ------ lib/src/map/options.dart | 78 +++---- lib/src/misc/center_zoom.dart | 16 ++ lib/src/misc/fit_bounds_options.dart | 81 +++++++ lib/src/misc/map_boundary.dart | 142 ++++++++--- test/flutter_map_controller_test.dart | 30 +-- test/flutter_map_test.dart | 4 +- test/misc/map_boundary_test.dart | 36 +++ test/test_utils/test_app.dart | 4 +- 43 files changed, 458 insertions(+), 463 deletions(-) delete mode 100644 lib/src/map/map_safe_area.dart create mode 100644 test/misc/map_boundary_test.dart diff --git a/example/lib/pages/animated_map_controller.dart b/example/lib/pages/animated_map_controller.dart index 67ee6c194..3a2803cc6 100644 --- a/example/lib/pages/animated_map_controller.dart +++ b/example/lib/pages/animated_map_controller.dart @@ -165,7 +165,7 @@ class AnimatedMapControllerPageState extends State mapController.fitBounds( bounds, options: const FitBoundsOptions( - padding: EdgeInsets.only(left: 15, right: 15), + padding: EdgeInsets.symmetric(horizontal: 15), ), ); }, @@ -179,8 +179,8 @@ class AnimatedMapControllerPageState extends State london, ]); - final centerZoom = - mapController.mapState.centerZoomFitBounds(bounds); + final centerZoom = const FitBoundsOptions() + .fit(mapController.mapState, bounds); _animatedMapMove(centerZoom.center, centerZoom.zoom); }, child: const Text('Fit Bounds animated'), @@ -192,8 +192,8 @@ class AnimatedMapControllerPageState extends State child: FlutterMap( mapController: mapController, options: const MapOptions( - center: LatLng(51.5, -0.09), - zoom: 5, + initialCenter: LatLng(51.5, -0.09), + initialZoom: 5, maxZoom: 10, minZoom: 3), children: [ diff --git a/example/lib/pages/circle.dart b/example/lib/pages/circle.dart index 2f6c75f78..88cc3f8e7 100644 --- a/example/lib/pages/circle.dart +++ b/example/lib/pages/circle.dart @@ -34,8 +34,8 @@ class CirclePage extends StatelessWidget { Flexible( child: FlutterMap( options: const MapOptions( - center: LatLng(51.5, -0.09), - zoom: 11, + initialCenter: LatLng(51.5, -0.09), + initialZoom: 11, ), children: [ TileLayer( diff --git a/example/lib/pages/custom_crs/custom_crs.dart b/example/lib/pages/custom_crs/custom_crs.dart index 83d887747..3022f32d2 100644 --- a/example/lib/pages/custom_crs/custom_crs.dart +++ b/example/lib/pages/custom_crs/custom_crs.dart @@ -128,8 +128,8 @@ class _CustomCrsPageState extends State { options: MapOptions( // Set the default CRS crs: epsg3413CRS, - center: LatLng(point.x, point.y), - zoom: 3, + initialCenter: LatLng(point.x, point.y), + initialZoom: 3, // Set maxZoom usually scales.length - 1 OR resolutions.length - 1 // but not greater maxZoom: maxZoom, diff --git a/example/lib/pages/epsg3413_crs.dart b/example/lib/pages/epsg3413_crs.dart index 3d7d4affd..11d9f4636 100644 --- a/example/lib/pages/epsg3413_crs.dart +++ b/example/lib/pages/epsg3413_crs.dart @@ -130,8 +130,8 @@ class _EPSG3413PageState extends State { child: FlutterMap( options: MapOptions( crs: epsg3413CRS, - center: const LatLng(90, 0), - zoom: 3, + initialCenter: const LatLng(90, 0), + initialZoom: 3, maxZoom: maxZoom, ), nonRotatedChildren: [ diff --git a/example/lib/pages/epsg4326_crs.dart b/example/lib/pages/epsg4326_crs.dart index d3a6488fe..0bc393905 100644 --- a/example/lib/pages/epsg4326_crs.dart +++ b/example/lib/pages/epsg4326_crs.dart @@ -26,8 +26,8 @@ class EPSG4326Page extends StatelessWidget { options: const MapOptions( minZoom: 0, crs: Epsg4326(), - center: LatLng(0, 0), - zoom: 0, + initialCenter: LatLng(0, 0), + initialZoom: 0, ), children: [ TileLayer( diff --git a/example/lib/pages/fallback_url.dart b/example/lib/pages/fallback_url.dart index 619de1e60..11fd6e8e9 100644 --- a/example/lib/pages/fallback_url.dart +++ b/example/lib/pages/fallback_url.dart @@ -41,8 +41,8 @@ class FallbackUrlPage extends StatelessWidget { Flexible( child: FlutterMap( options: MapOptions( - center: center, - zoom: zoom, + initialCenter: center, + initialZoom: zoom, maxZoom: maxZoom, minZoom: minZoom, ), diff --git a/example/lib/pages/home.dart b/example/lib/pages/home.dart index 794d8f61d..9bf0e9fd2 100644 --- a/example/lib/pages/home.dart +++ b/example/lib/pages/home.dart @@ -112,11 +112,13 @@ class _HomePageState extends State { Flexible( child: FlutterMap( options: MapOptions( - center: const LatLng(51.5, -0.09), - zoom: 5, - maxBounds: LatLngBounds( - const LatLng(-90, -180), - const LatLng(90, 180), + initialCenter: const LatLng(51.5, -0.09), + initialZoom: 5, + boundary: VisibleCenterBoundary( + latLngBounds: LatLngBounds( + const LatLng(-90, -180), + const LatLng(90, 180), + ), ), ), nonRotatedChildren: [ diff --git a/example/lib/pages/interactive_test_page.dart b/example/lib/pages/interactive_test_page.dart index 5b9997291..43d7b1266 100644 --- a/example/lib/pages/interactive_test_page.dart +++ b/example/lib/pages/interactive_test_page.dart @@ -144,8 +144,8 @@ class _InteractiveTestPageState extends State { child: FlutterMap( options: MapOptions( onMapEvent: onMapEvent, - center: const LatLng(51.5, -0.09), - zoom: 11, + initialCenter: const LatLng(51.5, -0.09), + initialZoom: 11, interactiveFlags: flags, ), children: [ diff --git a/example/lib/pages/latlng_to_screen_point.dart b/example/lib/pages/latlng_to_screen_point.dart index 8b28a120f..149eeac77 100644 --- a/example/lib/pages/latlng_to_screen_point.dart +++ b/example/lib/pages/latlng_to_screen_point.dart @@ -52,9 +52,9 @@ class _LatLngScreenPointTestPageState extends State { _textPos = CustomPoint(pt1.x, pt1.y); setState(() {}); }, - center: const LatLng(51.5, -0.09), - zoom: 11, - rotation: 0, + initialCenter: const LatLng(51.5, -0.09), + initialZoom: 11, + initialRotation: 0, ), children: [ TileLayer( diff --git a/example/lib/pages/many_markers.dart b/example/lib/pages/many_markers.dart index e2da101fd..aab04bfba 100644 --- a/example/lib/pages/many_markers.dart +++ b/example/lib/pages/many_markers.dart @@ -73,8 +73,8 @@ class _ManyMarkersPageState extends State { Flexible( child: FlutterMap( options: const MapOptions( - center: LatLng(50, 20), - zoom: 5, + initialCenter: LatLng(50, 20), + initialZoom: 5, interactiveFlags: InteractiveFlag.all - InteractiveFlag.rotate, ), children: [ diff --git a/example/lib/pages/map_controller.dart b/example/lib/pages/map_controller.dart index 4fbea3d55..28d1e0427 100644 --- a/example/lib/pages/map_controller.dart +++ b/example/lib/pages/map_controller.dart @@ -152,8 +152,8 @@ class MapControllerPageState extends State { child: FlutterMap( mapController: _mapController, options: const MapOptions( - center: LatLng(51.5, -0.09), - zoom: 5, + initialCenter: LatLng(51.5, -0.09), + initialZoom: 5, maxZoom: 5, minZoom: 3, ), diff --git a/example/lib/pages/map_inside_listview.dart b/example/lib/pages/map_inside_listview.dart index 9298ba634..edc4df362 100644 --- a/example/lib/pages/map_inside_listview.dart +++ b/example/lib/pages/map_inside_listview.dart @@ -23,8 +23,8 @@ class MapInsideListViewPage extends StatelessWidget { height: 300, child: FlutterMap( options: const MapOptions( - center: LatLng(51.5, -0.09), - zoom: 5, + initialCenter: LatLng(51.5, -0.09), + initialZoom: 5, ), nonRotatedChildren: const [ FlutterMapZoomButtons( diff --git a/example/lib/pages/markers.dart b/example/lib/pages/markers.dart index a0f417c97..746d1af5b 100644 --- a/example/lib/pages/markers.dart +++ b/example/lib/pages/markers.dart @@ -104,8 +104,8 @@ class MarkerPageState extends State { Flexible( child: FlutterMap( options: MapOptions( - center: const LatLng(51.5, -0.09), - zoom: 5, + initialCenter: const LatLng(51.5, -0.09), + initialZoom: 5, onTap: (_, p) => setState(() => customMarkers.add(buildPin(p))), interactiveFlags: ~InteractiveFlag.doubleTapZoom, diff --git a/example/lib/pages/moving_markers.dart b/example/lib/pages/moving_markers.dart index b65efaca2..02b984034 100644 --- a/example/lib/pages/moving_markers.dart +++ b/example/lib/pages/moving_markers.dart @@ -55,8 +55,8 @@ class _MovingMarkersPageState extends State { Flexible( child: FlutterMap( options: const MapOptions( - center: LatLng(51.5, -0.09), - zoom: 5, + initialCenter: LatLng(51.5, -0.09), + initialZoom: 5, ), children: [ TileLayer( diff --git a/example/lib/pages/offline_map.dart b/example/lib/pages/offline_map.dart index a156e212d..e67876fc3 100644 --- a/example/lib/pages/offline_map.dart +++ b/example/lib/pages/offline_map.dart @@ -25,10 +25,10 @@ class OfflineMapPage extends StatelessWidget { Flexible( child: FlutterMap( options: MapOptions( - center: const LatLng(56.704173, 11.543808), + initialCenter: const LatLng(56.704173, 11.543808), minZoom: 12, maxZoom: 14, - boundary: FixedBoundary( + boundary: VisibleEdgeBoundary( latLngBounds: LatLngBounds( const LatLng(56.7378, 11.6644), const LatLng(56.6877, 11.5089), diff --git a/example/lib/pages/overlay_image.dart b/example/lib/pages/overlay_image.dart index 203a47862..f3310238f 100644 --- a/example/lib/pages/overlay_image.dart +++ b/example/lib/pages/overlay_image.dart @@ -46,8 +46,8 @@ class OverlayImagePage extends StatelessWidget { Flexible( child: FlutterMap( options: const MapOptions( - center: LatLng(51.5, -0.09), - zoom: 6, + initialCenter: LatLng(51.5, -0.09), + initialZoom: 6, ), children: [ TileLayer( diff --git a/example/lib/pages/plugin_scalebar.dart b/example/lib/pages/plugin_scalebar.dart index 744dd8c39..f0fc28596 100644 --- a/example/lib/pages/plugin_scalebar.dart +++ b/example/lib/pages/plugin_scalebar.dart @@ -21,8 +21,8 @@ class PluginScaleBar extends StatelessWidget { Flexible( child: FlutterMap( options: const MapOptions( - center: LatLng(51.5, -0.09), - zoom: 5, + initialCenter: LatLng(51.5, -0.09), + initialZoom: 5, ), nonRotatedChildren: [ ScaleLayerWidget( diff --git a/example/lib/pages/plugin_zoombuttons.dart b/example/lib/pages/plugin_zoombuttons.dart index a324e8bf4..ca24acb3c 100644 --- a/example/lib/pages/plugin_zoombuttons.dart +++ b/example/lib/pages/plugin_zoombuttons.dart @@ -21,8 +21,8 @@ class PluginZoomButtons extends StatelessWidget { Flexible( child: FlutterMap( options: const MapOptions( - center: LatLng(51.5, -0.09), - zoom: 5, + initialCenter: LatLng(51.5, -0.09), + initialZoom: 5, ), nonRotatedChildren: const [ FlutterMapZoomButtons( diff --git a/example/lib/pages/point_to_latlng.dart b/example/lib/pages/point_to_latlng.dart index fb6230695..756d51c33 100644 --- a/example/lib/pages/point_to_latlng.dart +++ b/example/lib/pages/point_to_latlng.dart @@ -59,8 +59,8 @@ class PointToLatlngPage extends State { onMapEvent: (event) { updatePoint(null, context); }, - center: const LatLng(51.5, -0.09), - zoom: 5, + initialCenter: const LatLng(51.5, -0.09), + initialZoom: 5, minZoom: 3, ), children: [ diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index 345ad0f11..7667e39e1 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -78,8 +78,8 @@ class PolygonPage extends StatelessWidget { Flexible( child: FlutterMap( options: const MapOptions( - center: LatLng(51.5, -0.09), - zoom: 5, + initialCenter: LatLng(51.5, -0.09), + initialZoom: 5, ), children: [ TileLayer( diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index a3f178ff9..ce6e170e9 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -29,8 +29,8 @@ class _PolylinePageState extends State { Flexible( child: FlutterMap( options: const MapOptions( - center: LatLng(51.5, -0.09), - zoom: 5, + initialCenter: LatLng(51.5, -0.09), + initialZoom: 5, ), children: [ TileLayer( diff --git a/example/lib/pages/reset_tile_layer.dart b/example/lib/pages/reset_tile_layer.dart index 930000dd3..8393a8409 100644 --- a/example/lib/pages/reset_tile_layer.dart +++ b/example/lib/pages/reset_tile_layer.dart @@ -72,8 +72,8 @@ class ResetTileLayerPageState extends State { Flexible( child: FlutterMap( options: const MapOptions( - center: LatLng(51.5, -0.09), - zoom: 5, + initialCenter: LatLng(51.5, -0.09), + initialZoom: 5, ), children: [ TileLayer( diff --git a/example/lib/pages/secondary_tap.dart b/example/lib/pages/secondary_tap.dart index 6a82a66f9..f0a9273d7 100644 --- a/example/lib/pages/secondary_tap.dart +++ b/example/lib/pages/secondary_tap.dart @@ -29,8 +29,8 @@ class SecondaryTapPage extends StatelessWidget { SnackBar(content: Text('Secondary tap at $latLng')), ); }, - center: const LatLng(51.5, -0.09), - zoom: 5, + initialCenter: const LatLng(51.5, -0.09), + initialZoom: 5, ), children: [ TileLayer( diff --git a/example/lib/pages/sliding_map.dart b/example/lib/pages/sliding_map.dart index 83c512096..eb7c63984 100644 --- a/example/lib/pages/sliding_map.dart +++ b/example/lib/pages/sliding_map.dart @@ -25,12 +25,11 @@ class SlidingMapPage extends StatelessWidget { Flexible( child: FlutterMap( options: MapOptions( - center: const LatLng(56.704173, 11.543808), + initialCenter: const LatLng(56.704173, 11.543808), minZoom: 12, maxZoom: 14, - zoom: 13, - boundary: AdaptiveBoundary( - screenSize: MediaQuery.of(context).size, + initialZoom: 13, + boundary: VisibleEdgeBoundary( latLngBounds: LatLngBounds( const LatLng(56.7378, 11.6644), const LatLng(56.6877, 11.5089), diff --git a/example/lib/pages/stateful_markers.dart b/example/lib/pages/stateful_markers.dart index 2021f7d0f..1b5468628 100644 --- a/example/lib/pages/stateful_markers.dart +++ b/example/lib/pages/stateful_markers.dart @@ -60,8 +60,8 @@ class _StatefulMarkersPageState extends State { Flexible( child: FlutterMap( options: const MapOptions( - center: LatLng(51.5, -0.09), - zoom: 5, + initialCenter: LatLng(51.5, -0.09), + initialZoom: 5, ), children: [ TileLayer( diff --git a/example/lib/pages/tile_builder_example.dart b/example/lib/pages/tile_builder_example.dart index ca2b9bda0..cad94dec5 100644 --- a/example/lib/pages/tile_builder_example.dart +++ b/example/lib/pages/tile_builder_example.dart @@ -121,8 +121,8 @@ class _TileBuilderPageState extends State { padding: const EdgeInsets.all(8), child: FlutterMap( options: const MapOptions( - center: LatLng(51.5, -0.09), - zoom: 5, + initialCenter: LatLng(51.5, -0.09), + initialZoom: 5, ), children: [ _darkModeContainerIfEnabled( diff --git a/example/lib/pages/tile_loading_error_handle.dart b/example/lib/pages/tile_loading_error_handle.dart index 5c1f55cd4..64de27f77 100644 --- a/example/lib/pages/tile_loading_error_handle.dart +++ b/example/lib/pages/tile_loading_error_handle.dart @@ -32,8 +32,8 @@ class _TileLoadingErrorHandleState extends State { child: Builder(builder: (BuildContext context) { return FlutterMap( options: MapOptions( - center: const LatLng(51.5, -0.09), - zoom: 5, + initialCenter: const LatLng(51.5, -0.09), + initialZoom: 5, onPositionChanged: (MapPosition mapPosition, bool _) { needLoadingError = true; }, diff --git a/example/lib/pages/wms_tile_layer.dart b/example/lib/pages/wms_tile_layer.dart index a035222ef..4e90e8100 100644 --- a/example/lib/pages/wms_tile_layer.dart +++ b/example/lib/pages/wms_tile_layer.dart @@ -25,8 +25,8 @@ class WMSLayerPage extends StatelessWidget { Flexible( child: FlutterMap( options: const MapOptions( - center: LatLng(42.58, 12.43), - zoom: 6, + initialCenter: LatLng(42.58, 12.43), + initialZoom: 6, ), nonRotatedChildren: [ RichAttributionWidget( diff --git a/lib/src/gestures/flutter_map_interactive_viewer.dart b/lib/src/gestures/flutter_map_interactive_viewer.dart index 7f5f4a3a0..9f1f2b8be 100644 --- a/lib/src/gestures/flutter_map_interactive_viewer.dart +++ b/lib/src/gestures/flutter_map_interactive_viewer.dart @@ -713,9 +713,7 @@ class FlutterMapInteractiveViewerState _closeFlingAnimationController(MapEventSource.doubleTap); _closeDoubleTapController(MapEventSource.doubleTap); - debugPrint('CHECKING'); if (InteractiveFlag.hasDoubleTapZoom(_options.interactiveFlags)) { - debugPrint('YES'); final newZoom = _getZoomForScale(_mapState.zoom, 2); final newCenter = _mapState.focusedZoomCenter( tapPosition.relative!.toCustomPoint(), @@ -846,7 +844,7 @@ class FlutterMapInteractiveViewerState double _getZoomForScale(double startZoom, double scale) { final resultZoom = scale == 1.0 ? startZoom : startZoom + math.log(scale) / math.ln2; - return _mapState.fitZoomToBounds(resultZoom); + return _mapState.clampZoom(resultZoom); } Offset _rotateOffset(Offset offset) { diff --git a/lib/src/map/flutter_map_internal_controller.dart b/lib/src/map/flutter_map_internal_controller.dart index a24867c14..022165e79 100644 --- a/lib/src/map/flutter_map_internal_controller.dart +++ b/lib/src/map/flutter_map_internal_controller.dart @@ -33,7 +33,7 @@ class FlutterMapInternalController void linkMapController(MapControllerImpl mapControllerImpl) { _mapControllerImpl = mapControllerImpl; - _mapControllerImpl.stateController = this; + _mapControllerImpl.internalController = this; } /// This setter should only be called in this class or within tests. Changes @@ -53,8 +53,6 @@ class FlutterMapInternalController required MapEventSource source, required String? id, }) { - newZoom = mapState.fitZoomToBounds(newZoom); - // Algorithm thanks to https://github.com/tlserver/flutter_map_location_marker if (offset != Offset.zero) { final newPoint = mapState.project(newCenter, newZoom); @@ -67,22 +65,16 @@ class FlutterMapInternalController ); } - // TODO: Why do we have two separate methods of adjusting the bounds? - if (mapState.isOutOfBounds(newCenter)) { - if (!options.slideOnBoundaries) return false; - newCenter = mapState.clampWithFallback(newCenter, mapState.center); - } - - if (options.maxBounds != null) { - final adjustedCenter = mapState.adjustCenterIfOutsideMaxBounds( - newCenter, - newZoom, - options.maxBounds!, - ); - - if (adjustedCenter == null) return false; - newCenter = adjustedCenter; - } + newZoom = mapState.clampZoom(newZoom); + final centerZoom = mapState.clampCenterZoom( + CenterZoom( + center: newCenter, + zoom: newZoom, + ), + ); + if (centerZoom == null) return false; + newCenter = centerZoom.center; + newZoom = centerZoom.zoom; if (newCenter == mapState.center && newZoom == mapState.zoom) { return false; diff --git a/lib/src/map/flutter_map_state.dart b/lib/src/map/flutter_map_state.dart index be999ec54..c0940cdfb 100644 --- a/lib/src/map/flutter_map_state.dart +++ b/lib/src/map/flutter_map_state.dart @@ -23,7 +23,7 @@ class FlutterMapState { final Crs crs; final double? minZoom; final double? maxZoom; - final MapBoundary? boundary; + final MapBoundary boundary; final LatLng center; final double zoom; @@ -56,9 +56,9 @@ class FlutterMapState { minZoom = options.minZoom, maxZoom = options.maxZoom, boundary = options.boundary, - center = options.center, - zoom = options.zoom, - rotation = options.rotation, + center = options.initialCenter, + zoom = options.initialZoom, + rotation = options.initialRotation, nonRotatedSize = kImpossibleSize, size = kImpossibleSize; @@ -94,7 +94,7 @@ class FlutterMapState { zoom: zoom, rotation: rotation, nonRotatedSize: nonRotatedSize, - size: _calculateSize(rotation, nonRotatedSize), + size: calculateRotatedSize(rotation, nonRotatedSize), ); } @@ -110,7 +110,7 @@ class FlutterMapState { zoom: zoom, rotation: rotation, nonRotatedSize: nonRotatedSize, - size: _calculateSize(rotation, nonRotatedSize), + size: calculateRotatedSize(rotation, nonRotatedSize), ); } @@ -131,7 +131,7 @@ class FlutterMapState { zoom: zoom, rotation: rotation, nonRotatedSize: nonRotatedSize, - size: _calculateSize(rotation, nonRotatedSize), + size: calculateRotatedSize(rotation, nonRotatedSize), ); } @@ -151,9 +151,6 @@ class FlutterMapState { size: size, ); - Bounds get pixelBounds => - _pixelBounds ?? (_pixelBounds = getPixelBounds()); - @Deprecated('Use visibleBounds instead.') LatLngBounds get bounds => visibleBounds; @@ -168,7 +165,7 @@ class FlutterMapState { _pixelOrigin ?? (_pixelOrigin = (project(center, zoom) - size / 2.0).round()); - static CustomPoint _calculateSize( + static CustomPoint calculateRotatedSize( double rotation, CustomPoint nonRotatedSize, ) { @@ -188,84 +185,11 @@ class FlutterMapState { CenterZoom centerZoomFitBounds( LatLngBounds bounds, { - FitBoundsOptions options = - const FitBoundsOptions(padding: EdgeInsets.all(12)), - }) { - final paddingTL = - CustomPoint(options.padding.left, options.padding.top); - final paddingBR = - CustomPoint(options.padding.right, options.padding.bottom); - - final paddingTotalXY = paddingTL + paddingBR; - - var zoom = getBoundsZoom( - bounds, - paddingTotalXY, - inside: options.inside, - forceIntegerZoomLevel: options.forceIntegerZoomLevel, - ); - zoom = math.min(options.maxZoom, zoom); - - final paddingOffset = (paddingBR - paddingTL) / 2; - final swPoint = project(bounds.southWest, zoom); - final nePoint = project(bounds.northEast, zoom); - - final CustomPoint projectedCenter; - if (rotation != 0.0) { - final swPointRotated = swPoint.rotate(-rotationRad); - final nePointRotated = nePoint.rotate(-rotationRad); - final centerRotated = - (swPointRotated + nePointRotated) / 2 + paddingOffset; - - projectedCenter = centerRotated.rotate(rotationRad); - } else { - projectedCenter = (swPoint + nePoint) / 2 + paddingOffset; - } - - final center = unproject(projectedCenter, zoom); - - return CenterZoom( - center: center, - zoom: zoom, - ); - } - - double getBoundsZoom( - LatLngBounds bounds, - CustomPoint padding, { - bool inside = false, - bool forceIntegerZoomLevel = false, - }) { - final min = minZoom ?? 0.0; - final max = maxZoom ?? double.infinity; - final nw = bounds.northWest; - final se = bounds.southEast; - var size = nonRotatedSize - padding; - // Prevent negative size which results in NaN zoom value later on in the calculation - size = CustomPoint(math.max(0, size.x), math.max(0, size.y)); - var boundsSize = Bounds(project(se, zoom), project(nw, zoom)).size; - if (rotation != 0.0) { - final cosAngle = math.cos(rotationRad).abs(); - final sinAngle = math.sin(rotationRad).abs(); - boundsSize = CustomPoint( - (boundsSize.x * cosAngle) + (boundsSize.y * sinAngle), - (boundsSize.y * cosAngle) + (boundsSize.x * sinAngle), - ); - } - - final scaleX = size.x / boundsSize.x; - final scaleY = size.y / boundsSize.y; - final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); - - var boundsZoom = getScaleZoom(scale, zoom); - - if (forceIntegerZoomLevel) { - boundsZoom = - inside ? boundsZoom.ceilToDouble() : boundsZoom.floorToDouble(); - } - - return math.max(min, math.min(max, boundsZoom)); - } + FitBoundsOptions options = const FitBoundsOptions( + padding: EdgeInsets.all(12), + ), + }) => + options.fit(this, bounds); CustomPoint project(LatLng latlng, [double? zoom]) => crs.latLngToPoint(latlng, zoom ?? this.zoom); @@ -294,9 +218,12 @@ class FlutterMapState { return (project(center, zoom) - halfSize).round(); } - Bounds getPixelBounds([double? zoom]) { + Bounds get pixelBounds => + _pixelBounds ?? (_pixelBounds = pixelBoundsAtZoom(zoom)); + + Bounds pixelBoundsAtZoom(double zoom) { CustomPoint halfSize = size / 2; - if (zoom != null) { + if (zoom != this.zoom) { final scale = getZoomScale(this.zoom, zoom); halfSize = size / (scale * 2); } @@ -358,39 +285,20 @@ class FlutterMapState { return CustomPoint(tp.dx, tp.dy); } - double fitZoomToBounds(double zoom) { - // Abide to min/max zoom - if (maxZoom != null) { - zoom = (zoom > maxZoom!) ? maxZoom! : zoom; - } - if (minZoom != null) { - zoom = (zoom < minZoom!) ? minZoom! : zoom; - } - return zoom; - } - - // Returns true if given [center] is outside of the allowed bounds. - bool isOutOfBounds(LatLng latLng) { - switch (boundary) { - case FixedBoundary(): - return !(boundary as FixedBoundary).contains(latLng); - case AdaptiveBoundary(): - return !(boundary as AdaptiveBoundary).contains(latLng, zoom); - case null: - return false; - } - } + double clampZoom(double zoom) => zoom.clamp( + minZoom ?? double.negativeInfinity, + maxZoom ?? double.infinity, + ); - LatLng clampWithFallback(LatLng point, LatLng fallback) { - switch (boundary) { - case FixedBoundary(): - return (boundary as FixedBoundary).clamp(point); - case AdaptiveBoundary(): - return (boundary as AdaptiveBoundary) - .clampWithFallback(point, fallback, zoom); - case null: - return point; - } + CenterZoom? clampCenterZoom( + CenterZoom centerZoom, { + MapBoundary? boundary, + }) { + return (boundary ?? this.boundary).clampCenterZoom( + crs: crs, + visibleSize: size, + centerZoom: centerZoom, + ); } LatLng offsetToCrs(Offset offset, [double? zoom]) { @@ -415,70 +323,4 @@ class FlutterMapState { final newCenter = unproject(mapCenter + newOffset); return newCenter; } - - LatLng? adjustCenterIfOutsideMaxBounds( - LatLng testCenter, - double testZoom, - LatLngBounds maxBounds, - ) { - LatLng? newCenter; - - final swPixel = project(maxBounds.southWest, testZoom); - final nePixel = project(maxBounds.northEast, testZoom); - - final centerPix = project(testCenter, testZoom); - - final halfSizeX = size.x / 2; - final halfSizeY = size.y / 2; - - // Try and find the edge value that the center could use to stay within - // the maxBounds. This should be ok for panning. If we zoom, it is possible - // there is no solution to keep all corners within the bounds. If the edges - // are still outside the bounds, don't return anything. - final leftOkCenter = math.min(swPixel.x, nePixel.x) + halfSizeX; - final rightOkCenter = math.max(swPixel.x, nePixel.x) - halfSizeX; - final topOkCenter = math.min(swPixel.y, nePixel.y) + halfSizeY; - final botOkCenter = math.max(swPixel.y, nePixel.y) - halfSizeY; - - double? newCenterX; - double? newCenterY; - - var wasAdjusted = false; - - if (centerPix.x < leftOkCenter) { - wasAdjusted = true; - newCenterX = leftOkCenter; - } else if (centerPix.x > rightOkCenter) { - wasAdjusted = true; - newCenterX = rightOkCenter; - } - - if (centerPix.y < topOkCenter) { - wasAdjusted = true; - newCenterY = topOkCenter; - } else if (centerPix.y > botOkCenter) { - wasAdjusted = true; - newCenterY = botOkCenter; - } - - if (!wasAdjusted) { - return testCenter; - } - - final newCx = newCenterX ?? centerPix.x; - final newCy = newCenterY ?? centerPix.y; - - // Have a final check, see if the adjusted center is within maxBounds. - // If not, give up. - if (newCx < leftOkCenter || - newCx > rightOkCenter || - newCy < topOkCenter || - newCy > botOkCenter) { - return null; - } else { - newCenter = unproject(CustomPoint(newCx, newCy), testZoom); - } - - return newCenter; - } } diff --git a/lib/src/map/flutter_map_state_container.dart b/lib/src/map/flutter_map_state_container.dart index 9c07e823b..2cc6fbcd2 100644 --- a/lib/src/map/flutter_map_state_container.dart +++ b/lib/src/map/flutter_map_state_container.dart @@ -9,14 +9,15 @@ import 'package:flutter_map/src/map/map_controller_impl.dart'; class FlutterMapStateContainer extends State { bool _hasFitInitialBounds = false; - late final FlutterMapInternalController _flutterMapStateController; + late final FlutterMapInternalController _flutterMapInternalController; late MapControllerImpl _mapController; late bool _mapControllerCreatedInternally; @override void initState() { super.initState(); - _flutterMapStateController = FlutterMapInternalController(widget.options); + _flutterMapInternalController = + FlutterMapInternalController(widget.options); _initializeAndLinkMapController(); WidgetsBinding.instance @@ -25,7 +26,7 @@ class FlutterMapStateContainer extends State { @override void didUpdateWidget(FlutterMap oldWidget) { - _flutterMapStateController.setOptions(widget.options); + _flutterMapInternalController.setOptions(widget.options); if (oldWidget.mapController != widget.mapController) { _initializeAndLinkMapController(); } @@ -35,7 +36,7 @@ class FlutterMapStateContainer extends State { @override void dispose() { if (_mapControllerCreatedInternally) _mapController.dispose(); - _flutterMapStateController.dispose(); + _flutterMapInternalController.dispose(); super.dispose(); } @@ -43,7 +44,7 @@ class FlutterMapStateContainer extends State { _mapController = (widget.mapController ?? MapController()) as MapControllerImpl; _mapControllerCreatedInternally = widget.mapController == null; - _flutterMapStateController.linkMapController(_mapController); + _flutterMapInternalController.linkMapController(_mapController); } @override @@ -54,7 +55,7 @@ class FlutterMapStateContainer extends State { _setInitialFitBounds(constraints); return FlutterMapInteractiveViewer( - controller: _flutterMapStateController, + controller: _flutterMapInternalController, options: widget.options, builder: (context, mapState) => MapStateInheritedWidget( controller: _mapController, @@ -91,9 +92,9 @@ class FlutterMapStateContainer extends State { _parentConstraintsAreSet(context, constraints)) { _hasFitInitialBounds = true; - _flutterMapStateController.fitBounds( + _flutterMapInternalController.fitBounds( widget.options.initialBounds!, - widget.options.boundsOptions, + widget.options.initialBoundsOptions, offset: Offset.zero, ); } @@ -104,16 +105,16 @@ class FlutterMapStateContainer extends State { constraints.maxWidth, constraints.maxHeight, ); - final oldMapState = _flutterMapStateController.mapState; - if (_flutterMapStateController + final oldMapState = _flutterMapInternalController.mapState; + if (_flutterMapInternalController .setNonRotatedSizeWithoutEmittingEvent(nonRotatedSize)) { - final newMapState = _flutterMapStateController.mapState; + final newMapState = _flutterMapInternalController.mapState; // Avoid emitting the event during build otherwise if the user calls // setState in the onMapEvent callback it will throw. WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { - _flutterMapStateController.nonRotatedSizeChange( + _flutterMapInternalController.nonRotatedSizeChange( MapEventSource.nonRotatedSizeChange, oldMapState, newMapState, diff --git a/lib/src/map/map_controller.dart b/lib/src/map/map_controller.dart index 4dc925f2e..ea5ed4208 100644 --- a/lib/src/map/map_controller.dart +++ b/lib/src/map/map_controller.dart @@ -125,7 +125,19 @@ abstract class MapController { /// /// For information about return value meaning and emitted events, see [move]'s /// documentation. - bool fitBounds(LatLngBounds bounds, {FitBoundsOptions? options}); + bool fitBounds( + LatLngBounds bounds, { + FitBoundsOptions options, + }); + + /// Calculates the appropriate center and zoom level for the map to perfectly + /// fit [bounds], with additional configurable [options] + /// + /// Does not move/zoom the map: see [fitBounds]. + CenterZoom centerZoomFitBounds( + LatLngBounds bounds, { + FitBoundsOptions options, + }); /// Current FlutterMapState. FlutterMapState get mapState; diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/map_controller_impl.dart index aeb076542..053d407b9 100644 --- a/lib/src/map/map_controller_impl.dart +++ b/lib/src/map/map_controller_impl.dart @@ -6,6 +6,7 @@ import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/map/flutter_map_internal_controller.dart'; import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:flutter_map/src/map/map_controller.dart'; +import 'package:flutter_map/src/misc/center_zoom.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; import 'package:flutter_map/src/misc/move_and_rotate_result.dart'; import 'package:flutter_map/src/misc/point.dart'; @@ -21,7 +22,7 @@ class MapControllerImpl implements MapController { Offset offset = Offset.zero, String? id, }) => - _stateController.move( + _internalController.move( center, zoom, offset: offset, @@ -31,7 +32,7 @@ class MapControllerImpl implements MapController { ); @override - bool rotate(double degree, {String? id}) => _stateController.rotate( + bool rotate(double degree, {String? id}) => _internalController.rotate( degree, hasGesture: false, source: MapEventSource.mapController, @@ -45,7 +46,7 @@ class MapControllerImpl implements MapController { Offset? offset, String? id, }) => - _stateController.rotateAroundPoint( + _internalController.rotateAroundPoint( degree, point: point, offset: offset, @@ -61,7 +62,7 @@ class MapControllerImpl implements MapController { double degree, { String? id, }) => - _stateController.moveAndRotate( + _internalController.moveAndRotate( center, zoom, degree, @@ -74,17 +75,23 @@ class MapControllerImpl implements MapController { @override bool fitBounds( LatLngBounds bounds, { - FitBoundsOptions? options = - const FitBoundsOptions(padding: EdgeInsets.all(12)), + FitBoundsOptions options = const FitBoundsOptions( + padding: EdgeInsets.all(12), + ), }) => - _stateController.fitBounds( - bounds, - options!, - offset: Offset.zero, - ); + _internalController.fitBounds(bounds, options, offset: Offset.zero); + + @override + CenterZoom centerZoomFitBounds( + LatLngBounds bounds, { + FitBoundsOptions options = const FitBoundsOptions( + padding: EdgeInsets.all(12), + ), + }) => + options.fit(mapState, bounds); @override - FlutterMapState get mapState => _stateController.mapState; + FlutterMapState get mapState => _internalController.mapState; final _mapEventStreamController = StreamController.broadcast(); @@ -93,10 +100,10 @@ class MapControllerImpl implements MapController { StreamSink get mapEventSink => _mapEventStreamController.sink; - late FlutterMapInternalController _stateController; + late FlutterMapInternalController _internalController; - set stateController(FlutterMapInternalController stateController) { - _stateController = stateController; + set internalController(FlutterMapInternalController internalController) { + _internalController = internalController; } @override diff --git a/lib/src/map/map_safe_area.dart b/lib/src/map/map_safe_area.dart deleted file mode 100644 index 340cf0bca..000000000 --- a/lib/src/map/map_safe_area.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'dart:math' as math; -import 'dart:ui'; - -import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:latlong2/latlong.dart'; - -class MapSafeArea { - final LatLngBounds bounds; - final double zoom; - final bool _isLatitudeBlocked; - final bool _isLongitudeBlocked; - - MapSafeArea._({ - required this.bounds, - required this.zoom, - }) : _isLatitudeBlocked = bounds.south > bounds.north, - _isLongitudeBlocked = bounds.west > bounds.east; - - factory MapSafeArea({ - required Size screenSize, - required LatLngBounds bounds, - required double zoom, - }) { - final halfScreenHeightDeg = _halfScreenHeightDegrees(screenSize, zoom); - final halfScreenWidthDeg = _halfScreenWidthDegrees(screenSize, zoom); - - final safeBounds = LatLngBounds( - LatLng( - bounds.north - halfScreenHeightDeg, - bounds.east - halfScreenWidthDeg, - ), - LatLng( - bounds.south + halfScreenHeightDeg, - bounds.west + halfScreenWidthDeg, - ), - ); - - return MapSafeArea._( - bounds: safeBounds, - zoom: zoom, - ); - } - - bool contains(LatLng point) => - _isLatitudeBlocked || _isLongitudeBlocked ? false : bounds.contains(point); - - LatLng clampWithFallback(LatLng point, LatLng fallback) => LatLng( - _isLatitudeBlocked - ? fallback.latitude - : point.latitude.clamp(bounds.south, bounds.north), - _isLongitudeBlocked - ? fallback.longitude - : point.longitude.clamp(bounds.west, bounds.east), - ); - - static double _halfScreenWidthDegrees( - Size screenSize, - double zoom, - ) { - final degreesPerPixel = 360 / math.pow(2, zoom + 8); - return (screenSize.width * degreesPerPixel) / 2; - } - - static double _halfScreenHeightDegrees( - Size screenSize, - double zoom, - ) => - (screenSize.height * 170.102258 / math.pow(2, zoom + 8)) / 2; -} diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index 0b49efeed..0f4439596 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -4,30 +4,24 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; import 'package:latlong2/latlong.dart'; -/// Allows you to provide your map's starting properties for [zoom], [rotation] -/// and [center]. Alternatively you can provide [initialBounds] instead of -/// [center]. If both [center] and [initialBounds] are provided, initialBounds -/// will take preference over [center]. -/// -/// Zoom, pan boundary and interactivity constraints can be specified here too. -/// -/// Callbacks for [onTap], [onSecondaryTap], [onLongPress] and -/// [onPositionChanged] can be registered here. -/// -/// Through [crs] the Coordinate Reference System can be -/// defined, it defaults to [Epsg3857]. -/// -/// Checks if a coordinate is outside of the map's -/// defined boundaries. -/// -/// If you download offline tiles dynamically, you can set [adaptiveBoundaries] -/// (make sure to also set an external [controller]), which will enforce -/// panning/zooming to ensure there is never a need to display tiles outside the -/// boundaries set by [swPanBoundary] and [nePanBoundary]. class MapOptions { + /// The Coordinate Reference System, defaults to [Epsg3857]. final Crs crs; - final double zoom; - final double rotation; + + /// The center when the map is first loaded. If [initialBounds] is defined + /// this has no effect. + final LatLng initialCenter; + + /// The zoom when the map is first loaded. If [initialBounds] is defined this + /// has no effect. + final double initialZoom; + + /// The rotation when the map is first loaded. + final double initialRotation; + + /// The visible bounds when the map is first loaded. Takes precedence over + /// [initialCenter]/[initialZoom]. + final LatLngBounds? initialBounds; /// Prints multi finger gesture winner Helps to fine adjust /// [rotationThreshold] and [pinchZoomThreshold] and [pinchMoveThreshold] @@ -102,10 +96,11 @@ class MapOptions { final PositionCallback? onPositionChanged; final MapEventCallback? onMapEvent; final bool slideOnBoundaries; - final MapBoundary? boundary; - final LatLng center; - final LatLngBounds? initialBounds; - final FitBoundsOptions boundsOptions; + + /// Define limits for viewing the map. + final MapBoundary boundary; + + final FitBoundsOptions initialBoundsOptions; /// OnMapReady is called after the map runs it's initState. /// At that point the map has assigned its state to the controller @@ -115,11 +110,6 @@ class MapOptions { /// In initState to controll the map before the next frame final void Function()? onMapReady; - /// Restrict outer edges of map to LatLng Bounds, to prevent gray areas when - /// panning or zooming. LatLngBounds(LatLng(-90, -180.0), LatLng(90.0, 180.0)) - /// would represent the full extent of the map, so no gray area outside of it. - final LatLngBounds? maxBounds; - /// Flag to enable the built in keep alive functionality /// /// If the map is within a complex layout, such as a [ListView] or [PageView], @@ -130,11 +120,18 @@ class MapOptions { const MapOptions({ this.crs = const Epsg3857(), - this.center = const LatLng(50.5, 30.51), - this.initialBounds, - this.boundsOptions = const FitBoundsOptions(), - this.zoom = 13.0, - this.rotation = 0.0, + @Deprecated('Use initialCenter instead') LatLng? center, + LatLng initialCenter = const LatLng(50.5, 30.51), + @Deprecated('Use initialZoom instead') double? zoom, + double initialZoom = 13.0, + @Deprecated('Use initialRotation instead') double? rotation, + double initialRotation = 0.0, + @Deprecated('Use initialBounds instead') LatLngBounds? bounds, + LatLngBounds? initialBounds, + @Deprecated('Use initialBoundsOptions instead') + FitBoundsOptions? boundsOptions, + FitBoundsOptions initialBoundsOptions = const FitBoundsOptions(), + this.boundary = const CrsBoundary(), this.debugMultiFingerGestureWinner = false, this.enableMultiFingerGestureRace = false, this.rotationThreshold = 20.0, @@ -161,10 +158,13 @@ class MapOptions { this.onMapEvent, this.onMapReady, this.slideOnBoundaries = false, - this.boundary, - this.maxBounds, this.keepAlive = false, - }) : assert(rotationThreshold >= 0.0), + }) : initialCenter = center ?? initialCenter, + initialZoom = zoom ?? initialZoom, + initialRotation = rotation ?? initialRotation, + initialBounds = bounds ?? initialBounds, + initialBoundsOptions = boundsOptions ?? initialBoundsOptions, + assert(rotationThreshold >= 0.0), assert(pinchZoomThreshold >= 0.0), assert(pinchMoveThreshold >= 0.0); diff --git a/lib/src/misc/center_zoom.dart b/lib/src/misc/center_zoom.dart index 8bf2c8aa6..d7a7063c7 100644 --- a/lib/src/misc/center_zoom.dart +++ b/lib/src/misc/center_zoom.dart @@ -7,5 +7,21 @@ class CenterZoom { /// Zoom value final double zoom; + CenterZoom({required this.center, required this.zoom}); + + CenterZoom withCenter(LatLng center) => + CenterZoom(center: center, zoom: zoom); + + CenterZoom withZoom(double zoom) => CenterZoom(center: center, zoom: zoom); + + @override + int get hashCode => Object.hash(center, zoom); + + @override + bool operator ==(Object other) => + other is CenterZoom && other.center == center && other.zoom == zoom; + + @override + String toString() => 'CenterZoom(center: $center, zoom: $zoom)'; } diff --git a/lib/src/misc/fit_bounds_options.dart b/lib/src/misc/fit_bounds_options.dart index d0b87b0d4..c31c324c3 100644 --- a/lib/src/misc/fit_bounds_options.dart +++ b/lib/src/misc/fit_bounds_options.dart @@ -1,4 +1,11 @@ +import 'dart:math' as math; + import 'package:flutter/widgets.dart'; +import 'package:flutter_map/src/geo/latlng_bounds.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/misc/center_zoom.dart'; +import 'package:flutter_map/src/misc/point.dart'; +import 'package:flutter_map/src/misc/private/bounds.dart'; class FitBoundsOptions { final EdgeInsets padding; @@ -16,4 +23,78 @@ class FitBoundsOptions { this.inside = false, this.forceIntegerZoomLevel = false, }); + + CenterZoom fit( + FlutterMapState mapState, + LatLngBounds bounds, + ) { + final paddingTL = CustomPoint(padding.left, padding.top); + final paddingBR = CustomPoint(padding.right, padding.bottom); + + final paddingTotalXY = paddingTL + paddingBR; + + var zoom = getBoundsZoom(mapState, bounds, paddingTotalXY); + zoom = math.min(maxZoom, zoom); + + final paddingOffset = (paddingBR - paddingTL) / 2; + final swPoint = mapState.project(bounds.southWest, zoom); + final nePoint = mapState.project(bounds.northEast, zoom); + + final CustomPoint projectedCenter; + if (mapState.rotation != 0.0) { + final swPointRotated = swPoint.rotate(-mapState.rotationRad); + final nePointRotated = nePoint.rotate(-mapState.rotationRad); + final centerRotated = + (swPointRotated + nePointRotated) / 2 + paddingOffset; + + projectedCenter = centerRotated.rotate(mapState.rotationRad); + } else { + projectedCenter = (swPoint + nePoint) / 2 + paddingOffset; + } + + final center = mapState.unproject(projectedCenter, zoom); + return CenterZoom( + center: center, + zoom: zoom, + ); + } + + double getBoundsZoom( + FlutterMapState mapState, + LatLngBounds bounds, + CustomPoint pixelPadding, + ) { + final min = mapState.minZoom ?? 0.0; + final max = mapState.maxZoom ?? double.infinity; + final nw = bounds.northWest; + final se = bounds.southEast; + var size = mapState.nonRotatedSize - pixelPadding; + // Prevent negative size which results in NaN zoom value later on in the calculation + size = CustomPoint(math.max(0, size.x), math.max(0, size.y)); + var boundsSize = Bounds( + mapState.project(se, mapState.zoom), + mapState.project(nw, mapState.zoom), + ).size; + if (mapState.rotation != 0.0) { + final cosAngle = math.cos(mapState.rotationRad).abs(); + final sinAngle = math.sin(mapState.rotationRad).abs(); + boundsSize = CustomPoint( + (boundsSize.x * cosAngle) + (boundsSize.y * sinAngle), + (boundsSize.y * cosAngle) + (boundsSize.x * sinAngle), + ); + } + + final scaleX = size.x / boundsSize.x; + final scaleY = size.y / boundsSize.y; + final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); + + var boundsZoom = mapState.getScaleZoom(scale, mapState.zoom); + + if (forceIntegerZoomLevel) { + boundsZoom = + inside ? boundsZoom.ceilToDouble() : boundsZoom.floorToDouble(); + } + + return math.max(min, math.min(max, boundsZoom)); + } } diff --git a/lib/src/misc/map_boundary.dart b/lib/src/misc/map_boundary.dart index da6fcd961..58e62d9ea 100644 --- a/lib/src/misc/map_boundary.dart +++ b/lib/src/misc/map_boundary.dart @@ -1,64 +1,142 @@ -import 'dart:ui'; +import 'dart:math' as math; import 'package:flutter_map/plugin_api.dart'; -import 'package:flutter_map/src/map/map_safe_area.dart'; import 'package:latlong2/latlong.dart'; -sealed class MapBoundary { +abstract class MapBoundary { const MapBoundary(); + + CenterZoom? clampCenterZoom({ + required Crs crs, + required CustomPoint visibleSize, + required CenterZoom centerZoom, + }); +} + +class CrsBoundary extends MapBoundary { + const CrsBoundary(); + + @override + CenterZoom clampCenterZoom({ + required Crs crs, + required CustomPoint visibleSize, + required CenterZoom centerZoom, + }) => + centerZoom; } -class FixedBoundary extends MapBoundary { +class VisibleCenterBoundary extends MapBoundary { final LatLngBounds latLngBounds; - const FixedBoundary({ + /// Defines bounds for the center of the visible portion of the map. + const VisibleCenterBoundary({ required this.latLngBounds, }); - bool contains(LatLng latLng) => latLngBounds.contains(latLng); - - LatLng clamp(LatLng latLng) => LatLng( - latLng.latitude.clamp(latLngBounds.south, latLngBounds.north), - latLng.longitude.clamp(latLngBounds.west, latLngBounds.east), + @override + CenterZoom clampCenterZoom({ + required Crs crs, + required CustomPoint visibleSize, + required CenterZoom centerZoom, + }) => + centerZoom.withCenter( + LatLng( + centerZoom.center.latitude.clamp( + latLngBounds.south, + latLngBounds.north, + ), + centerZoom.center.longitude.clamp( + latLngBounds.west, + latLngBounds.east, + ), + ), ); } -class AdaptiveBoundary extends MapBoundary { - final Size screenSize; +class VisibleEdgeBoundary extends MapBoundary { final LatLngBounds latLngBounds; - MapSafeArea? _mapSafeAreaCache; - /// Tiles outside of these bounds will never be displayed. - AdaptiveBoundary({ - required this.screenSize, + /// Defines bounds for the visible edges of the map. Only points within these + /// bounds will be displayed. + VisibleEdgeBoundary({ required this.latLngBounds, }); - bool contains(LatLng latLng, double zoom) => - _mapSafeArea(zoom).contains(latLng); + @override + CenterZoom? clampCenterZoom({ + required Crs crs, + required CustomPoint visibleSize, + required CenterZoom centerZoom, + }) { + LatLng? newCenter; + + final testZoom = centerZoom.zoom; + final testCenter = centerZoom.center; - LatLng clampWithFallback(LatLng latLng, LatLng fallback, double zoom) => - _mapSafeArea(zoom).clampWithFallback(latLng, fallback); + final swPixel = crs.latLngToPoint(latLngBounds.southWest, testZoom); + final nePixel = crs.latLngToPoint(latLngBounds.northEast, testZoom); - MapSafeArea _mapSafeArea(double zoom) { - if (_mapSafeAreaCache?.zoom != zoom) { - return MapSafeArea( - screenSize: screenSize, - bounds: latLngBounds, - zoom: zoom, - ); + final centerPix = crs.latLngToPoint(testCenter, testZoom); + + final halfSizeX = visibleSize.x / 2; + final halfSizeY = visibleSize.y / 2; + + // Try and find the edge value that the center could use to stay within + // the maxBounds. This should be ok for panning. If we zoom, it is possible + // there is no solution to keep all corners within the bounds. If the edges + // are still outside the bounds, don't return anything. + final leftOkCenter = math.min(swPixel.x, nePixel.x) + halfSizeX; + final rightOkCenter = math.max(swPixel.x, nePixel.x) - halfSizeX; + final topOkCenter = math.min(swPixel.y, nePixel.y) + halfSizeY; + final botOkCenter = math.max(swPixel.y, nePixel.y) - halfSizeY; + + double? newCenterX; + double? newCenterY; + + var wasAdjusted = false; + + if (centerPix.x < leftOkCenter) { + wasAdjusted = true; + newCenterX = leftOkCenter; + } else if (centerPix.x > rightOkCenter) { + wasAdjusted = true; + newCenterX = rightOkCenter; + } + + if (centerPix.y < topOkCenter) { + wasAdjusted = true; + newCenterY = topOkCenter; + } else if (centerPix.y > botOkCenter) { + wasAdjusted = true; + newCenterY = botOkCenter; + } + + if (!wasAdjusted) { + return centerZoom; + } + + final newCx = newCenterX ?? centerPix.x; + final newCy = newCenterY ?? centerPix.y; + + // Have a final check, see if the adjusted center is within maxBounds. + // If not, give up. + if (newCx < leftOkCenter || + newCx > rightOkCenter || + newCy < topOkCenter || + newCy > botOkCenter) { + return null; + } else { + newCenter = crs.pointToLatLng(CustomPoint(newCx, newCy), testZoom); } - return _mapSafeAreaCache!; + return centerZoom.withCenter(newCenter); } @override bool operator ==(Object other) { - return other is AdaptiveBoundary && - other.screenSize == screenSize && - other.latLngBounds == latLngBounds; + return other is VisibleEdgeBoundary && other.latLngBounds == latLngBounds; } @override - int get hashCode => Object.hash(screenSize, latLngBounds); + int get hashCode => latLngBounds.hashCode; } diff --git a/test/flutter_map_controller_test.dart b/test/flutter_map_controller_test.dart index 2e2683971..478f02b9f 100644 --- a/test/flutter_map_controller_test.dart +++ b/test/flutter_map_controller_test.dart @@ -34,7 +34,7 @@ void main() { expect(mapState.center, equals(expectedCenter)); expect(mapState.zoom, equals(expectedZoom)); - controller.fitBounds(bounds, options: fitOptions); + controller.fitBounds(bounds); await tester.pump(); mapState = controller.mapState; expect(mapState.visibleBounds, equals(expectedBounds)); @@ -151,58 +151,58 @@ void main() { controller.move(fit.center, fit.zoom); await tester.pump(); expect( - controller.bounds?.northWest.latitude, + controller.mapState.visibleBounds.northWest.latitude, moreOrLessEquals(expectedBounds.northWest.latitude), ); expect( - controller.bounds?.northWest.longitude, + controller.mapState.visibleBounds.northWest.longitude, moreOrLessEquals(expectedBounds.northWest.longitude), ); expect( - controller.bounds?.southEast.latitude, + controller.mapState.visibleBounds.southEast.latitude, moreOrLessEquals(expectedBounds.southEast.latitude), ); expect( - controller.bounds?.southEast.longitude, + controller.mapState.visibleBounds.southEast.longitude, moreOrLessEquals(expectedBounds.southEast.longitude), ); expect( - controller.center.latitude, + controller.mapState.center.latitude, moreOrLessEquals(expectedCenter.latitude), ); expect( - controller.center.longitude, + controller.mapState.center.longitude, moreOrLessEquals(expectedCenter.longitude), ); - expect(controller.zoom, moreOrLessEquals(expectedZoom)); + expect(controller.mapState.zoom, moreOrLessEquals(expectedZoom)); controller.fitBounds(bounds, options: options); await tester.pump(); expect( - controller.bounds?.northWest.latitude, + controller.mapState.visibleBounds.northWest.latitude, moreOrLessEquals(expectedBounds.northWest.latitude), ); expect( - controller.bounds?.northWest.longitude, + controller.mapState.visibleBounds.northWest.longitude, moreOrLessEquals(expectedBounds.northWest.longitude), ); expect( - controller.bounds?.southEast.latitude, + controller.mapState.visibleBounds.southEast.latitude, moreOrLessEquals(expectedBounds.southEast.latitude), ); expect( - controller.bounds?.southEast.longitude, + controller.mapState.visibleBounds.southEast.longitude, moreOrLessEquals(expectedBounds.southEast.longitude), ); expect( - controller.center.latitude, + controller.mapState.center.latitude, moreOrLessEquals(expectedCenter.latitude), ); expect( - controller.center.longitude, + controller.mapState.center.longitude, moreOrLessEquals(expectedCenter.longitude), ); - expect(controller.zoom, moreOrLessEquals(expectedZoom)); + expect(controller.mapState.zoom, moreOrLessEquals(expectedZoom)); } // Tests with no padding diff --git a/test/flutter_map_test.dart b/test/flutter_map_test.dart index 608cee7d4..ef8baac1d 100644 --- a/test/flutter_map_test.dart +++ b/test/flutter_map_test.dart @@ -37,8 +37,8 @@ void main() { final map = FlutterMap( options: const MapOptions( - center: LatLng(45.5231, -122.6765), - zoom: 13, + initialCenter: LatLng(45.5231, -122.6765), + initialZoom: 13, ), children: [ Builder( diff --git a/test/misc/map_boundary_test.dart b/test/misc/map_boundary_test.dart new file mode 100644 index 000000000..1682a557f --- /dev/null +++ b/test/misc/map_boundary_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:latlong2/latlong.dart'; + +void main() { + group('VisibleEdgeBoundary', () { + group('minimumZoomFor', () { + test('rotated', () { + final mapBoundary = VisibleEdgeBoundary( + latLngBounds: LatLngBounds( + const LatLng(-90, -180), + const LatLng(90, 180), + ), + ); + + final clamped = mapBoundary.clampCenterZoom( + crs: const Epsg3857(), + visibleSize: const CustomPoint(300, 300), + centerZoom: CenterZoom( + center: const LatLng(-90, -180), + zoom: 1, + ), + ); + + expect( + clamped, + isA() + .having((c) => c.center.latitude, 'latitude', + closeTo(-59.534, 0.001)) + .having((c) => c.center.longitude, 'longitude', + closeTo(-74.531, 0.001)) + .having((c) => c.zoom, 'zoom', 1)); + }); + }); + }); +} diff --git a/test/test_utils/test_app.dart b/test/test_utils/test_app.dart index 1aa11496f..794cc2225 100644 --- a/test/test_utils/test_app.dart +++ b/test/test_utils/test_app.dart @@ -32,8 +32,8 @@ class TestApp extends StatelessWidget { child: FlutterMap( mapController: controller, options: const MapOptions( - center: LatLng(45.5231, -122.6765), - zoom: 13, + initialCenter: LatLng(45.5231, -122.6765), + initialZoom: 13, ), children: [ TileLayer( From 8ac52f97ab88f5133b034a84bb6f5bec715eaead Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Wed, 14 Jun 2023 11:43:43 +0200 Subject: [PATCH 15/46] Create FrameConstraint and FrameFit abstraction FrameConstraint unites the various methods of setting a maximum bounds for the map frame. Previously MapOptions had three different concepts for setting a max bounds: adaptive bounds, maxBounds and se/nw pan boundaries. Adaptive bounds and maxBounds are now FrameConstraint.contain whilst sw/ne pan boundaries is replaced by FrameConstraint.containCenter. FrameFit is a replacement for FitBoundsOptions, combinining the options with the bounds. This means bounds/boundsOptions now become initialFrameFit (since bounds/boundsOptions were actually initial bounds and the options for those initial bounds). Additionally this sets up an abstraction for different map fits since coordinate fit will be added next. --- example/lib/main.dart | 1 - .../lib/pages/animated_map_controller.dart | 15 +- example/lib/pages/home.dart | 4 +- example/lib/pages/map_controller.dart | 8 +- example/lib/pages/offline_map.dart | 4 +- example/lib/pages/sliding_map.dart | 47 ++- .../lib/pages/zoombuttons_plugin_option.dart | 27 +- lib/flutter_map.dart | 3 +- .../map/flutter_map_internal_controller.dart | 54 ++-- lib/src/map/flutter_map_state.dart | 61 +--- lib/src/map/flutter_map_state_container.dart | 39 ++- lib/src/map/map_controller.dart | 64 +++- lib/src/map/map_controller_impl.dart | 84 ++++- lib/src/map/options.dart | 45 +-- lib/src/misc/fit_bounds_options.dart | 81 ----- lib/src/misc/frame_constraint.dart | 100 ++++++ lib/src/misc/frame_fit.dart | 113 +++++++ lib/src/misc/map_boundary.dart | 142 --------- test/flutter_map_controller_test.dart | 294 +++++++++++------- test/misc/map_boundary_test.dart | 36 --- 20 files changed, 689 insertions(+), 533 deletions(-) create mode 100644 lib/src/misc/frame_constraint.dart create mode 100644 lib/src/misc/frame_fit.dart delete mode 100644 lib/src/misc/map_boundary.dart delete mode 100644 test/misc/map_boundary_test.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index fbc18d44c..c2ad69bd1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:flutter_map_example/pages/animated_map_controller.dart'; import 'package:flutter_map_example/pages/circle.dart'; import 'package:flutter_map_example/pages/custom_crs/custom_crs.dart'; diff --git a/example/lib/pages/animated_map_controller.dart b/example/lib/pages/animated_map_controller.dart index 3a2803cc6..f6d10b43a 100644 --- a/example/lib/pages/animated_map_controller.dart +++ b/example/lib/pages/animated_map_controller.dart @@ -162,10 +162,10 @@ class AnimatedMapControllerPageState extends State london, ]); - mapController.fitBounds( - bounds, - options: const FitBoundsOptions( - padding: EdgeInsets.symmetric(horizontal: 15), + mapController.fitFrame( + FrameFit.bounds( + bounds: bounds, + padding: const EdgeInsets.symmetric(horizontal: 15), ), ); }, @@ -179,9 +179,10 @@ class AnimatedMapControllerPageState extends State london, ]); - final centerZoom = const FitBoundsOptions() - .fit(mapController.mapState, bounds); - _animatedMapMove(centerZoom.center, centerZoom.zoom); + final constrained = FrameFit.bounds( + bounds: bounds, + ).fit(mapController.mapState); + _animatedMapMove(constrained.center, constrained.zoom); }, child: const Text('Fit Bounds animated'), ), diff --git a/example/lib/pages/home.dart b/example/lib/pages/home.dart index 9bf0e9fd2..dfa6c5694 100644 --- a/example/lib/pages/home.dart +++ b/example/lib/pages/home.dart @@ -114,8 +114,8 @@ class _HomePageState extends State { options: MapOptions( initialCenter: const LatLng(51.5, -0.09), initialZoom: 5, - boundary: VisibleCenterBoundary( - latLngBounds: LatLngBounds( + frameConstraint: FrameConstraint.contain( + bounds: LatLngBounds( const LatLng(-90, -180), const LatLng(90, 180), ), diff --git a/example/lib/pages/map_controller.dart b/example/lib/pages/map_controller.dart index 28d1e0427..48bf6456c 100644 --- a/example/lib/pages/map_controller.dart +++ b/example/lib/pages/map_controller.dart @@ -104,10 +104,10 @@ class MapControllerPageState extends State { london, ]); - _mapController.fitBounds( - bounds, - options: const FitBoundsOptions( - padding: EdgeInsets.only(left: 15, right: 15), + _mapController.fitFrame( + FitBounds( + bounds: bounds, + padding: const EdgeInsets.symmetric(horizontal: 15), ), ); }, diff --git a/example/lib/pages/offline_map.dart b/example/lib/pages/offline_map.dart index e67876fc3..5755fb78a 100644 --- a/example/lib/pages/offline_map.dart +++ b/example/lib/pages/offline_map.dart @@ -28,8 +28,8 @@ class OfflineMapPage extends StatelessWidget { initialCenter: const LatLng(56.704173, 11.543808), minZoom: 12, maxZoom: 14, - boundary: VisibleEdgeBoundary( - latLngBounds: LatLngBounds( + frameConstraint: FrameConstraint.contain( + bounds: LatLngBounds( const LatLng(56.7378, 11.6644), const LatLng(56.6877, 11.5089), ), diff --git a/example/lib/pages/sliding_map.dart b/example/lib/pages/sliding_map.dart index eb7c63984..d176a91fa 100644 --- a/example/lib/pages/sliding_map.dart +++ b/example/lib/pages/sliding_map.dart @@ -5,6 +5,8 @@ import 'package:latlong2/latlong.dart'; class SlidingMapPage extends StatelessWidget { static const String route = '/sliding_map'; + static const northEast = LatLng(56.7378, 11.6644); + static const southWest = LatLng(56.6877, 11.5089); const SlidingMapPage({Key? key}) : super(key: key); @@ -29,13 +31,9 @@ class SlidingMapPage extends StatelessWidget { minZoom: 12, maxZoom: 14, initialZoom: 13, - boundary: VisibleEdgeBoundary( - latLngBounds: LatLngBounds( - const LatLng(56.7378, 11.6644), - const LatLng(56.6877, 11.5089), - ), + frameConstraint: FrameConstraint.contain( + bounds: LatLngBounds(northEast, southWest), ), - slideOnBoundaries: true, ), children: [ TileLayer( @@ -43,6 +41,31 @@ class SlidingMapPage extends StatelessWidget { maxZoom: 14, urlTemplate: 'assets/map/anholt_osmbright/{z}/{x}/{y}.png', ), + MarkerLayer( + anchorPos: AnchorPos.align(AnchorAlign.top), + markers: [ + Marker( + point: northEast, + builder: (context) => _cornerMarker(Icons.north_east), + anchorPos: AnchorPos.align(AnchorAlign.bottomLeft), + ), + Marker( + point: LatLng(southWest.latitude, northEast.longitude), + builder: (context) => _cornerMarker(Icons.south_east), + anchorPos: AnchorPos.align(AnchorAlign.topLeft), + ), + Marker( + point: southWest, + builder: (context) => _cornerMarker(Icons.south_west), + anchorPos: AnchorPos.align(AnchorAlign.topRight), + ), + Marker( + point: LatLng(northEast.latitude, southWest.longitude), + builder: (context) => _cornerMarker(Icons.north_west), + anchorPos: AnchorPos.align(AnchorAlign.bottomRight), + ), + ], + ) ], ), ), @@ -51,4 +74,16 @@ class SlidingMapPage extends StatelessWidget { ), ); } + + Widget _cornerMarker(IconData iconData) { + return Container( + decoration: BoxDecoration( + color: Colors.red, + border: Border.all(color: Colors.black), + ), + width: 30, + height: 30, + child: Icon(iconData), + ); + } } diff --git a/example/lib/pages/zoombuttons_plugin_option.dart b/example/lib/pages/zoombuttons_plugin_option.dart index 82bcfc91c..741adbe32 100644 --- a/example/lib/pages/zoombuttons_plugin_option.dart +++ b/example/lib/pages/zoombuttons_plugin_option.dart @@ -14,8 +14,7 @@ class FlutterMapZoomButtons extends StatelessWidget { final IconData zoomInIcon; final IconData zoomOutIcon; - final FitBoundsOptions options = - const FitBoundsOptions(padding: EdgeInsets.all(12)); + static const _fitBoundsPadding = EdgeInsets.all(12); const FlutterMapZoomButtons({ super.key, @@ -48,14 +47,15 @@ class FlutterMapZoomButtons extends StatelessWidget { mini: mini, backgroundColor: zoomInColor ?? Theme.of(context).primaryColor, onPressed: () { - final bounds = map.visibleBounds; - final centerZoom = - map.centerZoomFitBounds(bounds, options: options); - var zoom = centerZoom.zoom + 1; + final paddedMapState = FrameFit.bounds( + bounds: map.visibleBounds, + padding: _fitBoundsPadding, + ).fit(map); + var zoom = paddedMapState.zoom + 1; if (zoom > maxZoom) { zoom = maxZoom; } - MapController.of(context).move(centerZoom.center, zoom); + MapController.of(context).move(paddedMapState.center, zoom); }, child: Icon(zoomInIcon, color: zoomInColorIcon ?? IconTheme.of(context).color), @@ -68,16 +68,15 @@ class FlutterMapZoomButtons extends StatelessWidget { mini: mini, backgroundColor: zoomOutColor ?? Theme.of(context).primaryColor, onPressed: () { - final bounds = map.visibleBounds; - final centerZoom = map.centerZoomFitBounds( - bounds, - options: options, - ); - var zoom = centerZoom.zoom - 1; + final paddedMapState = FrameFit.bounds( + bounds: map.visibleBounds, + padding: _fitBoundsPadding, + ).fit(map); + var zoom = paddedMapState.zoom - 1; if (zoom < minZoom) { zoom = minZoom; } - MapController.of(context).move(centerZoom.center, zoom); + MapController.of(context).move(paddedMapState.center, zoom); }, child: Icon(zoomOutIcon, color: zoomOutColorIcon ?? IconTheme.of(context).color), diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index 69865ecd0..d9a9addad 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -31,7 +31,8 @@ export 'package:flutter_map/src/map/options.dart'; export 'package:flutter_map/src/map/widget.dart'; export 'package:flutter_map/src/misc/center_zoom.dart'; export 'package:flutter_map/src/misc/fit_bounds_options.dart'; -export 'package:flutter_map/src/misc/map_boundary.dart'; +export 'package:flutter_map/src/misc/frame_constraint.dart'; +export 'package:flutter_map/src/misc/frame_fit.dart'; export 'package:flutter_map/src/misc/move_and_rotate_result.dart'; export 'package:flutter_map/src/misc/point.dart'; export 'package:flutter_map/src/misc/position.dart'; diff --git a/lib/src/map/flutter_map_internal_controller.dart b/lib/src/map/flutter_map_internal_controller.dart index 022165e79..72358ad46 100644 --- a/lib/src/map/flutter_map_internal_controller.dart +++ b/lib/src/map/flutter_map_internal_controller.dart @@ -65,25 +65,20 @@ class FlutterMapInternalController ); } - newZoom = mapState.clampZoom(newZoom); - final centerZoom = mapState.clampCenterZoom( - CenterZoom( - center: newCenter, - zoom: newZoom, - ), + FlutterMapState? newMapState = mapState.withPosition( + center: newCenter, + zoom: mapState.clampZoom(newZoom), ); - if (centerZoom == null) return false; - newCenter = centerZoom.center; - newZoom = centerZoom.zoom; - if (newCenter == mapState.center && newZoom == mapState.zoom) { + newMapState = options.frameConstraint.constrain(newMapState); + if (newMapState == null || + (newMapState.center == mapState.center && + newMapState.zoom == mapState.zoom)) { return false; } final oldMapState = mapState; - value = value.withMapState( - mapState.withPosition(zoom: newZoom, center: newCenter), - ); + value = value.withMapState(newMapState); final movementEvent = MapEventWithMove.fromSource( oldMapState: oldMapState, @@ -117,9 +112,15 @@ class FlutterMapInternalController required String? id, }) { if (newRotation != mapState.rotation) { + final newMapState = options.frameConstraint.constrain( + mapState.withRotation(newRotation), + ); + if (newMapState == null) return false; + final oldMapState = mapState; + // Apply state then emit events and callbacks - value = value.withMapState(mapState.withRotation(newRotation)); + value = value.withMapState(newMapState); _emitMapEvent( MapEventRotate( @@ -225,15 +226,15 @@ class FlutterMapInternalController // Note: All named parameters are required to prevent inconsistent default // values since this method can be called by MapController which declares // defaults. - bool fitBounds( - LatLngBounds bounds, - FitBoundsOptions options, { + bool fitFrame( + FrameFit frameFit, { required Offset offset, }) { - final target = mapState.centerZoomFitBounds(bounds, options: options); + final fitted = frameFit.fit(mapState); + return move( - target.center, - target.zoom, + fitted.center, + fitted.zoom, offset: offset, hasGesture: false, source: MapEventSource.fitBounds, @@ -254,8 +255,19 @@ class FlutterMapInternalController } void setOptions(MapOptions options) { + if (options == value.options) return; + if (options != this.options) { - value = value.withMapState(mapState.withOptions(options)); + final newMapState = mapState.withOptions(options); + + assert( + options.frameConstraint.constrain(newMapState) == newMapState, + 'FlutterMapState is no longer within the frameConstraint after an option change.', + ); + value = FlutterMapInternalState( + options: options, + mapState: newMapState, + ); } } diff --git a/lib/src/map/flutter_map_state.dart b/lib/src/map/flutter_map_state.dart index c0940cdfb..1c8cd5786 100644 --- a/lib/src/map/flutter_map_state.dart +++ b/lib/src/map/flutter_map_state.dart @@ -5,9 +5,6 @@ import 'package:flutter_map/src/geo/crs.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; import 'package:flutter_map/src/map/options.dart'; -import 'package:flutter_map/src/misc/center_zoom.dart'; -import 'package:flutter_map/src/misc/fit_bounds_options.dart'; -import 'package:flutter_map/src/misc/map_boundary.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; @@ -23,7 +20,6 @@ class FlutterMapState { final Crs crs; final double? minZoom; final double? maxZoom; - final MapBoundary boundary; final LatLng center; final double zoom; @@ -32,10 +28,8 @@ class FlutterMapState { // Original size of the map where rotation isn't calculated final CustomPoint nonRotatedSize; - // Extended size of the map where rotation is calculated - final CustomPoint size; - // Lazily calculated fields. + CustomPoint? _size; Bounds? _pixelBounds; LatLngBounds? _bounds; CustomPoint? _pixelOrigin; @@ -55,30 +49,28 @@ class FlutterMapState { : crs = options.crs, minZoom = options.minZoom, maxZoom = options.maxZoom, - boundary = options.boundary, center = options.initialCenter, zoom = options.initialZoom, rotation = options.initialRotation, - nonRotatedSize = kImpossibleSize, - size = kImpossibleSize; + nonRotatedSize = kImpossibleSize; // Create an instance of FlutterMapState. The [pixelOrigin], [bounds], and // [pixelBounds] may be set if they are known already. Otherwise if left // null they will be calculated lazily when they are used. FlutterMapState({ required this.crs, - required this.minZoom, - required this.maxZoom, - required this.boundary, required this.center, required this.zoom, required this.rotation, required this.nonRotatedSize, - required this.size, + this.minZoom, + this.maxZoom, + CustomPoint? size, Bounds? pixelBounds, LatLngBounds? bounds, CustomPoint? pixelOrigin, - }) : _pixelBounds = pixelBounds, + }) : _size = size ?? calculateRotatedSize(rotation, nonRotatedSize), + _pixelBounds = pixelBounds, _bounds = bounds, _pixelOrigin = pixelOrigin; @@ -89,12 +81,10 @@ class FlutterMapState { crs: crs, minZoom: minZoom, maxZoom: maxZoom, - boundary: boundary, center: center, zoom: zoom, rotation: rotation, nonRotatedSize: nonRotatedSize, - size: calculateRotatedSize(rotation, nonRotatedSize), ); } @@ -105,20 +95,17 @@ class FlutterMapState { crs: crs, minZoom: minZoom, maxZoom: maxZoom, - boundary: boundary, center: center, zoom: zoom, rotation: rotation, nonRotatedSize: nonRotatedSize, - size: calculateRotatedSize(rotation, nonRotatedSize), ); } FlutterMapState withOptions(MapOptions options) { if (options.crs == crs && options.minZoom == minZoom && - options.maxZoom == maxZoom && - options.boundary == boundary) { + options.maxZoom == maxZoom) { return this; } @@ -126,12 +113,11 @@ class FlutterMapState { crs: options.crs, minZoom: options.minZoom, maxZoom: options.maxZoom, - boundary: options.boundary, center: center, zoom: zoom, rotation: rotation, nonRotatedSize: nonRotatedSize, - size: calculateRotatedSize(rotation, nonRotatedSize), + size: _size, ); } @@ -143,12 +129,11 @@ class FlutterMapState { crs: crs, minZoom: minZoom, maxZoom: maxZoom, - boundary: boundary, center: center ?? this.center, zoom: zoom ?? this.zoom, rotation: rotation, nonRotatedSize: nonRotatedSize, - size: size, + size: _size, ); @Deprecated('Use visibleBounds instead.') @@ -161,6 +146,13 @@ class FlutterMapState { unproject(pixelBounds.topRight, zoom), )); + CustomPoint get size => + _size ?? + calculateRotatedSize( + rotation, + nonRotatedSize, + ); + CustomPoint get pixelOrigin => _pixelOrigin ?? (_pixelOrigin = (project(center, zoom) - size / 2.0).round()); @@ -183,14 +175,6 @@ class FlutterMapState { double get rotationRad => degToRadian(rotation); - CenterZoom centerZoomFitBounds( - LatLngBounds bounds, { - FitBoundsOptions options = const FitBoundsOptions( - padding: EdgeInsets.all(12), - ), - }) => - options.fit(this, bounds); - CustomPoint project(LatLng latlng, [double? zoom]) => crs.latLngToPoint(latlng, zoom ?? this.zoom); @@ -290,17 +274,6 @@ class FlutterMapState { maxZoom ?? double.infinity, ); - CenterZoom? clampCenterZoom( - CenterZoom centerZoom, { - MapBoundary? boundary, - }) { - return (boundary ?? this.boundary).clampCenterZoom( - crs: crs, - visibleSize: size, - centerZoom: centerZoom, - ); - } - LatLng offsetToCrs(Offset offset, [double? zoom]) { final focalStartPt = project(center, zoom ?? this.zoom); final point = diff --git a/lib/src/map/flutter_map_state_container.dart b/lib/src/map/flutter_map_state_container.dart index 2cc6fbcd2..8831a7d60 100644 --- a/lib/src/map/flutter_map_state_container.dart +++ b/lib/src/map/flutter_map_state_container.dart @@ -7,7 +7,7 @@ import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; class FlutterMapStateContainer extends State { - bool _hasFitInitialBounds = false; + bool _initialFrameFitApplied = false; late final FlutterMapInternalController _flutterMapInternalController; late MapControllerImpl _mapController; @@ -52,7 +52,7 @@ class FlutterMapStateContainer extends State { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { _updateAndEmitSizeIfConstraintsChanged(constraints); - _setInitialFitBounds(constraints); + _applyInitialFrameFit(constraints); return FlutterMapInteractiveViewer( controller: _flutterMapInternalController, @@ -84,17 +84,34 @@ class FlutterMapStateContainer extends State { ); } - void _setInitialFitBounds(BoxConstraints constraints) { - // If bounds were provided set the initial center/zoom to match those - // bounds once the parent constraints are available. - if (widget.options.initialBounds != null && - !_hasFitInitialBounds && + void _applyInitialFrameFit(BoxConstraints constraints) { + // If an initial frame fit was provided apply it to the map state once the + // the parent constraints are available. + + if (!_initialFrameFitApplied && + (widget.options.bounds != null || + widget.options.initialFrameFit != null) && _parentConstraintsAreSet(context, constraints)) { - _hasFitInitialBounds = true; + _initialFrameFitApplied = true; + + final FrameFit frameFit; + + if (widget.options.bounds != null) { + // Create the frame fit from the deprecated option. + final fitBoundsOptions = widget.options.boundsOptions; + frameFit = FrameFit.bounds( + bounds: widget.options.bounds!, + padding: fitBoundsOptions.padding, + maxZoom: fitBoundsOptions.maxZoom, + inside: fitBoundsOptions.inside, + forceIntegerZoomLevel: fitBoundsOptions.forceIntegerZoomLevel, + ); + } else { + frameFit = widget.options.initialFrameFit!; + } - _flutterMapInternalController.fitBounds( - widget.options.initialBounds!, - widget.options.initialBoundsOptions, + _flutterMapInternalController.fitFrame( + frameFit, offset: Offset.zero, ); } diff --git a/lib/src/map/map_controller.dart b/lib/src/map/map_controller.dart index ea5ed4208..3d37db696 100644 --- a/lib/src/map/map_controller.dart +++ b/lib/src/map/map_controller.dart @@ -21,7 +21,7 @@ abstract class MapController { /// instance. /// /// Factory constructor redirects to underlying implementation's constructor. - factory MapController() = MapControllerImpl; + factory MapController() => MapControllerImpl(); static MapController? maybeOf(BuildContext context) => context .dependOnInheritedWidgetOfExactType() @@ -49,7 +49,7 @@ abstract class MapController { /// through [MapEventCallback]s, such as [MapOptions.onMapEvent]), unless /// the move failed because (after adjustment when necessary): /// * [center] and [zoom] are equal to the current values - /// * [center] is out of bounds & [MapOptions.slideOnBoundaries] isn't enabled + /// * [center] [MapOptions.frameConstraint] does not allow the movement. bool move( LatLng center, double zoom, { @@ -84,8 +84,8 @@ abstract class MapController { /// pixels), where `Offset(0,0)` is the top-left of the map widget, and the /// bottom right is `Offset(mapWidth, mapHeight)`. /// * [offset]: allows rotation around a screen-based offset (in normal logical - /// pixels) from the map's [center]. For example, `Offset(100, 100)` will mean - /// the point is the 100px down & 100px right from the [center]. + /// pixels) from the map's center. For example, `Offset(100, 100)` will mean + /// the point is the 100px down & 100px right from the center. /// /// May cause glitchy movement if rotated against the map's bounds. /// @@ -125,25 +125,69 @@ abstract class MapController { /// /// For information about return value meaning and emitted events, see [move]'s /// documentation. + @Deprecated('Use fitFrame with a MapFit.bounds() instead') bool fitBounds( LatLngBounds bounds, { - FitBoundsOptions options, + FitBoundsOptions options = + const FitBoundsOptions(padding: EdgeInsets.all(12)), }); + /// Move and zoom the map to fit [frameFit]. + /// + /// For information about the return value and emitted events, see [move]'s + /// documentation. + bool fitFrame(FrameFit frameFit); + + /// Current FlutterMapState. + FlutterMapState get mapState; + + /// [Stream] of all emitted [MapEvent]s + Stream get mapEventStream; + /// Calculates the appropriate center and zoom level for the map to perfectly /// fit [bounds], with additional configurable [options] /// /// Does not move/zoom the map: see [fitBounds]. + @Deprecated( + 'Use FrameFit.bounds(bounds: bounds).fit(controller.mapState) instead.') CenterZoom centerZoomFitBounds( LatLngBounds bounds, { - FitBoundsOptions options, + FitBoundsOptions options = + const FitBoundsOptions(padding: EdgeInsets.all(12)), }); - /// Current FlutterMapState. - FlutterMapState get mapState; + /// Convert a screen point (x/y) to its corresponding map coordinate (lat/lng), + /// based on the map's current properties + @Deprecated('Use controller.mapState.pointToLatLng() instead.') + LatLng pointToLatLng(CustomPoint screenPoint); - /// [Stream] of all emitted [MapEvent]s - Stream get mapEventStream; + /// Convert a map coordinate (lat/lng) to its corresponding screen point (x/y), + /// based on the map's current screen positioning + @Deprecated('Use controller.mapState.latLngToScreenPoint() instead.') + CustomPoint latLngToScreenPoint(LatLng mapCoordinate); + + @Deprecated('Use controller.mapState.rotatePoint() instead.') + CustomPoint rotatePoint( + CustomPoint mapCenter, + CustomPoint point, { + bool counterRotation = true, + }); + + /// Current center coordinates + @Deprecated('Use controller.mapState.center instead.') + LatLng get center; + + /// Current outer points/boundaries coordinates + @Deprecated('Use controller.mapState.visibleBounds instead.') + LatLngBounds? get bounds; + + /// Current zoom level + @Deprecated('Use controller.mapState.zoom instead.') + double get zoom; + + /// Current rotation in degrees, where 0° is North + @Deprecated('Use controller.mapState.rotation instead.') + double get rotation; /// Dispose of this controller. void dispose(); diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/map_controller_impl.dart index 053d407b9..11a81aaae 100644 --- a/lib/src/map/map_controller_impl.dart +++ b/lib/src/map/map_controller_impl.dart @@ -8,6 +8,7 @@ import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:flutter_map/src/map/map_controller.dart'; import 'package:flutter_map/src/misc/center_zoom.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; +import 'package:flutter_map/src/misc/frame_fit.dart'; import 'package:flutter_map/src/misc/move_and_rotate_result.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:latlong2/latlong.dart'; @@ -72,23 +73,33 @@ class MapControllerImpl implements MapController { id: id, ); + /// Move and zoom the map to perfectly fit [bounds], with additional + /// configurable [options] + /// + /// For information about return value meaning and emitted events, see [move]'s + /// documentation. @override + @Deprecated('Use fitFrame with a MapFit.bounds() instead') bool fitBounds( LatLngBounds bounds, { - FitBoundsOptions options = const FitBoundsOptions( - padding: EdgeInsets.all(12), - ), + FitBoundsOptions options = + const FitBoundsOptions(padding: EdgeInsets.all(12)), }) => - _internalController.fitBounds(bounds, options, offset: Offset.zero); + fitFrame( + FrameFit.bounds( + bounds: bounds, + padding: options.padding, + maxZoom: options.maxZoom, + inside: options.inside, + forceIntegerZoomLevel: options.forceIntegerZoomLevel, + ), + ); @override - CenterZoom centerZoomFitBounds( - LatLngBounds bounds, { - FitBoundsOptions options = const FitBoundsOptions( - padding: EdgeInsets.all(12), - ), - }) => - options.fit(mapState, bounds); + bool fitFrame(FrameFit frameFit) => _internalController.fitFrame( + frameFit, + offset: Offset.zero, + ); @override FlutterMapState get mapState => _internalController.mapState; @@ -110,4 +121,55 @@ class MapControllerImpl implements MapController { void dispose() { _mapEventStreamController.close(); } + + @override + LatLngBounds? get bounds => mapState.visibleBounds; + + @override + LatLng get center => mapState.center; + + @override + CenterZoom centerZoomFitBounds( + LatLngBounds bounds, { + FitBoundsOptions options = + const FitBoundsOptions(padding: EdgeInsets.all(12)), + }) { + final fittedState = FrameFit.bounds( + bounds: bounds, + padding: options.padding, + maxZoom: options.maxZoom, + inside: options.inside, + forceIntegerZoomLevel: options.forceIntegerZoomLevel, + ).fit(mapState); + return CenterZoom( + center: fittedState.center, + zoom: fittedState.zoom, + ); + } + + @override + CustomPoint latLngToScreenPoint(LatLng mapCoordinate) => + mapState.latLngToScreenPoint(mapCoordinate); + + @override + LatLng pointToLatLng(CustomPoint screenPoint) => + mapState.pointToLatLng(screenPoint); + + @override + CustomPoint rotatePoint( + CustomPoint mapCenter, + CustomPoint point, { + bool counterRotation = true, + }) => + mapState.rotatePoint( + mapCenter.toDoublePoint(), + point.toDoublePoint(), + counterRotation: counterRotation, + ); + + @override + double get rotation => mapState.rotation; + + @override + double get zoom => mapState.zoom; } diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index 0f4439596..12c629c03 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -1,27 +1,39 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/geo/crs.dart'; +import 'package:flutter_map/src/geo/latlng_bounds.dart'; +import 'package:flutter_map/src/gestures/interactive_flag.dart'; +import 'package:flutter_map/src/gestures/map_events.dart'; +import 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; +import 'package:flutter_map/src/misc/fit_bounds_options.dart'; +import 'package:flutter_map/src/misc/frame_constraint.dart'; +import 'package:flutter_map/src/misc/frame_fit.dart'; +import 'package:flutter_map/src/misc/position.dart'; +import 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; import 'package:latlong2/latlong.dart'; class MapOptions { /// The Coordinate Reference System, defaults to [Epsg3857]. final Crs crs; - /// The center when the map is first loaded. If [initialBounds] is defined + /// The center when the map is first loaded. If [initialFrameFit] is defined /// this has no effect. final LatLng initialCenter; - /// The zoom when the map is first loaded. If [initialBounds] is defined this - /// has no effect. + /// The zoom when the map is first loaded. If [initialFrameFit] is defined + /// this has no effect. final double initialZoom; /// The rotation when the map is first loaded. final double initialRotation; - /// The visible bounds when the map is first loaded. Takes precedence over - /// [initialCenter]/[initialZoom]. - final LatLngBounds? initialBounds; + /// Defines the visible bounds when the map is first loaded. Takes precedence + /// over [initialCenter]/[initialZoom]. + final FrameFit? initialFrameFit; + + final LatLngBounds? bounds; + final FitBoundsOptions boundsOptions; /// Prints multi finger gesture winner Helps to fine adjust /// [rotationThreshold] and [pinchZoomThreshold] and [pinchMoveThreshold] @@ -95,12 +107,9 @@ class MapOptions { final PointerHoverCallback? onPointerHover; final PositionCallback? onPositionChanged; final MapEventCallback? onMapEvent; - final bool slideOnBoundaries; /// Define limits for viewing the map. - final MapBoundary boundary; - - final FitBoundsOptions initialBoundsOptions; + final FrameConstraint frameConstraint; /// OnMapReady is called after the map runs it's initState. /// At that point the map has assigned its state to the controller @@ -126,12 +135,11 @@ class MapOptions { double initialZoom = 13.0, @Deprecated('Use initialRotation instead') double? rotation, double initialRotation = 0.0, - @Deprecated('Use initialBounds instead') LatLngBounds? bounds, - LatLngBounds? initialBounds, - @Deprecated('Use initialBoundsOptions instead') - FitBoundsOptions? boundsOptions, - FitBoundsOptions initialBoundsOptions = const FitBoundsOptions(), - this.boundary = const CrsBoundary(), + @Deprecated('Use initialFrameFit instead') this.bounds, + @Deprecated('Use initialFrameFit instead') + this.boundsOptions = const FitBoundsOptions(), + this.initialFrameFit, + this.frameConstraint = const FrameConstraint.unconstrained(), this.debugMultiFingerGestureWinner = false, this.enableMultiFingerGestureRace = false, this.rotationThreshold = 20.0, @@ -157,13 +165,10 @@ class MapOptions { this.onPositionChanged, this.onMapEvent, this.onMapReady, - this.slideOnBoundaries = false, this.keepAlive = false, }) : initialCenter = center ?? initialCenter, initialZoom = zoom ?? initialZoom, initialRotation = rotation ?? initialRotation, - initialBounds = bounds ?? initialBounds, - initialBoundsOptions = boundsOptions ?? initialBoundsOptions, assert(rotationThreshold >= 0.0), assert(pinchZoomThreshold >= 0.0), assert(pinchMoveThreshold >= 0.0); diff --git a/lib/src/misc/fit_bounds_options.dart b/lib/src/misc/fit_bounds_options.dart index c31c324c3..d0b87b0d4 100644 --- a/lib/src/misc/fit_bounds_options.dart +++ b/lib/src/misc/fit_bounds_options.dart @@ -1,11 +1,4 @@ -import 'dart:math' as math; - import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/map/flutter_map_state.dart'; -import 'package:flutter_map/src/misc/center_zoom.dart'; -import 'package:flutter_map/src/misc/point.dart'; -import 'package:flutter_map/src/misc/private/bounds.dart'; class FitBoundsOptions { final EdgeInsets padding; @@ -23,78 +16,4 @@ class FitBoundsOptions { this.inside = false, this.forceIntegerZoomLevel = false, }); - - CenterZoom fit( - FlutterMapState mapState, - LatLngBounds bounds, - ) { - final paddingTL = CustomPoint(padding.left, padding.top); - final paddingBR = CustomPoint(padding.right, padding.bottom); - - final paddingTotalXY = paddingTL + paddingBR; - - var zoom = getBoundsZoom(mapState, bounds, paddingTotalXY); - zoom = math.min(maxZoom, zoom); - - final paddingOffset = (paddingBR - paddingTL) / 2; - final swPoint = mapState.project(bounds.southWest, zoom); - final nePoint = mapState.project(bounds.northEast, zoom); - - final CustomPoint projectedCenter; - if (mapState.rotation != 0.0) { - final swPointRotated = swPoint.rotate(-mapState.rotationRad); - final nePointRotated = nePoint.rotate(-mapState.rotationRad); - final centerRotated = - (swPointRotated + nePointRotated) / 2 + paddingOffset; - - projectedCenter = centerRotated.rotate(mapState.rotationRad); - } else { - projectedCenter = (swPoint + nePoint) / 2 + paddingOffset; - } - - final center = mapState.unproject(projectedCenter, zoom); - return CenterZoom( - center: center, - zoom: zoom, - ); - } - - double getBoundsZoom( - FlutterMapState mapState, - LatLngBounds bounds, - CustomPoint pixelPadding, - ) { - final min = mapState.minZoom ?? 0.0; - final max = mapState.maxZoom ?? double.infinity; - final nw = bounds.northWest; - final se = bounds.southEast; - var size = mapState.nonRotatedSize - pixelPadding; - // Prevent negative size which results in NaN zoom value later on in the calculation - size = CustomPoint(math.max(0, size.x), math.max(0, size.y)); - var boundsSize = Bounds( - mapState.project(se, mapState.zoom), - mapState.project(nw, mapState.zoom), - ).size; - if (mapState.rotation != 0.0) { - final cosAngle = math.cos(mapState.rotationRad).abs(); - final sinAngle = math.sin(mapState.rotationRad).abs(); - boundsSize = CustomPoint( - (boundsSize.x * cosAngle) + (boundsSize.y * sinAngle), - (boundsSize.y * cosAngle) + (boundsSize.x * sinAngle), - ); - } - - final scaleX = size.x / boundsSize.x; - final scaleY = size.y / boundsSize.y; - final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); - - var boundsZoom = mapState.getScaleZoom(scale, mapState.zoom); - - if (forceIntegerZoomLevel) { - boundsZoom = - inside ? boundsZoom.ceilToDouble() : boundsZoom.floorToDouble(); - } - - return math.max(min, math.min(max, boundsZoom)); - } } diff --git a/lib/src/misc/frame_constraint.dart b/lib/src/misc/frame_constraint.dart new file mode 100644 index 000000000..8c40b9102 --- /dev/null +++ b/lib/src/misc/frame_constraint.dart @@ -0,0 +1,100 @@ +import 'dart:math' as math; + +import 'package:flutter_map/plugin_api.dart'; +import 'package:latlong2/latlong.dart'; + +abstract class FrameConstraint { + const FrameConstraint(); + + const factory FrameConstraint.unconstrained() = UnconstrainedFrame._; + + const factory FrameConstraint.containCenter({ + required LatLngBounds bounds, + }) = ContainFrameCenter._; + + const factory FrameConstraint.contain({ + required LatLngBounds bounds, + }) = ContainFrame._; + + FlutterMapState? constrain(FlutterMapState mapState); +} + +class UnconstrainedFrame extends FrameConstraint { + const UnconstrainedFrame._(); + + @override + FlutterMapState constrain(FlutterMapState mapState) => mapState; +} + +class ContainFrameCenter extends FrameConstraint { + final LatLngBounds bounds; + + const ContainFrameCenter._({ + required this.bounds, + }); + + @override + FlutterMapState constrain(FlutterMapState mapState) => mapState.withPosition( + center: LatLng( + mapState.center.latitude.clamp( + bounds.south, + bounds.north, + ), + mapState.center.longitude.clamp( + bounds.west, + bounds.east, + ), + ), + ); +} + +class ContainFrame extends FrameConstraint { + final LatLngBounds bounds; + + /// Keeps the center of the frame within [bounds]. + const ContainFrame._({ + required this.bounds, + }); + + @override + FlutterMapState? constrain(FlutterMapState mapState) { + final testZoom = mapState.zoom; + final testCenter = mapState.center; + + final nePixel = mapState.project(bounds.northEast, testZoom); + final swPixel = mapState.project(bounds.southWest, testZoom); + + final halfSize = mapState.size / 2; + + // Find the limits for the map center which would keep the frame within the + // [latLngBounds]. + final leftOkCenter = math.min(swPixel.x, nePixel.x) + halfSize.x; + final rightOkCenter = math.max(swPixel.x, nePixel.x) - halfSize.x; + final topOkCenter = math.min(swPixel.y, nePixel.y) + halfSize.y; + final botOkCenter = math.max(swPixel.y, nePixel.y) - halfSize.y; + + // Stop if we are zoomed out so far that the frame cannot be translated to + // stay within [latLngBounds]. + if (leftOkCenter > rightOkCenter || topOkCenter > botOkCenter) return null; + + final centerPix = mapState.project(testCenter, testZoom); + final newCenterPix = CustomPoint( + centerPix.x.clamp(leftOkCenter, rightOkCenter), + centerPix.y.clamp(topOkCenter, botOkCenter), + ); + + if (newCenterPix == centerPix) return mapState; + + return mapState.withPosition( + center: mapState.unproject(newCenterPix, testZoom), + ); + } + + @override + bool operator ==(Object other) { + return other is ContainFrame && other.bounds == bounds; + } + + @override + int get hashCode => bounds.hashCode; +} diff --git a/lib/src/misc/frame_fit.dart b/lib/src/misc/frame_fit.dart new file mode 100644 index 000000000..69492cb88 --- /dev/null +++ b/lib/src/misc/frame_fit.dart @@ -0,0 +1,113 @@ +import 'dart:math' as math; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/src/geo/latlng_bounds.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/misc/point.dart'; +import 'package:flutter_map/src/misc/private/bounds.dart'; + +abstract class FrameFit { + const FrameFit(); + + const factory FrameFit.bounds({ + required LatLngBounds bounds, + EdgeInsets padding, + double maxZoom, + bool inside, + bool forceIntegerZoomLevel, + }) = FitBounds; + + FlutterMapState fit(FlutterMapState mapState); +} + +class FitBounds extends FrameFit { + final LatLngBounds bounds; + final EdgeInsets padding; + final double maxZoom; + final bool inside; + + /// By default calculations will return fractional zoom levels. + /// If this parameter is set to [true] fractional zoom levels will be round + /// to the next suitable integer. + final bool forceIntegerZoomLevel; + + const FitBounds({ + required this.bounds, + this.padding = EdgeInsets.zero, + this.maxZoom = 17.0, + this.inside = false, + this.forceIntegerZoomLevel = false, + }); + + @override + FlutterMapState fit(FlutterMapState mapState) { + final paddingTL = CustomPoint(padding.left, padding.top); + final paddingBR = CustomPoint(padding.right, padding.bottom); + + final paddingTotalXY = paddingTL + paddingBR; + + var newZoom = getBoundsZoom(mapState, bounds, paddingTotalXY); + newZoom = math.min(maxZoom, newZoom); + + final paddingOffset = (paddingBR - paddingTL) / 2; + final swPoint = mapState.project(bounds.southWest, newZoom); + final nePoint = mapState.project(bounds.northEast, newZoom); + + final CustomPoint projectedCenter; + if (mapState.rotation != 0.0) { + final swPointRotated = swPoint.rotate(-mapState.rotationRad); + final nePointRotated = nePoint.rotate(-mapState.rotationRad); + final centerRotated = + (swPointRotated + nePointRotated) / 2 + paddingOffset; + + projectedCenter = centerRotated.rotate(mapState.rotationRad); + } else { + projectedCenter = (swPoint + nePoint) / 2 + paddingOffset; + } + + final center = mapState.unproject(projectedCenter, newZoom); + return mapState.withPosition( + center: center, + zoom: newZoom, + ); + } + + double getBoundsZoom( + FlutterMapState mapState, + LatLngBounds bounds, + CustomPoint pixelPadding, + ) { + final min = mapState.minZoom ?? 0.0; + final max = mapState.maxZoom ?? double.infinity; + final nw = bounds.northWest; + final se = bounds.southEast; + var size = mapState.nonRotatedSize - pixelPadding; + // Prevent negative size which results in NaN zoom value later on in the calculation + size = CustomPoint(math.max(0, size.x), math.max(0, size.y)); + var boundsSize = Bounds( + mapState.project(se, mapState.zoom), + mapState.project(nw, mapState.zoom), + ).size; + if (mapState.rotation != 0.0) { + final cosAngle = math.cos(mapState.rotationRad).abs(); + final sinAngle = math.sin(mapState.rotationRad).abs(); + boundsSize = CustomPoint( + (boundsSize.x * cosAngle) + (boundsSize.y * sinAngle), + (boundsSize.y * cosAngle) + (boundsSize.x * sinAngle), + ); + } + + final scaleX = size.x / boundsSize.x; + final scaleY = size.y / boundsSize.y; + final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); + + var boundsZoom = mapState.getScaleZoom(scale, mapState.zoom); + + if (forceIntegerZoomLevel) { + boundsZoom = + inside ? boundsZoom.ceilToDouble() : boundsZoom.floorToDouble(); + } + + return math.max(min, math.min(max, boundsZoom)); + } +} diff --git a/lib/src/misc/map_boundary.dart b/lib/src/misc/map_boundary.dart deleted file mode 100644 index 58e62d9ea..000000000 --- a/lib/src/misc/map_boundary.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'dart:math' as math; - -import 'package:flutter_map/plugin_api.dart'; -import 'package:latlong2/latlong.dart'; - -abstract class MapBoundary { - const MapBoundary(); - - CenterZoom? clampCenterZoom({ - required Crs crs, - required CustomPoint visibleSize, - required CenterZoom centerZoom, - }); -} - -class CrsBoundary extends MapBoundary { - const CrsBoundary(); - - @override - CenterZoom clampCenterZoom({ - required Crs crs, - required CustomPoint visibleSize, - required CenterZoom centerZoom, - }) => - centerZoom; -} - -class VisibleCenterBoundary extends MapBoundary { - final LatLngBounds latLngBounds; - - /// Defines bounds for the center of the visible portion of the map. - const VisibleCenterBoundary({ - required this.latLngBounds, - }); - - @override - CenterZoom clampCenterZoom({ - required Crs crs, - required CustomPoint visibleSize, - required CenterZoom centerZoom, - }) => - centerZoom.withCenter( - LatLng( - centerZoom.center.latitude.clamp( - latLngBounds.south, - latLngBounds.north, - ), - centerZoom.center.longitude.clamp( - latLngBounds.west, - latLngBounds.east, - ), - ), - ); -} - -class VisibleEdgeBoundary extends MapBoundary { - final LatLngBounds latLngBounds; - - /// Defines bounds for the visible edges of the map. Only points within these - /// bounds will be displayed. - VisibleEdgeBoundary({ - required this.latLngBounds, - }); - - @override - CenterZoom? clampCenterZoom({ - required Crs crs, - required CustomPoint visibleSize, - required CenterZoom centerZoom, - }) { - LatLng? newCenter; - - final testZoom = centerZoom.zoom; - final testCenter = centerZoom.center; - - final swPixel = crs.latLngToPoint(latLngBounds.southWest, testZoom); - final nePixel = crs.latLngToPoint(latLngBounds.northEast, testZoom); - - final centerPix = crs.latLngToPoint(testCenter, testZoom); - - final halfSizeX = visibleSize.x / 2; - final halfSizeY = visibleSize.y / 2; - - // Try and find the edge value that the center could use to stay within - // the maxBounds. This should be ok for panning. If we zoom, it is possible - // there is no solution to keep all corners within the bounds. If the edges - // are still outside the bounds, don't return anything. - final leftOkCenter = math.min(swPixel.x, nePixel.x) + halfSizeX; - final rightOkCenter = math.max(swPixel.x, nePixel.x) - halfSizeX; - final topOkCenter = math.min(swPixel.y, nePixel.y) + halfSizeY; - final botOkCenter = math.max(swPixel.y, nePixel.y) - halfSizeY; - - double? newCenterX; - double? newCenterY; - - var wasAdjusted = false; - - if (centerPix.x < leftOkCenter) { - wasAdjusted = true; - newCenterX = leftOkCenter; - } else if (centerPix.x > rightOkCenter) { - wasAdjusted = true; - newCenterX = rightOkCenter; - } - - if (centerPix.y < topOkCenter) { - wasAdjusted = true; - newCenterY = topOkCenter; - } else if (centerPix.y > botOkCenter) { - wasAdjusted = true; - newCenterY = botOkCenter; - } - - if (!wasAdjusted) { - return centerZoom; - } - - final newCx = newCenterX ?? centerPix.x; - final newCy = newCenterY ?? centerPix.y; - - // Have a final check, see if the adjusted center is within maxBounds. - // If not, give up. - if (newCx < leftOkCenter || - newCx > rightOkCenter || - newCy < topOkCenter || - newCy > botOkCenter) { - return null; - } else { - newCenter = crs.pointToLatLng(CustomPoint(newCx, newCy), testZoom); - } - - return centerZoom.withCenter(newCenter); - } - - @override - bool operator ==(Object other) { - return other is VisibleEdgeBoundary && other.latLngBounds == latLngBounds; - } - - @override - int get hashCode => latLngBounds.hashCode; -} diff --git a/test/flutter_map_controller_test.dart b/test/flutter_map_controller_test.dart index 478f02b9f..4f1c2fb34 100644 --- a/test/flutter_map_controller_test.dart +++ b/test/flutter_map_controller_test.dart @@ -17,33 +17,24 @@ void main() { await tester.pumpWidget(TestApp(controller: controller)); { - const fitOptions = FitBoundsOptions(); - + final frameConstraint = FrameFit.bounds(bounds: bounds); final expectedBounds = LatLngBounds( const LatLng(51.00145915187144, -0.3079873797085076), const LatLng(52.001427481787005, 1.298485398623206), ); const expectedZoom = 7.451812751543818; - var mapState = controller.mapState; - final fit = mapState.centerZoomFitBounds(bounds, options: fitOptions); - controller.move(fit.center, fit.zoom); - await tester.pump(); - mapState = controller.mapState; - expect(mapState.visibleBounds, equals(expectedBounds)); - expect(mapState.center, equals(expectedCenter)); - expect(mapState.zoom, equals(expectedZoom)); - - controller.fitBounds(bounds); + controller.fitFrame(frameConstraint); await tester.pump(); - mapState = controller.mapState; + final mapState = controller.mapState; expect(mapState.visibleBounds, equals(expectedBounds)); expect(mapState.center, equals(expectedCenter)); expect(mapState.zoom, equals(expectedZoom)); } { - const fitOptions = FitBoundsOptions( + final frameConstraint = FrameFit.bounds( + bounds: bounds, forceIntegerZoomLevel: true, ); @@ -53,25 +44,17 @@ void main() { ); const expectedZoom = 7; - var mapState = controller.mapState; - final fit = mapState.centerZoomFitBounds(bounds, options: fitOptions); - controller.move(fit.center, fit.zoom); + controller.fitFrame(frameConstraint); await tester.pump(); - mapState = controller.mapState; - expect(mapState.visibleBounds, equals(expectedBounds)); - expect(mapState.center, equals(expectedCenter)); - expect(mapState.zoom, equals(expectedZoom)); - - controller.fitBounds(bounds, options: fitOptions); - await tester.pump(); - mapState = controller.mapState; + final mapState = controller.mapState; expect(mapState.visibleBounds, equals(expectedBounds)); expect(mapState.center, equals(expectedCenter)); expect(mapState.zoom, equals(expectedZoom)); } { - const fitOptions = FitBoundsOptions( + final frameConstraint = FrameFit.bounds( + bounds: bounds, inside: true, ); @@ -81,27 +64,18 @@ void main() { ); const expectedZoom = 8.135709286104404; - var mapState = controller.mapState; - final fit = mapState.centerZoomFitBounds(bounds, options: fitOptions); - controller.move(fit.center, fit.zoom); + controller.fitFrame(frameConstraint); await tester.pump(); - mapState = controller.mapState; - expect(mapState.visibleBounds, equals(expectedBounds)); - expect(mapState.center, equals(expectedCenter)); - expect(mapState.zoom, equals(expectedZoom)); - - controller.fitBounds(bounds, options: fitOptions); - await tester.pump(); - - mapState = controller.mapState; + final mapState = controller.mapState; expect(mapState.visibleBounds, equals(expectedBounds)); expect(mapState.center, equals(expectedCenter)); expect(mapState.zoom, equals(expectedZoom)); } { - const fitOptions = FitBoundsOptions( + final frameConstraint = FrameFit.bounds( + bounds: bounds, inside: true, forceIntegerZoomLevel: true, ); @@ -112,24 +86,15 @@ void main() { ); const expectedZoom = 9; - var mapState = controller.mapState; - final fit = mapState.centerZoomFitBounds(bounds, options: fitOptions); - controller.move(fit.center, fit.zoom); + controller.fitFrame(frameConstraint); await tester.pump(); - mapState = controller.mapState; - expect(mapState.visibleBounds, equals(expectedBounds)); - expect(mapState.center, equals(expectedCenter)); - expect(mapState.zoom, equals(expectedZoom)); - - controller.fitBounds(bounds, options: fitOptions); - await tester.pump(); - - mapState = controller.mapState; + final mapState = controller.mapState; expect(mapState.visibleBounds, equals(expectedBounds)); expect(mapState.center, equals(expectedCenter)); expect(mapState.zoom, equals(expectedZoom)); } }); + testWidgets('test fit bounds methods with rotation', (tester) async { final controller = MapController(); final bounds = LatLngBounds( @@ -141,42 +106,14 @@ void main() { Future testFitBounds({ required double rotation, - required FitBoundsOptions options, + required FrameFit frameConstraint, required LatLngBounds expectedBounds, required LatLng expectedCenter, required double expectedZoom, }) async { controller.rotate(rotation); - final fit = controller.centerZoomFitBounds(bounds, options: options); - controller.move(fit.center, fit.zoom); - await tester.pump(); - expect( - controller.mapState.visibleBounds.northWest.latitude, - moreOrLessEquals(expectedBounds.northWest.latitude), - ); - expect( - controller.mapState.visibleBounds.northWest.longitude, - moreOrLessEquals(expectedBounds.northWest.longitude), - ); - expect( - controller.mapState.visibleBounds.southEast.latitude, - moreOrLessEquals(expectedBounds.southEast.latitude), - ); - expect( - controller.mapState.visibleBounds.southEast.longitude, - moreOrLessEquals(expectedBounds.southEast.longitude), - ); - expect( - controller.mapState.center.latitude, - moreOrLessEquals(expectedCenter.latitude), - ); - expect( - controller.mapState.center.longitude, - moreOrLessEquals(expectedCenter.longitude), - ); - expect(controller.mapState.zoom, moreOrLessEquals(expectedZoom)); - controller.fitBounds(bounds, options: options); + controller.fitFrame(frameConstraint); await tester.pump(); expect( controller.mapState.visibleBounds.northWest.latitude, @@ -209,7 +146,10 @@ void main() { await testFitBounds( rotation: -360, - options: const FitBoundsOptions(padding: EdgeInsets.zero), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), expectedBounds: LatLngBounds( const LatLng(4.220875035073316, 28.95466920920177), const LatLng(-1.3562295282017047, 34.53572340816548), @@ -219,7 +159,10 @@ void main() { ); await testFitBounds( rotation: -300, - options: const FitBoundsOptions(padding: EdgeInsets.zero), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), expectedBounds: LatLngBounds( const LatLng(6.229878688707217, 26.943661553415026), const LatLng(-3.3298966942067114, 36.517625059412495), @@ -229,7 +172,10 @@ void main() { ); await testFitBounds( rotation: -240, - options: const FitBoundsOptions(padding: EdgeInsets.zero), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), expectedBounds: LatLngBounds( const LatLng(6.229878688707217, 26.943661553415026), const LatLng(-3.3298966942067114, 36.517625059412495), @@ -239,7 +185,10 @@ void main() { ); await testFitBounds( rotation: -180, - options: const FitBoundsOptions(padding: EdgeInsets.zero), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), expectedBounds: LatLngBounds( const LatLng(4.220875035073316, 28.95466920920177), const LatLng(-1.3562295282017047, 34.53572340816548), @@ -249,7 +198,10 @@ void main() { ); await testFitBounds( rotation: -120, - options: const FitBoundsOptions(padding: EdgeInsets.zero), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), expectedBounds: LatLngBounds( const LatLng(6.2298786887073065, 26.943661553414902), const LatLng(-3.329896694206635, 36.517625059412374), @@ -259,7 +211,10 @@ void main() { ); await testFitBounds( rotation: -60, - options: const FitBoundsOptions(padding: EdgeInsets.zero), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), expectedBounds: LatLngBounds( const LatLng(6.2298786887073065, 26.943661553414902), const LatLng(-3.329896694206635, 36.517625059412374), @@ -269,7 +224,10 @@ void main() { ); await testFitBounds( rotation: 0, - options: const FitBoundsOptions(padding: EdgeInsets.zero), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), expectedBounds: LatLngBounds( const LatLng(4.220875035073316, 28.95466920920177), const LatLng(-1.3562295282017047, 34.53572340816548), @@ -279,7 +237,10 @@ void main() { ); await testFitBounds( rotation: 60, - options: const FitBoundsOptions(padding: EdgeInsets.zero), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), expectedBounds: LatLngBounds( const LatLng(6.229878688707217, 26.943661553415026), const LatLng(-3.3298966942067114, 36.517625059412495), @@ -289,7 +250,10 @@ void main() { ); await testFitBounds( rotation: 120, - options: const FitBoundsOptions(padding: EdgeInsets.zero), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), expectedBounds: LatLngBounds( const LatLng(6.229878688707217, 26.943661553415026), const LatLng(-3.3298966942067114, 36.517625059412495), @@ -299,7 +263,10 @@ void main() { ); await testFitBounds( rotation: 180, - options: const FitBoundsOptions(padding: EdgeInsets.zero), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), expectedBounds: LatLngBounds( const LatLng(4.220875035073316, 28.95466920920177), const LatLng(-1.3562295282017047, 34.53572340816548), @@ -309,7 +276,10 @@ void main() { ); await testFitBounds( rotation: 240, - options: const FitBoundsOptions(padding: EdgeInsets.zero), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), expectedBounds: LatLngBounds( const LatLng(6.229878688706365, 26.94366155341602), const LatLng(-3.3298966942076276, 36.51762505941353), @@ -319,7 +289,10 @@ void main() { ); await testFitBounds( rotation: 300, - options: const FitBoundsOptions(padding: EdgeInsets.zero), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), expectedBounds: LatLngBounds( const LatLng(6.229878688707217, 26.943661553415026), const LatLng(-3.3298966942067114, 36.517625059412495), @@ -329,7 +302,10 @@ void main() { ); await testFitBounds( rotation: 360, - options: const FitBoundsOptions(padding: EdgeInsets.zero), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), expectedBounds: LatLngBounds( const LatLng(4.220875035073316, 28.95466920920177), const LatLng(-1.3562295282017047, 34.53572340816548), @@ -344,7 +320,10 @@ void main() { await testFitBounds( rotation: -360, - options: const FitBoundsOptions(padding: symmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: symmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(4.604066851713044, 28.560190151047802), const LatLng(-1.732813138431261, 34.902297195324785), @@ -354,7 +333,10 @@ void main() { ); await testFitBounds( rotation: -300, - options: const FitBoundsOptions(padding: symmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: symmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(6.862564855409817, 26.292484184306595), const LatLng(-3.997225315187129, 37.171988168394705), @@ -364,7 +346,10 @@ void main() { ); await testFitBounds( rotation: -240, - options: const FitBoundsOptions(padding: symmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: symmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(6.862564855410326, 26.292484184305955), const LatLng(-3.9972253151865824, 37.17198816839402), @@ -374,7 +359,10 @@ void main() { ); await testFitBounds( rotation: -180, - options: const FitBoundsOptions(padding: symmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: symmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(4.6040668517126235, 28.560190151048324), const LatLng(-1.7328131384316936, 34.9022971953253), @@ -384,7 +372,10 @@ void main() { ); await testFitBounds( rotation: -120, - options: const FitBoundsOptions(padding: symmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: symmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(6.862564855410096, 26.292484184306193), const LatLng(-3.997225315186811, 37.17198816839431), @@ -394,7 +385,10 @@ void main() { ); await testFitBounds( rotation: -60, - options: const FitBoundsOptions(padding: symmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: symmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(6.8625648554105165, 26.292484184305717), const LatLng(-3.9972253151863786, 37.17198816839379), @@ -404,7 +398,10 @@ void main() { ); await testFitBounds( rotation: 0, - options: const FitBoundsOptions(padding: symmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: symmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(4.604066851712751, 28.560190151048204), const LatLng(-1.732813138431579, 34.90229719532515), @@ -414,7 +411,10 @@ void main() { ); await testFitBounds( rotation: 60, - options: const FitBoundsOptions(padding: symmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: symmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(6.862564855410008, 26.292484184306353), const LatLng(-3.9972253151869386, 37.17198816839443), @@ -424,7 +424,10 @@ void main() { ); await testFitBounds( rotation: 120, - options: const FitBoundsOptions(padding: symmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: symmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(6.8625648554105165, 26.292484184305717), const LatLng(-3.9972253151863786, 37.17198816839379), @@ -434,7 +437,10 @@ void main() { ); await testFitBounds( rotation: 180, - options: const FitBoundsOptions(padding: symmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: symmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(4.6040668517126235, 28.560190151048324), const LatLng(-1.7328131384316936, 34.9022971953253), @@ -444,7 +450,10 @@ void main() { ); await testFitBounds( rotation: 240, - options: const FitBoundsOptions(padding: symmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: symmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(6.862564855410008, 26.292484184306353), const LatLng(-3.9972253151869386, 37.17198816839443), @@ -454,7 +463,10 @@ void main() { ); await testFitBounds( rotation: 300, - options: const FitBoundsOptions(padding: symmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: symmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(6.862564855411076, 26.292484184305035), const LatLng(-3.997225315185781, 37.171988168393064), @@ -464,7 +476,10 @@ void main() { ); await testFitBounds( rotation: 360, - options: const FitBoundsOptions(padding: symmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: symmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(4.604066851711988, 28.56019015104908), const LatLng(-1.7328131384323806, 34.902297195326106), @@ -479,7 +494,10 @@ void main() { await testFitBounds( rotation: -360, - options: const FitBoundsOptions(padding: asymmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: asymmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(4.634132562246874, 28.54085445883965), const LatLng(-2.1664538621122844, 35.34701811611249), @@ -489,7 +507,10 @@ void main() { ); await testFitBounds( rotation: -300, - options: const FitBoundsOptions(padding: asymmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: asymmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(7.353914452121884, 26.258676859164435), const LatLng(-4.297341450189851, 37.9342421103809), @@ -499,7 +520,10 @@ void main() { ); await testFitBounds( rotation: -240, - options: const FitBoundsOptions(padding: asymmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: asymmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(7.6081448623143, 26.00226365003461), const LatLng(-4.041607090303907, 37.677828901251075), @@ -509,7 +533,10 @@ void main() { ); await testFitBounds( rotation: -180, - options: const FitBoundsOptions(padding: asymmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: asymmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(5.041046797566381, 28.132484639403017), const LatLng(-1.7583244079256093, 34.93864829667586), @@ -519,7 +546,10 @@ void main() { ); await testFitBounds( rotation: -120, - options: const FitBoundsOptions(padding: asymmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: asymmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(7.184346279929569, 25.53217276663045), const LatLng(-4.467783700569064, 37.207738017846864), @@ -529,7 +559,10 @@ void main() { ); await testFitBounds( rotation: -60, - options: const FitBoundsOptions(padding: asymmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: asymmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(6.929875826124592, 25.788585975760196), const LatLng(-4.723372343263628, 37.46415122697666), @@ -539,7 +572,10 @@ void main() { ); await testFitBounds( rotation: 0, - options: const FitBoundsOptions(padding: asymmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: asymmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(4.63413256224709, 28.540854458839405), const LatLng(-2.166453862112043, 35.347018116112245), @@ -549,7 +585,10 @@ void main() { ); await testFitBounds( rotation: 60, - options: const FitBoundsOptions(padding: asymmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: asymmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(7.353914452122737, 26.258676859163398), const LatLng(-4.297341450188935, 37.93424211037982), @@ -559,7 +598,10 @@ void main() { ); await testFitBounds( rotation: 120, - options: const FitBoundsOptions(padding: asymmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: asymmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(7.6081448623143, 26.00226365003461), const LatLng(-4.041607090303907, 37.677828901251075), @@ -569,7 +611,10 @@ void main() { ); await testFitBounds( rotation: 180, - options: const FitBoundsOptions(padding: asymmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: asymmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(5.041046797566381, 28.132484639403017), const LatLng(-1.7583244079256093, 34.93864829667586), @@ -579,7 +624,10 @@ void main() { ); await testFitBounds( rotation: 240, - options: const FitBoundsOptions(padding: asymmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: asymmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(7.184346279929569, 25.53217276663045), const LatLng(-4.467783700569064, 37.207738017846864), @@ -589,7 +637,10 @@ void main() { ); await testFitBounds( rotation: 300, - options: const FitBoundsOptions(padding: asymmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: asymmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(6.929875826125113, 25.788585975759595), const LatLng(-4.7233723432630805, 37.46415122697602), @@ -599,7 +650,10 @@ void main() { ); await testFitBounds( rotation: 360, - options: const FitBoundsOptions(padding: asymmetricPadding), + frameConstraint: FrameFit.bounds( + bounds: bounds, + padding: asymmetricPadding, + ), expectedBounds: LatLngBounds( const LatLng(4.634132562246874, 28.54085445883965), const LatLng(-2.1664538621122844, 35.34701811611249), diff --git a/test/misc/map_boundary_test.dart b/test/misc/map_boundary_test.dart deleted file mode 100644 index 1682a557f..000000000 --- a/test/misc/map_boundary_test.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter_map/plugin_api.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:latlong2/latlong.dart'; - -void main() { - group('VisibleEdgeBoundary', () { - group('minimumZoomFor', () { - test('rotated', () { - final mapBoundary = VisibleEdgeBoundary( - latLngBounds: LatLngBounds( - const LatLng(-90, -180), - const LatLng(90, 180), - ), - ); - - final clamped = mapBoundary.clampCenterZoom( - crs: const Epsg3857(), - visibleSize: const CustomPoint(300, 300), - centerZoom: CenterZoom( - center: const LatLng(-90, -180), - zoom: 1, - ), - ); - - expect( - clamped, - isA() - .having((c) => c.center.latitude, 'latitude', - closeTo(-59.534, 0.001)) - .having((c) => c.center.longitude, 'longitude', - closeTo(-74.531, 0.001)) - .having((c) => c.zoom, 'zoom', 1)); - }); - }); - }); -} From d16bc554b3bc90411ec80831546f0f01e80f7ec9 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Wed, 14 Jun 2023 12:23:13 +0200 Subject: [PATCH 16/46] Add deprecations on MapControllerImpl --- lib/src/map/map_controller_impl.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/map_controller_impl.dart index 11a81aaae..86bc6d85d 100644 --- a/lib/src/map/map_controller_impl.dart +++ b/lib/src/map/map_controller_impl.dart @@ -123,12 +123,16 @@ class MapControllerImpl implements MapController { } @override + @Deprecated('Use controller.mapState.visibleBounds instead.') LatLngBounds? get bounds => mapState.visibleBounds; @override + @Deprecated('Use controller.mapState.center instead.') LatLng get center => mapState.center; @override + @Deprecated( + 'Use FrameFit.bounds(bounds: bounds).fit(controller.mapState) instead.') CenterZoom centerZoomFitBounds( LatLngBounds bounds, { FitBoundsOptions options = @@ -148,14 +152,17 @@ class MapControllerImpl implements MapController { } @override + @Deprecated('Use controller.mapState.latLngToScreenPoint() instead.') CustomPoint latLngToScreenPoint(LatLng mapCoordinate) => mapState.latLngToScreenPoint(mapCoordinate); @override + @Deprecated('Use controller.mapState.pointToLatLng() instead.') LatLng pointToLatLng(CustomPoint screenPoint) => mapState.pointToLatLng(screenPoint); @override + @Deprecated('Use controller.mapState.rotatePoint() instead.') CustomPoint rotatePoint( CustomPoint mapCenter, CustomPoint point, { @@ -168,8 +175,10 @@ class MapControllerImpl implements MapController { ); @override + @Deprecated('Use controller.mapState.rotation instead.') double get rotation => mapState.rotation; @override + @Deprecated('Use controller.mapState.zoom instead.') double get zoom => mapState.zoom; } From dae90c8a7f394d58996f0a5ea5f7c889f1262e3b Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Wed, 14 Jun 2023 12:31:25 +0200 Subject: [PATCH 17/46] Add fitting by coordinates This commit incorporates @jjoelson's coordinate fit implementation in to the new FrameFit abstraction. Co-authored-by: Jonathan Joelson --- lib/src/gestures/map_events.dart | 2 +- .../map/flutter_map_internal_controller.dart | 2 +- lib/src/misc/fit_bounds_options.dart | 1 + lib/src/misc/frame_fit.dart | 98 ++++++++- lib/src/misc/private/bounds.dart | 31 ++- test/flutter_map_controller_test.dart | 198 ++++++++++++++++++ test/misc/frame_constraint_test.dart | 32 +++ 7 files changed, 359 insertions(+), 5 deletions(-) create mode 100644 test/misc/frame_constraint_test.dart diff --git a/lib/src/gestures/map_events.dart b/lib/src/gestures/map_events.dart index 09a640ca2..d3ca33968 100644 --- a/lib/src/gestures/map_events.dart +++ b/lib/src/gestures/map_events.dart @@ -19,7 +19,7 @@ enum MapEventSource { flingAnimationController, doubleTapZoomAnimationController, interactiveFlagsChanged, - fitBounds, + fitFrame, custom, scrollWheel, nonRotatedSizeChange, diff --git a/lib/src/map/flutter_map_internal_controller.dart b/lib/src/map/flutter_map_internal_controller.dart index 72358ad46..be12b4461 100644 --- a/lib/src/map/flutter_map_internal_controller.dart +++ b/lib/src/map/flutter_map_internal_controller.dart @@ -237,7 +237,7 @@ class FlutterMapInternalController fitted.zoom, offset: offset, hasGesture: false, - source: MapEventSource.fitBounds, + source: MapEventSource.fitFrame, id: null, ); } diff --git a/lib/src/misc/fit_bounds_options.dart b/lib/src/misc/fit_bounds_options.dart index d0b87b0d4..27e516fbb 100644 --- a/lib/src/misc/fit_bounds_options.dart +++ b/lib/src/misc/fit_bounds_options.dart @@ -10,6 +10,7 @@ class FitBoundsOptions { /// to the next suitable integer. final bool forceIntegerZoomLevel; + @Deprecated('Use FitFrame.bounds instead.') const FitBoundsOptions({ this.padding = EdgeInsets.zero, this.maxZoom = 17.0, diff --git a/lib/src/misc/frame_fit.dart b/lib/src/misc/frame_fit.dart index 69492cb88..8c2ec175c 100644 --- a/lib/src/misc/frame_fit.dart +++ b/lib/src/misc/frame_fit.dart @@ -5,6 +5,7 @@ import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; +import 'package:latlong2/latlong.dart'; abstract class FrameFit { const FrameFit(); @@ -17,6 +18,14 @@ abstract class FrameFit { bool forceIntegerZoomLevel, }) = FitBounds; + const factory FrameFit.coordinates({ + required List coordinates, + EdgeInsets padding, + double maxZoom, + bool inside, + bool forceIntegerZoomLevel, + }) = FitCoordinates; + FlutterMapState fit(FlutterMapState mapState); } @@ -46,7 +55,7 @@ class FitBounds extends FrameFit { final paddingTotalXY = paddingTL + paddingBR; - var newZoom = getBoundsZoom(mapState, bounds, paddingTotalXY); + var newZoom = getBoundsZoom(mapState, paddingTotalXY); newZoom = math.min(maxZoom, newZoom); final paddingOffset = (paddingBR - paddingTL) / 2; @@ -74,7 +83,6 @@ class FitBounds extends FrameFit { double getBoundsZoom( FlutterMapState mapState, - LatLngBounds bounds, CustomPoint pixelPadding, ) { final min = mapState.minZoom ?? 0.0; @@ -111,3 +119,89 @@ class FitBounds extends FrameFit { return math.max(min, math.min(max, boundsZoom)); } } + +class FitCoordinates extends FrameFit { + final List coordinates; + final EdgeInsets padding; + final double maxZoom; + final bool inside; + + /// By default calculations will return fractional zoom levels. + /// If this parameter is set to [true] fractional zoom levels will be round + /// to the next suitable integer. + final bool forceIntegerZoomLevel; + + const FitCoordinates({ + required this.coordinates, + this.padding = EdgeInsets.zero, + this.maxZoom = 17.0, + this.inside = false, + this.forceIntegerZoomLevel = false, + }); + + @override + FlutterMapState fit(FlutterMapState mapState) { + final paddingTL = CustomPoint(padding.left, padding.top); + final paddingBR = CustomPoint(padding.right, padding.bottom); + + final paddingTotalXY = paddingTL + paddingBR; + + var newZoom = getCoordinatesZoom(mapState, paddingTotalXY); + newZoom = math.min(maxZoom, newZoom); + + final projectedPoints = [ + for (final coord in coordinates) mapState.project(coord, newZoom) + ]; + + final rotatedPoints = + projectedPoints.map((point) => point.rotate(-mapState.rotationRad)); + + final rotatedBounds = Bounds.containing(rotatedPoints); + + // Apply padding + final paddingOffset = (paddingBR - paddingTL) / 2; + final rotatedNewCenter = rotatedBounds.center + paddingOffset; + + // Undo the rotation + final unrotatedNewCenter = rotatedNewCenter.rotate(mapState.rotationRad); + + final newCenter = mapState.unproject(unrotatedNewCenter, newZoom); + + return mapState.withPosition( + center: newCenter, + zoom: newZoom, + ); + } + + double getCoordinatesZoom( + FlutterMapState mapState, + CustomPoint pixelPadding, + ) { + final min = mapState.minZoom ?? 0.0; + final max = mapState.maxZoom ?? double.infinity; + var size = mapState.nonRotatedSize - pixelPadding; + // Prevent negative size which results in NaN zoom value later on in the calculation + size = CustomPoint(math.max(0, size.x), math.max(0, size.y)); + + final projectedPoints = [ + for (final coord in coordinates) mapState.project(coord) + ]; + + final rotatedPoints = + projectedPoints.map((point) => point.rotate(-mapState.rotationRad)); + final rotatedBounds = Bounds.containing(rotatedPoints); + + final boundsSize = rotatedBounds.size; + + final scaleX = size.x / boundsSize.x; + final scaleY = size.y / boundsSize.y; + final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); + + var newZoom = mapState.getScaleZoom(scale, mapState.zoom); + if (forceIntegerZoomLevel) { + newZoom = inside ? newZoom.ceilToDouble() : newZoom.floorToDouble(); + } + + return math.max(min, math.min(max, newZoom)); + } +} diff --git a/lib/src/misc/private/bounds.dart b/lib/src/misc/private/bounds.dart index 191fc86a9..aea499226 100644 --- a/lib/src/misc/private/bounds.dart +++ b/lib/src/misc/private/bounds.dart @@ -8,13 +8,42 @@ class Bounds { final CustomPoint min; final CustomPoint max; + const Bounds._(this.min, this.max); + factory Bounds(CustomPoint a, CustomPoint b) { final bounds1 = Bounds._(a, b); final bounds2 = bounds1.extend(a); return bounds2.extend(b); } - const Bounds._(this.min, this.max); + static Bounds containing(Iterable> points) { + var maxX = double.negativeInfinity; + var maxY = double.negativeInfinity; + var minX = double.infinity; + var minY = double.infinity; + + for (final point in points) { + if (point.x > maxX) { + maxX = point.x; + } + if (point.x < minX) { + minX = point.x; + } + if (point.y > maxY) { + maxY = point.y; + } + if (point.y < minY) { + minY = point.y; + } + } + + final bounds = Bounds._( + CustomPoint(minX, minY), + CustomPoint(maxX, maxY), + ); + + return bounds; + } /// Creates a new [Bounds] obtained by expanding the current ones with a new /// point. diff --git a/test/flutter_map_controller_test.dart b/test/flutter_map_controller_test.dart index 4f1c2fb34..38f93a31e 100644 --- a/test/flutter_map_controller_test.dart +++ b/test/flutter_map_controller_test.dart @@ -662,4 +662,202 @@ void main() { expectedZoom: 5.368867444131886, ); }); + + testWidgets('test fit coordinates methods', (tester) async { + final controller = MapController(); + const coordinates = [ + LatLng(4.214943, 33.925781), + LatLng(3.480523, 30.844116), + LatLng(-1.362176, 29.575195), + LatLng(-0.999705, 33.925781), + ]; + + await tester.pumpWidget(TestApp(controller: controller)); + + Future testFitCoordinates({ + required double rotation, + required FitCoordinates fitCoordinates, + required LatLng expectedCenter, + required double expectedZoom, + }) async { + controller.rotate(rotation); + + controller.fitFrame(fitCoordinates); + await tester.pump(); + expect( + controller.mapState.center.latitude, + moreOrLessEquals(expectedCenter.latitude), + ); + expect( + controller.mapState.center.longitude, + moreOrLessEquals(expectedCenter.longitude), + ); + expect(controller.mapState.zoom, moreOrLessEquals(expectedZoom)); + } + + FitCoordinates fitCoordinates({ + EdgeInsets padding = EdgeInsets.zero, + }) => + FitCoordinates( + coordinates: coordinates, + padding: padding, + ); + + // Tests with no padding + + await testFitCoordinates( + rotation: 45, + fitCoordinates: fitCoordinates(), + expectedCenter: const LatLng(1.0175550985081283, 32.16110216543986), + expectedZoom: 5.323677289246632, + ); + await testFitCoordinates( + rotation: 90, + fitCoordinates: fitCoordinates(), + expectedCenter: const LatLng(1.4280748738291353, 31.75048799999998), + expectedZoom: 5.655171629288528, + ); + await testFitCoordinates( + rotation: 135, + fitCoordinates: fitCoordinates(), + expectedCenter: const LatLng(1.0175550985081538, 32.16110216543989), + expectedZoom: 5.323677289246641, + ); + await testFitCoordinates( + rotation: 180, + fitCoordinates: fitCoordinates(), + expectedCenter: const LatLng(1.4280748738291353, 31.75048799999998), + expectedZoom: 5.655171629288529, + ); + await testFitCoordinates( + rotation: 225, + fitCoordinates: fitCoordinates(), + expectedCenter: const LatLng(1.0175550985080901, 32.16110216543997), + expectedZoom: 5.323677289246641, + ); + await testFitCoordinates( + rotation: 270, + fitCoordinates: fitCoordinates(), + expectedCenter: const LatLng(1.4280748738291353, 31.75048799999998), + expectedZoom: 5.655171629288529, + ); + await testFitCoordinates( + rotation: 315, + fitCoordinates: fitCoordinates(), + expectedCenter: const LatLng(1.0175550985081538, 32.16110216543989), + expectedZoom: 5.323677289246641, + ); + await testFitCoordinates( + rotation: 360, + fitCoordinates: fitCoordinates(), + expectedCenter: const LatLng(1.4280748738291353, 31.75048799999998), + expectedZoom: 5.655171629288529, + ); + + // Tests with symmetric padding + + const equalPadding = EdgeInsets.all(12); + + await testFitCoordinates( + rotation: 45, + fitCoordinates: fitCoordinates(padding: equalPadding), + expectedCenter: const LatLng(1.0175550985081538, 32.16110216543986), + expectedZoom: 5.139252718109209, + ); + await testFitCoordinates( + rotation: 90, + fitCoordinates: fitCoordinates(padding: equalPadding), + expectedCenter: const LatLng(1.4280748738291353, 31.75048799999998), + expectedZoom: 5.470747058151099, + ); + await testFitCoordinates( + rotation: 135, + fitCoordinates: fitCoordinates(padding: equalPadding), + expectedCenter: const LatLng(1.0175550985081538, 32.161102165439935), + expectedZoom: 5.139252718109208, + ); + await testFitCoordinates( + rotation: 180, + fitCoordinates: fitCoordinates(padding: equalPadding), + expectedCenter: const LatLng(1.4280748738291353, 31.75048799999998), + expectedZoom: 5.470747058151097, + ); + await testFitCoordinates( + rotation: 225, + fitCoordinates: fitCoordinates(padding: equalPadding), + expectedCenter: const LatLng(1.0175550985081157, 32.16110216543997), + expectedZoom: 5.13925271810921, + ); + await testFitCoordinates( + rotation: 270, + fitCoordinates: fitCoordinates(padding: equalPadding), + expectedCenter: const LatLng(1.4280748738291353, 31.75048799999998), + expectedZoom: 5.470747058151099, + ); + await testFitCoordinates( + rotation: 315, + fitCoordinates: fitCoordinates(padding: equalPadding), + expectedCenter: const LatLng(1.0175550985081538, 32.16110216543986), + expectedZoom: 5.13925271810921, + ); + await testFitCoordinates( + rotation: 360, + fitCoordinates: fitCoordinates(padding: equalPadding), + expectedCenter: const LatLng(1.4280748738291353, 31.75048799999998), + expectedZoom: 5.470747058151099, + ); + + // Tests with asymmetric padding + + const asymmetricPadding = EdgeInsets.fromLTRB(12, 12, 24, 24); + + await testFitCoordinates( + rotation: 45, + fitCoordinates: fitCoordinates(padding: asymmetricPadding), + expectedCenter: const LatLng(1.0175550985081665, 32.524454855645835), + expectedZoom: 5.037373104089995, + ); + await testFitCoordinates( + rotation: 90, + fitCoordinates: fitCoordinates(padding: asymmetricPadding), + expectedCenter: const LatLng(1.63218686735705, 31.954672909718134), + expectedZoom: 5.36886744413189, + ); + await testFitCoordinates( + rotation: 135, + fitCoordinates: fitCoordinates(padding: asymmetricPadding), + expectedCenter: const LatLng(1.3808275978186646, 32.16110216543989), + expectedZoom: 5.037373104089992, + ); + await testFitCoordinates( + rotation: 180, + fitCoordinates: fitCoordinates(padding: asymmetricPadding), + expectedCenter: const LatLng(1.63218686735705, 31.546303090281786), + expectedZoom: 5.3688674441318875, + ); + await testFitCoordinates( + rotation: 225, + fitCoordinates: fitCoordinates(padding: asymmetricPadding), + expectedCenter: const LatLng(1.0175550985081283, 31.797749475233953), + expectedZoom: 5.037373104089987, + ); + await testFitCoordinates( + rotation: 270, + fitCoordinates: fitCoordinates(padding: asymmetricPadding), + expectedCenter: const LatLng(1.2239447514276816, 31.546303090281786), + expectedZoom: 5.368867444131882, + ); + await testFitCoordinates( + rotation: 315, + fitCoordinates: fitCoordinates(padding: asymmetricPadding), + expectedCenter: const LatLng(0.6542416853021571, 32.16110216543989), + expectedZoom: 5.037373104089994, + ); + await testFitCoordinates( + rotation: 360, + fitCoordinates: fitCoordinates(padding: asymmetricPadding), + expectedCenter: const LatLng(1.223944751427707, 31.954672909718177), + expectedZoom: 5.368867444131889, + ); + }); } diff --git a/test/misc/frame_constraint_test.dart b/test/misc/frame_constraint_test.dart new file mode 100644 index 000000000..755ac3da6 --- /dev/null +++ b/test/misc/frame_constraint_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:latlong2/latlong.dart'; + +void main() { + group('FrameConstraint', () { + group('contain', () { + test('rotated', () { + final mapBoundary = FrameConstraint.contain( + bounds: LatLngBounds( + const LatLng(-90, -180), + const LatLng(90, 180), + ), + ); + + final mapState = FlutterMapState( + crs: const Epsg3857(), + center: const LatLng(-90, -180), + zoom: 1, + rotation: 45, + nonRotatedSize: const CustomPoint(200, 300), + ); + + final clamped = mapBoundary.constrain(mapState)!; + + expect(clamped.zoom, 1); + expect(clamped.center.latitude, closeTo(-48.562, 0.001)); + expect(clamped.center.longitude, closeTo(-55.703, 0.001)); + }); + }); + }); +} From f4d5a04b4feb82d3a10e068736b67c103dc07b69 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Wed, 14 Jun 2023 15:40:38 +0200 Subject: [PATCH 18/46] Rename FlutterMapState to FlutterMapFrame, add InteractionOptions collection to tidy up options and change how options are propagated in preparation for changing the inherited widget to an inherited model --- .../lib/pages/animated_map_controller.dart | 10 +- example/lib/pages/interactive_test_page.dart | 4 +- example/lib/pages/latlng_to_screen_point.dart | 2 +- example/lib/pages/many_markers.dart | 4 +- example/lib/pages/map_controller.dart | 2 +- example/lib/pages/markers.dart | 4 +- example/lib/pages/point_to_latlng.dart | 2 +- .../lib/pages/scale_layer_plugin_option.dart | 3 +- .../lib/pages/zoombuttons_plugin_option.dart | 14 +- lib/plugin_api.dart | 2 +- .../flutter_map_interactive_viewer.dart | 326 +++++++++--------- lib/src/gestures/interactive_flag.dart | 1 + lib/src/gestures/map_events.dart | 82 ++--- lib/src/layer/circle_layer.dart | 4 +- lib/src/layer/marker_layer.dart | 4 +- lib/src/layer/overlay_image_layer.dart | 10 +- lib/src/layer/polygon_layer.dart | 6 +- lib/src/layer/polyline_layer.dart | 8 +- lib/src/layer/tile_layer/tile_layer.dart | 34 +- .../tile_layer/tile_range_calculator.dart | 24 +- .../layer/tile_layer/tile_update_event.dart | 8 +- ..._map_state.dart => flutter_map_frame.dart} | 46 +-- .../map/flutter_map_internal_controller.dart | 132 +++---- lib/src/map/flutter_map_internal_state.dart | 8 +- lib/src/map/flutter_map_state_container.dart | 23 +- .../flutter_map_state_inherited_widget.dart | 8 +- lib/src/map/map_controller.dart | 24 +- lib/src/map/map_controller_impl.dart | 36 +- lib/src/map/options.dart | 186 ++++++++-- lib/src/misc/frame_constraint.dart | 34 +- lib/src/misc/frame_fit.dart | 72 ++-- test/flutter_map_controller_test.dart | 52 +-- test/flutter_map_test.dart | 2 +- test/misc/frame_constraint_test.dart | 6 +- 34 files changed, 669 insertions(+), 514 deletions(-) rename lib/src/map/{flutter_map_state.dart => flutter_map_frame.dart} (90%) diff --git a/example/lib/pages/animated_map_controller.dart b/example/lib/pages/animated_map_controller.dart index f6d10b43a..a8db1dca3 100644 --- a/example/lib/pages/animated_map_controller.dart +++ b/example/lib/pages/animated_map_controller.dart @@ -35,12 +35,12 @@ class AnimatedMapControllerPageState extends State void _animatedMapMove(LatLng destLocation, double destZoom) { // Create some tweens. These serve to split up the transition from one location to another. // In our case, we want to split the transition be our current map center and the destination. - final mapState = mapController.mapState; + final mapFrame = mapController.mapFrame; final latTween = Tween( - begin: mapState.center.latitude, end: destLocation.latitude); + begin: mapFrame.center.latitude, end: destLocation.latitude); final lngTween = Tween( - begin: mapState.center.longitude, end: destLocation.longitude); - final zoomTween = Tween(begin: mapState.zoom, end: destZoom); + begin: mapFrame.center.longitude, end: destLocation.longitude); + final zoomTween = Tween(begin: mapFrame.zoom, end: destZoom); // Create a animation controller that has a duration and a TickerProvider. final controller = AnimationController( @@ -181,7 +181,7 @@ class AnimatedMapControllerPageState extends State final constrained = FrameFit.bounds( bounds: bounds, - ).fit(mapController.mapState); + ).fit(mapController.mapFrame); _animatedMapMove(constrained.center, constrained.zoom); }, child: const Text('Fit Bounds animated'), diff --git a/example/lib/pages/interactive_test_page.dart b/example/lib/pages/interactive_test_page.dart index 43d7b1266..884319653 100644 --- a/example/lib/pages/interactive_test_page.dart +++ b/example/lib/pages/interactive_test_page.dart @@ -146,7 +146,9 @@ class _InteractiveTestPageState extends State { onMapEvent: onMapEvent, initialCenter: const LatLng(51.5, -0.09), initialZoom: 11, - interactiveFlags: flags, + interactionOptions: InteractionOptions( + flags: flags, + ), ), children: [ TileLayer( diff --git a/example/lib/pages/latlng_to_screen_point.dart b/example/lib/pages/latlng_to_screen_point.dart index 149eeac77..179db1b8f 100644 --- a/example/lib/pages/latlng_to_screen_point.dart +++ b/example/lib/pages/latlng_to_screen_point.dart @@ -48,7 +48,7 @@ class _LatLngScreenPointTestPageState extends State { onMapEvent: onMapEvent, onTap: (tapPos, latLng) { final pt1 = - _mapController.mapState.latLngToScreenPoint(latLng); + _mapController.mapFrame.latLngToScreenPoint(latLng); _textPos = CustomPoint(pt1.x, pt1.y); setState(() {}); }, diff --git a/example/lib/pages/many_markers.dart b/example/lib/pages/many_markers.dart index aab04bfba..55817f747 100644 --- a/example/lib/pages/many_markers.dart +++ b/example/lib/pages/many_markers.dart @@ -75,7 +75,9 @@ class _ManyMarkersPageState extends State { options: const MapOptions( initialCenter: LatLng(50, 20), initialZoom: 5, - interactiveFlags: InteractiveFlag.all - InteractiveFlag.rotate, + interactionOptions: InteractionOptions( + flags: InteractiveFlag.all - InteractiveFlag.rotate, + ), ), children: [ TileLayer( diff --git a/example/lib/pages/map_controller.dart b/example/lib/pages/map_controller.dart index 48bf6456c..53ea4e9fc 100644 --- a/example/lib/pages/map_controller.dart +++ b/example/lib/pages/map_controller.dart @@ -116,7 +116,7 @@ class MapControllerPageState extends State { Builder(builder: (BuildContext context) { return MaterialButton( onPressed: () { - final bounds = _mapController.mapState.visibleBounds; + final bounds = _mapController.mapFrame.visibleBounds; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( diff --git a/example/lib/pages/markers.dart b/example/lib/pages/markers.dart index 746d1af5b..6c12ecaa0 100644 --- a/example/lib/pages/markers.dart +++ b/example/lib/pages/markers.dart @@ -108,7 +108,9 @@ class MarkerPageState extends State { initialZoom: 5, onTap: (_, p) => setState(() => customMarkers.add(buildPin(p))), - interactiveFlags: ~InteractiveFlag.doubleTapZoom, + interactionOptions: const InteractionOptions( + flags: ~InteractiveFlag.doubleTapZoom, + ), ), children: [ TileLayer( diff --git a/example/lib/pages/point_to_latlng.dart b/example/lib/pages/point_to_latlng.dart index 756d51c33..6ba51fb53 100644 --- a/example/lib/pages/point_to_latlng.dart +++ b/example/lib/pages/point_to_latlng.dart @@ -107,7 +107,7 @@ class PointToLatlngPage extends State { final pointX = _getPointX(context); setState(() { latLng = - mapController.mapState.pointToLatLng(CustomPoint(pointX, pointY)); + mapController.mapFrame.pointToLatLng(CustomPoint(pointX, pointY)); }); } diff --git a/example/lib/pages/scale_layer_plugin_option.dart b/example/lib/pages/scale_layer_plugin_option.dart index ba760efd8..89bbcfdec 100644 --- a/example/lib/pages/scale_layer_plugin_option.dart +++ b/example/lib/pages/scale_layer_plugin_option.dart @@ -3,7 +3,6 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_map/plugin_api.dart'; - import 'package:flutter_map_example/pages/scalebar_utils.dart' as util; class ScaleLayerPluginOption { @@ -52,7 +51,7 @@ class ScaleLayerWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final map = FlutterMapState.of(context); + final map = FlutterMapFrame.of(context); final zoom = map.zoom; final distance = scale[max(0, min(20, zoom.round() + 2))].toDouble(); final center = map.center; diff --git a/example/lib/pages/zoombuttons_plugin_option.dart b/example/lib/pages/zoombuttons_plugin_option.dart index 741adbe32..b6d33c6c7 100644 --- a/example/lib/pages/zoombuttons_plugin_option.dart +++ b/example/lib/pages/zoombuttons_plugin_option.dart @@ -33,7 +33,7 @@ class FlutterMapZoomButtons extends StatelessWidget { @override Widget build(BuildContext context) { - final map = FlutterMapState.of(context); + final map = FlutterMapFrame.of(context); return Align( alignment: alignment, child: Column( @@ -47,15 +47,15 @@ class FlutterMapZoomButtons extends StatelessWidget { mini: mini, backgroundColor: zoomInColor ?? Theme.of(context).primaryColor, onPressed: () { - final paddedMapState = FrameFit.bounds( + final paddedMapFrame = FrameFit.bounds( bounds: map.visibleBounds, padding: _fitBoundsPadding, ).fit(map); - var zoom = paddedMapState.zoom + 1; + var zoom = paddedMapFrame.zoom + 1; if (zoom > maxZoom) { zoom = maxZoom; } - MapController.of(context).move(paddedMapState.center, zoom); + MapController.of(context).move(paddedMapFrame.center, zoom); }, child: Icon(zoomInIcon, color: zoomInColorIcon ?? IconTheme.of(context).color), @@ -68,15 +68,15 @@ class FlutterMapZoomButtons extends StatelessWidget { mini: mini, backgroundColor: zoomOutColor ?? Theme.of(context).primaryColor, onPressed: () { - final paddedMapState = FrameFit.bounds( + final paddedMapFrame = FrameFit.bounds( bounds: map.visibleBounds, padding: _fitBoundsPadding, ).fit(map); - var zoom = paddedMapState.zoom - 1; + var zoom = paddedMapFrame.zoom - 1; if (zoom < minZoom) { zoom = minZoom; } - MapController.of(context).move(paddedMapState.center, zoom); + MapController.of(context).move(paddedMapFrame.center, zoom); }, child: Icon(zoomOutIcon, color: zoomOutColorIcon ?? IconTheme.of(context).color), diff --git a/lib/plugin_api.dart b/lib/plugin_api.dart index e1fbcaa87..f7608530a 100644 --- a/lib/plugin_api.dart +++ b/lib/plugin_api.dart @@ -1,7 +1,7 @@ library flutter_map.plugin_api; export 'package:flutter_map/flutter_map.dart'; -export 'package:flutter_map/src/map/flutter_map_state.dart'; +export 'package:flutter_map/src/map/flutter_map_frame.dart'; export 'package:flutter_map/src/map/map_controller.dart'; export 'package:flutter_map/src/misc/private/bounds.dart'; export 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; diff --git a/lib/src/gestures/flutter_map_interactive_viewer.dart b/lib/src/gestures/flutter_map_interactive_viewer.dart index 9f1f2b8be..ccdf2b337 100644 --- a/lib/src/gestures/flutter_map_interactive_viewer.dart +++ b/lib/src/gestures/flutter_map_interactive_viewer.dart @@ -7,22 +7,22 @@ import 'package:flutter_map/src/gestures/interactive_flag.dart'; import 'package:flutter_map/src/gestures/latlng_tween.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; +import 'package:flutter_map/src/map/flutter_map_frame.dart'; import 'package:flutter_map/src/map/flutter_map_internal_controller.dart'; -import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/map/flutter_map_internal_state.dart'; import 'package:flutter_map/src/map/options.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; import 'package:latlong2/latlong.dart'; class FlutterMapInteractiveViewer extends StatefulWidget { - final Widget Function(BuildContext context, FlutterMapState mapState) builder; - final MapOptions options; + final Widget Function(BuildContext context, FlutterMapInternalState mapState) + builder; final FlutterMapInternalController controller; const FlutterMapInteractiveViewer({ super.key, required this.builder, - required this.options, required this.controller, }); @@ -72,9 +72,9 @@ class FlutterMapInteractiveViewerState int _tapUpCounter = 0; Timer? _doubleTapHoldMaxDelay; - FlutterMapState get _mapState => widget.controller.mapState; + FlutterMapFrame get _mapFrame => widget.controller.mapFrame; - MapOptions get _options => widget.options; + MapOptions get _options => widget.controller.options; @override void initState() { @@ -100,155 +100,149 @@ class FlutterMapInteractiveViewerState @override void didChangeDependencies() { + // _createGestures uses a MediaQuery to determine gesture settings. This + // will update those gesture settings if they change. + _gestures = _createGestures( - MediaQuery.gestureSettingsOf(context), dragEnabled: InteractiveFlag.hasDrag(_options.interactiveFlags), ); super.didChangeDependencies(); } @override - void didUpdateWidget(FlutterMapInteractiveViewer oldWidget) { - super.didUpdateWidget(oldWidget); - - final oldFlags = oldWidget.options.interactiveFlags; - final flags = widget.options.interactiveFlags; + void dispose() { + widget.controller.removeListener(_onMapStateChange); + _flingController.dispose(); + _doubleTapController.dispose(); - final oldGestures = _getMultiFingerGestureFlags(options: oldWidget.options); - final gestures = _getMultiFingerGestureFlags(); + super.dispose(); + } - if (flags != oldFlags) { - _gestures = _createGestures( - MediaQuery.gestureSettingsOf(context), - dragEnabled: InteractiveFlag.hasDrag(widget.options.interactiveFlags), - ); + void updateGestures( + InteractionOptions oldOptions, + InteractionOptions newOptions, + ) { + if (newOptions.dragEnabled != oldOptions.dragEnabled) { + _gestures = _createGestures(dragEnabled: newOptions.dragEnabled); } - if (flags != oldFlags || gestures != oldGestures) { - var emitMapEventMoveEnd = false; - - if (!InteractiveFlag.hasFlingAnimation(flags)) { - _closeFlingAnimationController(MapEventSource.interactiveFlagsChanged); - } - if (!InteractiveFlag.hasDoubleTapZoom(flags)) { - _closeDoubleTapController(MapEventSource.interactiveFlagsChanged); - } + if (!newOptions.flingEnabled) { + _closeFlingAnimationController(MapEventSource.interactiveFlagsChanged); + } + if (newOptions.doubleTapZoomEnabled) { + _closeDoubleTapController(MapEventSource.interactiveFlagsChanged); + } - if (_rotationStarted && - !(InteractiveFlag.hasRotate(flags) && - MultiFingerGesture.hasRotate(gestures))) { - _rotationStarted = false; + final gestures = _getMultiFingerGestureFlags(newOptions); - if (_gestureWinner == MultiFingerGesture.rotate) { - _gestureWinner = MultiFingerGesture.none; - } + if (_rotationStarted && + !newOptions.rotateEnabled && + !MultiFingerGesture.hasRotate(gestures)) { + _rotationStarted = false; - widget.controller.rotateEnded(MapEventSource.interactiveFlagsChanged); + if (_gestureWinner == MultiFingerGesture.rotate) { + _gestureWinner = MultiFingerGesture.none; } - if (_pinchZoomStarted && - !(InteractiveFlag.hasPinchZoom(flags) && - MultiFingerGesture.hasPinchZoom(gestures))) { - _pinchZoomStarted = false; - emitMapEventMoveEnd = true; + widget.controller.rotateEnded(MapEventSource.interactiveFlagsChanged); + } - if (_gestureWinner == MultiFingerGesture.pinchZoom) { - _gestureWinner = MultiFingerGesture.none; - } - } + var emitMapEventMoveEnd = false; - if (_pinchMoveStarted && - !(InteractiveFlag.hasPinchMove(flags) && - MultiFingerGesture.hasPinchMove(gestures))) { - _pinchMoveStarted = false; - emitMapEventMoveEnd = true; + if (_pinchZoomStarted && + !newOptions.pinchZoomEnabled && + !MultiFingerGesture.hasPinchZoom(gestures)) { + _pinchZoomStarted = false; + emitMapEventMoveEnd = true; - if (_gestureWinner == MultiFingerGesture.pinchMove) { - _gestureWinner = MultiFingerGesture.none; - } + if (_gestureWinner == MultiFingerGesture.pinchZoom) { + _gestureWinner = MultiFingerGesture.none; } + } - if (_dragStarted && !InteractiveFlag.hasDrag(flags)) { - _dragStarted = false; - emitMapEventMoveEnd = true; - } + if (_pinchMoveStarted && + !newOptions.pinchMoveEnabled && + !MultiFingerGesture.hasPinchMove(gestures)) { + _pinchMoveStarted = false; + emitMapEventMoveEnd = true; - if (emitMapEventMoveEnd) { - widget.controller.moveEnded(MapEventSource.interactiveFlagsChanged); + if (_gestureWinner == MultiFingerGesture.pinchMove) { + _gestureWinner = MultiFingerGesture.none; } } - } - @override - void dispose() { - widget.controller.removeListener(_onMapStateChange); - _flingController.dispose(); - _doubleTapController.dispose(); + if (_dragStarted && !newOptions.dragEnabled) { + _dragStarted = false; + emitMapEventMoveEnd = true; + } - super.dispose(); + if (emitMapEventMoveEnd) { + widget.controller.moveEnded(MapEventSource.interactiveFlagsChanged); + } } - Map _createGestures( - DeviceGestureSettings gestureSettings, { + Map _createGestures({ required bool dragEnabled, - }) => - { - TapGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(debugOwner: this), - (TapGestureRecognizer instance) { - instance - ..onTapDown = _positionedTapController.onTapDown - ..onTapUp = _handleOnTapUp - ..onTap = _positionedTapController.onTap - ..onSecondaryTap = _positionedTapController.onSecondaryTap - ..onSecondaryTapDown = _positionedTapController.onTapDown; - }, - ), - LongPressGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => LongPressGestureRecognizer(debugOwner: this), - (LongPressGestureRecognizer instance) { - instance.onLongPress = _positionedTapController.onLongPress; + }) { + final gestureSettings = MediaQuery.gestureSettingsOf(context); + return { + TapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(debugOwner: this), + (TapGestureRecognizer instance) { + instance + ..onTapDown = _positionedTapController.onTapDown + ..onTapUp = _handleOnTapUp + ..onTap = _positionedTapController.onTap + ..onSecondaryTap = _positionedTapController.onSecondaryTap + ..onSecondaryTapDown = _positionedTapController.onTapDown; + }, + ), + LongPressGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => LongPressGestureRecognizer(debugOwner: this), + (LongPressGestureRecognizer instance) { + instance.onLongPress = _positionedTapController.onLongPress; + }, + ), + if (dragEnabled) + VerticalDragGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => VerticalDragGestureRecognizer(debugOwner: this), + (VerticalDragGestureRecognizer instance) { + instance.onUpdate = (details) { + // Absorbing vertical drags + }; + instance.gestureSettings = gestureSettings; + instance.team ??= _gestureArenaTeam; }, ), - if (dragEnabled) - VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers< - VerticalDragGestureRecognizer>( - () => VerticalDragGestureRecognizer(debugOwner: this), - (VerticalDragGestureRecognizer instance) { - instance.onUpdate = (details) { - // Absorbing vertical drags - }; - instance.gestureSettings = gestureSettings; - instance.team ??= _gestureArenaTeam; - }, - ), - if (dragEnabled) - HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers< - HorizontalDragGestureRecognizer>( - () => HorizontalDragGestureRecognizer(debugOwner: this), - (HorizontalDragGestureRecognizer instance) { - instance.onUpdate = (details) { - // Absorbing horizontal drags - }; - instance.gestureSettings = gestureSettings; - instance.team ??= _gestureArenaTeam; - }, - ), - ScaleGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => ScaleGestureRecognizer(debugOwner: this), - (ScaleGestureRecognizer instance) { - instance - ..onStart = _handleScaleStart - ..onUpdate = _handleScaleUpdate - ..onEnd = _handleScaleEnd; + if (dragEnabled) + HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers< + HorizontalDragGestureRecognizer>( + () => HorizontalDragGestureRecognizer(debugOwner: this), + (HorizontalDragGestureRecognizer instance) { + instance.onUpdate = (details) { + // Absorbing horizontal drags + }; + instance.gestureSettings = gestureSettings; instance.team ??= _gestureArenaTeam; - _gestureArenaTeam.captain = instance; }, ), - }; + ScaleGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => ScaleGestureRecognizer(debugOwner: this), + (ScaleGestureRecognizer instance) { + instance + ..onStart = _handleScaleStart + ..onUpdate = _handleScaleUpdate + ..onEnd = _handleScaleEnd; + instance.team ??= _gestureArenaTeam; + _gestureArenaTeam.captain = instance; + }, + ), + }; + } @override Widget build(BuildContext context) { @@ -270,7 +264,7 @@ class FlutterMapInteractiveViewerState : Duration.zero, child: RawGestureDetector( gestures: _gestures, - child: widget.builder(context, _mapState), + child: widget.builder(context, widget.controller.value), ), ), ); @@ -280,7 +274,7 @@ class FlutterMapInteractiveViewerState ++_pointerCounter; if (_options.onPointerDown != null) { - final latlng = _mapState.offsetToCrs(event.localPosition); + final latlng = _mapFrame.offsetToCrs(event.localPosition); _options.onPointerDown!(event, latlng); } } @@ -289,7 +283,7 @@ class FlutterMapInteractiveViewerState --_pointerCounter; if (_options.onPointerUp != null) { - final latlng = _mapState.offsetToCrs(event.localPosition); + final latlng = _mapFrame.offsetToCrs(event.localPosition); _options.onPointerUp!(event, latlng); } } @@ -298,14 +292,14 @@ class FlutterMapInteractiveViewerState --_pointerCounter; if (_options.onPointerCancel != null) { - final latlng = _mapState.offsetToCrs(event.localPosition); + final latlng = _mapFrame.offsetToCrs(event.localPosition); _options.onPointerCancel!(event, latlng); } } void _onPointerHover(PointerHoverEvent event) { if (_options.onPointerHover != null) { - final latlng = _mapState.offsetToCrs(event.localPosition); + final latlng = _mapFrame.offsetToCrs(event.localPosition); _options.onPointerHover!(event, latlng); } } @@ -323,11 +317,11 @@ class FlutterMapInteractiveViewerState pointerSignal as PointerScrollEvent; final minZoom = _options.minZoom ?? 0.0; final maxZoom = _options.maxZoom ?? double.infinity; - final newZoom = (_mapState.zoom - + final newZoom = (_mapFrame.zoom - pointerSignal.scrollDelta.dy * _options.scrollWheelVelocity) .clamp(minZoom, maxZoom); // Calculate offset of mouse cursor from viewport center - final newCenter = _mapState.focusedZoomCenter( + final newCenter = _mapFrame.focusedZoomCenter( pointerSignal.localPosition.toCustomPoint(), newZoom, ); @@ -344,16 +338,14 @@ class FlutterMapInteractiveViewerState } } - int _getMultiFingerGestureFlags({MapOptions? options}) { - options ??= _options; - - if (options.enableMultiFingerGestureRace) { + int _getMultiFingerGestureFlags(InteractionOptions interactionOptions) { + if (interactionOptions.enableMultiFingerGestureRace) { if (_gestureWinner == MultiFingerGesture.pinchZoom) { - return options.pinchZoomWinGestures; + return interactionOptions.pinchZoomWinGestures; } else if (_gestureWinner == MultiFingerGesture.rotate) { - return options.rotationWinGestures; + return interactionOptions.rotationWinGestures; } else if (_gestureWinner == MultiFingerGesture.pinchMove) { - return options.pinchMoveWinGestures; + return interactionOptions.pinchMoveWinGestures; } return MultiFingerGesture.none; @@ -394,10 +386,10 @@ class FlutterMapInteractiveViewerState _gestureWinner = MultiFingerGesture.none; - _mapZoomStart = _mapState.zoom; - _mapCenterStart = _mapState.center; + _mapZoomStart = _mapFrame.zoom; + _mapCenterStart = _mapFrame.center; _focalStartLocal = _lastFocalLocal = details.localFocalPoint; - _focalStartLatLng = _mapState.offsetToCrs(_focalStartLocal); + _focalStartLatLng = _mapFrame.offsetToCrs(_focalStartLocal); _dragStarted = false; _pinchZoomStarted = false; @@ -469,7 +461,7 @@ class FlutterMapInteractiveViewerState } if (!hasGestureRace || _gestureWinner != MultiFingerGesture.none) { - final gestures = _getMultiFingerGestureFlags(); + final gestures = _getMultiFingerGestureFlags(_options.interactionOptions); final hasPinchZoom = InteractiveFlag.hasPinchZoom(_options.interactiveFlags) && @@ -493,8 +485,8 @@ class FlutterMapInteractiveViewerState bool hasPinchZoom, bool hasPinchMove, ) { - LatLng newCenter = _mapState.center; - double newZoom = _mapState.zoom; + LatLng newCenter = _mapFrame.center; + double newZoom = _mapFrame.zoom; // Handle pinch zoom. if (hasPinchZoom && details.scale > 0.0) { @@ -546,17 +538,17 @@ class FlutterMapInteractiveViewerState ScaleUpdateDetails details, double zoomAfterPinchZoom, ) { - final oldCenterPt = _mapState.project(_mapState.center, zoomAfterPinchZoom); + final oldCenterPt = _mapFrame.project(_mapFrame.center, zoomAfterPinchZoom); final newFocalLatLong = - _mapState.offsetToCrs(_focalStartLocal, zoomAfterPinchZoom); - final newFocalPt = _mapState.project(newFocalLatLong, zoomAfterPinchZoom); - final oldFocalPt = _mapState.project(_focalStartLatLng, zoomAfterPinchZoom); + _mapFrame.offsetToCrs(_focalStartLocal, zoomAfterPinchZoom); + final newFocalPt = _mapFrame.project(newFocalLatLong, zoomAfterPinchZoom); + final oldFocalPt = _mapFrame.project(_focalStartLatLng, zoomAfterPinchZoom); final zoomDifference = oldFocalPt - newFocalPt; final moveDifference = _rotateOffset(_focalStartLocal - _lastFocalLocal); final newCenterPt = oldCenterPt + zoomDifference + moveDifference.toCustomPoint(); - return _mapState.unproject(newCenterPt, zoomAfterPinchZoom); + return _mapFrame.unproject(newCenterPt, zoomAfterPinchZoom); } void _handleScalePinchRotate( @@ -570,17 +562,17 @@ class FlutterMapInteractiveViewerState if (_rotationStarted) { final rotationDiff = currentRotation - _lastRotation; - final oldCenterPt = _mapState.project(_mapState.center); + final oldCenterPt = _mapFrame.project(_mapFrame.center); final rotationCenter = - _mapState.project(_mapState.offsetToCrs(_lastFocalLocal)); + _mapFrame.project(_mapFrame.offsetToCrs(_lastFocalLocal)); final vector = oldCenterPt - rotationCenter; final rotatedVector = vector.rotate(degToRadian(rotationDiff)); final newCenter = rotationCenter + rotatedVector; widget.controller.moveAndRotate( - _mapState.unproject(newCenter), - _mapState.zoom, - _mapState.rotation + rotationDiff, + _mapFrame.unproject(newCenter), + _mapFrame.zoom, + _mapFrame.rotation + rotationDiff, offset: Offset.zero, hasGesture: true, source: MapEventSource.onMultiFinger, @@ -646,7 +638,7 @@ class FlutterMapInteractiveViewerState final direction = details.velocity.pixelsPerSecond / magnitude; final distance = (Offset.zero & - Size(_mapState.nonRotatedSize.x, _mapState.nonRotatedSize.y)) + Size(_mapFrame.nonRotatedSize.x, _mapFrame.nonRotatedSize.y)) .shortestSide; final flingOffset = _focalStartLocal - _lastFocalLocal; @@ -676,7 +668,7 @@ class FlutterMapInteractiveViewerState widget.controller.tapped( MapEventSource.tap, position, - _mapState.offsetToCrs(relativePosition), + _mapFrame.offsetToCrs(relativePosition), ); } @@ -690,7 +682,7 @@ class FlutterMapInteractiveViewerState widget.controller.secondaryTapped( MapEventSource.secondaryTap, position, - _mapState.offsetToCrs(relativePosition), + _mapFrame.offsetToCrs(relativePosition), ); } @@ -703,7 +695,7 @@ class FlutterMapInteractiveViewerState widget.controller.longPressed( MapEventSource.longPress, position, - _mapState.offsetToCrs(position.relative!), + _mapFrame.offsetToCrs(position.relative!), ); } @@ -714,8 +706,8 @@ class FlutterMapInteractiveViewerState _closeDoubleTapController(MapEventSource.doubleTap); if (InteractiveFlag.hasDoubleTapZoom(_options.interactiveFlags)) { - final newZoom = _getZoomForScale(_mapState.zoom, 2); - final newCenter = _mapState.focusedZoomCenter( + final newZoom = _getZoomForScale(_mapFrame.zoom, 2); + final newCenter = _mapFrame.focusedZoomCenter( tapPosition.relative!.toCustomPoint(), newZoom, ); @@ -724,11 +716,11 @@ class FlutterMapInteractiveViewerState } void _startDoubleTapAnimation(double newZoom, LatLng newCenter) { - _doubleTapZoomAnimation = Tween(begin: _mapState.zoom, end: newZoom) + _doubleTapZoomAnimation = Tween(begin: _mapFrame.zoom, end: newZoom) .chain(CurveTween(curve: Curves.linear)) .animate(_doubleTapController); _doubleTapCenterAnimation = - LatLngTween(begin: _mapState.center, end: newCenter) + LatLngTween(begin: _mapFrame.center, end: newCenter) .chain(CurveTween(curve: Curves.linear)) .animate(_doubleTapController); _doubleTapController.forward(from: 0); @@ -775,14 +767,14 @@ class FlutterMapInteractiveViewerState final flags = _options.interactiveFlags; if (InteractiveFlag.hasPinchZoom(flags)) { final verticalOffset = (_focalStartLocal - details.localFocalPoint).dy; - final newZoom = _mapZoomStart - verticalOffset / 360 * _mapState.zoom; + final newZoom = _mapZoomStart - verticalOffset / 360 * _mapFrame.zoom; final min = _options.minZoom ?? 0.0; final max = _options.maxZoom ?? double.infinity; final actualZoom = math.max(min, math.min(max, newZoom)); widget.controller.move( - _mapState.center, + _mapFrame.center, actualZoom, offset: Offset.zero, hasGesture: true, @@ -799,13 +791,13 @@ class FlutterMapInteractiveViewerState _startListeningForAnimationInterruptions(); } - final newCenterPoint = _mapState.project(_mapCenterStart) + - _flingAnimation.value.toCustomPoint().rotate(_mapState.rotationRad); - final newCenter = _mapState.unproject(newCenterPoint); + final newCenterPoint = _mapFrame.project(_mapCenterStart) + + _flingAnimation.value.toCustomPoint().rotate(_mapFrame.rotationRad); + final newCenter = _mapFrame.unproject(newCenterPoint); widget.controller.move( newCenter, - _mapState.zoom, + _mapFrame.zoom, offset: Offset.zero, hasGesture: true, source: MapEventSource.flingAnimationController, @@ -844,11 +836,11 @@ class FlutterMapInteractiveViewerState double _getZoomForScale(double startZoom, double scale) { final resultZoom = scale == 1.0 ? startZoom : startZoom + math.log(scale) / math.ln2; - return _mapState.clampZoom(resultZoom); + return _mapFrame.clampZoom(resultZoom); } Offset _rotateOffset(Offset offset) { - final radians = _mapState.rotationRad; + final radians = _mapFrame.rotationRad; if (radians != 0.0) { final cos = math.cos(radians); final sin = math.sin(radians); diff --git a/lib/src/gestures/interactive_flag.dart b/lib/src/gestures/interactive_flag.dart index 561557932..25c967a91 100644 --- a/lib/src/gestures/interactive_flag.dart +++ b/lib/src/gestures/interactive_flag.dart @@ -9,6 +9,7 @@ /// ~[InteractiveFlag.pinchMove] & ~[InteractiveFlag.pinchZoom] & /// ~[InteractiveFlag.doubleTapZoom] class InteractiveFlag { + const InteractiveFlag._(); static const int all = drag | flingAnimation | pinchMove | pinchZoom | doubleTapZoom | rotate; static const int none = 0; diff --git a/lib/src/gestures/map_events.dart b/lib/src/gestures/map_events.dart index d3ca33968..4191515db 100644 --- a/lib/src/gestures/map_events.dart +++ b/lib/src/gestures/map_events.dart @@ -1,4 +1,4 @@ -import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/map/flutter_map_frame.dart'; import 'package:latlong2/latlong.dart'; /// Event sources which are used to identify different types of @@ -32,12 +32,12 @@ abstract class MapEvent { /// Who / what issued the event. final MapEventSource source; - /// The state of the map after the event. - final FlutterMapState mapState; + /// The map frame after the event. + final FlutterMapFrame mapFrame; const MapEvent({ required this.source, - required this.mapState, + required this.mapFrame, }); } @@ -45,38 +45,38 @@ abstract class MapEvent { /// includes information about camera movement /// which are not partial (e.g start rotate, rotate, end rotate). abstract class MapEventWithMove extends MapEvent { - final FlutterMapState oldMapState; + final FlutterMapFrame oldMapFrame; const MapEventWithMove({ required super.source, - required this.oldMapState, - required super.mapState, + required this.oldMapFrame, + required super.mapFrame, }); /// Returns a subclass of [MapEventWithMove] if the [source] belongs to a /// movement event, otherwise returns null. static MapEventWithMove? fromSource({ - required FlutterMapState oldMapState, - required FlutterMapState mapState, + required FlutterMapFrame oldMapFrame, + required FlutterMapFrame mapFrame, required bool hasGesture, required MapEventSource source, String? id, }) => switch (source) { MapEventSource.flingAnimationController => MapEventFlingAnimation( - oldMapState: oldMapState, - mapState: mapState, + oldMapFrame: oldMapFrame, + mapFrame: mapFrame, source: source, ), MapEventSource.doubleTapZoomAnimationController => MapEventDoubleTapZoom( - oldMapState: oldMapState, - mapState: mapState, + oldMapFrame: oldMapFrame, + mapFrame: mapFrame, source: source, ), MapEventSource.scrollWheel => MapEventScrollWheelZoom( - oldMapState: oldMapState, - mapState: mapState, + oldMapFrame: oldMapFrame, + mapFrame: mapFrame, source: source, ), MapEventSource.onDrag || @@ -85,8 +85,8 @@ abstract class MapEventWithMove extends MapEvent { MapEventSource.custom => MapEventMove( id: id, - oldMapState: oldMapState, - mapState: mapState, + oldMapFrame: oldMapFrame, + mapFrame: mapFrame, source: source, ), _ => null, @@ -101,7 +101,7 @@ class MapEventTap extends MapEvent { const MapEventTap({ required this.tapPosition, required super.source, - required super.mapState, + required super.mapFrame, }); } @@ -112,7 +112,7 @@ class MapEventSecondaryTap extends MapEvent { const MapEventSecondaryTap({ required this.tapPosition, required super.source, - required super.mapState, + required super.mapFrame, }); } @@ -124,7 +124,7 @@ class MapEventLongPress extends MapEvent { const MapEventLongPress({ required this.tapPosition, required super.source, - required super.mapState, + required super.mapFrame, }); } @@ -136,8 +136,8 @@ class MapEventMove extends MapEventWithMove { const MapEventMove({ this.id, required super.source, - required super.oldMapState, - required super.mapState, + required super.oldMapFrame, + required super.mapFrame, }); } @@ -145,7 +145,7 @@ class MapEventMove extends MapEventWithMove { class MapEventMoveStart extends MapEvent { const MapEventMoveStart({ required super.source, - required super.mapState, + required super.mapFrame, }); } @@ -153,7 +153,7 @@ class MapEventMoveStart extends MapEvent { class MapEventMoveEnd extends MapEvent { const MapEventMoveEnd({ required super.source, - required super.mapState, + required super.mapFrame, }); } @@ -161,8 +161,8 @@ class MapEventMoveEnd extends MapEvent { class MapEventFlingAnimation extends MapEventWithMove { const MapEventFlingAnimation({ required super.source, - required super.oldMapState, - required super.mapState, + required super.oldMapFrame, + required super.mapFrame, }); } @@ -171,7 +171,7 @@ class MapEventFlingAnimation extends MapEventWithMove { class MapEventFlingAnimationNotStarted extends MapEvent { const MapEventFlingAnimationNotStarted({ required super.source, - required super.mapState, + required super.mapFrame, }); } @@ -179,7 +179,7 @@ class MapEventFlingAnimationNotStarted extends MapEvent { class MapEventFlingAnimationStart extends MapEvent { const MapEventFlingAnimationStart({ required super.source, - required super.mapState, + required super.mapFrame, }); } @@ -187,7 +187,7 @@ class MapEventFlingAnimationStart extends MapEvent { class MapEventFlingAnimationEnd extends MapEvent { const MapEventFlingAnimationEnd({ required super.source, - required super.mapState, + required super.mapFrame, }); } @@ -195,8 +195,8 @@ class MapEventFlingAnimationEnd extends MapEvent { class MapEventDoubleTapZoom extends MapEventWithMove { const MapEventDoubleTapZoom({ required super.source, - required super.oldMapState, - required super.mapState, + required super.oldMapFrame, + required super.mapFrame, }); } @@ -204,8 +204,8 @@ class MapEventDoubleTapZoom extends MapEventWithMove { class MapEventScrollWheelZoom extends MapEventWithMove { const MapEventScrollWheelZoom({ required super.source, - required super.oldMapState, - required super.mapState, + required super.oldMapFrame, + required super.mapFrame, }); } @@ -213,7 +213,7 @@ class MapEventScrollWheelZoom extends MapEventWithMove { class MapEventDoubleTapZoomStart extends MapEvent { const MapEventDoubleTapZoomStart({ required super.source, - required super.mapState, + required super.mapFrame, }); } @@ -221,7 +221,7 @@ class MapEventDoubleTapZoomStart extends MapEvent { class MapEventDoubleTapZoomEnd extends MapEvent { const MapEventDoubleTapZoomEnd({ required super.source, - required super.mapState, + required super.mapFrame, }); } @@ -233,8 +233,8 @@ class MapEventRotate extends MapEventWithMove { const MapEventRotate({ required this.id, required super.source, - required super.oldMapState, - required super.mapState, + required super.oldMapFrame, + required super.mapFrame, }); } @@ -242,21 +242,21 @@ class MapEventRotate extends MapEventWithMove { class MapEventRotateStart extends MapEvent { const MapEventRotateStart({ required super.source, - required super.mapState, + required super.mapFrame, }); } class MapEventRotateEnd extends MapEvent { const MapEventRotateEnd({ required super.source, - required super.mapState, + required super.mapFrame, }); } class MapEventNonRotatedSizeChange extends MapEventWithMove { const MapEventNonRotatedSizeChange({ required super.source, - required super.oldMapState, - required super.mapState, + required super.oldMapFrame, + required super.mapFrame, }); } diff --git a/lib/src/layer/circle_layer.dart b/lib/src/layer/circle_layer.dart index a8b736c18..0df0074cf 100644 --- a/lib/src/layer/circle_layer.dart +++ b/lib/src/layer/circle_layer.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/map/flutter_map_frame.dart'; import 'package:latlong2/latlong.dart' hide Path; class CircleMarker { @@ -36,7 +36,7 @@ class CircleLayer extends StatelessWidget { return LayoutBuilder( builder: (BuildContext context, BoxConstraints bc) { final size = Size(bc.maxWidth, bc.maxHeight); - final map = FlutterMapState.of(context); + final map = FlutterMapFrame.of(context); final circleWidgets = []; for (final circle in circles) { circle.offset = map.getOffsetFromOrigin(circle.point); diff --git a/lib/src/layer/marker_layer.dart b/lib/src/layer/marker_layer.dart index 00c273a77..5744d2bfc 100644 --- a/lib/src/layer/marker_layer.dart +++ b/lib/src/layer/marker_layer.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/map/flutter_map_frame.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; @@ -192,7 +192,7 @@ class MarkerLayer extends StatelessWidget { @override Widget build(BuildContext context) { - final map = FlutterMapState.of(context); + final map = FlutterMapFrame.of(context); final markerWidgets = []; for (final marker in markers) { diff --git a/lib/src/layer/overlay_image_layer.dart b/lib/src/layer/overlay_image_layer.dart index d8e8dff21..c29943f73 100644 --- a/lib/src/layer/overlay_image_layer.dart +++ b/lib/src/layer/overlay_image_layer.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/map/flutter_map_frame.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; @@ -12,7 +12,7 @@ abstract class BaseOverlayImage { bool get gaplessPlayback; - Positioned buildPositionedForOverlay(FlutterMapState map); + Positioned buildPositionedForOverlay(FlutterMapFrame map); Image buildImageForOverlay() { return Image( @@ -45,7 +45,7 @@ class OverlayImage extends BaseOverlayImage { this.gaplessPlayback = false}); @override - Positioned buildPositionedForOverlay(FlutterMapState map) { + Positioned buildPositionedForOverlay(FlutterMapFrame map) { // northWest is not necessarily upperLeft depending on projection final bounds = Bounds( map.project(this.bounds.northWest) - map.pixelOrigin, @@ -92,7 +92,7 @@ class RotatedOverlayImage extends BaseOverlayImage { this.filterQuality = FilterQuality.medium}); @override - Positioned buildPositionedForOverlay(FlutterMapState map) { + Positioned buildPositionedForOverlay(FlutterMapFrame map) { final pxTopLeft = map.project(topLeftCorner) - map.pixelOrigin; final pxBottomRight = map.project(bottomRightCorner) - map.pixelOrigin; final pxBottomLeft = map.project(bottomLeftCorner) - map.pixelOrigin; @@ -136,7 +136,7 @@ class OverlayImageLayer extends StatelessWidget { @override Widget build(BuildContext context) { - final map = FlutterMapState.of(context); + final map = FlutterMapFrame.of(context); return ClipRect( child: Stack( children: [ diff --git a/lib/src/layer/polygon_layer.dart b/lib/src/layer/polygon_layer.dart index 6f970ae7a..1fa04ec5d 100644 --- a/lib/src/layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer.dart @@ -3,7 +3,7 @@ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/layer/label.dart'; -import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/map/flutter_map_frame.dart'; import 'package:latlong2/latlong.dart' hide Path; // conflict with Path from UI enum PolygonLabelPlacement { @@ -78,7 +78,7 @@ class PolygonLayer extends StatelessWidget { @override Widget build(BuildContext context) { - final map = FlutterMapState.of(context); + final map = FlutterMapFrame.of(context); final size = Size(map.size.x, map.size.y); final List pgons = polygonCulling @@ -97,7 +97,7 @@ class PolygonLayer extends StatelessWidget { class PolygonPainter extends CustomPainter { final List polygons; - final FlutterMapState map; + final FlutterMapFrame map; final LatLngBounds bounds; PolygonPainter(this.polygons, this.map) : bounds = map.visibleBounds; diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index 7c09bf32c..0327e1383 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -2,8 +2,8 @@ import 'dart:core'; import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/geo/latlng_bounds.dart'; +import 'package:flutter_map/src/map/flutter_map_frame.dart'; import 'package:latlong2/latlong.dart'; class Polyline { @@ -66,7 +66,7 @@ class PolylineLayer extends StatelessWidget { @override Widget build(BuildContext context) { - final map = FlutterMapState.of(context); + final map = FlutterMapFrame.of(context); return CustomPaint( painter: PolylinePainter( @@ -86,7 +86,7 @@ class PolylineLayer extends StatelessWidget { class PolylinePainter extends CustomPainter { final List polylines; - final FlutterMapState map; + final FlutterMapFrame map; final LatLngBounds bounds; PolylinePainter(this.polylines, this.map) : bounds = map.visibleBounds; diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 3d8efc037..1c4f2c3f1 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -286,7 +286,7 @@ class TileLayer extends StatefulWidget { } class _TileLayerState extends State with TickerProviderStateMixin { - bool _initializedFromMapState = false; + bool _initializedFromMapFrame = false; final TileImageManager _tileImageManager = TileImageManager(); late TileBounds _tileBounds; @@ -329,7 +329,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { void didChangeDependencies() { super.didChangeDependencies(); - final mapOptions = MapOptions.of(context); + final mapFrame = FlutterMapFrame.of(context); final mapController = MapController.of(context); if (_mapControllerHashCode != mapController.hashCode) { @@ -343,29 +343,29 @@ class _TileLayerState extends State with TickerProviderStateMixin { } bool reloadTiles = false; - if (!_initializedFromMapState || + if (!_initializedFromMapFrame || _tileBounds.shouldReplace( - mapOptions.crs, widget.tileSize, widget.tileBounds)) { + mapFrame.crs, widget.tileSize, widget.tileBounds)) { reloadTiles = true; _tileBounds = TileBounds( - crs: mapOptions.crs, + crs: mapFrame.crs, tileSize: widget.tileSize, latLngBounds: widget.tileBounds, ); } - if (!_initializedFromMapState || - _tileScaleCalculator.shouldReplace(mapOptions.crs, widget.tileSize)) { + if (!_initializedFromMapFrame || + _tileScaleCalculator.shouldReplace(mapFrame.crs, widget.tileSize)) { reloadTiles = true; _tileScaleCalculator = TileScaleCalculator( - crs: mapOptions.crs, + crs: mapFrame.crs, tileSize: widget.tileSize, ); } - if (reloadTiles) _loadAndPruneInVisibleBounds(FlutterMapState.of(context)); + if (reloadTiles) _loadAndPruneInVisibleBounds(mapFrame); - _initializedFromMapState = true; + _initializedFromMapFrame = true; } @override @@ -421,7 +421,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { if (reloadTiles) { _tileImageManager.removeAll(widget.evictErrorTileStrategy); - _loadAndPruneInVisibleBounds(FlutterMapState.maybeOf(context)!); + _loadAndPruneInVisibleBounds(FlutterMapFrame.maybeOf(context)!); } else if (oldWidget.tileDisplay != widget.tileDisplay) { _tileImageManager.updateTileDisplay(widget.tileDisplay); } @@ -440,14 +440,14 @@ class _TileLayerState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - final map = FlutterMapState.of(context); + final map = FlutterMapFrame.of(context); if (_outsideZoomLimits(map.zoom.round())) return const SizedBox.shrink(); final tileZoom = _clampToNativeZoom(map.zoom); final tileBoundsAtZoom = _tileBounds.atZoom(tileZoom); final visibleTileRange = _tileRangeCalculator.calculate( - mapState: map, + mapFrame: map, tileZoom: tileZoom, ); @@ -522,7 +522,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { void _onTileUpdateEvent(TileUpdateEvent event) { final tileZoom = _clampToNativeZoom(event.zoom); final visibleTileRange = _tileRangeCalculator.calculate( - mapState: event.mapState, + mapFrame: event.mapFrame, tileZoom: tileZoom, center: event.center, viewingZoom: event.zoom, @@ -540,10 +540,10 @@ class _TileLayerState extends State with TickerProviderStateMixin { } // Load new tiles in the visible bounds and prune those outside. - void _loadAndPruneInVisibleBounds(FlutterMapState mapState) { - final tileZoom = _clampToNativeZoom(mapState.zoom); + void _loadAndPruneInVisibleBounds(FlutterMapFrame mapFrame) { + final tileZoom = _clampToNativeZoom(mapFrame.zoom); final visibleTileRange = _tileRangeCalculator.calculate( - mapState: mapState, + mapFrame: mapFrame, tileZoom: tileZoom, ); diff --git a/lib/src/layer/tile_layer/tile_range_calculator.dart b/lib/src/layer/tile_layer/tile_range_calculator.dart index 1b6b79a08..07cc4712f 100644 --- a/lib/src/layer/tile_layer/tile_range_calculator.dart +++ b/lib/src/layer/tile_layer/tile_range_calculator.dart @@ -1,5 +1,5 @@ import 'package:flutter_map/src/layer/tile_layer/tile_range.dart'; -import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/map/flutter_map_frame.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; @@ -12,38 +12,38 @@ class TileRangeCalculator { /// viewing the map from the [viewingZoom] centered at the [center]. The /// resulting tile range is expanded by [panBuffer]. DiscreteTileRange calculate({ - // The map state used to calculate the bounds. - required FlutterMapState mapState, + // The map frame used to calculate the bounds. + required FlutterMapFrame mapFrame, // The zoom level at which the bounds should be calculated. required int tileZoom, - // The center from which the map is viewed, defaults to [mapState.center]. + // The center from which the map is viewed, defaults to [mapFrame.center]. LatLng? center, - // The zoom from which the map is viewed, defaults to [mapState.zoom]. + // The zoom from which the map is viewed, defaults to [mapFrame.zoom]. double? viewingZoom, }) { return DiscreteTileRange.fromPixelBounds( zoom: tileZoom, tileSize: tileSize, pixelBounds: _calculatePixelBounds( - mapState, - center ?? mapState.center, - viewingZoom ?? mapState.zoom, + mapFrame, + center ?? mapFrame.center, + viewingZoom ?? mapFrame.zoom, tileZoom, ), ); } Bounds _calculatePixelBounds( - FlutterMapState mapState, + FlutterMapFrame mapFrame, LatLng center, double viewingZoom, int tileZoom, ) { final tileZoomDouble = tileZoom.toDouble(); - final scale = mapState.getZoomScale(viewingZoom, tileZoomDouble); + final scale = mapFrame.getZoomScale(viewingZoom, tileZoomDouble); final pixelCenter = - mapState.project(center, tileZoomDouble).floor().toDoublePoint(); - final halfSize = mapState.size / (scale * 2); + mapFrame.project(center, tileZoomDouble).floor().toDoublePoint(); + final halfSize = mapFrame.size / (scale * 2); return Bounds(pixelCenter - halfSize, pixelCenter + halfSize); } diff --git a/lib/src/layer/tile_layer/tile_update_event.dart b/lib/src/layer/tile_layer/tile_update_event.dart index 01425dbed..6809c1b3a 100644 --- a/lib/src/layer/tile_layer/tile_update_event.dart +++ b/lib/src/layer/tile_layer/tile_update_event.dart @@ -1,5 +1,5 @@ import 'package:flutter_map/src/gestures/map_events.dart'; -import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/map/flutter_map_frame.dart'; import 'package:latlong2/latlong.dart'; /// Describes whether loading and/or pruning should occur and allows overriding @@ -19,11 +19,11 @@ class TileUpdateEvent { this.loadZoomOverride, }); - double get zoom => loadZoomOverride ?? mapEvent.mapState.zoom; + double get zoom => loadZoomOverride ?? mapEvent.mapFrame.zoom; - LatLng get center => loadCenterOverride ?? mapEvent.mapState.center; + LatLng get center => loadCenterOverride ?? mapEvent.mapFrame.center; - FlutterMapState get mapState => mapEvent.mapState; + FlutterMapFrame get mapFrame => mapEvent.mapFrame; /// Returns a copy of this TileUpdateEvent with only pruning enabled and the /// loadCenterOverride/loadZoomOverride removed. diff --git a/lib/src/map/flutter_map_state.dart b/lib/src/map/flutter_map_frame.dart similarity index 90% rename from lib/src/map/flutter_map_state.dart rename to lib/src/map/flutter_map_frame.dart index 1c8cd5786..0ca9f7190 100644 --- a/lib/src/map/flutter_map_state.dart +++ b/lib/src/map/flutter_map_frame.dart @@ -9,7 +9,7 @@ import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; -class FlutterMapState { +class FlutterMapFrame { // During Flutter startup the native platform resolution is not immediately // available which can cause constraints to be zero before they are updated // in a subsequent build to the actual constraints. We set the size to this @@ -29,23 +29,23 @@ class FlutterMapState { final CustomPoint nonRotatedSize; // Lazily calculated fields. - CustomPoint? _size; + CustomPoint? _frameSize; Bounds? _pixelBounds; LatLngBounds? _bounds; CustomPoint? _pixelOrigin; - static FlutterMapState? maybeOf(BuildContext context) => context + static FlutterMapFrame? maybeOf(BuildContext context) => context .dependOnInheritedWidgetOfExactType() - ?.state; + ?.frame; - static FlutterMapState of(BuildContext context) => + static FlutterMapFrame of(BuildContext context) => maybeOf(context) ?? (throw StateError( - '`FlutterMapState.of()` should not be called outside a `FlutterMap` and its descendants')); + '`FlutterMapFrame.of()` should not be called outside a `FlutterMap` and its descendants')); - /// Initializes FlutterMapState from the given [options] and with the - /// [nonRotatedSize] and [size] both set to [kImpossibleSize]. - FlutterMapState.initialState(MapOptions options) + /// Initializes FlutterMapFrame from the given [options] and with the + /// [nonRotatedSize] set to [kImpossibleSize]. + FlutterMapFrame.initialFrame(MapOptions options) : crs = options.crs, minZoom = options.minZoom, maxZoom = options.maxZoom, @@ -54,10 +54,10 @@ class FlutterMapState { rotation = options.initialRotation, nonRotatedSize = kImpossibleSize; - // Create an instance of FlutterMapState. The [pixelOrigin], [bounds], and + // Create an instance of FlutterMapFrame. The [pixelOrigin], [bounds], and // [pixelBounds] may be set if they are known already. Otherwise if left // null they will be calculated lazily when they are used. - FlutterMapState({ + FlutterMapFrame({ required this.crs, required this.center, required this.zoom, @@ -69,15 +69,15 @@ class FlutterMapState { Bounds? pixelBounds, LatLngBounds? bounds, CustomPoint? pixelOrigin, - }) : _size = size ?? calculateRotatedSize(rotation, nonRotatedSize), + }) : _frameSize = size ?? calculateRotatedSize(rotation, nonRotatedSize), _pixelBounds = pixelBounds, _bounds = bounds, _pixelOrigin = pixelOrigin; - FlutterMapState withNonRotatedSize(CustomPoint nonRotatedSize) { + FlutterMapFrame withNonRotatedSize(CustomPoint nonRotatedSize) { if (nonRotatedSize == this.nonRotatedSize) return this; - return FlutterMapState( + return FlutterMapFrame( crs: crs, minZoom: minZoom, maxZoom: maxZoom, @@ -88,10 +88,10 @@ class FlutterMapState { ); } - FlutterMapState withRotation(double rotation) { + FlutterMapFrame withRotation(double rotation) { if (rotation == this.rotation) return this; - return FlutterMapState( + return FlutterMapFrame( crs: crs, minZoom: minZoom, maxZoom: maxZoom, @@ -102,14 +102,14 @@ class FlutterMapState { ); } - FlutterMapState withOptions(MapOptions options) { + FlutterMapFrame withOptions(MapOptions options) { if (options.crs == crs && options.minZoom == minZoom && options.maxZoom == maxZoom) { return this; } - return FlutterMapState( + return FlutterMapFrame( crs: options.crs, minZoom: options.minZoom, maxZoom: options.maxZoom, @@ -117,15 +117,15 @@ class FlutterMapState { zoom: zoom, rotation: rotation, nonRotatedSize: nonRotatedSize, - size: _size, + size: _frameSize, ); } - FlutterMapState withPosition({ + FlutterMapFrame withPosition({ LatLng? center, double? zoom, }) => - FlutterMapState( + FlutterMapFrame( crs: crs, minZoom: minZoom, maxZoom: maxZoom, @@ -133,7 +133,7 @@ class FlutterMapState { zoom: zoom ?? this.zoom, rotation: rotation, nonRotatedSize: nonRotatedSize, - size: _size, + size: _frameSize, ); @Deprecated('Use visibleBounds instead.') @@ -147,7 +147,7 @@ class FlutterMapState { )); CustomPoint get size => - _size ?? + _frameSize ?? calculateRotatedSize( rotation, nonRotatedSize, diff --git a/lib/src/map/flutter_map_internal_controller.dart b/lib/src/map/flutter_map_internal_controller.dart index be12b4461..d758de522 100644 --- a/lib/src/map/flutter_map_internal_controller.dart +++ b/lib/src/map/flutter_map_internal_controller.dart @@ -17,7 +17,7 @@ class FlutterMapInternalController : super( FlutterMapInternalState( options: options, - mapState: FlutterMapState.initialState(options), + mapFrame: FlutterMapFrame.initialFrame(options), ), ); @@ -29,7 +29,7 @@ class FlutterMapInternalController _interactiveViewerState = interactiveViewerState; MapOptions get options => value.options; - FlutterMapState get mapState => value.mapState; + FlutterMapFrame get mapFrame => value.mapFrame; void linkMapController(MapControllerImpl mapControllerImpl) { _mapControllerImpl = mapControllerImpl; @@ -37,7 +37,7 @@ class FlutterMapInternalController } /// This setter should only be called in this class or within tests. Changes - /// to the FlutterMapState should be done via methods in this class. + /// to the [FlutterMapInternalState] should be done via methods in this class. @visibleForTesting @override set value(FlutterMapInternalState value) => super.value = value; @@ -55,9 +55,9 @@ class FlutterMapInternalController }) { // Algorithm thanks to https://github.com/tlserver/flutter_map_location_marker if (offset != Offset.zero) { - final newPoint = mapState.project(newCenter, newZoom); - newCenter = mapState.unproject( - mapState.rotatePoint( + final newPoint = mapFrame.project(newCenter, newZoom); + newCenter = mapFrame.unproject( + mapFrame.rotatePoint( newPoint, newPoint - CustomPoint(offset.dx, offset.dy), ), @@ -65,24 +65,24 @@ class FlutterMapInternalController ); } - FlutterMapState? newMapState = mapState.withPosition( + FlutterMapFrame? newMapFrame = mapFrame.withPosition( center: newCenter, - zoom: mapState.clampZoom(newZoom), + zoom: mapFrame.clampZoom(newZoom), ); - newMapState = options.frameConstraint.constrain(newMapState); - if (newMapState == null || - (newMapState.center == mapState.center && - newMapState.zoom == mapState.zoom)) { + newMapFrame = options.frameConstraint.constrain(newMapFrame); + if (newMapFrame == null || + (newMapFrame.center == mapFrame.center && + newMapFrame.zoom == mapFrame.zoom)) { return false; } - final oldMapState = mapState; - value = value.withMapState(newMapState); + final oldMapFrame = mapFrame; + value = value.withMapFrame(newMapFrame); final movementEvent = MapEventWithMove.fromSource( - oldMapState: oldMapState, - mapState: mapState, + oldMapFrame: oldMapFrame, + mapFrame: mapFrame, hasGesture: hasGesture, source: source, id: id, @@ -92,7 +92,7 @@ class FlutterMapInternalController options.onPositionChanged?.call( MapPosition( center: newCenter, - bounds: mapState.visibleBounds, + bounds: mapFrame.visibleBounds, zoom: newZoom, hasGesture: hasGesture, ), @@ -111,23 +111,23 @@ class FlutterMapInternalController required MapEventSource source, required String? id, }) { - if (newRotation != mapState.rotation) { - final newMapState = options.frameConstraint.constrain( - mapState.withRotation(newRotation), + if (newRotation != mapFrame.rotation) { + final newMapFrame = options.frameConstraint.constrain( + mapFrame.withRotation(newRotation), ); - if (newMapState == null) return false; + if (newMapFrame == null) return false; - final oldMapState = mapState; + final oldMapFrame = mapFrame; - // Apply state then emit events and callbacks - value = value.withMapState(newMapState); + // Update frame then emit events and callbacks + value = value.withMapFrame(newMapFrame); _emitMapEvent( MapEventRotate( id: id, source: source, - oldMapState: oldMapState, - mapState: mapState, + oldMapFrame: oldMapFrame, + mapFrame: mapFrame, ), ); return true; @@ -154,7 +154,7 @@ class FlutterMapInternalController throw ArgumentError('One of `point` or `offset` must be non-null'); } - if (degree == mapState.rotation) { + if (degree == mapFrame.rotation) { return MoveAndRotateResult(false, false); } @@ -170,28 +170,28 @@ class FlutterMapInternalController ); } - final rotationDiff = degree - mapState.rotation; - final rotationCenter = mapState.project(mapState.center) + + final rotationDiff = degree - mapFrame.rotation; + final rotationCenter = mapFrame.project(mapFrame.center) + (point != null - ? (point - (mapState.nonRotatedSize / 2.0)) + ? (point - (mapFrame.nonRotatedSize / 2.0)) : CustomPoint(offset!.dx, offset.dy)) - .rotate(mapState.rotationRad); + .rotate(mapFrame.rotationRad); return MoveAndRotateResult( move( - mapState.unproject( + mapFrame.unproject( rotationCenter + - (mapState.project(mapState.center) - rotationCenter) + (mapFrame.project(mapFrame.center) - rotationCenter) .rotate(degToRadian(rotationDiff)), ), - mapState.zoom, + mapFrame.zoom, offset: Offset.zero, hasGesture: hasGesture, source: source, id: id, ), rotate( - mapState.rotation + rotationDiff, + mapFrame.rotation + rotationDiff, hasGesture: hasGesture, source: source, id: id, @@ -230,7 +230,7 @@ class FlutterMapInternalController FrameFit frameFit, { required Offset offset, }) { - final fitted = frameFit.fit(mapState); + final fitted = frameFit.fit(mapFrame); return move( fitted.center, @@ -245,9 +245,9 @@ class FlutterMapInternalController bool setNonRotatedSizeWithoutEmittingEvent( CustomPoint nonRotatedSize, ) { - if (nonRotatedSize != FlutterMapState.kImpossibleSize && - nonRotatedSize != mapState.nonRotatedSize) { - value = value.withMapState(mapState.withNonRotatedSize(nonRotatedSize)); + if (nonRotatedSize != FlutterMapFrame.kImpossibleSize && + nonRotatedSize != mapFrame.nonRotatedSize) { + value = value.withMapFrame(mapFrame.withNonRotatedSize(nonRotatedSize)); return true; } @@ -258,15 +258,23 @@ class FlutterMapInternalController if (options == value.options) return; if (options != this.options) { - final newMapState = mapState.withOptions(options); + final newMapFrame = mapFrame.withOptions(options); assert( - options.frameConstraint.constrain(newMapState) == newMapState, - 'FlutterMapState is no longer within the frameConstraint after an option change.', + options.frameConstraint.constrain(newMapFrame) == newMapFrame, + 'FlutterMapFrame is no longer within the frameConstraint after an option change.', ); + + if (this.options.interactionOptions != options.interactionOptions) { + _interactiveViewerState.updateGestures( + this.options.interactionOptions, + options.interactionOptions, + ); + } + value = FlutterMapInternalState( options: options, - mapState: newMapState, + mapFrame: newMapFrame, ); } } @@ -275,7 +283,7 @@ class FlutterMapInternalController void moveStarted(MapEventSource source) { _emitMapEvent( MapEventMoveStart( - mapState: mapState, + mapFrame: mapFrame, source: source, ), ); @@ -283,14 +291,14 @@ class FlutterMapInternalController // To be called when an ongoing drag movement updates. void dragUpdated(MapEventSource source, Offset offset) { - final oldCenterPt = mapState.project(mapState.center); + final oldCenterPt = mapFrame.project(mapFrame.center); final newCenterPt = oldCenterPt + offset.toCustomPoint(); - final newCenter = mapState.unproject(newCenterPt); + final newCenter = mapFrame.unproject(newCenterPt); move( newCenter, - mapState.zoom, + mapFrame.zoom, offset: Offset.zero, hasGesture: true, source: source, @@ -302,7 +310,7 @@ class FlutterMapInternalController void moveEnded(MapEventSource source) { _emitMapEvent( MapEventMoveEnd( - mapState: mapState, + mapFrame: mapFrame, source: source, ), ); @@ -312,7 +320,7 @@ class FlutterMapInternalController void rotateStarted(MapEventSource source) { _emitMapEvent( MapEventRotateStart( - mapState: mapState, + mapFrame: mapFrame, source: source, ), ); @@ -322,7 +330,7 @@ class FlutterMapInternalController void rotateEnded(MapEventSource source) { _emitMapEvent( MapEventRotateEnd( - mapState: mapState, + mapFrame: mapFrame, source: source, ), ); @@ -332,7 +340,7 @@ class FlutterMapInternalController void flingStarted(MapEventSource source) { _emitMapEvent( MapEventFlingAnimationStart( - mapState: mapState, + mapFrame: mapFrame, source: MapEventSource.flingAnimationController, ), ); @@ -342,7 +350,7 @@ class FlutterMapInternalController void flingEnded(MapEventSource source) { _emitMapEvent( MapEventFlingAnimationEnd( - mapState: mapState, + mapFrame: mapFrame, source: source, ), ); @@ -352,7 +360,7 @@ class FlutterMapInternalController void flingNotStarted(MapEventSource source) { _emitMapEvent( MapEventFlingAnimationNotStarted( - mapState: mapState, + mapFrame: mapFrame, source: source, ), ); @@ -362,7 +370,7 @@ class FlutterMapInternalController void doubleTapZoomStarted(MapEventSource source) { _emitMapEvent( MapEventDoubleTapZoomStart( - mapState: mapState, + mapFrame: mapFrame, source: source, ), ); @@ -372,7 +380,7 @@ class FlutterMapInternalController void doubleTapZoomEnded(MapEventSource source) { _emitMapEvent( MapEventDoubleTapZoomEnd( - mapState: mapState, + mapFrame: mapFrame, source: source, ), ); @@ -387,7 +395,7 @@ class FlutterMapInternalController _emitMapEvent( MapEventTap( tapPosition: position, - mapState: mapState, + mapFrame: mapFrame, source: source, ), ); @@ -402,7 +410,7 @@ class FlutterMapInternalController _emitMapEvent( MapEventSecondaryTap( tapPosition: position, - mapState: mapState, + mapFrame: mapFrame, source: source, ), ); @@ -417,7 +425,7 @@ class FlutterMapInternalController _emitMapEvent( MapEventLongPress( tapPosition: position, - mapState: mapState, + mapFrame: mapFrame, source: MapEventSource.longPress, ), ); @@ -426,14 +434,14 @@ class FlutterMapInternalController // To be called when the map's size constraints change. void nonRotatedSizeChange( MapEventSource source, - FlutterMapState oldMapState, - FlutterMapState newMapState, + FlutterMapFrame oldMapFrame, + FlutterMapFrame newMapFrame, ) { _emitMapEvent( MapEventNonRotatedSizeChange( source: MapEventSource.nonRotatedSizeChange, - oldMapState: oldMapState, - mapState: newMapState, + oldMapFrame: oldMapFrame, + mapFrame: newMapFrame, ), ); } diff --git a/lib/src/map/flutter_map_internal_state.dart b/lib/src/map/flutter_map_internal_state.dart index 2092ed72e..4b1a616c1 100644 --- a/lib/src/map/flutter_map_internal_state.dart +++ b/lib/src/map/flutter_map_internal_state.dart @@ -1,17 +1,17 @@ import 'package:flutter_map/plugin_api.dart'; class FlutterMapInternalState { - final FlutterMapState mapState; + final FlutterMapFrame mapFrame; final MapOptions options; const FlutterMapInternalState({ required this.options, - required this.mapState, + required this.mapFrame, }); - FlutterMapInternalState withMapState(FlutterMapState mapState) => + FlutterMapInternalState withMapFrame(FlutterMapFrame mapFrame) => FlutterMapInternalState( options: options, - mapState: mapState, + mapFrame: mapFrame, ); } diff --git a/lib/src/map/flutter_map_state_container.dart b/lib/src/map/flutter_map_state_container.dart index 8831a7d60..822db3957 100644 --- a/lib/src/map/flutter_map_state_container.dart +++ b/lib/src/map/flutter_map_state_container.dart @@ -56,21 +56,20 @@ class FlutterMapStateContainer extends State { return FlutterMapInteractiveViewer( controller: _flutterMapInternalController, - options: widget.options, builder: (context, mapState) => MapStateInheritedWidget( controller: _mapController, - options: widget.options, - state: mapState, + options: mapState.options, + frame: mapState.mapFrame, child: ClipRect( child: Stack( children: [ OverflowBox( - minWidth: mapState.size.x, - maxWidth: mapState.size.x, - minHeight: mapState.size.y, - maxHeight: mapState.size.y, + minWidth: mapState.mapFrame.size.x, + maxWidth: mapState.mapFrame.size.x, + minHeight: mapState.mapFrame.size.y, + maxHeight: mapState.mapFrame.size.y, child: Transform.rotate( - angle: mapState.rotationRad, + angle: mapState.mapFrame.rotationRad, child: Stack(children: widget.children), ), ), @@ -122,10 +121,10 @@ class FlutterMapStateContainer extends State { constraints.maxWidth, constraints.maxHeight, ); - final oldMapState = _flutterMapInternalController.mapState; + final oldMapFrame = _flutterMapInternalController.mapFrame; if (_flutterMapInternalController .setNonRotatedSizeWithoutEmittingEvent(nonRotatedSize)) { - final newMapState = _flutterMapInternalController.mapState; + final newMapFrame = _flutterMapInternalController.mapFrame; // Avoid emitting the event during build otherwise if the user calls // setState in the onMapEvent callback it will throw. @@ -133,8 +132,8 @@ class FlutterMapStateContainer extends State { if (mounted) { _flutterMapInternalController.nonRotatedSizeChange( MapEventSource.nonRotatedSizeChange, - oldMapState, - newMapState, + oldMapFrame, + newMapFrame, ); } }); diff --git a/lib/src/map/flutter_map_state_inherited_widget.dart b/lib/src/map/flutter_map_state_inherited_widget.dart index 3b8b35851..09eb3271b 100644 --- a/lib/src/map/flutter_map_state_inherited_widget.dart +++ b/lib/src/map/flutter_map_state_inherited_widget.dart @@ -1,24 +1,24 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/map/flutter_map_frame.dart'; import 'package:flutter_map/src/map/map_controller.dart'; import 'package:flutter_map/src/map/options.dart'; class MapStateInheritedWidget extends InheritedWidget { const MapStateInheritedWidget({ super.key, - required this.state, + required this.frame, required this.controller, required this.options, required super.child, }); - final FlutterMapState state; + final FlutterMapFrame frame; final MapController controller; final MapOptions options; @override bool updateShouldNotify(MapStateInheritedWidget oldWidget) => - !identical(state, oldWidget.state) || + !identical(frame, oldWidget.frame) || !identical(controller, oldWidget.controller) || !identical(options, oldWidget.options); } diff --git a/lib/src/map/map_controller.dart b/lib/src/map/map_controller.dart index 3d37db696..16c65eb9d 100644 --- a/lib/src/map/map_controller.dart +++ b/lib/src/map/map_controller.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/map/flutter_map_frame.dart'; import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; import 'package:latlong2/latlong.dart'; @@ -138,8 +138,10 @@ abstract class MapController { /// documentation. bool fitFrame(FrameFit frameFit); - /// Current FlutterMapState. - FlutterMapState get mapState; + /// Current FlutterMapFrame. Accessing the frame from this getter is an + /// anti-pattern. It is preferable to use FlutterMapFrame.of(context) in a + /// child widget of FlutterMap state. + FlutterMapFrame get mapFrame; /// [Stream] of all emitted [MapEvent]s Stream get mapEventStream; @@ -149,7 +151,7 @@ abstract class MapController { /// /// Does not move/zoom the map: see [fitBounds]. @Deprecated( - 'Use FrameFit.bounds(bounds: bounds).fit(controller.mapState) instead.') + 'Use FrameFit.bounds(bounds: bounds).fit(controller.mapFrame) instead.') CenterZoom centerZoomFitBounds( LatLngBounds bounds, { FitBoundsOptions options = @@ -158,15 +160,15 @@ abstract class MapController { /// Convert a screen point (x/y) to its corresponding map coordinate (lat/lng), /// based on the map's current properties - @Deprecated('Use controller.mapState.pointToLatLng() instead.') + @Deprecated('Use controller.mapFrame.pointToLatLng() instead.') LatLng pointToLatLng(CustomPoint screenPoint); /// Convert a map coordinate (lat/lng) to its corresponding screen point (x/y), /// based on the map's current screen positioning - @Deprecated('Use controller.mapState.latLngToScreenPoint() instead.') + @Deprecated('Use controller.mapFrame.latLngToScreenPoint() instead.') CustomPoint latLngToScreenPoint(LatLng mapCoordinate); - @Deprecated('Use controller.mapState.rotatePoint() instead.') + @Deprecated('Use controller.mapFrame.rotatePoint() instead.') CustomPoint rotatePoint( CustomPoint mapCenter, CustomPoint point, { @@ -174,19 +176,19 @@ abstract class MapController { }); /// Current center coordinates - @Deprecated('Use controller.mapState.center instead.') + @Deprecated('Use controller.mapFrame.center instead.') LatLng get center; /// Current outer points/boundaries coordinates - @Deprecated('Use controller.mapState.visibleBounds instead.') + @Deprecated('Use controller.mapFrame.visibleBounds instead.') LatLngBounds? get bounds; /// Current zoom level - @Deprecated('Use controller.mapState.zoom instead.') + @Deprecated('Use controller.mapFrame.zoom instead.') double get zoom; /// Current rotation in degrees, where 0° is North - @Deprecated('Use controller.mapState.rotation instead.') + @Deprecated('Use controller.mapFrame.rotation instead.') double get rotation; /// Dispose of this controller. diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/map_controller_impl.dart index 86bc6d85d..5387f5cc8 100644 --- a/lib/src/map/map_controller_impl.dart +++ b/lib/src/map/map_controller_impl.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; +import 'package:flutter_map/src/map/flutter_map_frame.dart'; import 'package:flutter_map/src/map/flutter_map_internal_controller.dart'; -import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:flutter_map/src/map/map_controller.dart'; import 'package:flutter_map/src/misc/center_zoom.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; @@ -102,7 +102,7 @@ class MapControllerImpl implements MapController { ); @override - FlutterMapState get mapState => _internalController.mapState; + FlutterMapFrame get mapFrame => _internalController.mapFrame; final _mapEventStreamController = StreamController.broadcast(); @@ -123,16 +123,16 @@ class MapControllerImpl implements MapController { } @override - @Deprecated('Use controller.mapState.visibleBounds instead.') - LatLngBounds? get bounds => mapState.visibleBounds; + @Deprecated('Use controller.mapFrame.visibleBounds instead.') + LatLngBounds? get bounds => mapFrame.visibleBounds; @override - @Deprecated('Use controller.mapState.center instead.') - LatLng get center => mapState.center; + @Deprecated('Use controller.mapFrame.center instead.') + LatLng get center => mapFrame.center; @override @Deprecated( - 'Use FrameFit.bounds(bounds: bounds).fit(controller.mapState) instead.') + 'Use FrameFit.bounds(bounds: bounds).fit(controller.mapFrame) instead.') CenterZoom centerZoomFitBounds( LatLngBounds bounds, { FitBoundsOptions options = @@ -144,7 +144,7 @@ class MapControllerImpl implements MapController { maxZoom: options.maxZoom, inside: options.inside, forceIntegerZoomLevel: options.forceIntegerZoomLevel, - ).fit(mapState); + ).fit(mapFrame); return CenterZoom( center: fittedState.center, zoom: fittedState.zoom, @@ -152,33 +152,33 @@ class MapControllerImpl implements MapController { } @override - @Deprecated('Use controller.mapState.latLngToScreenPoint() instead.') + @Deprecated('Use controller.mapFrame.latLngToScreenPoint() instead.') CustomPoint latLngToScreenPoint(LatLng mapCoordinate) => - mapState.latLngToScreenPoint(mapCoordinate); + mapFrame.latLngToScreenPoint(mapCoordinate); @override - @Deprecated('Use controller.mapState.pointToLatLng() instead.') + @Deprecated('Use controller.mapFrame.pointToLatLng() instead.') LatLng pointToLatLng(CustomPoint screenPoint) => - mapState.pointToLatLng(screenPoint); + mapFrame.pointToLatLng(screenPoint); @override - @Deprecated('Use controller.mapState.rotatePoint() instead.') + @Deprecated('Use controller.mapFrame.rotatePoint() instead.') CustomPoint rotatePoint( CustomPoint mapCenter, CustomPoint point, { bool counterRotation = true, }) => - mapState.rotatePoint( + mapFrame.rotatePoint( mapCenter.toDoublePoint(), point.toDoublePoint(), counterRotation: counterRotation, ); @override - @Deprecated('Use controller.mapState.rotation instead.') - double get rotation => mapState.rotation; + @Deprecated('Use controller.mapFrame.rotation instead.') + double get rotation => mapFrame.rotation; @override - @Deprecated('Use controller.mapState.zoom instead.') - double get zoom => mapState.zoom; + @Deprecated('Use controller.mapFrame.zoom instead.') + double get zoom => mapFrame.zoom; } diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index 12c629c03..b2f3d43e7 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -13,6 +13,27 @@ import 'package:flutter_map/src/misc/position.dart'; import 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; import 'package:latlong2/latlong.dart'; +typedef MapEventCallback = void Function(MapEvent); + +typedef TapCallback = void Function(TapPosition tapPosition, LatLng point); +typedef LongPressCallback = void Function( + TapPosition tapPosition, + LatLng point, +); +typedef PointerDownCallback = void Function( + PointerDownEvent event, + LatLng point, +); +typedef PointerUpCallback = void Function(PointerUpEvent event, LatLng point); +typedef PointerCancelCallback = void Function( + PointerCancelEvent event, + LatLng point, +); +typedef PointerHoverCallback = void Function( + PointerHoverEvent event, + LatLng point, +); + class MapOptions { /// The Coordinate Reference System, defaults to [Epsg3857]. final Crs crs; @@ -127,6 +148,8 @@ class MapOptions { /// widget from rebuilding. final bool keepAlive; + final InteractionOptions? _interactionOptions; + const MapOptions({ this.crs = const Epsg3857(), @Deprecated('Use initialCenter instead') LatLng? center, @@ -140,21 +163,33 @@ class MapOptions { this.boundsOptions = const FitBoundsOptions(), this.initialFrameFit, this.frameConstraint = const FrameConstraint.unconstrained(), + @Deprecated('Should be set in interactionOptions instead') this.debugMultiFingerGestureWinner = false, + @Deprecated('Should be set in interactionOptions instead') this.enableMultiFingerGestureRace = false, + @Deprecated('Should be set in interactionOptions instead') this.rotationThreshold = 20.0, + @Deprecated('Should be set in interactionOptions instead') this.rotationWinGestures = MultiFingerGesture.rotate, + @Deprecated('Should be set in interactionOptions instead') this.pinchZoomThreshold = 0.5, + @Deprecated('Should be set in interactionOptions instead') this.pinchZoomWinGestures = MultiFingerGesture.pinchZoom | MultiFingerGesture.pinchMove, + @Deprecated('Should be set in interactionOptions instead') this.pinchMoveThreshold = 40.0, + @Deprecated('Should be set in interactionOptions instead') this.pinchMoveWinGestures = MultiFingerGesture.pinchZoom | MultiFingerGesture.pinchMove, + @Deprecated('Should be set in interactionOptions instead') this.enableScrollWheel = true, + @Deprecated('Should be set in interactionOptions instead') this.scrollWheelVelocity = 0.005, this.minZoom, this.maxZoom, + @Deprecated('Should be set in interactionOptions instead') this.interactiveFlags = InteractiveFlag.all, + InteractionOptions? interactionOptions, this.onTap, this.onSecondaryTap, this.onLongPress, @@ -166,7 +201,8 @@ class MapOptions { this.onMapEvent, this.onMapReady, this.keepAlive = false, - }) : initialCenter = center ?? initialCenter, + }) : _interactionOptions = interactionOptions, + initialCenter = center ?? initialCenter, initialZoom = zoom ?? initialZoom, initialRotation = rotation ?? initialRotation, assert(rotationThreshold >= 0.0), @@ -181,25 +217,135 @@ class MapOptions { maybeOf(context) ?? (throw StateError( '`MapOptions.of()` should not be called outside a `FlutterMap` and its descendants')); + + InteractionOptions get interactionOptions => + _interactionOptions ?? + InteractionOptions( + flags: interactiveFlags, + debugMultiFingerGestureWinner: debugMultiFingerGestureWinner, + enableMultiFingerGestureRace: enableMultiFingerGestureRace, + rotationThreshold: rotationThreshold, + rotationWinGestures: rotationWinGestures, + pinchZoomThreshold: pinchZoomThreshold, + pinchZoomWinGestures: pinchZoomWinGestures, + pinchMoveThreshold: pinchMoveThreshold, + pinchMoveWinGestures: pinchMoveWinGestures, + enableScrollWheel: enableScrollWheel, + scrollWheelVelocity: scrollWheelVelocity, + ); } -typedef MapEventCallback = void Function(MapEvent); +final class InteractionOptions { + /// See [InteractiveFlag] for custom settings + final int flags; -typedef TapCallback = void Function(TapPosition tapPosition, LatLng point); -typedef LongPressCallback = void Function( - TapPosition tapPosition, - LatLng point, -); -typedef PointerDownCallback = void Function( - PointerDownEvent event, - LatLng point, -); -typedef PointerUpCallback = void Function(PointerUpEvent event, LatLng point); -typedef PointerCancelCallback = void Function( - PointerCancelEvent event, - LatLng point, -); -typedef PointerHoverCallback = void Function( - PointerHoverEvent event, - LatLng point, -); + /// Prints multi finger gesture winner Helps to fine adjust + /// [rotationThreshold] and [pinchZoomThreshold] and [pinchMoveThreshold] + /// Note: only takes effect if [enableMultiFingerGestureRace] is true + final bool debugMultiFingerGestureWinner; + + /// If true then [rotationThreshold] and [pinchZoomThreshold] and + /// [pinchMoveThreshold] will race If multiple gestures win at the same time + /// then precedence: [pinchZoomWinGestures] > [rotationWinGestures] > + /// [pinchMoveWinGestures] + final bool enableMultiFingerGestureRace; + + /// Rotation threshold in degree default is 20.0 Map starts to rotate when + /// [rotationThreshold] has been achieved or another multi finger gesture wins + /// which allows [MultiFingerGesture.rotate] Note: if [interactiveFlags] + /// doesn't contain [InteractiveFlag.rotate] or [enableMultiFingerGestureRace] + /// is false then rotate cannot win + final double rotationThreshold; + + /// When [rotationThreshold] wins over [pinchZoomThreshold] and + /// [pinchMoveThreshold] then [rotationWinGestures] gestures will be used. By + /// default only [MultiFingerGesture.rotate] gesture will take effect see + /// [MultiFingerGesture] for custom settings + final int rotationWinGestures; + + /// Pinch Zoom threshold default is 0.5 Map starts to zoom when + /// [pinchZoomThreshold] has been achieved or another multi finger gesture + /// wins which allows [MultiFingerGesture.pinchZoom] Note: if + /// [interactiveFlags] doesn't contain [InteractiveFlag.pinchZoom] or + /// [enableMultiFingerGestureRace] is false then zoom cannot win + final double pinchZoomThreshold; + + /// When [pinchZoomThreshold] wins over [rotationThreshold] and + /// [pinchMoveThreshold] then [pinchZoomWinGestures] gestures will be used. By + /// default [MultiFingerGesture.pinchZoom] and [MultiFingerGesture.pinchMove] + /// gestures will take effect see [MultiFingerGesture] for custom settings + final int pinchZoomWinGestures; + + /// Pinch Move threshold default is 40.0 (note: this doesn't take any effect + /// on drag) Map starts to move when [pinchMoveThreshold] has been achieved or + /// another multi finger gesture wins which allows + /// [MultiFingerGesture.pinchMove] Note: if [interactiveFlags] doesn't contain + /// [InteractiveFlag.pinchMove] or [enableMultiFingerGestureRace] is false + /// then pinch move cannot win + final double pinchMoveThreshold; + + /// When [pinchMoveThreshold] wins over [rotationThreshold] and + /// [pinchZoomThreshold] then [pinchMoveWinGestures] gestures will be used. By + /// default [MultiFingerGesture.pinchMove] and [MultiFingerGesture.pinchZoom] + /// gestures will take effect see [MultiFingerGesture] for custom settings + final int pinchMoveWinGestures; + + /// If true then the map will scroll when the user uses the scroll wheel on + /// his mouse. This is supported on web and desktop, but might also work well + /// on Android. A [Listener] is used to capture the onPointerSignal events. + final bool enableScrollWheel; + final double scrollWheelVelocity; + + const InteractionOptions({ + required this.flags, + this.debugMultiFingerGestureWinner = false, + this.enableMultiFingerGestureRace = false, + this.rotationThreshold = 20.0, + this.rotationWinGestures = MultiFingerGesture.rotate, + this.pinchZoomThreshold = 0.5, + this.pinchZoomWinGestures = + MultiFingerGesture.pinchZoom | MultiFingerGesture.pinchMove, + this.pinchMoveThreshold = 40.0, + this.pinchMoveWinGestures = + MultiFingerGesture.pinchZoom | MultiFingerGesture.pinchMove, + this.enableScrollWheel = true, + this.scrollWheelVelocity = 0.005, + }); + + bool get dragEnabled => InteractiveFlag.hasDrag(flags); + bool get flingEnabled => InteractiveFlag.hasFlingAnimation(flags); + bool get doubleTapZoomEnabled => InteractiveFlag.hasDoubleTapZoom(flags); + bool get rotateEnabled => InteractiveFlag.hasRotate(flags); + bool get pinchZoomEnabled => InteractiveFlag.hasPinchZoom(flags); + bool get pinchMoveEnabled => InteractiveFlag.hasPinchMove(flags); + + @override + bool operator ==(Object other) => + other is InteractionOptions && + flags == other.flags && + debugMultiFingerGestureWinner == other.debugMultiFingerGestureWinner && + enableMultiFingerGestureRace == other.enableMultiFingerGestureRace && + rotationThreshold == other.rotationThreshold && + rotationWinGestures == other.rotationWinGestures && + pinchZoomThreshold == other.pinchZoomThreshold && + pinchZoomWinGestures == other.pinchZoomWinGestures && + pinchMoveThreshold == other.pinchMoveThreshold && + pinchMoveWinGestures == other.pinchMoveWinGestures && + enableScrollWheel == other.enableScrollWheel && + scrollWheelVelocity == other.scrollWheelVelocity; + + @override + int get hashCode => Object.hash( + flags, + debugMultiFingerGestureWinner, + enableMultiFingerGestureRace, + rotationThreshold, + rotationWinGestures, + pinchZoomThreshold, + pinchZoomWinGestures, + pinchMoveThreshold, + pinchMoveWinGestures, + enableScrollWheel, + scrollWheelVelocity, + ); +} diff --git a/lib/src/misc/frame_constraint.dart b/lib/src/misc/frame_constraint.dart index 8c40b9102..1e80f5f92 100644 --- a/lib/src/misc/frame_constraint.dart +++ b/lib/src/misc/frame_constraint.dart @@ -1,6 +1,8 @@ import 'dart:math' as math; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/src/geo/latlng_bounds.dart'; +import 'package:flutter_map/src/map/flutter_map_frame.dart'; +import 'package:flutter_map/src/misc/point.dart'; import 'package:latlong2/latlong.dart'; abstract class FrameConstraint { @@ -16,14 +18,14 @@ abstract class FrameConstraint { required LatLngBounds bounds, }) = ContainFrame._; - FlutterMapState? constrain(FlutterMapState mapState); + FlutterMapFrame? constrain(FlutterMapFrame mapFrame); } class UnconstrainedFrame extends FrameConstraint { const UnconstrainedFrame._(); @override - FlutterMapState constrain(FlutterMapState mapState) => mapState; + FlutterMapFrame constrain(FlutterMapFrame mapFrame) => mapFrame; } class ContainFrameCenter extends FrameConstraint { @@ -34,13 +36,13 @@ class ContainFrameCenter extends FrameConstraint { }); @override - FlutterMapState constrain(FlutterMapState mapState) => mapState.withPosition( + FlutterMapFrame constrain(FlutterMapFrame mapFrame) => mapFrame.withPosition( center: LatLng( - mapState.center.latitude.clamp( + mapFrame.center.latitude.clamp( bounds.south, bounds.north, ), - mapState.center.longitude.clamp( + mapFrame.center.longitude.clamp( bounds.west, bounds.east, ), @@ -57,14 +59,14 @@ class ContainFrame extends FrameConstraint { }); @override - FlutterMapState? constrain(FlutterMapState mapState) { - final testZoom = mapState.zoom; - final testCenter = mapState.center; + FlutterMapFrame? constrain(FlutterMapFrame mapFrame) { + final testZoom = mapFrame.zoom; + final testCenter = mapFrame.center; - final nePixel = mapState.project(bounds.northEast, testZoom); - final swPixel = mapState.project(bounds.southWest, testZoom); + final nePixel = mapFrame.project(bounds.northEast, testZoom); + final swPixel = mapFrame.project(bounds.southWest, testZoom); - final halfSize = mapState.size / 2; + final halfSize = mapFrame.size / 2; // Find the limits for the map center which would keep the frame within the // [latLngBounds]. @@ -77,16 +79,16 @@ class ContainFrame extends FrameConstraint { // stay within [latLngBounds]. if (leftOkCenter > rightOkCenter || topOkCenter > botOkCenter) return null; - final centerPix = mapState.project(testCenter, testZoom); + final centerPix = mapFrame.project(testCenter, testZoom); final newCenterPix = CustomPoint( centerPix.x.clamp(leftOkCenter, rightOkCenter), centerPix.y.clamp(topOkCenter, botOkCenter), ); - if (newCenterPix == centerPix) return mapState; + if (newCenterPix == centerPix) return mapFrame; - return mapState.withPosition( - center: mapState.unproject(newCenterPix, testZoom), + return mapFrame.withPosition( + center: mapFrame.unproject(newCenterPix, testZoom), ); } diff --git a/lib/src/misc/frame_fit.dart b/lib/src/misc/frame_fit.dart index 8c2ec175c..ca306993d 100644 --- a/lib/src/misc/frame_fit.dart +++ b/lib/src/misc/frame_fit.dart @@ -2,7 +2,7 @@ import 'dart:math' as math; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/map/flutter_map_state.dart'; +import 'package:flutter_map/src/map/flutter_map_frame.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; @@ -26,7 +26,7 @@ abstract class FrameFit { bool forceIntegerZoomLevel, }) = FitCoordinates; - FlutterMapState fit(FlutterMapState mapState); + FlutterMapFrame fit(FlutterMapFrame mapFrame); } class FitBounds extends FrameFit { @@ -49,56 +49,56 @@ class FitBounds extends FrameFit { }); @override - FlutterMapState fit(FlutterMapState mapState) { + FlutterMapFrame fit(FlutterMapFrame mapFrame) { final paddingTL = CustomPoint(padding.left, padding.top); final paddingBR = CustomPoint(padding.right, padding.bottom); final paddingTotalXY = paddingTL + paddingBR; - var newZoom = getBoundsZoom(mapState, paddingTotalXY); + var newZoom = getBoundsZoom(mapFrame, paddingTotalXY); newZoom = math.min(maxZoom, newZoom); final paddingOffset = (paddingBR - paddingTL) / 2; - final swPoint = mapState.project(bounds.southWest, newZoom); - final nePoint = mapState.project(bounds.northEast, newZoom); + final swPoint = mapFrame.project(bounds.southWest, newZoom); + final nePoint = mapFrame.project(bounds.northEast, newZoom); final CustomPoint projectedCenter; - if (mapState.rotation != 0.0) { - final swPointRotated = swPoint.rotate(-mapState.rotationRad); - final nePointRotated = nePoint.rotate(-mapState.rotationRad); + if (mapFrame.rotation != 0.0) { + final swPointRotated = swPoint.rotate(-mapFrame.rotationRad); + final nePointRotated = nePoint.rotate(-mapFrame.rotationRad); final centerRotated = (swPointRotated + nePointRotated) / 2 + paddingOffset; - projectedCenter = centerRotated.rotate(mapState.rotationRad); + projectedCenter = centerRotated.rotate(mapFrame.rotationRad); } else { projectedCenter = (swPoint + nePoint) / 2 + paddingOffset; } - final center = mapState.unproject(projectedCenter, newZoom); - return mapState.withPosition( + final center = mapFrame.unproject(projectedCenter, newZoom); + return mapFrame.withPosition( center: center, zoom: newZoom, ); } double getBoundsZoom( - FlutterMapState mapState, + FlutterMapFrame mapFrame, CustomPoint pixelPadding, ) { - final min = mapState.minZoom ?? 0.0; - final max = mapState.maxZoom ?? double.infinity; + final min = mapFrame.minZoom ?? 0.0; + final max = mapFrame.maxZoom ?? double.infinity; final nw = bounds.northWest; final se = bounds.southEast; - var size = mapState.nonRotatedSize - pixelPadding; + var size = mapFrame.nonRotatedSize - pixelPadding; // Prevent negative size which results in NaN zoom value later on in the calculation size = CustomPoint(math.max(0, size.x), math.max(0, size.y)); var boundsSize = Bounds( - mapState.project(se, mapState.zoom), - mapState.project(nw, mapState.zoom), + mapFrame.project(se, mapFrame.zoom), + mapFrame.project(nw, mapFrame.zoom), ).size; - if (mapState.rotation != 0.0) { - final cosAngle = math.cos(mapState.rotationRad).abs(); - final sinAngle = math.sin(mapState.rotationRad).abs(); + if (mapFrame.rotation != 0.0) { + final cosAngle = math.cos(mapFrame.rotationRad).abs(); + final sinAngle = math.sin(mapFrame.rotationRad).abs(); boundsSize = CustomPoint( (boundsSize.x * cosAngle) + (boundsSize.y * sinAngle), (boundsSize.y * cosAngle) + (boundsSize.x * sinAngle), @@ -109,7 +109,7 @@ class FitBounds extends FrameFit { final scaleY = size.y / boundsSize.y; final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); - var boundsZoom = mapState.getScaleZoom(scale, mapState.zoom); + var boundsZoom = mapFrame.getScaleZoom(scale, mapFrame.zoom); if (forceIntegerZoomLevel) { boundsZoom = @@ -140,21 +140,21 @@ class FitCoordinates extends FrameFit { }); @override - FlutterMapState fit(FlutterMapState mapState) { + FlutterMapFrame fit(FlutterMapFrame mapFrame) { final paddingTL = CustomPoint(padding.left, padding.top); final paddingBR = CustomPoint(padding.right, padding.bottom); final paddingTotalXY = paddingTL + paddingBR; - var newZoom = getCoordinatesZoom(mapState, paddingTotalXY); + var newZoom = getCoordinatesZoom(mapFrame, paddingTotalXY); newZoom = math.min(maxZoom, newZoom); final projectedPoints = [ - for (final coord in coordinates) mapState.project(coord, newZoom) + for (final coord in coordinates) mapFrame.project(coord, newZoom) ]; final rotatedPoints = - projectedPoints.map((point) => point.rotate(-mapState.rotationRad)); + projectedPoints.map((point) => point.rotate(-mapFrame.rotationRad)); final rotatedBounds = Bounds.containing(rotatedPoints); @@ -163,32 +163,32 @@ class FitCoordinates extends FrameFit { final rotatedNewCenter = rotatedBounds.center + paddingOffset; // Undo the rotation - final unrotatedNewCenter = rotatedNewCenter.rotate(mapState.rotationRad); + final unrotatedNewCenter = rotatedNewCenter.rotate(mapFrame.rotationRad); - final newCenter = mapState.unproject(unrotatedNewCenter, newZoom); + final newCenter = mapFrame.unproject(unrotatedNewCenter, newZoom); - return mapState.withPosition( + return mapFrame.withPosition( center: newCenter, zoom: newZoom, ); } double getCoordinatesZoom( - FlutterMapState mapState, + FlutterMapFrame mapFrame, CustomPoint pixelPadding, ) { - final min = mapState.minZoom ?? 0.0; - final max = mapState.maxZoom ?? double.infinity; - var size = mapState.nonRotatedSize - pixelPadding; + final min = mapFrame.minZoom ?? 0.0; + final max = mapFrame.maxZoom ?? double.infinity; + var size = mapFrame.nonRotatedSize - pixelPadding; // Prevent negative size which results in NaN zoom value later on in the calculation size = CustomPoint(math.max(0, size.x), math.max(0, size.y)); final projectedPoints = [ - for (final coord in coordinates) mapState.project(coord) + for (final coord in coordinates) mapFrame.project(coord) ]; final rotatedPoints = - projectedPoints.map((point) => point.rotate(-mapState.rotationRad)); + projectedPoints.map((point) => point.rotate(-mapFrame.rotationRad)); final rotatedBounds = Bounds.containing(rotatedPoints); final boundsSize = rotatedBounds.size; @@ -197,7 +197,7 @@ class FitCoordinates extends FrameFit { final scaleY = size.y / boundsSize.y; final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); - var newZoom = mapState.getScaleZoom(scale, mapState.zoom); + var newZoom = mapFrame.getScaleZoom(scale, mapFrame.zoom); if (forceIntegerZoomLevel) { newZoom = inside ? newZoom.ceilToDouble() : newZoom.floorToDouble(); } diff --git a/test/flutter_map_controller_test.dart b/test/flutter_map_controller_test.dart index 38f93a31e..0aaa88fd5 100644 --- a/test/flutter_map_controller_test.dart +++ b/test/flutter_map_controller_test.dart @@ -26,10 +26,10 @@ void main() { controller.fitFrame(frameConstraint); await tester.pump(); - final mapState = controller.mapState; - expect(mapState.visibleBounds, equals(expectedBounds)); - expect(mapState.center, equals(expectedCenter)); - expect(mapState.zoom, equals(expectedZoom)); + final mapFrame = controller.mapFrame; + expect(mapFrame.visibleBounds, equals(expectedBounds)); + expect(mapFrame.center, equals(expectedCenter)); + expect(mapFrame.zoom, equals(expectedZoom)); } { @@ -46,10 +46,10 @@ void main() { controller.fitFrame(frameConstraint); await tester.pump(); - final mapState = controller.mapState; - expect(mapState.visibleBounds, equals(expectedBounds)); - expect(mapState.center, equals(expectedCenter)); - expect(mapState.zoom, equals(expectedZoom)); + final mapFrame = controller.mapFrame; + expect(mapFrame.visibleBounds, equals(expectedBounds)); + expect(mapFrame.center, equals(expectedCenter)); + expect(mapFrame.zoom, equals(expectedZoom)); } { @@ -67,10 +67,10 @@ void main() { controller.fitFrame(frameConstraint); await tester.pump(); - final mapState = controller.mapState; - expect(mapState.visibleBounds, equals(expectedBounds)); - expect(mapState.center, equals(expectedCenter)); - expect(mapState.zoom, equals(expectedZoom)); + final mapFrame = controller.mapFrame; + expect(mapFrame.visibleBounds, equals(expectedBounds)); + expect(mapFrame.center, equals(expectedCenter)); + expect(mapFrame.zoom, equals(expectedZoom)); } { @@ -88,10 +88,10 @@ void main() { controller.fitFrame(frameConstraint); await tester.pump(); - final mapState = controller.mapState; - expect(mapState.visibleBounds, equals(expectedBounds)); - expect(mapState.center, equals(expectedCenter)); - expect(mapState.zoom, equals(expectedZoom)); + final mapFrame = controller.mapFrame; + expect(mapFrame.visibleBounds, equals(expectedBounds)); + expect(mapFrame.center, equals(expectedCenter)); + expect(mapFrame.zoom, equals(expectedZoom)); } }); @@ -116,30 +116,30 @@ void main() { controller.fitFrame(frameConstraint); await tester.pump(); expect( - controller.mapState.visibleBounds.northWest.latitude, + controller.mapFrame.visibleBounds.northWest.latitude, moreOrLessEquals(expectedBounds.northWest.latitude), ); expect( - controller.mapState.visibleBounds.northWest.longitude, + controller.mapFrame.visibleBounds.northWest.longitude, moreOrLessEquals(expectedBounds.northWest.longitude), ); expect( - controller.mapState.visibleBounds.southEast.latitude, + controller.mapFrame.visibleBounds.southEast.latitude, moreOrLessEquals(expectedBounds.southEast.latitude), ); expect( - controller.mapState.visibleBounds.southEast.longitude, + controller.mapFrame.visibleBounds.southEast.longitude, moreOrLessEquals(expectedBounds.southEast.longitude), ); expect( - controller.mapState.center.latitude, + controller.mapFrame.center.latitude, moreOrLessEquals(expectedCenter.latitude), ); expect( - controller.mapState.center.longitude, + controller.mapFrame.center.longitude, moreOrLessEquals(expectedCenter.longitude), ); - expect(controller.mapState.zoom, moreOrLessEquals(expectedZoom)); + expect(controller.mapFrame.zoom, moreOrLessEquals(expectedZoom)); } // Tests with no padding @@ -685,14 +685,14 @@ void main() { controller.fitFrame(fitCoordinates); await tester.pump(); expect( - controller.mapState.center.latitude, + controller.mapFrame.center.latitude, moreOrLessEquals(expectedCenter.latitude), ); expect( - controller.mapState.center.longitude, + controller.mapFrame.center.longitude, moreOrLessEquals(expectedCenter.longitude), ); - expect(controller.mapState.zoom, moreOrLessEquals(expectedZoom)); + expect(controller.mapFrame.zoom, moreOrLessEquals(expectedZoom)); } FitCoordinates fitCoordinates({ diff --git a/test/flutter_map_test.dart b/test/flutter_map_test.dart index ef8baac1d..8af7f0b48 100644 --- a/test/flutter_map_test.dart +++ b/test/flutter_map_test.dart @@ -43,7 +43,7 @@ void main() { children: [ Builder( builder: (context) { - final _ = FlutterMapState.of(context); + final _ = FlutterMapFrame.of(context); builds++; return const SizedBox.shrink(); }, diff --git a/test/misc/frame_constraint_test.dart b/test/misc/frame_constraint_test.dart index 755ac3da6..28f002b08 100644 --- a/test/misc/frame_constraint_test.dart +++ b/test/misc/frame_constraint_test.dart @@ -6,14 +6,14 @@ void main() { group('FrameConstraint', () { group('contain', () { test('rotated', () { - final mapBoundary = FrameConstraint.contain( + final mapConstraint = FrameConstraint.contain( bounds: LatLngBounds( const LatLng(-90, -180), const LatLng(90, 180), ), ); - final mapState = FlutterMapState( + final mapFrame = FlutterMapFrame( crs: const Epsg3857(), center: const LatLng(-90, -180), zoom: 1, @@ -21,7 +21,7 @@ void main() { nonRotatedSize: const CustomPoint(200, 300), ); - final clamped = mapBoundary.constrain(mapState)!; + final clamped = mapConstraint.constrain(mapFrame)!; expect(clamped.zoom, 1); expect(clamped.center.latitude, closeTo(-48.562, 0.001)); From 66e508db91176aa89a201b6f1e9cfbf90112f7cf Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Thu, 15 Jun 2023 13:43:55 +0200 Subject: [PATCH 19/46] Use InhertiedModel instead of InheritedWidget --- .../tile_layer/tile_update_transformer.dart | 2 +- lib/src/map/flutter_map_frame.dart | 10 +-- lib/src/map/flutter_map_inherited_model.dart | 78 +++++++++++++++++++ lib/src/map/flutter_map_state_container.dart | 4 +- .../flutter_map_state_inherited_widget.dart | 24 ------ lib/src/map/map_controller.dart | 7 +- lib/src/map/options.dart | 7 +- 7 files changed, 92 insertions(+), 40 deletions(-) create mode 100644 lib/src/map/flutter_map_inherited_model.dart delete mode 100644 lib/src/map/flutter_map_state_inherited_widget.dart diff --git a/lib/src/layer/tile_layer/tile_update_transformer.dart b/lib/src/layer/tile_layer/tile_update_transformer.dart index c75c80373..da8dcfb4d 100644 --- a/lib/src/layer/tile_layer/tile_update_transformer.dart +++ b/lib/src/layer/tile_layer/tile_update_transformer.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:flutter_map/src/misc/private/util.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_layer.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_update_event.dart'; +import 'package:flutter_map/src/misc/private/util.dart'; /// Defines which [TileUpdateEvent]s should cause which [TileUpdateEvent]s and /// when diff --git a/lib/src/map/flutter_map_frame.dart b/lib/src/map/flutter_map_frame.dart index 0ca9f7190..d33f387a6 100644 --- a/lib/src/map/flutter_map_frame.dart +++ b/lib/src/map/flutter_map_frame.dart @@ -3,7 +3,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_map/src/geo/crs.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; +import 'package:flutter_map/src/map/flutter_map_inherited_model.dart'; import 'package:flutter_map/src/map/options.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; @@ -33,10 +33,10 @@ class FlutterMapFrame { Bounds? _pixelBounds; LatLngBounds? _bounds; CustomPoint? _pixelOrigin; + double? _rotationRad; - static FlutterMapFrame? maybeOf(BuildContext context) => context - .dependOnInheritedWidgetOfExactType() - ?.frame; + static FlutterMapFrame? maybeOf(BuildContext context) => + FlutterMapInheritedModel.maybeFrameOf(context); static FlutterMapFrame of(BuildContext context) => maybeOf(context) ?? @@ -173,7 +173,7 @@ class FlutterMapFrame { return CustomPoint(width, height); } - double get rotationRad => degToRadian(rotation); + double get rotationRad => _rotationRad ??= degToRadian(rotation); CustomPoint project(LatLng latlng, [double? zoom]) => crs.latLngToPoint(latlng, zoom ?? this.zoom); diff --git a/lib/src/map/flutter_map_inherited_model.dart b/lib/src/map/flutter_map_inherited_model.dart new file mode 100644 index 000000000..781db0c83 --- /dev/null +++ b/lib/src/map/flutter_map_inherited_model.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/src/map/flutter_map_frame.dart'; +import 'package:flutter_map/src/map/map_controller.dart'; +import 'package:flutter_map/src/map/options.dart'; + +class FlutterMapInheritedModel extends InheritedModel<_FlutterMapAspect> { + final FlutterMapData data; + + FlutterMapInheritedModel({ + super.key, + required FlutterMapFrame frame, + required MapController controller, + required MapOptions options, + required super.child, + }) : data = FlutterMapData( + frame: frame, + controller: controller, + options: options, + ); + + static FlutterMapData? _maybeOf( + BuildContext context, [ + _FlutterMapAspect? aspect, + ]) => + InheritedModel.inheritFrom(context, + aspect: aspect) + ?.data; + + static FlutterMapFrame? maybeFrameOf(BuildContext context) => + _maybeOf(context, _FlutterMapAspect.frame)?.frame; + + static MapController? maybeControllerOf(BuildContext context) => + _maybeOf(context, _FlutterMapAspect.controller)?.controller; + + static MapOptions? maybeOptionsOf(BuildContext context) => + _maybeOf(context, _FlutterMapAspect.options)?.options; + + @override + bool updateShouldNotify(FlutterMapInheritedModel oldWidget) => + data != oldWidget.data; + + @override + bool updateShouldNotifyDependent( + covariant FlutterMapInheritedModel oldWidget, Set dependencies) { + for (final dependency in dependencies) { + if (dependency is _FlutterMapAspect) { + switch (dependency) { + case _FlutterMapAspect.frame: + if (data.frame != oldWidget.data.frame) return true; + case _FlutterMapAspect.controller: + if (data.controller != oldWidget.data.controller) return true; + case _FlutterMapAspect.options: + if (data.options != oldWidget.data.options) return true; + } + } + } + + return false; + } +} + +class FlutterMapData { + final FlutterMapFrame frame; + final MapController controller; + final MapOptions options; + + const FlutterMapData({ + required this.frame, + required this.controller, + required this.options, + }); +} + +enum _FlutterMapAspect { + frame, + controller, + options; +} diff --git a/lib/src/map/flutter_map_state_container.dart b/lib/src/map/flutter_map_state_container.dart index 822db3957..899744342 100644 --- a/lib/src/map/flutter_map_state_container.dart +++ b/lib/src/map/flutter_map_state_container.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/gestures/flutter_map_interactive_viewer.dart'; +import 'package:flutter_map/src/map/flutter_map_inherited_model.dart'; import 'package:flutter_map/src/map/flutter_map_internal_controller.dart'; -import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; class FlutterMapStateContainer extends State { @@ -56,7 +56,7 @@ class FlutterMapStateContainer extends State { return FlutterMapInteractiveViewer( controller: _flutterMapInternalController, - builder: (context, mapState) => MapStateInheritedWidget( + builder: (context, mapState) => FlutterMapInheritedModel( controller: _mapController, options: mapState.options, frame: mapState.mapFrame, diff --git a/lib/src/map/flutter_map_state_inherited_widget.dart b/lib/src/map/flutter_map_state_inherited_widget.dart deleted file mode 100644 index 09eb3271b..000000000 --- a/lib/src/map/flutter_map_state_inherited_widget.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map/src/map/flutter_map_frame.dart'; -import 'package:flutter_map/src/map/map_controller.dart'; -import 'package:flutter_map/src/map/options.dart'; - -class MapStateInheritedWidget extends InheritedWidget { - const MapStateInheritedWidget({ - super.key, - required this.frame, - required this.controller, - required this.options, - required super.child, - }); - - final FlutterMapFrame frame; - final MapController controller; - final MapOptions options; - - @override - bool updateShouldNotify(MapStateInheritedWidget oldWidget) => - !identical(frame, oldWidget.frame) || - !identical(controller, oldWidget.controller) || - !identical(options, oldWidget.options); -} diff --git a/lib/src/map/map_controller.dart b/lib/src/map/map_controller.dart index 16c65eb9d..5010c025d 100644 --- a/lib/src/map/map_controller.dart +++ b/lib/src/map/map_controller.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/map/flutter_map_frame.dart'; -import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; +import 'package:flutter_map/src/map/flutter_map_inherited_model.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; import 'package:latlong2/latlong.dart'; @@ -23,9 +23,8 @@ abstract class MapController { /// Factory constructor redirects to underlying implementation's constructor. factory MapController() => MapControllerImpl(); - static MapController? maybeOf(BuildContext context) => context - .dependOnInheritedWidgetOfExactType() - ?.controller; + static MapController? maybeOf(BuildContext context) => + FlutterMapInheritedModel.maybeControllerOf(context); static MapController of(BuildContext context) => maybeOf(context) ?? diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index b2f3d43e7..6f6ee191f 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -5,7 +5,7 @@ import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/gestures/interactive_flag.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; -import 'package:flutter_map/src/map/flutter_map_state_inherited_widget.dart'; +import 'package:flutter_map/src/map/flutter_map_inherited_model.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; import 'package:flutter_map/src/misc/frame_constraint.dart'; import 'package:flutter_map/src/misc/frame_fit.dart'; @@ -209,9 +209,8 @@ class MapOptions { assert(pinchZoomThreshold >= 0.0), assert(pinchMoveThreshold >= 0.0); - static MapOptions? maybeOf(BuildContext context) => context - .dependOnInheritedWidgetOfExactType() - ?.options; + static MapOptions? maybeOf(BuildContext context) => + FlutterMapInheritedModel.maybeOptionsOf(context); static MapOptions of(BuildContext context) => maybeOf(context) ?? From ced3a03b2a8f668323d823f43a29b709603b9aa2 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Thu, 15 Jun 2023 13:48:42 +0200 Subject: [PATCH 20/46] Remove FitCoordinates' inside parameter because fitting inside a set of coordinates doesn't have an unambiguous meaning --- lib/src/misc/frame_fit.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/src/misc/frame_fit.dart b/lib/src/misc/frame_fit.dart index ca306993d..952fbc90a 100644 --- a/lib/src/misc/frame_fit.dart +++ b/lib/src/misc/frame_fit.dart @@ -22,7 +22,6 @@ abstract class FrameFit { required List coordinates, EdgeInsets padding, double maxZoom, - bool inside, bool forceIntegerZoomLevel, }) = FitCoordinates; @@ -124,7 +123,6 @@ class FitCoordinates extends FrameFit { final List coordinates; final EdgeInsets padding; final double maxZoom; - final bool inside; /// By default calculations will return fractional zoom levels. /// If this parameter is set to [true] fractional zoom levels will be round @@ -135,7 +133,6 @@ class FitCoordinates extends FrameFit { required this.coordinates, this.padding = EdgeInsets.zero, this.maxZoom = 17.0, - this.inside = false, this.forceIntegerZoomLevel = false, }); @@ -195,11 +192,11 @@ class FitCoordinates extends FrameFit { final scaleX = size.x / boundsSize.x; final scaleY = size.y / boundsSize.y; - final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); + final scale = math.min(scaleX, scaleY); var newZoom = mapFrame.getScaleZoom(scale, mapFrame.zoom); if (forceIntegerZoomLevel) { - newZoom = inside ? newZoom.ceilToDouble() : newZoom.floorToDouble(); + newZoom = newZoom.floorToDouble(); } return math.max(min, math.min(max, newZoom)); From 24263d286cbbeb9e45a2dc169f840eb06d4f36fb Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Thu, 15 Jun 2023 17:05:12 +0200 Subject: [PATCH 21/46] Fix event names appearing minified when running web release build --- example/lib/pages/interactive_test_page.dart | 49 +++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/example/lib/pages/interactive_test_page.dart b/example/lib/pages/interactive_test_page.dart index 884319653..883b2af73 100644 --- a/example/lib/pages/interactive_test_page.dart +++ b/example/lib/pages/interactive_test_page.dart @@ -28,7 +28,7 @@ class _InteractiveTestPageState extends State { void onMapEvent(MapEvent mapEvent) { if (mapEvent is! MapEventMove && mapEvent is! MapEventRotate) { // do not flood console with move and rotate events - debugPrint(mapEvent.toString()); + debugPrint(_eventName(mapEvent)); } setState(() { @@ -135,7 +135,7 @@ class _InteractiveTestPageState extends State { padding: const EdgeInsets.only(top: 8, bottom: 8), child: Center( child: Text( - 'Current event: ${_latestEvent?.runtimeType ?? "none"}\nSource: ${_latestEvent?.source ?? "none"}', + 'Current event: ${_eventName(_latestEvent)}\nSource: ${_latestEvent?.source.name ?? "none"}', textAlign: TextAlign.center, ), ), @@ -164,4 +164,49 @@ class _InteractiveTestPageState extends State { ), ); } + + String _eventName(MapEvent? event) { + switch (event) { + case MapEventTap(): + return 'MapEventTap'; + case MapEventSecondaryTap(): + return 'MapEventSecondaryTap'; + case MapEventLongPress(): + return 'MapEventLongPress'; + case MapEventMove(): + return 'MapEventMove'; + case MapEventMoveStart(): + return 'MapEventMoveStart'; + case MapEventMoveEnd(): + return 'MapEventMoveEnd'; + case MapEventFlingAnimation(): + return 'MapEventFlingAnimation'; + case MapEventFlingAnimationNotStarted(): + return 'MapEventFlingAnimationNotStarted'; + case MapEventFlingAnimationStart(): + return 'MapEventFlingAnimationStart'; + case MapEventFlingAnimationEnd(): + return 'MapEventFlingAnimationEnd'; + case MapEventDoubleTapZoom(): + return 'MapEventDoubleTapZoom'; + case MapEventScrollWheelZoom(): + return 'MapEventScrollWheelZoom'; + case MapEventDoubleTapZoomStart(): + return 'MapEventDoubleTapZoomStart'; + case MapEventDoubleTapZoomEnd(): + return 'MapEventDoubleTapZoomEnd'; + case MapEventRotate(): + return 'MapEventRotate'; + case MapEventRotateStart(): + return 'MapEventRotateStart'; + case MapEventRotateEnd(): + return 'MapEventRotateEnd'; + case MapEventNonRotatedSizeChange(): + return 'MapEventNonRotatedSizeChange'; + case null: + return 'null'; + default: + return 'Unknown'; + } + } } From f5f573542d081f1c50d4f0d379e9048a33ff4868 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Thu, 15 Jun 2023 17:05:40 +0200 Subject: [PATCH 22/46] Set constraints that match the old adaptive constraints These old adaptive constraints did not prevent the map from going outside of the specified bounds, they stopped the center of the map from going outside of those bounds. This commit sets the constraints appropriately. --- example/lib/pages/offline_map.dart | 2 +- example/lib/pages/sliding_map.dart | 39 +----------------------------- 2 files changed, 2 insertions(+), 39 deletions(-) diff --git a/example/lib/pages/offline_map.dart b/example/lib/pages/offline_map.dart index 5755fb78a..a01265020 100644 --- a/example/lib/pages/offline_map.dart +++ b/example/lib/pages/offline_map.dart @@ -28,7 +28,7 @@ class OfflineMapPage extends StatelessWidget { initialCenter: const LatLng(56.704173, 11.543808), minZoom: 12, maxZoom: 14, - frameConstraint: FrameConstraint.contain( + frameConstraint: FrameConstraint.containCenter( bounds: LatLngBounds( const LatLng(56.7378, 11.6644), const LatLng(56.6877, 11.5089), diff --git a/example/lib/pages/sliding_map.dart b/example/lib/pages/sliding_map.dart index d176a91fa..dd7083910 100644 --- a/example/lib/pages/sliding_map.dart +++ b/example/lib/pages/sliding_map.dart @@ -31,7 +31,7 @@ class SlidingMapPage extends StatelessWidget { minZoom: 12, maxZoom: 14, initialZoom: 13, - frameConstraint: FrameConstraint.contain( + frameConstraint: FrameConstraint.containCenter( bounds: LatLngBounds(northEast, southWest), ), ), @@ -41,31 +41,6 @@ class SlidingMapPage extends StatelessWidget { maxZoom: 14, urlTemplate: 'assets/map/anholt_osmbright/{z}/{x}/{y}.png', ), - MarkerLayer( - anchorPos: AnchorPos.align(AnchorAlign.top), - markers: [ - Marker( - point: northEast, - builder: (context) => _cornerMarker(Icons.north_east), - anchorPos: AnchorPos.align(AnchorAlign.bottomLeft), - ), - Marker( - point: LatLng(southWest.latitude, northEast.longitude), - builder: (context) => _cornerMarker(Icons.south_east), - anchorPos: AnchorPos.align(AnchorAlign.topLeft), - ), - Marker( - point: southWest, - builder: (context) => _cornerMarker(Icons.south_west), - anchorPos: AnchorPos.align(AnchorAlign.topRight), - ), - Marker( - point: LatLng(northEast.latitude, southWest.longitude), - builder: (context) => _cornerMarker(Icons.north_west), - anchorPos: AnchorPos.align(AnchorAlign.bottomRight), - ), - ], - ) ], ), ), @@ -74,16 +49,4 @@ class SlidingMapPage extends StatelessWidget { ), ); } - - Widget _cornerMarker(IconData iconData) { - return Container( - decoration: BoxDecoration( - color: Colors.red, - border: Border.all(color: Colors.black), - ), - width: 30, - height: 30, - child: Icon(iconData), - ); - } } From 3cbea2122966c64e6f758311dee518323d8c7f0f Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Thu, 15 Jun 2023 17:21:35 +0200 Subject: [PATCH 23/46] Make documentation easier to read --- lib/src/gestures/interactive_flag.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/src/gestures/interactive_flag.dart b/lib/src/gestures/interactive_flag.dart index 25c967a91..7cb621353 100644 --- a/lib/src/gestures/interactive_flag.dart +++ b/lib/src/gestures/interactive_flag.dart @@ -3,11 +3,14 @@ /// disable all events /// /// If you want mix interactions for example drag and rotate interactions then -/// you have two options A.) add you own flags: [InteractiveFlag.drag] | -/// [InteractiveFlag.rotate] B.) remove unnecessary flags from all: -/// [InteractiveFlag.all] & ~[InteractiveFlag.flingAnimation] & -/// ~[InteractiveFlag.pinchMove] & ~[InteractiveFlag.pinchZoom] & -/// ~[InteractiveFlag.doubleTapZoom] +/// you have two options: +/// a. Add you own flags: [InteractiveFlag.drag] | [InteractiveFlag.rotate] +/// b. Remove unnecessary flags from all: +/// [InteractiveFlag.all] & +/// ~[InteractiveFlag.flingAnimation] & +/// ~[InteractiveFlag.pinchMove] & +/// ~[InteractiveFlag.pinchZoom] & +/// ~[InteractiveFlag.doubleTapZoom] class InteractiveFlag { const InteractiveFlag._(); static const int all = From b7b5bf2a5b6bc98f5a5950533fb4cb07bf398c35 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Thu, 15 Jun 2023 17:22:54 +0200 Subject: [PATCH 24/46] Use flags/options from InteractionOptions not the old deprecated values, unless InteractiveOptions is not provided --- .../flutter_map_interactive_viewer.dart | 47 ++--- lib/src/map/options.dart | 194 ++++++++++-------- 2 files changed, 129 insertions(+), 112 deletions(-) diff --git a/lib/src/gestures/flutter_map_interactive_viewer.dart b/lib/src/gestures/flutter_map_interactive_viewer.dart index ccdf2b337..31d134f33 100644 --- a/lib/src/gestures/flutter_map_interactive_viewer.dart +++ b/lib/src/gestures/flutter_map_interactive_viewer.dart @@ -75,6 +75,7 @@ class FlutterMapInteractiveViewerState FlutterMapFrame get _mapFrame => widget.controller.mapFrame; MapOptions get _options => widget.controller.options; + InteractionOptions get _interactionOptions => _options.interactionOptions; @override void initState() { @@ -102,9 +103,8 @@ class FlutterMapInteractiveViewerState void didChangeDependencies() { // _createGestures uses a MediaQuery to determine gesture settings. This // will update those gesture settings if they change. - _gestures = _createGestures( - dragEnabled: InteractiveFlag.hasDrag(_options.interactiveFlags), + dragEnabled: InteractiveFlag.hasDrag(_interactionOptions.flags), ); super.didChangeDependencies(); } @@ -259,7 +259,7 @@ class FlutterMapInteractiveViewerState onLongPress: _handleLongPress, onDoubleTap: _handleDoubleTap, doubleTapDelay: - InteractiveFlag.hasDoubleTapZoom(_options.interactiveFlags) + InteractiveFlag.hasDoubleTapZoom(_interactionOptions.flags) ? null : Duration.zero, child: RawGestureDetector( @@ -307,7 +307,7 @@ class FlutterMapInteractiveViewerState void _onPointerSignal(PointerSignalEvent pointerSignal) { // Handle mouse scroll events if the enableScrollWheel parameter is enabled if (pointerSignal is PointerScrollEvent && - _options.enableScrollWheel && + _interactionOptions.enableScrollWheel && pointerSignal.scrollDelta.dy != 0) { // Prevent scrolling of parent/child widgets simultaneously. See // [PointerSignalResolver] documentation for more information. @@ -318,7 +318,8 @@ class FlutterMapInteractiveViewerState final minZoom = _options.minZoom ?? 0.0; final maxZoom = _options.maxZoom ?? double.infinity; final newZoom = (_mapFrame.zoom - - pointerSignal.scrollDelta.dy * _options.scrollWheelVelocity) + pointerSignal.scrollDelta.dy * + _interactionOptions.scrollWheelVelocity) .clamp(minZoom, maxZoom); // Calculate offset of mouse cursor from viewport center final newCenter = _mapFrame.focusedZoomCenter( @@ -410,7 +411,7 @@ class FlutterMapInteractiveViewerState final currentRotation = radianToDeg(details.rotation); if (_dragMode) { _handleScaleDragUpdate(details); - } else if (InteractiveFlag.hasMultiFinger(_options.interactiveFlags)) { + } else if (InteractiveFlag.hasMultiFinger(_interactionOptions.flags)) { _handleScaleMultiFingerUpdate(details, currentRotation); } @@ -422,7 +423,7 @@ class FlutterMapInteractiveViewerState void _handleScaleDragUpdate(ScaleUpdateDetails details) { const eventSource = MapEventSource.onDrag; - if (InteractiveFlag.hasDrag(_options.interactiveFlags)) { + if (InteractiveFlag.hasDrag(_interactionOptions.flags)) { if (!_dragStarted) { // We could emit start event at [handleScaleStart], however it is // possible drag will be disabled during ongoing drag then @@ -444,11 +445,11 @@ class FlutterMapInteractiveViewerState ScaleUpdateDetails details, double currentRotation, ) { - final hasGestureRace = _options.enableMultiFingerGestureRace; + final hasGestureRace = _interactionOptions.enableMultiFingerGestureRace; if (hasGestureRace && _gestureWinner == MultiFingerGesture.none) { final gestureWinner = _determineMultiFingerGestureWinner( - _options.rotationThreshold, + _interactionOptions.rotationThreshold, currentRotation, details.scale, details.localFocalPoint, @@ -464,16 +465,16 @@ class FlutterMapInteractiveViewerState final gestures = _getMultiFingerGestureFlags(_options.interactionOptions); final hasPinchZoom = - InteractiveFlag.hasPinchZoom(_options.interactiveFlags) && + InteractiveFlag.hasPinchZoom(_interactionOptions.flags) && MultiFingerGesture.hasPinchZoom(gestures); final hasPinchMove = - InteractiveFlag.hasPinchMove(_options.interactiveFlags) && + InteractiveFlag.hasPinchMove(_interactionOptions.flags) && MultiFingerGesture.hasPinchMove(gestures); if (hasPinchZoom || hasPinchMove) { _handleScalePinchZoomAndMove(details, hasPinchZoom, hasPinchMove); } - if (InteractiveFlag.hasRotate(_options.interactiveFlags) && + if (InteractiveFlag.hasRotate(_interactionOptions.flags) && MultiFingerGesture.hasRotate(gestures)) { _handleScalePinchRotate(details, currentRotation); } @@ -584,23 +585,23 @@ class FlutterMapInteractiveViewerState int? _determineMultiFingerGestureWinner(double rotationThreshold, double currentRotation, double scale, Offset focalOffset) { final int winner; - if (InteractiveFlag.hasPinchZoom(_options.interactiveFlags) && + if (InteractiveFlag.hasPinchZoom(_interactionOptions.flags) && (_getZoomForScale(_mapZoomStart, scale) - _mapZoomStart).abs() >= - _options.pinchZoomThreshold) { - if (_options.debugMultiFingerGestureWinner) { + _interactionOptions.pinchZoomThreshold) { + if (_interactionOptions.debugMultiFingerGestureWinner) { debugPrint('Multi Finger Gesture winner: Pinch Zoom'); } winner = MultiFingerGesture.pinchZoom; - } else if (InteractiveFlag.hasRotate(_options.interactiveFlags) && + } else if (InteractiveFlag.hasRotate(_interactionOptions.flags) && currentRotation.abs() >= rotationThreshold) { - if (_options.debugMultiFingerGestureWinner) { + if (_interactionOptions.debugMultiFingerGestureWinner) { debugPrint('Multi Finger Gesture winner: Rotate'); } winner = MultiFingerGesture.rotate; - } else if (InteractiveFlag.hasPinchMove(_options.interactiveFlags) && + } else if (InteractiveFlag.hasPinchMove(_interactionOptions.flags) && (_focalStartLocal - focalOffset).distance >= - _options.pinchMoveThreshold) { - if (_options.debugMultiFingerGestureWinner) { + _interactionOptions.pinchMoveThreshold) { + if (_interactionOptions.debugMultiFingerGestureWinner) { debugPrint('Multi Finger Gesture winner: Pinch Move'); } winner = MultiFingerGesture.pinchMove; @@ -628,7 +629,7 @@ class FlutterMapInteractiveViewerState } final hasFling = - InteractiveFlag.hasFlingAnimation(_options.interactiveFlags); + InteractiveFlag.hasFlingAnimation(_interactionOptions.flags); final magnitude = details.velocity.pixelsPerSecond.distance; if (magnitude < _kMinFlingVelocity || !hasFling) { @@ -705,7 +706,7 @@ class FlutterMapInteractiveViewerState _closeFlingAnimationController(MapEventSource.doubleTap); _closeDoubleTapController(MapEventSource.doubleTap); - if (InteractiveFlag.hasDoubleTapZoom(_options.interactiveFlags)) { + if (InteractiveFlag.hasDoubleTapZoom(_interactionOptions.flags)) { final newZoom = _getZoomForScale(_mapFrame.zoom, 2); final newCenter = _mapFrame.focusedZoomCenter( tapPosition.relative!.toCustomPoint(), @@ -764,7 +765,7 @@ class FlutterMapInteractiveViewerState void _handleDoubleTapHold(ScaleUpdateDetails details) { _doubleTapHoldMaxDelay?.cancel(); - final flags = _options.interactiveFlags; + final flags = _interactionOptions.flags; if (InteractiveFlag.hasPinchZoom(flags)) { final verticalOffset = (_focalStartLocal - details.localFocalPoint).dy; final newZoom = _mapZoomStart - verticalOffset / 360 * _mapFrame.zoom; diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index 6f6ee191f..f14e04f29 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -56,68 +56,22 @@ class MapOptions { final LatLngBounds? bounds; final FitBoundsOptions boundsOptions; - /// Prints multi finger gesture winner Helps to fine adjust - /// [rotationThreshold] and [pinchZoomThreshold] and [pinchMoveThreshold] - /// Note: only takes effect if [enableMultiFingerGestureRace] is true - final bool debugMultiFingerGestureWinner; - - /// If true then [rotationThreshold] and [pinchZoomThreshold] and - /// [pinchMoveThreshold] will race If multiple gestures win at the same time - /// then precedence: [pinchZoomWinGestures] > [rotationWinGestures] > - /// [pinchMoveWinGestures] - final bool enableMultiFingerGestureRace; - - /// Rotation threshold in degree default is 20.0 Map starts to rotate when - /// [rotationThreshold] has been achieved or another multi finger gesture wins - /// which allows [MultiFingerGesture.rotate] Note: if [interactiveFlags] - /// doesn't contain [InteractiveFlag.rotate] or [enableMultiFingerGestureRace] - /// is false then rotate cannot win - final double rotationThreshold; - - /// When [rotationThreshold] wins over [pinchZoomThreshold] and - /// [pinchMoveThreshold] then [rotationWinGestures] gestures will be used. By - /// default only [MultiFingerGesture.rotate] gesture will take effect see - /// [MultiFingerGesture] for custom settings - final int rotationWinGestures; - - /// Pinch Zoom threshold default is 0.5 Map starts to zoom when - /// [pinchZoomThreshold] has been achieved or another multi finger gesture - /// wins which allows [MultiFingerGesture.pinchZoom] Note: if - /// [interactiveFlags] doesn't contain [InteractiveFlag.pinchZoom] or - /// [enableMultiFingerGestureRace] is false then zoom cannot win - final double pinchZoomThreshold; - - /// When [pinchZoomThreshold] wins over [rotationThreshold] and - /// [pinchMoveThreshold] then [pinchZoomWinGestures] gestures will be used. By - /// default [MultiFingerGesture.pinchZoom] and [MultiFingerGesture.pinchMove] - /// gestures will take effect see [MultiFingerGesture] for custom settings - final int pinchZoomWinGestures; - - /// Pinch Move threshold default is 40.0 (note: this doesn't take any effect - /// on drag) Map starts to move when [pinchMoveThreshold] has been achieved or - /// another multi finger gesture wins which allows - /// [MultiFingerGesture.pinchMove] Note: if [interactiveFlags] doesn't contain - /// [InteractiveFlag.pinchMove] or [enableMultiFingerGestureRace] is false - /// then pinch move cannot win - final double pinchMoveThreshold; - - /// When [pinchMoveThreshold] wins over [rotationThreshold] and - /// [pinchZoomThreshold] then [pinchMoveWinGestures] gestures will be used. By - /// default [MultiFingerGesture.pinchMove] and [MultiFingerGesture.pinchZoom] - /// gestures will take effect see [MultiFingerGesture] for custom settings - final int pinchMoveWinGestures; - - /// If true then the map will scroll when the user uses the scroll wheel on - /// his mouse. This is supported on web and desktop, but might also work well - /// on Android. A [Listener] is used to capture the onPointerSignal events. - final bool enableScrollWheel; - final double scrollWheelVelocity; + final bool? _debugMultiFingerGestureWinner; + final bool? _enableMultiFingerGestureRace; + final double? _rotationThreshold; + final int? _rotationWinGestures; + final double? _pinchZoomThreshold; + final int? _pinchZoomWinGestures; + final double? _pinchMoveThreshold; + final int? _pinchMoveWinGestures; + final bool? _enableScrollWheel; + final double? _scrollWheelVelocity; final double? minZoom; final double? maxZoom; /// see [InteractiveFlag] for custom settings - final int interactiveFlags; + final int? _interactiveFlags; final TapCallback? onTap; final TapCallback? onSecondaryTap; @@ -163,33 +117,31 @@ class MapOptions { this.boundsOptions = const FitBoundsOptions(), this.initialFrameFit, this.frameConstraint = const FrameConstraint.unconstrained(), + InteractionOptions? interactionOptions, @Deprecated('Should be set in interactionOptions instead') - this.debugMultiFingerGestureWinner = false, + int? interactiveFlags, @Deprecated('Should be set in interactionOptions instead') - this.enableMultiFingerGestureRace = false, + bool? debugMultiFingerGestureWinner, @Deprecated('Should be set in interactionOptions instead') - this.rotationThreshold = 20.0, + bool? enableMultiFingerGestureRace, @Deprecated('Should be set in interactionOptions instead') - this.rotationWinGestures = MultiFingerGesture.rotate, + double? rotationThreshold, @Deprecated('Should be set in interactionOptions instead') - this.pinchZoomThreshold = 0.5, + int? rotationWinGestures, @Deprecated('Should be set in interactionOptions instead') - this.pinchZoomWinGestures = - MultiFingerGesture.pinchZoom | MultiFingerGesture.pinchMove, + double? pinchZoomThreshold, @Deprecated('Should be set in interactionOptions instead') - this.pinchMoveThreshold = 40.0, + int? pinchZoomWinGestures, @Deprecated('Should be set in interactionOptions instead') - this.pinchMoveWinGestures = - MultiFingerGesture.pinchZoom | MultiFingerGesture.pinchMove, + double? pinchMoveThreshold, @Deprecated('Should be set in interactionOptions instead') - this.enableScrollWheel = true, + int? pinchMoveWinGestures, @Deprecated('Should be set in interactionOptions instead') - this.scrollWheelVelocity = 0.005, + bool? enableScrollWheel, + @Deprecated('Should be set in interactionOptions instead') + double? scrollWheelVelocity, this.minZoom, this.maxZoom, - @Deprecated('Should be set in interactionOptions instead') - this.interactiveFlags = InteractiveFlag.all, - InteractionOptions? interactionOptions, this.onTap, this.onSecondaryTap, this.onLongPress, @@ -202,12 +154,20 @@ class MapOptions { this.onMapReady, this.keepAlive = false, }) : _interactionOptions = interactionOptions, + _interactiveFlags = interactiveFlags, + _debugMultiFingerGestureWinner = debugMultiFingerGestureWinner, + _enableMultiFingerGestureRace = enableMultiFingerGestureRace, + _rotationThreshold = rotationThreshold, + _rotationWinGestures = rotationWinGestures, + _pinchZoomThreshold = pinchZoomThreshold, + _pinchZoomWinGestures = pinchZoomWinGestures, + _pinchMoveThreshold = pinchMoveThreshold, + _pinchMoveWinGestures = pinchMoveWinGestures, + _enableScrollWheel = enableScrollWheel, + _scrollWheelVelocity = scrollWheelVelocity, initialCenter = center ?? initialCenter, initialZoom = zoom ?? initialZoom, - initialRotation = rotation ?? initialRotation, - assert(rotationThreshold >= 0.0), - assert(pinchZoomThreshold >= 0.0), - assert(pinchMoveThreshold >= 0.0); + initialRotation = rotation ?? initialRotation; static MapOptions? maybeOf(BuildContext context) => FlutterMapInheritedModel.maybeOptionsOf(context); @@ -220,18 +180,72 @@ class MapOptions { InteractionOptions get interactionOptions => _interactionOptions ?? InteractionOptions( - flags: interactiveFlags, - debugMultiFingerGestureWinner: debugMultiFingerGestureWinner, - enableMultiFingerGestureRace: enableMultiFingerGestureRace, - rotationThreshold: rotationThreshold, - rotationWinGestures: rotationWinGestures, - pinchZoomThreshold: pinchZoomThreshold, - pinchZoomWinGestures: pinchZoomWinGestures, - pinchMoveThreshold: pinchMoveThreshold, - pinchMoveWinGestures: pinchMoveWinGestures, - enableScrollWheel: enableScrollWheel, - scrollWheelVelocity: scrollWheelVelocity, + flags: _interactiveFlags ?? InteractiveFlag.all, + debugMultiFingerGestureWinner: _debugMultiFingerGestureWinner ?? false, + enableMultiFingerGestureRace: _enableMultiFingerGestureRace ?? false, + rotationThreshold: _rotationThreshold ?? 20.0, + rotationWinGestures: _rotationWinGestures ?? MultiFingerGesture.rotate, + pinchZoomThreshold: _pinchZoomThreshold ?? 0.5, + pinchZoomWinGestures: _pinchZoomWinGestures ?? + MultiFingerGesture.pinchZoom | MultiFingerGesture.pinchMove, + pinchMoveThreshold: _pinchMoveThreshold ?? 40.0, + pinchMoveWinGestures: _pinchMoveWinGestures ?? + MultiFingerGesture.pinchZoom | MultiFingerGesture.pinchMove, + enableScrollWheel: _enableScrollWheel ?? true, + scrollWheelVelocity: _scrollWheelVelocity ?? 0.005, ); + + @override + bool operator ==(Object other) => + other is MapOptions && + crs == other.crs && + initialCenter == other.initialCenter && + initialZoom == other.initialZoom && + initialRotation == other.initialRotation && + initialFrameFit == other.initialFrameFit && + bounds == other.bounds && + boundsOptions == other.boundsOptions && + minZoom == other.minZoom && + maxZoom == other.maxZoom && + onTap == other.onTap && + onSecondaryTap == other.onSecondaryTap && + onLongPress == other.onLongPress && + onPointerDown == other.onPointerDown && + onPointerUp == other.onPointerUp && + onPointerCancel == other.onPointerCancel && + onPointerHover == other.onPointerHover && + onPositionChanged == other.onPositionChanged && + onMapEvent == other.onMapEvent && + frameConstraint == other.frameConstraint && + onMapReady == other.onMapReady && + keepAlive == other.keepAlive && + interactionOptions == other.interactionOptions; + + @override + int get hashCode => Object.hashAll([ + crs, + initialCenter, + initialZoom, + initialRotation, + initialFrameFit, + bounds, + boundsOptions, + minZoom, + maxZoom, + onTap, + onSecondaryTap, + onLongPress, + onPointerDown, + onPointerUp, + onPointerCancel, + onPointerHover, + onPositionChanged, + onMapEvent, + frameConstraint, + onMapReady, + keepAlive, + interactionOptions, + ]); } final class InteractionOptions { @@ -296,7 +310,7 @@ final class InteractionOptions { final double scrollWheelVelocity; const InteractionOptions({ - required this.flags, + this.flags = InteractiveFlag.all, this.debugMultiFingerGestureWinner = false, this.enableMultiFingerGestureRace = false, this.rotationThreshold = 20.0, @@ -309,7 +323,9 @@ final class InteractionOptions { MultiFingerGesture.pinchZoom | MultiFingerGesture.pinchMove, this.enableScrollWheel = true, this.scrollWheelVelocity = 0.005, - }); + }) : assert(rotationThreshold >= 0.0), + assert(pinchZoomThreshold >= 0.0), + assert(pinchMoveThreshold >= 0.0); bool get dragEnabled => InteractiveFlag.hasDrag(flags); bool get flingEnabled => InteractiveFlag.hasFlingAnimation(flags); From 4e60392f452a4d6abea0754f26e3d5bbb778fe22 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Thu, 15 Jun 2023 17:25:44 +0200 Subject: [PATCH 25/46] Fix options propagation --- .../map/flutter_map_internal_controller.dart | 37 ++++++++++--------- lib/src/map/flutter_map_state_container.dart | 4 +- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/lib/src/map/flutter_map_internal_controller.dart b/lib/src/map/flutter_map_internal_controller.dart index d758de522..d1685f0f4 100644 --- a/lib/src/map/flutter_map_internal_controller.dart +++ b/lib/src/map/flutter_map_internal_controller.dart @@ -254,29 +254,30 @@ class FlutterMapInternalController return false; } - void setOptions(MapOptions options) { - if (options == value.options) return; - - if (options != this.options) { - final newMapFrame = mapFrame.withOptions(options); + void setOptions(MapOptions newOptions) { + assert( + newOptions != value.options, + 'Should not update options unless they change', + ); - assert( - options.frameConstraint.constrain(newMapFrame) == newMapFrame, - 'FlutterMapFrame is no longer within the frameConstraint after an option change.', - ); + final newMapFrame = mapFrame.withOptions(newOptions); - if (this.options.interactionOptions != options.interactionOptions) { - _interactiveViewerState.updateGestures( - this.options.interactionOptions, - options.interactionOptions, - ); - } + assert( + newOptions.frameConstraint.constrain(newMapFrame) == newMapFrame, + 'FlutterMapFrame is no longer within the frameConstraint after an option change.', + ); - value = FlutterMapInternalState( - options: options, - mapFrame: newMapFrame, + if (options.interactionOptions != newOptions.interactionOptions) { + _interactiveViewerState.updateGestures( + options.interactionOptions, + newOptions.interactionOptions, ); } + + value = FlutterMapInternalState( + options: newOptions, + mapFrame: newMapFrame, + ); } // To be called when a gesture that causes movement starts. diff --git a/lib/src/map/flutter_map_state_container.dart b/lib/src/map/flutter_map_state_container.dart index 899744342..f479708b3 100644 --- a/lib/src/map/flutter_map_state_container.dart +++ b/lib/src/map/flutter_map_state_container.dart @@ -26,7 +26,9 @@ class FlutterMapStateContainer extends State { @override void didUpdateWidget(FlutterMap oldWidget) { - _flutterMapInternalController.setOptions(widget.options); + if (oldWidget.options != widget.options) { + _flutterMapInternalController.setOptions(widget.options); + } if (oldWidget.mapController != widget.mapController) { _initializeAndLinkMapController(); } From 3e4413c2923ffbeaf3961ab25153a61bbb013394 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Thu, 15 Jun 2023 17:26:23 +0200 Subject: [PATCH 26/46] Add tests to make sure the InheritedModel notifies if and only if the relevnat aspect changes --- test/flutter_map_test.dart | 153 ++++++++++++++++++++++++++++++++++ test/test_utils/test_app.dart | 2 +- 2 files changed, 154 insertions(+), 1 deletion(-) diff --git a/test/flutter_map_test.dart b/test/flutter_map_test.dart index 8af7f0b48..3943a8f82 100644 --- a/test/flutter_map_test.dart +++ b/test/flutter_map_test.dart @@ -75,4 +75,157 @@ void main() { // The map should not have rebuild after the first build. expect(builds, equals(1)); }); + + testWidgets('FlutterMapFrame.of only rebuilds when frame changes', + (tester) async { + int buildCount = 0; + final Widget builder = Builder(builder: (BuildContext context) { + FlutterMapFrame.of(context); + buildCount++; + return const SizedBox.shrink(); + }); + + await tester.pumpWidget(TestRebuildsApp(child: builder)); + expect(buildCount, equals(1)); + + await tester.tap(find.widgetWithText(TextButton, 'Change flags')); + await tester.pumpAndSettle(); + expect(buildCount, equals(1)); + + await tester.tap(find.widgetWithText(TextButton, 'Change MapController')); + await tester.pumpAndSettle(); + expect(buildCount, equals(1)); + + await tester.tap(find.widgetWithText(TextButton, 'Change Crs')); + await tester.pumpAndSettle(); + expect(buildCount, equals(2)); + }); + + testWidgets('MapOptions.of only rebuilds when options change', + (tester) async { + int buildCount = 0; + final Widget builder = Builder(builder: (BuildContext context) { + MapOptions.of(context); + buildCount++; + return const SizedBox.shrink(); + }); + + await tester.pumpWidget(TestRebuildsApp(child: builder)); + expect(buildCount, equals(1)); + + await tester.tap(find.widgetWithText(TextButton, 'Change flags')); + await tester.pumpAndSettle(); + expect(buildCount, equals(2)); + + await tester.tap(find.widgetWithText(TextButton, 'Change MapController')); + await tester.pumpAndSettle(); + expect(buildCount, equals(2)); + + await tester.tap(find.widgetWithText(TextButton, 'Change Crs')); + await tester.pumpAndSettle(); + expect(buildCount, equals(3)); + }); + + testWidgets('MapController.of only rebuilds when controller changes', + (tester) async { + int buildCount = 0; + final Widget builder = Builder(builder: (BuildContext context) { + MapController.of(context); + buildCount++; + return const SizedBox.shrink(); + }); + + await tester.pumpWidget(TestRebuildsApp(child: builder)); + expect(buildCount, equals(1)); + + await tester.tap(find.widgetWithText(TextButton, 'Change flags')); + await tester.pumpAndSettle(); + expect(buildCount, equals(1)); + + await tester.tap(find.widgetWithText(TextButton, 'Change MapController')); + await tester.pumpAndSettle(); + expect(buildCount, equals(2)); + + await tester.tap(find.widgetWithText(TextButton, 'Change Crs')); + await tester.pumpAndSettle(); + expect(buildCount, equals(2)); + }); +} + +class TestRebuildsApp extends StatefulWidget { + final Widget child; + + const TestRebuildsApp({ + super.key, + required this.child, + }); + + @override + State createState() => _TestRebuildsAppState(); +} + +class _TestRebuildsAppState extends State { + MapController _mapController = MapController(); + Crs _crs = const Epsg3857(); + int _interactiveFlags = InteractiveFlag.all; + + @override + void dispose() { + _mapController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: FlutterMap( + mapController: _mapController, + options: MapOptions( + crs: _crs, + interactionOptions: InteractionOptions( + flags: _interactiveFlags, + ), + ), + children: [ + widget.child, + Column( + children: [ + TextButton( + onPressed: () { + setState(() { + _interactiveFlags = + InteractiveFlag.hasDrag(_interactiveFlags) + ? _interactiveFlags & ~InteractiveFlag.drag + : InteractiveFlag.all; + }); + }, + child: const Text('Change flags'), + ), + TextButton( + onPressed: () { + setState(() { + _crs = _crs == const Epsg3857() + ? const Epsg4326() + : const Epsg3857(); + }); + }, + child: const Text('Change Crs'), + ), + TextButton( + onPressed: () { + _mapController.dispose(); + setState(() { + _mapController = MapController(); + }); + }, + child: const Text('Change MapController'), + ), + ], + ), + ], + ), + ), + ); + } } diff --git a/test/test_utils/test_app.dart b/test/test_utils/test_app.dart index 794cc2225..e2d01e48c 100644 --- a/test/test_utils/test_app.dart +++ b/test/test_utils/test_app.dart @@ -25,7 +25,7 @@ class TestApp extends StatelessWidget { return MaterialApp( home: Scaffold( body: Center( - // ensure that map is always of the same size + // Ensure that map is always of the same size child: SizedBox( width: 200, height: 200, From 3a0c0bb088cb8f75c52ecb8a8f385f9cc1f07a41 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Thu, 15 Jun 2023 17:36:09 +0200 Subject: [PATCH 27/46] Rename FlutterMapFrame to MapFrame --- .../lib/pages/scale_layer_plugin_option.dart | 2 +- .../lib/pages/zoombuttons_plugin_option.dart | 2 +- .../flutter_map_interactive_viewer.dart | 2 +- lib/src/gestures/map_events.dart | 8 ++--- lib/src/layer/circle_layer.dart | 2 +- lib/src/layer/marker_layer.dart | 2 +- lib/src/layer/overlay_image_layer.dart | 8 ++--- lib/src/layer/polygon_layer.dart | 4 +-- lib/src/layer/polyline_layer.dart | 4 +-- lib/src/layer/tile_layer/tile_layer.dart | 8 ++--- .../tile_layer/tile_range_calculator.dart | 4 +-- .../layer/tile_layer/tile_update_event.dart | 2 +- lib/src/map/flutter_map_frame.dart | 32 +++++++++---------- lib/src/map/flutter_map_inherited_model.dart | 6 ++-- .../map/flutter_map_internal_controller.dart | 14 ++++---- lib/src/map/flutter_map_internal_state.dart | 4 +-- lib/src/map/map_controller.dart | 8 ++--- lib/src/map/map_controller_impl.dart | 2 +- lib/src/misc/frame_constraint.dart | 8 ++--- lib/src/misc/frame_fit.dart | 10 +++--- test/flutter_map_test.dart | 11 ++++--- test/misc/frame_constraint_test.dart | 2 +- 22 files changed, 73 insertions(+), 72 deletions(-) diff --git a/example/lib/pages/scale_layer_plugin_option.dart b/example/lib/pages/scale_layer_plugin_option.dart index 89bbcfdec..3d9875cdb 100644 --- a/example/lib/pages/scale_layer_plugin_option.dart +++ b/example/lib/pages/scale_layer_plugin_option.dart @@ -51,7 +51,7 @@ class ScaleLayerWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final map = FlutterMapFrame.of(context); + final map = MapFrame.of(context); final zoom = map.zoom; final distance = scale[max(0, min(20, zoom.round() + 2))].toDouble(); final center = map.center; diff --git a/example/lib/pages/zoombuttons_plugin_option.dart b/example/lib/pages/zoombuttons_plugin_option.dart index b6d33c6c7..957f8b1f1 100644 --- a/example/lib/pages/zoombuttons_plugin_option.dart +++ b/example/lib/pages/zoombuttons_plugin_option.dart @@ -33,7 +33,7 @@ class FlutterMapZoomButtons extends StatelessWidget { @override Widget build(BuildContext context) { - final map = FlutterMapFrame.of(context); + final map = MapFrame.of(context); return Align( alignment: alignment, child: Column( diff --git a/lib/src/gestures/flutter_map_interactive_viewer.dart b/lib/src/gestures/flutter_map_interactive_viewer.dart index 31d134f33..e136f08e1 100644 --- a/lib/src/gestures/flutter_map_interactive_viewer.dart +++ b/lib/src/gestures/flutter_map_interactive_viewer.dart @@ -72,7 +72,7 @@ class FlutterMapInteractiveViewerState int _tapUpCounter = 0; Timer? _doubleTapHoldMaxDelay; - FlutterMapFrame get _mapFrame => widget.controller.mapFrame; + MapFrame get _mapFrame => widget.controller.mapFrame; MapOptions get _options => widget.controller.options; InteractionOptions get _interactionOptions => _options.interactionOptions; diff --git a/lib/src/gestures/map_events.dart b/lib/src/gestures/map_events.dart index 4191515db..5810375cd 100644 --- a/lib/src/gestures/map_events.dart +++ b/lib/src/gestures/map_events.dart @@ -33,7 +33,7 @@ abstract class MapEvent { final MapEventSource source; /// The map frame after the event. - final FlutterMapFrame mapFrame; + final MapFrame mapFrame; const MapEvent({ required this.source, @@ -45,7 +45,7 @@ abstract class MapEvent { /// includes information about camera movement /// which are not partial (e.g start rotate, rotate, end rotate). abstract class MapEventWithMove extends MapEvent { - final FlutterMapFrame oldMapFrame; + final MapFrame oldMapFrame; const MapEventWithMove({ required super.source, @@ -56,8 +56,8 @@ abstract class MapEventWithMove extends MapEvent { /// Returns a subclass of [MapEventWithMove] if the [source] belongs to a /// movement event, otherwise returns null. static MapEventWithMove? fromSource({ - required FlutterMapFrame oldMapFrame, - required FlutterMapFrame mapFrame, + required MapFrame oldMapFrame, + required MapFrame mapFrame, required bool hasGesture, required MapEventSource source, String? id, diff --git a/lib/src/layer/circle_layer.dart b/lib/src/layer/circle_layer.dart index 0df0074cf..d80690e43 100644 --- a/lib/src/layer/circle_layer.dart +++ b/lib/src/layer/circle_layer.dart @@ -36,7 +36,7 @@ class CircleLayer extends StatelessWidget { return LayoutBuilder( builder: (BuildContext context, BoxConstraints bc) { final size = Size(bc.maxWidth, bc.maxHeight); - final map = FlutterMapFrame.of(context); + final map = MapFrame.of(context); final circleWidgets = []; for (final circle in circles) { circle.offset = map.getOffsetFromOrigin(circle.point); diff --git a/lib/src/layer/marker_layer.dart b/lib/src/layer/marker_layer.dart index 5744d2bfc..6076d165e 100644 --- a/lib/src/layer/marker_layer.dart +++ b/lib/src/layer/marker_layer.dart @@ -192,7 +192,7 @@ class MarkerLayer extends StatelessWidget { @override Widget build(BuildContext context) { - final map = FlutterMapFrame.of(context); + final map = MapFrame.of(context); final markerWidgets = []; for (final marker in markers) { diff --git a/lib/src/layer/overlay_image_layer.dart b/lib/src/layer/overlay_image_layer.dart index c29943f73..32dafe999 100644 --- a/lib/src/layer/overlay_image_layer.dart +++ b/lib/src/layer/overlay_image_layer.dart @@ -12,7 +12,7 @@ abstract class BaseOverlayImage { bool get gaplessPlayback; - Positioned buildPositionedForOverlay(FlutterMapFrame map); + Positioned buildPositionedForOverlay(MapFrame map); Image buildImageForOverlay() { return Image( @@ -45,7 +45,7 @@ class OverlayImage extends BaseOverlayImage { this.gaplessPlayback = false}); @override - Positioned buildPositionedForOverlay(FlutterMapFrame map) { + Positioned buildPositionedForOverlay(MapFrame map) { // northWest is not necessarily upperLeft depending on projection final bounds = Bounds( map.project(this.bounds.northWest) - map.pixelOrigin, @@ -92,7 +92,7 @@ class RotatedOverlayImage extends BaseOverlayImage { this.filterQuality = FilterQuality.medium}); @override - Positioned buildPositionedForOverlay(FlutterMapFrame map) { + Positioned buildPositionedForOverlay(MapFrame map) { final pxTopLeft = map.project(topLeftCorner) - map.pixelOrigin; final pxBottomRight = map.project(bottomRightCorner) - map.pixelOrigin; final pxBottomLeft = map.project(bottomLeftCorner) - map.pixelOrigin; @@ -136,7 +136,7 @@ class OverlayImageLayer extends StatelessWidget { @override Widget build(BuildContext context) { - final map = FlutterMapFrame.of(context); + final map = MapFrame.of(context); return ClipRect( child: Stack( children: [ diff --git a/lib/src/layer/polygon_layer.dart b/lib/src/layer/polygon_layer.dart index 1fa04ec5d..1e251bc3d 100644 --- a/lib/src/layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer.dart @@ -78,7 +78,7 @@ class PolygonLayer extends StatelessWidget { @override Widget build(BuildContext context) { - final map = FlutterMapFrame.of(context); + final map = MapFrame.of(context); final size = Size(map.size.x, map.size.y); final List pgons = polygonCulling @@ -97,7 +97,7 @@ class PolygonLayer extends StatelessWidget { class PolygonPainter extends CustomPainter { final List polygons; - final FlutterMapFrame map; + final MapFrame map; final LatLngBounds bounds; PolygonPainter(this.polygons, this.map) : bounds = map.visibleBounds; diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index 0327e1383..a888609ed 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -66,7 +66,7 @@ class PolylineLayer extends StatelessWidget { @override Widget build(BuildContext context) { - final map = FlutterMapFrame.of(context); + final map = MapFrame.of(context); return CustomPaint( painter: PolylinePainter( @@ -86,7 +86,7 @@ class PolylineLayer extends StatelessWidget { class PolylinePainter extends CustomPainter { final List polylines; - final FlutterMapFrame map; + final MapFrame map; final LatLngBounds bounds; PolylinePainter(this.polylines, this.map) : bounds = map.visibleBounds; diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 1c4f2c3f1..dfac4e1fe 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -329,7 +329,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { void didChangeDependencies() { super.didChangeDependencies(); - final mapFrame = FlutterMapFrame.of(context); + final mapFrame = MapFrame.of(context); final mapController = MapController.of(context); if (_mapControllerHashCode != mapController.hashCode) { @@ -421,7 +421,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { if (reloadTiles) { _tileImageManager.removeAll(widget.evictErrorTileStrategy); - _loadAndPruneInVisibleBounds(FlutterMapFrame.maybeOf(context)!); + _loadAndPruneInVisibleBounds(MapFrame.maybeOf(context)!); } else if (oldWidget.tileDisplay != widget.tileDisplay) { _tileImageManager.updateTileDisplay(widget.tileDisplay); } @@ -440,7 +440,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - final map = FlutterMapFrame.of(context); + final map = MapFrame.of(context); if (_outsideZoomLimits(map.zoom.round())) return const SizedBox.shrink(); @@ -540,7 +540,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { } // Load new tiles in the visible bounds and prune those outside. - void _loadAndPruneInVisibleBounds(FlutterMapFrame mapFrame) { + void _loadAndPruneInVisibleBounds(MapFrame mapFrame) { final tileZoom = _clampToNativeZoom(mapFrame.zoom); final visibleTileRange = _tileRangeCalculator.calculate( mapFrame: mapFrame, diff --git a/lib/src/layer/tile_layer/tile_range_calculator.dart b/lib/src/layer/tile_layer/tile_range_calculator.dart index 07cc4712f..c46eab45d 100644 --- a/lib/src/layer/tile_layer/tile_range_calculator.dart +++ b/lib/src/layer/tile_layer/tile_range_calculator.dart @@ -13,7 +13,7 @@ class TileRangeCalculator { /// resulting tile range is expanded by [panBuffer]. DiscreteTileRange calculate({ // The map frame used to calculate the bounds. - required FlutterMapFrame mapFrame, + required MapFrame mapFrame, // The zoom level at which the bounds should be calculated. required int tileZoom, // The center from which the map is viewed, defaults to [mapFrame.center]. @@ -34,7 +34,7 @@ class TileRangeCalculator { } Bounds _calculatePixelBounds( - FlutterMapFrame mapFrame, + MapFrame mapFrame, LatLng center, double viewingZoom, int tileZoom, diff --git a/lib/src/layer/tile_layer/tile_update_event.dart b/lib/src/layer/tile_layer/tile_update_event.dart index 6809c1b3a..5ba0c84fd 100644 --- a/lib/src/layer/tile_layer/tile_update_event.dart +++ b/lib/src/layer/tile_layer/tile_update_event.dart @@ -23,7 +23,7 @@ class TileUpdateEvent { LatLng get center => loadCenterOverride ?? mapEvent.mapFrame.center; - FlutterMapFrame get mapFrame => mapEvent.mapFrame; + MapFrame get mapFrame => mapEvent.mapFrame; /// Returns a copy of this TileUpdateEvent with only pruning enabled and the /// loadCenterOverride/loadZoomOverride removed. diff --git a/lib/src/map/flutter_map_frame.dart b/lib/src/map/flutter_map_frame.dart index d33f387a6..d5c0e846b 100644 --- a/lib/src/map/flutter_map_frame.dart +++ b/lib/src/map/flutter_map_frame.dart @@ -9,7 +9,7 @@ import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; -class FlutterMapFrame { +class MapFrame { // During Flutter startup the native platform resolution is not immediately // available which can cause constraints to be zero before they are updated // in a subsequent build to the actual constraints. We set the size to this @@ -35,17 +35,17 @@ class FlutterMapFrame { CustomPoint? _pixelOrigin; double? _rotationRad; - static FlutterMapFrame? maybeOf(BuildContext context) => + static MapFrame? maybeOf(BuildContext context) => FlutterMapInheritedModel.maybeFrameOf(context); - static FlutterMapFrame of(BuildContext context) => + static MapFrame of(BuildContext context) => maybeOf(context) ?? (throw StateError( - '`FlutterMapFrame.of()` should not be called outside a `FlutterMap` and its descendants')); + '`MapFrame.of()` should not be called outside a `FlutterMap` and its descendants')); - /// Initializes FlutterMapFrame from the given [options] and with the + /// Initializes [MapFrame] from the given [options] and with the /// [nonRotatedSize] set to [kImpossibleSize]. - FlutterMapFrame.initialFrame(MapOptions options) + MapFrame.initialFrame(MapOptions options) : crs = options.crs, minZoom = options.minZoom, maxZoom = options.maxZoom, @@ -54,10 +54,10 @@ class FlutterMapFrame { rotation = options.initialRotation, nonRotatedSize = kImpossibleSize; - // Create an instance of FlutterMapFrame. The [pixelOrigin], [bounds], and + // Create an instance of [MapFrame]. The [pixelOrigin], [bounds], and // [pixelBounds] may be set if they are known already. Otherwise if left // null they will be calculated lazily when they are used. - FlutterMapFrame({ + MapFrame({ required this.crs, required this.center, required this.zoom, @@ -74,10 +74,10 @@ class FlutterMapFrame { _bounds = bounds, _pixelOrigin = pixelOrigin; - FlutterMapFrame withNonRotatedSize(CustomPoint nonRotatedSize) { + MapFrame withNonRotatedSize(CustomPoint nonRotatedSize) { if (nonRotatedSize == this.nonRotatedSize) return this; - return FlutterMapFrame( + return MapFrame( crs: crs, minZoom: minZoom, maxZoom: maxZoom, @@ -88,10 +88,10 @@ class FlutterMapFrame { ); } - FlutterMapFrame withRotation(double rotation) { + MapFrame withRotation(double rotation) { if (rotation == this.rotation) return this; - return FlutterMapFrame( + return MapFrame( crs: crs, minZoom: minZoom, maxZoom: maxZoom, @@ -102,14 +102,14 @@ class FlutterMapFrame { ); } - FlutterMapFrame withOptions(MapOptions options) { + MapFrame withOptions(MapOptions options) { if (options.crs == crs && options.minZoom == minZoom && options.maxZoom == maxZoom) { return this; } - return FlutterMapFrame( + return MapFrame( crs: options.crs, minZoom: options.minZoom, maxZoom: options.maxZoom, @@ -121,11 +121,11 @@ class FlutterMapFrame { ); } - FlutterMapFrame withPosition({ + MapFrame withPosition({ LatLng? center, double? zoom, }) => - FlutterMapFrame( + MapFrame( crs: crs, minZoom: minZoom, maxZoom: maxZoom, diff --git a/lib/src/map/flutter_map_inherited_model.dart b/lib/src/map/flutter_map_inherited_model.dart index 781db0c83..f5486b755 100644 --- a/lib/src/map/flutter_map_inherited_model.dart +++ b/lib/src/map/flutter_map_inherited_model.dart @@ -8,7 +8,7 @@ class FlutterMapInheritedModel extends InheritedModel<_FlutterMapAspect> { FlutterMapInheritedModel({ super.key, - required FlutterMapFrame frame, + required MapFrame frame, required MapController controller, required MapOptions options, required super.child, @@ -26,7 +26,7 @@ class FlutterMapInheritedModel extends InheritedModel<_FlutterMapAspect> { aspect: aspect) ?.data; - static FlutterMapFrame? maybeFrameOf(BuildContext context) => + static MapFrame? maybeFrameOf(BuildContext context) => _maybeOf(context, _FlutterMapAspect.frame)?.frame; static MapController? maybeControllerOf(BuildContext context) => @@ -60,7 +60,7 @@ class FlutterMapInheritedModel extends InheritedModel<_FlutterMapAspect> { } class FlutterMapData { - final FlutterMapFrame frame; + final MapFrame frame; final MapController controller; final MapOptions options; diff --git a/lib/src/map/flutter_map_internal_controller.dart b/lib/src/map/flutter_map_internal_controller.dart index d1685f0f4..270f8084c 100644 --- a/lib/src/map/flutter_map_internal_controller.dart +++ b/lib/src/map/flutter_map_internal_controller.dart @@ -17,7 +17,7 @@ class FlutterMapInternalController : super( FlutterMapInternalState( options: options, - mapFrame: FlutterMapFrame.initialFrame(options), + mapFrame: MapFrame.initialFrame(options), ), ); @@ -29,7 +29,7 @@ class FlutterMapInternalController _interactiveViewerState = interactiveViewerState; MapOptions get options => value.options; - FlutterMapFrame get mapFrame => value.mapFrame; + MapFrame get mapFrame => value.mapFrame; void linkMapController(MapControllerImpl mapControllerImpl) { _mapControllerImpl = mapControllerImpl; @@ -65,7 +65,7 @@ class FlutterMapInternalController ); } - FlutterMapFrame? newMapFrame = mapFrame.withPosition( + MapFrame? newMapFrame = mapFrame.withPosition( center: newCenter, zoom: mapFrame.clampZoom(newZoom), ); @@ -245,7 +245,7 @@ class FlutterMapInternalController bool setNonRotatedSizeWithoutEmittingEvent( CustomPoint nonRotatedSize, ) { - if (nonRotatedSize != FlutterMapFrame.kImpossibleSize && + if (nonRotatedSize != MapFrame.kImpossibleSize && nonRotatedSize != mapFrame.nonRotatedSize) { value = value.withMapFrame(mapFrame.withNonRotatedSize(nonRotatedSize)); return true; @@ -264,7 +264,7 @@ class FlutterMapInternalController assert( newOptions.frameConstraint.constrain(newMapFrame) == newMapFrame, - 'FlutterMapFrame is no longer within the frameConstraint after an option change.', + 'MapFrame is no longer within the frameConstraint after an option change.', ); if (options.interactionOptions != newOptions.interactionOptions) { @@ -435,8 +435,8 @@ class FlutterMapInternalController // To be called when the map's size constraints change. void nonRotatedSizeChange( MapEventSource source, - FlutterMapFrame oldMapFrame, - FlutterMapFrame newMapFrame, + MapFrame oldMapFrame, + MapFrame newMapFrame, ) { _emitMapEvent( MapEventNonRotatedSizeChange( diff --git a/lib/src/map/flutter_map_internal_state.dart b/lib/src/map/flutter_map_internal_state.dart index 4b1a616c1..77eb929fb 100644 --- a/lib/src/map/flutter_map_internal_state.dart +++ b/lib/src/map/flutter_map_internal_state.dart @@ -1,7 +1,7 @@ import 'package:flutter_map/plugin_api.dart'; class FlutterMapInternalState { - final FlutterMapFrame mapFrame; + final MapFrame mapFrame; final MapOptions options; const FlutterMapInternalState({ @@ -9,7 +9,7 @@ class FlutterMapInternalState { required this.mapFrame, }); - FlutterMapInternalState withMapFrame(FlutterMapFrame mapFrame) => + FlutterMapInternalState withMapFrame(MapFrame mapFrame) => FlutterMapInternalState( options: options, mapFrame: mapFrame, diff --git a/lib/src/map/map_controller.dart b/lib/src/map/map_controller.dart index 5010c025d..c2ef0a299 100644 --- a/lib/src/map/map_controller.dart +++ b/lib/src/map/map_controller.dart @@ -137,10 +137,10 @@ abstract class MapController { /// documentation. bool fitFrame(FrameFit frameFit); - /// Current FlutterMapFrame. Accessing the frame from this getter is an - /// anti-pattern. It is preferable to use FlutterMapFrame.of(context) in a - /// child widget of FlutterMap state. - FlutterMapFrame get mapFrame; + /// Current [MapFrame]. Accessing the frame from this getter is an + /// anti-pattern. It is preferable to use [MapFrame.of(context)] in a child + /// widget of FlutterMap. + MapFrame get mapFrame; /// [Stream] of all emitted [MapEvent]s Stream get mapEventStream; diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/map_controller_impl.dart index 5387f5cc8..295435073 100644 --- a/lib/src/map/map_controller_impl.dart +++ b/lib/src/map/map_controller_impl.dart @@ -102,7 +102,7 @@ class MapControllerImpl implements MapController { ); @override - FlutterMapFrame get mapFrame => _internalController.mapFrame; + MapFrame get mapFrame => _internalController.mapFrame; final _mapEventStreamController = StreamController.broadcast(); diff --git a/lib/src/misc/frame_constraint.dart b/lib/src/misc/frame_constraint.dart index 1e80f5f92..e5d089d59 100644 --- a/lib/src/misc/frame_constraint.dart +++ b/lib/src/misc/frame_constraint.dart @@ -18,14 +18,14 @@ abstract class FrameConstraint { required LatLngBounds bounds, }) = ContainFrame._; - FlutterMapFrame? constrain(FlutterMapFrame mapFrame); + MapFrame? constrain(MapFrame mapFrame); } class UnconstrainedFrame extends FrameConstraint { const UnconstrainedFrame._(); @override - FlutterMapFrame constrain(FlutterMapFrame mapFrame) => mapFrame; + MapFrame constrain(MapFrame mapFrame) => mapFrame; } class ContainFrameCenter extends FrameConstraint { @@ -36,7 +36,7 @@ class ContainFrameCenter extends FrameConstraint { }); @override - FlutterMapFrame constrain(FlutterMapFrame mapFrame) => mapFrame.withPosition( + MapFrame constrain(MapFrame mapFrame) => mapFrame.withPosition( center: LatLng( mapFrame.center.latitude.clamp( bounds.south, @@ -59,7 +59,7 @@ class ContainFrame extends FrameConstraint { }); @override - FlutterMapFrame? constrain(FlutterMapFrame mapFrame) { + MapFrame? constrain(MapFrame mapFrame) { final testZoom = mapFrame.zoom; final testCenter = mapFrame.center; diff --git a/lib/src/misc/frame_fit.dart b/lib/src/misc/frame_fit.dart index 952fbc90a..cab8163c9 100644 --- a/lib/src/misc/frame_fit.dart +++ b/lib/src/misc/frame_fit.dart @@ -25,7 +25,7 @@ abstract class FrameFit { bool forceIntegerZoomLevel, }) = FitCoordinates; - FlutterMapFrame fit(FlutterMapFrame mapFrame); + MapFrame fit(MapFrame mapFrame); } class FitBounds extends FrameFit { @@ -48,7 +48,7 @@ class FitBounds extends FrameFit { }); @override - FlutterMapFrame fit(FlutterMapFrame mapFrame) { + MapFrame fit(MapFrame mapFrame) { final paddingTL = CustomPoint(padding.left, padding.top); final paddingBR = CustomPoint(padding.right, padding.bottom); @@ -81,7 +81,7 @@ class FitBounds extends FrameFit { } double getBoundsZoom( - FlutterMapFrame mapFrame, + MapFrame mapFrame, CustomPoint pixelPadding, ) { final min = mapFrame.minZoom ?? 0.0; @@ -137,7 +137,7 @@ class FitCoordinates extends FrameFit { }); @override - FlutterMapFrame fit(FlutterMapFrame mapFrame) { + MapFrame fit(MapFrame mapFrame) { final paddingTL = CustomPoint(padding.left, padding.top); final paddingBR = CustomPoint(padding.right, padding.bottom); @@ -171,7 +171,7 @@ class FitCoordinates extends FrameFit { } double getCoordinatesZoom( - FlutterMapFrame mapFrame, + MapFrame mapFrame, CustomPoint pixelPadding, ) { final min = mapFrame.minZoom ?? 0.0; diff --git a/test/flutter_map_test.dart b/test/flutter_map_test.dart index 3943a8f82..629dcb9df 100644 --- a/test/flutter_map_test.dart +++ b/test/flutter_map_test.dart @@ -43,7 +43,7 @@ void main() { children: [ Builder( builder: (context) { - final _ = FlutterMapFrame.of(context); + final _ = MapFrame.of(context); builds++; return const SizedBox.shrink(); }, @@ -76,11 +76,11 @@ void main() { expect(builds, equals(1)); }); - testWidgets('FlutterMapFrame.of only rebuilds when frame changes', + testWidgets('MapFrame.of only notifies dependencies when frame changes', (tester) async { int buildCount = 0; final Widget builder = Builder(builder: (BuildContext context) { - FlutterMapFrame.of(context); + MapFrame.of(context); buildCount++; return const SizedBox.shrink(); }); @@ -101,7 +101,7 @@ void main() { expect(buildCount, equals(2)); }); - testWidgets('MapOptions.of only rebuilds when options change', + testWidgets('MapOptions.of only notifies dependencies when options change', (tester) async { int buildCount = 0; final Widget builder = Builder(builder: (BuildContext context) { @@ -126,7 +126,8 @@ void main() { expect(buildCount, equals(3)); }); - testWidgets('MapController.of only rebuilds when controller changes', + testWidgets( + 'MapController.of only notifies dependencies when controller changes', (tester) async { int buildCount = 0; final Widget builder = Builder(builder: (BuildContext context) { diff --git a/test/misc/frame_constraint_test.dart b/test/misc/frame_constraint_test.dart index 28f002b08..476c6f941 100644 --- a/test/misc/frame_constraint_test.dart +++ b/test/misc/frame_constraint_test.dart @@ -13,7 +13,7 @@ void main() { ), ); - final mapFrame = FlutterMapFrame( + final mapFrame = MapFrame( crs: const Epsg3857(), center: const LatLng(-90, -180), zoom: 1, From ccea34a9bc7c02c0ecfeed9232b3bdd054819345 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Thu, 15 Jun 2023 18:59:20 +0200 Subject: [PATCH 28/46] Avoid an extra Stack --- lib/src/map/flutter_map_state_container.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/map/flutter_map_state_container.dart b/lib/src/map/flutter_map_state_container.dart index f479708b3..54b10b753 100644 --- a/lib/src/map/flutter_map_state_container.dart +++ b/lib/src/map/flutter_map_state_container.dart @@ -75,7 +75,7 @@ class FlutterMapStateContainer extends State { child: Stack(children: widget.children), ), ), - Stack(children: widget.nonRotatedChildren), + ...widget.nonRotatedChildren, ], ), ), From bdae8ef62a73f877b64c0f6303d76ca6888a9b5f Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Mon, 19 Jun 2023 11:39:34 +0200 Subject: [PATCH 29/46] Assign AnimationControllers where they are declared --- .../flutter_map_interactive_viewer.dart | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/src/gestures/flutter_map_interactive_viewer.dart b/lib/src/gestures/flutter_map_interactive_viewer.dart index e136f08e1..6ee3018cb 100644 --- a/lib/src/gestures/flutter_map_interactive_viewer.dart +++ b/lib/src/gestures/flutter_map_interactive_viewer.dart @@ -62,10 +62,16 @@ class FlutterMapInteractiveViewerState late Offset _focalStartLocal; late LatLng _focalStartLatLng; - late final AnimationController _flingController; + late final AnimationController _flingController = + AnimationController(vsync: this); late Animation _flingAnimation; - late final AnimationController _doubleTapController; + late final AnimationController _doubleTapController = AnimationController( + vsync: this, + duration: const Duration( + milliseconds: _kDoubleTapZoomDuration, + ), + ); late Animation _doubleTapZoomAnimation; late Animation _doubleTapCenterAnimation; @@ -82,15 +88,10 @@ class FlutterMapInteractiveViewerState super.initState(); widget.controller.interactiveViewerState = this; widget.controller.addListener(_onMapStateChange); - _flingController = AnimationController(vsync: this) + _flingController ..addListener(_handleFlingAnimation) ..addStatusListener(_flingAnimationStatusListener); - _doubleTapController = AnimationController( - vsync: this, - duration: const Duration( - milliseconds: _kDoubleTapZoomDuration, - ), - ) + _doubleTapController ..addListener(_handleDoubleTapZoomAnimation) ..addStatusListener(_doubleTapZoomStatusListener); } From 4dedb1c05e0ca2d2284f6ec103a3af3d3f9283ab Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Tue, 20 Jun 2023 13:36:38 +0200 Subject: [PATCH 30/46] Rename from frame to camera --- .../lib/pages/animated_map_controller.dart | 16 +- example/lib/pages/home.dart | 2 +- example/lib/pages/latlng_to_screen_point.dart | 2 +- example/lib/pages/map_controller.dart | 4 +- example/lib/pages/offline_map.dart | 2 +- example/lib/pages/point_to_latlng.dart | 2 +- .../lib/pages/scale_layer_plugin_option.dart | 2 +- example/lib/pages/sliding_map.dart | 2 +- .../lib/pages/zoombuttons_plugin_option.dart | 14 +- lib/flutter_map.dart | 4 +- lib/plugin_api.dart | 2 +- .../flutter_map_interactive_viewer.dart | 85 +++++----- lib/src/gestures/map_events.dart | 84 +++++----- lib/src/layer/circle_layer.dart | 4 +- lib/src/layer/marker_layer.dart | 6 +- lib/src/layer/overlay_image_layer.dart | 10 +- lib/src/layer/polygon_layer.dart | 6 +- lib/src/layer/polyline_layer.dart | 6 +- lib/src/layer/tile_layer/tile_layer.dart | 34 ++-- .../tile_layer/tile_range_calculator.dart | 24 +-- .../layer/tile_layer/tile_update_event.dart | 8 +- .../{flutter_map_frame.dart => camera.dart} | 44 ++--- lib/src/map/flutter_map_inherited_model.dart | 20 +-- .../map/flutter_map_internal_controller.dart | 128 +++++++-------- lib/src/map/flutter_map_internal_state.dart | 11 +- lib/src/map/flutter_map_state_container.dart | 46 +++--- lib/src/map/map_controller.dart | 32 ++-- lib/src/map/map_controller_impl.dart | 50 +++--- lib/src/map/options.dart | 30 ++-- lib/src/misc/camera_constraint.dart | 102 ++++++++++++ .../misc/{frame_fit.dart => camera_fit.dart} | 84 +++++----- lib/src/misc/fit_bounds_options.dart | 2 +- lib/src/misc/frame_constraint.dart | 102 ------------ test/flutter_map_controller_test.dart | 152 +++++++++--------- test/flutter_map_test.dart | 6 +- test/misc/frame_constraint_test.dart | 14 +- 36 files changed, 575 insertions(+), 567 deletions(-) rename lib/src/map/{flutter_map_frame.dart => camera.dart} (90%) create mode 100644 lib/src/misc/camera_constraint.dart rename lib/src/misc/{frame_fit.dart => camera_fit.dart} (66%) delete mode 100644 lib/src/misc/frame_constraint.dart diff --git a/example/lib/pages/animated_map_controller.dart b/example/lib/pages/animated_map_controller.dart index a8db1dca3..1e2c5ad06 100644 --- a/example/lib/pages/animated_map_controller.dart +++ b/example/lib/pages/animated_map_controller.dart @@ -35,12 +35,12 @@ class AnimatedMapControllerPageState extends State void _animatedMapMove(LatLng destLocation, double destZoom) { // Create some tweens. These serve to split up the transition from one location to another. // In our case, we want to split the transition be our current map center and the destination. - final mapFrame = mapController.mapFrame; + final mapCamera = mapController.mapCamera; final latTween = Tween( - begin: mapFrame.center.latitude, end: destLocation.latitude); + begin: mapCamera.center.latitude, end: destLocation.latitude); final lngTween = Tween( - begin: mapFrame.center.longitude, end: destLocation.longitude); - final zoomTween = Tween(begin: mapFrame.zoom, end: destZoom); + begin: mapCamera.center.longitude, end: destLocation.longitude); + final zoomTween = Tween(begin: mapCamera.zoom, end: destZoom); // Create a animation controller that has a duration and a TickerProvider. final controller = AnimationController( @@ -162,8 +162,8 @@ class AnimatedMapControllerPageState extends State london, ]); - mapController.fitFrame( - FrameFit.bounds( + mapController.fitCamera( + CameraFit.bounds( bounds: bounds, padding: const EdgeInsets.symmetric(horizontal: 15), ), @@ -179,9 +179,9 @@ class AnimatedMapControllerPageState extends State london, ]); - final constrained = FrameFit.bounds( + final constrained = CameraFit.bounds( bounds: bounds, - ).fit(mapController.mapFrame); + ).fit(mapController.mapCamera); _animatedMapMove(constrained.center, constrained.zoom); }, child: const Text('Fit Bounds animated'), diff --git a/example/lib/pages/home.dart b/example/lib/pages/home.dart index dfa6c5694..6015db39a 100644 --- a/example/lib/pages/home.dart +++ b/example/lib/pages/home.dart @@ -114,7 +114,7 @@ class _HomePageState extends State { options: MapOptions( initialCenter: const LatLng(51.5, -0.09), initialZoom: 5, - frameConstraint: FrameConstraint.contain( + cameraConstraint: CameraConstraint.contain( bounds: LatLngBounds( const LatLng(-90, -180), const LatLng(90, 180), diff --git a/example/lib/pages/latlng_to_screen_point.dart b/example/lib/pages/latlng_to_screen_point.dart index 179db1b8f..6754de890 100644 --- a/example/lib/pages/latlng_to_screen_point.dart +++ b/example/lib/pages/latlng_to_screen_point.dart @@ -48,7 +48,7 @@ class _LatLngScreenPointTestPageState extends State { onMapEvent: onMapEvent, onTap: (tapPos, latLng) { final pt1 = - _mapController.mapFrame.latLngToScreenPoint(latLng); + _mapController.mapCamera.latLngToScreenPoint(latLng); _textPos = CustomPoint(pt1.x, pt1.y); setState(() {}); }, diff --git a/example/lib/pages/map_controller.dart b/example/lib/pages/map_controller.dart index 53ea4e9fc..6bfb26925 100644 --- a/example/lib/pages/map_controller.dart +++ b/example/lib/pages/map_controller.dart @@ -104,7 +104,7 @@ class MapControllerPageState extends State { london, ]); - _mapController.fitFrame( + _mapController.fitCamera( FitBounds( bounds: bounds, padding: const EdgeInsets.symmetric(horizontal: 15), @@ -116,7 +116,7 @@ class MapControllerPageState extends State { Builder(builder: (BuildContext context) { return MaterialButton( onPressed: () { - final bounds = _mapController.mapFrame.visibleBounds; + final bounds = _mapController.mapCamera.visibleBounds; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( diff --git a/example/lib/pages/offline_map.dart b/example/lib/pages/offline_map.dart index a01265020..89d6e5a2b 100644 --- a/example/lib/pages/offline_map.dart +++ b/example/lib/pages/offline_map.dart @@ -28,7 +28,7 @@ class OfflineMapPage extends StatelessWidget { initialCenter: const LatLng(56.704173, 11.543808), minZoom: 12, maxZoom: 14, - frameConstraint: FrameConstraint.containCenter( + cameraConstraint: CameraConstraint.containCenter( bounds: LatLngBounds( const LatLng(56.7378, 11.6644), const LatLng(56.6877, 11.5089), diff --git a/example/lib/pages/point_to_latlng.dart b/example/lib/pages/point_to_latlng.dart index 6ba51fb53..cbe93fe12 100644 --- a/example/lib/pages/point_to_latlng.dart +++ b/example/lib/pages/point_to_latlng.dart @@ -107,7 +107,7 @@ class PointToLatlngPage extends State { final pointX = _getPointX(context); setState(() { latLng = - mapController.mapFrame.pointToLatLng(CustomPoint(pointX, pointY)); + mapController.mapCamera.pointToLatLng(CustomPoint(pointX, pointY)); }); } diff --git a/example/lib/pages/scale_layer_plugin_option.dart b/example/lib/pages/scale_layer_plugin_option.dart index 3d9875cdb..6bb64d146 100644 --- a/example/lib/pages/scale_layer_plugin_option.dart +++ b/example/lib/pages/scale_layer_plugin_option.dart @@ -51,7 +51,7 @@ class ScaleLayerWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final map = MapFrame.of(context); + final map = MapCamera.of(context); final zoom = map.zoom; final distance = scale[max(0, min(20, zoom.round() + 2))].toDouble(); final center = map.center; diff --git a/example/lib/pages/sliding_map.dart b/example/lib/pages/sliding_map.dart index dd7083910..69ff0d870 100644 --- a/example/lib/pages/sliding_map.dart +++ b/example/lib/pages/sliding_map.dart @@ -31,7 +31,7 @@ class SlidingMapPage extends StatelessWidget { minZoom: 12, maxZoom: 14, initialZoom: 13, - frameConstraint: FrameConstraint.containCenter( + cameraConstraint: CameraConstraint.containCenter( bounds: LatLngBounds(northEast, southWest), ), ), diff --git a/example/lib/pages/zoombuttons_plugin_option.dart b/example/lib/pages/zoombuttons_plugin_option.dart index 957f8b1f1..fff8d099f 100644 --- a/example/lib/pages/zoombuttons_plugin_option.dart +++ b/example/lib/pages/zoombuttons_plugin_option.dart @@ -33,7 +33,7 @@ class FlutterMapZoomButtons extends StatelessWidget { @override Widget build(BuildContext context) { - final map = MapFrame.of(context); + final map = MapCamera.of(context); return Align( alignment: alignment, child: Column( @@ -47,15 +47,15 @@ class FlutterMapZoomButtons extends StatelessWidget { mini: mini, backgroundColor: zoomInColor ?? Theme.of(context).primaryColor, onPressed: () { - final paddedMapFrame = FrameFit.bounds( + final paddedMapCamera = CameraFit.bounds( bounds: map.visibleBounds, padding: _fitBoundsPadding, ).fit(map); - var zoom = paddedMapFrame.zoom + 1; + var zoom = paddedMapCamera.zoom + 1; if (zoom > maxZoom) { zoom = maxZoom; } - MapController.of(context).move(paddedMapFrame.center, zoom); + MapController.of(context).move(paddedMapCamera.center, zoom); }, child: Icon(zoomInIcon, color: zoomInColorIcon ?? IconTheme.of(context).color), @@ -68,15 +68,15 @@ class FlutterMapZoomButtons extends StatelessWidget { mini: mini, backgroundColor: zoomOutColor ?? Theme.of(context).primaryColor, onPressed: () { - final paddedMapFrame = FrameFit.bounds( + final paddedMapCamera = CameraFit.bounds( bounds: map.visibleBounds, padding: _fitBoundsPadding, ).fit(map); - var zoom = paddedMapFrame.zoom - 1; + var zoom = paddedMapCamera.zoom - 1; if (zoom < minZoom) { zoom = minZoom; } - MapController.of(context).move(paddedMapFrame.center, zoom); + MapController.of(context).move(paddedMapCamera.center, zoom); }, child: Icon(zoomOutIcon, color: zoomOutColorIcon ?? IconTheme.of(context).color), diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index d9a9addad..5aaf7fdcd 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -29,10 +29,10 @@ export 'package:flutter_map/src/layer/tile_layer/tile_update_transformer.dart'; export 'package:flutter_map/src/map/map_controller.dart'; export 'package:flutter_map/src/map/options.dart'; export 'package:flutter_map/src/map/widget.dart'; +export 'package:flutter_map/src/misc/camera_constraint.dart'; +export 'package:flutter_map/src/misc/camera_fit.dart'; export 'package:flutter_map/src/misc/center_zoom.dart'; export 'package:flutter_map/src/misc/fit_bounds_options.dart'; -export 'package:flutter_map/src/misc/frame_constraint.dart'; -export 'package:flutter_map/src/misc/frame_fit.dart'; export 'package:flutter_map/src/misc/move_and_rotate_result.dart'; export 'package:flutter_map/src/misc/point.dart'; export 'package:flutter_map/src/misc/position.dart'; diff --git a/lib/plugin_api.dart b/lib/plugin_api.dart index f7608530a..a2385fe47 100644 --- a/lib/plugin_api.dart +++ b/lib/plugin_api.dart @@ -1,7 +1,7 @@ library flutter_map.plugin_api; export 'package:flutter_map/flutter_map.dart'; -export 'package:flutter_map/src/map/flutter_map_frame.dart'; +export 'package:flutter_map/src/map/camera.dart'; export 'package:flutter_map/src/map/map_controller.dart'; export 'package:flutter_map/src/misc/private/bounds.dart'; export 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; diff --git a/lib/src/gestures/flutter_map_interactive_viewer.dart b/lib/src/gestures/flutter_map_interactive_viewer.dart index 6ee3018cb..693371623 100644 --- a/lib/src/gestures/flutter_map_interactive_viewer.dart +++ b/lib/src/gestures/flutter_map_interactive_viewer.dart @@ -7,7 +7,7 @@ import 'package:flutter_map/src/gestures/interactive_flag.dart'; import 'package:flutter_map/src/gestures/latlng_tween.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; -import 'package:flutter_map/src/map/flutter_map_frame.dart'; +import 'package:flutter_map/src/map/camera.dart'; import 'package:flutter_map/src/map/flutter_map_internal_controller.dart'; import 'package:flutter_map/src/map/flutter_map_internal_state.dart'; import 'package:flutter_map/src/map/options.dart'; @@ -78,7 +78,7 @@ class FlutterMapInteractiveViewerState int _tapUpCounter = 0; Timer? _doubleTapHoldMaxDelay; - MapFrame get _mapFrame => widget.controller.mapFrame; + MapCamera get _mapCamera => widget.controller.mapCamera; MapOptions get _options => widget.controller.options; InteractionOptions get _interactionOptions => _options.interactionOptions; @@ -275,7 +275,7 @@ class FlutterMapInteractiveViewerState ++_pointerCounter; if (_options.onPointerDown != null) { - final latlng = _mapFrame.offsetToCrs(event.localPosition); + final latlng = _mapCamera.offsetToCrs(event.localPosition); _options.onPointerDown!(event, latlng); } } @@ -284,7 +284,7 @@ class FlutterMapInteractiveViewerState --_pointerCounter; if (_options.onPointerUp != null) { - final latlng = _mapFrame.offsetToCrs(event.localPosition); + final latlng = _mapCamera.offsetToCrs(event.localPosition); _options.onPointerUp!(event, latlng); } } @@ -293,14 +293,14 @@ class FlutterMapInteractiveViewerState --_pointerCounter; if (_options.onPointerCancel != null) { - final latlng = _mapFrame.offsetToCrs(event.localPosition); + final latlng = _mapCamera.offsetToCrs(event.localPosition); _options.onPointerCancel!(event, latlng); } } void _onPointerHover(PointerHoverEvent event) { if (_options.onPointerHover != null) { - final latlng = _mapFrame.offsetToCrs(event.localPosition); + final latlng = _mapCamera.offsetToCrs(event.localPosition); _options.onPointerHover!(event, latlng); } } @@ -318,12 +318,12 @@ class FlutterMapInteractiveViewerState pointerSignal as PointerScrollEvent; final minZoom = _options.minZoom ?? 0.0; final maxZoom = _options.maxZoom ?? double.infinity; - final newZoom = (_mapFrame.zoom - + final newZoom = (_mapCamera.zoom - pointerSignal.scrollDelta.dy * _interactionOptions.scrollWheelVelocity) .clamp(minZoom, maxZoom); // Calculate offset of mouse cursor from viewport center - final newCenter = _mapFrame.focusedZoomCenter( + final newCenter = _mapCamera.focusedZoomCenter( pointerSignal.localPosition.toCustomPoint(), newZoom, ); @@ -388,10 +388,10 @@ class FlutterMapInteractiveViewerState _gestureWinner = MultiFingerGesture.none; - _mapZoomStart = _mapFrame.zoom; - _mapCenterStart = _mapFrame.center; + _mapZoomStart = _mapCamera.zoom; + _mapCenterStart = _mapCamera.center; _focalStartLocal = _lastFocalLocal = details.localFocalPoint; - _focalStartLatLng = _mapFrame.offsetToCrs(_focalStartLocal); + _focalStartLatLng = _mapCamera.offsetToCrs(_focalStartLocal); _dragStarted = false; _pinchZoomStarted = false; @@ -487,8 +487,8 @@ class FlutterMapInteractiveViewerState bool hasPinchZoom, bool hasPinchMove, ) { - LatLng newCenter = _mapFrame.center; - double newZoom = _mapFrame.zoom; + LatLng newCenter = _mapCamera.center; + double newZoom = _mapCamera.zoom; // Handle pinch zoom. if (hasPinchZoom && details.scale > 0.0) { @@ -540,17 +540,19 @@ class FlutterMapInteractiveViewerState ScaleUpdateDetails details, double zoomAfterPinchZoom, ) { - final oldCenterPt = _mapFrame.project(_mapFrame.center, zoomAfterPinchZoom); + final oldCenterPt = + _mapCamera.project(_mapCamera.center, zoomAfterPinchZoom); final newFocalLatLong = - _mapFrame.offsetToCrs(_focalStartLocal, zoomAfterPinchZoom); - final newFocalPt = _mapFrame.project(newFocalLatLong, zoomAfterPinchZoom); - final oldFocalPt = _mapFrame.project(_focalStartLatLng, zoomAfterPinchZoom); + _mapCamera.offsetToCrs(_focalStartLocal, zoomAfterPinchZoom); + final newFocalPt = _mapCamera.project(newFocalLatLong, zoomAfterPinchZoom); + final oldFocalPt = + _mapCamera.project(_focalStartLatLng, zoomAfterPinchZoom); final zoomDifference = oldFocalPt - newFocalPt; final moveDifference = _rotateOffset(_focalStartLocal - _lastFocalLocal); final newCenterPt = oldCenterPt + zoomDifference + moveDifference.toCustomPoint(); - return _mapFrame.unproject(newCenterPt, zoomAfterPinchZoom); + return _mapCamera.unproject(newCenterPt, zoomAfterPinchZoom); } void _handleScalePinchRotate( @@ -564,17 +566,17 @@ class FlutterMapInteractiveViewerState if (_rotationStarted) { final rotationDiff = currentRotation - _lastRotation; - final oldCenterPt = _mapFrame.project(_mapFrame.center); + final oldCenterPt = _mapCamera.project(_mapCamera.center); final rotationCenter = - _mapFrame.project(_mapFrame.offsetToCrs(_lastFocalLocal)); + _mapCamera.project(_mapCamera.offsetToCrs(_lastFocalLocal)); final vector = oldCenterPt - rotationCenter; final rotatedVector = vector.rotate(degToRadian(rotationDiff)); final newCenter = rotationCenter + rotatedVector; widget.controller.moveAndRotate( - _mapFrame.unproject(newCenter), - _mapFrame.zoom, - _mapFrame.rotation + rotationDiff, + _mapCamera.unproject(newCenter), + _mapCamera.zoom, + _mapCamera.rotation + rotationDiff, offset: Offset.zero, hasGesture: true, source: MapEventSource.onMultiFinger, @@ -640,7 +642,7 @@ class FlutterMapInteractiveViewerState final direction = details.velocity.pixelsPerSecond / magnitude; final distance = (Offset.zero & - Size(_mapFrame.nonRotatedSize.x, _mapFrame.nonRotatedSize.y)) + Size(_mapCamera.nonRotatedSize.x, _mapCamera.nonRotatedSize.y)) .shortestSide; final flingOffset = _focalStartLocal - _lastFocalLocal; @@ -670,7 +672,7 @@ class FlutterMapInteractiveViewerState widget.controller.tapped( MapEventSource.tap, position, - _mapFrame.offsetToCrs(relativePosition), + _mapCamera.offsetToCrs(relativePosition), ); } @@ -684,7 +686,7 @@ class FlutterMapInteractiveViewerState widget.controller.secondaryTapped( MapEventSource.secondaryTap, position, - _mapFrame.offsetToCrs(relativePosition), + _mapCamera.offsetToCrs(relativePosition), ); } @@ -697,7 +699,7 @@ class FlutterMapInteractiveViewerState widget.controller.longPressed( MapEventSource.longPress, position, - _mapFrame.offsetToCrs(position.relative!), + _mapCamera.offsetToCrs(position.relative!), ); } @@ -708,8 +710,8 @@ class FlutterMapInteractiveViewerState _closeDoubleTapController(MapEventSource.doubleTap); if (InteractiveFlag.hasDoubleTapZoom(_interactionOptions.flags)) { - final newZoom = _getZoomForScale(_mapFrame.zoom, 2); - final newCenter = _mapFrame.focusedZoomCenter( + final newZoom = _getZoomForScale(_mapCamera.zoom, 2); + final newCenter = _mapCamera.focusedZoomCenter( tapPosition.relative!.toCustomPoint(), newZoom, ); @@ -718,11 +720,12 @@ class FlutterMapInteractiveViewerState } void _startDoubleTapAnimation(double newZoom, LatLng newCenter) { - _doubleTapZoomAnimation = Tween(begin: _mapFrame.zoom, end: newZoom) - .chain(CurveTween(curve: Curves.linear)) - .animate(_doubleTapController); + _doubleTapZoomAnimation = + Tween(begin: _mapCamera.zoom, end: newZoom) + .chain(CurveTween(curve: Curves.linear)) + .animate(_doubleTapController); _doubleTapCenterAnimation = - LatLngTween(begin: _mapFrame.center, end: newCenter) + LatLngTween(begin: _mapCamera.center, end: newCenter) .chain(CurveTween(curve: Curves.linear)) .animate(_doubleTapController); _doubleTapController.forward(from: 0); @@ -769,14 +772,14 @@ class FlutterMapInteractiveViewerState final flags = _interactionOptions.flags; if (InteractiveFlag.hasPinchZoom(flags)) { final verticalOffset = (_focalStartLocal - details.localFocalPoint).dy; - final newZoom = _mapZoomStart - verticalOffset / 360 * _mapFrame.zoom; + final newZoom = _mapZoomStart - verticalOffset / 360 * _mapCamera.zoom; final min = _options.minZoom ?? 0.0; final max = _options.maxZoom ?? double.infinity; final actualZoom = math.max(min, math.min(max, newZoom)); widget.controller.move( - _mapFrame.center, + _mapCamera.center, actualZoom, offset: Offset.zero, hasGesture: true, @@ -793,13 +796,13 @@ class FlutterMapInteractiveViewerState _startListeningForAnimationInterruptions(); } - final newCenterPoint = _mapFrame.project(_mapCenterStart) + - _flingAnimation.value.toCustomPoint().rotate(_mapFrame.rotationRad); - final newCenter = _mapFrame.unproject(newCenterPoint); + final newCenterPoint = _mapCamera.project(_mapCenterStart) + + _flingAnimation.value.toCustomPoint().rotate(_mapCamera.rotationRad); + final newCenter = _mapCamera.unproject(newCenterPoint); widget.controller.move( newCenter, - _mapFrame.zoom, + _mapCamera.zoom, offset: Offset.zero, hasGesture: true, source: MapEventSource.flingAnimationController, @@ -838,11 +841,11 @@ class FlutterMapInteractiveViewerState double _getZoomForScale(double startZoom, double scale) { final resultZoom = scale == 1.0 ? startZoom : startZoom + math.log(scale) / math.ln2; - return _mapFrame.clampZoom(resultZoom); + return _mapCamera.clampZoom(resultZoom); } Offset _rotateOffset(Offset offset) { - final radians = _mapFrame.rotationRad; + final radians = _mapCamera.rotationRad; if (radians != 0.0) { final cos = math.cos(radians); final sin = math.sin(radians); diff --git a/lib/src/gestures/map_events.dart b/lib/src/gestures/map_events.dart index 5810375cd..08825a415 100644 --- a/lib/src/gestures/map_events.dart +++ b/lib/src/gestures/map_events.dart @@ -1,4 +1,4 @@ -import 'package:flutter_map/src/map/flutter_map_frame.dart'; +import 'package:flutter_map/src/map/camera.dart'; import 'package:latlong2/latlong.dart'; /// Event sources which are used to identify different types of @@ -19,7 +19,7 @@ enum MapEventSource { flingAnimationController, doubleTapZoomAnimationController, interactiveFlagsChanged, - fitFrame, + fitCamera, custom, scrollWheel, nonRotatedSizeChange, @@ -32,12 +32,12 @@ abstract class MapEvent { /// Who / what issued the event. final MapEventSource source; - /// The map frame after the event. - final MapFrame mapFrame; + /// The map camera after the event. + final MapCamera mapCamera; const MapEvent({ required this.source, - required this.mapFrame, + required this.mapCamera, }); } @@ -45,38 +45,38 @@ abstract class MapEvent { /// includes information about camera movement /// which are not partial (e.g start rotate, rotate, end rotate). abstract class MapEventWithMove extends MapEvent { - final MapFrame oldMapFrame; + final MapCamera oldMapCamera; const MapEventWithMove({ required super.source, - required this.oldMapFrame, - required super.mapFrame, + required this.oldMapCamera, + required super.mapCamera, }); /// Returns a subclass of [MapEventWithMove] if the [source] belongs to a /// movement event, otherwise returns null. static MapEventWithMove? fromSource({ - required MapFrame oldMapFrame, - required MapFrame mapFrame, + required MapCamera oldMapCamera, + required MapCamera mapCamera, required bool hasGesture, required MapEventSource source, String? id, }) => switch (source) { MapEventSource.flingAnimationController => MapEventFlingAnimation( - oldMapFrame: oldMapFrame, - mapFrame: mapFrame, + oldMapCamera: oldMapCamera, + mapCamera: mapCamera, source: source, ), MapEventSource.doubleTapZoomAnimationController => MapEventDoubleTapZoom( - oldMapFrame: oldMapFrame, - mapFrame: mapFrame, + oldMapCamera: oldMapCamera, + mapCamera: mapCamera, source: source, ), MapEventSource.scrollWheel => MapEventScrollWheelZoom( - oldMapFrame: oldMapFrame, - mapFrame: mapFrame, + oldMapCamera: oldMapCamera, + mapCamera: mapCamera, source: source, ), MapEventSource.onDrag || @@ -85,8 +85,8 @@ abstract class MapEventWithMove extends MapEvent { MapEventSource.custom => MapEventMove( id: id, - oldMapFrame: oldMapFrame, - mapFrame: mapFrame, + oldMapCamera: oldMapCamera, + mapCamera: mapCamera, source: source, ), _ => null, @@ -101,7 +101,7 @@ class MapEventTap extends MapEvent { const MapEventTap({ required this.tapPosition, required super.source, - required super.mapFrame, + required super.mapCamera, }); } @@ -112,7 +112,7 @@ class MapEventSecondaryTap extends MapEvent { const MapEventSecondaryTap({ required this.tapPosition, required super.source, - required super.mapFrame, + required super.mapCamera, }); } @@ -124,7 +124,7 @@ class MapEventLongPress extends MapEvent { const MapEventLongPress({ required this.tapPosition, required super.source, - required super.mapFrame, + required super.mapCamera, }); } @@ -136,8 +136,8 @@ class MapEventMove extends MapEventWithMove { const MapEventMove({ this.id, required super.source, - required super.oldMapFrame, - required super.mapFrame, + required super.oldMapCamera, + required super.mapCamera, }); } @@ -145,7 +145,7 @@ class MapEventMove extends MapEventWithMove { class MapEventMoveStart extends MapEvent { const MapEventMoveStart({ required super.source, - required super.mapFrame, + required super.mapCamera, }); } @@ -153,7 +153,7 @@ class MapEventMoveStart extends MapEvent { class MapEventMoveEnd extends MapEvent { const MapEventMoveEnd({ required super.source, - required super.mapFrame, + required super.mapCamera, }); } @@ -161,8 +161,8 @@ class MapEventMoveEnd extends MapEvent { class MapEventFlingAnimation extends MapEventWithMove { const MapEventFlingAnimation({ required super.source, - required super.oldMapFrame, - required super.mapFrame, + required super.oldMapCamera, + required super.mapCamera, }); } @@ -171,7 +171,7 @@ class MapEventFlingAnimation extends MapEventWithMove { class MapEventFlingAnimationNotStarted extends MapEvent { const MapEventFlingAnimationNotStarted({ required super.source, - required super.mapFrame, + required super.mapCamera, }); } @@ -179,7 +179,7 @@ class MapEventFlingAnimationNotStarted extends MapEvent { class MapEventFlingAnimationStart extends MapEvent { const MapEventFlingAnimationStart({ required super.source, - required super.mapFrame, + required super.mapCamera, }); } @@ -187,7 +187,7 @@ class MapEventFlingAnimationStart extends MapEvent { class MapEventFlingAnimationEnd extends MapEvent { const MapEventFlingAnimationEnd({ required super.source, - required super.mapFrame, + required super.mapCamera, }); } @@ -195,8 +195,8 @@ class MapEventFlingAnimationEnd extends MapEvent { class MapEventDoubleTapZoom extends MapEventWithMove { const MapEventDoubleTapZoom({ required super.source, - required super.oldMapFrame, - required super.mapFrame, + required super.oldMapCamera, + required super.mapCamera, }); } @@ -204,8 +204,8 @@ class MapEventDoubleTapZoom extends MapEventWithMove { class MapEventScrollWheelZoom extends MapEventWithMove { const MapEventScrollWheelZoom({ required super.source, - required super.oldMapFrame, - required super.mapFrame, + required super.oldMapCamera, + required super.mapCamera, }); } @@ -213,7 +213,7 @@ class MapEventScrollWheelZoom extends MapEventWithMove { class MapEventDoubleTapZoomStart extends MapEvent { const MapEventDoubleTapZoomStart({ required super.source, - required super.mapFrame, + required super.mapCamera, }); } @@ -221,7 +221,7 @@ class MapEventDoubleTapZoomStart extends MapEvent { class MapEventDoubleTapZoomEnd extends MapEvent { const MapEventDoubleTapZoomEnd({ required super.source, - required super.mapFrame, + required super.mapCamera, }); } @@ -233,8 +233,8 @@ class MapEventRotate extends MapEventWithMove { const MapEventRotate({ required this.id, required super.source, - required super.oldMapFrame, - required super.mapFrame, + required super.oldMapCamera, + required super.mapCamera, }); } @@ -242,21 +242,21 @@ class MapEventRotate extends MapEventWithMove { class MapEventRotateStart extends MapEvent { const MapEventRotateStart({ required super.source, - required super.mapFrame, + required super.mapCamera, }); } class MapEventRotateEnd extends MapEvent { const MapEventRotateEnd({ required super.source, - required super.mapFrame, + required super.mapCamera, }); } class MapEventNonRotatedSizeChange extends MapEventWithMove { const MapEventNonRotatedSizeChange({ required super.source, - required super.oldMapFrame, - required super.mapFrame, + required super.oldMapCamera, + required super.mapCamera, }); } diff --git a/lib/src/layer/circle_layer.dart b/lib/src/layer/circle_layer.dart index d80690e43..0d7faa158 100644 --- a/lib/src/layer/circle_layer.dart +++ b/lib/src/layer/circle_layer.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/map/flutter_map_frame.dart'; +import 'package:flutter_map/src/map/camera.dart'; import 'package:latlong2/latlong.dart' hide Path; class CircleMarker { @@ -36,7 +36,7 @@ class CircleLayer extends StatelessWidget { return LayoutBuilder( builder: (BuildContext context, BoxConstraints bc) { final size = Size(bc.maxWidth, bc.maxHeight); - final map = MapFrame.of(context); + final map = MapCamera.of(context); final circleWidgets = []; for (final circle in circles) { circle.offset = map.getOffsetFromOrigin(circle.point); diff --git a/lib/src/layer/marker_layer.dart b/lib/src/layer/marker_layer.dart index 6076d165e..4ed5cf87d 100644 --- a/lib/src/layer/marker_layer.dart +++ b/lib/src/layer/marker_layer.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/flutter_map_frame.dart'; +import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; @@ -192,7 +192,7 @@ class MarkerLayer extends StatelessWidget { @override Widget build(BuildContext context) { - final map = MapFrame.of(context); + final map = MapCamera.of(context); final markerWidgets = []; for (final marker in markers) { diff --git a/lib/src/layer/overlay_image_layer.dart b/lib/src/layer/overlay_image_layer.dart index 32dafe999..14d354a57 100644 --- a/lib/src/layer/overlay_image_layer.dart +++ b/lib/src/layer/overlay_image_layer.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/flutter_map_frame.dart'; +import 'package:flutter_map/src/map/camera.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; @@ -12,7 +12,7 @@ abstract class BaseOverlayImage { bool get gaplessPlayback; - Positioned buildPositionedForOverlay(MapFrame map); + Positioned buildPositionedForOverlay(MapCamera map); Image buildImageForOverlay() { return Image( @@ -45,7 +45,7 @@ class OverlayImage extends BaseOverlayImage { this.gaplessPlayback = false}); @override - Positioned buildPositionedForOverlay(MapFrame map) { + Positioned buildPositionedForOverlay(MapCamera map) { // northWest is not necessarily upperLeft depending on projection final bounds = Bounds( map.project(this.bounds.northWest) - map.pixelOrigin, @@ -92,7 +92,7 @@ class RotatedOverlayImage extends BaseOverlayImage { this.filterQuality = FilterQuality.medium}); @override - Positioned buildPositionedForOverlay(MapFrame map) { + Positioned buildPositionedForOverlay(MapCamera map) { final pxTopLeft = map.project(topLeftCorner) - map.pixelOrigin; final pxBottomRight = map.project(bottomRightCorner) - map.pixelOrigin; final pxBottomLeft = map.project(bottomLeftCorner) - map.pixelOrigin; @@ -136,7 +136,7 @@ class OverlayImageLayer extends StatelessWidget { @override Widget build(BuildContext context) { - final map = MapFrame.of(context); + final map = MapCamera.of(context); return ClipRect( child: Stack( children: [ diff --git a/lib/src/layer/polygon_layer.dart b/lib/src/layer/polygon_layer.dart index 1e251bc3d..f24076aba 100644 --- a/lib/src/layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer.dart @@ -3,7 +3,7 @@ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/layer/label.dart'; -import 'package:flutter_map/src/map/flutter_map_frame.dart'; +import 'package:flutter_map/src/map/camera.dart'; import 'package:latlong2/latlong.dart' hide Path; // conflict with Path from UI enum PolygonLabelPlacement { @@ -78,7 +78,7 @@ class PolygonLayer extends StatelessWidget { @override Widget build(BuildContext context) { - final map = MapFrame.of(context); + final map = MapCamera.of(context); final size = Size(map.size.x, map.size.y); final List pgons = polygonCulling @@ -97,7 +97,7 @@ class PolygonLayer extends StatelessWidget { class PolygonPainter extends CustomPainter { final List polygons; - final MapFrame map; + final MapCamera map; final LatLngBounds bounds; PolygonPainter(this.polygons, this.map) : bounds = map.visibleBounds; diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index a888609ed..48b07480b 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -3,7 +3,7 @@ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/map/flutter_map_frame.dart'; +import 'package:flutter_map/src/map/camera.dart'; import 'package:latlong2/latlong.dart'; class Polyline { @@ -66,7 +66,7 @@ class PolylineLayer extends StatelessWidget { @override Widget build(BuildContext context) { - final map = MapFrame.of(context); + final map = MapCamera.of(context); return CustomPaint( painter: PolylinePainter( @@ -86,7 +86,7 @@ class PolylineLayer extends StatelessWidget { class PolylinePainter extends CustomPainter { final List polylines; - final MapFrame map; + final MapCamera map; final LatLngBounds bounds; PolylinePainter(this.polylines, this.map) : bounds = map.visibleBounds; diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index dfac4e1fe..758eac420 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -286,7 +286,7 @@ class TileLayer extends StatefulWidget { } class _TileLayerState extends State with TickerProviderStateMixin { - bool _initializedFromMapFrame = false; + bool _initializedFromMapCamera = false; final TileImageManager _tileImageManager = TileImageManager(); late TileBounds _tileBounds; @@ -329,7 +329,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { void didChangeDependencies() { super.didChangeDependencies(); - final mapFrame = MapFrame.of(context); + final mapCamera = MapCamera.of(context); final mapController = MapController.of(context); if (_mapControllerHashCode != mapController.hashCode) { @@ -343,29 +343,29 @@ class _TileLayerState extends State with TickerProviderStateMixin { } bool reloadTiles = false; - if (!_initializedFromMapFrame || + if (!_initializedFromMapCamera || _tileBounds.shouldReplace( - mapFrame.crs, widget.tileSize, widget.tileBounds)) { + mapCamera.crs, widget.tileSize, widget.tileBounds)) { reloadTiles = true; _tileBounds = TileBounds( - crs: mapFrame.crs, + crs: mapCamera.crs, tileSize: widget.tileSize, latLngBounds: widget.tileBounds, ); } - if (!_initializedFromMapFrame || - _tileScaleCalculator.shouldReplace(mapFrame.crs, widget.tileSize)) { + if (!_initializedFromMapCamera || + _tileScaleCalculator.shouldReplace(mapCamera.crs, widget.tileSize)) { reloadTiles = true; _tileScaleCalculator = TileScaleCalculator( - crs: mapFrame.crs, + crs: mapCamera.crs, tileSize: widget.tileSize, ); } - if (reloadTiles) _loadAndPruneInVisibleBounds(mapFrame); + if (reloadTiles) _loadAndPruneInVisibleBounds(mapCamera); - _initializedFromMapFrame = true; + _initializedFromMapCamera = true; } @override @@ -421,7 +421,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { if (reloadTiles) { _tileImageManager.removeAll(widget.evictErrorTileStrategy); - _loadAndPruneInVisibleBounds(MapFrame.maybeOf(context)!); + _loadAndPruneInVisibleBounds(MapCamera.maybeOf(context)!); } else if (oldWidget.tileDisplay != widget.tileDisplay) { _tileImageManager.updateTileDisplay(widget.tileDisplay); } @@ -440,14 +440,14 @@ class _TileLayerState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - final map = MapFrame.of(context); + final map = MapCamera.of(context); if (_outsideZoomLimits(map.zoom.round())) return const SizedBox.shrink(); final tileZoom = _clampToNativeZoom(map.zoom); final tileBoundsAtZoom = _tileBounds.atZoom(tileZoom); final visibleTileRange = _tileRangeCalculator.calculate( - mapFrame: map, + mapCamera: map, tileZoom: tileZoom, ); @@ -522,7 +522,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { void _onTileUpdateEvent(TileUpdateEvent event) { final tileZoom = _clampToNativeZoom(event.zoom); final visibleTileRange = _tileRangeCalculator.calculate( - mapFrame: event.mapFrame, + mapCamera: event.mapCamera, tileZoom: tileZoom, center: event.center, viewingZoom: event.zoom, @@ -540,10 +540,10 @@ class _TileLayerState extends State with TickerProviderStateMixin { } // Load new tiles in the visible bounds and prune those outside. - void _loadAndPruneInVisibleBounds(MapFrame mapFrame) { - final tileZoom = _clampToNativeZoom(mapFrame.zoom); + void _loadAndPruneInVisibleBounds(MapCamera mapCamera) { + final tileZoom = _clampToNativeZoom(mapCamera.zoom); final visibleTileRange = _tileRangeCalculator.calculate( - mapFrame: mapFrame, + mapCamera: mapCamera, tileZoom: tileZoom, ); diff --git a/lib/src/layer/tile_layer/tile_range_calculator.dart b/lib/src/layer/tile_layer/tile_range_calculator.dart index c46eab45d..4abce5c51 100644 --- a/lib/src/layer/tile_layer/tile_range_calculator.dart +++ b/lib/src/layer/tile_layer/tile_range_calculator.dart @@ -1,5 +1,5 @@ import 'package:flutter_map/src/layer/tile_layer/tile_range.dart'; -import 'package:flutter_map/src/map/flutter_map_frame.dart'; +import 'package:flutter_map/src/map/camera.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; @@ -12,38 +12,38 @@ class TileRangeCalculator { /// viewing the map from the [viewingZoom] centered at the [center]. The /// resulting tile range is expanded by [panBuffer]. DiscreteTileRange calculate({ - // The map frame used to calculate the bounds. - required MapFrame mapFrame, + // The map camera used to calculate the bounds. + required MapCamera mapCamera, // The zoom level at which the bounds should be calculated. required int tileZoom, - // The center from which the map is viewed, defaults to [mapFrame.center]. + // The center from which the map is viewed, defaults to [mapCamera.center]. LatLng? center, - // The zoom from which the map is viewed, defaults to [mapFrame.zoom]. + // The zoom from which the map is viewed, defaults to [mapCamera.zoom]. double? viewingZoom, }) { return DiscreteTileRange.fromPixelBounds( zoom: tileZoom, tileSize: tileSize, pixelBounds: _calculatePixelBounds( - mapFrame, - center ?? mapFrame.center, - viewingZoom ?? mapFrame.zoom, + mapCamera, + center ?? mapCamera.center, + viewingZoom ?? mapCamera.zoom, tileZoom, ), ); } Bounds _calculatePixelBounds( - MapFrame mapFrame, + MapCamera mapCamera, LatLng center, double viewingZoom, int tileZoom, ) { final tileZoomDouble = tileZoom.toDouble(); - final scale = mapFrame.getZoomScale(viewingZoom, tileZoomDouble); + final scale = mapCamera.getZoomScale(viewingZoom, tileZoomDouble); final pixelCenter = - mapFrame.project(center, tileZoomDouble).floor().toDoublePoint(); - final halfSize = mapFrame.size / (scale * 2); + mapCamera.project(center, tileZoomDouble).floor().toDoublePoint(); + final halfSize = mapCamera.size / (scale * 2); return Bounds(pixelCenter - halfSize, pixelCenter + halfSize); } diff --git a/lib/src/layer/tile_layer/tile_update_event.dart b/lib/src/layer/tile_layer/tile_update_event.dart index 5ba0c84fd..8e25e3eb8 100644 --- a/lib/src/layer/tile_layer/tile_update_event.dart +++ b/lib/src/layer/tile_layer/tile_update_event.dart @@ -1,5 +1,5 @@ import 'package:flutter_map/src/gestures/map_events.dart'; -import 'package:flutter_map/src/map/flutter_map_frame.dart'; +import 'package:flutter_map/src/map/camera.dart'; import 'package:latlong2/latlong.dart'; /// Describes whether loading and/or pruning should occur and allows overriding @@ -19,11 +19,11 @@ class TileUpdateEvent { this.loadZoomOverride, }); - double get zoom => loadZoomOverride ?? mapEvent.mapFrame.zoom; + double get zoom => loadZoomOverride ?? mapEvent.mapCamera.zoom; - LatLng get center => loadCenterOverride ?? mapEvent.mapFrame.center; + LatLng get center => loadCenterOverride ?? mapEvent.mapCamera.center; - MapFrame get mapFrame => mapEvent.mapFrame; + MapCamera get mapCamera => mapEvent.mapCamera; /// Returns a copy of this TileUpdateEvent with only pruning enabled and the /// loadCenterOverride/loadZoomOverride removed. diff --git a/lib/src/map/flutter_map_frame.dart b/lib/src/map/camera.dart similarity index 90% rename from lib/src/map/flutter_map_frame.dart rename to lib/src/map/camera.dart index d5c0e846b..b88848b53 100644 --- a/lib/src/map/flutter_map_frame.dart +++ b/lib/src/map/camera.dart @@ -9,7 +9,7 @@ import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; -class MapFrame { +class MapCamera { // During Flutter startup the native platform resolution is not immediately // available which can cause constraints to be zero before they are updated // in a subsequent build to the actual constraints. We set the size to this @@ -29,23 +29,23 @@ class MapFrame { final CustomPoint nonRotatedSize; // Lazily calculated fields. - CustomPoint? _frameSize; + CustomPoint? _cameraSize; Bounds? _pixelBounds; LatLngBounds? _bounds; CustomPoint? _pixelOrigin; double? _rotationRad; - static MapFrame? maybeOf(BuildContext context) => - FlutterMapInheritedModel.maybeFrameOf(context); + static MapCamera? maybeOf(BuildContext context) => + FlutterMapInheritedModel.maybeCameraOf(context); - static MapFrame of(BuildContext context) => + static MapCamera of(BuildContext context) => maybeOf(context) ?? (throw StateError( - '`MapFrame.of()` should not be called outside a `FlutterMap` and its descendants')); + '`MapCamera.of()` should not be called outside a `FlutterMap` and its descendants')); - /// Initializes [MapFrame] from the given [options] and with the + /// Initializes [MapCamera] from the given [options] and with the /// [nonRotatedSize] set to [kImpossibleSize]. - MapFrame.initialFrame(MapOptions options) + MapCamera.initialCamera(MapOptions options) : crs = options.crs, minZoom = options.minZoom, maxZoom = options.maxZoom, @@ -54,10 +54,10 @@ class MapFrame { rotation = options.initialRotation, nonRotatedSize = kImpossibleSize; - // Create an instance of [MapFrame]. The [pixelOrigin], [bounds], and + // Create an instance of [MapCamera]. The [pixelOrigin], [bounds], and // [pixelBounds] may be set if they are known already. Otherwise if left // null they will be calculated lazily when they are used. - MapFrame({ + MapCamera({ required this.crs, required this.center, required this.zoom, @@ -69,15 +69,15 @@ class MapFrame { Bounds? pixelBounds, LatLngBounds? bounds, CustomPoint? pixelOrigin, - }) : _frameSize = size ?? calculateRotatedSize(rotation, nonRotatedSize), + }) : _cameraSize = size ?? calculateRotatedSize(rotation, nonRotatedSize), _pixelBounds = pixelBounds, _bounds = bounds, _pixelOrigin = pixelOrigin; - MapFrame withNonRotatedSize(CustomPoint nonRotatedSize) { + MapCamera withNonRotatedSize(CustomPoint nonRotatedSize) { if (nonRotatedSize == this.nonRotatedSize) return this; - return MapFrame( + return MapCamera( crs: crs, minZoom: minZoom, maxZoom: maxZoom, @@ -88,10 +88,10 @@ class MapFrame { ); } - MapFrame withRotation(double rotation) { + MapCamera withRotation(double rotation) { if (rotation == this.rotation) return this; - return MapFrame( + return MapCamera( crs: crs, minZoom: minZoom, maxZoom: maxZoom, @@ -102,14 +102,14 @@ class MapFrame { ); } - MapFrame withOptions(MapOptions options) { + MapCamera withOptions(MapOptions options) { if (options.crs == crs && options.minZoom == minZoom && options.maxZoom == maxZoom) { return this; } - return MapFrame( + return MapCamera( crs: options.crs, minZoom: options.minZoom, maxZoom: options.maxZoom, @@ -117,15 +117,15 @@ class MapFrame { zoom: zoom, rotation: rotation, nonRotatedSize: nonRotatedSize, - size: _frameSize, + size: _cameraSize, ); } - MapFrame withPosition({ + MapCamera withPosition({ LatLng? center, double? zoom, }) => - MapFrame( + MapCamera( crs: crs, minZoom: minZoom, maxZoom: maxZoom, @@ -133,7 +133,7 @@ class MapFrame { zoom: zoom ?? this.zoom, rotation: rotation, nonRotatedSize: nonRotatedSize, - size: _frameSize, + size: _cameraSize, ); @Deprecated('Use visibleBounds instead.') @@ -147,7 +147,7 @@ class MapFrame { )); CustomPoint get size => - _frameSize ?? + _cameraSize ?? calculateRotatedSize( rotation, nonRotatedSize, diff --git a/lib/src/map/flutter_map_inherited_model.dart b/lib/src/map/flutter_map_inherited_model.dart index f5486b755..f956fef3b 100644 --- a/lib/src/map/flutter_map_inherited_model.dart +++ b/lib/src/map/flutter_map_inherited_model.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/src/map/flutter_map_frame.dart'; +import 'package:flutter_map/src/map/camera.dart'; import 'package:flutter_map/src/map/map_controller.dart'; import 'package:flutter_map/src/map/options.dart'; @@ -8,12 +8,12 @@ class FlutterMapInheritedModel extends InheritedModel<_FlutterMapAspect> { FlutterMapInheritedModel({ super.key, - required MapFrame frame, + required MapCamera camera, required MapController controller, required MapOptions options, required super.child, }) : data = FlutterMapData( - frame: frame, + camera: camera, controller: controller, options: options, ); @@ -26,8 +26,8 @@ class FlutterMapInheritedModel extends InheritedModel<_FlutterMapAspect> { aspect: aspect) ?.data; - static MapFrame? maybeFrameOf(BuildContext context) => - _maybeOf(context, _FlutterMapAspect.frame)?.frame; + static MapCamera? maybeCameraOf(BuildContext context) => + _maybeOf(context, _FlutterMapAspect.camera)?.camera; static MapController? maybeControllerOf(BuildContext context) => _maybeOf(context, _FlutterMapAspect.controller)?.controller; @@ -45,8 +45,8 @@ class FlutterMapInheritedModel extends InheritedModel<_FlutterMapAspect> { for (final dependency in dependencies) { if (dependency is _FlutterMapAspect) { switch (dependency) { - case _FlutterMapAspect.frame: - if (data.frame != oldWidget.data.frame) return true; + case _FlutterMapAspect.camera: + if (data.camera != oldWidget.data.camera) return true; case _FlutterMapAspect.controller: if (data.controller != oldWidget.data.controller) return true; case _FlutterMapAspect.options: @@ -60,19 +60,19 @@ class FlutterMapInheritedModel extends InheritedModel<_FlutterMapAspect> { } class FlutterMapData { - final MapFrame frame; + final MapCamera camera; final MapController controller; final MapOptions options; const FlutterMapData({ - required this.frame, + required this.camera, required this.controller, required this.options, }); } enum _FlutterMapAspect { - frame, + camera, controller, options; } diff --git a/lib/src/map/flutter_map_internal_controller.dart b/lib/src/map/flutter_map_internal_controller.dart index 270f8084c..17d106919 100644 --- a/lib/src/map/flutter_map_internal_controller.dart +++ b/lib/src/map/flutter_map_internal_controller.dart @@ -17,7 +17,7 @@ class FlutterMapInternalController : super( FlutterMapInternalState( options: options, - mapFrame: MapFrame.initialFrame(options), + mapCamera: MapCamera.initialCamera(options), ), ); @@ -29,7 +29,7 @@ class FlutterMapInternalController _interactiveViewerState = interactiveViewerState; MapOptions get options => value.options; - MapFrame get mapFrame => value.mapFrame; + MapCamera get mapCamera => value.mapCamera; void linkMapController(MapControllerImpl mapControllerImpl) { _mapControllerImpl = mapControllerImpl; @@ -55,9 +55,9 @@ class FlutterMapInternalController }) { // Algorithm thanks to https://github.com/tlserver/flutter_map_location_marker if (offset != Offset.zero) { - final newPoint = mapFrame.project(newCenter, newZoom); - newCenter = mapFrame.unproject( - mapFrame.rotatePoint( + final newPoint = mapCamera.project(newCenter, newZoom); + newCenter = mapCamera.unproject( + mapCamera.rotatePoint( newPoint, newPoint - CustomPoint(offset.dx, offset.dy), ), @@ -65,24 +65,24 @@ class FlutterMapInternalController ); } - MapFrame? newMapFrame = mapFrame.withPosition( + MapCamera? newMapCamera = mapCamera.withPosition( center: newCenter, - zoom: mapFrame.clampZoom(newZoom), + zoom: mapCamera.clampZoom(newZoom), ); - newMapFrame = options.frameConstraint.constrain(newMapFrame); - if (newMapFrame == null || - (newMapFrame.center == mapFrame.center && - newMapFrame.zoom == mapFrame.zoom)) { + newMapCamera = options.cameraConstraint.constrain(newMapCamera); + if (newMapCamera == null || + (newMapCamera.center == mapCamera.center && + newMapCamera.zoom == mapCamera.zoom)) { return false; } - final oldMapFrame = mapFrame; - value = value.withMapFrame(newMapFrame); + final oldMapCamera = mapCamera; + value = value.withMapCamera(newMapCamera); final movementEvent = MapEventWithMove.fromSource( - oldMapFrame: oldMapFrame, - mapFrame: mapFrame, + oldMapCamera: oldMapCamera, + mapCamera: mapCamera, hasGesture: hasGesture, source: source, id: id, @@ -92,7 +92,7 @@ class FlutterMapInternalController options.onPositionChanged?.call( MapPosition( center: newCenter, - bounds: mapFrame.visibleBounds, + bounds: mapCamera.visibleBounds, zoom: newZoom, hasGesture: hasGesture, ), @@ -111,23 +111,23 @@ class FlutterMapInternalController required MapEventSource source, required String? id, }) { - if (newRotation != mapFrame.rotation) { - final newMapFrame = options.frameConstraint.constrain( - mapFrame.withRotation(newRotation), + if (newRotation != mapCamera.rotation) { + final newMapCamera = options.cameraConstraint.constrain( + mapCamera.withRotation(newRotation), ); - if (newMapFrame == null) return false; + if (newMapCamera == null) return false; - final oldMapFrame = mapFrame; + final oldMapCamera = mapCamera; - // Update frame then emit events and callbacks - value = value.withMapFrame(newMapFrame); + // Update camera then emit events and callbacks + value = value.withMapCamera(newMapCamera); _emitMapEvent( MapEventRotate( id: id, source: source, - oldMapFrame: oldMapFrame, - mapFrame: mapFrame, + oldMapCamera: oldMapCamera, + mapCamera: mapCamera, ), ); return true; @@ -154,7 +154,7 @@ class FlutterMapInternalController throw ArgumentError('One of `point` or `offset` must be non-null'); } - if (degree == mapFrame.rotation) { + if (degree == mapCamera.rotation) { return MoveAndRotateResult(false, false); } @@ -170,28 +170,28 @@ class FlutterMapInternalController ); } - final rotationDiff = degree - mapFrame.rotation; - final rotationCenter = mapFrame.project(mapFrame.center) + + final rotationDiff = degree - mapCamera.rotation; + final rotationCenter = mapCamera.project(mapCamera.center) + (point != null - ? (point - (mapFrame.nonRotatedSize / 2.0)) + ? (point - (mapCamera.nonRotatedSize / 2.0)) : CustomPoint(offset!.dx, offset.dy)) - .rotate(mapFrame.rotationRad); + .rotate(mapCamera.rotationRad); return MoveAndRotateResult( move( - mapFrame.unproject( + mapCamera.unproject( rotationCenter + - (mapFrame.project(mapFrame.center) - rotationCenter) + (mapCamera.project(mapCamera.center) - rotationCenter) .rotate(degToRadian(rotationDiff)), ), - mapFrame.zoom, + mapCamera.zoom, offset: Offset.zero, hasGesture: hasGesture, source: source, id: id, ), rotate( - mapFrame.rotation + rotationDiff, + mapCamera.rotation + rotationDiff, hasGesture: hasGesture, source: source, id: id, @@ -226,18 +226,18 @@ class FlutterMapInternalController // Note: All named parameters are required to prevent inconsistent default // values since this method can be called by MapController which declares // defaults. - bool fitFrame( - FrameFit frameFit, { + bool fitCamera( + CameraFit cameraFit, { required Offset offset, }) { - final fitted = frameFit.fit(mapFrame); + final fitted = cameraFit.fit(mapCamera); return move( fitted.center, fitted.zoom, offset: offset, hasGesture: false, - source: MapEventSource.fitFrame, + source: MapEventSource.fitCamera, id: null, ); } @@ -245,9 +245,9 @@ class FlutterMapInternalController bool setNonRotatedSizeWithoutEmittingEvent( CustomPoint nonRotatedSize, ) { - if (nonRotatedSize != MapFrame.kImpossibleSize && - nonRotatedSize != mapFrame.nonRotatedSize) { - value = value.withMapFrame(mapFrame.withNonRotatedSize(nonRotatedSize)); + if (nonRotatedSize != MapCamera.kImpossibleSize && + nonRotatedSize != mapCamera.nonRotatedSize) { + value = value.withMapCamera(mapCamera.withNonRotatedSize(nonRotatedSize)); return true; } @@ -260,11 +260,11 @@ class FlutterMapInternalController 'Should not update options unless they change', ); - final newMapFrame = mapFrame.withOptions(newOptions); + final newMapCamera = mapCamera.withOptions(newOptions); assert( - newOptions.frameConstraint.constrain(newMapFrame) == newMapFrame, - 'MapFrame is no longer within the frameConstraint after an option change.', + newOptions.cameraConstraint.constrain(newMapCamera) == newMapCamera, + 'MapCamera is no longer within the cameraConstraint after an option change.', ); if (options.interactionOptions != newOptions.interactionOptions) { @@ -276,7 +276,7 @@ class FlutterMapInternalController value = FlutterMapInternalState( options: newOptions, - mapFrame: newMapFrame, + mapCamera: newMapCamera, ); } @@ -284,7 +284,7 @@ class FlutterMapInternalController void moveStarted(MapEventSource source) { _emitMapEvent( MapEventMoveStart( - mapFrame: mapFrame, + mapCamera: mapCamera, source: source, ), ); @@ -292,14 +292,14 @@ class FlutterMapInternalController // To be called when an ongoing drag movement updates. void dragUpdated(MapEventSource source, Offset offset) { - final oldCenterPt = mapFrame.project(mapFrame.center); + final oldCenterPt = mapCamera.project(mapCamera.center); final newCenterPt = oldCenterPt + offset.toCustomPoint(); - final newCenter = mapFrame.unproject(newCenterPt); + final newCenter = mapCamera.unproject(newCenterPt); move( newCenter, - mapFrame.zoom, + mapCamera.zoom, offset: Offset.zero, hasGesture: true, source: source, @@ -311,7 +311,7 @@ class FlutterMapInternalController void moveEnded(MapEventSource source) { _emitMapEvent( MapEventMoveEnd( - mapFrame: mapFrame, + mapCamera: mapCamera, source: source, ), ); @@ -321,7 +321,7 @@ class FlutterMapInternalController void rotateStarted(MapEventSource source) { _emitMapEvent( MapEventRotateStart( - mapFrame: mapFrame, + mapCamera: mapCamera, source: source, ), ); @@ -331,7 +331,7 @@ class FlutterMapInternalController void rotateEnded(MapEventSource source) { _emitMapEvent( MapEventRotateEnd( - mapFrame: mapFrame, + mapCamera: mapCamera, source: source, ), ); @@ -341,7 +341,7 @@ class FlutterMapInternalController void flingStarted(MapEventSource source) { _emitMapEvent( MapEventFlingAnimationStart( - mapFrame: mapFrame, + mapCamera: mapCamera, source: MapEventSource.flingAnimationController, ), ); @@ -351,7 +351,7 @@ class FlutterMapInternalController void flingEnded(MapEventSource source) { _emitMapEvent( MapEventFlingAnimationEnd( - mapFrame: mapFrame, + mapCamera: mapCamera, source: source, ), ); @@ -361,7 +361,7 @@ class FlutterMapInternalController void flingNotStarted(MapEventSource source) { _emitMapEvent( MapEventFlingAnimationNotStarted( - mapFrame: mapFrame, + mapCamera: mapCamera, source: source, ), ); @@ -371,7 +371,7 @@ class FlutterMapInternalController void doubleTapZoomStarted(MapEventSource source) { _emitMapEvent( MapEventDoubleTapZoomStart( - mapFrame: mapFrame, + mapCamera: mapCamera, source: source, ), ); @@ -381,7 +381,7 @@ class FlutterMapInternalController void doubleTapZoomEnded(MapEventSource source) { _emitMapEvent( MapEventDoubleTapZoomEnd( - mapFrame: mapFrame, + mapCamera: mapCamera, source: source, ), ); @@ -396,7 +396,7 @@ class FlutterMapInternalController _emitMapEvent( MapEventTap( tapPosition: position, - mapFrame: mapFrame, + mapCamera: mapCamera, source: source, ), ); @@ -411,7 +411,7 @@ class FlutterMapInternalController _emitMapEvent( MapEventSecondaryTap( tapPosition: position, - mapFrame: mapFrame, + mapCamera: mapCamera, source: source, ), ); @@ -426,7 +426,7 @@ class FlutterMapInternalController _emitMapEvent( MapEventLongPress( tapPosition: position, - mapFrame: mapFrame, + mapCamera: mapCamera, source: MapEventSource.longPress, ), ); @@ -435,14 +435,14 @@ class FlutterMapInternalController // To be called when the map's size constraints change. void nonRotatedSizeChange( MapEventSource source, - MapFrame oldMapFrame, - MapFrame newMapFrame, + MapCamera oldMapCamera, + MapCamera newMapCamera, ) { _emitMapEvent( MapEventNonRotatedSizeChange( source: MapEventSource.nonRotatedSizeChange, - oldMapFrame: oldMapFrame, - mapFrame: newMapFrame, + oldMapCamera: oldMapCamera, + mapCamera: newMapCamera, ), ); } diff --git a/lib/src/map/flutter_map_internal_state.dart b/lib/src/map/flutter_map_internal_state.dart index 77eb929fb..ce232c164 100644 --- a/lib/src/map/flutter_map_internal_state.dart +++ b/lib/src/map/flutter_map_internal_state.dart @@ -1,17 +1,18 @@ -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/map/options.dart'; class FlutterMapInternalState { - final MapFrame mapFrame; + final MapCamera mapCamera; final MapOptions options; const FlutterMapInternalState({ required this.options, - required this.mapFrame, + required this.mapCamera, }); - FlutterMapInternalState withMapFrame(MapFrame mapFrame) => + FlutterMapInternalState withMapCamera(MapCamera mapCamera) => FlutterMapInternalState( options: options, - mapFrame: mapFrame, + mapCamera: mapCamera, ); } diff --git a/lib/src/map/flutter_map_state_container.dart b/lib/src/map/flutter_map_state_container.dart index 54b10b753..6cbaeabc8 100644 --- a/lib/src/map/flutter_map_state_container.dart +++ b/lib/src/map/flutter_map_state_container.dart @@ -7,7 +7,7 @@ import 'package:flutter_map/src/map/flutter_map_internal_controller.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; class FlutterMapStateContainer extends State { - bool _initialFrameFitApplied = false; + bool _initialCameraFitApplied = false; late final FlutterMapInternalController _flutterMapInternalController; late MapControllerImpl _mapController; @@ -54,24 +54,24 @@ class FlutterMapStateContainer extends State { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { _updateAndEmitSizeIfConstraintsChanged(constraints); - _applyInitialFrameFit(constraints); + _applyInitialCameraFit(constraints); return FlutterMapInteractiveViewer( controller: _flutterMapInternalController, builder: (context, mapState) => FlutterMapInheritedModel( controller: _mapController, options: mapState.options, - frame: mapState.mapFrame, + camera: mapState.mapCamera, child: ClipRect( child: Stack( children: [ OverflowBox( - minWidth: mapState.mapFrame.size.x, - maxWidth: mapState.mapFrame.size.x, - minHeight: mapState.mapFrame.size.y, - maxHeight: mapState.mapFrame.size.y, + minWidth: mapState.mapCamera.size.x, + maxWidth: mapState.mapCamera.size.x, + minHeight: mapState.mapCamera.size.y, + maxHeight: mapState.mapCamera.size.y, child: Transform.rotate( - angle: mapState.mapFrame.rotationRad, + angle: mapState.mapCamera.rotationRad, child: Stack(children: widget.children), ), ), @@ -85,22 +85,22 @@ class FlutterMapStateContainer extends State { ); } - void _applyInitialFrameFit(BoxConstraints constraints) { - // If an initial frame fit was provided apply it to the map state once the + void _applyInitialCameraFit(BoxConstraints constraints) { + // If an initial camera fit was provided apply it to the map state once the // the parent constraints are available. - if (!_initialFrameFitApplied && + if (!_initialCameraFitApplied && (widget.options.bounds != null || - widget.options.initialFrameFit != null) && + widget.options.initialCameraFit != null) && _parentConstraintsAreSet(context, constraints)) { - _initialFrameFitApplied = true; + _initialCameraFitApplied = true; - final FrameFit frameFit; + final CameraFit cameraFit; if (widget.options.bounds != null) { - // Create the frame fit from the deprecated option. + // Create the camera fit from the deprecated option. final fitBoundsOptions = widget.options.boundsOptions; - frameFit = FrameFit.bounds( + cameraFit = CameraFit.bounds( bounds: widget.options.bounds!, padding: fitBoundsOptions.padding, maxZoom: fitBoundsOptions.maxZoom, @@ -108,11 +108,11 @@ class FlutterMapStateContainer extends State { forceIntegerZoomLevel: fitBoundsOptions.forceIntegerZoomLevel, ); } else { - frameFit = widget.options.initialFrameFit!; + cameraFit = widget.options.initialCameraFit!; } - _flutterMapInternalController.fitFrame( - frameFit, + _flutterMapInternalController.fitCamera( + cameraFit, offset: Offset.zero, ); } @@ -123,10 +123,10 @@ class FlutterMapStateContainer extends State { constraints.maxWidth, constraints.maxHeight, ); - final oldMapFrame = _flutterMapInternalController.mapFrame; + final oldMapCamera = _flutterMapInternalController.mapCamera; if (_flutterMapInternalController .setNonRotatedSizeWithoutEmittingEvent(nonRotatedSize)) { - final newMapFrame = _flutterMapInternalController.mapFrame; + final newMapCamera = _flutterMapInternalController.mapCamera; // Avoid emitting the event during build otherwise if the user calls // setState in the onMapEvent callback it will throw. @@ -134,8 +134,8 @@ class FlutterMapStateContainer extends State { if (mounted) { _flutterMapInternalController.nonRotatedSizeChange( MapEventSource.nonRotatedSizeChange, - oldMapFrame, - newMapFrame, + oldMapCamera, + newMapCamera, ); } }); diff --git a/lib/src/map/map_controller.dart b/lib/src/map/map_controller.dart index c2ef0a299..627e8a137 100644 --- a/lib/src/map/map_controller.dart +++ b/lib/src/map/map_controller.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/flutter_map_frame.dart'; +import 'package:flutter_map/src/map/camera.dart'; import 'package:flutter_map/src/map/flutter_map_inherited_model.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; import 'package:latlong2/latlong.dart'; @@ -48,7 +48,7 @@ abstract class MapController { /// through [MapEventCallback]s, such as [MapOptions.onMapEvent]), unless /// the move failed because (after adjustment when necessary): /// * [center] and [zoom] are equal to the current values - /// * [center] [MapOptions.frameConstraint] does not allow the movement. + /// * [center] [MapOptions.cameraConstraint] does not allow the movement. bool move( LatLng center, double zoom, { @@ -124,23 +124,23 @@ abstract class MapController { /// /// For information about return value meaning and emitted events, see [move]'s /// documentation. - @Deprecated('Use fitFrame with a MapFit.bounds() instead') + @Deprecated('Use fitCamera with a MapFit.bounds() instead') bool fitBounds( LatLngBounds bounds, { FitBoundsOptions options = const FitBoundsOptions(padding: EdgeInsets.all(12)), }); - /// Move and zoom the map to fit [frameFit]. + /// Move and zoom the map to fit [cameraFit]. /// /// For information about the return value and emitted events, see [move]'s /// documentation. - bool fitFrame(FrameFit frameFit); + bool fitCamera(CameraFit cameraFit); - /// Current [MapFrame]. Accessing the frame from this getter is an - /// anti-pattern. It is preferable to use [MapFrame.of(context)] in a child + /// Current [MapCamera]. Accessing the camera from this getter is an + /// anti-pattern. It is preferable to use [MapCamera.of(context)] in a child /// widget of FlutterMap. - MapFrame get mapFrame; + MapCamera get mapCamera; /// [Stream] of all emitted [MapEvent]s Stream get mapEventStream; @@ -150,7 +150,7 @@ abstract class MapController { /// /// Does not move/zoom the map: see [fitBounds]. @Deprecated( - 'Use FrameFit.bounds(bounds: bounds).fit(controller.mapFrame) instead.') + 'Use CameraFit.bounds(bounds: bounds).fit(controller.mapCamera) instead.') CenterZoom centerZoomFitBounds( LatLngBounds bounds, { FitBoundsOptions options = @@ -159,15 +159,15 @@ abstract class MapController { /// Convert a screen point (x/y) to its corresponding map coordinate (lat/lng), /// based on the map's current properties - @Deprecated('Use controller.mapFrame.pointToLatLng() instead.') + @Deprecated('Use controller.mapCamera.pointToLatLng() instead.') LatLng pointToLatLng(CustomPoint screenPoint); /// Convert a map coordinate (lat/lng) to its corresponding screen point (x/y), /// based on the map's current screen positioning - @Deprecated('Use controller.mapFrame.latLngToScreenPoint() instead.') + @Deprecated('Use controller.mapCamera.latLngToScreenPoint() instead.') CustomPoint latLngToScreenPoint(LatLng mapCoordinate); - @Deprecated('Use controller.mapFrame.rotatePoint() instead.') + @Deprecated('Use controller.mapCamera.rotatePoint() instead.') CustomPoint rotatePoint( CustomPoint mapCenter, CustomPoint point, { @@ -175,19 +175,19 @@ abstract class MapController { }); /// Current center coordinates - @Deprecated('Use controller.mapFrame.center instead.') + @Deprecated('Use controller.mapCamera.center instead.') LatLng get center; /// Current outer points/boundaries coordinates - @Deprecated('Use controller.mapFrame.visibleBounds instead.') + @Deprecated('Use controller.mapCamera.visibleBounds instead.') LatLngBounds? get bounds; /// Current zoom level - @Deprecated('Use controller.mapFrame.zoom instead.') + @Deprecated('Use controller.mapCamera.zoom instead.') double get zoom; /// Current rotation in degrees, where 0° is North - @Deprecated('Use controller.mapFrame.rotation instead.') + @Deprecated('Use controller.mapCamera.rotation instead.') double get rotation; /// Dispose of this controller. diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/map_controller_impl.dart index 295435073..ac3cbd237 100644 --- a/lib/src/map/map_controller_impl.dart +++ b/lib/src/map/map_controller_impl.dart @@ -3,12 +3,12 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; -import 'package:flutter_map/src/map/flutter_map_frame.dart'; +import 'package:flutter_map/src/map/camera.dart'; import 'package:flutter_map/src/map/flutter_map_internal_controller.dart'; import 'package:flutter_map/src/map/map_controller.dart'; +import 'package:flutter_map/src/misc/camera_fit.dart'; import 'package:flutter_map/src/misc/center_zoom.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; -import 'package:flutter_map/src/misc/frame_fit.dart'; import 'package:flutter_map/src/misc/move_and_rotate_result.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:latlong2/latlong.dart'; @@ -79,14 +79,14 @@ class MapControllerImpl implements MapController { /// For information about return value meaning and emitted events, see [move]'s /// documentation. @override - @Deprecated('Use fitFrame with a MapFit.bounds() instead') + @Deprecated('Use fitCamera with a MapFit.bounds() instead') bool fitBounds( LatLngBounds bounds, { FitBoundsOptions options = const FitBoundsOptions(padding: EdgeInsets.all(12)), }) => - fitFrame( - FrameFit.bounds( + fitCamera( + CameraFit.bounds( bounds: bounds, padding: options.padding, maxZoom: options.maxZoom, @@ -96,13 +96,13 @@ class MapControllerImpl implements MapController { ); @override - bool fitFrame(FrameFit frameFit) => _internalController.fitFrame( - frameFit, + bool fitCamera(CameraFit cameraFit) => _internalController.fitCamera( + cameraFit, offset: Offset.zero, ); @override - MapFrame get mapFrame => _internalController.mapFrame; + MapCamera get mapCamera => _internalController.mapCamera; final _mapEventStreamController = StreamController.broadcast(); @@ -123,28 +123,28 @@ class MapControllerImpl implements MapController { } @override - @Deprecated('Use controller.mapFrame.visibleBounds instead.') - LatLngBounds? get bounds => mapFrame.visibleBounds; + @Deprecated('Use controller.mapCamera.visibleBounds instead.') + LatLngBounds? get bounds => mapCamera.visibleBounds; @override - @Deprecated('Use controller.mapFrame.center instead.') - LatLng get center => mapFrame.center; + @Deprecated('Use controller.mapCamera.center instead.') + LatLng get center => mapCamera.center; @override @Deprecated( - 'Use FrameFit.bounds(bounds: bounds).fit(controller.mapFrame) instead.') + 'Use CameraFit.bounds(bounds: bounds).fit(controller.mapCamera) instead.') CenterZoom centerZoomFitBounds( LatLngBounds bounds, { FitBoundsOptions options = const FitBoundsOptions(padding: EdgeInsets.all(12)), }) { - final fittedState = FrameFit.bounds( + final fittedState = CameraFit.bounds( bounds: bounds, padding: options.padding, maxZoom: options.maxZoom, inside: options.inside, forceIntegerZoomLevel: options.forceIntegerZoomLevel, - ).fit(mapFrame); + ).fit(mapCamera); return CenterZoom( center: fittedState.center, zoom: fittedState.zoom, @@ -152,33 +152,33 @@ class MapControllerImpl implements MapController { } @override - @Deprecated('Use controller.mapFrame.latLngToScreenPoint() instead.') + @Deprecated('Use controller.mapCamera.latLngToScreenPoint() instead.') CustomPoint latLngToScreenPoint(LatLng mapCoordinate) => - mapFrame.latLngToScreenPoint(mapCoordinate); + mapCamera.latLngToScreenPoint(mapCoordinate); @override - @Deprecated('Use controller.mapFrame.pointToLatLng() instead.') + @Deprecated('Use controller.mapCamera.pointToLatLng() instead.') LatLng pointToLatLng(CustomPoint screenPoint) => - mapFrame.pointToLatLng(screenPoint); + mapCamera.pointToLatLng(screenPoint); @override - @Deprecated('Use controller.mapFrame.rotatePoint() instead.') + @Deprecated('Use controller.mapCamera.rotatePoint() instead.') CustomPoint rotatePoint( CustomPoint mapCenter, CustomPoint point, { bool counterRotation = true, }) => - mapFrame.rotatePoint( + mapCamera.rotatePoint( mapCenter.toDoublePoint(), point.toDoublePoint(), counterRotation: counterRotation, ); @override - @Deprecated('Use controller.mapFrame.rotation instead.') - double get rotation => mapFrame.rotation; + @Deprecated('Use controller.mapCamera.rotation instead.') + double get rotation => mapCamera.rotation; @override - @Deprecated('Use controller.mapFrame.zoom instead.') - double get zoom => mapFrame.zoom; + @Deprecated('Use controller.mapCamera.zoom instead.') + double get zoom => mapCamera.zoom; } diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index f14e04f29..f8587e033 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -6,9 +6,9 @@ import 'package:flutter_map/src/gestures/interactive_flag.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; import 'package:flutter_map/src/map/flutter_map_inherited_model.dart'; +import 'package:flutter_map/src/misc/camera_constraint.dart'; +import 'package:flutter_map/src/misc/camera_fit.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; -import 'package:flutter_map/src/misc/frame_constraint.dart'; -import 'package:flutter_map/src/misc/frame_fit.dart'; import 'package:flutter_map/src/misc/position.dart'; import 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; import 'package:latlong2/latlong.dart'; @@ -38,11 +38,11 @@ class MapOptions { /// The Coordinate Reference System, defaults to [Epsg3857]. final Crs crs; - /// The center when the map is first loaded. If [initialFrameFit] is defined + /// The center when the map is first loaded. If [initialCameraFit] is defined /// this has no effect. final LatLng initialCenter; - /// The zoom when the map is first loaded. If [initialFrameFit] is defined + /// The zoom when the map is first loaded. If [initialCameraFit] is defined /// this has no effect. final double initialZoom; @@ -51,7 +51,7 @@ class MapOptions { /// Defines the visible bounds when the map is first loaded. Takes precedence /// over [initialCenter]/[initialZoom]. - final FrameFit? initialFrameFit; + final CameraFit? initialCameraFit; final LatLngBounds? bounds; final FitBoundsOptions boundsOptions; @@ -84,14 +84,14 @@ class MapOptions { final MapEventCallback? onMapEvent; /// Define limits for viewing the map. - final FrameConstraint frameConstraint; + final CameraConstraint cameraConstraint; /// OnMapReady is called after the map runs it's initState. /// At that point the map has assigned its state to the controller /// Only use this if your map isn't built immediately (like inside FutureBuilder) /// and you need to access the controller as soon as the map is built. /// Otherwise you can use WidgetsBinding.instance.addPostFrameCallback - /// In initState to controll the map before the next frame + /// In initState to controll the map before the next frame. final void Function()? onMapReady; /// Flag to enable the built in keep alive functionality @@ -112,11 +112,11 @@ class MapOptions { double initialZoom = 13.0, @Deprecated('Use initialRotation instead') double? rotation, double initialRotation = 0.0, - @Deprecated('Use initialFrameFit instead') this.bounds, - @Deprecated('Use initialFrameFit instead') + @Deprecated('Use initialCameraFit instead') this.bounds, + @Deprecated('Use initialCameraFit instead') this.boundsOptions = const FitBoundsOptions(), - this.initialFrameFit, - this.frameConstraint = const FrameConstraint.unconstrained(), + this.initialCameraFit, + this.cameraConstraint = const CameraConstraint.unconstrained(), InteractionOptions? interactionOptions, @Deprecated('Should be set in interactionOptions instead') int? interactiveFlags, @@ -202,7 +202,7 @@ class MapOptions { initialCenter == other.initialCenter && initialZoom == other.initialZoom && initialRotation == other.initialRotation && - initialFrameFit == other.initialFrameFit && + initialCameraFit == other.initialCameraFit && bounds == other.bounds && boundsOptions == other.boundsOptions && minZoom == other.minZoom && @@ -216,7 +216,7 @@ class MapOptions { onPointerHover == other.onPointerHover && onPositionChanged == other.onPositionChanged && onMapEvent == other.onMapEvent && - frameConstraint == other.frameConstraint && + cameraConstraint == other.cameraConstraint && onMapReady == other.onMapReady && keepAlive == other.keepAlive && interactionOptions == other.interactionOptions; @@ -227,7 +227,7 @@ class MapOptions { initialCenter, initialZoom, initialRotation, - initialFrameFit, + initialCameraFit, bounds, boundsOptions, minZoom, @@ -241,7 +241,7 @@ class MapOptions { onPointerHover, onPositionChanged, onMapEvent, - frameConstraint, + cameraConstraint, onMapReady, keepAlive, interactionOptions, diff --git a/lib/src/misc/camera_constraint.dart b/lib/src/misc/camera_constraint.dart new file mode 100644 index 000000000..4db4cef0a --- /dev/null +++ b/lib/src/misc/camera_constraint.dart @@ -0,0 +1,102 @@ +import 'dart:math' as math; + +import 'package:flutter_map/src/geo/latlng_bounds.dart'; +import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/misc/point.dart'; +import 'package:latlong2/latlong.dart'; + +abstract class CameraConstraint { + const CameraConstraint(); + + const factory CameraConstraint.unconstrained() = UnconstrainedCamera._; + + const factory CameraConstraint.containCenter({ + required LatLngBounds bounds, + }) = ContainCameraCenter._; + + const factory CameraConstraint.contain({ + required LatLngBounds bounds, + }) = ContainCamera._; + + MapCamera? constrain(MapCamera mapCamera); +} + +class UnconstrainedCamera extends CameraConstraint { + const UnconstrainedCamera._(); + + @override + MapCamera constrain(MapCamera mapCamera) => mapCamera; +} + +class ContainCameraCenter extends CameraConstraint { + final LatLngBounds bounds; + + const ContainCameraCenter._({ + required this.bounds, + }); + + @override + MapCamera constrain(MapCamera mapCamera) => mapCamera.withPosition( + center: LatLng( + mapCamera.center.latitude.clamp( + bounds.south, + bounds.north, + ), + mapCamera.center.longitude.clamp( + bounds.west, + bounds.east, + ), + ), + ); +} + +class ContainCamera extends CameraConstraint { + final LatLngBounds bounds; + + /// Keeps the center of the camera within [bounds]. + const ContainCamera._({ + required this.bounds, + }); + + @override + MapCamera? constrain(MapCamera mapCamera) { + final testZoom = mapCamera.zoom; + final testCenter = mapCamera.center; + + final nePixel = mapCamera.project(bounds.northEast, testZoom); + final swPixel = mapCamera.project(bounds.southWest, testZoom); + + final halfSize = mapCamera.size / 2; + + // Find the limits for the map center which would keep the camera within the + // [latLngBounds]. + final leftOkCenter = math.min(swPixel.x, nePixel.x) + halfSize.x; + final rightOkCenter = math.max(swPixel.x, nePixel.x) - halfSize.x; + final topOkCenter = math.min(swPixel.y, nePixel.y) + halfSize.y; + final botOkCenter = math.max(swPixel.y, nePixel.y) - halfSize.y; + + // Stop if we are zoomed out so far that the camera cannot be translated to + // stay within [latLngBounds]. + if (leftOkCenter > rightOkCenter || topOkCenter > botOkCenter) return null; + + final centerPix = mapCamera.project(testCenter, testZoom); + final newCenterPix = CustomPoint( + centerPix.x.clamp(leftOkCenter, rightOkCenter), + centerPix.y.clamp(topOkCenter, botOkCenter), + ); + + if (newCenterPix == centerPix) return mapCamera; + + return mapCamera.withPosition( + center: mapCamera.unproject(newCenterPix, testZoom), + ); + } + + @override + bool operator ==(Object other) { + return other is ContainCamera && other.bounds == bounds; + } + + @override + int get hashCode => bounds.hashCode; +} diff --git a/lib/src/misc/frame_fit.dart b/lib/src/misc/camera_fit.dart similarity index 66% rename from lib/src/misc/frame_fit.dart rename to lib/src/misc/camera_fit.dart index cab8163c9..d0b9c50cb 100644 --- a/lib/src/misc/frame_fit.dart +++ b/lib/src/misc/camera_fit.dart @@ -2,15 +2,15 @@ import 'dart:math' as math; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/map/flutter_map_frame.dart'; +import 'package:flutter_map/src/map/camera.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; -abstract class FrameFit { - const FrameFit(); +abstract class CameraFit { + const CameraFit(); - const factory FrameFit.bounds({ + const factory CameraFit.bounds({ required LatLngBounds bounds, EdgeInsets padding, double maxZoom, @@ -18,17 +18,17 @@ abstract class FrameFit { bool forceIntegerZoomLevel, }) = FitBounds; - const factory FrameFit.coordinates({ + const factory CameraFit.coordinates({ required List coordinates, EdgeInsets padding, double maxZoom, bool forceIntegerZoomLevel, }) = FitCoordinates; - MapFrame fit(MapFrame mapFrame); + MapCamera fit(MapCamera mapCamera); } -class FitBounds extends FrameFit { +class FitBounds extends CameraFit { final LatLngBounds bounds; final EdgeInsets padding; final double maxZoom; @@ -48,56 +48,56 @@ class FitBounds extends FrameFit { }); @override - MapFrame fit(MapFrame mapFrame) { + MapCamera fit(MapCamera mapCamera) { final paddingTL = CustomPoint(padding.left, padding.top); final paddingBR = CustomPoint(padding.right, padding.bottom); final paddingTotalXY = paddingTL + paddingBR; - var newZoom = getBoundsZoom(mapFrame, paddingTotalXY); + var newZoom = getBoundsZoom(mapCamera, paddingTotalXY); newZoom = math.min(maxZoom, newZoom); final paddingOffset = (paddingBR - paddingTL) / 2; - final swPoint = mapFrame.project(bounds.southWest, newZoom); - final nePoint = mapFrame.project(bounds.northEast, newZoom); + final swPoint = mapCamera.project(bounds.southWest, newZoom); + final nePoint = mapCamera.project(bounds.northEast, newZoom); final CustomPoint projectedCenter; - if (mapFrame.rotation != 0.0) { - final swPointRotated = swPoint.rotate(-mapFrame.rotationRad); - final nePointRotated = nePoint.rotate(-mapFrame.rotationRad); + if (mapCamera.rotation != 0.0) { + final swPointRotated = swPoint.rotate(-mapCamera.rotationRad); + final nePointRotated = nePoint.rotate(-mapCamera.rotationRad); final centerRotated = (swPointRotated + nePointRotated) / 2 + paddingOffset; - projectedCenter = centerRotated.rotate(mapFrame.rotationRad); + projectedCenter = centerRotated.rotate(mapCamera.rotationRad); } else { projectedCenter = (swPoint + nePoint) / 2 + paddingOffset; } - final center = mapFrame.unproject(projectedCenter, newZoom); - return mapFrame.withPosition( + final center = mapCamera.unproject(projectedCenter, newZoom); + return mapCamera.withPosition( center: center, zoom: newZoom, ); } double getBoundsZoom( - MapFrame mapFrame, + MapCamera mapCamera, CustomPoint pixelPadding, ) { - final min = mapFrame.minZoom ?? 0.0; - final max = mapFrame.maxZoom ?? double.infinity; + final min = mapCamera.minZoom ?? 0.0; + final max = mapCamera.maxZoom ?? double.infinity; final nw = bounds.northWest; final se = bounds.southEast; - var size = mapFrame.nonRotatedSize - pixelPadding; + var size = mapCamera.nonRotatedSize - pixelPadding; // Prevent negative size which results in NaN zoom value later on in the calculation size = CustomPoint(math.max(0, size.x), math.max(0, size.y)); var boundsSize = Bounds( - mapFrame.project(se, mapFrame.zoom), - mapFrame.project(nw, mapFrame.zoom), + mapCamera.project(se, mapCamera.zoom), + mapCamera.project(nw, mapCamera.zoom), ).size; - if (mapFrame.rotation != 0.0) { - final cosAngle = math.cos(mapFrame.rotationRad).abs(); - final sinAngle = math.sin(mapFrame.rotationRad).abs(); + if (mapCamera.rotation != 0.0) { + final cosAngle = math.cos(mapCamera.rotationRad).abs(); + final sinAngle = math.sin(mapCamera.rotationRad).abs(); boundsSize = CustomPoint( (boundsSize.x * cosAngle) + (boundsSize.y * sinAngle), (boundsSize.y * cosAngle) + (boundsSize.x * sinAngle), @@ -108,7 +108,7 @@ class FitBounds extends FrameFit { final scaleY = size.y / boundsSize.y; final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); - var boundsZoom = mapFrame.getScaleZoom(scale, mapFrame.zoom); + var boundsZoom = mapCamera.getScaleZoom(scale, mapCamera.zoom); if (forceIntegerZoomLevel) { boundsZoom = @@ -119,7 +119,7 @@ class FitBounds extends FrameFit { } } -class FitCoordinates extends FrameFit { +class FitCoordinates extends CameraFit { final List coordinates; final EdgeInsets padding; final double maxZoom; @@ -137,21 +137,21 @@ class FitCoordinates extends FrameFit { }); @override - MapFrame fit(MapFrame mapFrame) { + MapCamera fit(MapCamera mapCamera) { final paddingTL = CustomPoint(padding.left, padding.top); final paddingBR = CustomPoint(padding.right, padding.bottom); final paddingTotalXY = paddingTL + paddingBR; - var newZoom = getCoordinatesZoom(mapFrame, paddingTotalXY); + var newZoom = getCoordinatesZoom(mapCamera, paddingTotalXY); newZoom = math.min(maxZoom, newZoom); final projectedPoints = [ - for (final coord in coordinates) mapFrame.project(coord, newZoom) + for (final coord in coordinates) mapCamera.project(coord, newZoom) ]; final rotatedPoints = - projectedPoints.map((point) => point.rotate(-mapFrame.rotationRad)); + projectedPoints.map((point) => point.rotate(-mapCamera.rotationRad)); final rotatedBounds = Bounds.containing(rotatedPoints); @@ -160,32 +160,32 @@ class FitCoordinates extends FrameFit { final rotatedNewCenter = rotatedBounds.center + paddingOffset; // Undo the rotation - final unrotatedNewCenter = rotatedNewCenter.rotate(mapFrame.rotationRad); + final unrotatedNewCenter = rotatedNewCenter.rotate(mapCamera.rotationRad); - final newCenter = mapFrame.unproject(unrotatedNewCenter, newZoom); + final newCenter = mapCamera.unproject(unrotatedNewCenter, newZoom); - return mapFrame.withPosition( + return mapCamera.withPosition( center: newCenter, zoom: newZoom, ); } double getCoordinatesZoom( - MapFrame mapFrame, + MapCamera mapCamera, CustomPoint pixelPadding, ) { - final min = mapFrame.minZoom ?? 0.0; - final max = mapFrame.maxZoom ?? double.infinity; - var size = mapFrame.nonRotatedSize - pixelPadding; + final min = mapCamera.minZoom ?? 0.0; + final max = mapCamera.maxZoom ?? double.infinity; + var size = mapCamera.nonRotatedSize - pixelPadding; // Prevent negative size which results in NaN zoom value later on in the calculation size = CustomPoint(math.max(0, size.x), math.max(0, size.y)); final projectedPoints = [ - for (final coord in coordinates) mapFrame.project(coord) + for (final coord in coordinates) mapCamera.project(coord) ]; final rotatedPoints = - projectedPoints.map((point) => point.rotate(-mapFrame.rotationRad)); + projectedPoints.map((point) => point.rotate(-mapCamera.rotationRad)); final rotatedBounds = Bounds.containing(rotatedPoints); final boundsSize = rotatedBounds.size; @@ -194,7 +194,7 @@ class FitCoordinates extends FrameFit { final scaleY = size.y / boundsSize.y; final scale = math.min(scaleX, scaleY); - var newZoom = mapFrame.getScaleZoom(scale, mapFrame.zoom); + var newZoom = mapCamera.getScaleZoom(scale, mapCamera.zoom); if (forceIntegerZoomLevel) { newZoom = newZoom.floorToDouble(); } diff --git a/lib/src/misc/fit_bounds_options.dart b/lib/src/misc/fit_bounds_options.dart index 27e516fbb..4442c9a10 100644 --- a/lib/src/misc/fit_bounds_options.dart +++ b/lib/src/misc/fit_bounds_options.dart @@ -10,7 +10,7 @@ class FitBoundsOptions { /// to the next suitable integer. final bool forceIntegerZoomLevel; - @Deprecated('Use FitFrame.bounds instead.') + @Deprecated('Use FitCamera.bounds instead.') const FitBoundsOptions({ this.padding = EdgeInsets.zero, this.maxZoom = 17.0, diff --git a/lib/src/misc/frame_constraint.dart b/lib/src/misc/frame_constraint.dart deleted file mode 100644 index e5d089d59..000000000 --- a/lib/src/misc/frame_constraint.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'dart:math' as math; - -import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/map/flutter_map_frame.dart'; -import 'package:flutter_map/src/misc/point.dart'; -import 'package:latlong2/latlong.dart'; - -abstract class FrameConstraint { - const FrameConstraint(); - - const factory FrameConstraint.unconstrained() = UnconstrainedFrame._; - - const factory FrameConstraint.containCenter({ - required LatLngBounds bounds, - }) = ContainFrameCenter._; - - const factory FrameConstraint.contain({ - required LatLngBounds bounds, - }) = ContainFrame._; - - MapFrame? constrain(MapFrame mapFrame); -} - -class UnconstrainedFrame extends FrameConstraint { - const UnconstrainedFrame._(); - - @override - MapFrame constrain(MapFrame mapFrame) => mapFrame; -} - -class ContainFrameCenter extends FrameConstraint { - final LatLngBounds bounds; - - const ContainFrameCenter._({ - required this.bounds, - }); - - @override - MapFrame constrain(MapFrame mapFrame) => mapFrame.withPosition( - center: LatLng( - mapFrame.center.latitude.clamp( - bounds.south, - bounds.north, - ), - mapFrame.center.longitude.clamp( - bounds.west, - bounds.east, - ), - ), - ); -} - -class ContainFrame extends FrameConstraint { - final LatLngBounds bounds; - - /// Keeps the center of the frame within [bounds]. - const ContainFrame._({ - required this.bounds, - }); - - @override - MapFrame? constrain(MapFrame mapFrame) { - final testZoom = mapFrame.zoom; - final testCenter = mapFrame.center; - - final nePixel = mapFrame.project(bounds.northEast, testZoom); - final swPixel = mapFrame.project(bounds.southWest, testZoom); - - final halfSize = mapFrame.size / 2; - - // Find the limits for the map center which would keep the frame within the - // [latLngBounds]. - final leftOkCenter = math.min(swPixel.x, nePixel.x) + halfSize.x; - final rightOkCenter = math.max(swPixel.x, nePixel.x) - halfSize.x; - final topOkCenter = math.min(swPixel.y, nePixel.y) + halfSize.y; - final botOkCenter = math.max(swPixel.y, nePixel.y) - halfSize.y; - - // Stop if we are zoomed out so far that the frame cannot be translated to - // stay within [latLngBounds]. - if (leftOkCenter > rightOkCenter || topOkCenter > botOkCenter) return null; - - final centerPix = mapFrame.project(testCenter, testZoom); - final newCenterPix = CustomPoint( - centerPix.x.clamp(leftOkCenter, rightOkCenter), - centerPix.y.clamp(topOkCenter, botOkCenter), - ); - - if (newCenterPix == centerPix) return mapFrame; - - return mapFrame.withPosition( - center: mapFrame.unproject(newCenterPix, testZoom), - ); - } - - @override - bool operator ==(Object other) { - return other is ContainFrame && other.bounds == bounds; - } - - @override - int get hashCode => bounds.hashCode; -} diff --git a/test/flutter_map_controller_test.dart b/test/flutter_map_controller_test.dart index 0aaa88fd5..603bfe2b3 100644 --- a/test/flutter_map_controller_test.dart +++ b/test/flutter_map_controller_test.dart @@ -17,23 +17,23 @@ void main() { await tester.pumpWidget(TestApp(controller: controller)); { - final frameConstraint = FrameFit.bounds(bounds: bounds); + final cameraConstraint = CameraFit.bounds(bounds: bounds); final expectedBounds = LatLngBounds( const LatLng(51.00145915187144, -0.3079873797085076), const LatLng(52.001427481787005, 1.298485398623206), ); const expectedZoom = 7.451812751543818; - controller.fitFrame(frameConstraint); + controller.fitCamera(cameraConstraint); await tester.pump(); - final mapFrame = controller.mapFrame; - expect(mapFrame.visibleBounds, equals(expectedBounds)); - expect(mapFrame.center, equals(expectedCenter)); - expect(mapFrame.zoom, equals(expectedZoom)); + final mapCamera = controller.mapCamera; + expect(mapCamera.visibleBounds, equals(expectedBounds)); + expect(mapCamera.center, equals(expectedCenter)); + expect(mapCamera.zoom, equals(expectedZoom)); } { - final frameConstraint = FrameFit.bounds( + final cameraConstraint = CameraFit.bounds( bounds: bounds, forceIntegerZoomLevel: true, ); @@ -44,16 +44,16 @@ void main() { ); const expectedZoom = 7; - controller.fitFrame(frameConstraint); + controller.fitCamera(cameraConstraint); await tester.pump(); - final mapFrame = controller.mapFrame; - expect(mapFrame.visibleBounds, equals(expectedBounds)); - expect(mapFrame.center, equals(expectedCenter)); - expect(mapFrame.zoom, equals(expectedZoom)); + final mapCamera = controller.mapCamera; + expect(mapCamera.visibleBounds, equals(expectedBounds)); + expect(mapCamera.center, equals(expectedCenter)); + expect(mapCamera.zoom, equals(expectedZoom)); } { - final frameConstraint = FrameFit.bounds( + final cameraConstraint = CameraFit.bounds( bounds: bounds, inside: true, ); @@ -64,17 +64,17 @@ void main() { ); const expectedZoom = 8.135709286104404; - controller.fitFrame(frameConstraint); + controller.fitCamera(cameraConstraint); await tester.pump(); - final mapFrame = controller.mapFrame; - expect(mapFrame.visibleBounds, equals(expectedBounds)); - expect(mapFrame.center, equals(expectedCenter)); - expect(mapFrame.zoom, equals(expectedZoom)); + final mapCamera = controller.mapCamera; + expect(mapCamera.visibleBounds, equals(expectedBounds)); + expect(mapCamera.center, equals(expectedCenter)); + expect(mapCamera.zoom, equals(expectedZoom)); } { - final frameConstraint = FrameFit.bounds( + final cameraConstraint = CameraFit.bounds( bounds: bounds, inside: true, forceIntegerZoomLevel: true, @@ -86,12 +86,12 @@ void main() { ); const expectedZoom = 9; - controller.fitFrame(frameConstraint); + controller.fitCamera(cameraConstraint); await tester.pump(); - final mapFrame = controller.mapFrame; - expect(mapFrame.visibleBounds, equals(expectedBounds)); - expect(mapFrame.center, equals(expectedCenter)); - expect(mapFrame.zoom, equals(expectedZoom)); + final mapCamera = controller.mapCamera; + expect(mapCamera.visibleBounds, equals(expectedBounds)); + expect(mapCamera.center, equals(expectedCenter)); + expect(mapCamera.zoom, equals(expectedZoom)); } }); @@ -106,47 +106,47 @@ void main() { Future testFitBounds({ required double rotation, - required FrameFit frameConstraint, + required CameraFit cameraConstraint, required LatLngBounds expectedBounds, required LatLng expectedCenter, required double expectedZoom, }) async { controller.rotate(rotation); - controller.fitFrame(frameConstraint); + controller.fitCamera(cameraConstraint); await tester.pump(); expect( - controller.mapFrame.visibleBounds.northWest.latitude, + controller.mapCamera.visibleBounds.northWest.latitude, moreOrLessEquals(expectedBounds.northWest.latitude), ); expect( - controller.mapFrame.visibleBounds.northWest.longitude, + controller.mapCamera.visibleBounds.northWest.longitude, moreOrLessEquals(expectedBounds.northWest.longitude), ); expect( - controller.mapFrame.visibleBounds.southEast.latitude, + controller.mapCamera.visibleBounds.southEast.latitude, moreOrLessEquals(expectedBounds.southEast.latitude), ); expect( - controller.mapFrame.visibleBounds.southEast.longitude, + controller.mapCamera.visibleBounds.southEast.longitude, moreOrLessEquals(expectedBounds.southEast.longitude), ); expect( - controller.mapFrame.center.latitude, + controller.mapCamera.center.latitude, moreOrLessEquals(expectedCenter.latitude), ); expect( - controller.mapFrame.center.longitude, + controller.mapCamera.center.longitude, moreOrLessEquals(expectedCenter.longitude), ); - expect(controller.mapFrame.zoom, moreOrLessEquals(expectedZoom)); + expect(controller.mapCamera.zoom, moreOrLessEquals(expectedZoom)); } // Tests with no padding await testFitBounds( rotation: -360, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: EdgeInsets.zero, ), @@ -159,7 +159,7 @@ void main() { ); await testFitBounds( rotation: -300, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: EdgeInsets.zero, ), @@ -172,7 +172,7 @@ void main() { ); await testFitBounds( rotation: -240, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: EdgeInsets.zero, ), @@ -185,7 +185,7 @@ void main() { ); await testFitBounds( rotation: -180, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: EdgeInsets.zero, ), @@ -198,7 +198,7 @@ void main() { ); await testFitBounds( rotation: -120, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: EdgeInsets.zero, ), @@ -211,7 +211,7 @@ void main() { ); await testFitBounds( rotation: -60, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: EdgeInsets.zero, ), @@ -224,7 +224,7 @@ void main() { ); await testFitBounds( rotation: 0, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: EdgeInsets.zero, ), @@ -237,7 +237,7 @@ void main() { ); await testFitBounds( rotation: 60, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: EdgeInsets.zero, ), @@ -250,7 +250,7 @@ void main() { ); await testFitBounds( rotation: 120, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: EdgeInsets.zero, ), @@ -263,7 +263,7 @@ void main() { ); await testFitBounds( rotation: 180, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: EdgeInsets.zero, ), @@ -276,7 +276,7 @@ void main() { ); await testFitBounds( rotation: 240, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: EdgeInsets.zero, ), @@ -289,7 +289,7 @@ void main() { ); await testFitBounds( rotation: 300, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: EdgeInsets.zero, ), @@ -302,7 +302,7 @@ void main() { ); await testFitBounds( rotation: 360, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: EdgeInsets.zero, ), @@ -320,7 +320,7 @@ void main() { await testFitBounds( rotation: -360, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: symmetricPadding, ), @@ -333,7 +333,7 @@ void main() { ); await testFitBounds( rotation: -300, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: symmetricPadding, ), @@ -346,7 +346,7 @@ void main() { ); await testFitBounds( rotation: -240, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: symmetricPadding, ), @@ -359,7 +359,7 @@ void main() { ); await testFitBounds( rotation: -180, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: symmetricPadding, ), @@ -372,7 +372,7 @@ void main() { ); await testFitBounds( rotation: -120, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: symmetricPadding, ), @@ -385,7 +385,7 @@ void main() { ); await testFitBounds( rotation: -60, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: symmetricPadding, ), @@ -398,7 +398,7 @@ void main() { ); await testFitBounds( rotation: 0, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: symmetricPadding, ), @@ -411,7 +411,7 @@ void main() { ); await testFitBounds( rotation: 60, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: symmetricPadding, ), @@ -424,7 +424,7 @@ void main() { ); await testFitBounds( rotation: 120, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: symmetricPadding, ), @@ -437,7 +437,7 @@ void main() { ); await testFitBounds( rotation: 180, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: symmetricPadding, ), @@ -450,7 +450,7 @@ void main() { ); await testFitBounds( rotation: 240, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: symmetricPadding, ), @@ -463,7 +463,7 @@ void main() { ); await testFitBounds( rotation: 300, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: symmetricPadding, ), @@ -476,7 +476,7 @@ void main() { ); await testFitBounds( rotation: 360, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: symmetricPadding, ), @@ -494,7 +494,7 @@ void main() { await testFitBounds( rotation: -360, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: asymmetricPadding, ), @@ -507,7 +507,7 @@ void main() { ); await testFitBounds( rotation: -300, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: asymmetricPadding, ), @@ -520,7 +520,7 @@ void main() { ); await testFitBounds( rotation: -240, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: asymmetricPadding, ), @@ -533,7 +533,7 @@ void main() { ); await testFitBounds( rotation: -180, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: asymmetricPadding, ), @@ -546,7 +546,7 @@ void main() { ); await testFitBounds( rotation: -120, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: asymmetricPadding, ), @@ -559,7 +559,7 @@ void main() { ); await testFitBounds( rotation: -60, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: asymmetricPadding, ), @@ -572,7 +572,7 @@ void main() { ); await testFitBounds( rotation: 0, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: asymmetricPadding, ), @@ -585,7 +585,7 @@ void main() { ); await testFitBounds( rotation: 60, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: asymmetricPadding, ), @@ -598,7 +598,7 @@ void main() { ); await testFitBounds( rotation: 120, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: asymmetricPadding, ), @@ -611,7 +611,7 @@ void main() { ); await testFitBounds( rotation: 180, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: asymmetricPadding, ), @@ -624,7 +624,7 @@ void main() { ); await testFitBounds( rotation: 240, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: asymmetricPadding, ), @@ -637,7 +637,7 @@ void main() { ); await testFitBounds( rotation: 300, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: asymmetricPadding, ), @@ -650,7 +650,7 @@ void main() { ); await testFitBounds( rotation: 360, - frameConstraint: FrameFit.bounds( + cameraConstraint: CameraFit.bounds( bounds: bounds, padding: asymmetricPadding, ), @@ -682,17 +682,17 @@ void main() { }) async { controller.rotate(rotation); - controller.fitFrame(fitCoordinates); + controller.fitCamera(fitCoordinates); await tester.pump(); expect( - controller.mapFrame.center.latitude, + controller.mapCamera.center.latitude, moreOrLessEquals(expectedCenter.latitude), ); expect( - controller.mapFrame.center.longitude, + controller.mapCamera.center.longitude, moreOrLessEquals(expectedCenter.longitude), ); - expect(controller.mapFrame.zoom, moreOrLessEquals(expectedZoom)); + expect(controller.mapCamera.zoom, moreOrLessEquals(expectedZoom)); } FitCoordinates fitCoordinates({ diff --git a/test/flutter_map_test.dart b/test/flutter_map_test.dart index 629dcb9df..46a92d54d 100644 --- a/test/flutter_map_test.dart +++ b/test/flutter_map_test.dart @@ -43,7 +43,7 @@ void main() { children: [ Builder( builder: (context) { - final _ = MapFrame.of(context); + final _ = MapCamera.of(context); builds++; return const SizedBox.shrink(); }, @@ -76,11 +76,11 @@ void main() { expect(builds, equals(1)); }); - testWidgets('MapFrame.of only notifies dependencies when frame changes', + testWidgets('MapCamera.of only notifies dependencies when camera changes', (tester) async { int buildCount = 0; final Widget builder = Builder(builder: (BuildContext context) { - MapFrame.of(context); + MapCamera.of(context); buildCount++; return const SizedBox.shrink(); }); diff --git a/test/misc/frame_constraint_test.dart b/test/misc/frame_constraint_test.dart index 476c6f941..a14d1f54f 100644 --- a/test/misc/frame_constraint_test.dart +++ b/test/misc/frame_constraint_test.dart @@ -1,19 +1,23 @@ -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/src/geo/crs.dart'; +import 'package:flutter_map/src/geo/latlng_bounds.dart'; +import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/misc/camera_constraint.dart'; +import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; void main() { - group('FrameConstraint', () { + group('CameraConstraint', () { group('contain', () { test('rotated', () { - final mapConstraint = FrameConstraint.contain( + final mapConstraint = CameraConstraint.contain( bounds: LatLngBounds( const LatLng(-90, -180), const LatLng(90, 180), ), ); - final mapFrame = MapFrame( + final mapCamera = MapCamera( crs: const Epsg3857(), center: const LatLng(-90, -180), zoom: 1, @@ -21,7 +25,7 @@ void main() { nonRotatedSize: const CustomPoint(200, 300), ); - final clamped = mapConstraint.constrain(mapFrame)!; + final clamped = mapConstraint.constrain(mapCamera)!; expect(clamped.zoom, 1); expect(clamped.center.latitude, closeTo(-48.562, 0.001)); From b743773c84bc2cdb1c041ac0e7107a070544cd73 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Tue, 20 Jun 2023 17:36:37 +0200 Subject: [PATCH 31/46] Remove flutter_map_ prefixes from files in lib/src/map/ --- .../flutter_map_interactive_viewer.dart | 18 ++++++---- lib/src/map/camera.dart | 2 +- lib/src/map/flutter_map_internal_state.dart | 18 ---------- lib/src/map/flutter_map_state_container.dart | 24 ++++++------- ...erited_model.dart => inherited_model.dart} | 0 ...ntroller.dart => internal_controller.dart} | 26 ++++++++++---- lib/src/map/map_controller.dart | 2 +- lib/src/map/map_controller_impl.dart | 34 +++++++++---------- lib/src/map/options.dart | 2 +- 9 files changed, 63 insertions(+), 63 deletions(-) delete mode 100644 lib/src/map/flutter_map_internal_state.dart rename lib/src/map/{flutter_map_inherited_model.dart => inherited_model.dart} (100%) rename lib/src/map/{flutter_map_internal_controller.dart => internal_controller.dart} (95%) diff --git a/lib/src/gestures/flutter_map_interactive_viewer.dart b/lib/src/gestures/flutter_map_interactive_viewer.dart index 693371623..00f68d036 100644 --- a/lib/src/gestures/flutter_map_interactive_viewer.dart +++ b/lib/src/gestures/flutter_map_interactive_viewer.dart @@ -8,16 +8,18 @@ import 'package:flutter_map/src/gestures/latlng_tween.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; import 'package:flutter_map/src/map/camera.dart'; -import 'package:flutter_map/src/map/flutter_map_internal_controller.dart'; -import 'package:flutter_map/src/map/flutter_map_internal_state.dart'; +import 'package:flutter_map/src/map/internal_controller.dart'; import 'package:flutter_map/src/map/options.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; import 'package:latlong2/latlong.dart'; class FlutterMapInteractiveViewer extends StatefulWidget { - final Widget Function(BuildContext context, FlutterMapInternalState mapState) - builder; + final Widget Function( + BuildContext context, + MapOptions options, + MapCamera camera, + ) builder; final FlutterMapInternalController controller; const FlutterMapInteractiveViewer({ @@ -78,7 +80,7 @@ class FlutterMapInteractiveViewerState int _tapUpCounter = 0; Timer? _doubleTapHoldMaxDelay; - MapCamera get _mapCamera => widget.controller.mapCamera; + MapCamera get _mapCamera => widget.controller.camera; MapOptions get _options => widget.controller.options; InteractionOptions get _interactionOptions => _options.interactionOptions; @@ -265,7 +267,11 @@ class FlutterMapInteractiveViewerState : Duration.zero, child: RawGestureDetector( gestures: _gestures, - child: widget.builder(context, widget.controller.value), + child: widget.builder( + context, + widget.controller.options, + widget.controller.camera, + ), ), ), ); diff --git a/lib/src/map/camera.dart b/lib/src/map/camera.dart index b88848b53..d5ae5312d 100644 --- a/lib/src/map/camera.dart +++ b/lib/src/map/camera.dart @@ -3,7 +3,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_map/src/geo/crs.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/map/flutter_map_inherited_model.dart'; +import 'package:flutter_map/src/map/inherited_model.dart'; import 'package:flutter_map/src/map/options.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; diff --git a/lib/src/map/flutter_map_internal_state.dart b/lib/src/map/flutter_map_internal_state.dart deleted file mode 100644 index ce232c164..000000000 --- a/lib/src/map/flutter_map_internal_state.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter_map/src/map/camera.dart'; -import 'package:flutter_map/src/map/options.dart'; - -class FlutterMapInternalState { - final MapCamera mapCamera; - final MapOptions options; - - const FlutterMapInternalState({ - required this.options, - required this.mapCamera, - }); - - FlutterMapInternalState withMapCamera(MapCamera mapCamera) => - FlutterMapInternalState( - options: options, - mapCamera: mapCamera, - ); -} diff --git a/lib/src/map/flutter_map_state_container.dart b/lib/src/map/flutter_map_state_container.dart index 6cbaeabc8..a37fd634e 100644 --- a/lib/src/map/flutter_map_state_container.dart +++ b/lib/src/map/flutter_map_state_container.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/gestures/flutter_map_interactive_viewer.dart'; -import 'package:flutter_map/src/map/flutter_map_inherited_model.dart'; -import 'package:flutter_map/src/map/flutter_map_internal_controller.dart'; +import 'package:flutter_map/src/map/inherited_model.dart'; +import 'package:flutter_map/src/map/internal_controller.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; class FlutterMapStateContainer extends State { @@ -58,20 +58,20 @@ class FlutterMapStateContainer extends State { return FlutterMapInteractiveViewer( controller: _flutterMapInternalController, - builder: (context, mapState) => FlutterMapInheritedModel( + builder: (context, options, camera) => FlutterMapInheritedModel( controller: _mapController, - options: mapState.options, - camera: mapState.mapCamera, + options: options, + camera: camera, child: ClipRect( child: Stack( children: [ OverflowBox( - minWidth: mapState.mapCamera.size.x, - maxWidth: mapState.mapCamera.size.x, - minHeight: mapState.mapCamera.size.y, - maxHeight: mapState.mapCamera.size.y, + minWidth: camera.size.x, + maxWidth: camera.size.x, + minHeight: camera.size.y, + maxHeight: camera.size.y, child: Transform.rotate( - angle: mapState.mapCamera.rotationRad, + angle: camera.rotationRad, child: Stack(children: widget.children), ), ), @@ -123,10 +123,10 @@ class FlutterMapStateContainer extends State { constraints.maxWidth, constraints.maxHeight, ); - final oldMapCamera = _flutterMapInternalController.mapCamera; + final oldMapCamera = _flutterMapInternalController.camera; if (_flutterMapInternalController .setNonRotatedSizeWithoutEmittingEvent(nonRotatedSize)) { - final newMapCamera = _flutterMapInternalController.mapCamera; + final newMapCamera = _flutterMapInternalController.camera; // Avoid emitting the event during build otherwise if the user calls // setState in the onMapEvent callback it will throw. diff --git a/lib/src/map/flutter_map_inherited_model.dart b/lib/src/map/inherited_model.dart similarity index 100% rename from lib/src/map/flutter_map_inherited_model.dart rename to lib/src/map/inherited_model.dart diff --git a/lib/src/map/flutter_map_internal_controller.dart b/lib/src/map/internal_controller.dart similarity index 95% rename from lib/src/map/flutter_map_internal_controller.dart rename to lib/src/map/internal_controller.dart index 17d106919..22a841931 100644 --- a/lib/src/map/flutter_map_internal_controller.dart +++ b/lib/src/map/internal_controller.dart @@ -2,20 +2,18 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_map/plugin_api.dart'; import 'package:flutter_map/src/gestures/flutter_map_interactive_viewer.dart'; -import 'package:flutter_map/src/map/flutter_map_internal_state.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; import 'package:latlong2/latlong.dart'; // This controller is for internal use. All updates to the state should be done // by calling methods of this class to ensure consistency. -class FlutterMapInternalController - extends ValueNotifier { +class FlutterMapInternalController extends ValueNotifier<_InternalState> { late final FlutterMapInteractiveViewerState _interactiveViewerState; late MapControllerImpl _mapControllerImpl; FlutterMapInternalController(MapOptions options) : super( - FlutterMapInternalState( + _InternalState( options: options, mapCamera: MapCamera.initialCamera(options), ), @@ -40,7 +38,8 @@ class FlutterMapInternalController /// to the [FlutterMapInternalState] should be done via methods in this class. @visibleForTesting @override - set value(FlutterMapInternalState value) => super.value = value; + // ignore: library_private_types_in_public_api + set value(_InternalState value) => super.value = value; // Note: All named parameters are required to prevent inconsistent default // values since this method can be called by MapController which declares @@ -274,7 +273,7 @@ class FlutterMapInternalController ); } - value = FlutterMapInternalState( + value = _InternalState( options: newOptions, mapCamera: newMapCamera, ); @@ -457,3 +456,18 @@ class FlutterMapInternalController _mapControllerImpl.mapEventSink.add(event); } } + +class _InternalState { + final MapCamera mapCamera; + final MapOptions options; + + const _InternalState({ + required this.options, + required this.mapCamera, + }); + + _InternalState withMapCamera(MapCamera mapCamera) => _InternalState( + options: options, + mapCamera: mapCamera, + ); +} diff --git a/lib/src/map/map_controller.dart b/lib/src/map/map_controller.dart index 627e8a137..058f927ba 100644 --- a/lib/src/map/map_controller.dart +++ b/lib/src/map/map_controller.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/map/camera.dart'; -import 'package:flutter_map/src/map/flutter_map_inherited_model.dart'; +import 'package:flutter_map/src/map/inherited_model.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/map_controller_impl.dart index ac3cbd237..dd43bb652 100644 --- a/lib/src/map/map_controller_impl.dart +++ b/lib/src/map/map_controller_impl.dart @@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/map/camera.dart'; -import 'package:flutter_map/src/map/flutter_map_internal_controller.dart'; +import 'package:flutter_map/src/map/internal_controller.dart'; import 'package:flutter_map/src/map/map_controller.dart'; import 'package:flutter_map/src/misc/camera_fit.dart'; import 'package:flutter_map/src/misc/center_zoom.dart'; @@ -14,8 +14,23 @@ import 'package:flutter_map/src/misc/point.dart'; import 'package:latlong2/latlong.dart'; class MapControllerImpl implements MapController { + late FlutterMapInternalController _internalController; + final _mapEventStreamController = StreamController.broadcast(); + MapControllerImpl(); + set internalController(FlutterMapInternalController internalController) { + _internalController = internalController; + } + + @override + MapCamera get mapCamera => _internalController.mapCamera; + + @override + Stream get mapEventStream => _mapEventStreamController.stream; + + StreamSink get mapEventSink => _mapEventStreamController.sink; + @override bool move( LatLng center, @@ -100,23 +115,6 @@ class MapControllerImpl implements MapController { cameraFit, offset: Offset.zero, ); - - @override - MapCamera get mapCamera => _internalController.mapCamera; - - final _mapEventStreamController = StreamController.broadcast(); - - @override - Stream get mapEventStream => _mapEventStreamController.stream; - - StreamSink get mapEventSink => _mapEventStreamController.sink; - - late FlutterMapInternalController _internalController; - - set internalController(FlutterMapInternalController internalController) { - _internalController = internalController; - } - @override void dispose() { _mapEventStreamController.close(); diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index f8587e033..a20439c35 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -5,7 +5,7 @@ import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/gestures/interactive_flag.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; -import 'package:flutter_map/src/map/flutter_map_inherited_model.dart'; +import 'package:flutter_map/src/map/inherited_model.dart'; import 'package:flutter_map/src/misc/camera_constraint.dart'; import 'package:flutter_map/src/misc/camera_fit.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; From 7f0a3867c21bedcb34d9ac070cde60ba436a95d2 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Tue, 20 Jun 2023 17:37:18 +0200 Subject: [PATCH 32/46] Move FlutterMapStateContainer in to FlutterMap's file since it is just the widget state --- lib/src/map/flutter_map_state_container.dart | 153 ------------------ lib/src/map/widget.dart | 157 ++++++++++++++++++- 2 files changed, 155 insertions(+), 155 deletions(-) delete mode 100644 lib/src/map/flutter_map_state_container.dart diff --git a/lib/src/map/flutter_map_state_container.dart b/lib/src/map/flutter_map_state_container.dart deleted file mode 100644 index a37fd634e..000000000 --- a/lib/src/map/flutter_map_state_container.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/gestures/flutter_map_interactive_viewer.dart'; -import 'package:flutter_map/src/map/inherited_model.dart'; -import 'package:flutter_map/src/map/internal_controller.dart'; -import 'package:flutter_map/src/map/map_controller_impl.dart'; - -class FlutterMapStateContainer extends State { - bool _initialCameraFitApplied = false; - - late final FlutterMapInternalController _flutterMapInternalController; - late MapControllerImpl _mapController; - late bool _mapControllerCreatedInternally; - - @override - void initState() { - super.initState(); - _flutterMapInternalController = - FlutterMapInternalController(widget.options); - _initializeAndLinkMapController(); - - WidgetsBinding.instance - .addPostFrameCallback((_) => widget.options.onMapReady?.call()); - } - - @override - void didUpdateWidget(FlutterMap oldWidget) { - if (oldWidget.options != widget.options) { - _flutterMapInternalController.setOptions(widget.options); - } - if (oldWidget.mapController != widget.mapController) { - _initializeAndLinkMapController(); - } - super.didUpdateWidget(oldWidget); - } - - @override - void dispose() { - if (_mapControllerCreatedInternally) _mapController.dispose(); - _flutterMapInternalController.dispose(); - super.dispose(); - } - - void _initializeAndLinkMapController() { - _mapController = - (widget.mapController ?? MapController()) as MapControllerImpl; - _mapControllerCreatedInternally = widget.mapController == null; - _flutterMapInternalController.linkMapController(_mapController); - } - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - _updateAndEmitSizeIfConstraintsChanged(constraints); - _applyInitialCameraFit(constraints); - - return FlutterMapInteractiveViewer( - controller: _flutterMapInternalController, - builder: (context, options, camera) => FlutterMapInheritedModel( - controller: _mapController, - options: options, - camera: camera, - child: ClipRect( - child: Stack( - children: [ - OverflowBox( - minWidth: camera.size.x, - maxWidth: camera.size.x, - minHeight: camera.size.y, - maxHeight: camera.size.y, - child: Transform.rotate( - angle: camera.rotationRad, - child: Stack(children: widget.children), - ), - ), - ...widget.nonRotatedChildren, - ], - ), - ), - ), - ); - }, - ); - } - - void _applyInitialCameraFit(BoxConstraints constraints) { - // If an initial camera fit was provided apply it to the map state once the - // the parent constraints are available. - - if (!_initialCameraFitApplied && - (widget.options.bounds != null || - widget.options.initialCameraFit != null) && - _parentConstraintsAreSet(context, constraints)) { - _initialCameraFitApplied = true; - - final CameraFit cameraFit; - - if (widget.options.bounds != null) { - // Create the camera fit from the deprecated option. - final fitBoundsOptions = widget.options.boundsOptions; - cameraFit = CameraFit.bounds( - bounds: widget.options.bounds!, - padding: fitBoundsOptions.padding, - maxZoom: fitBoundsOptions.maxZoom, - inside: fitBoundsOptions.inside, - forceIntegerZoomLevel: fitBoundsOptions.forceIntegerZoomLevel, - ); - } else { - cameraFit = widget.options.initialCameraFit!; - } - - _flutterMapInternalController.fitCamera( - cameraFit, - offset: Offset.zero, - ); - } - } - - void _updateAndEmitSizeIfConstraintsChanged(BoxConstraints constraints) { - final nonRotatedSize = CustomPoint( - constraints.maxWidth, - constraints.maxHeight, - ); - final oldMapCamera = _flutterMapInternalController.camera; - if (_flutterMapInternalController - .setNonRotatedSizeWithoutEmittingEvent(nonRotatedSize)) { - final newMapCamera = _flutterMapInternalController.camera; - - // Avoid emitting the event during build otherwise if the user calls - // setState in the onMapEvent callback it will throw. - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - _flutterMapInternalController.nonRotatedSizeChange( - MapEventSource.nonRotatedSizeChange, - oldMapCamera, - newMapCamera, - ); - } - }); - } - } - - // During Flutter startup the native platform resolution is not immediately - // available which can cause constraints to be zero before they are updated - // in a subsequent build to the actual constraints. This check allows us to - // differentiate zero constraints caused by missing platform resolution vs - // zero constraints which were actually provided by the parent widget. - bool _parentConstraintsAreSet( - BuildContext context, BoxConstraints constraints) => - constraints.maxWidth != 0 || MediaQuery.sizeOf(context) != Size.zero; -} diff --git a/lib/src/map/widget.dart b/lib/src/map/widget.dart index a90e58c4c..0454828bd 100644 --- a/lib/src/map/widget.dart +++ b/lib/src/map/widget.dart @@ -1,6 +1,13 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/flutter_map_state_container.dart'; +import 'package:flutter_map/src/gestures/flutter_map_interactive_viewer.dart'; +import 'package:flutter_map/src/gestures/map_events.dart'; +import 'package:flutter_map/src/map/inherited_model.dart'; +import 'package:flutter_map/src/map/internal_controller.dart'; +import 'package:flutter_map/src/map/map_controller.dart'; +import 'package:flutter_map/src/map/map_controller_impl.dart'; +import 'package:flutter_map/src/map/options.dart'; +import 'package:flutter_map/src/misc/camera_fit.dart'; +import 'package:flutter_map/src/misc/point.dart'; /// Renders an interactive geographical map as a widget /// @@ -34,3 +41,149 @@ class FlutterMap extends StatefulWidget { @override State createState() => FlutterMapStateContainer(); } + +class FlutterMapStateContainer extends State { + bool _initialCameraFitApplied = false; + + late final FlutterMapInternalController _flutterMapInternalController; + late MapControllerImpl _mapController; + late bool _mapControllerCreatedInternally; + + @override + void initState() { + super.initState(); + _flutterMapInternalController = + FlutterMapInternalController(widget.options); + _initializeAndLinkMapController(); + + WidgetsBinding.instance + .addPostFrameCallback((_) => widget.options.onMapReady?.call()); + } + + @override + void didUpdateWidget(FlutterMap oldWidget) { + if (oldWidget.options != widget.options) { + _flutterMapInternalController.setOptions(widget.options); + } + if (oldWidget.mapController != widget.mapController) { + _initializeAndLinkMapController(); + } + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + if (_mapControllerCreatedInternally) _mapController.dispose(); + _flutterMapInternalController.dispose(); + super.dispose(); + } + + void _initializeAndLinkMapController() { + _mapController = + (widget.mapController ?? MapController()) as MapControllerImpl; + _mapControllerCreatedInternally = widget.mapController == null; + _flutterMapInternalController.linkMapController(_mapController); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + _updateAndEmitSizeIfConstraintsChanged(constraints); + _applyInitialCameraFit(constraints); + + return FlutterMapInteractiveViewer( + controller: _flutterMapInternalController, + builder: (context, options, camera) => FlutterMapInheritedModel( + controller: _mapController, + options: options, + camera: camera, + child: ClipRect( + child: Stack( + children: [ + OverflowBox( + minWidth: camera.size.x, + maxWidth: camera.size.x, + minHeight: camera.size.y, + maxHeight: camera.size.y, + child: Transform.rotate( + angle: camera.rotationRad, + child: Stack(children: widget.children), + ), + ), + ...widget.nonRotatedChildren, + ], + ), + ), + ), + ); + }, + ); + } + + void _applyInitialCameraFit(BoxConstraints constraints) { + // If an initial camera fit was provided apply it to the map state once the + // the parent constraints are available. + + if (!_initialCameraFitApplied && + (widget.options.bounds != null || + widget.options.initialCameraFit != null) && + _parentConstraintsAreSet(context, constraints)) { + _initialCameraFitApplied = true; + + final CameraFit cameraFit; + + if (widget.options.bounds != null) { + // Create the camera fit from the deprecated option. + final fitBoundsOptions = widget.options.boundsOptions; + cameraFit = CameraFit.bounds( + bounds: widget.options.bounds!, + padding: fitBoundsOptions.padding, + maxZoom: fitBoundsOptions.maxZoom, + inside: fitBoundsOptions.inside, + forceIntegerZoomLevel: fitBoundsOptions.forceIntegerZoomLevel, + ); + } else { + cameraFit = widget.options.initialCameraFit!; + } + + _flutterMapInternalController.fitCamera( + cameraFit, + offset: Offset.zero, + ); + } + } + + void _updateAndEmitSizeIfConstraintsChanged(BoxConstraints constraints) { + final nonRotatedSize = CustomPoint( + constraints.maxWidth, + constraints.maxHeight, + ); + final oldMapCamera = _flutterMapInternalController.camera; + if (_flutterMapInternalController + .setNonRotatedSizeWithoutEmittingEvent(nonRotatedSize)) { + final newMapCamera = _flutterMapInternalController.camera; + + // Avoid emitting the event during build otherwise if the user calls + // setState in the onMapEvent callback it will throw. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _flutterMapInternalController.nonRotatedSizeChange( + MapEventSource.nonRotatedSizeChange, + oldMapCamera, + newMapCamera, + ); + } + }); + } + } + + // During Flutter startup the native platform resolution is not immediately + // available which can cause constraints to be zero before they are updated + // in a subsequent build to the actual constraints. This check allows us to + // differentiate zero constraints caused by missing platform resolution vs + // zero constraints which were actually provided by the parent widget. + bool _parentConstraintsAreSet( + BuildContext context, BoxConstraints constraints) => + constraints.maxWidth != 0 || MediaQuery.sizeOf(context) != Size.zero; +} From 4c99df78eab30bed6848861c76da6f308a3534cf Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Tue, 20 Jun 2023 17:45:48 +0200 Subject: [PATCH 33/46] Rename mapCamera variables to camera for consistency with options for MapOptions --- .../lib/pages/animated_map_controller.dart | 10 +- example/lib/pages/latlng_to_screen_point.dart | 3 +- example/lib/pages/map_controller.dart | 2 +- example/lib/pages/point_to_latlng.dart | 3 +- .../flutter_map_interactive_viewer.dart | 87 ++++++------- lib/src/gestures/map_events.dart | 78 +++++------ lib/src/layer/tile_layer/tile_layer.dart | 22 ++-- .../tile_layer/tile_range_calculator.dart | 20 +-- .../layer/tile_layer/tile_update_event.dart | 6 +- lib/src/map/internal_controller.dart | 123 +++++++++--------- lib/src/map/map_controller.dart | 18 +-- lib/src/map/map_controller_impl.dart | 34 ++--- lib/src/map/widget.dart | 4 +- lib/src/misc/camera_constraint.dart | 30 ++--- lib/src/misc/camera_fit.dart | 70 +++++----- test/flutter_map_controller_test.dart | 52 ++++---- test/misc/frame_constraint_test.dart | 4 +- 17 files changed, 280 insertions(+), 286 deletions(-) diff --git a/example/lib/pages/animated_map_controller.dart b/example/lib/pages/animated_map_controller.dart index 1e2c5ad06..8aeb6a15d 100644 --- a/example/lib/pages/animated_map_controller.dart +++ b/example/lib/pages/animated_map_controller.dart @@ -35,12 +35,12 @@ class AnimatedMapControllerPageState extends State void _animatedMapMove(LatLng destLocation, double destZoom) { // Create some tweens. These serve to split up the transition from one location to another. // In our case, we want to split the transition be our current map center and the destination. - final mapCamera = mapController.mapCamera; + final camera = mapController.camera; final latTween = Tween( - begin: mapCamera.center.latitude, end: destLocation.latitude); + begin: camera.center.latitude, end: destLocation.latitude); final lngTween = Tween( - begin: mapCamera.center.longitude, end: destLocation.longitude); - final zoomTween = Tween(begin: mapCamera.zoom, end: destZoom); + begin: camera.center.longitude, end: destLocation.longitude); + final zoomTween = Tween(begin: camera.zoom, end: destZoom); // Create a animation controller that has a duration and a TickerProvider. final controller = AnimationController( @@ -181,7 +181,7 @@ class AnimatedMapControllerPageState extends State final constrained = CameraFit.bounds( bounds: bounds, - ).fit(mapController.mapCamera); + ).fit(mapController.camera); _animatedMapMove(constrained.center, constrained.zoom); }, child: const Text('Fit Bounds animated'), diff --git a/example/lib/pages/latlng_to_screen_point.dart b/example/lib/pages/latlng_to_screen_point.dart index 6754de890..db629de91 100644 --- a/example/lib/pages/latlng_to_screen_point.dart +++ b/example/lib/pages/latlng_to_screen_point.dart @@ -47,8 +47,7 @@ class _LatLngScreenPointTestPageState extends State { options: MapOptions( onMapEvent: onMapEvent, onTap: (tapPos, latLng) { - final pt1 = - _mapController.mapCamera.latLngToScreenPoint(latLng); + final pt1 = _mapController.camera.latLngToScreenPoint(latLng); _textPos = CustomPoint(pt1.x, pt1.y); setState(() {}); }, diff --git a/example/lib/pages/map_controller.dart b/example/lib/pages/map_controller.dart index 6bfb26925..24dc7755b 100644 --- a/example/lib/pages/map_controller.dart +++ b/example/lib/pages/map_controller.dart @@ -116,7 +116,7 @@ class MapControllerPageState extends State { Builder(builder: (BuildContext context) { return MaterialButton( onPressed: () { - final bounds = _mapController.mapCamera.visibleBounds; + final bounds = _mapController.camera.visibleBounds; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( diff --git a/example/lib/pages/point_to_latlng.dart b/example/lib/pages/point_to_latlng.dart index cbe93fe12..e0ab2c55f 100644 --- a/example/lib/pages/point_to_latlng.dart +++ b/example/lib/pages/point_to_latlng.dart @@ -106,8 +106,7 @@ class PointToLatlngPage extends State { void updatePoint(MapEvent? event, BuildContext context) { final pointX = _getPointX(context); setState(() { - latLng = - mapController.mapCamera.pointToLatLng(CustomPoint(pointX, pointY)); + latLng = mapController.camera.pointToLatLng(CustomPoint(pointX, pointY)); }); } diff --git a/lib/src/gestures/flutter_map_interactive_viewer.dart b/lib/src/gestures/flutter_map_interactive_viewer.dart index 00f68d036..1b21a9a87 100644 --- a/lib/src/gestures/flutter_map_interactive_viewer.dart +++ b/lib/src/gestures/flutter_map_interactive_viewer.dart @@ -80,7 +80,7 @@ class FlutterMapInteractiveViewerState int _tapUpCounter = 0; Timer? _doubleTapHoldMaxDelay; - MapCamera get _mapCamera => widget.controller.camera; + MapCamera get _camera => widget.controller.camera; MapOptions get _options => widget.controller.options; InteractionOptions get _interactionOptions => _options.interactionOptions; @@ -281,7 +281,7 @@ class FlutterMapInteractiveViewerState ++_pointerCounter; if (_options.onPointerDown != null) { - final latlng = _mapCamera.offsetToCrs(event.localPosition); + final latlng = _camera.offsetToCrs(event.localPosition); _options.onPointerDown!(event, latlng); } } @@ -290,7 +290,7 @@ class FlutterMapInteractiveViewerState --_pointerCounter; if (_options.onPointerUp != null) { - final latlng = _mapCamera.offsetToCrs(event.localPosition); + final latlng = _camera.offsetToCrs(event.localPosition); _options.onPointerUp!(event, latlng); } } @@ -299,14 +299,14 @@ class FlutterMapInteractiveViewerState --_pointerCounter; if (_options.onPointerCancel != null) { - final latlng = _mapCamera.offsetToCrs(event.localPosition); + final latlng = _camera.offsetToCrs(event.localPosition); _options.onPointerCancel!(event, latlng); } } void _onPointerHover(PointerHoverEvent event) { if (_options.onPointerHover != null) { - final latlng = _mapCamera.offsetToCrs(event.localPosition); + final latlng = _camera.offsetToCrs(event.localPosition); _options.onPointerHover!(event, latlng); } } @@ -324,12 +324,12 @@ class FlutterMapInteractiveViewerState pointerSignal as PointerScrollEvent; final minZoom = _options.minZoom ?? 0.0; final maxZoom = _options.maxZoom ?? double.infinity; - final newZoom = (_mapCamera.zoom - + final newZoom = (_camera.zoom - pointerSignal.scrollDelta.dy * _interactionOptions.scrollWheelVelocity) .clamp(minZoom, maxZoom); // Calculate offset of mouse cursor from viewport center - final newCenter = _mapCamera.focusedZoomCenter( + final newCenter = _camera.focusedZoomCenter( pointerSignal.localPosition.toCustomPoint(), newZoom, ); @@ -394,10 +394,10 @@ class FlutterMapInteractiveViewerState _gestureWinner = MultiFingerGesture.none; - _mapZoomStart = _mapCamera.zoom; - _mapCenterStart = _mapCamera.center; + _mapZoomStart = _camera.zoom; + _mapCenterStart = _camera.center; _focalStartLocal = _lastFocalLocal = details.localFocalPoint; - _focalStartLatLng = _mapCamera.offsetToCrs(_focalStartLocal); + _focalStartLatLng = _camera.offsetToCrs(_focalStartLocal); _dragStarted = false; _pinchZoomStarted = false; @@ -493,8 +493,8 @@ class FlutterMapInteractiveViewerState bool hasPinchZoom, bool hasPinchMove, ) { - LatLng newCenter = _mapCamera.center; - double newZoom = _mapCamera.zoom; + LatLng newCenter = _camera.center; + double newZoom = _camera.zoom; // Handle pinch zoom. if (hasPinchZoom && details.scale > 0.0) { @@ -546,19 +546,17 @@ class FlutterMapInteractiveViewerState ScaleUpdateDetails details, double zoomAfterPinchZoom, ) { - final oldCenterPt = - _mapCamera.project(_mapCamera.center, zoomAfterPinchZoom); + final oldCenterPt = _camera.project(_camera.center, zoomAfterPinchZoom); final newFocalLatLong = - _mapCamera.offsetToCrs(_focalStartLocal, zoomAfterPinchZoom); - final newFocalPt = _mapCamera.project(newFocalLatLong, zoomAfterPinchZoom); - final oldFocalPt = - _mapCamera.project(_focalStartLatLng, zoomAfterPinchZoom); + _camera.offsetToCrs(_focalStartLocal, zoomAfterPinchZoom); + final newFocalPt = _camera.project(newFocalLatLong, zoomAfterPinchZoom); + final oldFocalPt = _camera.project(_focalStartLatLng, zoomAfterPinchZoom); final zoomDifference = oldFocalPt - newFocalPt; final moveDifference = _rotateOffset(_focalStartLocal - _lastFocalLocal); final newCenterPt = oldCenterPt + zoomDifference + moveDifference.toCustomPoint(); - return _mapCamera.unproject(newCenterPt, zoomAfterPinchZoom); + return _camera.unproject(newCenterPt, zoomAfterPinchZoom); } void _handleScalePinchRotate( @@ -572,17 +570,17 @@ class FlutterMapInteractiveViewerState if (_rotationStarted) { final rotationDiff = currentRotation - _lastRotation; - final oldCenterPt = _mapCamera.project(_mapCamera.center); + final oldCenterPt = _camera.project(_camera.center); final rotationCenter = - _mapCamera.project(_mapCamera.offsetToCrs(_lastFocalLocal)); + _camera.project(_camera.offsetToCrs(_lastFocalLocal)); final vector = oldCenterPt - rotationCenter; final rotatedVector = vector.rotate(degToRadian(rotationDiff)); final newCenter = rotationCenter + rotatedVector; widget.controller.moveAndRotate( - _mapCamera.unproject(newCenter), - _mapCamera.zoom, - _mapCamera.rotation + rotationDiff, + _camera.unproject(newCenter), + _camera.zoom, + _camera.rotation + rotationDiff, offset: Offset.zero, hasGesture: true, source: MapEventSource.onMultiFinger, @@ -647,9 +645,9 @@ class FlutterMapInteractiveViewerState } final direction = details.velocity.pixelsPerSecond / magnitude; - final distance = (Offset.zero & - Size(_mapCamera.nonRotatedSize.x, _mapCamera.nonRotatedSize.y)) - .shortestSide; + final distance = + (Offset.zero & Size(_camera.nonRotatedSize.x, _camera.nonRotatedSize.y)) + .shortestSide; final flingOffset = _focalStartLocal - _lastFocalLocal; _flingAnimation = Tween( @@ -678,7 +676,7 @@ class FlutterMapInteractiveViewerState widget.controller.tapped( MapEventSource.tap, position, - _mapCamera.offsetToCrs(relativePosition), + _camera.offsetToCrs(relativePosition), ); } @@ -692,7 +690,7 @@ class FlutterMapInteractiveViewerState widget.controller.secondaryTapped( MapEventSource.secondaryTap, position, - _mapCamera.offsetToCrs(relativePosition), + _camera.offsetToCrs(relativePosition), ); } @@ -705,7 +703,7 @@ class FlutterMapInteractiveViewerState widget.controller.longPressed( MapEventSource.longPress, position, - _mapCamera.offsetToCrs(position.relative!), + _camera.offsetToCrs(position.relative!), ); } @@ -716,8 +714,8 @@ class FlutterMapInteractiveViewerState _closeDoubleTapController(MapEventSource.doubleTap); if (InteractiveFlag.hasDoubleTapZoom(_interactionOptions.flags)) { - final newZoom = _getZoomForScale(_mapCamera.zoom, 2); - final newCenter = _mapCamera.focusedZoomCenter( + final newZoom = _getZoomForScale(_camera.zoom, 2); + final newCenter = _camera.focusedZoomCenter( tapPosition.relative!.toCustomPoint(), newZoom, ); @@ -726,12 +724,11 @@ class FlutterMapInteractiveViewerState } void _startDoubleTapAnimation(double newZoom, LatLng newCenter) { - _doubleTapZoomAnimation = - Tween(begin: _mapCamera.zoom, end: newZoom) - .chain(CurveTween(curve: Curves.linear)) - .animate(_doubleTapController); + _doubleTapZoomAnimation = Tween(begin: _camera.zoom, end: newZoom) + .chain(CurveTween(curve: Curves.linear)) + .animate(_doubleTapController); _doubleTapCenterAnimation = - LatLngTween(begin: _mapCamera.center, end: newCenter) + LatLngTween(begin: _camera.center, end: newCenter) .chain(CurveTween(curve: Curves.linear)) .animate(_doubleTapController); _doubleTapController.forward(from: 0); @@ -778,14 +775,14 @@ class FlutterMapInteractiveViewerState final flags = _interactionOptions.flags; if (InteractiveFlag.hasPinchZoom(flags)) { final verticalOffset = (_focalStartLocal - details.localFocalPoint).dy; - final newZoom = _mapZoomStart - verticalOffset / 360 * _mapCamera.zoom; + final newZoom = _mapZoomStart - verticalOffset / 360 * _camera.zoom; final min = _options.minZoom ?? 0.0; final max = _options.maxZoom ?? double.infinity; final actualZoom = math.max(min, math.min(max, newZoom)); widget.controller.move( - _mapCamera.center, + _camera.center, actualZoom, offset: Offset.zero, hasGesture: true, @@ -802,13 +799,13 @@ class FlutterMapInteractiveViewerState _startListeningForAnimationInterruptions(); } - final newCenterPoint = _mapCamera.project(_mapCenterStart) + - _flingAnimation.value.toCustomPoint().rotate(_mapCamera.rotationRad); - final newCenter = _mapCamera.unproject(newCenterPoint); + final newCenterPoint = _camera.project(_mapCenterStart) + + _flingAnimation.value.toCustomPoint().rotate(_camera.rotationRad); + final newCenter = _camera.unproject(newCenterPoint); widget.controller.move( newCenter, - _mapCamera.zoom, + _camera.zoom, offset: Offset.zero, hasGesture: true, source: MapEventSource.flingAnimationController, @@ -847,11 +844,11 @@ class FlutterMapInteractiveViewerState double _getZoomForScale(double startZoom, double scale) { final resultZoom = scale == 1.0 ? startZoom : startZoom + math.log(scale) / math.ln2; - return _mapCamera.clampZoom(resultZoom); + return _camera.clampZoom(resultZoom); } Offset _rotateOffset(Offset offset) { - final radians = _mapCamera.rotationRad; + final radians = _camera.rotationRad; if (radians != 0.0) { final cos = math.cos(radians); final sin = math.sin(radians); diff --git a/lib/src/gestures/map_events.dart b/lib/src/gestures/map_events.dart index 08825a415..d597ed27d 100644 --- a/lib/src/gestures/map_events.dart +++ b/lib/src/gestures/map_events.dart @@ -33,11 +33,11 @@ abstract class MapEvent { final MapEventSource source; /// The map camera after the event. - final MapCamera mapCamera; + final MapCamera camera; const MapEvent({ required this.source, - required this.mapCamera, + required this.camera, }); } @@ -45,38 +45,38 @@ abstract class MapEvent { /// includes information about camera movement /// which are not partial (e.g start rotate, rotate, end rotate). abstract class MapEventWithMove extends MapEvent { - final MapCamera oldMapCamera; + final MapCamera oldCamera; const MapEventWithMove({ required super.source, - required this.oldMapCamera, - required super.mapCamera, + required this.oldCamera, + required super.camera, }); /// Returns a subclass of [MapEventWithMove] if the [source] belongs to a /// movement event, otherwise returns null. static MapEventWithMove? fromSource({ - required MapCamera oldMapCamera, - required MapCamera mapCamera, + required MapCamera oldCamera, + required MapCamera camera, required bool hasGesture, required MapEventSource source, String? id, }) => switch (source) { MapEventSource.flingAnimationController => MapEventFlingAnimation( - oldMapCamera: oldMapCamera, - mapCamera: mapCamera, + oldCamera: oldCamera, + camera: camera, source: source, ), MapEventSource.doubleTapZoomAnimationController => MapEventDoubleTapZoom( - oldMapCamera: oldMapCamera, - mapCamera: mapCamera, + oldCamera: oldCamera, + camera: camera, source: source, ), MapEventSource.scrollWheel => MapEventScrollWheelZoom( - oldMapCamera: oldMapCamera, - mapCamera: mapCamera, + oldCamera: oldCamera, + camera: camera, source: source, ), MapEventSource.onDrag || @@ -85,8 +85,8 @@ abstract class MapEventWithMove extends MapEvent { MapEventSource.custom => MapEventMove( id: id, - oldMapCamera: oldMapCamera, - mapCamera: mapCamera, + oldCamera: oldCamera, + camera: camera, source: source, ), _ => null, @@ -101,7 +101,7 @@ class MapEventTap extends MapEvent { const MapEventTap({ required this.tapPosition, required super.source, - required super.mapCamera, + required super.camera, }); } @@ -112,7 +112,7 @@ class MapEventSecondaryTap extends MapEvent { const MapEventSecondaryTap({ required this.tapPosition, required super.source, - required super.mapCamera, + required super.camera, }); } @@ -124,7 +124,7 @@ class MapEventLongPress extends MapEvent { const MapEventLongPress({ required this.tapPosition, required super.source, - required super.mapCamera, + required super.camera, }); } @@ -136,8 +136,8 @@ class MapEventMove extends MapEventWithMove { const MapEventMove({ this.id, required super.source, - required super.oldMapCamera, - required super.mapCamera, + required super.oldCamera, + required super.camera, }); } @@ -145,7 +145,7 @@ class MapEventMove extends MapEventWithMove { class MapEventMoveStart extends MapEvent { const MapEventMoveStart({ required super.source, - required super.mapCamera, + required super.camera, }); } @@ -153,7 +153,7 @@ class MapEventMoveStart extends MapEvent { class MapEventMoveEnd extends MapEvent { const MapEventMoveEnd({ required super.source, - required super.mapCamera, + required super.camera, }); } @@ -161,8 +161,8 @@ class MapEventMoveEnd extends MapEvent { class MapEventFlingAnimation extends MapEventWithMove { const MapEventFlingAnimation({ required super.source, - required super.oldMapCamera, - required super.mapCamera, + required super.oldCamera, + required super.camera, }); } @@ -171,7 +171,7 @@ class MapEventFlingAnimation extends MapEventWithMove { class MapEventFlingAnimationNotStarted extends MapEvent { const MapEventFlingAnimationNotStarted({ required super.source, - required super.mapCamera, + required super.camera, }); } @@ -179,7 +179,7 @@ class MapEventFlingAnimationNotStarted extends MapEvent { class MapEventFlingAnimationStart extends MapEvent { const MapEventFlingAnimationStart({ required super.source, - required super.mapCamera, + required super.camera, }); } @@ -187,7 +187,7 @@ class MapEventFlingAnimationStart extends MapEvent { class MapEventFlingAnimationEnd extends MapEvent { const MapEventFlingAnimationEnd({ required super.source, - required super.mapCamera, + required super.camera, }); } @@ -195,8 +195,8 @@ class MapEventFlingAnimationEnd extends MapEvent { class MapEventDoubleTapZoom extends MapEventWithMove { const MapEventDoubleTapZoom({ required super.source, - required super.oldMapCamera, - required super.mapCamera, + required super.oldCamera, + required super.camera, }); } @@ -204,8 +204,8 @@ class MapEventDoubleTapZoom extends MapEventWithMove { class MapEventScrollWheelZoom extends MapEventWithMove { const MapEventScrollWheelZoom({ required super.source, - required super.oldMapCamera, - required super.mapCamera, + required super.oldCamera, + required super.camera, }); } @@ -213,7 +213,7 @@ class MapEventScrollWheelZoom extends MapEventWithMove { class MapEventDoubleTapZoomStart extends MapEvent { const MapEventDoubleTapZoomStart({ required super.source, - required super.mapCamera, + required super.camera, }); } @@ -221,7 +221,7 @@ class MapEventDoubleTapZoomStart extends MapEvent { class MapEventDoubleTapZoomEnd extends MapEvent { const MapEventDoubleTapZoomEnd({ required super.source, - required super.mapCamera, + required super.camera, }); } @@ -233,8 +233,8 @@ class MapEventRotate extends MapEventWithMove { const MapEventRotate({ required this.id, required super.source, - required super.oldMapCamera, - required super.mapCamera, + required super.oldCamera, + required super.camera, }); } @@ -242,21 +242,21 @@ class MapEventRotate extends MapEventWithMove { class MapEventRotateStart extends MapEvent { const MapEventRotateStart({ required super.source, - required super.mapCamera, + required super.camera, }); } class MapEventRotateEnd extends MapEvent { const MapEventRotateEnd({ required super.source, - required super.mapCamera, + required super.camera, }); } class MapEventNonRotatedSizeChange extends MapEventWithMove { const MapEventNonRotatedSizeChange({ required super.source, - required super.oldMapCamera, - required super.mapCamera, + required super.oldCamera, + required super.camera, }); } diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 758eac420..0a45adbe0 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -329,7 +329,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { void didChangeDependencies() { super.didChangeDependencies(); - final mapCamera = MapCamera.of(context); + final camera = MapCamera.of(context); final mapController = MapController.of(context); if (_mapControllerHashCode != mapController.hashCode) { @@ -345,25 +345,25 @@ class _TileLayerState extends State with TickerProviderStateMixin { bool reloadTiles = false; if (!_initializedFromMapCamera || _tileBounds.shouldReplace( - mapCamera.crs, widget.tileSize, widget.tileBounds)) { + camera.crs, widget.tileSize, widget.tileBounds)) { reloadTiles = true; _tileBounds = TileBounds( - crs: mapCamera.crs, + crs: camera.crs, tileSize: widget.tileSize, latLngBounds: widget.tileBounds, ); } if (!_initializedFromMapCamera || - _tileScaleCalculator.shouldReplace(mapCamera.crs, widget.tileSize)) { + _tileScaleCalculator.shouldReplace(camera.crs, widget.tileSize)) { reloadTiles = true; _tileScaleCalculator = TileScaleCalculator( - crs: mapCamera.crs, + crs: camera.crs, tileSize: widget.tileSize, ); } - if (reloadTiles) _loadAndPruneInVisibleBounds(mapCamera); + if (reloadTiles) _loadAndPruneInVisibleBounds(camera); _initializedFromMapCamera = true; } @@ -447,7 +447,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { final tileZoom = _clampToNativeZoom(map.zoom); final tileBoundsAtZoom = _tileBounds.atZoom(tileZoom); final visibleTileRange = _tileRangeCalculator.calculate( - mapCamera: map, + camera: map, tileZoom: tileZoom, ); @@ -522,7 +522,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { void _onTileUpdateEvent(TileUpdateEvent event) { final tileZoom = _clampToNativeZoom(event.zoom); final visibleTileRange = _tileRangeCalculator.calculate( - mapCamera: event.mapCamera, + camera: event.camera, tileZoom: tileZoom, center: event.center, viewingZoom: event.zoom, @@ -540,10 +540,10 @@ class _TileLayerState extends State with TickerProviderStateMixin { } // Load new tiles in the visible bounds and prune those outside. - void _loadAndPruneInVisibleBounds(MapCamera mapCamera) { - final tileZoom = _clampToNativeZoom(mapCamera.zoom); + void _loadAndPruneInVisibleBounds(MapCamera camera) { + final tileZoom = _clampToNativeZoom(camera.zoom); final visibleTileRange = _tileRangeCalculator.calculate( - mapCamera: mapCamera, + camera: camera, tileZoom: tileZoom, ); diff --git a/lib/src/layer/tile_layer/tile_range_calculator.dart b/lib/src/layer/tile_layer/tile_range_calculator.dart index 4abce5c51..6d4922e46 100644 --- a/lib/src/layer/tile_layer/tile_range_calculator.dart +++ b/lib/src/layer/tile_layer/tile_range_calculator.dart @@ -13,37 +13,37 @@ class TileRangeCalculator { /// resulting tile range is expanded by [panBuffer]. DiscreteTileRange calculate({ // The map camera used to calculate the bounds. - required MapCamera mapCamera, + required MapCamera camera, // The zoom level at which the bounds should be calculated. required int tileZoom, - // The center from which the map is viewed, defaults to [mapCamera.center]. + // The center from which the map is viewed, defaults to [camera.center]. LatLng? center, - // The zoom from which the map is viewed, defaults to [mapCamera.zoom]. + // The zoom from which the map is viewed, defaults to [camera.zoom]. double? viewingZoom, }) { return DiscreteTileRange.fromPixelBounds( zoom: tileZoom, tileSize: tileSize, pixelBounds: _calculatePixelBounds( - mapCamera, - center ?? mapCamera.center, - viewingZoom ?? mapCamera.zoom, + camera, + center ?? camera.center, + viewingZoom ?? camera.zoom, tileZoom, ), ); } Bounds _calculatePixelBounds( - MapCamera mapCamera, + MapCamera camera, LatLng center, double viewingZoom, int tileZoom, ) { final tileZoomDouble = tileZoom.toDouble(); - final scale = mapCamera.getZoomScale(viewingZoom, tileZoomDouble); + final scale = camera.getZoomScale(viewingZoom, tileZoomDouble); final pixelCenter = - mapCamera.project(center, tileZoomDouble).floor().toDoublePoint(); - final halfSize = mapCamera.size / (scale * 2); + camera.project(center, tileZoomDouble).floor().toDoublePoint(); + final halfSize = camera.size / (scale * 2); return Bounds(pixelCenter - halfSize, pixelCenter + halfSize); } diff --git a/lib/src/layer/tile_layer/tile_update_event.dart b/lib/src/layer/tile_layer/tile_update_event.dart index 8e25e3eb8..ed9bf36c8 100644 --- a/lib/src/layer/tile_layer/tile_update_event.dart +++ b/lib/src/layer/tile_layer/tile_update_event.dart @@ -19,11 +19,11 @@ class TileUpdateEvent { this.loadZoomOverride, }); - double get zoom => loadZoomOverride ?? mapEvent.mapCamera.zoom; + double get zoom => loadZoomOverride ?? mapEvent.camera.zoom; - LatLng get center => loadCenterOverride ?? mapEvent.mapCamera.center; + LatLng get center => loadCenterOverride ?? mapEvent.camera.center; - MapCamera get mapCamera => mapEvent.mapCamera; + MapCamera get camera => mapEvent.camera; /// Returns a copy of this TileUpdateEvent with only pruning enabled and the /// loadCenterOverride/loadZoomOverride removed. diff --git a/lib/src/map/internal_controller.dart b/lib/src/map/internal_controller.dart index 22a841931..513b1ee90 100644 --- a/lib/src/map/internal_controller.dart +++ b/lib/src/map/internal_controller.dart @@ -15,7 +15,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { : super( _InternalState( options: options, - mapCamera: MapCamera.initialCamera(options), + camera: MapCamera.initialCamera(options), ), ); @@ -27,7 +27,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { _interactiveViewerState = interactiveViewerState; MapOptions get options => value.options; - MapCamera get mapCamera => value.mapCamera; + MapCamera get camera => value.camera; void linkMapController(MapControllerImpl mapControllerImpl) { _mapControllerImpl = mapControllerImpl; @@ -54,9 +54,9 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { }) { // Algorithm thanks to https://github.com/tlserver/flutter_map_location_marker if (offset != Offset.zero) { - final newPoint = mapCamera.project(newCenter, newZoom); - newCenter = mapCamera.unproject( - mapCamera.rotatePoint( + final newPoint = camera.project(newCenter, newZoom); + newCenter = camera.unproject( + camera.rotatePoint( newPoint, newPoint - CustomPoint(offset.dx, offset.dy), ), @@ -64,24 +64,23 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { ); } - MapCamera? newMapCamera = mapCamera.withPosition( + MapCamera? newCamera = camera.withPosition( center: newCenter, - zoom: mapCamera.clampZoom(newZoom), + zoom: camera.clampZoom(newZoom), ); - newMapCamera = options.cameraConstraint.constrain(newMapCamera); - if (newMapCamera == null || - (newMapCamera.center == mapCamera.center && - newMapCamera.zoom == mapCamera.zoom)) { + newCamera = options.cameraConstraint.constrain(newCamera); + if (newCamera == null || + (newCamera.center == camera.center && newCamera.zoom == camera.zoom)) { return false; } - final oldMapCamera = mapCamera; - value = value.withMapCamera(newMapCamera); + final oldCamera = camera; + value = value.withMapCamera(newCamera); final movementEvent = MapEventWithMove.fromSource( - oldMapCamera: oldMapCamera, - mapCamera: mapCamera, + oldCamera: oldCamera, + camera: camera, hasGesture: hasGesture, source: source, id: id, @@ -91,7 +90,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { options.onPositionChanged?.call( MapPosition( center: newCenter, - bounds: mapCamera.visibleBounds, + bounds: camera.visibleBounds, zoom: newZoom, hasGesture: hasGesture, ), @@ -110,23 +109,23 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { required MapEventSource source, required String? id, }) { - if (newRotation != mapCamera.rotation) { - final newMapCamera = options.cameraConstraint.constrain( - mapCamera.withRotation(newRotation), + if (newRotation != camera.rotation) { + final newCamera = options.cameraConstraint.constrain( + camera.withRotation(newRotation), ); - if (newMapCamera == null) return false; + if (newCamera == null) return false; - final oldMapCamera = mapCamera; + final oldCamera = camera; // Update camera then emit events and callbacks - value = value.withMapCamera(newMapCamera); + value = value.withMapCamera(newCamera); _emitMapEvent( MapEventRotate( id: id, source: source, - oldMapCamera: oldMapCamera, - mapCamera: mapCamera, + oldCamera: oldCamera, + camera: camera, ), ); return true; @@ -153,7 +152,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { throw ArgumentError('One of `point` or `offset` must be non-null'); } - if (degree == mapCamera.rotation) { + if (degree == camera.rotation) { return MoveAndRotateResult(false, false); } @@ -169,28 +168,28 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { ); } - final rotationDiff = degree - mapCamera.rotation; - final rotationCenter = mapCamera.project(mapCamera.center) + + final rotationDiff = degree - camera.rotation; + final rotationCenter = camera.project(camera.center) + (point != null - ? (point - (mapCamera.nonRotatedSize / 2.0)) + ? (point - (camera.nonRotatedSize / 2.0)) : CustomPoint(offset!.dx, offset.dy)) - .rotate(mapCamera.rotationRad); + .rotate(camera.rotationRad); return MoveAndRotateResult( move( - mapCamera.unproject( + camera.unproject( rotationCenter + - (mapCamera.project(mapCamera.center) - rotationCenter) + (camera.project(camera.center) - rotationCenter) .rotate(degToRadian(rotationDiff)), ), - mapCamera.zoom, + camera.zoom, offset: Offset.zero, hasGesture: hasGesture, source: source, id: id, ), rotate( - mapCamera.rotation + rotationDiff, + camera.rotation + rotationDiff, hasGesture: hasGesture, source: source, id: id, @@ -229,7 +228,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { CameraFit cameraFit, { required Offset offset, }) { - final fitted = cameraFit.fit(mapCamera); + final fitted = cameraFit.fit(camera); return move( fitted.center, @@ -245,8 +244,8 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { CustomPoint nonRotatedSize, ) { if (nonRotatedSize != MapCamera.kImpossibleSize && - nonRotatedSize != mapCamera.nonRotatedSize) { - value = value.withMapCamera(mapCamera.withNonRotatedSize(nonRotatedSize)); + nonRotatedSize != camera.nonRotatedSize) { + value = value.withMapCamera(camera.withNonRotatedSize(nonRotatedSize)); return true; } @@ -259,10 +258,10 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { 'Should not update options unless they change', ); - final newMapCamera = mapCamera.withOptions(newOptions); + final newCamera = camera.withOptions(newOptions); assert( - newOptions.cameraConstraint.constrain(newMapCamera) == newMapCamera, + newOptions.cameraConstraint.constrain(newCamera) == newCamera, 'MapCamera is no longer within the cameraConstraint after an option change.', ); @@ -275,7 +274,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { value = _InternalState( options: newOptions, - mapCamera: newMapCamera, + camera: newCamera, ); } @@ -283,7 +282,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { void moveStarted(MapEventSource source) { _emitMapEvent( MapEventMoveStart( - mapCamera: mapCamera, + camera: camera, source: source, ), ); @@ -291,14 +290,14 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { // To be called when an ongoing drag movement updates. void dragUpdated(MapEventSource source, Offset offset) { - final oldCenterPt = mapCamera.project(mapCamera.center); + final oldCenterPt = camera.project(camera.center); final newCenterPt = oldCenterPt + offset.toCustomPoint(); - final newCenter = mapCamera.unproject(newCenterPt); + final newCenter = camera.unproject(newCenterPt); move( newCenter, - mapCamera.zoom, + camera.zoom, offset: Offset.zero, hasGesture: true, source: source, @@ -310,7 +309,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { void moveEnded(MapEventSource source) { _emitMapEvent( MapEventMoveEnd( - mapCamera: mapCamera, + camera: camera, source: source, ), ); @@ -320,7 +319,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { void rotateStarted(MapEventSource source) { _emitMapEvent( MapEventRotateStart( - mapCamera: mapCamera, + camera: camera, source: source, ), ); @@ -330,7 +329,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { void rotateEnded(MapEventSource source) { _emitMapEvent( MapEventRotateEnd( - mapCamera: mapCamera, + camera: camera, source: source, ), ); @@ -340,7 +339,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { void flingStarted(MapEventSource source) { _emitMapEvent( MapEventFlingAnimationStart( - mapCamera: mapCamera, + camera: camera, source: MapEventSource.flingAnimationController, ), ); @@ -350,7 +349,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { void flingEnded(MapEventSource source) { _emitMapEvent( MapEventFlingAnimationEnd( - mapCamera: mapCamera, + camera: camera, source: source, ), ); @@ -360,7 +359,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { void flingNotStarted(MapEventSource source) { _emitMapEvent( MapEventFlingAnimationNotStarted( - mapCamera: mapCamera, + camera: camera, source: source, ), ); @@ -370,7 +369,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { void doubleTapZoomStarted(MapEventSource source) { _emitMapEvent( MapEventDoubleTapZoomStart( - mapCamera: mapCamera, + camera: camera, source: source, ), ); @@ -380,7 +379,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { void doubleTapZoomEnded(MapEventSource source) { _emitMapEvent( MapEventDoubleTapZoomEnd( - mapCamera: mapCamera, + camera: camera, source: source, ), ); @@ -395,7 +394,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { _emitMapEvent( MapEventTap( tapPosition: position, - mapCamera: mapCamera, + camera: camera, source: source, ), ); @@ -410,7 +409,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { _emitMapEvent( MapEventSecondaryTap( tapPosition: position, - mapCamera: mapCamera, + camera: camera, source: source, ), ); @@ -425,7 +424,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { _emitMapEvent( MapEventLongPress( tapPosition: position, - mapCamera: mapCamera, + camera: camera, source: MapEventSource.longPress, ), ); @@ -434,14 +433,14 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { // To be called when the map's size constraints change. void nonRotatedSizeChange( MapEventSource source, - MapCamera oldMapCamera, - MapCamera newMapCamera, + MapCamera oldCamera, + MapCamera newCamera, ) { _emitMapEvent( MapEventNonRotatedSizeChange( source: MapEventSource.nonRotatedSizeChange, - oldMapCamera: oldMapCamera, - mapCamera: newMapCamera, + oldCamera: oldCamera, + camera: newCamera, ), ); } @@ -458,16 +457,16 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> { } class _InternalState { - final MapCamera mapCamera; + final MapCamera camera; final MapOptions options; const _InternalState({ required this.options, - required this.mapCamera, + required this.camera, }); - _InternalState withMapCamera(MapCamera mapCamera) => _InternalState( + _InternalState withMapCamera(MapCamera camera) => _InternalState( options: options, - mapCamera: mapCamera, + camera: camera, ); } diff --git a/lib/src/map/map_controller.dart b/lib/src/map/map_controller.dart index 058f927ba..790f91a83 100644 --- a/lib/src/map/map_controller.dart +++ b/lib/src/map/map_controller.dart @@ -140,7 +140,7 @@ abstract class MapController { /// Current [MapCamera]. Accessing the camera from this getter is an /// anti-pattern. It is preferable to use [MapCamera.of(context)] in a child /// widget of FlutterMap. - MapCamera get mapCamera; + MapCamera get camera; /// [Stream] of all emitted [MapEvent]s Stream get mapEventStream; @@ -150,7 +150,7 @@ abstract class MapController { /// /// Does not move/zoom the map: see [fitBounds]. @Deprecated( - 'Use CameraFit.bounds(bounds: bounds).fit(controller.mapCamera) instead.') + 'Use CameraFit.bounds(bounds: bounds).fit(controller.camera) instead.') CenterZoom centerZoomFitBounds( LatLngBounds bounds, { FitBoundsOptions options = @@ -159,15 +159,15 @@ abstract class MapController { /// Convert a screen point (x/y) to its corresponding map coordinate (lat/lng), /// based on the map's current properties - @Deprecated('Use controller.mapCamera.pointToLatLng() instead.') + @Deprecated('Use controller.camera.pointToLatLng() instead.') LatLng pointToLatLng(CustomPoint screenPoint); /// Convert a map coordinate (lat/lng) to its corresponding screen point (x/y), /// based on the map's current screen positioning - @Deprecated('Use controller.mapCamera.latLngToScreenPoint() instead.') + @Deprecated('Use controller.camera.latLngToScreenPoint() instead.') CustomPoint latLngToScreenPoint(LatLng mapCoordinate); - @Deprecated('Use controller.mapCamera.rotatePoint() instead.') + @Deprecated('Use controller.camera.rotatePoint() instead.') CustomPoint rotatePoint( CustomPoint mapCenter, CustomPoint point, { @@ -175,19 +175,19 @@ abstract class MapController { }); /// Current center coordinates - @Deprecated('Use controller.mapCamera.center instead.') + @Deprecated('Use controller.camera.center instead.') LatLng get center; /// Current outer points/boundaries coordinates - @Deprecated('Use controller.mapCamera.visibleBounds instead.') + @Deprecated('Use controller.camera.visibleBounds instead.') LatLngBounds? get bounds; /// Current zoom level - @Deprecated('Use controller.mapCamera.zoom instead.') + @Deprecated('Use controller.camera.zoom instead.') double get zoom; /// Current rotation in degrees, where 0° is North - @Deprecated('Use controller.mapCamera.rotation instead.') + @Deprecated('Use controller.camera.rotation instead.') double get rotation; /// Dispose of this controller. diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/map_controller_impl.dart index dd43bb652..3e415112c 100644 --- a/lib/src/map/map_controller_impl.dart +++ b/lib/src/map/map_controller_impl.dart @@ -24,7 +24,7 @@ class MapControllerImpl implements MapController { } @override - MapCamera get mapCamera => _internalController.mapCamera; + MapCamera get camera => _internalController.camera; @override Stream get mapEventStream => _mapEventStreamController.stream; @@ -121,16 +121,16 @@ class MapControllerImpl implements MapController { } @override - @Deprecated('Use controller.mapCamera.visibleBounds instead.') - LatLngBounds? get bounds => mapCamera.visibleBounds; + @Deprecated('Use controller.camera.visibleBounds instead.') + LatLngBounds? get bounds => camera.visibleBounds; @override - @Deprecated('Use controller.mapCamera.center instead.') - LatLng get center => mapCamera.center; + @Deprecated('Use controller.camera.center instead.') + LatLng get center => camera.center; @override @Deprecated( - 'Use CameraFit.bounds(bounds: bounds).fit(controller.mapCamera) instead.') + 'Use CameraFit.bounds(bounds: bounds).fit(controller.camera) instead.') CenterZoom centerZoomFitBounds( LatLngBounds bounds, { FitBoundsOptions options = @@ -142,7 +142,7 @@ class MapControllerImpl implements MapController { maxZoom: options.maxZoom, inside: options.inside, forceIntegerZoomLevel: options.forceIntegerZoomLevel, - ).fit(mapCamera); + ).fit(camera); return CenterZoom( center: fittedState.center, zoom: fittedState.zoom, @@ -150,33 +150,33 @@ class MapControllerImpl implements MapController { } @override - @Deprecated('Use controller.mapCamera.latLngToScreenPoint() instead.') + @Deprecated('Use controller.camera.latLngToScreenPoint() instead.') CustomPoint latLngToScreenPoint(LatLng mapCoordinate) => - mapCamera.latLngToScreenPoint(mapCoordinate); + camera.latLngToScreenPoint(mapCoordinate); @override - @Deprecated('Use controller.mapCamera.pointToLatLng() instead.') + @Deprecated('Use controller.camera.pointToLatLng() instead.') LatLng pointToLatLng(CustomPoint screenPoint) => - mapCamera.pointToLatLng(screenPoint); + camera.pointToLatLng(screenPoint); @override - @Deprecated('Use controller.mapCamera.rotatePoint() instead.') + @Deprecated('Use controller.camera.rotatePoint() instead.') CustomPoint rotatePoint( CustomPoint mapCenter, CustomPoint point, { bool counterRotation = true, }) => - mapCamera.rotatePoint( + camera.rotatePoint( mapCenter.toDoublePoint(), point.toDoublePoint(), counterRotation: counterRotation, ); @override - @Deprecated('Use controller.mapCamera.rotation instead.') - double get rotation => mapCamera.rotation; + @Deprecated('Use controller.camera.rotation instead.') + double get rotation => camera.rotation; @override - @Deprecated('Use controller.mapCamera.zoom instead.') - double get zoom => mapCamera.zoom; + @Deprecated('Use controller.camera.zoom instead.') + double get zoom => camera.zoom; } diff --git a/lib/src/map/widget.dart b/lib/src/map/widget.dart index 0454828bd..b5189003e 100644 --- a/lib/src/map/widget.dart +++ b/lib/src/map/widget.dart @@ -159,7 +159,7 @@ class FlutterMapStateContainer extends State { constraints.maxWidth, constraints.maxHeight, ); - final oldMapCamera = _flutterMapInternalController.camera; + final oldCamera = _flutterMapInternalController.camera; if (_flutterMapInternalController .setNonRotatedSizeWithoutEmittingEvent(nonRotatedSize)) { final newMapCamera = _flutterMapInternalController.camera; @@ -170,7 +170,7 @@ class FlutterMapStateContainer extends State { if (mounted) { _flutterMapInternalController.nonRotatedSizeChange( MapEventSource.nonRotatedSizeChange, - oldMapCamera, + oldCamera, newMapCamera, ); } diff --git a/lib/src/misc/camera_constraint.dart b/lib/src/misc/camera_constraint.dart index 4db4cef0a..6730de810 100644 --- a/lib/src/misc/camera_constraint.dart +++ b/lib/src/misc/camera_constraint.dart @@ -18,14 +18,14 @@ abstract class CameraConstraint { required LatLngBounds bounds, }) = ContainCamera._; - MapCamera? constrain(MapCamera mapCamera); + MapCamera? constrain(MapCamera camera); } class UnconstrainedCamera extends CameraConstraint { const UnconstrainedCamera._(); @override - MapCamera constrain(MapCamera mapCamera) => mapCamera; + MapCamera constrain(MapCamera camera) => camera; } class ContainCameraCenter extends CameraConstraint { @@ -36,13 +36,13 @@ class ContainCameraCenter extends CameraConstraint { }); @override - MapCamera constrain(MapCamera mapCamera) => mapCamera.withPosition( + MapCamera constrain(MapCamera camera) => camera.withPosition( center: LatLng( - mapCamera.center.latitude.clamp( + camera.center.latitude.clamp( bounds.south, bounds.north, ), - mapCamera.center.longitude.clamp( + camera.center.longitude.clamp( bounds.west, bounds.east, ), @@ -59,14 +59,14 @@ class ContainCamera extends CameraConstraint { }); @override - MapCamera? constrain(MapCamera mapCamera) { - final testZoom = mapCamera.zoom; - final testCenter = mapCamera.center; + MapCamera? constrain(MapCamera camera) { + final testZoom = camera.zoom; + final testCenter = camera.center; - final nePixel = mapCamera.project(bounds.northEast, testZoom); - final swPixel = mapCamera.project(bounds.southWest, testZoom); + final nePixel = camera.project(bounds.northEast, testZoom); + final swPixel = camera.project(bounds.southWest, testZoom); - final halfSize = mapCamera.size / 2; + final halfSize = camera.size / 2; // Find the limits for the map center which would keep the camera within the // [latLngBounds]. @@ -79,16 +79,16 @@ class ContainCamera extends CameraConstraint { // stay within [latLngBounds]. if (leftOkCenter > rightOkCenter || topOkCenter > botOkCenter) return null; - final centerPix = mapCamera.project(testCenter, testZoom); + final centerPix = camera.project(testCenter, testZoom); final newCenterPix = CustomPoint( centerPix.x.clamp(leftOkCenter, rightOkCenter), centerPix.y.clamp(topOkCenter, botOkCenter), ); - if (newCenterPix == centerPix) return mapCamera; + if (newCenterPix == centerPix) return camera; - return mapCamera.withPosition( - center: mapCamera.unproject(newCenterPix, testZoom), + return camera.withPosition( + center: camera.unproject(newCenterPix, testZoom), ); } diff --git a/lib/src/misc/camera_fit.dart b/lib/src/misc/camera_fit.dart index d0b9c50cb..57689e19b 100644 --- a/lib/src/misc/camera_fit.dart +++ b/lib/src/misc/camera_fit.dart @@ -25,7 +25,7 @@ abstract class CameraFit { bool forceIntegerZoomLevel, }) = FitCoordinates; - MapCamera fit(MapCamera mapCamera); + MapCamera fit(MapCamera camera); } class FitBounds extends CameraFit { @@ -48,56 +48,56 @@ class FitBounds extends CameraFit { }); @override - MapCamera fit(MapCamera mapCamera) { + MapCamera fit(MapCamera camera) { final paddingTL = CustomPoint(padding.left, padding.top); final paddingBR = CustomPoint(padding.right, padding.bottom); final paddingTotalXY = paddingTL + paddingBR; - var newZoom = getBoundsZoom(mapCamera, paddingTotalXY); + var newZoom = getBoundsZoom(camera, paddingTotalXY); newZoom = math.min(maxZoom, newZoom); final paddingOffset = (paddingBR - paddingTL) / 2; - final swPoint = mapCamera.project(bounds.southWest, newZoom); - final nePoint = mapCamera.project(bounds.northEast, newZoom); + final swPoint = camera.project(bounds.southWest, newZoom); + final nePoint = camera.project(bounds.northEast, newZoom); final CustomPoint projectedCenter; - if (mapCamera.rotation != 0.0) { - final swPointRotated = swPoint.rotate(-mapCamera.rotationRad); - final nePointRotated = nePoint.rotate(-mapCamera.rotationRad); + if (camera.rotation != 0.0) { + final swPointRotated = swPoint.rotate(-camera.rotationRad); + final nePointRotated = nePoint.rotate(-camera.rotationRad); final centerRotated = (swPointRotated + nePointRotated) / 2 + paddingOffset; - projectedCenter = centerRotated.rotate(mapCamera.rotationRad); + projectedCenter = centerRotated.rotate(camera.rotationRad); } else { projectedCenter = (swPoint + nePoint) / 2 + paddingOffset; } - final center = mapCamera.unproject(projectedCenter, newZoom); - return mapCamera.withPosition( + final center = camera.unproject(projectedCenter, newZoom); + return camera.withPosition( center: center, zoom: newZoom, ); } double getBoundsZoom( - MapCamera mapCamera, + MapCamera camera, CustomPoint pixelPadding, ) { - final min = mapCamera.minZoom ?? 0.0; - final max = mapCamera.maxZoom ?? double.infinity; + final min = camera.minZoom ?? 0.0; + final max = camera.maxZoom ?? double.infinity; final nw = bounds.northWest; final se = bounds.southEast; - var size = mapCamera.nonRotatedSize - pixelPadding; + var size = camera.nonRotatedSize - pixelPadding; // Prevent negative size which results in NaN zoom value later on in the calculation size = CustomPoint(math.max(0, size.x), math.max(0, size.y)); var boundsSize = Bounds( - mapCamera.project(se, mapCamera.zoom), - mapCamera.project(nw, mapCamera.zoom), + camera.project(se, camera.zoom), + camera.project(nw, camera.zoom), ).size; - if (mapCamera.rotation != 0.0) { - final cosAngle = math.cos(mapCamera.rotationRad).abs(); - final sinAngle = math.sin(mapCamera.rotationRad).abs(); + if (camera.rotation != 0.0) { + final cosAngle = math.cos(camera.rotationRad).abs(); + final sinAngle = math.sin(camera.rotationRad).abs(); boundsSize = CustomPoint( (boundsSize.x * cosAngle) + (boundsSize.y * sinAngle), (boundsSize.y * cosAngle) + (boundsSize.x * sinAngle), @@ -108,7 +108,7 @@ class FitBounds extends CameraFit { final scaleY = size.y / boundsSize.y; final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); - var boundsZoom = mapCamera.getScaleZoom(scale, mapCamera.zoom); + var boundsZoom = camera.getScaleZoom(scale, camera.zoom); if (forceIntegerZoomLevel) { boundsZoom = @@ -137,21 +137,21 @@ class FitCoordinates extends CameraFit { }); @override - MapCamera fit(MapCamera mapCamera) { + MapCamera fit(MapCamera camera) { final paddingTL = CustomPoint(padding.left, padding.top); final paddingBR = CustomPoint(padding.right, padding.bottom); final paddingTotalXY = paddingTL + paddingBR; - var newZoom = getCoordinatesZoom(mapCamera, paddingTotalXY); + var newZoom = getCoordinatesZoom(camera, paddingTotalXY); newZoom = math.min(maxZoom, newZoom); final projectedPoints = [ - for (final coord in coordinates) mapCamera.project(coord, newZoom) + for (final coord in coordinates) camera.project(coord, newZoom) ]; final rotatedPoints = - projectedPoints.map((point) => point.rotate(-mapCamera.rotationRad)); + projectedPoints.map((point) => point.rotate(-camera.rotationRad)); final rotatedBounds = Bounds.containing(rotatedPoints); @@ -160,32 +160,32 @@ class FitCoordinates extends CameraFit { final rotatedNewCenter = rotatedBounds.center + paddingOffset; // Undo the rotation - final unrotatedNewCenter = rotatedNewCenter.rotate(mapCamera.rotationRad); + final unrotatedNewCenter = rotatedNewCenter.rotate(camera.rotationRad); - final newCenter = mapCamera.unproject(unrotatedNewCenter, newZoom); + final newCenter = camera.unproject(unrotatedNewCenter, newZoom); - return mapCamera.withPosition( + return camera.withPosition( center: newCenter, zoom: newZoom, ); } double getCoordinatesZoom( - MapCamera mapCamera, + MapCamera camera, CustomPoint pixelPadding, ) { - final min = mapCamera.minZoom ?? 0.0; - final max = mapCamera.maxZoom ?? double.infinity; - var size = mapCamera.nonRotatedSize - pixelPadding; + final min = camera.minZoom ?? 0.0; + final max = camera.maxZoom ?? double.infinity; + var size = camera.nonRotatedSize - pixelPadding; // Prevent negative size which results in NaN zoom value later on in the calculation size = CustomPoint(math.max(0, size.x), math.max(0, size.y)); final projectedPoints = [ - for (final coord in coordinates) mapCamera.project(coord) + for (final coord in coordinates) camera.project(coord) ]; final rotatedPoints = - projectedPoints.map((point) => point.rotate(-mapCamera.rotationRad)); + projectedPoints.map((point) => point.rotate(-camera.rotationRad)); final rotatedBounds = Bounds.containing(rotatedPoints); final boundsSize = rotatedBounds.size; @@ -194,7 +194,7 @@ class FitCoordinates extends CameraFit { final scaleY = size.y / boundsSize.y; final scale = math.min(scaleX, scaleY); - var newZoom = mapCamera.getScaleZoom(scale, mapCamera.zoom); + var newZoom = camera.getScaleZoom(scale, camera.zoom); if (forceIntegerZoomLevel) { newZoom = newZoom.floorToDouble(); } diff --git a/test/flutter_map_controller_test.dart b/test/flutter_map_controller_test.dart index 603bfe2b3..5381e8af8 100644 --- a/test/flutter_map_controller_test.dart +++ b/test/flutter_map_controller_test.dart @@ -26,10 +26,10 @@ void main() { controller.fitCamera(cameraConstraint); await tester.pump(); - final mapCamera = controller.mapCamera; - expect(mapCamera.visibleBounds, equals(expectedBounds)); - expect(mapCamera.center, equals(expectedCenter)); - expect(mapCamera.zoom, equals(expectedZoom)); + final camera = controller.camera; + expect(camera.visibleBounds, equals(expectedBounds)); + expect(camera.center, equals(expectedCenter)); + expect(camera.zoom, equals(expectedZoom)); } { @@ -46,10 +46,10 @@ void main() { controller.fitCamera(cameraConstraint); await tester.pump(); - final mapCamera = controller.mapCamera; - expect(mapCamera.visibleBounds, equals(expectedBounds)); - expect(mapCamera.center, equals(expectedCenter)); - expect(mapCamera.zoom, equals(expectedZoom)); + final camera = controller.camera; + expect(camera.visibleBounds, equals(expectedBounds)); + expect(camera.center, equals(expectedCenter)); + expect(camera.zoom, equals(expectedZoom)); } { @@ -67,10 +67,10 @@ void main() { controller.fitCamera(cameraConstraint); await tester.pump(); - final mapCamera = controller.mapCamera; - expect(mapCamera.visibleBounds, equals(expectedBounds)); - expect(mapCamera.center, equals(expectedCenter)); - expect(mapCamera.zoom, equals(expectedZoom)); + final camera = controller.camera; + expect(camera.visibleBounds, equals(expectedBounds)); + expect(camera.center, equals(expectedCenter)); + expect(camera.zoom, equals(expectedZoom)); } { @@ -88,10 +88,10 @@ void main() { controller.fitCamera(cameraConstraint); await tester.pump(); - final mapCamera = controller.mapCamera; - expect(mapCamera.visibleBounds, equals(expectedBounds)); - expect(mapCamera.center, equals(expectedCenter)); - expect(mapCamera.zoom, equals(expectedZoom)); + final camera = controller.camera; + expect(camera.visibleBounds, equals(expectedBounds)); + expect(camera.center, equals(expectedCenter)); + expect(camera.zoom, equals(expectedZoom)); } }); @@ -116,30 +116,30 @@ void main() { controller.fitCamera(cameraConstraint); await tester.pump(); expect( - controller.mapCamera.visibleBounds.northWest.latitude, + controller.camera.visibleBounds.northWest.latitude, moreOrLessEquals(expectedBounds.northWest.latitude), ); expect( - controller.mapCamera.visibleBounds.northWest.longitude, + controller.camera.visibleBounds.northWest.longitude, moreOrLessEquals(expectedBounds.northWest.longitude), ); expect( - controller.mapCamera.visibleBounds.southEast.latitude, + controller.camera.visibleBounds.southEast.latitude, moreOrLessEquals(expectedBounds.southEast.latitude), ); expect( - controller.mapCamera.visibleBounds.southEast.longitude, + controller.camera.visibleBounds.southEast.longitude, moreOrLessEquals(expectedBounds.southEast.longitude), ); expect( - controller.mapCamera.center.latitude, + controller.camera.center.latitude, moreOrLessEquals(expectedCenter.latitude), ); expect( - controller.mapCamera.center.longitude, + controller.camera.center.longitude, moreOrLessEquals(expectedCenter.longitude), ); - expect(controller.mapCamera.zoom, moreOrLessEquals(expectedZoom)); + expect(controller.camera.zoom, moreOrLessEquals(expectedZoom)); } // Tests with no padding @@ -685,14 +685,14 @@ void main() { controller.fitCamera(fitCoordinates); await tester.pump(); expect( - controller.mapCamera.center.latitude, + controller.camera.center.latitude, moreOrLessEquals(expectedCenter.latitude), ); expect( - controller.mapCamera.center.longitude, + controller.camera.center.longitude, moreOrLessEquals(expectedCenter.longitude), ); - expect(controller.mapCamera.zoom, moreOrLessEquals(expectedZoom)); + expect(controller.camera.zoom, moreOrLessEquals(expectedZoom)); } FitCoordinates fitCoordinates({ diff --git a/test/misc/frame_constraint_test.dart b/test/misc/frame_constraint_test.dart index a14d1f54f..3760d2a17 100644 --- a/test/misc/frame_constraint_test.dart +++ b/test/misc/frame_constraint_test.dart @@ -17,7 +17,7 @@ void main() { ), ); - final mapCamera = MapCamera( + final camera = MapCamera( crs: const Epsg3857(), center: const LatLng(-90, -180), zoom: 1, @@ -25,7 +25,7 @@ void main() { nonRotatedSize: const CustomPoint(200, 300), ); - final clamped = mapConstraint.constrain(mapCamera)!; + final clamped = mapConstraint.constrain(camera)!; expect(clamped.zoom, 1); expect(clamped.center.latitude, closeTo(-48.562, 0.001)); From cbf26b4a71c421b3ef5460f214dbdb1523277e6a Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Tue, 20 Jun 2023 19:01:45 +0200 Subject: [PATCH 34/46] Documentation --- .../flutter_map_interactive_viewer.dart | 2 + lib/src/map/camera.dart | 111 +++++++++++++----- lib/src/map/inherited_model.dart | 6 + lib/src/map/map_controller.dart | 6 + lib/src/map/map_controller_impl.dart | 3 + lib/src/map/options.dart | 4 + lib/src/misc/camera_constraint.dart | 23 +++- lib/src/misc/camera_fit.dart | 47 +++++++- 8 files changed, 165 insertions(+), 37 deletions(-) diff --git a/lib/src/gestures/flutter_map_interactive_viewer.dart b/lib/src/gestures/flutter_map_interactive_viewer.dart index 1b21a9a87..37b24be09 100644 --- a/lib/src/gestures/flutter_map_interactive_viewer.dart +++ b/lib/src/gestures/flutter_map_interactive_viewer.dart @@ -14,6 +14,8 @@ import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; import 'package:latlong2/latlong.dart'; +/// Applies interactions (gestures/scroll/taps etc) to the current [MapCamera] +/// via the internal [controller]. class FlutterMapInteractiveViewer extends StatefulWidget { final Widget Function( BuildContext context, diff --git a/lib/src/map/camera.dart b/lib/src/map/camera.dart index d5ae5312d..2a219b8c6 100644 --- a/lib/src/map/camera.dart +++ b/lib/src/map/camera.dart @@ -9,6 +9,10 @@ import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; +/// Describes the view of a map. This includes the size/zoom/position/crs as +/// well as the minimum/maximum zoom. This class is immutable, changes to the +/// map view may occur via the [MapController] or user interactions which will +/// result in a new [MapCamera] value. class MapCamera { // During Flutter startup the native platform resolution is not immediately // available which can cause constraints to be zero before they are updated @@ -21,11 +25,18 @@ class MapCamera { final double? minZoom; final double? maxZoom; + /// The [LatLng] which corresponds with the center of this camera. final LatLng center; + + /// How far zoomed this camera is. final double zoom; + + /// The rotation, in degrees, of the camera. See [rotationRad] for the same + /// value in radians. final double rotation; - // Original size of the map where rotation isn't calculated + /// The size of the map view ignoring rotation. This will be the size of the + /// FlutterMap widget. final CustomPoint nonRotatedSize; // Lazily calculated fields. @@ -35,9 +46,43 @@ class MapCamera { CustomPoint? _pixelOrigin; double? _rotationRad; + @Deprecated('Use visibleBounds instead.') + LatLngBounds get bounds => visibleBounds; + + /// This is the [LatLngBounds] corresponding to four corners of this camera. + /// This takes rotation in to account. + LatLngBounds get visibleBounds => + _bounds ?? + (_bounds = LatLngBounds( + unproject(pixelBounds.bottomLeft, zoom), + unproject(pixelBounds.topRight, zoom), + )); + + /// The size of bounding box of this camera taking in to account its + /// rotation. When the rotation is zero this will equal [nonRotatedSize], + /// otherwise it will be the size of the rectangle which contains this + /// camera. + CustomPoint get size => + _cameraSize ?? + calculateRotatedSize( + rotation, + nonRotatedSize, + ); + + /// The offset of the top-left corner of the bounding rectangle of this + /// camera. This will not equal the offset of the top-left visible pixel when + /// the map is rotated. + CustomPoint get pixelOrigin => + _pixelOrigin ?? + (_pixelOrigin = (project(center, zoom) - size / 2.0).round()); + + /// The camera of the closest [FlutterMap] ancestor. If this is called from a + /// context with no [FlutterMap] ancestor null, is returned. static MapCamera? maybeOf(BuildContext context) => FlutterMapInheritedModel.maybeCameraOf(context); + /// The camera of the closest [FlutterMap] ancestor. If this is called from a + /// context with no [FlutterMap] ancestor a [StateError] will be thrown. static MapCamera of(BuildContext context) => maybeOf(context) ?? (throw StateError( @@ -69,39 +114,45 @@ class MapCamera { Bounds? pixelBounds, LatLngBounds? bounds, CustomPoint? pixelOrigin, + double? rotationRad, }) : _cameraSize = size ?? calculateRotatedSize(rotation, nonRotatedSize), _pixelBounds = pixelBounds, _bounds = bounds, - _pixelOrigin = pixelOrigin; + _pixelOrigin = pixelOrigin, + _rotationRad = rotationRad; + /// Returns a new instance of [MapCamera] with the given [nonRotatedSize]. MapCamera withNonRotatedSize(CustomPoint nonRotatedSize) { if (nonRotatedSize == this.nonRotatedSize) return this; return MapCamera( crs: crs, - minZoom: minZoom, - maxZoom: maxZoom, center: center, zoom: zoom, rotation: rotation, nonRotatedSize: nonRotatedSize, + minZoom: minZoom, + maxZoom: maxZoom, + rotationRad: _rotationRad, ); } + /// Returns a new instance of [MapCamera] with the given [rotation]. MapCamera withRotation(double rotation) { if (rotation == this.rotation) return this; return MapCamera( crs: crs, - minZoom: minZoom, - maxZoom: maxZoom, center: center, zoom: zoom, - rotation: rotation, nonRotatedSize: nonRotatedSize, + rotation: rotation, + minZoom: minZoom, + maxZoom: maxZoom, ); } + /// Returns a new instance of [MapCamera] with the given [options]. MapCamera withOptions(MapOptions options) { if (options.crs == crs && options.minZoom == minZoom && @@ -118,9 +169,11 @@ class MapCamera { rotation: rotation, nonRotatedSize: nonRotatedSize, size: _cameraSize, + rotationRad: _rotationRad, ); } + /// Returns a new instance of [MapCamera] with the given [center]/[zoom]. MapCamera withPosition({ LatLng? center, double? zoom, @@ -134,29 +187,11 @@ class MapCamera { rotation: rotation, nonRotatedSize: nonRotatedSize, size: _cameraSize, + rotationRad: _rotationRad, ); - @Deprecated('Use visibleBounds instead.') - LatLngBounds get bounds => visibleBounds; - - LatLngBounds get visibleBounds => - _bounds ?? - (_bounds = LatLngBounds( - unproject(pixelBounds.bottomLeft, zoom), - unproject(pixelBounds.topRight, zoom), - )); - - CustomPoint get size => - _cameraSize ?? - calculateRotatedSize( - rotation, - nonRotatedSize, - ); - - CustomPoint get pixelOrigin => - _pixelOrigin ?? - (_pixelOrigin = (project(center, zoom) - size / 2.0).round()); - + /// Calculates the size of a bounding box which surrounds a box of size + /// [nonRotatedSize] which is rotated by [rotation]. static CustomPoint calculateRotatedSize( double rotation, CustomPoint nonRotatedSize, @@ -173,38 +208,52 @@ class MapCamera { return CustomPoint(width, height); } + /// The current rotation value in radians. This is calculated and cached when + /// it is first called. double get rotationRad => _rotationRad ??= degToRadian(rotation); + /// Calculates point value for the given [latLng] using this camera's + /// [crs] and [zoom] (or the provided [zoom]). CustomPoint project(LatLng latlng, [double? zoom]) => crs.latLngToPoint(latlng, zoom ?? this.zoom); + /// Calculates the [LatLng] for the given [point] using this camera's + /// [crs] and [zoom] (or the provided [zoom]). LatLng unproject(CustomPoint point, [double? zoom]) => crs.pointToLatLng(point, zoom ?? this.zoom); LatLng layerPointToLatLng(CustomPoint point) => unproject(point); + /// Calculates the scale for a zoom from [fromZoom] to [toZoom] using this + /// camera\s [crs]. double getZoomScale(double toZoom, double fromZoom) => crs.scale(toZoom) / crs.scale(fromZoom); - double getScaleZoom(double scale, double? fromZoom) => - crs.zoom(scale * crs.scale(fromZoom ?? zoom)); + /// Calculates the scale for this camera's [zoom]. + double getScaleZoom(double scale) => crs.zoom(scale * crs.scale(zoom)); + /// Calculates the pixel bounds of this camera's [crs]. Bounds? getPixelWorldBounds(double? zoom) => crs.getProjectedBounds(zoom ?? this.zoom); + /// Calculates the [Offset] from the [pos] to this camera's [pixelOrigin]. Offset getOffsetFromOrigin(LatLng pos) { final delta = project(pos) - pixelOrigin; return Offset(delta.x, delta.y); } + /// Calculates the pixel origin of this [MapCamera] at the given + /// [center]/[zoom]. CustomPoint getNewPixelOrigin(LatLng center, [double? zoom]) { final halfSize = size / 2.0; return (project(center, zoom) - halfSize).round(); } + /// Calculates the pixel bounds of this [MapCamera]. This value is cached. Bounds get pixelBounds => _pixelBounds ?? (_pixelBounds = pixelBoundsAtZoom(zoom)); + /// Calculates the pixel bounds of this [MapCamera] at the given [zoom]. Bounds pixelBoundsAtZoom(double zoom) { CustomPoint halfSize = size / 2; if (zoom != this.zoom) { @@ -269,6 +318,8 @@ class MapCamera { return CustomPoint(tp.dx, tp.dy); } + /// Clamps the provided [zoom] to the range specified by [minZoom] and + /// [maxZoom], if set. double clampZoom(double zoom) => zoom.clamp( minZoom ?? double.negativeInfinity, maxZoom ?? double.infinity, diff --git a/lib/src/map/inherited_model.dart b/lib/src/map/inherited_model.dart index f956fef3b..a59ae6986 100644 --- a/lib/src/map/inherited_model.dart +++ b/lib/src/map/inherited_model.dart @@ -3,6 +3,12 @@ import 'package:flutter_map/src/map/camera.dart'; import 'package:flutter_map/src/map/map_controller.dart'; import 'package:flutter_map/src/map/options.dart'; +/// Allows descendents of [FlutterMap] to access the [MapCamera], [MapOptions] +/// and [MapController]. Those classes provide of/maybeOf methods for users to +/// use, those methods call the relevant methods provided by this class. +/// +/// Using an [InheritedModel] means dependent widgets will only rebuild when +/// the aspect they reference is updated. class FlutterMapInheritedModel extends InheritedModel<_FlutterMapAspect> { final FlutterMapData data; diff --git a/lib/src/map/map_controller.dart b/lib/src/map/map_controller.dart index 790f91a83..e59612f0f 100644 --- a/lib/src/map/map_controller.dart +++ b/lib/src/map/map_controller.dart @@ -23,9 +23,15 @@ abstract class MapController { /// Factory constructor redirects to underlying implementation's constructor. factory MapController() => MapControllerImpl(); + /// The controller for the closest [FlutterMap] ancestor. If this is called + /// from a context with no [FlutterMap] ancestor a [StateError] will be + /// thrown. static MapController? maybeOf(BuildContext context) => FlutterMapInheritedModel.maybeControllerOf(context); + /// The controller for the closest [FlutterMap] ancestor. If this is called + /// from a context with no [FlutterMap] ancestor a [StateError] will be + /// thrown. static MapController of(BuildContext context) => maybeOf(context) ?? (throw StateError( diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/map_controller_impl.dart index 3e415112c..fd1748b9d 100644 --- a/lib/src/map/map_controller_impl.dart +++ b/lib/src/map/map_controller_impl.dart @@ -13,6 +13,9 @@ import 'package:flutter_map/src/misc/move_and_rotate_result.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:latlong2/latlong.dart'; +/// Implements [MapController] whilst exposing methods for internal use which +/// should not be visible to the user (e.g. for setting the current camera or +/// linking the internal controller). class MapControllerImpl implements MapController { late FlutterMapInternalController _internalController; final _mapEventStreamController = StreamController.broadcast(); diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index a20439c35..753e5f349 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -169,9 +169,13 @@ class MapOptions { initialZoom = zoom ?? initialZoom, initialRotation = rotation ?? initialRotation; + /// The options of the closest [FlutterMap] ancestor. If this is called from a + /// context with no [FlutterMap] ancestor, null is returned. static MapOptions? maybeOf(BuildContext context) => FlutterMapInheritedModel.maybeOptionsOf(context); + /// The optoins of the closest [FlutterMap] ancestor. If this is called from a + /// context with no [FlutterMap] ancestor a [StateError] will be thrown. static MapOptions of(BuildContext context) => maybeOf(context) ?? (throw StateError( diff --git a/lib/src/misc/camera_constraint.dart b/lib/src/misc/camera_constraint.dart index 6730de810..66d793cce 100644 --- a/lib/src/misc/camera_constraint.dart +++ b/lib/src/misc/camera_constraint.dart @@ -5,15 +5,24 @@ import 'package:flutter_map/src/map/camera.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:latlong2/latlong.dart'; +/// Describes a limit for the map's camera. This separate from constraints that +/// may be imposed by the chosen CRS. abstract class CameraConstraint { const CameraConstraint(); + /// Does not apply any constraint to the map movement. Note that the CRS + /// system of the map may still constrain the camera. const factory CameraConstraint.unconstrained() = UnconstrainedCamera._; + /// Limits the camera such that the center point of the camera remains within + /// [bounds]. This does not prevent points outside of [bounds] from being + /// shown, to achieve that you must use [ContainCamera]. const factory CameraConstraint.containCenter({ required LatLngBounds bounds, }) = ContainCameraCenter._; + /// Limits the camera such that no point outside of the [bounds] will become + /// visible. const factory CameraConstraint.contain({ required LatLngBounds bounds, }) = ContainCamera._; @@ -21,6 +30,8 @@ abstract class CameraConstraint { MapCamera? constrain(MapCamera camera); } +/// Allows the map to be moved without constraints. The CRS system of the map +/// may still resrict map movement to prevent invalid positions. class UnconstrainedCamera extends CameraConstraint { const UnconstrainedCamera._(); @@ -28,6 +39,9 @@ class UnconstrainedCamera extends CameraConstraint { MapCamera constrain(MapCamera camera) => camera; } +/// Limits the camera such that the center point of the camera remains within +/// [bounds]. This does not prevent points outside of [bounds] from being +/// shown, to achieve that you must use [ContainCamera]. class ContainCameraCenter extends CameraConstraint { final LatLngBounds bounds; @@ -35,6 +49,8 @@ class ContainCameraCenter extends CameraConstraint { required this.bounds, }); + /// Returns a new [MapCamera] with the center point contained within + /// [bounds]. @override MapCamera constrain(MapCamera camera) => camera.withPosition( center: LatLng( @@ -50,14 +66,19 @@ class ContainCameraCenter extends CameraConstraint { ); } +/// Limits the camera such that no point outside of the [bounds] will become +/// visible. class ContainCamera extends CameraConstraint { final LatLngBounds bounds; - /// Keeps the center of the camera within [bounds]. const ContainCamera._({ required this.bounds, }); + /// Tries to determine a movement such that the [camera] only contains points + /// within [bounds]. If no movement is necessary the provided [camera] is + /// returned. If remaining within the [bounds] solely via movement is not + /// possible, because the camera is zoomed too far out, null is returned. @override MapCamera? constrain(MapCamera camera) { final testZoom = camera.zoom; diff --git a/lib/src/misc/camera_fit.dart b/lib/src/misc/camera_fit.dart index 57689e19b..a802c8512 100644 --- a/lib/src/misc/camera_fit.dart +++ b/lib/src/misc/camera_fit.dart @@ -7,9 +7,19 @@ import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; +/// Determines a suitable map camera given the current camera and a set of +/// constraints. See [CameraFit.bounds] if you wish to fit a given set of +/// bounds, or [CameraFit.coordinates] if you wish to fit a set of coordinates. abstract class CameraFit { const CameraFit(); + /// A configuration for fitting a [MapCamera] to the given [bounds]. The + /// [padding] may be used to leave extra space around the [bounds]. To + /// limit the zoom of the resulting [MapCamera] use [maxZoom] (default 17.0). + /// If [inside] is true then fit will be within the [bounds], otherwise it + /// will contain the [bounds] (default to false, contain). Finally + /// [forceIntegerZoomLevel] forces the resulting zoom to be rounded to the + /// nearest whole number. const factory CameraFit.bounds({ required LatLngBounds bounds, EdgeInsets padding, @@ -18,6 +28,14 @@ abstract class CameraFit { bool forceIntegerZoomLevel, }) = FitBounds; + /// A configuration for fitting a [MapCamera] to the given [coordinates] such + /// that all of the [coordinates] are contained in the resulting [MapCamera]. + /// The [padding] may be used to leave extra space around the [bounds]. To + /// limit the zoom of the resulting [MapCamera] use [maxZoom] (default 17.0). + /// If [inside] is true then fit will be within the [bounds], otherwise it + /// will contain the [bounds] (default to false, contain). Finally + /// [forceIntegerZoomLevel] forces the resulting zoom to be rounded to the + /// nearest whole number. const factory CameraFit.coordinates({ required List coordinates, EdgeInsets padding, @@ -28,6 +46,13 @@ abstract class CameraFit { MapCamera fit(MapCamera camera); } +/// A configuration for fitting a [MapCamera] to the given [bounds]. The +/// [padding] may be used to leave extra space around the [bounds]. To +/// limit the zoom of the resulting [MapCamera] use [maxZoom] (default 17.0). +/// If [inside] is true then fit will be within the [bounds], otherwise it +/// will contain the [bounds] (default to false, contain). Finally +/// [forceIntegerZoomLevel] forces the resulting zoom to be rounded to the +/// nearest whole number. class FitBounds extends CameraFit { final LatLngBounds bounds; final EdgeInsets padding; @@ -47,6 +72,7 @@ class FitBounds extends CameraFit { this.forceIntegerZoomLevel = false, }); + /// Returns a new [MapCamera] which fits this classes configuration. @override MapCamera fit(MapCamera camera) { final paddingTL = CustomPoint(padding.left, padding.top); @@ -54,7 +80,7 @@ class FitBounds extends CameraFit { final paddingTotalXY = paddingTL + paddingBR; - var newZoom = getBoundsZoom(camera, paddingTotalXY); + var newZoom = _getBoundsZoom(camera, paddingTotalXY); newZoom = math.min(maxZoom, newZoom); final paddingOffset = (paddingBR - paddingTL) / 2; @@ -80,7 +106,7 @@ class FitBounds extends CameraFit { ); } - double getBoundsZoom( + double _getBoundsZoom( MapCamera camera, CustomPoint pixelPadding, ) { @@ -108,7 +134,7 @@ class FitBounds extends CameraFit { final scaleY = size.y / boundsSize.y; final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); - var boundsZoom = camera.getScaleZoom(scale, camera.zoom); + var boundsZoom = camera.getScaleZoom(scale); if (forceIntegerZoomLevel) { boundsZoom = @@ -119,6 +145,14 @@ class FitBounds extends CameraFit { } } +/// A configuration for fitting a [MapCamera] to the given [coordinates] such +/// that all of the [coordinates] are contained in the resulting [MapCamera]. +/// The [padding] may be used to leave extra space around the [bounds]. To +/// limit the zoom of the resulting [MapCamera] use [maxZoom] (default 17.0). +/// If [inside] is true then fit will be within the [bounds], otherwise it +/// will contain the [bounds] (default to false, contain). Finally +/// [forceIntegerZoomLevel] forces the resulting zoom to be rounded to the +/// nearest whole number. class FitCoordinates extends CameraFit { final List coordinates; final EdgeInsets padding; @@ -136,6 +170,7 @@ class FitCoordinates extends CameraFit { this.forceIntegerZoomLevel = false, }); + /// Returns a new [MapCamera] which fits this classes configuration. @override MapCamera fit(MapCamera camera) { final paddingTL = CustomPoint(padding.left, padding.top); @@ -143,7 +178,7 @@ class FitCoordinates extends CameraFit { final paddingTotalXY = paddingTL + paddingBR; - var newZoom = getCoordinatesZoom(camera, paddingTotalXY); + var newZoom = _getCoordinatesZoom(camera, paddingTotalXY); newZoom = math.min(maxZoom, newZoom); final projectedPoints = [ @@ -170,7 +205,7 @@ class FitCoordinates extends CameraFit { ); } - double getCoordinatesZoom( + double _getCoordinatesZoom( MapCamera camera, CustomPoint pixelPadding, ) { @@ -194,7 +229,7 @@ class FitCoordinates extends CameraFit { final scaleY = size.y / boundsSize.y; final scale = math.min(scaleX, scaleY); - var newZoom = camera.getScaleZoom(scale, camera.zoom); + var newZoom = camera.getScaleZoom(scale); if (forceIntegerZoomLevel) { newZoom = newZoom.floorToDouble(); } From 354e6d348184bd40c978e21c2b1b996fb93c9415 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Tue, 20 Jun 2023 22:43:30 +0200 Subject: [PATCH 35/46] Use standard deprecations format In passing re-ordered the methods in MapControllerImpl to match MapController. --- lib/src/map/camera.dart | 6 +- lib/src/map/map_controller.dart | 79 +++++++++++++------ lib/src/map/map_controller_impl.dart | 109 +++++++++++++++++---------- lib/src/map/options.dart | 100 ++++++++++++++++++++---- lib/src/misc/fit_bounds_options.dart | 6 +- 5 files changed, 221 insertions(+), 79 deletions(-) diff --git a/lib/src/map/camera.dart b/lib/src/map/camera.dart index 2a219b8c6..87766f4f2 100644 --- a/lib/src/map/camera.dart +++ b/lib/src/map/camera.dart @@ -46,7 +46,11 @@ class MapCamera { CustomPoint? _pixelOrigin; double? _rotationRad; - @Deprecated('Use visibleBounds instead.') + @Deprecated( + 'Prefer `visibleBounds`. ' + 'This getter has been changed to clarify its meaning. ' + 'This getter is deprecated since v6.', + ) LatLngBounds get bounds => visibleBounds; /// This is the [LatLngBounds] corresponding to four corners of this camera. diff --git a/lib/src/map/map_controller.dart b/lib/src/map/map_controller.dart index e59612f0f..1f203255e 100644 --- a/lib/src/map/map_controller.dart +++ b/lib/src/map/map_controller.dart @@ -37,6 +37,9 @@ abstract class MapController { (throw StateError( '`MapController.of()` should not be called outside a `FlutterMap` and its children')); + /// [Stream] of all emitted [MapEvent]s + Stream get mapEventStream; + /// Moves and zooms the map to a [center] and [zoom] level /// /// [offset] allows a screen-based offset (in normal logical pixels) to be @@ -125,18 +128,6 @@ abstract class MapController { String? id, }); - /// Move and zoom the map to perfectly fit [bounds], with additional - /// configurable [options] - /// - /// For information about return value meaning and emitted events, see [move]'s - /// documentation. - @Deprecated('Use fitCamera with a MapFit.bounds() instead') - bool fitBounds( - LatLngBounds bounds, { - FitBoundsOptions options = - const FitBoundsOptions(padding: EdgeInsets.all(12)), - }); - /// Move and zoom the map to fit [cameraFit]. /// /// For information about the return value and emitted events, see [move]'s @@ -148,15 +139,31 @@ abstract class MapController { /// widget of FlutterMap. MapCamera get camera; - /// [Stream] of all emitted [MapEvent]s - Stream get mapEventStream; + /// Move and zoom the map to perfectly fit [bounds], with additional + /// configurable [options] + /// + /// For information about return value meaning and emitted events, see [move]'s + /// documentation. + @Deprecated( + 'Prefer `fitCamera` with a CameraFit.bounds() instead. ' + 'This method has been changed to use the new `CameraFit` classes which allows different kinds of fit. ' + 'This method is deprecated since v6.', + ) + bool fitBounds( + LatLngBounds bounds, { + FitBoundsOptions options = + const FitBoundsOptions(padding: EdgeInsets.all(12)), + }); /// Calculates the appropriate center and zoom level for the map to perfectly /// fit [bounds], with additional configurable [options] /// /// Does not move/zoom the map: see [fitBounds]. @Deprecated( - 'Use CameraFit.bounds(bounds: bounds).fit(controller.camera) instead.') + 'Prefer `CameraFit.bounds(bounds: bounds).fit(controller.camera)`. ' + 'This method is replaced by applying a CameraFit to the MapCamera. ' + 'This method is deprecated since v6.', + ) CenterZoom centerZoomFitBounds( LatLngBounds bounds, { FitBoundsOptions options = @@ -165,15 +172,27 @@ abstract class MapController { /// Convert a screen point (x/y) to its corresponding map coordinate (lat/lng), /// based on the map's current properties - @Deprecated('Use controller.camera.pointToLatLng() instead.') + @Deprecated( + 'Prefer `controller.camera.pointToLatLng()`. ' + 'This method is now accessible via the camera. ' + 'This method is deprecated since v6.', + ) LatLng pointToLatLng(CustomPoint screenPoint); /// Convert a map coordinate (lat/lng) to its corresponding screen point (x/y), /// based on the map's current screen positioning - @Deprecated('Use controller.camera.latLngToScreenPoint() instead.') + @Deprecated( + 'Prefer `controller.camera.latLngToScreenPoint()`. ' + 'This method is now accessible via the camera. ' + 'This method is deprecated since v6.', + ) CustomPoint latLngToScreenPoint(LatLng mapCoordinate); - @Deprecated('Use controller.camera.rotatePoint() instead.') + @Deprecated( + 'Prefer `controller.camera.rotatePoint()`. ' + 'This method is now accessible via the camera. ' + 'This method is deprecated since v6.', + ) CustomPoint rotatePoint( CustomPoint mapCenter, CustomPoint point, { @@ -181,19 +200,35 @@ abstract class MapController { }); /// Current center coordinates - @Deprecated('Use controller.camera.center instead.') + @Deprecated( + 'Prefer `controller.camera.center`. ' + 'This getter is now accessible via the camera. ' + 'This getter is deprecated since v6.', + ) LatLng get center; /// Current outer points/boundaries coordinates - @Deprecated('Use controller.camera.visibleBounds instead.') + @Deprecated( + 'Prefer `controller.camera.visibleBounds`. ' + 'This getter is now accessible via the camera. ' + 'This getter is deprecated since v6.', + ) LatLngBounds? get bounds; /// Current zoom level - @Deprecated('Use controller.camera.zoom instead.') + @Deprecated( + 'Prefer `controller.camera.zoom`. ' + 'This getter is now accessible via the camera. ' + 'This getter is deprecated since v6.', + ) double get zoom; /// Current rotation in degrees, where 0° is North - @Deprecated('Use controller.camera.rotation instead.') + @Deprecated( + 'Prefer `controller.camera.rotation`. ' + 'This getter is now accessible via the camera. ' + 'This getter is deprecated since v6.', + ) double get rotation; /// Dispose of this controller. diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/map_controller_impl.dart index fd1748b9d..a04821930 100644 --- a/lib/src/map/map_controller_impl.dart +++ b/lib/src/map/map_controller_impl.dart @@ -26,14 +26,11 @@ class MapControllerImpl implements MapController { _internalController = internalController; } - @override - MapCamera get camera => _internalController.camera; + StreamSink get mapEventSink => _mapEventStreamController.sink; @override Stream get mapEventStream => _mapEventStreamController.stream; - StreamSink get mapEventSink => _mapEventStreamController.sink; - @override bool move( LatLng center, @@ -91,13 +88,21 @@ class MapControllerImpl implements MapController { id: id, ); - /// Move and zoom the map to perfectly fit [bounds], with additional - /// configurable [options] - /// - /// For information about return value meaning and emitted events, see [move]'s - /// documentation. @override - @Deprecated('Use fitCamera with a MapFit.bounds() instead') + bool fitCamera(CameraFit cameraFit) => _internalController.fitCamera( + cameraFit, + offset: Offset.zero, + ); + + @override + MapCamera get camera => _internalController.camera; + + @override + @Deprecated( + 'Prefer `fitCamera` with a CameraFit.bounds() instead. ' + 'This method has been changed to use the new `CameraFit` classes which allows different kinds of fit. ' + 'This method is deprecated since v6.', + ) bool fitBounds( LatLngBounds bounds, { FitBoundsOptions options = @@ -113,27 +118,12 @@ class MapControllerImpl implements MapController { ), ); - @override - bool fitCamera(CameraFit cameraFit) => _internalController.fitCamera( - cameraFit, - offset: Offset.zero, - ); - @override - void dispose() { - _mapEventStreamController.close(); - } - - @override - @Deprecated('Use controller.camera.visibleBounds instead.') - LatLngBounds? get bounds => camera.visibleBounds; - - @override - @Deprecated('Use controller.camera.center instead.') - LatLng get center => camera.center; - @override @Deprecated( - 'Use CameraFit.bounds(bounds: bounds).fit(controller.camera) instead.') + 'Prefer `CameraFit.bounds(bounds: bounds).fit(controller.camera)`. ' + 'This method is replaced by applying a CameraFit to the MapCamera. ' + 'This method is deprecated since v6.', + ) CenterZoom centerZoomFitBounds( LatLngBounds bounds, { FitBoundsOptions options = @@ -153,17 +143,29 @@ class MapControllerImpl implements MapController { } @override - @Deprecated('Use controller.camera.latLngToScreenPoint() instead.') - CustomPoint latLngToScreenPoint(LatLng mapCoordinate) => - camera.latLngToScreenPoint(mapCoordinate); - - @override - @Deprecated('Use controller.camera.pointToLatLng() instead.') + @Deprecated( + 'Prefer `controller.camera.pointToLatLng()`. ' + 'This method is now accessible via the camera. ' + 'This method is deprecated since v6.', + ) LatLng pointToLatLng(CustomPoint screenPoint) => camera.pointToLatLng(screenPoint); @override - @Deprecated('Use controller.camera.rotatePoint() instead.') + @Deprecated( + 'Prefer `controller.camera.latLngToScreenPoint()`. ' + 'This method is now accessible via the camera. ' + 'This method is deprecated since v6.', + ) + CustomPoint latLngToScreenPoint(LatLng mapCoordinate) => + camera.latLngToScreenPoint(mapCoordinate); + + @override + @Deprecated( + 'Prefer `controller.camera.rotatePoint()`. ' + 'This method is now accessible via the camera. ' + 'This method is deprecated since v6.', + ) CustomPoint rotatePoint( CustomPoint mapCenter, CustomPoint point, { @@ -176,10 +178,39 @@ class MapControllerImpl implements MapController { ); @override - @Deprecated('Use controller.camera.rotation instead.') - double get rotation => camera.rotation; + @Deprecated( + 'Prefer `controller.camera.center`. ' + 'This getter is now accessible via the camera. ' + 'This getter is deprecated since v6.', + ) + LatLng get center => camera.center; @override - @Deprecated('Use controller.camera.zoom instead.') + @Deprecated( + 'Prefer `controller.camera.visibleBounds`. ' + 'This getter is now accessible via the camera. ' + 'This getter is deprecated since v6.', + ) + LatLngBounds? get bounds => camera.visibleBounds; + + @override + @Deprecated( + 'Prefer `controller.camera.zoom`. ' + 'This getter is now accessible via the camera. ' + 'This getter is deprecated since v6.', + ) double get zoom => camera.zoom; + + @override + @Deprecated( + 'Prefer `controller.camera.rotation`. ' + 'This getter is now accessible via the camera. ' + 'This getter is deprecated since v6.', + ) + double get rotation => camera.rotation; + + @override + void dispose() { + _mapEventStreamController.close(); + } } diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index 753e5f349..3f9da2a3e 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -106,39 +106,107 @@ class MapOptions { const MapOptions({ this.crs = const Epsg3857(), - @Deprecated('Use initialCenter instead') LatLng? center, + @Deprecated( + 'Prefer `initialCenter` instead. ' + 'This option has been renamed to clarify its meaning. ' + 'This option is deprecated since v6.', + ) + LatLng? center, LatLng initialCenter = const LatLng(50.5, 30.51), - @Deprecated('Use initialZoom instead') double? zoom, + @Deprecated( + 'Prefer `initialZoom` instead. ' + 'This option has been renamed to clarify its meaning. ' + 'This option is deprecated since v6.', + ) + double? zoom, double initialZoom = 13.0, - @Deprecated('Use initialRotation instead') double? rotation, + @Deprecated( + 'Prefer `initialRotation` instead. ' + 'This option has been renamed to clarify its meaning. ' + 'This option is deprecated since v6.', + ) + double? rotation, double initialRotation = 0.0, - @Deprecated('Use initialCameraFit instead') this.bounds, - @Deprecated('Use initialCameraFit instead') + @Deprecated( + 'Prefer `initialCameraFit` instead. ' + 'This option is now part of `initalCameraFit`. ' + 'This option is deprecated since v6.', + ) + this.bounds, + @Deprecated( + 'Prefer `initialCameraFit` instead. ' + 'This option is now part of `initalCameraFit`. ' + 'This option is deprecated since v6.', + ) this.boundsOptions = const FitBoundsOptions(), this.initialCameraFit, this.cameraConstraint = const CameraConstraint.unconstrained(), InteractionOptions? interactionOptions, - @Deprecated('Should be set in interactionOptions instead') + @Deprecated( + 'Prefer setting this in `interactionOptions`. ' + 'This option is now part of `interactionOptions` to group all interaction related options. ' + 'This option is deprecated since v6.', + ) int? interactiveFlags, - @Deprecated('Should be set in interactionOptions instead') + @Deprecated( + 'Prefer setting this in `interactionOptions`. ' + 'This option is now part of `interactionOptions` to group all interaction related options. ' + 'This option is deprecated since v6.', + ) bool? debugMultiFingerGestureWinner, - @Deprecated('Should be set in interactionOptions instead') + @Deprecated( + 'Prefer setting this in `interactionOptions`. ' + 'This option is now part of `interactionOptions` to group all interaction related options. ' + 'This option is deprecated since v6.', + ) bool? enableMultiFingerGestureRace, - @Deprecated('Should be set in interactionOptions instead') + @Deprecated( + 'Prefer setting this in `interactionOptions`. ' + 'This option is now part of `interactionOptions` to group all interaction related options. ' + 'This option is deprecated since v6.', + ) double? rotationThreshold, - @Deprecated('Should be set in interactionOptions instead') + @Deprecated( + 'Prefer setting this in `interactionOptions`. ' + 'This option is now part of `interactionOptions` to group all interaction related options. ' + 'This option is deprecated since v6.', + ) int? rotationWinGestures, - @Deprecated('Should be set in interactionOptions instead') + @Deprecated( + 'Prefer setting this in `interactionOptions`. ' + 'This option is now part of `interactionOptions` to group all interaction related options. ' + 'This option is deprecated since v6.', + ) double? pinchZoomThreshold, - @Deprecated('Should be set in interactionOptions instead') + @Deprecated( + 'Prefer setting this in `interactionOptions`. ' + 'This option is now part of `interactionOptions` to group all interaction related options. ' + 'This option is deprecated since v6.', + ) int? pinchZoomWinGestures, - @Deprecated('Should be set in interactionOptions instead') + @Deprecated( + 'Prefer setting this in `interactionOptions`. ' + 'This option is now part of `interactionOptions` to group all interaction related options. ' + 'This option is deprecated since v6.', + ) double? pinchMoveThreshold, - @Deprecated('Should be set in interactionOptions instead') + @Deprecated( + 'Prefer setting this in `interactionOptions`. ' + 'This option is now part of `interactionOptions` to group all interaction related options. ' + 'This option is deprecated since v6.', + ) int? pinchMoveWinGestures, - @Deprecated('Should be set in interactionOptions instead') + @Deprecated( + 'Prefer setting this in `interactionOptions`. ' + 'This option is now part of `interactionOptions` to group all interaction related options. ' + 'This option is deprecated since v6.', + ) bool? enableScrollWheel, - @Deprecated('Should be set in interactionOptions instead') + @Deprecated( + 'Prefer setting this in `interactionOptions`. ' + 'This option is now part of `interactionOptions` to group all interaction related options. ' + 'This option is deprecated since v6.', + ) double? scrollWheelVelocity, this.minZoom, this.maxZoom, diff --git a/lib/src/misc/fit_bounds_options.dart b/lib/src/misc/fit_bounds_options.dart index 4442c9a10..f8bd31629 100644 --- a/lib/src/misc/fit_bounds_options.dart +++ b/lib/src/misc/fit_bounds_options.dart @@ -10,7 +10,11 @@ class FitBoundsOptions { /// to the next suitable integer. final bool forceIntegerZoomLevel; - @Deprecated('Use FitCamera.bounds instead.') + @Deprecated( + 'Prefer `FitCamera.bounds` instead. ' + 'This class has been renamed to clarify its meaning and is now a sublass of CameraFit to allow other fit types. ' + 'This class is deprecated since v6.', + ) const FitBoundsOptions({ this.padding = EdgeInsets.zero, this.maxZoom = 17.0, From dabf734fe7442396762894967fb9409fe7dbfd0e Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 21 Jun 2023 23:10:57 +0100 Subject: [PATCH 36/46] Re-organized camera related source files Improved some documentation (part 1) Prevent public exposure of `FitCoordinates` and `FitBounds` constructors --- example/lib/pages/map_controller.dart | 2 +- lib/flutter_map.dart | 4 +- lib/plugin_api.dart | 2 +- .../flutter_map_interactive_viewer.dart | 2 +- lib/src/gestures/map_events.dart | 2 +- lib/src/layer/circle_layer.dart | 2 +- lib/src/layer/marker_layer.dart | 2 +- lib/src/layer/overlay_image_layer.dart | 2 +- lib/src/layer/polygon_layer.dart | 2 +- lib/src/layer/polyline_layer.dart | 2 +- .../tile_layer/tile_range_calculator.dart | 2 +- .../layer/tile_layer/tile_update_event.dart | 2 +- lib/src/map/{ => camera}/camera.dart | 0 .../camera}/camera_constraint.dart | 83 ++++++++----- lib/src/{misc => map/camera}/camera_fit.dart | 114 ++++++++++-------- lib/src/map/inherited_model.dart | 12 +- lib/src/map/map_controller.dart | 4 +- lib/src/map/map_controller_impl.dart | 4 +- lib/src/map/options.dart | 4 +- lib/src/map/widget.dart | 2 +- test/flutter_map_controller_test.dart | 4 +- test/misc/frame_constraint_test.dart | 4 +- 22 files changed, 147 insertions(+), 110 deletions(-) rename lib/src/map/{ => camera}/camera.dart (100%) rename lib/src/{misc => map/camera}/camera_constraint.dart (56%) rename lib/src/{misc => map/camera}/camera_fit.dart (77%) diff --git a/example/lib/pages/map_controller.dart b/example/lib/pages/map_controller.dart index 24dc7755b..6d173a4c7 100644 --- a/example/lib/pages/map_controller.dart +++ b/example/lib/pages/map_controller.dart @@ -105,7 +105,7 @@ class MapControllerPageState extends State { ]); _mapController.fitCamera( - FitBounds( + CameraFit.bounds( bounds: bounds, padding: const EdgeInsets.symmetric(horizontal: 15), ), diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index 5aaf7fdcd..912c7b7c8 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -29,8 +29,8 @@ export 'package:flutter_map/src/layer/tile_layer/tile_update_transformer.dart'; export 'package:flutter_map/src/map/map_controller.dart'; export 'package:flutter_map/src/map/options.dart'; export 'package:flutter_map/src/map/widget.dart'; -export 'package:flutter_map/src/misc/camera_constraint.dart'; -export 'package:flutter_map/src/misc/camera_fit.dart'; +export 'package:flutter_map/src/map/camera/camera_constraint.dart'; +export 'package:flutter_map/src/map/camera/camera_fit.dart'; export 'package:flutter_map/src/misc/center_zoom.dart'; export 'package:flutter_map/src/misc/fit_bounds_options.dart'; export 'package:flutter_map/src/misc/move_and_rotate_result.dart'; diff --git a/lib/plugin_api.dart b/lib/plugin_api.dart index a2385fe47..c52513066 100644 --- a/lib/plugin_api.dart +++ b/lib/plugin_api.dart @@ -1,7 +1,7 @@ library flutter_map.plugin_api; export 'package:flutter_map/flutter_map.dart'; -export 'package:flutter_map/src/map/camera.dart'; +export 'package:flutter_map/src/map/camera/camera.dart'; export 'package:flutter_map/src/map/map_controller.dart'; export 'package:flutter_map/src/misc/private/bounds.dart'; export 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; diff --git a/lib/src/gestures/flutter_map_interactive_viewer.dart b/lib/src/gestures/flutter_map_interactive_viewer.dart index 37b24be09..96171be4e 100644 --- a/lib/src/gestures/flutter_map_interactive_viewer.dart +++ b/lib/src/gestures/flutter_map_interactive_viewer.dart @@ -7,7 +7,7 @@ import 'package:flutter_map/src/gestures/interactive_flag.dart'; import 'package:flutter_map/src/gestures/latlng_tween.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; -import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/map/internal_controller.dart'; import 'package:flutter_map/src/map/options.dart'; import 'package:flutter_map/src/misc/point.dart'; diff --git a/lib/src/gestures/map_events.dart b/lib/src/gestures/map_events.dart index d597ed27d..a943ee7a2 100644 --- a/lib/src/gestures/map_events.dart +++ b/lib/src/gestures/map_events.dart @@ -1,4 +1,4 @@ -import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:latlong2/latlong.dart'; /// Event sources which are used to identify different types of diff --git a/lib/src/layer/circle_layer.dart b/lib/src/layer/circle_layer.dart index 0d7faa158..2b649980e 100644 --- a/lib/src/layer/circle_layer.dart +++ b/lib/src/layer/circle_layer.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:latlong2/latlong.dart' hide Path; class CircleMarker { diff --git a/lib/src/layer/marker_layer.dart b/lib/src/layer/marker_layer.dart index 4ed5cf87d..4a970e171 100644 --- a/lib/src/layer/marker_layer.dart +++ b/lib/src/layer/marker_layer.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/src/layer/overlay_image_layer.dart b/lib/src/layer/overlay_image_layer.dart index 14d354a57..6eb45e7d4 100644 --- a/lib/src/layer/overlay_image_layer.dart +++ b/lib/src/layer/overlay_image_layer.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/src/layer/polygon_layer.dart b/lib/src/layer/polygon_layer.dart index f24076aba..d41166efd 100644 --- a/lib/src/layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer.dart @@ -3,7 +3,7 @@ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/layer/label.dart'; -import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:latlong2/latlong.dart' hide Path; // conflict with Path from UI enum PolygonLabelPlacement { diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index 48b07480b..f43ba9a30 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -3,7 +3,7 @@ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:latlong2/latlong.dart'; class Polyline { diff --git a/lib/src/layer/tile_layer/tile_range_calculator.dart b/lib/src/layer/tile_layer/tile_range_calculator.dart index 6d4922e46..9ea140fd9 100644 --- a/lib/src/layer/tile_layer/tile_range_calculator.dart +++ b/lib/src/layer/tile_layer/tile_range_calculator.dart @@ -1,5 +1,5 @@ import 'package:flutter_map/src/layer/tile_layer/tile_range.dart'; -import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/src/layer/tile_layer/tile_update_event.dart b/lib/src/layer/tile_layer/tile_update_event.dart index ed9bf36c8..46530effc 100644 --- a/lib/src/layer/tile_layer/tile_update_event.dart +++ b/lib/src/layer/tile_layer/tile_update_event.dart @@ -1,5 +1,5 @@ import 'package:flutter_map/src/gestures/map_events.dart'; -import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:latlong2/latlong.dart'; /// Describes whether loading and/or pruning should occur and allows overriding diff --git a/lib/src/map/camera.dart b/lib/src/map/camera/camera.dart similarity index 100% rename from lib/src/map/camera.dart rename to lib/src/map/camera/camera.dart diff --git a/lib/src/misc/camera_constraint.dart b/lib/src/map/camera/camera_constraint.dart similarity index 56% rename from lib/src/misc/camera_constraint.dart rename to lib/src/map/camera/camera_constraint.dart index 66d793cce..a68beef00 100644 --- a/lib/src/misc/camera_constraint.dart +++ b/lib/src/map/camera/camera_constraint.dart @@ -1,37 +1,53 @@ import 'dart:math' as math; +import 'package:flutter_map/src/map/camera/camera_fit.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:latlong2/latlong.dart'; -/// Describes a limit for the map's camera. This separate from constraints that -/// may be imposed by the chosen CRS. +/// Describes a boundary for a [MapCamera], that cannot be exceeded by movement +/// +/// This separate from constraints that may be imposed by the chosen CRS. +/// +/// Positioning is handled by [CameraFit]. abstract class CameraConstraint { + /// Describes a boundary for a [MapCamera], that cannot be exceeded by movement + /// + /// This separate from constraints that may be imposed by the chosen CRS. + /// + /// Positioning is handled by [CameraFit]. const CameraConstraint(); - /// Does not apply any constraint to the map movement. Note that the CRS - /// system of the map may still constrain the camera. + /// Does not apply any constraint const factory CameraConstraint.unconstrained() = UnconstrainedCamera._; - /// Limits the camera such that the center point of the camera remains within - /// [bounds]. This does not prevent points outside of [bounds] from being - /// shown, to achieve that you must use [ContainCamera]. + /// Constrains the center coordinate of the camera to within [bounds] + /// + /// Areas outside of [bounds] are likely to be visible. To instead constrain + /// by the edges of the camera, use [CameraConstraint.contain]. const factory CameraConstraint.containCenter({ required LatLngBounds bounds, }) = ContainCameraCenter._; - /// Limits the camera such that no point outside of the [bounds] will become - /// visible. + /// Constrains the edges of the camera to within [bounds] + /// + /// To instead constrain the center coordinate of the camera to these bounds, + /// use [CameraConstraint.containCenter]. const factory CameraConstraint.contain({ required LatLngBounds bounds, }) = ContainCamera._; + /// Create a new constrained camera based off the current [camera] + /// + /// May return `null` if no appropriate camera could be generated by movement, + /// for example because the camera was zoomed too far out. MapCamera? constrain(MapCamera camera); } -/// Allows the map to be moved without constraints. The CRS system of the map -/// may still resrict map movement to prevent invalid positions. +/// Does not apply any constraint to a [MapCamera] +/// +/// See [CameraConstraint] for more information. class UnconstrainedCamera extends CameraConstraint { const UnconstrainedCamera._(); @@ -39,18 +55,17 @@ class UnconstrainedCamera extends CameraConstraint { MapCamera constrain(MapCamera camera) => camera; } -/// Limits the camera such that the center point of the camera remains within -/// [bounds]. This does not prevent points outside of [bounds] from being -/// shown, to achieve that you must use [ContainCamera]. +/// Constrains the center coordinate of the camera to within [bounds] +/// +/// Areas outside of [bounds] are likely to be visible. To instead constrain +/// by the edges of the camera, use [ContainCamera]. +/// +/// See [CameraConstraint] for more information. class ContainCameraCenter extends CameraConstraint { - final LatLngBounds bounds; + const ContainCameraCenter._({required this.bounds}); - const ContainCameraCenter._({ - required this.bounds, - }); + final LatLngBounds bounds; - /// Returns a new [MapCamera] with the center point contained within - /// [bounds]. @override MapCamera constrain(MapCamera camera) => camera.withPosition( center: LatLng( @@ -64,21 +79,27 @@ class ContainCameraCenter extends CameraConstraint { ), ), ); + + @override + bool operator ==(Object other) { + return other is ContainCameraCenter && other.bounds == bounds; + } + + @override + int get hashCode => bounds.hashCode; } -/// Limits the camera such that no point outside of the [bounds] will become -/// visible. +/// Constrains the edges of the camera to within [bounds] +/// +/// To instead constrain the center coordinate of the camera to these bounds, +/// use [ContainCameraCenter]. +/// +/// See [CameraConstraint] for more information. class ContainCamera extends CameraConstraint { - final LatLngBounds bounds; + const ContainCamera._({required this.bounds}); - const ContainCamera._({ - required this.bounds, - }); + final LatLngBounds bounds; - /// Tries to determine a movement such that the [camera] only contains points - /// within [bounds]. If no movement is necessary the provided [camera] is - /// returned. If remaining within the [bounds] solely via movement is not - /// possible, because the camera is zoomed too far out, null is returned. @override MapCamera? constrain(MapCamera camera) { final testZoom = camera.zoom; diff --git a/lib/src/misc/camera_fit.dart b/lib/src/map/camera/camera_fit.dart similarity index 77% rename from lib/src/misc/camera_fit.dart rename to lib/src/map/camera/camera_fit.dart index a802c8512..8affaf799 100644 --- a/lib/src/misc/camera_fit.dart +++ b/lib/src/map/camera/camera_fit.dart @@ -1,48 +1,80 @@ import 'dart:math' as math; import 'package:flutter/widgets.dart'; +import 'package:flutter_map/src/map/camera/camera_constraint.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; -/// Determines a suitable map camera given the current camera and a set of -/// constraints. See [CameraFit.bounds] if you wish to fit a given set of -/// bounds, or [CameraFit.coordinates] if you wish to fit a set of coordinates. +/// Describes a position for a [MapCamera] +/// +/// Constraints are handled by [CameraConstraint]. abstract class CameraFit { - const CameraFit(); - - /// A configuration for fitting a [MapCamera] to the given [bounds]. The - /// [padding] may be used to leave extra space around the [bounds]. To - /// limit the zoom of the resulting [MapCamera] use [maxZoom] (default 17.0). - /// If [inside] is true then fit will be within the [bounds], otherwise it - /// will contain the [bounds] (default to false, contain). Finally - /// [forceIntegerZoomLevel] forces the resulting zoom to be rounded to the - /// nearest whole number. + /// Describes a position for a [MapCamera] + /// + /// Constraints are handled by [CameraConstraint]. + const CameraFit({ + this.padding = EdgeInsets.zero, + this.maxZoom = 18, + this.inside = false, + this.forceIntegerZoomLevel = false, + }); + + /// Adds a constant/pixel-based padding to the normal fit + /// + /// Defaults to [EdgeInsets.zero] + final EdgeInsets padding; + + /// Limits the maximum zoom level of the resulting fit + /// + /// Defaults to zoom level 18. + final double maxZoom; + + /// Whether the camera will be entirely inside the boundaries, or the + /// boundaries will be entirely inside the camera + /// + /// Defaults to forcing boundaries entirely inside the camera. + /// + /// Has no effect when using [FitCoordinates] due to lack of implemenation. + final bool inside; + + /// Whether the zoom level of the resulting fit should be rounded to the + /// nearest integer level + /// + /// Defaults to `false`. + final bool forceIntegerZoomLevel; + + /// Fits the camera to the [bounds], as closely as possible based on [inside] + /// + /// For information about available options, see the documentation on the + /// appropriate properties. const factory CameraFit.bounds({ required LatLngBounds bounds, EdgeInsets padding, double maxZoom, bool inside, bool forceIntegerZoomLevel, - }) = FitBounds; - - /// A configuration for fitting a [MapCamera] to the given [coordinates] such - /// that all of the [coordinates] are contained in the resulting [MapCamera]. - /// The [padding] may be used to leave extra space around the [bounds]. To - /// limit the zoom of the resulting [MapCamera] use [maxZoom] (default 17.0). - /// If [inside] is true then fit will be within the [bounds], otherwise it - /// will contain the [bounds] (default to false, contain). Finally - /// [forceIntegerZoomLevel] forces the resulting zoom to be rounded to the - /// nearest whole number. + }) = FitBounds._; + + /// Fits the camera to the [coordinates], as closely as possible + /// + /// For information about available options, see the documentation on the + /// appropriate properties. + /// + /// Allows for more fine grained boundaries when the camera is rotated. See + /// https://github.com/fleaflet/flutter_map/pull/1549 for more information. + /// + /// [inside] is not supported due to lack of implementation. const factory CameraFit.coordinates({ required List coordinates, EdgeInsets padding, double maxZoom, bool forceIntegerZoomLevel, - }) = FitCoordinates; + }) = FitCoordinates._; + /// Create a new fitted camera based off the current [camera] MapCamera fit(MapCamera camera); } @@ -55,21 +87,13 @@ abstract class CameraFit { /// nearest whole number. class FitBounds extends CameraFit { final LatLngBounds bounds; - final EdgeInsets padding; - final double maxZoom; - final bool inside; - - /// By default calculations will return fractional zoom levels. - /// If this parameter is set to [true] fractional zoom levels will be round - /// to the next suitable integer. - final bool forceIntegerZoomLevel; - const FitBounds({ + const FitBounds._({ required this.bounds, - this.padding = EdgeInsets.zero, - this.maxZoom = 17.0, - this.inside = false, - this.forceIntegerZoomLevel = false, + super.padding, + super.maxZoom, + super.inside, + super.forceIntegerZoomLevel, }); /// Returns a new [MapCamera] which fits this classes configuration. @@ -155,19 +179,13 @@ class FitBounds extends CameraFit { /// nearest whole number. class FitCoordinates extends CameraFit { final List coordinates; - final EdgeInsets padding; - final double maxZoom; - - /// By default calculations will return fractional zoom levels. - /// If this parameter is set to [true] fractional zoom levels will be round - /// to the next suitable integer. - final bool forceIntegerZoomLevel; - const FitCoordinates({ + const FitCoordinates._({ required this.coordinates, - this.padding = EdgeInsets.zero, - this.maxZoom = 17.0, - this.forceIntegerZoomLevel = false, + super.padding, + super.maxZoom, + super.inside, + super.forceIntegerZoomLevel, }); /// Returns a new [MapCamera] which fits this classes configuration. diff --git a/lib/src/map/inherited_model.dart b/lib/src/map/inherited_model.dart index a59ae6986..f11e0d2e6 100644 --- a/lib/src/map/inherited_model.dart +++ b/lib/src/map/inherited_model.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/map/map_controller.dart'; import 'package:flutter_map/src/map/options.dart'; @@ -47,7 +47,9 @@ class FlutterMapInheritedModel extends InheritedModel<_FlutterMapAspect> { @override bool updateShouldNotifyDependent( - covariant FlutterMapInheritedModel oldWidget, Set dependencies) { + covariant FlutterMapInheritedModel oldWidget, + Set dependencies, + ) { for (final dependency in dependencies) { if (dependency is _FlutterMapAspect) { switch (dependency) { @@ -77,8 +79,4 @@ class FlutterMapData { }); } -enum _FlutterMapAspect { - camera, - controller, - options; -} +enum _FlutterMapAspect { camera, controller, options } diff --git a/lib/src/map/map_controller.dart b/lib/src/map/map_controller.dart index 1f203255e..cf75deb80 100644 --- a/lib/src/map/map_controller.dart +++ b/lib/src/map/map_controller.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/map/inherited_model.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; import 'package:latlong2/latlong.dart'; @@ -21,7 +21,7 @@ abstract class MapController { /// instance. /// /// Factory constructor redirects to underlying implementation's constructor. - factory MapController() => MapControllerImpl(); + factory MapController() = MapControllerImpl; /// The controller for the closest [FlutterMap] ancestor. If this is called /// from a context with no [FlutterMap] ancestor a [StateError] will be diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/map_controller_impl.dart index a04821930..385c8fffc 100644 --- a/lib/src/map/map_controller_impl.dart +++ b/lib/src/map/map_controller_impl.dart @@ -3,10 +3,10 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; -import 'package:flutter_map/src/map/camera.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/map/internal_controller.dart'; import 'package:flutter_map/src/map/map_controller.dart'; -import 'package:flutter_map/src/misc/camera_fit.dart'; +import 'package:flutter_map/src/map/camera/camera_fit.dart'; import 'package:flutter_map/src/misc/center_zoom.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; import 'package:flutter_map/src/misc/move_and_rotate_result.dart'; diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index 3f9da2a3e..65462dda6 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -6,8 +6,8 @@ import 'package:flutter_map/src/gestures/interactive_flag.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; import 'package:flutter_map/src/map/inherited_model.dart'; -import 'package:flutter_map/src/misc/camera_constraint.dart'; -import 'package:flutter_map/src/misc/camera_fit.dart'; +import 'package:flutter_map/src/map/camera/camera_constraint.dart'; +import 'package:flutter_map/src/map/camera/camera_fit.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; import 'package:flutter_map/src/misc/position.dart'; import 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; diff --git a/lib/src/map/widget.dart b/lib/src/map/widget.dart index b5189003e..d500c3015 100644 --- a/lib/src/map/widget.dart +++ b/lib/src/map/widget.dart @@ -6,7 +6,7 @@ import 'package:flutter_map/src/map/internal_controller.dart'; import 'package:flutter_map/src/map/map_controller.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; import 'package:flutter_map/src/map/options.dart'; -import 'package:flutter_map/src/misc/camera_fit.dart'; +import 'package:flutter_map/src/map/camera/camera_fit.dart'; import 'package:flutter_map/src/misc/point.dart'; /// Renders an interactive geographical map as a widget diff --git a/test/flutter_map_controller_test.dart b/test/flutter_map_controller_test.dart index 5381e8af8..b7d903227 100644 --- a/test/flutter_map_controller_test.dart +++ b/test/flutter_map_controller_test.dart @@ -698,10 +698,10 @@ void main() { FitCoordinates fitCoordinates({ EdgeInsets padding = EdgeInsets.zero, }) => - FitCoordinates( + CameraFit.coordinates( coordinates: coordinates, padding: padding, - ); + ) as FitCoordinates; // Tests with no padding diff --git a/test/misc/frame_constraint_test.dart b/test/misc/frame_constraint_test.dart index 3760d2a17..3d02b91a4 100644 --- a/test/misc/frame_constraint_test.dart +++ b/test/misc/frame_constraint_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_map/src/geo/crs.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/map/camera.dart'; -import 'package:flutter_map/src/misc/camera_constraint.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; +import 'package:flutter_map/src/map/camera/camera_constraint.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; From 9e775c6bb99d339387257012a215601ac298db7a Mon Sep 17 00:00:00 2001 From: Jonathan Joelson Date: Thu, 22 Jun 2023 21:27:58 -0400 Subject: [PATCH 37/46] Add FitInsideBounds --- lib/src/map/camera/camera_fit.dart | 205 ++++++++- lib/src/map/map_controller_impl.dart | 46 ++- lib/src/map/widget.dart | 20 +- test/flutter_map_controller_test.dart | 574 +++++++++++++++++++++++++- 4 files changed, 802 insertions(+), 43 deletions(-) diff --git a/lib/src/map/camera/camera_fit.dart b/lib/src/map/camera/camera_fit.dart index 8affaf799..9c2663bcb 100644 --- a/lib/src/map/camera/camera_fit.dart +++ b/lib/src/map/camera/camera_fit.dart @@ -18,7 +18,6 @@ abstract class CameraFit { const CameraFit({ this.padding = EdgeInsets.zero, this.maxZoom = 18, - this.inside = false, this.forceIntegerZoomLevel = false, }); @@ -32,21 +31,13 @@ abstract class CameraFit { /// Defaults to zoom level 18. final double maxZoom; - /// Whether the camera will be entirely inside the boundaries, or the - /// boundaries will be entirely inside the camera - /// - /// Defaults to forcing boundaries entirely inside the camera. - /// - /// Has no effect when using [FitCoordinates] due to lack of implemenation. - final bool inside; - /// Whether the zoom level of the resulting fit should be rounded to the /// nearest integer level /// /// Defaults to `false`. final bool forceIntegerZoomLevel; - /// Fits the camera to the [bounds], as closely as possible based on [inside] + /// Fits the [bounds] inside the camera /// /// For information about available options, see the documentation on the /// appropriate properties. @@ -54,10 +45,20 @@ abstract class CameraFit { required LatLngBounds bounds, EdgeInsets padding, double maxZoom, - bool inside, bool forceIntegerZoomLevel, }) = FitBounds._; + /// Fits the camera inside the [bounds] + /// + /// For information about available options, see the documentation on the + /// appropriate properties. + const factory CameraFit.insideBounds({ + required LatLngBounds bounds, + EdgeInsets padding, + double maxZoom, + bool forceIntegerZoomLevel, + }) = FitInsideBounds._; + /// Fits the camera to the [coordinates], as closely as possible /// /// For information about available options, see the documentation on the @@ -92,7 +93,6 @@ class FitBounds extends CameraFit { required this.bounds, super.padding, super.maxZoom, - super.inside, super.forceIntegerZoomLevel, }); @@ -156,19 +156,193 @@ class FitBounds extends CameraFit { final scaleX = size.x / boundsSize.x; final scaleY = size.y / boundsSize.y; - final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); + final scale = math.min(scaleX, scaleY); var boundsZoom = camera.getScaleZoom(scale); if (forceIntegerZoomLevel) { - boundsZoom = - inside ? boundsZoom.ceilToDouble() : boundsZoom.floorToDouble(); + boundsZoom = boundsZoom.floorToDouble(); } return math.max(min, math.min(max, boundsZoom)); } } +class FitInsideBounds extends CameraFit { + final LatLngBounds bounds; + + const FitInsideBounds._({ + required this.bounds, + super.padding, + super.maxZoom, + super.forceIntegerZoomLevel, + }); + + @override + MapCamera fit(MapCamera camera) { + final paddingTL = CustomPoint(padding.left, padding.top); + final paddingBR = CustomPoint(padding.right, padding.bottom); + final paddingTotalXY = paddingTL + paddingBR; + final paddingOffset = (paddingBR - paddingTL) / 2; + + final cameraSize = camera.nonRotatedSize - paddingTotalXY; + + final projectedBoundsSize = Bounds( + camera.project(bounds.southEast, camera.zoom), + camera.project(bounds.northWest, camera.zoom), + ).size; + + final scale = _rectInRotRectScale( + angleRad: camera.rotationRad, + smallRectHalfWidth: cameraSize.x / 2.0, + smallRectHalfHeight: cameraSize.y / 2.0, + bigRectHalfWidth: projectedBoundsSize.x / 2.0, + bigRectHalfHeight: projectedBoundsSize.y / 2.0, + ); + + var newZoom = camera.getScaleZoom(1.0 / scale); + + if (forceIntegerZoomLevel) { + newZoom = newZoom.ceilToDouble(); + } + + newZoom = math.max( + camera.minZoom ?? 0.0, + math.min( + math.min(maxZoom, camera.maxZoom ?? double.infinity), + newZoom, + ), + ); + + final newCenter = _getCenter( + camera, + newZoom: newZoom, + paddingOffset: paddingOffset, + ); + + return camera.withPosition( + center: newCenter, + zoom: newZoom, + ); + } + + LatLng _getCenter( + MapCamera camera, { + required double newZoom, + required CustomPoint paddingOffset, + }) { + if (camera.rotation == 0.0) { + final swPoint = camera.project(bounds.southWest, newZoom); + final nePoint = camera.project(bounds.northEast, newZoom); + final projectedCenter = (swPoint + nePoint) / 2 + paddingOffset; + final newCenter = camera.unproject(projectedCenter, newZoom); + + return newCenter; + } + + // Handle rotation + final projectedCenter = camera.project(bounds.center, newZoom); + final rotatedCenter = projectedCenter.rotate(-camera.rotationRad); + final adjustedCenter = rotatedCenter + paddingOffset; + final derotatedAdjustedCenter = adjustedCenter.rotate(camera.rotationRad); + final newCenter = camera.unproject(derotatedAdjustedCenter, newZoom); + + return newCenter; + } + + static double _normalize(double value, double start, double end) { + final width = end - start; + final offsetValue = value - start; + + return (offsetValue - (offsetValue / width).floorToDouble() * width) + + start; + } + + /// Given two rectangles with their centers at the origin and an angle by + /// which the big rectangle is rotated, calculates the coefficient that would + /// be needed to scale the small rectangle such that it fits perfectly in the + /// larger rectangle while maintaining its aspect ratio. + /// + /// This algorithm has been adapted from https://stackoverflow.com/a/75907251 + static double _rectInRotRectScale({ + required double angleRad, + required double smallRectHalfWidth, + required double smallRectHalfHeight, + required double bigRectHalfWidth, + required double bigRectHalfHeight, + }) { + angleRad = _normalize(angleRad, 0, 2.0 * math.pi); + var kmin = double.infinity; + final quadrant = (2.0 * angleRad / math.pi).floor(); + if (quadrant.isOdd) { + final px = bigRectHalfWidth * math.cos(angleRad) + + bigRectHalfHeight * math.sin(angleRad); + final py = bigRectHalfWidth * math.sin(angleRad) - + bigRectHalfHeight * math.cos(angleRad); + final dx = -math.cos(angleRad); + final dy = -math.sin(angleRad); + + if (smallRectHalfWidth * dy - smallRectHalfHeight * dx != 0) { + var k = (px * dy - py * dx) / + (smallRectHalfWidth * dy - smallRectHalfHeight * dx); + if (quadrant >= 2) { + k = -k; + } + + if (k > 0) { + kmin = math.min(kmin, k); + } + } + + if (-smallRectHalfWidth * dx + smallRectHalfHeight * dy != 0) { + var k = (px * dx + py * dy) / + (-smallRectHalfWidth * dx + smallRectHalfHeight * dy); + if (quadrant >= 2) { + k = -k; + } + + if (k > 0) { + kmin = math.min(kmin, k); + } + } + + return kmin; + } else { + final px = bigRectHalfWidth * math.cos(angleRad) - + bigRectHalfHeight * math.sin(angleRad); + final py = bigRectHalfWidth * math.sin(angleRad) + + bigRectHalfHeight * math.cos(angleRad); + final dx = math.sin(angleRad); + final dy = -math.cos(angleRad); + if (smallRectHalfWidth * dy - smallRectHalfHeight * dx != 0) { + var k = (px * dy - py * dx) / + (smallRectHalfWidth * dy - smallRectHalfHeight * dx); + if (quadrant >= 2) { + k = -k; + } + + if (k > 0) { + kmin = math.min(kmin, k); + } + } + + if (-smallRectHalfWidth * dx + smallRectHalfHeight * dy != 0) { + var k = (px * dx + py * dy) / + (-smallRectHalfWidth * dx + smallRectHalfHeight * dy); + if (quadrant >= 2) { + k = -k; + } + + if (k > 0) { + kmin = math.min(kmin, k); + } + } + + return kmin; + } + } +} + /// A configuration for fitting a [MapCamera] to the given [coordinates] such /// that all of the [coordinates] are contained in the resulting [MapCamera]. /// The [padding] may be used to leave extra space around the [bounds]. To @@ -184,7 +358,6 @@ class FitCoordinates extends CameraFit { required this.coordinates, super.padding, super.maxZoom, - super.inside, super.forceIntegerZoomLevel, }); diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/map_controller_impl.dart index 385c8fffc..d21ae6745 100644 --- a/lib/src/map/map_controller_impl.dart +++ b/lib/src/map/map_controller_impl.dart @@ -99,7 +99,7 @@ class MapControllerImpl implements MapController { @override @Deprecated( - 'Prefer `fitCamera` with a CameraFit.bounds() instead. ' + 'Prefer `fitCamera` with a CameraFit.bounds() or CameraFit.insideBounds() instead. ' 'This method has been changed to use the new `CameraFit` classes which allows different kinds of fit. ' 'This method is deprecated since v6.', ) @@ -109,18 +109,24 @@ class MapControllerImpl implements MapController { const FitBoundsOptions(padding: EdgeInsets.all(12)), }) => fitCamera( - CameraFit.bounds( - bounds: bounds, - padding: options.padding, - maxZoom: options.maxZoom, - inside: options.inside, - forceIntegerZoomLevel: options.forceIntegerZoomLevel, - ), + options.inside + ? CameraFit.insideBounds( + bounds: bounds, + padding: options.padding, + maxZoom: options.maxZoom, + forceIntegerZoomLevel: options.forceIntegerZoomLevel, + ) + : CameraFit.bounds( + bounds: bounds, + padding: options.padding, + maxZoom: options.maxZoom, + forceIntegerZoomLevel: options.forceIntegerZoomLevel, + ), ); @override @Deprecated( - 'Prefer `CameraFit.bounds(bounds: bounds).fit(controller.camera)`. ' + 'Prefer `CameraFit.bounds(bounds: bounds).fit(controller.camera)` or `CameraFit.insideBounds(bounds: bounds).fit(controller.camera)`. ' 'This method is replaced by applying a CameraFit to the MapCamera. ' 'This method is deprecated since v6.', ) @@ -129,13 +135,21 @@ class MapControllerImpl implements MapController { FitBoundsOptions options = const FitBoundsOptions(padding: EdgeInsets.all(12)), }) { - final fittedState = CameraFit.bounds( - bounds: bounds, - padding: options.padding, - maxZoom: options.maxZoom, - inside: options.inside, - forceIntegerZoomLevel: options.forceIntegerZoomLevel, - ).fit(camera); + final cameraFit = options.inside + ? CameraFit.insideBounds( + bounds: bounds, + padding: options.padding, + maxZoom: options.maxZoom, + forceIntegerZoomLevel: options.forceIntegerZoomLevel, + ) + : CameraFit.bounds( + bounds: bounds, + padding: options.padding, + maxZoom: options.maxZoom, + forceIntegerZoomLevel: options.forceIntegerZoomLevel, + ); + + final fittedState = cameraFit.fit(camera); return CenterZoom( center: fittedState.center, zoom: fittedState.zoom, diff --git a/lib/src/map/widget.dart b/lib/src/map/widget.dart index d500c3015..e26544770 100644 --- a/lib/src/map/widget.dart +++ b/lib/src/map/widget.dart @@ -136,13 +136,19 @@ class FlutterMapStateContainer extends State { if (widget.options.bounds != null) { // Create the camera fit from the deprecated option. final fitBoundsOptions = widget.options.boundsOptions; - cameraFit = CameraFit.bounds( - bounds: widget.options.bounds!, - padding: fitBoundsOptions.padding, - maxZoom: fitBoundsOptions.maxZoom, - inside: fitBoundsOptions.inside, - forceIntegerZoomLevel: fitBoundsOptions.forceIntegerZoomLevel, - ); + cameraFit = fitBoundsOptions.inside + ? CameraFit.insideBounds( + bounds: widget.options.bounds!, + padding: fitBoundsOptions.padding, + maxZoom: fitBoundsOptions.maxZoom, + forceIntegerZoomLevel: fitBoundsOptions.forceIntegerZoomLevel, + ) + : CameraFit.bounds( + bounds: widget.options.bounds!, + padding: fitBoundsOptions.padding, + maxZoom: fitBoundsOptions.maxZoom, + forceIntegerZoomLevel: fitBoundsOptions.forceIntegerZoomLevel, + ); } else { cameraFit = widget.options.initialCameraFit!; } diff --git a/test/flutter_map_controller_test.dart b/test/flutter_map_controller_test.dart index b7d903227..42f237d7b 100644 --- a/test/flutter_map_controller_test.dart +++ b/test/flutter_map_controller_test.dart @@ -53,9 +53,8 @@ void main() { } { - final cameraConstraint = CameraFit.bounds( + final cameraConstraint = CameraFit.insideBounds( bounds: bounds, - inside: true, ); final expectedBounds = LatLngBounds( @@ -74,9 +73,8 @@ void main() { } { - final cameraConstraint = CameraFit.bounds( + final cameraConstraint = CameraFit.insideBounds( bounds: bounds, - inside: true, forceIntegerZoomLevel: true, ); @@ -860,4 +858,572 @@ void main() { expectedZoom: 5.368867444131889, ); }); + + testWidgets('test fit inside bounds with rotation', (tester) async { + final controller = MapController(); + final bounds = LatLngBounds( + const LatLng(4.214943, 33.925781), + const LatLng(-1.362176, 29.575195), + ); + + await tester.pumpWidget(TestApp(controller: controller)); + + Future testFitInsideBounds({ + required double rotation, + required CameraFit cameraConstraint, + required LatLngBounds expectedBounds, + required LatLng expectedCenter, + required double expectedZoom, + }) async { + controller.rotate(rotation); + + controller.fitCamera(cameraConstraint); + await tester.pump(); + expect( + controller.camera.visibleBounds.northWest.latitude, + moreOrLessEquals(expectedBounds.northWest.latitude), + ); + expect( + controller.camera.visibleBounds.northWest.longitude, + moreOrLessEquals(expectedBounds.northWest.longitude), + ); + expect( + controller.camera.visibleBounds.southEast.latitude, + moreOrLessEquals(expectedBounds.southEast.latitude), + ); + expect( + controller.camera.visibleBounds.southEast.longitude, + moreOrLessEquals(expectedBounds.southEast.longitude), + ); + expect( + controller.camera.center.latitude, + moreOrLessEquals(expectedCenter.latitude), + ); + expect( + controller.camera.center.longitude, + moreOrLessEquals(expectedCenter.longitude), + ); + expect(controller.camera.zoom, moreOrLessEquals(expectedZoom)); + } + + // Tests with no padding + + await testFitInsideBounds( + rotation: -360, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), + expectedBounds: LatLngBounds( + const LatLng(3.6031134233301474, 29.56772762000039), + const LatLng(-0.7450743699315154, 33.9183136200004), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447605964), + expectedZoom: 6.014499548969527, + ); + await testFitInsideBounds( + rotation: -300, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), + expectedBounds: LatLngBounds( + const LatLng(3.614278658020072, 29.56945889748712), + const LatLng(-0.7338878844415404, 33.920044897487124), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447606), + expectedZoom: 6.464483862446023, + ); + await testFitInsideBounds( + rotation: -240, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), + expectedBounds: LatLngBounds( + const LatLng(3.6142786580207207, 29.56945889748632), + const LatLng(-0.7338878844408534, 33.92004489748633), + ), + expectedCenter: const LatLng(1.427411699029666, 31.747848447605964), + expectedZoom: 6.464483862446028, + ); + await testFitInsideBounds( + rotation: -180, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), + expectedBounds: LatLngBounds( + const LatLng(3.6031134233301474, 29.56772762000039), + const LatLng(-0.7450743699315154, 33.9183136200004), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447605964), + expectedZoom: 6.014499548969527, + ); + await testFitInsideBounds( + rotation: -120, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), + expectedBounds: LatLngBounds( + const LatLng(3.614278658020072, 29.56945889748712), + const LatLng(-0.7338878844415404, 33.920044897487124), + ), + expectedCenter: const LatLng(1.4274116990296914, 31.74784844760592), + expectedZoom: 6.464483862446023, + ); + await testFitInsideBounds( + rotation: -60, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), + expectedBounds: LatLngBounds( + const LatLng(3.6142786580207207, 29.56945889748632), + const LatLng(-0.7338878844408534, 33.92004489748633), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447605964), + expectedZoom: 6.464483862446028, + ); + await testFitInsideBounds( + rotation: 0, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), + expectedBounds: LatLngBounds( + const LatLng(3.6031134233301474, 29.56772762000039), + const LatLng(-0.7450743699315154, 33.9183136200004), + ), + expectedCenter: const LatLng(1.4280748738291353, 31.75048799999998), + expectedZoom: 6.014499548969527, + ); + await testFitInsideBounds( + rotation: 60, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), + expectedBounds: LatLngBounds( + const LatLng(3.614278658020072, 29.56945889748712), + const LatLng(-0.7338878844415404, 33.920044897487124), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447606), + expectedZoom: 6.464483862446023, + ); + await testFitInsideBounds( + rotation: 120, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), + expectedBounds: LatLngBounds( + const LatLng(3.6142786580207207, 29.56945889748632), + const LatLng(-0.7338878844408534, 33.92004489748633), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447605964), + expectedZoom: 6.464483862446028, + ); + await testFitInsideBounds( + rotation: 180, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), + expectedBounds: LatLngBounds( + const LatLng(3.6031134233301474, 29.56772762000039), + const LatLng(-0.7450743699315154, 33.9183136200004), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447605964), + expectedZoom: 6.014499548969527, + ); + await testFitInsideBounds( + rotation: 240, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), + expectedBounds: LatLngBounds( + const LatLng(3.614278658020072, 29.56945889748712), + const LatLng(-0.7338878844415404, 33.920044897487124), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.74784844760592), + expectedZoom: 6.464483862446023, + ); + await testFitInsideBounds( + rotation: 300, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), + expectedBounds: LatLngBounds( + const LatLng(3.6142786580207207, 29.56945889748632), + const LatLng(-0.7338878844408534, 33.92004489748633), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447605964), + expectedZoom: 6.464483862446028, + ); + await testFitInsideBounds( + rotation: 360, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: EdgeInsets.zero, + ), + expectedBounds: LatLngBounds( + const LatLng(3.6031134233301474, 29.56772762000039), + const LatLng(-0.7450743699315154, 33.9183136200004), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447605964), + expectedZoom: 6.014499548969527, + ); + + // Tests with symmetric padding + + const equalPadding = EdgeInsets.all(12); + + await testFitInsideBounds( + rotation: -360, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: equalPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.8971355052392727, 29.273074295454837), + const LatLng(-1.0436460563295582, 34.21692202272759), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447605964), + expectedZoom: 5.8300749778321, + ); + await testFitInsideBounds( + rotation: -300, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: equalPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.900159833096254, 29.26631356795233), + const LatLng(-1.0406152150025456, 34.21016129522507), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447605964), + expectedZoom: 6.280059291308596, + ); + await testFitInsideBounds( + rotation: -240, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: equalPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.900159833095936, 29.266313567952732), + const LatLng(-1.0406152150029018, 34.210161295225475), + ), + expectedCenter: const LatLng(1.427411699029666, 31.747848447605964), + expectedZoom: 6.280059291308594, + ); + await testFitInsideBounds( + rotation: -180, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: equalPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.897135505240036, 29.273074295453956), + const LatLng(-1.0436460563287566, 34.21692202272667), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447605964), + expectedZoom: 5.830074977832107, + ); + await testFitInsideBounds( + rotation: -120, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: equalPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.900159833096941, 29.26631356795153), + const LatLng(-1.0406152150018586, 34.210161295224275), + ), + expectedCenter: const LatLng(1.427411699029615, 31.74784844760592), + expectedZoom: 6.280059291308602, + ); + await testFitInsideBounds( + rotation: -60, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: equalPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.9001598330968137, 29.266313567951688), + const LatLng(-1.0406152150019858, 34.21016129522444), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447606), + expectedZoom: 6.280059291308601, + ); + await testFitInsideBounds( + rotation: 0, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: equalPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.921797222702341, 29.273074295454474), + const LatLng(-1.0189308220167805, 34.21692202272719), + ), + expectedCenter: const LatLng(1.4280748738291607, 31.750488000000022), + expectedZoom: 5.830074977832103, + ); + await testFitInsideBounds( + rotation: 60, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: equalPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.9001598330947402, 29.26631356795413), + const LatLng(-1.0406152150040977, 34.21016129522692), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447605964), + expectedZoom: 6.280059291308584, + ); + await testFitInsideBounds( + rotation: 120, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: equalPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.900159833097259, 29.26631356795117), + const LatLng(-1.0406152150015406, 34.21016129522388), + ), + expectedCenter: const LatLng(1.427411699029615, 31.74784844760592), + expectedZoom: 6.280059291308604, + ); + await testFitInsideBounds( + rotation: 180, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: equalPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.897135505238624, 29.273074295455597), + const LatLng(-1.0436460563302323, 34.21692202272835), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447605964), + expectedZoom: 5.830074977832095, + ); + await testFitInsideBounds( + rotation: 240, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: equalPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.900159833097577, 29.266313567950775), + const LatLng(-1.0406152150011843, 34.21016129522348), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447605964), + expectedZoom: 6.280059291308607, + ); + await testFitInsideBounds( + rotation: 300, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: equalPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.900159833095936, 29.266313567952732), + const LatLng(-1.0406152150029018, 34.210161295225475), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447605964), + expectedZoom: 6.280059291308594, + ); + await testFitInsideBounds( + rotation: 360, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: equalPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.897135505240036, 29.273074295453956), + const LatLng(-1.0436460563287566, 34.21692202272667), + ), + expectedCenter: const LatLng(1.4274116990296404, 31.747848447605964), + expectedZoom: 5.830074977832107, + ); + + // Tests with asymmetric padding + + const asymmetricPadding = EdgeInsets.fromLTRB(12, 12, 24, 24); + + await testFitInsideBounds( + rotation: -360, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: asymmetricPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.93081962068567, 29.252575414633416), + const LatLng(-1.371554855609733, 34.558168097560255), + ), + expectedCenter: const LatLng(1.2682880092901039, 31.90701622809379), + expectedZoom: 5.728195363812894, + ); + await testFitInsideBounds( + rotation: -300, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: asymmetricPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(4.12285573833763, 29.236827391148324), + const LatLng(-1.179091165662991, 34.5424200740752), + ), + expectedCenter: const LatLng(1.4700469435297785, 31.90701622809379), + expectedZoom: 6.178179677289382, + ); + await testFitInsideBounds( + rotation: -240, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: asymmetricPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(4.239064535667103, 29.12030848890263), + const LatLng(-1.0625945779487183, 34.42590117182947), + ), + expectedCenter: const LatLng(1.5865243776059719, 31.790497325848776), + expectedZoom: 6.178179677289386, + ); + await testFitInsideBounds( + rotation: -180, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: asymmetricPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(4.248344214607476, 28.934239853659207), + const LatLng(-1.0532909733871119, 34.239832536586086), + ), + expectedCenter: const LatLng(1.5865243776059845, 31.58868066711818), + expectedZoom: 5.728195363812884, + ); + await testFitInsideBounds( + rotation: -120, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: asymmetricPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(4.045373737414225, 28.926110318495112), + const LatLng(-1.2567528788718025, 34.23170300142199), + ), + expectedCenter: const LatLng(1.3847756639611237, 31.58868066711814), + expectedZoom: 6.17817967728938, + ); + await testFitInsideBounds( + rotation: -60, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: asymmetricPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.9291368844072636, 29.04262922073941), + const LatLng(-1.373241074234739, 34.34822190366625), + ), + expectedCenter: const LatLng(1.2682880092901039, 31.70519956936319), + expectedZoom: 6.1781796772893856, + ); + await testFitInsideBounds( + rotation: 0, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: asymmetricPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.9308196206843724, 29.252575414634972), + const LatLng(-1.3715548556110944, 34.55816809756181), + ), + expectedCenter: const LatLng(1.2689512274367805, 31.909655780487807), + expectedZoom: 5.728195363812883, + ); + await testFitInsideBounds( + rotation: 60, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: asymmetricPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(4.122855738337325, 29.236827391148683), + const LatLng(-1.179091165663309, 34.542420074075565), + ), + expectedCenter: const LatLng(1.4700469435298167, 31.90701622809375), + expectedZoom: 6.178179677289379, + ); + await testFitInsideBounds( + rotation: 120, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: asymmetricPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(4.23906453566681, 29.12030848890299), + const LatLng(-1.0625945779490364, 34.42590117182983), + ), + expectedCenter: const LatLng(1.5865243776060227, 31.790497325848737), + expectedZoom: 6.178179677289384, + ); + await testFitInsideBounds( + rotation: 180, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: asymmetricPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(4.248344214608647, 28.934239853657804), + const LatLng(-1.053290973385865, 34.23983253658464), + ), + expectedCenter: const LatLng(1.5865243776059845, 31.58868066711818), + expectedZoom: 5.728195363812894, + ); + await testFitInsideBounds( + rotation: 240, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: asymmetricPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(4.045373737414429, 28.926110318494874), + const LatLng(-1.2567528788715607, 34.23170300142176), + ), + expectedCenter: const LatLng(1.3847756639610982, 31.58868066711814), + expectedZoom: 6.178179677289382, + ); + await testFitInsideBounds( + rotation: 300, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: asymmetricPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.9291368844075816, 29.04262922073901), + const LatLng(-1.3732410742343828, 34.348221903665845), + ), + expectedCenter: const LatLng(1.2682880092901676, 31.705199569363227), + expectedZoom: 6.178179677289388, + ); + await testFitInsideBounds( + rotation: 360, + cameraConstraint: CameraFit.insideBounds( + bounds: bounds, + padding: asymmetricPadding, + ), + expectedBounds: LatLngBounds( + const LatLng(3.9308196206847033, 29.252575414634578), + const LatLng(-1.3715548556107255, 34.55816809756141), + ), + expectedCenter: const LatLng(1.2682880092901039, 31.90701622809375), + expectedZoom: 5.728195363812886, + ); + }); } From 955e06674b1b426dc0267b8aa3c6711233e89055 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Tue, 27 Jun 2023 10:41:12 +0200 Subject: [PATCH 38/46] Move CameraFit attributes from the base class to the subclasses and tidy up documentation None of the fields which were on the CameraFit base class were conceptually essential for any imaginable camera fit. Moving them to the subclasses ensures that any future camera fits will not need to implement those options just because they already exist. It would also allow for individual camera fits to specify different default values if appropriate. --- lib/src/map/camera/camera_fit.dart | 108 +++++++++++++++++------------ 1 file changed, 62 insertions(+), 46 deletions(-) diff --git a/lib/src/map/camera/camera_fit.dart b/lib/src/map/camera/camera_fit.dart index 9c2663bcb..d6a8ca1d1 100644 --- a/lib/src/map/camera/camera_fit.dart +++ b/lib/src/map/camera/camera_fit.dart @@ -1,9 +1,9 @@ import 'dart:math' as math; import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/map/camera/camera_constraint.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; +import 'package:flutter_map/src/map/camera/camera_constraint.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; @@ -15,27 +15,7 @@ abstract class CameraFit { /// Describes a position for a [MapCamera] /// /// Constraints are handled by [CameraConstraint]. - const CameraFit({ - this.padding = EdgeInsets.zero, - this.maxZoom = 18, - this.forceIntegerZoomLevel = false, - }); - - /// Adds a constant/pixel-based padding to the normal fit - /// - /// Defaults to [EdgeInsets.zero] - final EdgeInsets padding; - - /// Limits the maximum zoom level of the resulting fit - /// - /// Defaults to zoom level 18. - final double maxZoom; - - /// Whether the zoom level of the resulting fit should be rounded to the - /// nearest integer level - /// - /// Defaults to `false`. - final bool forceIntegerZoomLevel; + const CameraFit(); /// Fits the [bounds] inside the camera /// @@ -79,21 +59,31 @@ abstract class CameraFit { MapCamera fit(MapCamera camera); } -/// A configuration for fitting a [MapCamera] to the given [bounds]. The -/// [padding] may be used to leave extra space around the [bounds]. To -/// limit the zoom of the resulting [MapCamera] use [maxZoom] (default 17.0). -/// If [inside] is true then fit will be within the [bounds], otherwise it -/// will contain the [bounds] (default to false, contain). Finally -/// [forceIntegerZoomLevel] forces the resulting zoom to be rounded to the -/// nearest whole number. class FitBounds extends CameraFit { + /// The bounds which the camera should contain once it is fitted. final LatLngBounds bounds; + /// Adds a constant/pixel-based padding to the normal fit. + /// + /// Defaults to [EdgeInsets.zero]. + final EdgeInsets padding; + + /// Limits the maximum zoom level of the resulting fit. + /// + /// Defaults to zoom level 18. + final double maxZoom; + + /// Whether the zoom level of the resulting fit should be rounded to the + /// nearest integer level. + /// + /// Defaults to `false`. + final bool forceIntegerZoomLevel; + const FitBounds._({ required this.bounds, - super.padding, - super.maxZoom, - super.forceIntegerZoomLevel, + this.padding = EdgeInsets.zero, + this.maxZoom = 18, + this.forceIntegerZoomLevel = false, }); /// Returns a new [MapCamera] which fits this classes configuration. @@ -169,13 +159,30 @@ class FitBounds extends CameraFit { } class FitInsideBounds extends CameraFit { + /// The bounds which the camera should fit inside once it is fitted. final LatLngBounds bounds; + /// Adds a constant/pixel-based padding to the normal fit. + /// + /// Defaults to [EdgeInsets.zero]. + final EdgeInsets padding; + + /// Limits the maximum zoom level of the resulting fit. + /// + /// Defaults to zoom level 18. + final double maxZoom; + + /// Whether the zoom level of the resulting fit should be rounded to the + /// nearest integer level. + /// + /// Defaults to `false`. + final bool forceIntegerZoomLevel; + const FitInsideBounds._({ required this.bounds, - super.padding, - super.maxZoom, - super.forceIntegerZoomLevel, + this.padding = EdgeInsets.zero, + this.maxZoom = 18, + this.forceIntegerZoomLevel = false, }); @override @@ -343,22 +350,31 @@ class FitInsideBounds extends CameraFit { } } -/// A configuration for fitting a [MapCamera] to the given [coordinates] such -/// that all of the [coordinates] are contained in the resulting [MapCamera]. -/// The [padding] may be used to leave extra space around the [bounds]. To -/// limit the zoom of the resulting [MapCamera] use [maxZoom] (default 17.0). -/// If [inside] is true then fit will be within the [bounds], otherwise it -/// will contain the [bounds] (default to false, contain). Finally -/// [forceIntegerZoomLevel] forces the resulting zoom to be rounded to the -/// nearest whole number. class FitCoordinates extends CameraFit { + /// The coordinates which the camera should contain once it is fitted. final List coordinates; + /// Adds a constant/pixel-based padding to the normal fit. + /// + /// Defaults to [EdgeInsets.zero]. + final EdgeInsets padding; + + /// Limits the maximum zoom level of the resulting fit. + /// + /// Defaults to zoom level 18. + final double maxZoom; + + /// Whether the zoom level of the resulting fit should be rounded to the + /// nearest integer level. + /// + /// Defaults to `false`. + final bool forceIntegerZoomLevel; + const FitCoordinates._({ required this.coordinates, - super.padding, - super.maxZoom, - super.forceIntegerZoomLevel, + this.padding = EdgeInsets.zero, + this.maxZoom = 18, + this.forceIntegerZoomLevel = false, }); /// Returns a new [MapCamera] which fits this classes configuration. From 2b4038184382573dd71f663db0e1ab12f8afdb86 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Tue, 27 Jun 2023 10:45:40 +0200 Subject: [PATCH 39/46] Re-order imports alphabetically --- lib/flutter_map.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index 912c7b7c8..65aad27ec 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -26,11 +26,11 @@ export 'package:flutter_map/src/layer/tile_layer/tile_provider/file_providers/ti export 'package:flutter_map/src/layer/tile_layer/tile_provider/network_tile_provider.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_update_event.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_update_transformer.dart'; +export 'package:flutter_map/src/map/camera/camera_constraint.dart'; +export 'package:flutter_map/src/map/camera/camera_fit.dart'; export 'package:flutter_map/src/map/map_controller.dart'; export 'package:flutter_map/src/map/options.dart'; export 'package:flutter_map/src/map/widget.dart'; -export 'package:flutter_map/src/map/camera/camera_constraint.dart'; -export 'package:flutter_map/src/map/camera/camera_fit.dart'; export 'package:flutter_map/src/misc/center_zoom.dart'; export 'package:flutter_map/src/misc/fit_bounds_options.dart'; export 'package:flutter_map/src/misc/move_and_rotate_result.dart'; From d0ad93c2386bef3b76760bb238902c02e9133e4f Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Tue, 27 Jun 2023 10:51:06 +0200 Subject: [PATCH 40/46] Add lint to enforce consistent import/export ordering --- analysis_options.yaml | 3 ++- lib/src/geo/crs.dart | 2 +- lib/src/layer/tile_layer/tile.dart | 2 +- lib/src/layer/tile_layer/tile_bounds/tile_bounds.dart | 2 +- lib/src/layer/tile_layer/tile_bounds/tile_bounds_at_zoom.dart | 2 +- lib/src/layer/tile_layer/tile_image_manager.dart | 2 +- lib/src/layer/tile_layer/tile_range.dart | 4 ++-- lib/src/map/camera/camera_constraint.dart | 2 +- lib/src/map/map_controller_impl.dart | 2 +- lib/src/map/options.dart | 2 +- lib/src/map/widget.dart | 2 +- test/core/bounds_test.dart | 2 +- test/layer/tile_layer/tile_bounds/crs_fakes.dart | 2 +- .../tile_layer/tile_bounds/tile_bounds_at_zoom_test.dart | 4 ++-- 14 files changed, 17 insertions(+), 16 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 58bbcff6c..d5adb5dc8 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -16,6 +16,7 @@ linter: avoid_dynamic_calls: true cancel_subscriptions: true close_sinks: true + directives_ordering: true package_api_docs: true prefer_constructors_over_static_methods: true prefer_final_in_for_each: true @@ -25,4 +26,4 @@ linter: throw_in_finally: true type_annotate_public_apis: true unnecessary_statements: true - use_named_constants: true \ No newline at end of file + use_named_constants: true diff --git a/lib/src/geo/crs.dart b/lib/src/geo/crs.dart index df4aee993..c0adefafb 100644 --- a/lib/src/geo/crs.dart +++ b/lib/src/geo/crs.dart @@ -1,7 +1,7 @@ import 'dart:math' as math; -import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:flutter_map/src/misc/point.dart'; +import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; import 'package:proj4dart/proj4dart.dart' as proj4; diff --git a/lib/src/layer/tile_layer/tile.dart b/lib/src/layer/tile_layer/tile.dart index a0a8b7c97..cfaca4e98 100644 --- a/lib/src/layer/tile_layer/tile.dart +++ b/lib/src/layer/tile_layer/tile.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_builder.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_image.dart'; +import 'package:flutter_map/src/misc/point.dart'; class Tile extends StatefulWidget { final TileImage tileImage; diff --git a/lib/src/layer/tile_layer/tile_bounds/tile_bounds.dart b/lib/src/layer/tile_layer/tile_bounds/tile_bounds.dart index 6cf9f8806..7d44013dd 100644 --- a/lib/src/layer/tile_layer/tile_bounds/tile_bounds.dart +++ b/lib/src/layer/tile_layer/tile_bounds/tile_bounds.dart @@ -1,8 +1,8 @@ -import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:flutter_map/src/geo/crs.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_bounds/tile_bounds_at_zoom.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_range.dart'; +import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; abstract class TileBounds { diff --git a/lib/src/layer/tile_layer/tile_bounds/tile_bounds_at_zoom.dart b/lib/src/layer/tile_layer/tile_bounds/tile_bounds_at_zoom.dart index b736d4f30..d89d856f7 100644 --- a/lib/src/layer/tile_layer/tile_bounds/tile_bounds_at_zoom.dart +++ b/lib/src/layer/tile_layer/tile_bounds/tile_bounds_at_zoom.dart @@ -1,6 +1,6 @@ -import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_range.dart'; +import 'package:flutter_map/src/misc/point.dart'; abstract class TileBoundsAtZoom { const TileBoundsAtZoom(); diff --git a/lib/src/layer/tile_layer/tile_image_manager.dart b/lib/src/layer/tile_layer/tile_image_manager.dart index c210dad1d..847cbf85b 100644 --- a/lib/src/layer/tile_layer/tile_image_manager.dart +++ b/lib/src/layer/tile_layer/tile_image_manager.dart @@ -1,5 +1,4 @@ import 'package:collection/collection.dart'; -import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_bounds/tile_bounds.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_bounds/tile_bounds_at_zoom.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; @@ -7,6 +6,7 @@ import 'package:flutter_map/src/layer/tile_layer/tile_display.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_image.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_layer.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_range.dart'; +import 'package:flutter_map/src/misc/point.dart'; typedef TileCreator = TileImage Function(TileCoordinates coordinates); diff --git a/lib/src/layer/tile_layer/tile_range.dart b/lib/src/layer/tile_layer/tile_range.dart index da142358a..fa4b4a789 100644 --- a/lib/src/layer/tile_layer/tile_range.dart +++ b/lib/src/layer/tile_layer/tile_range.dart @@ -1,8 +1,8 @@ import 'dart:math' as math; -import 'package:flutter_map/src/misc/private/bounds.dart'; -import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; +import 'package:flutter_map/src/misc/point.dart'; +import 'package:flutter_map/src/misc/private/bounds.dart'; abstract class TileRange { final int zoom; diff --git a/lib/src/map/camera/camera_constraint.dart b/lib/src/map/camera/camera_constraint.dart index a68beef00..f192eaa79 100644 --- a/lib/src/map/camera/camera_constraint.dart +++ b/lib/src/map/camera/camera_constraint.dart @@ -1,8 +1,8 @@ import 'dart:math' as math; -import 'package:flutter_map/src/map/camera/camera_fit.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; +import 'package:flutter_map/src/map/camera/camera_fit.dart'; import 'package:flutter_map/src/misc/point.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/map_controller_impl.dart index d21ae6745..889cace42 100644 --- a/lib/src/map/map_controller_impl.dart +++ b/lib/src/map/map_controller_impl.dart @@ -4,9 +4,9 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; +import 'package:flutter_map/src/map/camera/camera_fit.dart'; import 'package:flutter_map/src/map/internal_controller.dart'; import 'package:flutter_map/src/map/map_controller.dart'; -import 'package:flutter_map/src/map/camera/camera_fit.dart'; import 'package:flutter_map/src/misc/center_zoom.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; import 'package:flutter_map/src/misc/move_and_rotate_result.dart'; diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index 65462dda6..b04df2082 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -5,9 +5,9 @@ import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/gestures/interactive_flag.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; -import 'package:flutter_map/src/map/inherited_model.dart'; import 'package:flutter_map/src/map/camera/camera_constraint.dart'; import 'package:flutter_map/src/map/camera/camera_fit.dart'; +import 'package:flutter_map/src/map/inherited_model.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; import 'package:flutter_map/src/misc/position.dart'; import 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; diff --git a/lib/src/map/widget.dart b/lib/src/map/widget.dart index e26544770..c9f27dd45 100644 --- a/lib/src/map/widget.dart +++ b/lib/src/map/widget.dart @@ -1,12 +1,12 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/gestures/flutter_map_interactive_viewer.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; +import 'package:flutter_map/src/map/camera/camera_fit.dart'; import 'package:flutter_map/src/map/inherited_model.dart'; import 'package:flutter_map/src/map/internal_controller.dart'; import 'package:flutter_map/src/map/map_controller.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; import 'package:flutter_map/src/map/options.dart'; -import 'package:flutter_map/src/map/camera/camera_fit.dart'; import 'package:flutter_map/src/misc/point.dart'; /// Renders an interactive geographical map as a widget diff --git a/test/core/bounds_test.dart b/test/core/bounds_test.dart index fe58eab52..c05643d0b 100644 --- a/test/core/bounds_test.dart +++ b/test/core/bounds_test.dart @@ -1,5 +1,5 @@ -import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:flutter_map/src/misc/point.dart'; +import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:flutter_test/flutter_test.dart'; import '../helpers/core.dart'; diff --git a/test/layer/tile_layer/tile_bounds/crs_fakes.dart b/test/layer/tile_layer/tile_bounds/crs_fakes.dart index 594144930..6b2a02ab7 100644 --- a/test/layer/tile_layer/tile_bounds/crs_fakes.dart +++ b/test/layer/tile_layer/tile_bounds/crs_fakes.dart @@ -1,5 +1,5 @@ -import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/geo/crs.dart'; +import 'package:flutter_map/src/misc/point.dart'; import 'package:latlong2/latlong.dart'; class FakeInfiniteCrs extends Crs { diff --git a/test/layer/tile_layer/tile_bounds/tile_bounds_at_zoom_test.dart b/test/layer/tile_layer/tile_bounds/tile_bounds_at_zoom_test.dart index 460591669..dd986bd6a 100644 --- a/test/layer/tile_layer/tile_bounds/tile_bounds_at_zoom_test.dart +++ b/test/layer/tile_layer/tile_bounds/tile_bounds_at_zoom_test.dart @@ -1,8 +1,8 @@ -import 'package:flutter_map/src/misc/private/bounds.dart'; -import 'package:flutter_map/src/misc/point.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_bounds/tile_bounds_at_zoom.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_range.dart'; +import 'package:flutter_map/src/misc/point.dart'; +import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:test/test.dart'; void main() { From 5750b6a58db9334246766855fb3c62769aff612c Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Tue, 27 Jun 2023 11:17:49 +0200 Subject: [PATCH 41/46] Reinstate maxBounds as a deprecated option --- lib/src/map/options.dart | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index b04df2082..de92f94ea 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -84,7 +84,7 @@ class MapOptions { final MapEventCallback? onMapEvent; /// Define limits for viewing the map. - final CameraConstraint cameraConstraint; + final CameraConstraint? _cameraConstraint; /// OnMapReady is called after the map runs it's initState. /// At that point the map has assigned its state to the controller @@ -94,6 +94,8 @@ class MapOptions { /// In initState to controll the map before the next frame. final void Function()? onMapReady; + final LatLngBounds? maxBounds; + /// Flag to enable the built in keep alive functionality /// /// If the map is within a complex layout, such as a [ListView] or [PageView], @@ -140,7 +142,7 @@ class MapOptions { ) this.boundsOptions = const FitBoundsOptions(), this.initialCameraFit, - this.cameraConstraint = const CameraConstraint.unconstrained(), + CameraConstraint? cameraConstraint, InteractionOptions? interactionOptions, @Deprecated( 'Prefer setting this in `interactionOptions`. ' @@ -220,6 +222,12 @@ class MapOptions { this.onPositionChanged, this.onMapEvent, this.onMapReady, + @Deprecated( + 'Prefer `cameraConstraint` instead. ' + 'This option is now replaced by `cameraConstraint` which provides more flexibile limiting of the map position. ' + 'This option is deprecated since v6.', + ) + this.maxBounds, this.keepAlive = false, }) : _interactionOptions = interactionOptions, _interactiveFlags = interactiveFlags, @@ -235,14 +243,15 @@ class MapOptions { _scrollWheelVelocity = scrollWheelVelocity, initialCenter = center ?? initialCenter, initialZoom = zoom ?? initialZoom, - initialRotation = rotation ?? initialRotation; + initialRotation = rotation ?? initialRotation, + _cameraConstraint = cameraConstraint; /// The options of the closest [FlutterMap] ancestor. If this is called from a /// context with no [FlutterMap] ancestor, null is returned. static MapOptions? maybeOf(BuildContext context) => FlutterMapInheritedModel.maybeOptionsOf(context); - /// The optoins of the closest [FlutterMap] ancestor. If this is called from a + /// The options of the closest [FlutterMap] ancestor. If this is called from a /// context with no [FlutterMap] ancestor a [StateError] will be thrown. static MapOptions of(BuildContext context) => maybeOf(context) ?? @@ -267,6 +276,15 @@ class MapOptions { scrollWheelVelocity: _scrollWheelVelocity ?? 0.005, ); + // Note that this getter exists to make sure that the deprecated [maxBounds] + // option is consistently used. Making this a getter allows the constructor + // to remain const. + CameraConstraint get cameraConstraint => + _cameraConstraint ?? + (maxBounds != null + ? CameraConstraint.contain(bounds: maxBounds!) + : const CameraConstraint.unconstrained()); + @override bool operator ==(Object other) => other is MapOptions && @@ -290,6 +308,7 @@ class MapOptions { onMapEvent == other.onMapEvent && cameraConstraint == other.cameraConstraint && onMapReady == other.onMapReady && + maxBounds == other.maxBounds && keepAlive == other.keepAlive && interactionOptions == other.interactionOptions; @@ -316,6 +335,7 @@ class MapOptions { cameraConstraint, onMapReady, keepAlive, + maxBounds, interactionOptions, ]); } From 5d8aca1569e9c97b05d4aaa49e82de7d3ae4ff59 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Tue, 27 Jun 2023 16:01:35 +0200 Subject: [PATCH 42/46] Remove duplicate exports and make imports consistent The plugin API no longer exports classes which flutter_map already exports. Imports within this package now import the actual classes they use rather than the whole flutter_map library. Internal code should not depend on the exported library definition. --- lib/flutter_map.dart | 1 + lib/plugin_api.dart | 3 --- lib/src/layer/attribution_layer/simple.dart | 1 - lib/src/layer/overlay_image_layer.dart | 2 +- lib/src/layer/polygon_layer.dart | 2 +- lib/src/layer/tile_layer/tile_coordinates.dart | 2 +- .../tile_provider/network_tile_provider.dart | 4 +++- lib/src/map/map_controller.dart | 8 +++++++- lib/src/misc/position.dart | 2 +- test/flutter_map_controller_test.dart | 4 +++- test/layer/circle_layer_test.dart | 3 ++- test/layer/marker_layer_test.dart | 3 ++- test/layer/polygon_layer_test.dart | 3 ++- test/layer/polyline_layer_test.dart | 3 ++- test/test_utils/test_app.dart | 11 ++++++++++- 15 files changed, 36 insertions(+), 16 deletions(-) diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index 65aad27ec..99f6eeab0 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -26,6 +26,7 @@ export 'package:flutter_map/src/layer/tile_layer/tile_provider/file_providers/ti export 'package:flutter_map/src/layer/tile_layer/tile_provider/network_tile_provider.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_update_event.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_update_transformer.dart'; +export 'package:flutter_map/src/map/camera/camera.dart'; export 'package:flutter_map/src/map/camera/camera_constraint.dart'; export 'package:flutter_map/src/map/camera/camera_fit.dart'; export 'package:flutter_map/src/map/map_controller.dart'; diff --git a/lib/plugin_api.dart b/lib/plugin_api.dart index c52513066..628c08fde 100644 --- a/lib/plugin_api.dart +++ b/lib/plugin_api.dart @@ -1,8 +1,5 @@ library flutter_map.plugin_api; export 'package:flutter_map/flutter_map.dart'; -export 'package:flutter_map/src/map/camera/camera.dart'; -export 'package:flutter_map/src/map/map_controller.dart'; export 'package:flutter_map/src/misc/private/bounds.dart'; -export 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; export 'package:flutter_map/src/misc/private/util.dart'; diff --git a/lib/src/layer/attribution_layer/simple.dart b/lib/src/layer/attribution_layer/simple.dart index e81163e54..19465dd57 100644 --- a/lib/src/layer/attribution_layer/simple.dart +++ b/lib/src/layer/attribution_layer/simple.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; /// A simple, classic style, attribution widget, to be placed in /// [FlutterMap.nonRotatedChildren] diff --git a/lib/src/layer/overlay_image_layer.dart b/lib/src/layer/overlay_image_layer.dart index 6eb45e7d4..2099ccb7f 100644 --- a/lib/src/layer/overlay_image_layer.dart +++ b/lib/src/layer/overlay_image_layer.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/src/layer/polygon_layer.dart b/lib/src/layer/polygon_layer.dart index d41166efd..9044f732e 100644 --- a/lib/src/layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer.dart @@ -1,7 +1,7 @@ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/layer/label.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:latlong2/latlong.dart' hide Path; // conflict with Path from UI diff --git a/lib/src/layer/tile_layer/tile_coordinates.dart b/lib/src/layer/tile_layer/tile_coordinates.dart index 61929b6f4..35670fbc6 100644 --- a/lib/src/layer/tile_layer/tile_coordinates.dart +++ b/lib/src/layer/tile_layer/tile_coordinates.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/misc/point.dart'; class TileCoordinates extends CustomPoint { final int z; diff --git a/lib/src/layer/tile_layer/tile_provider/network_tile_provider.dart b/lib/src/layer/tile_layer/tile_provider/network_tile_provider.dart index 420e9b256..2bf1c62ec 100644 --- a/lib/src/layer/tile_layer/tile_provider/network_tile_provider.dart +++ b/lib/src/layer/tile_layer/tile_provider/network_tile_provider.dart @@ -1,5 +1,7 @@ import 'package:flutter/rendering.dart'; -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_layer.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_provider/base_tile_provider.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_provider/network_image_provider.dart'; import 'package:http/http.dart'; import 'package:http/retry.dart'; diff --git a/lib/src/map/map_controller.dart b/lib/src/map/map_controller.dart index cf75deb80..e29cecc0d 100644 --- a/lib/src/map/map_controller.dart +++ b/lib/src/map/map_controller.dart @@ -1,10 +1,16 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/geo/latlng_bounds.dart'; +import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; +import 'package:flutter_map/src/map/camera/camera_fit.dart'; import 'package:flutter_map/src/map/inherited_model.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; +import 'package:flutter_map/src/misc/center_zoom.dart'; +import 'package:flutter_map/src/misc/fit_bounds_options.dart'; +import 'package:flutter_map/src/misc/move_and_rotate_result.dart'; +import 'package:flutter_map/src/misc/point.dart'; import 'package:latlong2/latlong.dart'; /// Controller to programmatically interact with [FlutterMap], such as diff --git a/lib/src/misc/position.dart b/lib/src/misc/position.dart index 6f9bff191..adb88404a 100644 --- a/lib/src/misc/position.dart +++ b/lib/src/misc/position.dart @@ -1,4 +1,4 @@ -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:latlong2/latlong.dart'; class MapPosition { diff --git a/test/flutter_map_controller_test.dart b/test/flutter_map_controller_test.dart index 42f237d7b..cfaeccb39 100644 --- a/test/flutter_map_controller_test.dart +++ b/test/flutter_map_controller_test.dart @@ -1,5 +1,7 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/geo/latlng_bounds.dart'; +import 'package:flutter_map/src/map/camera/camera_fit.dart'; +import 'package:flutter_map/src/map/map_controller.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; diff --git a/test/layer/circle_layer_test.dart b/test/layer/circle_layer_test.dart index ac8194dfc..3a2a30d42 100644 --- a/test/layer/circle_layer_test.dart +++ b/test/layer/circle_layer_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/circle_layer.dart'; +import 'package:flutter_map/src/map/widget.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; diff --git a/test/layer/marker_layer_test.dart b/test/layer/marker_layer_test.dart index 825b5294a..0047edef9 100644 --- a/test/layer/marker_layer_test.dart +++ b/test/layer/marker_layer_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/marker_layer.dart'; +import 'package:flutter_map/src/map/widget.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; diff --git a/test/layer/polygon_layer_test.dart b/test/layer/polygon_layer_test.dart index ac55b4684..4291039f5 100644 --- a/test/layer/polygon_layer_test.dart +++ b/test/layer/polygon_layer_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/polygon_layer.dart'; +import 'package:flutter_map/src/map/widget.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; diff --git a/test/layer/polyline_layer_test.dart b/test/layer/polyline_layer_test.dart index 9fdf43f44..e2290ae70 100644 --- a/test/layer/polyline_layer_test.dart +++ b/test/layer/polyline_layer_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/polyline_layer.dart'; +import 'package:flutter_map/src/map/widget.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; diff --git a/test/test_utils/test_app.dart b/test/test_utils/test_app.dart index e2d01e48c..ca0fc6684 100644 --- a/test/test_utils/test_app.dart +++ b/test/test_utils/test_app.dart @@ -1,7 +1,16 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/circle_layer.dart'; +import 'package:flutter_map/src/layer/marker_layer.dart'; +import 'package:flutter_map/src/layer/polygon_layer.dart'; +import 'package:flutter_map/src/layer/polyline_layer.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_layer.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_provider/base_tile_provider.dart'; +import 'package:flutter_map/src/map/map_controller.dart'; +import 'package:flutter_map/src/map/options.dart'; +import 'package:flutter_map/src/map/widget.dart'; import 'package:latlong2/latlong.dart'; class TestApp extends StatelessWidget { From f986d77127acad4415bb25e9323055ba0306aa56 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Wed, 5 Jul 2023 09:36:00 +0200 Subject: [PATCH 43/46] Remove deprecated AnchorAlign --- lib/src/layer/marker_layer.dart | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/src/layer/marker_layer.dart b/lib/src/layer/marker_layer.dart index 4a970e171..58bf9e7d0 100644 --- a/lib/src/layer/marker_layer.dart +++ b/lib/src/layer/marker_layer.dart @@ -62,15 +62,7 @@ enum AnchorAlign { left(-1, 0), /// Right center - right(1, 0), - - @Deprecated( - 'Prefer `center`. ' - 'This value is equivalent to the `center` alignment. ' - 'If you notice a difference in behaviour, please open a bug report on GitHub. ' - 'This feature is deprecated since v5.', - ) - none(0, 0); + right(1, 0); final int _x; final int _y; From f5d6d136882e35187817de90ee95b4318eefe9da Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Wed, 5 Jul 2023 09:36:08 +0200 Subject: [PATCH 44/46] Add deprecation for nonrotatedSize --- lib/src/map/camera/camera.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/src/map/camera/camera.dart b/lib/src/map/camera/camera.dart index 87766f4f2..3b7ed9a97 100644 --- a/lib/src/map/camera/camera.dart +++ b/lib/src/map/camera/camera.dart @@ -35,6 +35,13 @@ class MapCamera { /// value in radians. final double rotation; + @Deprecated( + 'Prefer `nonRotatedSize`. ' + 'This getter has been changed to fix the capitalization. ' + 'This getter is deprecated since v6.', + ) + CustomPoint get nonrotatedSize => nonRotatedSize; + /// The size of the map view ignoring rotation. This will be the size of the /// FlutterMap widget. final CustomPoint nonRotatedSize; From 150213eefac88204d29907287d44e0a9b7df8528 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Wed, 5 Jul 2023 10:09:32 +0200 Subject: [PATCH 45/46] Fix deprecation --- lib/src/misc/fit_bounds_options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/misc/fit_bounds_options.dart b/lib/src/misc/fit_bounds_options.dart index f8bd31629..3ce8348a4 100644 --- a/lib/src/misc/fit_bounds_options.dart +++ b/lib/src/misc/fit_bounds_options.dart @@ -11,7 +11,7 @@ class FitBoundsOptions { final bool forceIntegerZoomLevel; @Deprecated( - 'Prefer `FitCamera.bounds` instead. ' + 'Prefer `CameraFit.bounds` instead. ' 'This class has been renamed to clarify its meaning and is now a sublass of CameraFit to allow other fit types. ' 'This class is deprecated since v6.', ) From c32772346502f49a8402718b64bb57e97e2aceb2 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Thu, 6 Jul 2023 09:14:32 +0200 Subject: [PATCH 46/46] Set default CameraFit maxZoom values to null The other parameters for CameraFit all have default values which will not cause changes to the calculated CameraFit (i.e. padding is zero, forceIntegerZoomLevel is false). Setting maxZoom to null makes it consistent with the other parameters in that it will not affect the calculated CameraFit. I chose null over double.infinity as in my opinion the intent is clearer, no maximum. --- lib/src/map/camera/camera_fit.dart | 48 +++++++++++++++++------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/lib/src/map/camera/camera_fit.dart b/lib/src/map/camera/camera_fit.dart index d6a8ca1d1..48c58d651 100644 --- a/lib/src/map/camera/camera_fit.dart +++ b/lib/src/map/camera/camera_fit.dart @@ -24,7 +24,7 @@ abstract class CameraFit { const factory CameraFit.bounds({ required LatLngBounds bounds, EdgeInsets padding, - double maxZoom, + double? maxZoom, bool forceIntegerZoomLevel, }) = FitBounds._; @@ -35,7 +35,7 @@ abstract class CameraFit { const factory CameraFit.insideBounds({ required LatLngBounds bounds, EdgeInsets padding, - double maxZoom, + double? maxZoom, bool forceIntegerZoomLevel, }) = FitInsideBounds._; @@ -51,7 +51,7 @@ abstract class CameraFit { const factory CameraFit.coordinates({ required List coordinates, EdgeInsets padding, - double maxZoom, + double? maxZoom, bool forceIntegerZoomLevel, }) = FitCoordinates._; @@ -68,10 +68,10 @@ class FitBounds extends CameraFit { /// Defaults to [EdgeInsets.zero]. final EdgeInsets padding; - /// Limits the maximum zoom level of the resulting fit. + /// Limits the maximum zoom level of the resulting fit if set. /// - /// Defaults to zoom level 18. - final double maxZoom; + /// Defaults to null. + final double? maxZoom; /// Whether the zoom level of the resulting fit should be rounded to the /// nearest integer level. @@ -82,7 +82,7 @@ class FitBounds extends CameraFit { const FitBounds._({ required this.bounds, this.padding = EdgeInsets.zero, - this.maxZoom = 18, + this.maxZoom, this.forceIntegerZoomLevel = false, }); @@ -95,7 +95,7 @@ class FitBounds extends CameraFit { final paddingTotalXY = paddingTL + paddingBR; var newZoom = _getBoundsZoom(camera, paddingTotalXY); - newZoom = math.min(maxZoom, newZoom); + if (maxZoom != null) newZoom = math.min(maxZoom!, newZoom); final paddingOffset = (paddingBR - paddingTL) / 2; final swPoint = camera.project(bounds.southWest, newZoom); @@ -125,7 +125,10 @@ class FitBounds extends CameraFit { CustomPoint pixelPadding, ) { final min = camera.minZoom ?? 0.0; - final max = camera.maxZoom ?? double.infinity; + final max = math.min( + camera.maxZoom ?? double.infinity, + maxZoom ?? double.infinity, + ); final nw = bounds.northWest; final se = bounds.southEast; var size = camera.nonRotatedSize - pixelPadding; @@ -167,10 +170,10 @@ class FitInsideBounds extends CameraFit { /// Defaults to [EdgeInsets.zero]. final EdgeInsets padding; - /// Limits the maximum zoom level of the resulting fit. + /// Limits the maximum zoom level of the resulting fit if set. /// - /// Defaults to zoom level 18. - final double maxZoom; + /// Defaults to null. + final double? maxZoom; /// Whether the zoom level of the resulting fit should be rounded to the /// nearest integer level. @@ -181,7 +184,7 @@ class FitInsideBounds extends CameraFit { const FitInsideBounds._({ required this.bounds, this.padding = EdgeInsets.zero, - this.maxZoom = 18, + this.maxZoom, this.forceIntegerZoomLevel = false, }); @@ -214,9 +217,9 @@ class FitInsideBounds extends CameraFit { } newZoom = math.max( - camera.minZoom ?? 0.0, + camera.minZoom ?? double.negativeInfinity, math.min( - math.min(maxZoom, camera.maxZoom ?? double.infinity), + math.min(maxZoom ?? double.infinity, camera.maxZoom ?? double.infinity), newZoom, ), ); @@ -359,10 +362,10 @@ class FitCoordinates extends CameraFit { /// Defaults to [EdgeInsets.zero]. final EdgeInsets padding; - /// Limits the maximum zoom level of the resulting fit. + /// Limits the maximum zoom level of the resulting fit if set. /// - /// Defaults to zoom level 18. - final double maxZoom; + /// Defaults to null. + final double? maxZoom; /// Whether the zoom level of the resulting fit should be rounded to the /// nearest integer level. @@ -373,7 +376,7 @@ class FitCoordinates extends CameraFit { const FitCoordinates._({ required this.coordinates, this.padding = EdgeInsets.zero, - this.maxZoom = 18, + this.maxZoom = double.infinity, this.forceIntegerZoomLevel = false, }); @@ -386,7 +389,7 @@ class FitCoordinates extends CameraFit { final paddingTotalXY = paddingTL + paddingBR; var newZoom = _getCoordinatesZoom(camera, paddingTotalXY); - newZoom = math.min(maxZoom, newZoom); + if (maxZoom != null) newZoom = math.min(maxZoom!, newZoom); final projectedPoints = [ for (final coord in coordinates) camera.project(coord, newZoom) @@ -417,7 +420,10 @@ class FitCoordinates extends CameraFit { CustomPoint pixelPadding, ) { final min = camera.minZoom ?? 0.0; - final max = camera.maxZoom ?? double.infinity; + final max = math.min( + camera.maxZoom ?? double.infinity, + maxZoom ?? double.infinity, + ); var size = camera.nonRotatedSize - pixelPadding; // Prevent negative size which results in NaN zoom value later on in the calculation size = CustomPoint(math.max(0, size.x), math.max(0, size.y));