Skip to content

Commit c0d37fd

Browse files
jokerttuiandis
authored andcommitted
[google_maps_flutter] Add support for marker clustering - platform interface (flutter#6158)
This PR introduces support for marker clustering This is prequel PR for: flutter#4319 Containing only changes to `google_maps_flutter_platform_interface` package. Follow up PR:s will hold the platform and app-facing plugin implementations. Linked issue: flutter/flutter#26863
1 parent f7f8b38 commit c0d37fd

19 files changed

+455
-4
lines changed

packages/google_maps_flutter/google_maps_flutter_platform_interface/AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,4 @@ Aleksandr Yurkovskiy <[email protected]>
6464
Anton Borries <[email protected]>
6565
6666
Rahul Raj <[email protected]>
67+
Joonas Kerttula <[email protected]>

packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.6.0
2+
3+
* Adds support for marker clustering.
4+
15
## 2.5.0
26

37
* Adds `style` to the `MapConfiguration` to allow setting style as part of

packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,12 @@ class MapLongPressEvent extends _PositionedMapEvent<void> {
167167
/// The `position` of this event is the LatLng where the Map was long pressed.
168168
MapLongPressEvent(int mapId, LatLng position) : super(mapId, position, null);
169169
}
170+
171+
/// An event fired when a cluster icon managed by [ClusterManager] is tapped.
172+
class ClusterTapEvent extends MapEvent<Cluster> {
173+
/// Build a ClusterTapEvent Event triggered from the map represented by `mapId`.
174+
///
175+
/// The `value` of this event is a [Cluster] object that represents the tapped
176+
/// cluster icon managed by [ClusterManager].
177+
ClusterTapEvent(super.mapId, super.cluster);
178+
}

packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,11 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform {
166166
return _events(mapId).whereType<MapLongPressEvent>();
167167
}
168168

169+
@override
170+
Stream<ClusterTapEvent> onClusterTap({required int mapId}) {
171+
return _events(mapId).whereType<ClusterTapEvent>();
172+
}
173+
169174
Future<dynamic> _handleMethodCall(MethodCall call, int mapId) async {
170175
switch (call.method) {
171176
case 'camera#onMoveStarted':
@@ -258,6 +263,36 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform {
258263
arguments['zoom'] as int?,
259264
);
260265
return tile.toJson();
266+
case 'cluster#onTap':
267+
final Map<String, Object?> arguments = _getArgumentDictionary(call);
268+
final ClusterManagerId clusterManagerId =
269+
ClusterManagerId(arguments['clusterManagerId']! as String);
270+
final LatLng position = LatLng.fromJson(arguments['position'])!;
271+
272+
final Map<String, List<dynamic>> latLngData =
273+
(arguments['bounds']! as Map<dynamic, dynamic>).map(
274+
(dynamic key, dynamic object) =>
275+
MapEntry<String, List<dynamic>>(
276+
key as String, object as List<dynamic>));
277+
278+
final LatLngBounds bounds = LatLngBounds(
279+
northeast: LatLng.fromJson(latLngData['northeast'])!,
280+
southwest: LatLng.fromJson(latLngData['southwest'])!);
281+
282+
final List<MarkerId> markerIds =
283+
(arguments['markerIds']! as List<dynamic>)
284+
.map((dynamic markerId) => MarkerId(markerId as String))
285+
.toList();
286+
287+
_mapEventStreamController.add(ClusterTapEvent(
288+
mapId,
289+
Cluster(
290+
clusterManagerId,
291+
markerIds,
292+
position: position,
293+
bounds: bounds,
294+
),
295+
));
261296
default:
262297
throw MissingPluginException();
263298
}
@@ -347,6 +382,17 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform {
347382
);
348383
}
349384

385+
@override
386+
Future<void> updateClusterManagers(
387+
ClusterManagerUpdates clusterManagerUpdates, {
388+
required int mapId,
389+
}) {
390+
return channel(mapId).invokeMethod<void>(
391+
'clusterManagers#update',
392+
clusterManagerUpdates.toJson(),
393+
);
394+
}
395+
350396
@override
351397
Future<void> clearTileCache(
352398
TileOverlayId tileOverlayId, {
@@ -585,6 +631,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform {
585631
Set<Polyline> polylines = const <Polyline>{},
586632
Set<Circle> circles = const <Circle>{},
587633
Set<TileOverlay> tileOverlays = const <TileOverlay>{},
634+
Set<ClusterManager> clusterManagers = const <ClusterManager>{},
588635
Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers,
589636
Map<String, dynamic> mapOptions = const <String, dynamic>{},
590637
}) {
@@ -599,6 +646,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform {
599646
polygons: polygons,
600647
polylines: polylines,
601648
circles: circles,
649+
clusterManagers: clusterManagers,
602650
tileOverlays: tileOverlays),
603651
mapOptions: mapOptions,
604652
);
@@ -614,6 +662,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform {
614662
Set<Polyline> polylines = const <Polyline>{},
615663
Set<Circle> circles = const <Circle>{},
616664
Set<TileOverlay> tileOverlays = const <TileOverlay>{},
665+
Set<ClusterManager> clusterManagers = const <ClusterManager>{},
617666
Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers,
618667
Map<String, dynamic> mapOptions = const <String, dynamic>{},
619668
}) {
@@ -627,6 +676,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform {
627676
polylines: polylines,
628677
circles: circles,
629678
tileOverlays: tileOverlays,
679+
clusterManagers: clusterManagers,
630680
gestureRecognizers: gestureRecognizers,
631681
mapOptions: mapOptions,
632682
);

packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,20 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface {
142142
throw UnimplementedError('updateTileOverlays() has not been implemented.');
143143
}
144144

145+
/// Updates cluster manager configuration.
146+
///
147+
/// Change listeners are notified once the update has been made on the
148+
/// platform side.
149+
///
150+
/// The returned [Future] completes after listeners have been notified.
151+
Future<void> updateClusterManagers(
152+
ClusterManagerUpdates clusterManagerUpdates, {
153+
required int mapId,
154+
}) {
155+
throw UnimplementedError(
156+
'updateClusterManagers() has not been implemented.');
157+
}
158+
145159
/// Clears the tile cache so that all tiles will be requested again from the
146160
/// [TileProvider].
147161
///
@@ -357,6 +371,11 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface {
357371
throw UnimplementedError('onLongPress() has not been implemented.');
358372
}
359373

374+
/// A marker icon managed by [ClusterManager] has been tapped.
375+
Stream<ClusterTapEvent> onClusterTap({required int mapId}) {
376+
throw UnimplementedError('onClusterTap() has not been implemented.');
377+
}
378+
360379
/// Dispose of whatever resources the `mapId` is holding on to.
361380
void dispose({required int mapId}) {
362381
throw UnimplementedError('dispose() has not been implemented.');

packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_inspector_platform.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,10 @@ abstract class GoogleMapsInspectorPlatform extends PlatformInterface {
115115
{required int mapId}) {
116116
throw UnimplementedError('getTileOverlayInfo() has not been implemented.');
117117
}
118+
119+
/// Returns current clusters from [ClusterManager].
120+
Future<List<Cluster>> getClusters(
121+
{required int mapId, required ClusterManagerId clusterManagerId}) {
122+
throw UnimplementedError('getClusters() has not been implemented.');
123+
}
118124
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/foundation.dart'
6+
show immutable, listEquals, objectRuntimeType;
7+
import 'types.dart';
8+
9+
/// A cluster containing multiple markers.
10+
@immutable
11+
class Cluster {
12+
/// Creates a cluster with its location [LatLng], bounds [LatLngBounds],
13+
/// and list of [MarkerId]s in the cluster.
14+
const Cluster(
15+
this.clusterManagerId,
16+
this.markerIds, {
17+
required this.position,
18+
required this.bounds,
19+
}) : assert(markerIds.length > 0);
20+
21+
/// ID of the [ClusterManager] of the cluster.
22+
final ClusterManagerId clusterManagerId;
23+
24+
/// Cluster marker location.
25+
final LatLng position;
26+
27+
/// The bounds containing all cluster markers.
28+
final LatLngBounds bounds;
29+
30+
/// List of [MarkerId]s in the cluster.
31+
final List<MarkerId> markerIds;
32+
33+
/// Returns the number of markers in the cluster.
34+
int get count => markerIds.length;
35+
36+
@override
37+
String toString() =>
38+
'${objectRuntimeType(this, 'Cluster')}($clusterManagerId, $position, $bounds, $markerIds)';
39+
40+
@override
41+
bool operator ==(Object other) {
42+
return other is Cluster &&
43+
other.clusterManagerId == clusterManagerId &&
44+
other.position == position &&
45+
other.bounds == bounds &&
46+
listEquals(other.markerIds, markerIds);
47+
}
48+
49+
@override
50+
int get hashCode =>
51+
Object.hash(clusterManagerId, position, bounds, markerIds);
52+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/foundation.dart' show immutable;
6+
import 'types.dart';
7+
8+
/// Uniquely identifies a [ClusterManager] among [GoogleMap] clusters.
9+
///
10+
/// This does not have to be globally unique, only unique among the list.
11+
@immutable
12+
class ClusterManagerId extends MapsObjectId<ClusterManager> {
13+
/// Creates an immutable identifier for a [ClusterManager].
14+
const ClusterManagerId(super.value);
15+
}
16+
17+
/// [ClusterManager] manages marker clustering for set of [Marker]s that have
18+
/// the same [ClusterManagerId] set.
19+
@immutable
20+
class ClusterManager implements MapsObject<ClusterManager> {
21+
/// Creates an immutable object for managing clustering for set of markers.
22+
const ClusterManager({
23+
required this.clusterManagerId,
24+
this.onClusterTap,
25+
});
26+
27+
/// Uniquely identifies a [ClusterManager].
28+
final ClusterManagerId clusterManagerId;
29+
30+
@override
31+
ClusterManagerId get mapsId => clusterManagerId;
32+
33+
/// Callback to receive tap events for cluster markers placed on this map.
34+
final ArgumentCallback<Cluster>? onClusterTap;
35+
36+
/// Creates a new [ClusterManager] object whose values are the same as this instance,
37+
/// unless overwritten by the specified parameters.
38+
ClusterManager copyWith({
39+
ArgumentCallback<Cluster>? onClusterTapParam,
40+
}) {
41+
return ClusterManager(
42+
clusterManagerId: clusterManagerId,
43+
onClusterTap: onClusterTapParam ?? onClusterTap,
44+
);
45+
}
46+
47+
/// Creates a new [ClusterManager] object whose values are the same as this instance.
48+
@override
49+
ClusterManager clone() => copyWith();
50+
51+
/// Converts this object to something serializable in JSON.
52+
@override
53+
Object toJson() {
54+
final Map<String, Object> json = <String, Object>{};
55+
56+
void addIfPresent(String fieldName, Object? value) {
57+
if (value != null) {
58+
json[fieldName] = value;
59+
}
60+
}
61+
62+
addIfPresent('clusterManagerId', clusterManagerId.value);
63+
return json;
64+
}
65+
66+
@override
67+
bool operator ==(Object other) {
68+
if (other.runtimeType != runtimeType) {
69+
return false;
70+
}
71+
return other is ClusterManager &&
72+
clusterManagerId == other.clusterManagerId;
73+
}
74+
75+
@override
76+
int get hashCode => clusterManagerId.hashCode;
77+
78+
@override
79+
String toString() {
80+
return 'Cluster{clusterManagerId: $clusterManagerId, onClusterTap: $onClusterTap}';
81+
}
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'types.dart';
6+
7+
/// [ClusterManager] update events to be applied to the [GoogleMap].
8+
///
9+
/// Used in [GoogleMapController] when the map is updated.
10+
// (Do not re-export)
11+
class ClusterManagerUpdates extends MapsObjectUpdates<ClusterManager> {
12+
/// Computes [ClusterManagerUpdates] given previous and current [ClusterManager]s.
13+
ClusterManagerUpdates.from(super.previous, super.current)
14+
: super.from(objectName: 'clusterManager');
15+
16+
/// Set of Clusters to be added in this update.
17+
Set<ClusterManager> get clusterManagersToAdd => objectsToAdd;
18+
19+
/// Set of ClusterManagerIds to be removed in this update.
20+
Set<ClusterManagerId> get clusterManagerIdsToRemove =>
21+
objectIdsToRemove.cast<ClusterManagerId>();
22+
23+
/// Set of Clusters to be changed in this update.
24+
Set<ClusterManager> get clusterManagersToChange => objectsToChange;
25+
}

packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_objects.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ class MapObjects {
2121
this.polylines = const <Polyline>{},
2222
this.circles = const <Circle>{},
2323
this.tileOverlays = const <TileOverlay>{},
24+
this.clusterManagers = const <ClusterManager>{},
2425
});
2526

2627
final Set<Marker> markers;
2728
final Set<Polygon> polygons;
2829
final Set<Polyline> polylines;
2930
final Set<Circle> circles;
3031
final Set<TileOverlay> tileOverlays;
32+
final Set<ClusterManager> clusterManagers;
3133
}

0 commit comments

Comments
 (0)