Skip to content
Merged
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
@@ -1,3 +1,7 @@
## 0.6.1

* Batches clustered marker add/remove operations to avoid redundant re-rendering.

## 0.6.0

* **BREAKING CHANGES**: Adds type constraints to generic type parameters:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ 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:google_maps_flutter_web/src/google_maps_inspector_web.dart';
import 'package:google_maps_flutter_web/src/marker_clustering.dart';
import 'package:integration_test/integration_test.dart';

void main() {
Expand All @@ -23,9 +25,8 @@ void main() {
const initialCameraPosition = CameraPosition(target: mapCenter);

group('MarkersController', () {
const testMapId = 33930;

testWidgets('Marker clustering', (WidgetTester tester) async {
const testMapId = 33930;
const clusterManagerId = ClusterManagerId('cluster 1');

final clusterManagers = <ClusterManager>{
Expand Down Expand Up @@ -67,8 +68,6 @@ void main() {
final int mapId = await mapIdCompleter.future;
expect(mapId, equals(testMapId));

addTearDown(() => plugin.dispose(mapId: mapId));

final List<Cluster> clusters =
await waitForValueMatchingPredicate<List<Cluster>>(
tester,
Expand Down Expand Up @@ -105,6 +104,83 @@ void main() {

expect(updatedClusters.length, 0);
});

testWidgets('clusters render once per batched add', (
WidgetTester tester,
) async {
const clusterManagerId = ClusterManagerId('cluster 1');

final clusterManagers = <ClusterManager>{
const ClusterManager(clusterManagerId: clusterManagerId),
};

// Create the marker with clusterManagerId.
final initialMarkers = <Marker>{
for (var i = 0; i < 3; i++)
Marker(
markerId: MarkerId(i.toString()),
position: mapCenter,
clusterManagerId: clusterManagerId,
),
};

final markersCluster1 = <Marker>{
for (var i = 3; i < 7; i++)
Marker(
markerId: MarkerId(i.toString()),
clusterManagerId: clusterManagerId,
position: mapCenter,
),
};

const testMapId = 33931;
final events = StreamController<ClusteringEvent>();
await _pumpMap(
tester,
plugin.buildViewWithConfiguration(
testMapId,
(int id) async {
final StreamSubscription<ClusteringEvent>? subscription =
(inspector as GoogleMapsInspectorWeb)
.getClusteringEvents(
mapId: testMapId,
clusterManagerId: clusterManagerId,
)
?.listen(events.add);

await plugin.updateMarkers(
MarkerUpdates.from(initialMarkers, markersCluster1),
mapId: testMapId,
);

await Future<void>.delayed(const Duration(seconds: 1));
await subscription?.cancel();
await events.close();
},
widgetConfiguration: const MapWidgetConfiguration(
initialCameraPosition: initialCameraPosition,
textDirection: TextDirection.ltr,
),
mapObjects: MapObjects(
clusterManagers: clusterManagers,
markers: initialMarkers,
),
),
);

await expectLater(
events.stream,
emitsInAnyOrder([
// Once per initial markers
ClusteringEvent.begin,
ClusteringEvent.end,
// Once per new cluster
ClusteringEvent.begin,
ClusteringEvent.end,
emitsDone,
]),
);
});
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'dart:js_interop';
import 'dart:typed_data';

import 'package:flutter/foundation.dart' show visibleForTesting;
import 'package:google_maps/google_maps.dart' as gmaps;
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';

Expand Down Expand Up @@ -150,4 +151,15 @@ class GoogleMapsInspectorWeb extends GoogleMapsInspectorPlatform {
)?.getClusters(clusterManagerId) ??
<Cluster>[];
}

/// Returns the stream of clustering events for a given [ClusterManager].
@visibleForTesting
Stream<ClusteringEvent>? getClusteringEvents({
required int mapId,
required ClusterManagerId clusterManagerId,
}) {
return _clusterManagersControllerProvider(
mapId,
)?.getClustererEvents(clusterManagerId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ abstract class MarkerController<T, O> {
///
/// This cannot be called after [remove].
void showInfoWindow();

/// Sets the map of the wrapped marker object.
void setMap(gmaps.Map map);
}

/// A `MarkerController` that wraps a [gmaps.Marker] object.
Expand Down Expand Up @@ -203,6 +206,9 @@ class LegacyMarkerController
_infoWindow.content = newInfoWindowContent;
}
}

@override
void setMap(gmaps.Map map) => _marker?.map = map;
}

/// A `MarkerController` that wraps a [gmaps.AdvancedMarkerElement] object.
Expand Down Expand Up @@ -324,4 +330,7 @@ class AdvancedMarkerController
_infoWindow.content = newInfoWindowContent;
}
}

@override
void setMap(gmaps.Map map) => _marker?.map = map;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,19 @@ 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 'marker_clustering_js_interop.dart' hide getClustererEvents;
import 'marker_clustering_js_interop.dart' as interop show getClustererEvents;
import 'types.dart';

/// Events emitted by the marker clustering lifecycle.
enum ClusteringEvent {
/// Clustering has started.
begin,

/// Clustering finished and clusters are available.
end,
}

/// A controller class for managing marker clustering.
///
/// This class maps [ClusterManager] objects to javascript [MarkerClusterer]
Expand Down Expand Up @@ -84,8 +94,19 @@ class ClusterManagersController<T extends Object> extends GeometryController {
}
}

/// Removes given marker from the [MarkerClusterer] with
/// given [ClusterManagerId].
/// Adds given list of [gmaps.Marker] to the [MarkerClusterer] with given
/// [ClusterManagerId].
void addItems(ClusterManagerId clusterManagerId, List<T> markers) {
final MarkerClusterer<T>? markerClusterer =
_clusterManagerIdToMarkerClusterer[clusterManagerId];
if (markerClusterer != null) {
markerClusterer.addMarkers(markers, true);
markerClusterer.render();
}
}

/// Removes given [gmaps.Marker] from the [MarkerClusterer] with given
/// [ClusterManagerId].
void removeItem(ClusterManagerId clusterManagerId, T? marker) {
if (marker != null) {
final MarkerClusterer<T>? markerClusterer =
Expand All @@ -97,6 +118,19 @@ class ClusterManagersController<T extends Object> extends GeometryController {
}
}

/// Removes given markers from the [MarkerClusterer] with given
/// [ClusterManagerId].
void removeItems(ClusterManagerId clusterManagerId, List<T>? markers) {
if (markers != null) {
final MarkerClusterer<T>? markerClusterer =
_clusterManagerIdToMarkerClusterer[clusterManagerId];
if (markerClusterer != null) {
markerClusterer.removeMarkers(markers, true);
markerClusterer.render();
}
}
}

/// Returns list of clusters in [MarkerClusterer] with given
/// [ClusterManagerId].
List<Cluster> getClusters(ClusterManagerId clusterManagerId) {
Expand All @@ -113,6 +147,13 @@ class ClusterManagersController<T extends Object> extends GeometryController {
return <Cluster>[];
}

/// Returns the stream of clustering lifecycle events for the given manager.
Stream<ClusteringEvent>? getClustererEvents(
ClusterManagerId clusterManagerId,
) => interop.getClustererEvents(
_clusterManagerIdToMarkerClusterer[clusterManagerId]!,
);

void _clusterClicked(
ClusterManagerId clusterManagerId,
gmaps.MapMouseEvent event,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
@JS()
library;

import 'dart:async';
import 'dart:js_interop';

import 'package:google_maps/google_maps.dart' as gmaps;

import 'marker_clustering.dart';

/// A typedef representing a callback function for handling cluster tap events.
typedef ClusterClickHandler<T> =
void Function(gmaps.MapMouseEvent, MarkerClustererCluster<T>, gmaps.Map);
Expand Down Expand Up @@ -64,6 +67,16 @@ extension type MarkerClustererOptions<T>._(JSObject _) implements JSObject {
external JSExportedDartFunction? get _onClusterClick;
}

@JS('google.maps.event.addListener')
external JSAny _gmapsAddListener(
JSAny instance,
String eventName,
JSFunction handler,
);

@JS('google.maps.event.removeListener')
external void _gmapsRemoveListener(JSAny listenerHandle);

/// The cluster object handled by the [MarkerClusterer].
///
/// https://googlemaps.github.io/js-markerclusterer/classes/Cluster.html
Expand Down Expand Up @@ -116,7 +129,8 @@ extension type MarkerClusterer<T>._(JSObject _) implements JSObject {

/// Adds a list of markers to be clustered by the [MarkerClusterer].
void addMarkers(List<T>? markers, bool? noDraw) =>
_addMarkers(markers?.cast<JSAny>().toJS, noDraw);
_addMarkers(markers?.cast<JSAny>().toList().toJS, noDraw);

@JS('addMarkers')
external void _addMarkers(JSArray<JSAny>? markers, bool? noDraw);

Expand All @@ -128,7 +142,7 @@ extension type MarkerClusterer<T>._(JSObject _) implements JSObject {

/// Removes a list of markers from the [MarkerClusterer].
bool removeMarkers(List<T>? markers, bool? noDraw) =>
_removeMarkers(markers?.cast<JSAny>().toJS, noDraw);
_removeMarkers(markers?.cast<JSAny>().toList().toJS, noDraw);
@JS('removeMarkers')
external bool _removeMarkers(JSArray<JSAny>? markers, bool? noDraw);

Expand Down Expand Up @@ -163,3 +177,29 @@ MarkerClusterer<T> createMarkerClusterer<T>(
);
return MarkerClusterer<T>(options);
}

/// Converts events emitted by the clustering manager during rendering into a stream
Stream<ClusteringEvent> getClustererEvents<T>(MarkerClusterer<T> clusterer) {
StreamController<ClusteringEvent>? controller;

final JSAny beginHandle = _gmapsAddListener(
clusterer,
'clusteringbegin',
((JSAny mc) => controller?.add(ClusteringEvent.begin)).toJS,
);

final JSAny endHandle = _gmapsAddListener(
clusterer,
'clusteringend',
((JSAny mc) => controller?.add(ClusteringEvent.end)).toJS,
);

controller = StreamController<ClusteringEvent>(
onCancel: () {
_gmapsRemoveListener(beginHandle);
_gmapsRemoveListener(endHandle);
},
);

return controller.stream;
}
Loading
Loading