Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[google_maps_flutter] Marker clustering support #6752

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,107 @@ void main() {
expect(tileOverlayInfo1, isNull);
},
);

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 tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: 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 tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: 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,
);
}

class _DebugTileProvider implements TileProvider {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
// 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: public_member_api_docs

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

import 'page.dart';

class ClusteringPage extends GoogleMapExampleAppPage {
const ClusteringPage({Key? key})
: super(const Icon(Icons.place), 'Manage clustering', key: key);

@override
Widget build(BuildContext context) {
return const ClusteringBody();
}
}

class ClusteringBody extends StatefulWidget {
const ClusteringBody({Key? key}) : super(key: key);

@override
State<StatefulWidget> createState() => ClusteringBodyState();
}

typedef MarkerUpdateAction = Marker Function(Marker marker);

class ClusteringBodyState extends State<ClusteringBody> {
ClusteringBodyState();
static const LatLng center = LatLng(-33.86711, 151.1547171);
static const double _scaleFactor = 0.05;

GoogleMapController? controller;
Map<ClusterManagerId, ClusterManager> clusterManagers =
<ClusterManagerId, ClusterManager>{};
Map<MarkerId, Marker> markers = <MarkerId, Marker>{};
MarkerId? selectedMarker;
int _clusterManagerIdCounter = 1;
int _markerIdCounter = 1;
Cluster? lastCluster;

// ignore: use_setters_to_change_properties
void _onMapCreated(GoogleMapController controller) {
this.controller = controller;
}

@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() {
final int clusterManagerCount = clusterManagers.length;

if (clusterManagerCount == 3) {
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 < 15; i++) {
final String markerIdVal =
'${clusterManager.clusterManagerId.value}_marker_id_$_markerIdCounter';
_markerIdCounter++;
final MarkerId markerId = MarkerId(markerIdVal);

final int clusterManagerIndex =
clusterManagers.values.toList().indexOf(clusterManager);
final Marker marker = Marker(
clusterManagerId: clusterManager.clusterManagerId,
markerId: markerId,
position: LatLng(
center.latitude + _getRandomOffset(),
center.longitude +
_getRandomOffset() +
clusterManagerIndex * _scaleFactor * 2,
),
infoWindow: InfoWindow(title: markerIdVal, snippet: '*'),
onTap: () => _onMarkerTapped(markerId),
);
markers[markerId] = marker;
}
setState(() {});
}

double _getRandomOffset() {
return (Random().nextDouble() - 0.5) * _scaleFactor;
}

void _remove(MarkerId markerId) {
setState(() {
if (markers.containsKey(markerId)) {
markers.remove(markerId);
}
});
}

void _changeMarkersRotation() {
for (final MarkerId markerId in markers.keys) {
final Marker marker = markers[markerId]!;
final double current = marker.rotation;
markers[markerId] = marker.copyWith(
rotationParam: current == 315.0 ? 0.0 : current + 45.0,
);
}
setState(() {});
}

@override
Widget build(BuildContext context) {
final MarkerId? selectedId = selectedMarker;
return Stack(children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
child: GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: const CameraPosition(
target: LatLng(-33.852, 151.211),
zoom: 11.0,
),
markers: Set<Marker>.of(markers.values),
clusterManagers: Set<ClusterManager>.of(clusterManagers.values),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
TextButton(
onPressed: () => _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 : () => _changeMarkersRotation(),
child: const Text('Change all markers rotation'),
),
],
),
if (lastCluster != null)
Padding(
padding: const EdgeInsets.all(10),
child: Text(
'Cluster with ${lastCluster!.count} markers clicked at ${lastCluster!.position}')),
],
),
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:google_maps_flutter_android/google_maps_flutter_android.dart';
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';

import 'animate_camera.dart';
import 'clustering.dart';
import 'lite_mode.dart';
import 'map_click.dart';
import 'map_coordinates.dart';
Expand Down Expand Up @@ -39,6 +40,7 @@ final List<GoogleMapExampleAppPage> _allPages = <GoogleMapExampleAppPage>[
const SnapshotPage(),
const LiteModePage(),
const TileOverlayPage(),
const ClusteringPage(),
];

/// MapsDemo is the Main Application.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,13 @@ flutter:
uses-material-design: true
assets:
- assets/


# FOR TESTING ONLY. DO NOT MERGE.
dependency_overrides:
google_maps_flutter_android:
path: ../../../google_maps_flutter/google_maps_flutter_android
google_maps_flutter_ios:
path: ../../../google_maps_flutter/google_maps_flutter_ios
google_maps_flutter_platform_interface:
path: ../../../google_maps_flutter/google_maps_flutter_platform_interface
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf
Cap,
Circle,
CircleId,
Cluster,
ClusterManager,
ClusterManagerId,
InfoWindow,
JointType,
LatLng,
Expand Down
Loading