Skip to content

Commit

Permalink
[google_maps_flutter] Add clustering support
Browse files Browse the repository at this point in the history
  • Loading branch information
jokerttu committed Aug 6, 2024
1 parent db8bfed commit bf56884
Show file tree
Hide file tree
Showing 10 changed files with 454 additions and 2 deletions.
4 changes: 4 additions & 0 deletions packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.8.0

* Adds clustering support.

## 2.7.1

* Updates the example app to use TLHC mode, per current package guidance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MarkerId, Marker> markers = <MarkerId, Marker>{};
final Set<ClusterManager> clusterManagers = <ClusterManager>{};

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<GoogleMapController> controllerCompleter =
Completer<GoogleMapController>();

await pumpMap(
tester,
GoogleMap(
key: key,
initialCameraPosition: kInitialCameraPosition,
clusterManagers: clusterManagers,
markers: Set<Marker>.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<Cluster> clusters = await inspector.getClusters(
mapId: controller.mapId, clusterManagerId: cm.clusterManagerId);
final int markersAmountForClusterManager = clusters
.map<int>((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<MarkerId, Marker> entry in markers.entries) {
markers[entry.key] = _copyMarkerWithClusterManagerId(entry.value, null);
}

await pumpMap(
tester,
GoogleMap(
key: key,
initialCameraPosition: kInitialCameraPosition,
clusterManagers: clusterManagers,
markers: Set<Marker>.of(markers.values)),
);

for (final ClusterManager cm in clusterManagers) {
final List<Cluster> 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,
);
}
Original file line number Diff line number Diff line change
@@ -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<StatefulWidget> createState() => ClusteringBodyState();
}

/// State of the clustering page.
class ClusteringBodyState extends State<ClusteringBody> {
/// 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<ClusterManagerId, ClusterManager> clusterManagers =
<ClusterManagerId, ClusterManager>{};

/// Map of markers with identifier as the key.
Map<MarkerId, Marker> markers = <MarkerId, Marker>{};

/// 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: <Widget>[
SizedBox(
height: 300.0,
child: GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: const CameraPosition(
target: LatLng(-33.852, 151.25),
zoom: 11.0,
),
markers: Set<Marker>.of(markers.values),
clusterManagers: Set<ClusterManager>.of(clusterManagers.values),
),
),
Column(children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
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: <Widget>[
for (final MapEntry<ClusterManagerId, ClusterManager> clusterEntry
in clusterManagers.entries)
TextButton(
onPressed: () => _addMarkersToCluster(clusterEntry.value),
child: Text('Add markers to ${clusterEntry.key.value}'),
),
],
),
Wrap(
alignment: WrapAlignment.spaceEvenly,
children: <Widget>[
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}')),
]),
],
);
}
}
Loading

0 comments on commit bf56884

Please sign in to comment.