From 244cd4cf9a0b6bfd8ab0a3f04b9eb47e832e854f Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Wed, 7 Aug 2024 15:10:18 +0300 Subject: [PATCH] [google_maps_flutter] Marker clustering support (#4319) Adds the initial support for the marker clustering for Android, iOS and Web platforms. Clustering is implemented using native google maps utils libraries for [Android](https://github.com/googlemaps/android-maps-utils), [iOS](https://github.com/googlemaps/google-maps-ios-utils) and [Web](https://github.com/googlemaps/js-markerclusterer). This PR is created from previous PR: https://github.com/flutter/plugins/pull/6752 Resolves https://github.com/flutter/flutter/issues/26863 **Android**: ![image](https://github.com/flutter/packages/assets/5219613/09b84a8e-f05b-4c71-8808-4043a25201f6) **iOS**: ![image](https://github.com/flutter/packages/assets/5219613/0859cf12-2e8c-4106-b7a7-cd4922a7dd1e) **Web**: ![image](https://github.com/flutter/packages/assets/5219613/9269d22a-1908-4c2c-a1ab-70addb06d0f2) --- .../google_maps_flutter/CHANGELOG.md | 4 + .../integration_test/src/maps_inspector.dart | 102 +++++++ .../example/lib/clustering.dart | 278 ++++++++++++++++++ .../google_maps_flutter/example/lib/main.dart | 2 + .../example/web/index.html | 1 + .../lib/google_maps_flutter.dart | 3 + .../lib/src/controller.dart | 15 + .../lib/src/google_map.dart | 29 ++ .../google_maps_flutter/pubspec.yaml | 4 +- .../test/clustermanager_updates_test.dart | 188 ++++++++++++ .../fake_google_maps_flutter_platform.dart | 18 ++ 11 files changed, 642 insertions(+), 2 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter/example/lib/clustering.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter/test/clustermanager_updates_test.dart diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 4fa7a9b37b98..3696ad836333 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.9.0 + +* Adds clustering support. + ## 2.8.0 * Adds support for heatmap layers. diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_inspector.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_inspector.dart index efa4baaea696..dc6973a5e8ff 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_inspector.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_inspector.dart @@ -533,4 +533,106 @@ void runTests() { expect(myLocationButtonEnabled, true); }); }, skip: !isIOS); + + testWidgets('marker clustering', (WidgetTester tester) async { + final Key key = GlobalKey(); + const int clusterManagersAmount = 2; + const int markersPerClusterManager = 5; + final Map markers = {}; + final Set clusterManagers = {}; + + for (int i = 0; i < clusterManagersAmount; i++) { + final ClusterManagerId clusterManagerId = + ClusterManagerId('cluster_manager_$i'); + final ClusterManager clusterManager = + ClusterManager(clusterManagerId: clusterManagerId); + clusterManagers.add(clusterManager); + } + + for (final ClusterManager cm in clusterManagers) { + for (int i = 0; i < markersPerClusterManager; i++) { + final MarkerId markerId = + MarkerId('${cm.clusterManagerId.value}_marker_$i'); + final Marker marker = Marker( + markerId: markerId, + clusterManagerId: cm.clusterManagerId, + position: LatLng( + kInitialMapCenter.latitude + i, kInitialMapCenter.longitude)); + markers[markerId] = marker; + } + } + + final Completer controllerCompleter = + Completer(); + + await pumpMap( + tester, + GoogleMap( + key: key, + initialCameraPosition: kInitialCameraPosition, + clusterManagers: clusterManagers, + markers: Set.of(markers.values), + onMapCreated: (GoogleMapController googleMapController) { + controllerCompleter.complete(googleMapController); + }, + ), + ); + + final GoogleMapController controller = await controllerCompleter.future; + + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + for (final ClusterManager cm in clusterManagers) { + final List clusters = await inspector.getClusters( + mapId: controller.mapId, clusterManagerId: cm.clusterManagerId); + final int markersAmountForClusterManager = clusters + .map((Cluster cluster) => cluster.count) + .reduce((int value, int element) => value + element); + expect(markersAmountForClusterManager, markersPerClusterManager); + } + + // Remove markers from clusterManagers and test that clusterManagers are empty. + for (final MapEntry entry in markers.entries) { + markers[entry.key] = _copyMarkerWithClusterManagerId(entry.value, null); + } + + await pumpMap( + tester, + GoogleMap( + key: key, + initialCameraPosition: kInitialCameraPosition, + clusterManagers: clusterManagers, + markers: Set.of(markers.values)), + ); + + for (final ClusterManager cm in clusterManagers) { + final List clusters = await inspector.getClusters( + mapId: controller.mapId, clusterManagerId: cm.clusterManagerId); + expect(clusters.length, 0); + } + }); +} + +Marker _copyMarkerWithClusterManagerId( + Marker marker, ClusterManagerId? clusterManagerId) { + return Marker( + markerId: marker.markerId, + alpha: marker.alpha, + anchor: marker.anchor, + consumeTapEvents: marker.consumeTapEvents, + draggable: marker.draggable, + flat: marker.flat, + icon: marker.icon, + infoWindow: marker.infoWindow, + position: marker.position, + rotation: marker.rotation, + visible: marker.visible, + zIndex: marker.zIndex, + onTap: marker.onTap, + onDragStart: marker.onDragStart, + onDrag: marker.onDrag, + onDragEnd: marker.onDragEnd, + clusterManagerId: clusterManagerId, + ); } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/clustering.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/clustering.dart new file mode 100644 index 000000000000..5c06a61b2a1e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/clustering.dart @@ -0,0 +1,278 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'page.dart'; + +/// Page for demonstrating marker clustering support. +class ClusteringPage extends GoogleMapExampleAppPage { + /// Default Constructor. + const ClusteringPage({Key? key}) + : super(const Icon(Icons.place), 'Manage clustering', key: key); + + @override + Widget build(BuildContext context) { + return const ClusteringBody(); + } +} + +/// Body of the clustering page. +class ClusteringBody extends StatefulWidget { + /// Default Constructor. + const ClusteringBody({super.key}); + + @override + State createState() => ClusteringBodyState(); +} + +/// State of the clustering page. +class ClusteringBodyState extends State { + /// Default Constructor. + ClusteringBodyState(); + + /// Starting point from where markers are added. + static const LatLng center = LatLng(-33.86, 151.1547171); + + /// Marker offset factor for randomizing marker placing. + static const double _markerOffsetFactor = 0.05; + + /// Offset for longitude when placing markers to different cluster managers. + static const double _clusterManagerLongitudeOffset = 0.1; + + /// Maximum amount of cluster managers. + static const int _clusterManagerMaxCount = 3; + + /// Amount of markers to be added to the cluster manager at once. + static const int _markersToAddToClusterManagerCount = 10; + + /// Fully visible alpha value. + static const double _fullyVisibleAlpha = 1.0; + + /// Half visible alpha value. + static const double _halfVisibleAlpha = 0.5; + + /// Google map controller. + GoogleMapController? controller; + + /// Map of clusterManagers with identifier as the key. + Map clusterManagers = + {}; + + /// Map of markers with identifier as the key. + Map markers = {}; + + /// Id of the currently selected marker. + MarkerId? selectedMarker; + + /// Counter for added cluster manager ids. + int _clusterManagerIdCounter = 1; + + /// Counter for added markers ids. + int _markerIdCounter = 1; + + /// Cluster that was tapped most recently. + Cluster? lastCluster; + + void _onMapCreated(GoogleMapController controllerParam) { + setState(() { + controller = controllerParam; + }); + } + + @override + void dispose() { + super.dispose(); + } + + void _onMarkerTapped(MarkerId markerId) { + final Marker? tappedMarker = markers[markerId]; + if (tappedMarker != null) { + setState(() { + final MarkerId? previousMarkerId = selectedMarker; + if (previousMarkerId != null && markers.containsKey(previousMarkerId)) { + final Marker resetOld = markers[previousMarkerId]! + .copyWith(iconParam: BitmapDescriptor.defaultMarker); + markers[previousMarkerId] = resetOld; + } + selectedMarker = markerId; + final Marker newMarker = tappedMarker.copyWith( + iconParam: BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueGreen, + ), + ); + markers[markerId] = newMarker; + }); + } + } + + void _addClusterManager() { + if (clusterManagers.length == _clusterManagerMaxCount) { + return; + } + + final String clusterManagerIdVal = + 'cluster_manager_id_$_clusterManagerIdCounter'; + _clusterManagerIdCounter++; + final ClusterManagerId clusterManagerId = + ClusterManagerId(clusterManagerIdVal); + + final ClusterManager clusterManager = ClusterManager( + clusterManagerId: clusterManagerId, + onClusterTap: (Cluster cluster) => setState(() { + lastCluster = cluster; + }), + ); + + setState(() { + clusterManagers[clusterManagerId] = clusterManager; + }); + _addMarkersToCluster(clusterManager); + } + + void _removeClusterManager(ClusterManager clusterManager) { + setState(() { + // Remove markers managed by cluster manager to be removed. + markers.removeWhere((MarkerId key, Marker marker) => + marker.clusterManagerId == clusterManager.clusterManagerId); + // Remove cluster manager. + clusterManagers.remove(clusterManager.clusterManagerId); + }); + } + + void _addMarkersToCluster(ClusterManager clusterManager) { + for (int i = 0; i < _markersToAddToClusterManagerCount; i++) { + final String markerIdVal = + '${clusterManager.clusterManagerId.value}_marker_id_$_markerIdCounter'; + _markerIdCounter++; + final MarkerId markerId = MarkerId(markerIdVal); + + final int clusterManagerIndex = + clusterManagers.values.toList().indexOf(clusterManager); + + // Add additional offset to longitude for each cluster manager to space + // out markers in different cluster managers. + final double clusterManagerLongitudeOffset = + clusterManagerIndex * _clusterManagerLongitudeOffset; + + final Marker marker = Marker( + clusterManagerId: clusterManager.clusterManagerId, + markerId: markerId, + position: LatLng( + center.latitude + _getRandomOffset(), + center.longitude + _getRandomOffset() + clusterManagerLongitudeOffset, + ), + infoWindow: InfoWindow(title: markerIdVal, snippet: '*'), + onTap: () => _onMarkerTapped(markerId), + ); + markers[markerId] = marker; + } + setState(() {}); + } + + double _getRandomOffset() { + return (Random().nextDouble() - 0.5) * _markerOffsetFactor; + } + + void _remove(MarkerId markerId) { + setState(() { + if (markers.containsKey(markerId)) { + markers.remove(markerId); + } + }); + } + + void _changeMarkersAlpha() { + for (final MarkerId markerId in markers.keys) { + final Marker marker = markers[markerId]!; + final double current = marker.alpha; + markers[markerId] = marker.copyWith( + alphaParam: current == _fullyVisibleAlpha + ? _halfVisibleAlpha + : _fullyVisibleAlpha, + ); + } + setState(() {}); + } + + @override + Widget build(BuildContext context) { + final MarkerId? selectedId = selectedMarker; + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SizedBox( + height: 300.0, + child: GoogleMap( + onMapCreated: _onMapCreated, + initialCameraPosition: const CameraPosition( + target: LatLng(-33.852, 151.25), + zoom: 11.0, + ), + markers: Set.of(markers.values), + clusterManagers: Set.of(clusterManagers.values), + ), + ), + Column(children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: clusterManagers.length >= _clusterManagerMaxCount + ? null + : () => _addClusterManager(), + child: const Text('Add cluster manager'), + ), + TextButton( + onPressed: clusterManagers.isEmpty + ? null + : () => _removeClusterManager(clusterManagers.values.last), + child: const Text('Remove cluster manager'), + ), + ], + ), + Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + for (final MapEntry clusterEntry + in clusterManagers.entries) + TextButton( + onPressed: () => _addMarkersToCluster(clusterEntry.value), + child: Text('Add markers to ${clusterEntry.key.value}'), + ), + ], + ), + Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: selectedId == null + ? null + : () { + _remove(selectedId); + setState(() { + selectedMarker = null; + }); + }, + child: const Text('Remove selected marker'), + ), + TextButton( + onPressed: markers.isEmpty ? null : () => _changeMarkersAlpha(), + child: const Text('Change all markers alpha'), + ), + ], + ), + if (lastCluster != null) + Padding( + padding: const EdgeInsets.all(10), + child: Text( + 'Cluster with ${lastCluster!.count} markers clicked at ${lastCluster!.position}')), + ]), + ], + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart index e000e22d2e81..73a47db723e8 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart @@ -9,6 +9,7 @@ import 'package:google_maps_flutter_android/google_maps_flutter_android.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'animate_camera.dart'; +import 'clustering.dart'; import 'heatmap.dart'; import 'lite_mode.dart'; import 'map_click.dart'; @@ -43,6 +44,7 @@ final List _allPages = [ const SnapshotPage(), const LiteModePage(), const TileOverlayPage(), + const ClusteringPage(), const MapIdPage(), const HeatmapPage(), ]; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/web/index.html b/packages/google_maps_flutter/google_maps_flutter/example/web/index.html index 62869e8931fc..e9ce66f9a870 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/web/index.html +++ b/packages/google_maps_flutter/google_maps_flutter/example/web/index.html @@ -38,6 +38,7 @@ + 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 a92e6ba6a0ff..5c3bb49133c8 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 @@ -27,6 +27,9 @@ export 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf Cap, Circle, CircleId, + Cluster, + ClusterManager, + ClusterManagerId, Heatmap, HeatmapGradient, HeatmapGradientColor, 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 9cea738dbc3d..5dd1cfdfd2ad 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 @@ -79,6 +79,9 @@ class GoogleMapController { .listen((MapTapEvent e) => _googleMapState.onTap(e.position)); GoogleMapsFlutterPlatform.instance.onLongPress(mapId: mapId).listen( (MapLongPressEvent e) => _googleMapState.onLongPress(e.position)); + GoogleMapsFlutterPlatform.instance + .onClusterTap(mapId: mapId) + .listen((ClusterTapEvent e) => _googleMapState.onClusterTap(e.value)); } /// Updates configuration options of the map user interface. @@ -103,6 +106,18 @@ class GoogleMapController { .updateMarkers(markerUpdates, mapId: mapId); } + /// Updates cluster manager configuration. + /// + /// Change listeners are notified once the update has been made on the + /// platform side. + /// + /// The returned [Future] completes after listeners have been notified. + Future _updateClusterManagers( + ClusterManagerUpdates clusterManagerUpdates) { + return GoogleMapsFlutterPlatform.instance + .updateClusterManagers(clusterManagerUpdates, mapId: mapId); + } + /// Updates polygon configuration. /// /// Change listeners are notified once the update has been made on the 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 bd1b0086358a..d93a9c9cc1af 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 @@ -120,6 +120,7 @@ class GoogleMap extends StatefulWidget { this.polygons = const {}, this.polylines = const {}, this.circles = const {}, + this.clusterManagers = const {}, this.heatmaps = const {}, this.onCameraMoveStarted, this.tileOverlays = const {}, @@ -221,6 +222,9 @@ class GoogleMap extends StatefulWidget { /// Tile overlays to be placed on the map. final Set tileOverlays; + /// Cluster Managers to be initialized for the map. + final Set clusterManagers; + /// Called when the camera starts moving. /// /// This can be initiated by the following: @@ -332,6 +336,8 @@ class _GoogleMapState extends State { Map _polygons = {}; Map _polylines = {}; Map _circles = {}; + Map _clusterManagers = + {}; Map _heatmaps = {}; late MapConfiguration _mapConfiguration; @@ -352,6 +358,7 @@ class _GoogleMapState extends State { polygons: widget.polygons, polylines: widget.polylines, circles: widget.circles, + clusterManagers: widget.clusterManagers, heatmaps: widget.heatmaps, ), mapConfiguration: _mapConfiguration, @@ -362,6 +369,7 @@ class _GoogleMapState extends State { void initState() { super.initState(); _mapConfiguration = _configurationFromMapWidget(widget); + _clusterManagers = keyByClusterManagerId(widget.clusterManagers); _markers = keyByMarkerId(widget.markers); _polygons = keyByPolygonId(widget.polygons); _polylines = keyByPolylineId(widget.polylines); @@ -384,6 +392,7 @@ class _GoogleMapState extends State { void didUpdateWidget(GoogleMap oldWidget) { super.didUpdateWidget(oldWidget); _updateOptions(); + _updateClusterManagers(); _updateMarkers(); _updatePolygons(); _updatePolylines(); @@ -410,6 +419,13 @@ class _GoogleMapState extends State { _markers = keyByMarkerId(widget.markers); } + Future _updateClusterManagers() async { + final GoogleMapController controller = await _controller.future; + unawaited(controller._updateClusterManagers(ClusterManagerUpdates.from( + _clusterManagers.values.toSet(), widget.clusterManagers))); + _clusterManagers = keyByClusterManagerId(widget.clusterManagers); + } + Future _updatePolygons() async { final GoogleMapController controller = await _controller.future; unawaited(controller._updatePolygons( @@ -561,6 +577,19 @@ class _GoogleMapState extends State { onLongPress(position); } } + + void onClusterTap(Cluster cluster) { + final ClusterManager? clusterManager = + _clusterManagers[cluster.clusterManagerId]; + if (clusterManager == null) { + throw UnknownMapObjectIdError( + 'clusterManager', cluster.clusterManagerId, 'onClusterTap'); + } + final ArgumentCallback? onClusterTap = clusterManager.onClusterTap; + if (onClusterTap != null) { + onClusterTap(cluster); + } + } } /// Builds a [MapConfiguration] from the given [map]. diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 413ad8421595..00a533874875 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.8.0 +version: 2.9.0 environment: sdk: ^3.4.0 @@ -22,7 +22,7 @@ dependencies: flutter: sdk: flutter google_maps_flutter_android: ^2.13.0 - google_maps_flutter_ios: ^2.11.0 + google_maps_flutter_ios: ^2.12.0 google_maps_flutter_platform_interface: ^2.9.0 google_maps_flutter_web: ^0.5.10 diff --git a/packages/google_maps_flutter/google_maps_flutter/test/clustermanager_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/clustermanager_updates_test.dart new file mode 100644 index 000000000000..fee29e1fb67b --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/test/clustermanager_updates_test.dart @@ -0,0 +1,188 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'fake_google_maps_flutter_platform.dart'; + +Widget _mapWithClusterManagers(Set clusterManagers) { + return Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + clusterManagers: clusterManagers, + ), + ); +} + +void main() { + late FakeGoogleMapsFlutterPlatform platform; + + setUp(() { + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; + }); + + testWidgets('Initializing a cluster manager', (WidgetTester tester) async { + const ClusterManager cm1 = ClusterManager( + clusterManagerId: ClusterManagerId('cm_1'), + ); + await tester.pumpWidget(_mapWithClusterManagers({cm1})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.clusterManagerUpdates.last.clusterManagersToAdd.length, 1); + + final ClusterManager initializedHeatmap = + map.clusterManagerUpdates.last.clusterManagersToAdd.first; + expect(initializedHeatmap, equals(cm1)); + expect( + map.clusterManagerUpdates.last.clusterManagerIdsToRemove.isEmpty, true); + expect( + map.clusterManagerUpdates.last.clusterManagersToChange.isEmpty, true); + }); + + testWidgets('Adding a cluster manager', (WidgetTester tester) async { + const ClusterManager cm1 = ClusterManager( + clusterManagerId: ClusterManagerId('cm_1'), + ); + const ClusterManager cm2 = ClusterManager( + clusterManagerId: ClusterManagerId('cm_2'), + ); + + await tester.pumpWidget(_mapWithClusterManagers({cm1})); + await tester + .pumpWidget(_mapWithClusterManagers({cm1, cm2})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.clusterManagerUpdates.last.clusterManagersToAdd.length, 1); + + final ClusterManager addedClusterManager = + map.clusterManagerUpdates.last.clusterManagersToAdd.first; + expect(addedClusterManager, equals(cm2)); + + expect( + map.clusterManagerUpdates.last.clusterManagerIdsToRemove.isEmpty, true); + + expect( + map.clusterManagerUpdates.last.clusterManagersToChange.isEmpty, true); + }); + + testWidgets('Removing a cluster manager', (WidgetTester tester) async { + const ClusterManager cm1 = ClusterManager( + clusterManagerId: ClusterManagerId('cm_1'), + ); + + await tester.pumpWidget(_mapWithClusterManagers({cm1})); + await tester.pumpWidget(_mapWithClusterManagers({})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.clusterManagerUpdates.last.clusterManagerIdsToRemove.length, 1); + expect(map.clusterManagerUpdates.last.clusterManagerIdsToRemove.first, + equals(cm1.clusterManagerId)); + + expect( + map.clusterManagerUpdates.last.clusterManagersToChange.isEmpty, true); + expect(map.clusterManagerUpdates.last.clusterManagersToAdd.isEmpty, true); + }); + + // This test checks that the cluster manager is not added again or changed + // when the data remains the same. Since [ClusterManager] does not have any + // properties to change, it should not trigger any updates. If new properties + // are added to [ClusterManager] in the future, this test will need to be + // updated accordingly to check that changes are triggered. + testWidgets('Updating a cluster manager with same data', + (WidgetTester tester) async { + const ClusterManager cm1 = ClusterManager( + clusterManagerId: ClusterManagerId('cm_1'), + ); + const ClusterManager cm2 = ClusterManager( + clusterManagerId: ClusterManagerId('cm_1'), + ); + + await tester.pumpWidget(_mapWithClusterManagers({cm1})); + await tester.pumpWidget(_mapWithClusterManagers({cm2})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + // As cluster manager does not have any properties to change, + // it should not populate the clusterManagersToChange set. + expect( + map.clusterManagerUpdates.last.clusterManagersToChange.isEmpty, true); + expect( + map.clusterManagerUpdates.last.clusterManagerIdsToRemove.isEmpty, true); + expect(map.clusterManagerUpdates.last.clusterManagersToAdd.isEmpty, true); + }); + + // This test checks that the cluster manager is not added again or changed + // when the data remains the same. Since [ClusterManager] does not have any + // properties to change, it should not trigger any updates. If new properties + // are added to [ClusterManager] in the future, this test will need to be + // updated accordingly to check that changes are triggered. + testWidgets('Multi update with same data', (WidgetTester tester) async { + ClusterManager cm1 = const ClusterManager( + clusterManagerId: ClusterManagerId('cm_1'), + ); + ClusterManager cm2 = const ClusterManager( + clusterManagerId: ClusterManagerId('cm_2'), + ); + final Set prev = {cm1, cm2}; + cm1 = const ClusterManager( + clusterManagerId: ClusterManagerId('cm_1'), + ); + cm2 = const ClusterManager( + clusterManagerId: ClusterManagerId('cm_2'), + ); + final Set cur = {cm1, cm2}; + + await tester.pumpWidget(_mapWithClusterManagers(prev)); + await tester.pumpWidget(_mapWithClusterManagers(cur)); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + // As cluster manager does not have any properties to change, + // it should not populate the clusterManagersToChange set. + expect(map.clusterManagerUpdates.last.clusterManagersToAdd.isEmpty, true); + expect( + map.clusterManagerUpdates.last.clusterManagerIdsToRemove.isEmpty, true); + expect(map.clusterManagerUpdates.last.clusterManagersToAdd.isEmpty, true); + }); + + // This test checks that the cluster manager is not added again or changed + // when the data remains the same. Since [ClusterManager] does not have any + // properties to change, it should not trigger any updates. If new properties + // are added to [ClusterManager] in the future, this test will need to be + // updated accordingly to check that changes are triggered. + testWidgets('Partial update with same data', (WidgetTester tester) async { + const ClusterManager cm1 = ClusterManager( + clusterManagerId: ClusterManagerId('heatmap_1'), + ); + const ClusterManager cm2 = ClusterManager( + clusterManagerId: ClusterManagerId('heatmap_2'), + ); + ClusterManager cm3 = const ClusterManager( + clusterManagerId: ClusterManagerId('heatmap_3'), + ); + final Set prev = {cm1, cm2, cm3}; + cm3 = const ClusterManager( + clusterManagerId: ClusterManagerId('heatmap_3'), + ); + final Set cur = {cm1, cm2, cm3}; + + await tester.pumpWidget(_mapWithClusterManagers(prev)); + await tester.pumpWidget(_mapWithClusterManagers(cur)); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + // As cluster manager does not have any properties to change, + // it should not populate the clusterManagersToChange set. + expect( + map.clusterManagerUpdates.last.clusterManagersToChange.isEmpty, true); + expect( + map.clusterManagerUpdates.last.clusterManagerIdsToRemove.isEmpty, true); + expect(map.clusterManagerUpdates.last.clusterManagersToAdd.isEmpty, true); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart index 422f6026b9f1..df83400c416b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart @@ -103,6 +103,15 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { await _fakeDelay(); } + @override + Future updateClusterManagers( + ClusterManagerUpdates clusterManagerUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.clusterManagerUpdates.add(clusterManagerUpdates); + await _fakeDelay(); + } + @override Future clearTileCache( TileOverlayId tileOverlayId, { @@ -250,6 +259,11 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { return mapEventStreamController.stream.whereType(); } + @override + Stream onClusterTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + @override void dispose({required int mapId}) { disposed = true; @@ -291,6 +305,8 @@ class PlatformMapStateRecorder { this.mapObjects = const MapObjects(), this.mapConfiguration = const MapConfiguration(), }) { + clusterManagerUpdates.add(ClusterManagerUpdates.from( + const {}, mapObjects.clusterManagers)); markerUpdates.add(MarkerUpdates.from(const {}, mapObjects.markers)); polygonUpdates .add(PolygonUpdates.from(const {}, mapObjects.polygons)); @@ -312,4 +328,6 @@ class PlatformMapStateRecorder { final List circleUpdates = []; final List heatmapUpdates = []; final List> tileOverlaySets = >[]; + final List clusterManagerUpdates = + []; }