Skip to content

Commit

Permalink
[google_maps_flutter] Add support for marker clustering - platform in…
Browse files Browse the repository at this point in the history
…terface (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
  • Loading branch information
jokerttu authored and LouiseHsu committed Mar 7, 2024
1 parent 0e20e3b commit 6792a47
Show file tree
Hide file tree
Showing 19 changed files with 455 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy <sanekyy@gmail.com>
Anton Borries <mail@antonborri.es>
Alex Li <google@alexv525.com>
Rahul Raj <64.rahulraj@gmail.com>
Joonas Kerttula <joonas.kerttula@codemate.com>
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.6.0

* Adds support for marker clustering.

## 2.5.0

* Adds `style` to the `MapConfiguration` to allow setting style as part of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,12 @@ class MapLongPressEvent extends _PositionedMapEvent<void> {
/// The `position` of this event is the LatLng where the Map was long pressed.
MapLongPressEvent(int mapId, LatLng position) : super(mapId, position, null);
}

/// An event fired when a cluster icon managed by [ClusterManager] is tapped.
class ClusterTapEvent extends MapEvent<Cluster> {
/// Build a ClusterTapEvent Event triggered from the map represented by `mapId`.
///
/// The `value` of this event is a [Cluster] object that represents the tapped
/// cluster icon managed by [ClusterManager].
ClusterTapEvent(super.mapId, super.cluster);
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform {
return _events(mapId).whereType<MapLongPressEvent>();
}

@override
Stream<ClusterTapEvent> onClusterTap({required int mapId}) {
return _events(mapId).whereType<ClusterTapEvent>();
}

Future<dynamic> _handleMethodCall(MethodCall call, int mapId) async {
switch (call.method) {
case 'camera#onMoveStarted':
Expand Down Expand Up @@ -258,6 +263,36 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform {
arguments['zoom'] as int?,
);
return tile.toJson();
case 'cluster#onTap':
final Map<String, Object?> arguments = _getArgumentDictionary(call);
final ClusterManagerId clusterManagerId =
ClusterManagerId(arguments['clusterManagerId']! as String);
final LatLng position = LatLng.fromJson(arguments['position'])!;

final Map<String, List<dynamic>> latLngData =
(arguments['bounds']! as Map<dynamic, dynamic>).map(
(dynamic key, dynamic object) =>
MapEntry<String, List<dynamic>>(
key as String, object as List<dynamic>));

final LatLngBounds bounds = LatLngBounds(
northeast: LatLng.fromJson(latLngData['northeast'])!,
southwest: LatLng.fromJson(latLngData['southwest'])!);

final List<MarkerId> markerIds =
(arguments['markerIds']! as List<dynamic>)
.map((dynamic markerId) => MarkerId(markerId as String))
.toList();

_mapEventStreamController.add(ClusterTapEvent(
mapId,
Cluster(
clusterManagerId,
markerIds,
position: position,
bounds: bounds,
),
));
default:
throw MissingPluginException();
}
Expand Down Expand Up @@ -347,6 +382,17 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform {
);
}

@override
Future<void> updateClusterManagers(
ClusterManagerUpdates clusterManagerUpdates, {
required int mapId,
}) {
return channel(mapId).invokeMethod<void>(
'clusterManagers#update',
clusterManagerUpdates.toJson(),
);
}

@override
Future<void> clearTileCache(
TileOverlayId tileOverlayId, {
Expand Down Expand Up @@ -585,6 +631,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform {
Set<Polyline> polylines = const <Polyline>{},
Set<Circle> circles = const <Circle>{},
Set<TileOverlay> tileOverlays = const <TileOverlay>{},
Set<ClusterManager> clusterManagers = const <ClusterManager>{},
Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers,
Map<String, dynamic> mapOptions = const <String, dynamic>{},
}) {
Expand All @@ -599,6 +646,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform {
polygons: polygons,
polylines: polylines,
circles: circles,
clusterManagers: clusterManagers,
tileOverlays: tileOverlays),
mapOptions: mapOptions,
);
Expand All @@ -614,6 +662,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform {
Set<Polyline> polylines = const <Polyline>{},
Set<Circle> circles = const <Circle>{},
Set<TileOverlay> tileOverlays = const <TileOverlay>{},
Set<ClusterManager> clusterManagers = const <ClusterManager>{},
Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers,
Map<String, dynamic> mapOptions = const <String, dynamic>{},
}) {
Expand All @@ -627,6 +676,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform {
polylines: polylines,
circles: circles,
tileOverlays: tileOverlays,
clusterManagers: clusterManagers,
gestureRecognizers: gestureRecognizers,
mapOptions: mapOptions,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,20 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface {
throw UnimplementedError('updateTileOverlays() has not been implemented.');
}

/// 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<void> updateClusterManagers(
ClusterManagerUpdates clusterManagerUpdates, {
required int mapId,
}) {
throw UnimplementedError(
'updateClusterManagers() has not been implemented.');
}

/// Clears the tile cache so that all tiles will be requested again from the
/// [TileProvider].
///
Expand Down Expand Up @@ -357,6 +371,11 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface {
throw UnimplementedError('onLongPress() has not been implemented.');
}

/// A marker icon managed by [ClusterManager] has been tapped.
Stream<ClusterTapEvent> onClusterTap({required int mapId}) {
throw UnimplementedError('onClusterTap() has not been implemented.');
}

/// Dispose of whatever resources the `mapId` is holding on to.
void dispose({required int mapId}) {
throw UnimplementedError('dispose() has not been implemented.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,10 @@ abstract class GoogleMapsInspectorPlatform extends PlatformInterface {
{required int mapId}) {
throw UnimplementedError('getTileOverlayInfo() has not been implemented.');
}

/// Returns current clusters from [ClusterManager].
Future<List<Cluster>> getClusters(
{required int mapId, required ClusterManagerId clusterManagerId}) {
throw UnimplementedError('getClusters() has not been implemented.');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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/foundation.dart'
show immutable, listEquals, objectRuntimeType;
import 'types.dart';

/// A cluster containing multiple markers.
@immutable
class Cluster {
/// Creates a cluster with its location [LatLng], bounds [LatLngBounds],
/// and list of [MarkerId]s in the cluster.
const Cluster(
this.clusterManagerId,
this.markerIds, {
required this.position,
required this.bounds,
}) : assert(markerIds.length > 0);

/// ID of the [ClusterManager] of the cluster.
final ClusterManagerId clusterManagerId;

/// Cluster marker location.
final LatLng position;

/// The bounds containing all cluster markers.
final LatLngBounds bounds;

/// List of [MarkerId]s in the cluster.
final List<MarkerId> markerIds;

/// Returns the number of markers in the cluster.
int get count => markerIds.length;

@override
String toString() =>
'${objectRuntimeType(this, 'Cluster')}($clusterManagerId, $position, $bounds, $markerIds)';

@override
bool operator ==(Object other) {
return other is Cluster &&
other.clusterManagerId == clusterManagerId &&
other.position == position &&
other.bounds == bounds &&
listEquals(other.markerIds, markerIds);
}

@override
int get hashCode =>
Object.hash(clusterManagerId, position, bounds, markerIds);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// 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/foundation.dart' show immutable;
import 'types.dart';

/// Uniquely identifies a [ClusterManager] among [GoogleMap] clusters.
///
/// This does not have to be globally unique, only unique among the list.
@immutable
class ClusterManagerId extends MapsObjectId<ClusterManager> {
/// Creates an immutable identifier for a [ClusterManager].
const ClusterManagerId(super.value);
}

/// [ClusterManager] manages marker clustering for set of [Marker]s that have
/// the same [ClusterManagerId] set.
@immutable
class ClusterManager implements MapsObject<ClusterManager> {
/// Creates an immutable object for managing clustering for set of markers.
const ClusterManager({
required this.clusterManagerId,
this.onClusterTap,
});

/// Uniquely identifies a [ClusterManager].
final ClusterManagerId clusterManagerId;

@override
ClusterManagerId get mapsId => clusterManagerId;

/// Callback to receive tap events for cluster markers placed on this map.
final ArgumentCallback<Cluster>? onClusterTap;

/// Creates a new [ClusterManager] object whose values are the same as this instance,
/// unless overwritten by the specified parameters.
ClusterManager copyWith({
ArgumentCallback<Cluster>? onClusterTapParam,
}) {
return ClusterManager(
clusterManagerId: clusterManagerId,
onClusterTap: onClusterTapParam ?? onClusterTap,
);
}

/// Creates a new [ClusterManager] object whose values are the same as this instance.
@override
ClusterManager clone() => copyWith();

/// Converts this object to something serializable in JSON.
@override
Object toJson() {
final Map<String, Object> json = <String, Object>{};

void addIfPresent(String fieldName, Object? value) {
if (value != null) {
json[fieldName] = value;
}
}

addIfPresent('clusterManagerId', clusterManagerId.value);
return json;
}

@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is ClusterManager &&
clusterManagerId == other.clusterManagerId;
}

@override
int get hashCode => clusterManagerId.hashCode;

@override
String toString() {
return 'Cluster{clusterManagerId: $clusterManagerId, onClusterTap: $onClusterTap}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// 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 'types.dart';

/// [ClusterManager] update events to be applied to the [GoogleMap].
///
/// Used in [GoogleMapController] when the map is updated.
// (Do not re-export)
class ClusterManagerUpdates extends MapsObjectUpdates<ClusterManager> {
/// Computes [ClusterManagerUpdates] given previous and current [ClusterManager]s.
ClusterManagerUpdates.from(super.previous, super.current)
: super.from(objectName: 'clusterManager');

/// Set of Clusters to be added in this update.
Set<ClusterManager> get clusterManagersToAdd => objectsToAdd;

/// Set of ClusterManagerIds to be removed in this update.
Set<ClusterManagerId> get clusterManagerIdsToRemove =>
objectIdsToRemove.cast<ClusterManagerId>();

/// Set of Clusters to be changed in this update.
Set<ClusterManager> get clusterManagersToChange => objectsToChange;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ class MapObjects {
this.polylines = const <Polyline>{},
this.circles = const <Circle>{},
this.tileOverlays = const <TileOverlay>{},
this.clusterManagers = const <ClusterManager>{},
});

final Set<Marker> markers;
final Set<Polygon> polygons;
final Set<Polyline> polylines;
final Set<Circle> circles;
final Set<TileOverlay> tileOverlays;
final Set<ClusterManager> clusterManagers;
}
Loading

0 comments on commit 6792a47

Please sign in to comment.