[google_maps_flutter_web] Add marker clustering support (#6187)
This PR introduces support for marker clustering for Web platform
This is prequel PR for: https://github.com/flutter/packages/pull/4319
and sequel PR for: https://github.com/flutter/packages/pull/6158
Containing only changes to `google_maps_flutter_web` package.
Follow up PR will hold the app-facing plugin implementation.
Linked issue: https://github.com/flutter/flutter/issues/26863
---------
Co-authored-by: David Iglesias Teixeira <ditman@gmail.com>
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS b/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS
index b5b0e84..906ab21 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS
+++ b/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS
@@ -65,3 +65,4 @@
Alex Li <google@alexv525.com>
Rahul Raj <64.rahulraj@gmail.com>
Justin Baumann <me@jxstxn.dev>
+Joonas Kerttula <joonas.kerttula@codemate.com>
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md
index 08d4608..77f222b 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md
+++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.5.7
+
+* Adds support for marker clustering.
+
## 0.5.6+2
* Uses `TrustedTypes` from `web: ^0.5.1`.
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/README.md b/packages/google_maps_flutter/google_maps_flutter_web/README.md
index b52da95..22acc45 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/README.md
+++ b/packages/google_maps_flutter/google_maps_flutter_web/README.md
@@ -50,6 +50,19 @@
Now you should be able to use the Google Maps plugin normally.
+## Marker clustering
+
+If you need marker clustering support, modify the <head> tag to load the [js-markerclusterer](https://github.com/googlemaps/js-markerclusterer#install) library. Ensure you are using the currently supported version `2.5.3`, like so:
+
+```html
+<head>
+
+ <!-- // Other stuff -->
+
+ <script src="https://unpkg.com/@googlemaps/markerclusterer@2.5.3/dist/index.min.js"></script>
+</head>
+```
+
## Limitations of the web version
The following map options are not available in web, because the map doesn't rotate there:
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart
index c855999..ee6ffe0 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart
@@ -42,7 +42,6 @@
returnValue: <_i4.CircleId, _i3.CircleController>{},
returnValueForMissingStub: <_i4.CircleId, _i3.CircleController>{},
) as Map<_i4.CircleId, _i3.CircleController>);
-
@override
_i2.GMap get googleMap => (super.noSuchMethod(
Invocation.getter(#googleMap),
@@ -55,7 +54,6 @@
Invocation.getter(#googleMap),
),
) as _i2.GMap);
-
@override
set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(
Invocation.setter(
@@ -64,14 +62,12 @@
),
returnValueForMissingStub: null,
);
-
@override
int get mapId => (super.noSuchMethod(
Invocation.getter(#mapId),
returnValue: 0,
returnValueForMissingStub: 0,
) as int);
-
@override
set mapId(int? _mapId) => super.noSuchMethod(
Invocation.setter(
@@ -80,7 +76,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void addCircles(Set<_i4.Circle>? circlesToAdd) => super.noSuchMethod(
Invocation.method(
@@ -89,7 +84,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void changeCircles(Set<_i4.Circle>? circlesToChange) => super.noSuchMethod(
Invocation.method(
@@ -98,7 +92,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void removeCircles(Set<_i4.CircleId>? circleIdsToRemove) =>
super.noSuchMethod(
@@ -108,7 +101,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void bindToMap(
int? mapId,
@@ -137,7 +129,6 @@
returnValue: <_i4.PolygonId, _i3.PolygonController>{},
returnValueForMissingStub: <_i4.PolygonId, _i3.PolygonController>{},
) as Map<_i4.PolygonId, _i3.PolygonController>);
-
@override
_i2.GMap get googleMap => (super.noSuchMethod(
Invocation.getter(#googleMap),
@@ -150,7 +141,6 @@
Invocation.getter(#googleMap),
),
) as _i2.GMap);
-
@override
set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(
Invocation.setter(
@@ -159,14 +149,12 @@
),
returnValueForMissingStub: null,
);
-
@override
int get mapId => (super.noSuchMethod(
Invocation.getter(#mapId),
returnValue: 0,
returnValueForMissingStub: 0,
) as int);
-
@override
set mapId(int? _mapId) => super.noSuchMethod(
Invocation.setter(
@@ -175,7 +163,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void addPolygons(Set<_i4.Polygon>? polygonsToAdd) => super.noSuchMethod(
Invocation.method(
@@ -184,7 +171,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void changePolygons(Set<_i4.Polygon>? polygonsToChange) => super.noSuchMethod(
Invocation.method(
@@ -193,7 +179,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void removePolygons(Set<_i4.PolygonId>? polygonIdsToRemove) =>
super.noSuchMethod(
@@ -203,7 +188,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void bindToMap(
int? mapId,
@@ -232,7 +216,6 @@
returnValue: <_i4.PolylineId, _i3.PolylineController>{},
returnValueForMissingStub: <_i4.PolylineId, _i3.PolylineController>{},
) as Map<_i4.PolylineId, _i3.PolylineController>);
-
@override
_i2.GMap get googleMap => (super.noSuchMethod(
Invocation.getter(#googleMap),
@@ -245,7 +228,6 @@
Invocation.getter(#googleMap),
),
) as _i2.GMap);
-
@override
set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(
Invocation.setter(
@@ -254,14 +236,12 @@
),
returnValueForMissingStub: null,
);
-
@override
int get mapId => (super.noSuchMethod(
Invocation.getter(#mapId),
returnValue: 0,
returnValueForMissingStub: 0,
) as int);
-
@override
set mapId(int? _mapId) => super.noSuchMethod(
Invocation.setter(
@@ -270,7 +250,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void addPolylines(Set<_i4.Polyline>? polylinesToAdd) => super.noSuchMethod(
Invocation.method(
@@ -279,7 +258,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void changePolylines(Set<_i4.Polyline>? polylinesToChange) =>
super.noSuchMethod(
@@ -289,7 +267,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void removePolylines(Set<_i4.PolylineId>? polylineIdsToRemove) =>
super.noSuchMethod(
@@ -299,7 +276,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void bindToMap(
int? mapId,
@@ -327,7 +303,6 @@
returnValue: <_i4.MarkerId, _i3.MarkerController>{},
returnValueForMissingStub: <_i4.MarkerId, _i3.MarkerController>{},
) as Map<_i4.MarkerId, _i3.MarkerController>);
-
@override
_i2.GMap get googleMap => (super.noSuchMethod(
Invocation.getter(#googleMap),
@@ -340,7 +315,6 @@
Invocation.getter(#googleMap),
),
) as _i2.GMap);
-
@override
set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(
Invocation.setter(
@@ -349,14 +323,12 @@
),
returnValueForMissingStub: null,
);
-
@override
int get mapId => (super.noSuchMethod(
Invocation.getter(#mapId),
returnValue: 0,
returnValueForMissingStub: 0,
) as int);
-
@override
set mapId(int? _mapId) => super.noSuchMethod(
Invocation.setter(
@@ -365,7 +337,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void addMarkers(Set<_i4.Marker>? markersToAdd) => super.noSuchMethod(
Invocation.method(
@@ -374,7 +345,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void changeMarkers(Set<_i4.Marker>? markersToChange) => super.noSuchMethod(
Invocation.method(
@@ -383,7 +353,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void removeMarkers(Set<_i4.MarkerId>? markerIdsToRemove) =>
super.noSuchMethod(
@@ -393,7 +362,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void showMarkerInfoWindow(_i4.MarkerId? markerId) => super.noSuchMethod(
Invocation.method(
@@ -402,7 +370,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void hideMarkerInfoWindow(_i4.MarkerId? markerId) => super.noSuchMethod(
Invocation.method(
@@ -411,7 +378,6 @@
),
returnValueForMissingStub: null,
);
-
@override
bool isInfoWindowShown(_i4.MarkerId? markerId) => (super.noSuchMethod(
Invocation.method(
@@ -421,7 +387,6 @@
returnValue: false,
returnValueForMissingStub: false,
) as bool);
-
@override
void bindToMap(
int? mapId,
@@ -456,7 +421,6 @@
Invocation.getter(#googleMap),
),
) as _i2.GMap);
-
@override
set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(
Invocation.setter(
@@ -465,14 +429,12 @@
),
returnValueForMissingStub: null,
);
-
@override
int get mapId => (super.noSuchMethod(
Invocation.getter(#mapId),
returnValue: 0,
returnValueForMissingStub: 0,
) as int);
-
@override
set mapId(int? _mapId) => super.noSuchMethod(
Invocation.setter(
@@ -481,7 +443,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void addTileOverlays(Set<_i4.TileOverlay>? tileOverlaysToAdd) =>
super.noSuchMethod(
@@ -491,7 +452,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void changeTileOverlays(Set<_i4.TileOverlay>? tileOverlays) =>
super.noSuchMethod(
@@ -501,7 +461,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void removeTileOverlays(Set<_i4.TileOverlayId>? tileOverlayIds) =>
super.noSuchMethod(
@@ -511,7 +470,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void clearTileCache(_i4.TileOverlayId? tileOverlayId) => super.noSuchMethod(
Invocation.method(
@@ -520,7 +478,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void bindToMap(
int? mapId,
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart
index 36b4d11..b84b267 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart
@@ -361,10 +361,10 @@
});
testWidgets('isMarkerInfoWindowShown', (WidgetTester tester) async {
- when(controller.isInfoWindowShown(any)).thenReturn(true);
-
const MarkerId markerId = MarkerId('testing-123');
+ when(controller.isInfoWindowShown(markerId)).thenReturn(true);
+
await plugin.isMarkerInfoWindowShown(markerId, mapId: mapId);
verify(controller.isInfoWindowShown(markerId));
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart
index 3f84b40..582d6dd 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart
@@ -9,6 +9,7 @@
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'
as _i2;
import 'package:google_maps_flutter_web/google_maps_flutter_web.dart' as _i4;
+import 'package:google_maps_flutter_web/src/marker_clustering.dart' as _i6;
import 'package:mockito/mockito.dart' as _i1;
// ignore_for_file: type=lint
@@ -94,42 +95,37 @@
Invocation.getter(#configuration),
),
) as _i2.MapConfiguration);
-
@override
- _i3.StreamController<_i2.MapEvent<Object?>> get stream => (super.noSuchMethod(
+ _i3.StreamController<_i2.MapEvent<dynamic>> get stream => (super.noSuchMethod(
Invocation.getter(#stream),
- returnValue: _FakeStreamController_1<_i2.MapEvent<Object?>>(
+ returnValue: _FakeStreamController_1<_i2.MapEvent<dynamic>>(
this,
Invocation.getter(#stream),
),
returnValueForMissingStub:
- _FakeStreamController_1<_i2.MapEvent<Object?>>(
+ _FakeStreamController_1<_i2.MapEvent<dynamic>>(
this,
Invocation.getter(#stream),
),
- ) as _i3.StreamController<_i2.MapEvent<Object?>>);
-
+ ) as _i3.StreamController<_i2.MapEvent<dynamic>>);
@override
- _i3.Stream<_i2.MapEvent<Object?>> get events => (super.noSuchMethod(
+ _i3.Stream<_i2.MapEvent<dynamic>> get events => (super.noSuchMethod(
Invocation.getter(#events),
- returnValue: _i3.Stream<_i2.MapEvent<Object?>>.empty(),
- returnValueForMissingStub: _i3.Stream<_i2.MapEvent<Object?>>.empty(),
- ) as _i3.Stream<_i2.MapEvent<Object?>>);
-
+ returnValue: _i3.Stream<_i2.MapEvent<dynamic>>.empty(),
+ returnValueForMissingStub: _i3.Stream<_i2.MapEvent<dynamic>>.empty(),
+ ) as _i3.Stream<_i2.MapEvent<dynamic>>);
@override
bool get isInitialized => (super.noSuchMethod(
Invocation.getter(#isInitialized),
returnValue: false,
returnValueForMissingStub: false,
) as bool);
-
@override
List<_i5.MapTypeStyle> get styles => (super.noSuchMethod(
Invocation.getter(#styles),
returnValue: <_i5.MapTypeStyle>[],
returnValueForMissingStub: <_i5.MapTypeStyle>[],
) as List<_i5.MapTypeStyle>);
-
@override
void debugSetOverrides({
_i4.DebugCreateMapFunction? createMap,
@@ -138,6 +134,7 @@
_i4.CirclesController? circles,
_i4.PolygonsController? polygons,
_i4.PolylinesController? polylines,
+ _i6.ClusterManagersController? clusterManagers,
_i4.TileOverlaysController? tileOverlays,
}) =>
super.noSuchMethod(
@@ -151,12 +148,12 @@
#circles: circles,
#polygons: polygons,
#polylines: polylines,
+ #clusterManagers: clusterManagers,
#tileOverlays: tileOverlays,
},
),
returnValueForMissingStub: null,
);
-
@override
void init() => super.noSuchMethod(
Invocation.method(
@@ -165,7 +162,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void updateMapConfiguration(_i2.MapConfiguration? update) =>
super.noSuchMethod(
@@ -175,7 +171,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void updateStyles(List<_i5.MapTypeStyle>? styles) => super.noSuchMethod(
Invocation.method(
@@ -184,7 +179,6 @@
),
returnValueForMissingStub: null,
);
-
@override
_i3.Future<_i2.LatLngBounds> getVisibleRegion() => (super.noSuchMethod(
Invocation.method(
@@ -207,7 +201,6 @@
),
)),
) as _i3.Future<_i2.LatLngBounds>);
-
@override
_i3.Future<_i2.ScreenCoordinate> getScreenCoordinate(_i2.LatLng? latLng) =>
(super.noSuchMethod(
@@ -232,7 +225,6 @@
),
)),
) as _i3.Future<_i2.ScreenCoordinate>);
-
@override
_i3.Future<_i2.LatLng> getLatLng(_i2.ScreenCoordinate? screenCoordinate) =>
(super.noSuchMethod(
@@ -255,7 +247,6 @@
),
)),
) as _i3.Future<_i2.LatLng>);
-
@override
_i3.Future<void> moveCamera(_i2.CameraUpdate? cameraUpdate) =>
(super.noSuchMethod(
@@ -266,7 +257,6 @@
returnValue: _i3.Future<void>.value(),
returnValueForMissingStub: _i3.Future<void>.value(),
) as _i3.Future<void>);
-
@override
_i3.Future<double> getZoomLevel() => (super.noSuchMethod(
Invocation.method(
@@ -276,7 +266,6 @@
returnValue: _i3.Future<double>.value(0.0),
returnValueForMissingStub: _i3.Future<double>.value(0.0),
) as _i3.Future<double>);
-
@override
void updateCircles(_i2.CircleUpdates? updates) => super.noSuchMethod(
Invocation.method(
@@ -285,7 +274,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void updatePolygons(_i2.PolygonUpdates? updates) => super.noSuchMethod(
Invocation.method(
@@ -294,7 +282,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void updatePolylines(_i2.PolylineUpdates? updates) => super.noSuchMethod(
Invocation.method(
@@ -303,7 +290,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void updateMarkers(_i2.MarkerUpdates? updates) => super.noSuchMethod(
Invocation.method(
@@ -312,7 +298,15 @@
),
returnValueForMissingStub: null,
);
-
+ @override
+ void updateClusterManagers(_i2.ClusterManagerUpdates? updates) =>
+ super.noSuchMethod(
+ Invocation.method(
+ #updateClusterManagers,
+ [updates],
+ ),
+ returnValueForMissingStub: null,
+ );
@override
void updateTileOverlays(Set<_i2.TileOverlay>? newOverlays) =>
super.noSuchMethod(
@@ -322,7 +316,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void clearTileCache(_i2.TileOverlayId? id) => super.noSuchMethod(
Invocation.method(
@@ -331,7 +324,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void showInfoWindow(_i2.MarkerId? markerId) => super.noSuchMethod(
Invocation.method(
@@ -340,7 +332,6 @@
),
returnValueForMissingStub: null,
);
-
@override
void hideInfoWindow(_i2.MarkerId? markerId) => super.noSuchMethod(
Invocation.method(
@@ -349,7 +340,6 @@
),
returnValueForMissingStub: null,
);
-
@override
bool isInfoWindowShown(_i2.MarkerId? markerId) => (super.noSuchMethod(
Invocation.method(
@@ -359,7 +349,6 @@
returnValue: false,
returnValueForMissingStub: false,
) as bool);
-
@override
void dispose() => super.noSuchMethod(
Invocation.method(
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart
new file mode 100644
index 0000000..c34d9c5
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart
@@ -0,0 +1,170 @@
+// 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.
+
+// ignore_for_file: unnecessary_nullable_for_final_variable_declarations
+
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
+import 'package:integration_test/integration_test.dart';
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ GoogleMapsFlutterPlatform.instance.enableDebugInspection();
+ final GoogleMapsFlutterPlatform plugin = GoogleMapsFlutterPlatform.instance;
+ final GoogleMapsInspectorPlatform inspector =
+ GoogleMapsInspectorPlatform.instance!;
+
+ const LatLng mapCenter = LatLng(20, 20);
+ const CameraPosition initialCameraPosition =
+ CameraPosition(target: mapCenter);
+
+ group('MarkersController', () {
+ const int testMapId = 33930;
+
+ testWidgets('Marker clustering', (WidgetTester tester) async {
+ const ClusterManagerId clusterManagerId = ClusterManagerId('cluster 1');
+
+ final Set<ClusterManager> clusterManagers = <ClusterManager>{
+ const ClusterManager(clusterManagerId: clusterManagerId),
+ };
+
+ // Create the marker with clusterManagerId.
+ final Set<Marker> initialMarkers = <Marker>{
+ const Marker(
+ markerId: MarkerId('1'),
+ position: mapCenter,
+ clusterManagerId: clusterManagerId),
+ const Marker(
+ markerId: MarkerId('2'),
+ position: mapCenter,
+ clusterManagerId: clusterManagerId),
+ };
+
+ final Completer<int> mapIdCompleter = Completer<int>();
+
+ await _pumpMap(
+ tester,
+ plugin.buildViewWithConfiguration(
+ testMapId, (int id) => mapIdCompleter.complete(id),
+ widgetConfiguration: const MapWidgetConfiguration(
+ initialCameraPosition: initialCameraPosition,
+ textDirection: TextDirection.ltr,
+ ),
+ mapObjects: MapObjects(
+ clusterManagers: clusterManagers, markers: initialMarkers)));
+
+ final int mapId = await mapIdCompleter.future;
+ expect(mapId, equals(testMapId));
+
+ addTearDown(() => plugin.dispose(mapId: mapId));
+
+ final LatLng latlon = await plugin
+ .getLatLng(const ScreenCoordinate(x: 0, y: 0), mapId: mapId);
+ debugPrint(latlon.toString());
+
+ final List<Cluster> clusters =
+ await waitForValueMatchingPredicate<List<Cluster>>(
+ tester,
+ () async => inspector.getClusters(
+ mapId: mapId, clusterManagerId: clusterManagerId),
+ (List<Cluster> clusters) => clusters.isNotEmpty) ??
+ <Cluster>[];
+
+ expect(clusters.length, 1);
+ expect(clusters[0].markerIds.length, 2);
+
+ // Copy only the first marker with null clusterManagerId.
+ // This means that both markers should be removed from the cluster.
+ final Set<Marker> updatedMarkers = <Marker>{
+ _copyMarkerWithClusterManagerId(initialMarkers.first, null)
+ };
+
+ final MarkerUpdates markerUpdates =
+ MarkerUpdates.from(initialMarkers, updatedMarkers);
+ await plugin.updateMarkers(markerUpdates, mapId: mapId);
+
+ final List<Cluster> updatedClusters =
+ await waitForValueMatchingPredicate<List<Cluster>>(
+ tester,
+ () async => inspector.getClusters(
+ mapId: mapId, clusterManagerId: clusterManagerId),
+ (List<Cluster> clusters) => clusters.isNotEmpty) ??
+ <Cluster>[];
+
+ expect(updatedClusters.length, 0);
+ });
+ });
+}
+
+// Repeatedly checks an asynchronous value against a test condition, waiting
+// one frame between each check, returing the value if it passes the predicate
+// before [maxTries] is reached.
+//
+// Returns null if the predicate is never satisfied.
+//
+// This is useful for cases where the Maps SDK has some internally
+// asynchronous operation that we don't have visibility into (e.g., native UI
+// animations).
+Future<T?> waitForValueMatchingPredicate<T>(WidgetTester tester,
+ Future<T> Function() getValue, bool Function(T) predicate,
+ {int maxTries = 100}) async {
+ for (int i = 0; i < maxTries; i++) {
+ final T value = await getValue();
+ if (predicate(value)) {
+ return value;
+ }
+ await tester.pump();
+ }
+ return null;
+}
+
+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,
+ );
+}
+
+/// Pumps a [map] widget in [tester] of a certain [size], then waits until it settles.
+Future<void> _pumpMap(WidgetTester tester, Widget map,
+ [Size size = const Size.square(200)]) async {
+ await tester.pumpWidget(_wrapMap(map, size));
+ await tester.pumpAndSettle();
+}
+
+/// Wraps a [map] in a bunch of widgets so it renders in all platforms.
+///
+/// An optional [size] can be passed.
+Widget _wrapMap(Widget map, [Size size = const Size.square(200)]) {
+ return MaterialApp(
+ home: Scaffold(
+ body: Center(
+ child: SizedBox.fromSize(
+ size: size,
+ child: map,
+ ),
+ ),
+ ),
+ );
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart
index d965435..81e1eb2 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart
@@ -11,6 +11,7 @@
import 'package:google_maps/google_maps.dart' as gmaps;
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
import 'package:google_maps_flutter_web/google_maps_flutter_web.dart';
+import 'package:google_maps_flutter_web/src/marker_clustering.dart';
// ignore: implementation_imports
import 'package:google_maps_flutter_web/src/utils.dart';
import 'package:http/http.dart' as http;
@@ -25,12 +26,17 @@
group('MarkersController', () {
late StreamController<MapEvent<Object?>> events;
late MarkersController controller;
+ late ClusterManagersController clusterManagersController;
late gmaps.GMap map;
setUp(() {
events = StreamController<MapEvent<Object?>>();
- controller = MarkersController(stream: events);
+
+ clusterManagersController = ClusterManagersController(stream: events);
+ controller = MarkersController(
+ stream: events, clusterManagersController: clusterManagersController);
map = gmaps.GMap(createDivElement());
+ clusterManagersController.bindToMap(123, map);
controller.bindToMap(123, map);
});
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml
index 2838157..a7222df 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml
@@ -9,7 +9,7 @@
dependencies:
flutter:
sdk: flutter
- google_maps_flutter_platform_interface: ^2.5.0
+ google_maps_flutter_platform_interface: ^2.6.0
google_maps_flutter_web:
path: ../
web: ^0.5.0
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html b/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html
index 3121d18..9cbd7be 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html
@@ -7,6 +7,7 @@
<title>Browser Tests</title>
<!-- This API key comes from: go/flutter-maps-web-tests-api-key (GCP project: flutter-infra) -->
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyAa9cRBkhuxGq3Xw3HPz8SPwaVOhRmm7kk&libraries=geometry"></script>
+ <script src="https://unpkg.com/@googlemaps/markerclusterer@2.5.3/dist/index.min.js"></script>
</head>
<body>
<script src="main.dart.js"></script>
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart
index fe44d41..cda20cf 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart
@@ -24,6 +24,7 @@
import 'src/dom_window_extension.dart';
import 'src/google_maps_inspector_web.dart';
import 'src/map_styler.dart';
+import 'src/marker_clustering.dart';
import 'src/third_party/to_screen_location/to_screen_location.dart';
import 'src/types.dart';
import 'src/utils.dart';
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart
index 0f73a7d..1822a2f 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart
@@ -172,20 +172,22 @@
return gmaps.LatLng(latLng.latitude, latLng.longitude);
}
-LatLng _gmLatLngToLatLng(gmaps.LatLng latLng) {
+/// Converts [gmaps.LatLng] to [LatLng].
+LatLng gmLatLngToLatLng(gmaps.LatLng latLng) {
return LatLng(latLng.lat.toDouble(), latLng.lng.toDouble());
}
-LatLngBounds _gmLatLngBoundsTolatLngBounds(gmaps.LatLngBounds latLngBounds) {
+/// Converts a [gmaps.LatLngBounds] into a [LatLngBounds].
+LatLngBounds gmLatLngBoundsTolatLngBounds(gmaps.LatLngBounds latLngBounds) {
return LatLngBounds(
- southwest: _gmLatLngToLatLng(latLngBounds.southWest),
- northeast: _gmLatLngToLatLng(latLngBounds.northEast),
+ southwest: gmLatLngToLatLng(latLngBounds.southWest),
+ northeast: gmLatLngToLatLng(latLngBounds.northEast),
);
}
CameraPosition _gmViewportToCameraPosition(gmaps.GMap map) {
return CameraPosition(
- target: _gmLatLngToLatLng(map.center ?? _nullGmapsLatLng),
+ target: gmLatLngToLatLng(map.center ?? _nullGmapsLatLng),
bearing: map.heading?.toDouble() ?? 0,
tilt: map.tilt?.toDouble() ?? 0,
zoom: map.zoom?.toDouble() ?? 0,
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart
index c60dd92..c2fe750 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart
@@ -29,12 +29,17 @@
_polygons = mapObjects.polygons,
_polylines = mapObjects.polylines,
_circles = mapObjects.circles,
+ _clusterManagers = mapObjects.clusterManagers,
_tileOverlays = mapObjects.tileOverlays,
_lastMapConfiguration = mapConfiguration {
_circlesController = CirclesController(stream: _streamController);
_polygonsController = PolygonsController(stream: _streamController);
_polylinesController = PolylinesController(stream: _streamController);
- _markersController = MarkersController(stream: _streamController);
+ _clusterManagersController =
+ ClusterManagersController(stream: _streamController);
+ _markersController = MarkersController(
+ stream: _streamController,
+ clusterManagersController: _clusterManagersController!);
_tileOverlaysController = TileOverlaysController();
_updateStylesFromConfiguration(mapConfiguration);
@@ -60,7 +65,9 @@
final Set<Polygon> _polygons;
final Set<Polyline> _polylines;
final Set<Circle> _circles;
+ final Set<ClusterManager> _clusterManagers;
Set<TileOverlay> _tileOverlays;
+
// The configuration passed by the user, before converting to gmaps.
// Caching this allows us to re-create the map faithfully when needed.
MapConfiguration _lastMapConfiguration = const MapConfiguration();
@@ -118,13 +125,20 @@
PolygonsController? _polygonsController;
PolylinesController? _polylinesController;
MarkersController? _markersController;
+ ClusterManagersController? _clusterManagersController;
TileOverlaysController? _tileOverlaysController;
+
// Keeps track if _attachGeometryControllers has been called or not.
bool _controllersBoundToMap = false;
// Keeps track if the map is moving or not.
bool _mapIsMoving = false;
+ /// The ClusterManagersController of this Map. Only for integration testing.
+ @visibleForTesting
+ ClusterManagersController? get clusterManagersController =>
+ _clusterManagersController;
+
/// Overrides certain properties to install mocks defined during testing.
@visibleForTesting
void debugSetOverrides({
@@ -134,6 +148,7 @@
CirclesController? circles,
PolygonsController? polygons,
PolylinesController? polylines,
+ ClusterManagersController? clusterManagers,
TileOverlaysController? tileOverlays,
}) {
_overrideCreateMap = createMap;
@@ -142,6 +157,7 @@
_circlesController = circles ?? _circlesController;
_polygonsController = polygons ?? _polygonsController;
_polylinesController = polylines ?? _polylinesController;
+ _clusterManagersController = clusterManagers ?? _clusterManagersController;
_tileOverlaysController = tileOverlays ?? _tileOverlaysController;
}
@@ -197,6 +213,8 @@
_attachMapEvents(map);
_attachGeometryControllers(map);
+ _initClustering(_clusterManagers);
+
// Now attach the geometry, traffic and any other layers...
_renderInitialGeometry();
_setTrafficLayer(map, _lastMapConfiguration.trafficEnabled ?? false);
@@ -211,13 +229,13 @@
map.onClick.listen((gmaps.IconMouseEvent event) {
assert(event.latLng != null);
_streamController.add(
- MapTapEvent(_mapId, _gmLatLngToLatLng(event.latLng!)),
+ MapTapEvent(_mapId, gmLatLngToLatLng(event.latLng!)),
);
});
map.onRightclick.listen((gmaps.MapMouseEvent event) {
assert(event.latLng != null);
_streamController.add(
- MapLongPressEvent(_mapId, _gmLatLngToLatLng(event.latLng!)),
+ MapLongPressEvent(_mapId, gmLatLngToLatLng(event.latLng!)),
);
});
map.onBoundsChanged.listen((void _) {
@@ -251,6 +269,8 @@
'Cannot attach a map to a null PolylinesController instance.');
assert(_markersController != null,
'Cannot attach a map to a null MarkersController instance.');
+ assert(_clusterManagersController != null,
+ 'Cannot attach a map to a null ClusterManagersController instance.');
assert(_tileOverlaysController != null,
'Cannot attach a map to a null TileOverlaysController instance.');
@@ -258,11 +278,16 @@
_polygonsController!.bindToMap(_mapId, map);
_polylinesController!.bindToMap(_mapId, map);
_markersController!.bindToMap(_mapId, map);
+ _clusterManagersController!.bindToMap(_mapId, map);
_tileOverlaysController!.bindToMap(_mapId, map);
_controllersBoundToMap = true;
}
+ void _initClustering(Set<ClusterManager> clusterManagers) {
+ _clusterManagersController!.addClusterManagers(clusterManagers);
+ }
+
// Renders the initial sets of geometry.
void _renderInitialGeometry() {
assert(
@@ -369,7 +394,7 @@
await Future<gmaps.LatLngBounds?>.value(_googleMap!.bounds) ??
_nullGmapsLatLngBounds;
- return _gmLatLngBoundsTolatLngBounds(bounds);
+ return gmLatLngBoundsTolatLngBounds(bounds);
}
/// Returns the [ScreenCoordinate] for a given viewport [LatLng].
@@ -390,7 +415,7 @@
final gmaps.LatLng latLng =
_pixelToLatLng(_googleMap!, screenCoordinate.x, screenCoordinate.y);
- return _gmLatLngToLatLng(latLng);
+ return gmLatLngToLatLng(latLng);
}
/// Applies a `cameraUpdate` to the current viewport.
@@ -447,6 +472,16 @@
_markersController?.removeMarkers(updates.markerIdsToRemove);
}
+ /// Applies [ClusterManagerUpdates] to the currently managed cluster managers.
+ void updateClusterManagers(ClusterManagerUpdates updates) {
+ assert(_clusterManagersController != null,
+ 'Cannot update markers after dispose().');
+ _clusterManagersController
+ ?.addClusterManagers(updates.clusterManagersToAdd);
+ _clusterManagersController
+ ?.removeClusterManagers(updates.clusterManagerIdsToRemove);
+ }
+
/// Updates the set of [TileOverlay]s.
void updateTileOverlays(Set<TileOverlay> newOverlays) {
final MapsObjectUpdates<TileOverlay> updates =
@@ -498,6 +533,7 @@
_polygonsController = null;
_polylinesController = null;
_markersController = null;
+ _clusterManagersController = null;
_tileOverlaysController = null;
_streamController.close();
}
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart
index 805887f..f71818a 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart
@@ -99,6 +99,14 @@
}
@override
+ Future<void> updateClusterManagers(
+ ClusterManagerUpdates clusterManagerUpdates, {
+ required int mapId,
+ }) async {
+ _map(mapId).updateClusterManagers(clusterManagerUpdates);
+ }
+
+ @override
Future<void> clearTileCache(
TileOverlayId tileOverlayId, {
required int mapId,
@@ -280,6 +288,11 @@
}
@override
+ Stream<ClusterTapEvent> onClusterTap({required int mapId}) {
+ return _events(mapId).whereType<ClusterTapEvent>();
+ }
+
+ @override
Future<String?> getStyleError({required int mapId}) async {
return _map(mapId).lastStyleError;
}
@@ -339,6 +352,7 @@
void enableDebugInspection() {
GoogleMapsInspectorPlatform.instance = GoogleMapsInspectorWeb(
(int mapId) => _map(mapId).configuration,
+ (int mapId) => _map(mapId).clusterManagersController,
);
}
}
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart
index 6d95531..98b4743 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart
@@ -3,17 +3,25 @@
// found in the LICENSE file.
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
+import 'marker_clustering.dart';
/// Function that gets the [MapConfiguration] for a given `mapId`.
typedef ConfigurationProvider = MapConfiguration Function(int mapId);
+/// Function that gets the [ClusterManagersController] for a given `mapId`.
+typedef ClusterManagersControllerProvider = ClusterManagersController? Function(
+ int mapId);
+
/// This platform implementation allows inspecting the running maps.
class GoogleMapsInspectorWeb extends GoogleMapsInspectorPlatform {
/// Build an "inspector" that is able to look into maps.
- GoogleMapsInspectorWeb(ConfigurationProvider configurationProvider)
- : _configurationProvider = configurationProvider;
+ GoogleMapsInspectorWeb(ConfigurationProvider configurationProvider,
+ ClusterManagersControllerProvider clusterManagersControllerProvider)
+ : _configurationProvider = configurationProvider,
+ _clusterManagersControllerProvider = clusterManagersControllerProvider;
final ConfigurationProvider _configurationProvider;
+ final ClusterManagersControllerProvider _clusterManagersControllerProvider;
@override
Future<bool> areBuildingsEnabled({required int mapId}) async {
@@ -85,4 +93,14 @@
Future<bool> isTrafficEnabled({required int mapId}) async {
return _configurationProvider(mapId).trafficEnabled ?? false;
}
+
+ @override
+ Future<List<Cluster>> getClusters({
+ required int mapId,
+ required ClusterManagerId clusterManagerId,
+ }) async {
+ return _clusterManagersControllerProvider(mapId)
+ ?.getClusters(clusterManagerId) ??
+ <Cluster>[];
+ }
}
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart
index c1b0772..518dce6 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart
@@ -15,9 +15,11 @@
LatLngCallback? onDrag,
LatLngCallback? onDragEnd,
VoidCallback? onTap,
+ ClusterManagerId? clusterManagerId,
}) : _marker = marker,
_infoWindow = infoWindow,
- _consumeTapEvents = consumeTapEvents {
+ _consumeTapEvents = consumeTapEvents,
+ _clusterManagerId = clusterManagerId {
if (onTap != null) {
marker.onClick.listen((gmaps.MapMouseEvent event) {
onTap.call();
@@ -47,6 +49,8 @@
final bool _consumeTapEvents;
+ final ClusterManagerId? _clusterManagerId;
+
final gmaps.InfoWindow? _infoWindow;
bool _infoWindowShown = false;
@@ -57,6 +61,9 @@
/// Returns `true` if the [gmaps.InfoWindow] associated to this marker is being shown.
bool get infoWindowShown => _infoWindowShown;
+ /// Returns [ClusterManagerId] if marker belongs to cluster.
+ ClusterManagerId? get clusterManagerId => _clusterManagerId;
+
/// Returns the [gmaps.Marker] associated to this controller.
gmaps.Marker? get marker => _marker;
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering.dart
new file mode 100644
index 0000000..d4b5ed3
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering.dart
@@ -0,0 +1,139 @@
+// 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:async';
+
+import 'package:google_maps/google_maps.dart' as gmaps;
+import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
+
+import '../google_maps_flutter_web.dart';
+import 'marker_clustering_js_interop.dart';
+import 'types.dart';
+
+/// A controller class for managing marker clustering.
+///
+/// This class maps [ClusterManager] objects to javascript [MarkerClusterer]
+/// objects and provides an interface for adding and removing markers from
+/// clusters.
+class ClusterManagersController extends GeometryController {
+ /// Creates a new [ClusterManagersController] instance.
+ ///
+ /// The [stream] parameter is a required [StreamController] used for
+ /// emitting map events.
+ ClusterManagersController(
+ {required StreamController<MapEvent<Object?>> stream})
+ : _streamController = stream,
+ _clusterManagerIdToMarkerClusterer =
+ <ClusterManagerId, MarkerClusterer>{};
+
+ // The stream over which cluster managers broadcast their events
+ final StreamController<MapEvent<Object?>> _streamController;
+
+ // A cache of [MarkerClusterer]s indexed by their [ClusterManagerId].
+ final Map<ClusterManagerId, MarkerClusterer>
+ _clusterManagerIdToMarkerClusterer;
+
+ /// Adds a set of [ClusterManager] objects to the cache.
+ void addClusterManagers(Set<ClusterManager> clusterManagersToAdd) {
+ clusterManagersToAdd.forEach(_addClusterManager);
+ }
+
+ void _addClusterManager(ClusterManager clusterManager) {
+ final MarkerClusterer markerClusterer = createMarkerClusterer(
+ googleMap,
+ (gmaps.MapMouseEvent event, MarkerClustererCluster cluster,
+ gmaps.GMap map) =>
+ _clusterClicked(
+ clusterManager.clusterManagerId, event, cluster, map));
+
+ _clusterManagerIdToMarkerClusterer[clusterManager.clusterManagerId] =
+ markerClusterer;
+ markerClusterer.onAdd();
+ }
+
+ /// Removes a set of [ClusterManagerId]s from the cache.
+ void removeClusterManagers(Set<ClusterManagerId> clusterManagerIdsToRemove) {
+ clusterManagerIdsToRemove.forEach(_removeClusterManager);
+ }
+
+ void _removeClusterManager(ClusterManagerId clusterManagerId) {
+ final MarkerClusterer? markerClusterer =
+ _clusterManagerIdToMarkerClusterer[clusterManagerId];
+ if (markerClusterer != null) {
+ markerClusterer.clearMarkers(true);
+ markerClusterer.onRemove();
+ }
+ _clusterManagerIdToMarkerClusterer.remove(clusterManagerId);
+ }
+
+ /// Adds given [gmaps.Marker] to the [MarkerClusterer] with given
+ /// [ClusterManagerId].
+ void addItem(ClusterManagerId clusterManagerId, gmaps.Marker marker) {
+ final MarkerClusterer? markerClusterer =
+ _clusterManagerIdToMarkerClusterer[clusterManagerId];
+ if (markerClusterer != null) {
+ markerClusterer.addMarker(marker, true);
+ markerClusterer.render();
+ }
+ }
+
+ /// Removes given [gmaps.Marker] from the [MarkerClusterer] with given
+ /// [ClusterManagerId].
+ void removeItem(ClusterManagerId clusterManagerId, gmaps.Marker? marker) {
+ if (marker != null) {
+ final MarkerClusterer? markerClusterer =
+ _clusterManagerIdToMarkerClusterer[clusterManagerId];
+ if (markerClusterer != null) {
+ markerClusterer.removeMarker(marker, true);
+ markerClusterer.render();
+ }
+ }
+ }
+
+ /// Returns list of clusters in [MarkerClusterer] with given
+ /// [ClusterManagerId].
+ List<Cluster> getClusters(ClusterManagerId clusterManagerId) {
+ final MarkerClusterer? markerClusterer =
+ _clusterManagerIdToMarkerClusterer[clusterManagerId];
+ if (markerClusterer != null) {
+ return markerClusterer.clusters
+ .map((MarkerClustererCluster cluster) =>
+ _convertCluster(clusterManagerId, cluster))
+ .toList();
+ }
+ return <Cluster>[];
+ }
+
+ void _clusterClicked(
+ ClusterManagerId clusterManagerId,
+ gmaps.MapMouseEvent event,
+ MarkerClustererCluster markerClustererCluster,
+ gmaps.GMap map) {
+ if (markerClustererCluster.count > 0 &&
+ markerClustererCluster.bounds != null) {
+ final Cluster cluster =
+ _convertCluster(clusterManagerId, markerClustererCluster);
+ _streamController.add(ClusterTapEvent(mapId, cluster));
+ }
+ }
+
+ /// Converts [MarkerClustererCluster] to [Cluster].
+ Cluster _convertCluster(ClusterManagerId clusterManagerId,
+ MarkerClustererCluster markerClustererCluster) {
+ final LatLng position = gmLatLngToLatLng(markerClustererCluster.position);
+ final LatLngBounds bounds =
+ gmLatLngBoundsTolatLngBounds(markerClustererCluster.bounds!);
+
+ final List<MarkerId> markerIds = markerClustererCluster.markers
+ .map<MarkerId>((gmaps.Marker marker) =>
+ MarkerId(marker.get('markerId')! as String))
+ .toList();
+ return Cluster(
+ clusterManagerId,
+ markerIds,
+ position: position,
+ bounds: bounds,
+ );
+ }
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering_js_interop.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering_js_interop.dart
new file mode 100644
index 0000000..e3bf42b
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering_js_interop.dart
@@ -0,0 +1,164 @@
+// 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.
+
+// TODO(srujzs): Needed for https://github.com/dart-lang/sdk/issues/54801. Once
+// we publish a version with a min SDK constraint that contains this fix,
+// remove.
+@JS()
+library;
+
+import 'dart:js_interop';
+
+import 'package:google_maps/google_maps.dart' as gmaps;
+
+/// A typedef representing a callback function for handling cluster tap events.
+typedef ClusterClickHandler = void Function(
+ gmaps.MapMouseEvent,
+ MarkerClustererCluster,
+ gmaps.GMap,
+);
+
+/// The [MarkerClustererOptions] object used to initialize [MarkerClusterer].
+///
+/// See: https://googlemaps.github.io/js-markerclusterer/interfaces/MarkerClustererOptions.html
+@JS()
+@anonymous
+extension type MarkerClustererOptions._(JSObject _) implements JSObject {
+ /// Constructs a new [MarkerClustererOptions] object.
+ factory MarkerClustererOptions({
+ gmaps.GMap? map,
+ List<gmaps.Marker>? markers,
+ ClusterClickHandler? onClusterClick,
+ }) =>
+ MarkerClustererOptions._js(
+ map: map as JSAny?,
+ markers: markers?.cast<JSAny>().toJS ?? JSArray<JSAny>(),
+ onClusterClick: onClusterClick != null
+ ? ((JSAny event, MarkerClustererCluster cluster, JSAny map) =>
+ onClusterClick(event as gmaps.MapMouseEvent, cluster,
+ map as gmaps.GMap)).toJS
+ : null,
+ );
+
+ external factory MarkerClustererOptions._js({
+ JSAny? map,
+ JSArray<JSAny> markers,
+ JSFunction? onClusterClick,
+ });
+
+ /// Returns the [gmaps.GMap] object.
+ gmaps.GMap? get map => _map as gmaps.GMap?;
+ @JS('map')
+ external JSAny? get _map;
+
+ /// Returns the list of [gmaps.Marker] objects.
+ List<gmaps.Marker>? get markers => _markers?.toDart.cast<gmaps.Marker>();
+ @JS('markers')
+ external JSArray<JSAny>? get _markers;
+
+ /// Returns the onClusterClick handler.
+ ClusterClickHandler? get onClusterClick =>
+ _onClusterClick?.toDart as ClusterClickHandler?;
+ @JS('onClusterClick')
+ external JSExportedDartFunction? get _onClusterClick;
+}
+
+/// The cluster object handled by the [MarkerClusterer].
+///
+/// https://googlemaps.github.io/js-markerclusterer/classes/Cluster.html
+@JS('markerClusterer.Cluster')
+extension type MarkerClustererCluster._(JSObject _) implements JSObject {
+ /// Getter for the cluster marker.
+ gmaps.Marker get marker => _marker as gmaps.Marker;
+ @JS('marker')
+ external JSAny get _marker;
+
+ /// List of markers in the cluster.
+ List<gmaps.Marker> get markers => _markers.toDart.cast<gmaps.Marker>();
+ @JS('markers')
+ external JSArray<JSAny> get _markers;
+
+ /// The bounds of the cluster.
+ gmaps.LatLngBounds? get bounds => _bounds as gmaps.LatLngBounds?;
+ @JS('bounds')
+ external JSAny? get _bounds;
+
+ /// The position of the cluster marker.
+ gmaps.LatLng get position => _position as gmaps.LatLng;
+ @JS('position')
+ external JSAny get _position;
+
+ /// Get the count of **visible** markers.
+ external int get count;
+
+ /// Deletes the cluster.
+ external void delete();
+
+ /// Adds a marker to the cluster.
+ void push(gmaps.Marker marker) => _push(marker as JSAny);
+ @JS('push')
+ external void _push(JSAny marker);
+}
+
+/// The [MarkerClusterer] object used to cluster markers on the map.
+///
+/// https://googlemaps.github.io/js-markerclusterer/classes/MarkerClusterer.html
+@JS('markerClusterer.MarkerClusterer')
+extension type MarkerClusterer._(JSObject _) implements JSObject {
+ /// Constructs a new [MarkerClusterer] object.
+ external MarkerClusterer(MarkerClustererOptions options);
+
+ /// Adds a marker to be clustered by the [MarkerClusterer].
+ void addMarker(gmaps.Marker marker, bool? noDraw) =>
+ _addMarker(marker as JSAny, noDraw);
+ @JS('addMarker')
+ external void _addMarker(JSAny marker, bool? noDraw);
+
+ /// Adds a list of markers to be clustered by the [MarkerClusterer].
+ void addMarkers(List<gmaps.Marker>? markers, bool? noDraw) =>
+ _addMarkers(markers?.cast<JSAny>().toJS, noDraw);
+ @JS('addMarkers')
+ external void _addMarkers(JSArray<JSAny>? markers, bool? noDraw);
+
+ /// Removes a marker from the [MarkerClusterer].
+ bool removeMarker(gmaps.Marker marker, bool? noDraw) =>
+ _removeMarker(marker as JSAny, noDraw);
+ @JS('removeMarker')
+ external bool _removeMarker(JSAny marker, bool? noDraw);
+
+ /// Removes a list of markers from the [MarkerClusterer].
+ bool removeMarkers(List<gmaps.Marker>? markers, bool? noDraw) =>
+ _removeMarkers(markers?.cast<JSAny>().toJS, noDraw);
+ @JS('removeMarkers')
+ external bool _removeMarkers(JSArray<JSAny>? markers, bool? noDraw);
+
+ /// Clears all the markers from the [MarkerClusterer].
+ external void clearMarkers(bool? noDraw);
+
+ /// Called when the [MarkerClusterer] is added to the map.
+ external void onAdd();
+
+ /// Called when the [MarkerClusterer] is removed from the map.
+ external void onRemove();
+
+ /// Returns the list of clusters.
+ List<MarkerClustererCluster> get clusters =>
+ _clusters.toDart.cast<MarkerClustererCluster>();
+ @JS('clusters')
+ external JSArray<JSAny> get _clusters;
+
+ /// Recalculates and draws all the marker clusters.
+ external void render();
+}
+
+/// Creates [MarkerClusterer] object with given [gmaps.GMap] and
+/// [ClusterClickHandler].
+MarkerClusterer createMarkerClusterer(
+ gmaps.GMap map, ClusterClickHandler onClusterClickHandler) {
+ final MarkerClustererOptions options = MarkerClustererOptions(
+ map: map,
+ onClusterClick: onClusterClickHandler,
+ );
+ return MarkerClusterer(options);
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart
index 26cb94f..962422f 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart
@@ -9,7 +9,9 @@
/// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers.
MarkersController({
required StreamController<MapEvent<Object?>> stream,
+ required ClusterManagersController clusterManagersController,
}) : _streamController = stream,
+ _clusterManagersController = clusterManagersController,
_markerIdToController = <MarkerId, MarkerController>{};
// A cache of [MarkerController]s indexed by their [MarkerId].
@@ -18,6 +20,8 @@
// The stream over which markers broadcast their events
final StreamController<MapEvent<Object?>> _streamController;
+ final ClusterManagersController _clusterManagersController;
+
/// Returns the cache of [MarkerController]s. Test only.
@visibleForTesting
Map<MarkerId, MarkerController> get markers => _markerIdToController;
@@ -53,9 +57,19 @@
final gmaps.MarkerOptions markerOptions =
_markerOptionsFromMarker(marker, currentMarker);
- final gmaps.Marker gmMarker = gmaps.Marker(markerOptions)..map = googleMap;
+
+ final gmaps.Marker gmMarker = gmaps.Marker(markerOptions);
+
+ gmMarker.set('markerId', marker.markerId.value);
+
+ if (marker.clusterManagerId != null) {
+ _clusterManagersController.addItem(marker.clusterManagerId!, gmMarker);
+ } else {
+ gmMarker.map = googleMap;
+ }
final MarkerController controller = MarkerController(
marker: gmMarker,
+ clusterManagerId: marker.clusterManagerId,
infoWindow: gmInfoWindow,
consumeTapEvents: marker.consumeTapEvents,
onTap: () {
@@ -84,16 +98,26 @@
final MarkerController? markerController =
_markerIdToController[marker.markerId];
if (markerController != null) {
- final gmaps.MarkerOptions markerOptions = _markerOptionsFromMarker(
- marker,
- markerController.marker,
- );
- final gmaps.InfoWindowOptions? infoWindow =
- _infoWindowOptionsFromMarker(marker);
- markerController.update(
- markerOptions,
- newInfoWindowContent: infoWindow?.content as HTMLElement?,
- );
+ final ClusterManagerId? oldClusterManagerId =
+ markerController.clusterManagerId;
+ final ClusterManagerId? newClusterManagerId = marker.clusterManagerId;
+
+ if (oldClusterManagerId != newClusterManagerId) {
+ // If clusterManagerId changes. Remove existing marker and create new one.
+ _removeMarker(marker.markerId);
+ _addMarker(marker);
+ } else {
+ final gmaps.MarkerOptions markerOptions = _markerOptionsFromMarker(
+ marker,
+ markerController.marker,
+ );
+ final gmaps.InfoWindowOptions? infoWindow =
+ _infoWindowOptionsFromMarker(marker);
+ markerController.update(
+ markerOptions,
+ newInfoWindowContent: infoWindow?.content as HTMLElement?,
+ );
+ }
}
}
@@ -104,6 +128,10 @@
void _removeMarker(MarkerId markerId) {
final MarkerController? markerController = _markerIdToController[markerId];
+ if (markerController?.clusterManagerId != null) {
+ _clusterManagersController.removeItem(
+ markerController!.clusterManagerId!, markerController.marker);
+ }
markerController?.remove();
_markerIdToController.remove(markerId);
}
@@ -151,7 +179,7 @@
void _onMarkerDragStart(MarkerId markerId, gmaps.LatLng latLng) {
_streamController.add(MarkerDragStartEvent(
mapId,
- _gmLatLngToLatLng(latLng),
+ gmLatLngToLatLng(latLng),
markerId,
));
}
@@ -159,7 +187,7 @@
void _onMarkerDrag(MarkerId markerId, gmaps.LatLng latLng) {
_streamController.add(MarkerDragEvent(
mapId,
- _gmLatLngToLatLng(latLng),
+ gmLatLngToLatLng(latLng),
markerId,
));
}
@@ -167,7 +195,7 @@
void _onMarkerDragEnd(MarkerId markerId, gmaps.LatLng latLng) {
_streamController.add(MarkerDragEndEvent(
mapId,
- _gmLatLngToLatLng(latLng),
+ gmLatLngToLatLng(latLng),
markerId,
));
}
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml
index f5c1793..3447e8b 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml
+++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml
@@ -2,7 +2,7 @@
description: Web platform implementation of google_maps_flutter
repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_web
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22
-version: 0.5.6+2
+version: 0.5.7
environment:
sdk: ^3.3.0
@@ -23,7 +23,7 @@
flutter_web_plugins:
sdk: flutter
google_maps: ^7.1.0
- google_maps_flutter_platform_interface: ^2.5.0
+ google_maps_flutter_platform_interface: ^2.6.0
sanitize_html: ^2.0.0
stream_transform: ^2.0.0
web: ^0.5.1