diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart index 7ca650dceb7..6fdc126a3d7 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart @@ -62,3 +62,5 @@ export 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf part 'src/controller.dart'; part 'src/google_map.dart'; +// MOD imperative map updates (added the line below) +part 'src/mod_imperative_map_updates.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart index 80f019d5a00..52951b928ec 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart @@ -37,6 +37,11 @@ class GoogleMapController { } final _GoogleMapState _googleMapState; + // MOD imperative map updates (added those 4 fields) + late final _GoogleMapStateMarkersUpdateHandler _markersUpdateHandler = _GoogleMapStateMarkersUpdateHandler(this); + late final _GoogleMapStatePolylinesUpdateHandler _polylinesUpdateHandler = _GoogleMapStatePolylinesUpdateHandler(this); + late final _GoogleMapStatePolygonsUpdateHandler _polygonsUpdateHandler = _GoogleMapStatePolygonsUpdateHandler(this); + late final _GoogleMapStateCirclesUpdateHandler _circlesUpdateHandler = _GoogleMapStateCirclesUpdateHandler(this); void _connectStreams(int mapId) { if (_googleMapState.widget.onCameraMoveStarted != null) { diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart index 44f6dd5d812..7b1950ea979 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart @@ -460,10 +460,11 @@ class _GoogleMapState extends State { _updateOptions(controller); _updateClusterManagers(controller); - _updateMarkers(controller); - _updatePolygons(controller); - _updatePolylines(controller); - _updateCircles(controller); + // MOD imperative map updates (commented the lines below) + //_updateMarkers(controller); + //_updatePolygons(controller); + //_updatePolylines(controller); + //_updateCircles(controller); _updateHeatmaps(controller); _updateTileOverlays(controller); _updateGroundOverlays(controller); diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/mod_imperative_map_updates.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/mod_imperative_map_updates.dart new file mode 100644 index 00000000000..e5b0b9e6a6d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/mod_imperative_map_updates.dart @@ -0,0 +1,399 @@ +/// Attempts to centralize Google Maps customizations, however some other files had to be modified, they are marked +/// with the following in-code comment: +/// // MOD imperative map updates + +part of '../google_maps_flutter.dart'; + +abstract class _GoogleMapStateUpdateHandler { + _GoogleMapStateUpdateHandler(this._mapController); + + // The max number of expensive updates that can be done within a frame's time per _GoogleMapStateUpdateHandler + // There are currently 4 (marker, polygon, polyline, circle) implementations of _GoogleMapStateUpdateHandler so the + // number of expensive operations happening within a frame will be capped at + // 4 x _MAX_EXPENSIVE_UPDATES_ITEMS_COUNT_PER_FRAME. + static const int _MAX_EXPENSIVE_UPDATES_ITEMS_COUNT_PER_FRAME = 2; + static const int _FRAME_DURATION_MILLIS = 16; + final GoogleMapController _mapController; + final List _queuedIdsToRemove = []; + final List _queuedItemsToAdd = []; + final List _queuedItemsToChange = []; + // We want this to be an instance class, since there can be multiple child instances of _GoogleMapStateUpdateHandler. + // If this becomes a performance issue, we can make this global somehow. + bool _isRunningUpdates = false; + + U _buildIncrementalUpdates(Set oldItems, Set newItems); + U _buildRACUpdates(Set idsToRemove, Set itemsToAdd, Set itemsToChange) { + final U updates = _buildIncrementalUpdates({}, {}); + _getIdsToRemove(updates).addAll(idsToRemove); + _getItemsToAdd(updates).addAll(itemsToAdd); + _getItemsToChange(updates).addAll(itemsToChange); + return updates; + } + + Set _getIdsToRemove(U updates); + Set _getItemsToAdd(U updates); + Set _getItemsToChange(U updates); + I _getItemId(E item); + + Map _getUpdatedItemsMap( + Map initialValue, + Set idsToRemove, + Set itemsToAdd, + Set itemsToChange, + ) { + final Map updatedItems = {}; + for (final MapEntry entry in initialValue.entries) { + if (!idsToRemove.contains(entry.key)) { + updatedItems[entry.key] = entry.value; + } + } + for (final E marker in itemsToAdd) { + updatedItems[_getItemId(marker)] = marker; + } + for (final E marker in itemsToChange) { + updatedItems[_getItemId(marker)] = marker; + } + return updatedItems; + } + + @nonVirtual + Future apply( + Set oldItems, + Set newItems, { + bool realTime = false, + }) async { + final U iUpdates = _buildIncrementalUpdates(oldItems, newItems); + final Set idsToRemove = _getIdsToRemove(iUpdates); + final Set itemsToAdd = _getItemsToAdd(iUpdates); + final Set itemsToChange = _getItemsToChange(iUpdates); + if (oldItems.isEmpty && newItems.isEmpty && idsToRemove.isEmpty && itemsToAdd.isEmpty && itemsToChange.isEmpty) { + return; + } + if (realTime) { + await _applyIncrementalUpdates( + _buildRACUpdates(idsToRemove, itemsToAdd, itemsToChange), + idsToRemove, + itemsToAdd, + itemsToChange, + ); + } else { + await _queueUpdates(idsToRemove, itemsToAdd, itemsToChange); + } + } + + Future _queueUpdates( + Set idsToRemove, + Set itemsToAdd, + Set itemsToChange, + ) async { + // Removing any queued add/change ops that will be negated by a remove op right after + if (idsToRemove.isNotEmpty) { + bool removeWhereFunc(E item) => idsToRemove.contains(_getItemId(item)); + _queuedItemsToAdd.removeWhere(removeWhereFunc); + itemsToChange.removeWhere(removeWhereFunc); + } + // Removing any queued change ops that will be further changed by a more recent change op right after + if (itemsToChange.isNotEmpty) { + final Set idsToChange = itemsToChange.map((E e) => _getItemId(e)).toSet(); + _queuedItemsToChange.removeWhere((E e) => idsToChange.contains(_getItemId(e))); + } + _queuedIdsToRemove.addAll(idsToRemove); + _queuedItemsToAdd.addAll(itemsToAdd); + _queuedItemsToChange.addAll(itemsToChange); + if (_isRunningUpdates) { + return; + } + _isRunningUpdates = true; + try { + while (_queuedIdsToRemove.isNotEmpty || _queuedItemsToAdd.isNotEmpty || _queuedItemsToChange.isNotEmpty) { + await _processQueuedUpdates(); + } + } finally { + _isRunningUpdates = false; + } + } + + Future _processQueuedUpdates() async { + // Remove operations are not expensive, hence the whole queue is added at once + final Set idsToRemove = {..._queuedIdsToRemove}; + _queuedIdsToRemove.clear(); + int expensiveOpsCount = 0; + final Set itemsToAdd = {}; + while (expensiveOpsCount < _MAX_EXPENSIVE_UPDATES_ITEMS_COUNT_PER_FRAME && _queuedItemsToAdd.isNotEmpty) { + final E itemToAdd = _queuedItemsToAdd.first; + itemsToAdd.add(itemToAdd); + _queuedItemsToAdd.remove(itemToAdd); + expensiveOpsCount++; + } + final Set itemsToChange = {}; + while (expensiveOpsCount < _MAX_EXPENSIVE_UPDATES_ITEMS_COUNT_PER_FRAME && _queuedItemsToChange.isNotEmpty) { + final E itemToChange = _queuedItemsToChange.first; + itemsToChange.add(itemToChange); + _queuedItemsToChange.remove(itemToChange); + expensiveOpsCount++; + } + await _applyIncrementalUpdates( + _buildRACUpdates(idsToRemove, itemsToAdd, itemsToChange), + idsToRemove, + itemsToAdd, + itemsToChange, + ); + await Future.delayed(const Duration(milliseconds: _FRAME_DURATION_MILLIS)); + } + + Future _applyIncrementalUpdates( + U updates, + Set idsToRemove, + Set itemsToAdd, + Set itemsToChange, + ); +} + +class _GoogleMapStateMarkersUpdateHandler extends _GoogleMapStateUpdateHandler { + _GoogleMapStateMarkersUpdateHandler(super.mapController); + + @override + MarkerUpdates _buildIncrementalUpdates(Set oldItems, Set newItems) { + return MarkerUpdates.from(oldItems, newItems); + } + + @override + Set _getIdsToRemove(MarkerUpdates updates) => updates.markerIdsToRemove; + + @override + Set _getItemsToAdd(MarkerUpdates updates) => updates.markersToAdd; + + @override + Set _getItemsToChange(MarkerUpdates updates) => updates.markersToChange; + + @override + MarkerId _getItemId(Marker item) => item.markerId; + + @override + Future _applyIncrementalUpdates( + MarkerUpdates updates, + Set idsToRemove, + Set itemsToAdd, + Set itemsToChange, + ) async { + await _mapController._updateMarkers(updates); + _mapController._googleMapState._markers = _getUpdatedItemsMap( + _mapController._googleMapState._markers, + idsToRemove, + itemsToAdd, + itemsToChange, + ); + } +} + +class _GoogleMapStatePolylinesUpdateHandler + extends _GoogleMapStateUpdateHandler { + _GoogleMapStatePolylinesUpdateHandler(super.mapController); + + @override + PolylineUpdates _buildIncrementalUpdates(Set oldItems, Set newItems) { + return PolylineUpdates.from(oldItems, newItems); + } + + @override + Set _getIdsToRemove(PolylineUpdates updates) => updates.polylineIdsToRemove; + + @override + Set _getItemsToAdd(PolylineUpdates updates) => updates.polylinesToAdd; + + @override + Set _getItemsToChange(PolylineUpdates updates) => updates.polylinesToChange; + + @override + PolylineId _getItemId(Polyline item) => item.polylineId; + + @override + Future _applyIncrementalUpdates( + PolylineUpdates updates, + Set idsToRemove, + Set itemsToAdd, + Set itemsToChange, + ) async { + await _mapController._updatePolylines(updates); + _mapController._googleMapState._polylines = _getUpdatedItemsMap( + _mapController._googleMapState._polylines, + idsToRemove, + itemsToAdd, + itemsToChange, + ); + } +} + +class _GoogleMapStatePolygonsUpdateHandler extends _GoogleMapStateUpdateHandler { + _GoogleMapStatePolygonsUpdateHandler(super.mapController); + + @override + PolygonUpdates _buildIncrementalUpdates(Set oldItems, Set newItems) { + return PolygonUpdates.from(oldItems, newItems); + } + + @override + Set _getIdsToRemove(PolygonUpdates updates) => updates.polygonIdsToRemove; + + @override + Set _getItemsToAdd(PolygonUpdates updates) => updates.polygonsToAdd; + + @override + Set _getItemsToChange(PolygonUpdates updates) => updates.polygonsToChange; + + @override + PolygonId _getItemId(Polygon item) => item.polygonId; + + @override + Future _applyIncrementalUpdates( + PolygonUpdates updates, + Set idsToRemove, + Set itemsToAdd, + Set itemsToChange, + ) async { + await _mapController._updatePolygons(updates); + _mapController._googleMapState._polygons = _getUpdatedItemsMap( + _mapController._googleMapState._polygons, + idsToRemove, + itemsToAdd, + itemsToChange, + ); + } +} + +class _GoogleMapStateCirclesUpdateHandler extends _GoogleMapStateUpdateHandler { + _GoogleMapStateCirclesUpdateHandler(super.mapController); + + @override + CircleUpdates _buildIncrementalUpdates(Set oldItems, Set newItems) { + return CircleUpdates.from(oldItems, newItems); + } + + @override + Set _getIdsToRemove(CircleUpdates updates) => updates.circleIdsToRemove; + + @override + Set _getItemsToAdd(CircleUpdates updates) => updates.circlesToAdd; + + @override + Set _getItemsToChange(CircleUpdates updates) => updates.circlesToChange; + + @override + CircleId _getItemId(Circle item) => item.circleId; + + @override + Future _applyIncrementalUpdates( + CircleUpdates updates, + Set idsToRemove, + Set itemsToAdd, + Set itemsToChange, + ) async { + await _mapController._updateCircles(updates); + _mapController._googleMapState._circles = _getUpdatedItemsMap( + _mapController._googleMapState._circles, + idsToRemove, + itemsToAdd, + itemsToChange, + ); + } +} + +extension _GoogleMapStateExtension on _GoogleMapState { + MapConfiguration _buildMapConfigurationWithMapType( + MapType mapType, { + required bool indoorViewEnabled, + }) { + return MapConfiguration( + compassEnabled: widget.compassEnabled, + mapToolbarEnabled: widget.mapToolbarEnabled, + cameraTargetBounds: widget.cameraTargetBounds, + mapType: mapType, + minMaxZoomPreference: widget.minMaxZoomPreference, + rotateGesturesEnabled: widget.rotateGesturesEnabled, + scrollGesturesEnabled: widget.scrollGesturesEnabled, + tiltGesturesEnabled: widget.tiltGesturesEnabled, + trackCameraPosition: widget.onCameraMove != null, + zoomControlsEnabled: widget.zoomControlsEnabled, + zoomGesturesEnabled: widget.zoomGesturesEnabled, + liteModeEnabled: widget.liteModeEnabled, + myLocationEnabled: widget.myLocationEnabled, + myLocationButtonEnabled: widget.myLocationButtonEnabled, + padding: widget.padding, + indoorViewEnabled: indoorViewEnabled, + trafficEnabled: widget.trafficEnabled, + buildingsEnabled: widget.buildingsEnabled, + ); + } +} + +/// Extension on the GoogleMapController object allowing to make map object updates imperatively for: +/// - Map camera +/// - Map type +/// - Markers +/// - Polylines +/// - Polygons +/// - Circles +extension GoogleMapControllerImperativeExtension on GoogleMapController { + // The following fields were added to the GoogleMapController class to support this extension: + // late final _GoogleMapStateMarkersUpdateHandler _markersUpdateHandler = _GoogleMapStateMarkersUpdateHandler(this); + // late final _GoogleMapStatePolylinesUpdateHandler _polylinesUpdateHandler = _GoogleMapStatePolylinesUpdateHandler(this); + // late final _GoogleMapStatePolygonsUpdateHandler _polygonsUpdateHandler = _GoogleMapStatePolygonsUpdateHandler(this); + // late final _GoogleMapStateCirclesUpdateHandler _circlesUpdateHandler = _GoogleMapStateCirclesUpdateHandler(this); + + /// Same as moveCamera, except the onCameraMove callback is invoked from this action + Future moveCameraToPosition(CameraPosition cameraPosition) async { + await moveCamera(CameraUpdate.newCameraPosition(cameraPosition)); + _googleMapState.widget.onCameraMove?.call(cameraPosition); + } + + /// Updates the map type used by the GoogleMap widget + Future updateMapType( + MapType mapType, { + required bool indoorViewEnabled, + }) { + return _updateMapConfiguration(_googleMapState._buildMapConfigurationWithMapType( + mapType, + indoorViewEnabled: indoorViewEnabled, + )); + } + + /// Updates a set of markers with a new set of markers. + /// The set of markers can be a partial list markers. + Future partiallyUpdateMarkers( + Set oldMarkers, + Set newMarkers, { + bool realTime = false, + }) async { + await _markersUpdateHandler.apply(oldMarkers, newMarkers, realTime: realTime); + } + + /// Updates a set of polylines with a new set of polylines. + /// The set of polylines can be a partial list polylines. + Future partiallyUpdatePolylines( + Set oldPolylines, + Set newPolylines, { + bool realTime = false, + }) async { + await _polylinesUpdateHandler.apply(oldPolylines, newPolylines, realTime: realTime); + } + + /// Updates a set of polygons with a new set of polygons. + /// The set of polygons can be a partial list polygons. + Future partiallyUpdatePolygons( + Set oldPolygons, + Set newPolygons, { + bool realTime = false, + }) async { + await _polygonsUpdateHandler.apply(oldPolygons, newPolygons, realTime: realTime); + } + + /// Updates a set of circles with a new set of circles. + /// The set of circles can be a partial list circles. + Future partiallyUpdateCircles( + Set oldCircles, + Set newCircles, { + bool realTime = false, + }) async { + await _circlesUpdateHandler.apply(oldCircles, newCircles, realTime: realTime); + } +}