Skip to content
Draft
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 @@ -335,6 +335,72 @@ void main() {
expect(icon.style.height, '${expectedSize}px');
});

testWidgets('changeMarkers preserves bitmap icon content when position changes', (
WidgetTester tester,
) async {
const markerId = MarkerId('1');
final Uint8List bytes = const Base64Decoder().convert(iconImageBase64);
final markers = <AdvancedMarker>{
AdvancedMarker(
markerId: markerId,
icon: BytesMapBitmap(bytes, imagePixelRatio: 1),
position: const LatLng(1, 2),
),
};
await controller.addMarkers(markers);

final gmaps.AdvancedMarkerElement? marker = controller.markers[markerId]?.marker;
expect(marker, isNotNull);
final icon = marker!.content as HTMLImageElement?;
expect(icon, isNotNull);
final String blobUrl = icon!.src;

final updatedMarkers = <AdvancedMarker>{
AdvancedMarker(
markerId: markerId,
icon: BytesMapBitmap(bytes, imagePixelRatio: 1),
position: const LatLng(42, 54),
),
};
await controller.changeMarkers(updatedMarkers);

final position = marker.position! as gmaps.LatLngLiteral;
expect(position.lat, equals(42));
expect(position.lng, equals(54));
expect(icon.isSameNode(marker.content), isTrue);
expect(icon.src, blobUrl);
});

testWidgets('changeMarkers replaces bitmap icon content when alpha changes', (
WidgetTester tester,
) async {
const markerId = MarkerId('1');
final Uint8List bytes = const Base64Decoder().convert(iconImageBase64);
final markers = <AdvancedMarker>{
AdvancedMarker(markerId: markerId, icon: BytesMapBitmap(bytes, imagePixelRatio: 1)),
};
await controller.addMarkers(markers);

final gmaps.AdvancedMarkerElement? marker = controller.markers[markerId]?.marker;
expect(marker, isNotNull);
final icon = marker!.content as HTMLImageElement?;
expect(icon, isNotNull);

final updatedMarkers = <AdvancedMarker>{
AdvancedMarker(
markerId: markerId,
alpha: 0.5,
icon: BytesMapBitmap(bytes, imagePixelRatio: 1),
),
};
await controller.changeMarkers(updatedMarkers);

final updatedIcon = marker.content as HTMLImageElement?;
expect(updatedIcon, isNotNull);
expect(updatedIcon!.isSameNode(icon), isFalse);
expect(updatedIcon.style.opacity, '0.5');
});

testWidgets('markers with custom bitmap icon and pixel ratio work', (
WidgetTester tester,
) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -689,22 +689,68 @@ Future<gmaps.Icon?> _gmIconFromBitmapDescriptor(
return icon;
}

class _AdvancedMarkerContentConfiguration {
_AdvancedMarkerContentConfiguration({
required this.alpha,
required this.visible,
required this.rotation,
required this.iconHash,
});

factory _AdvancedMarkerContentConfiguration.fromMarker(AdvancedMarker marker) {
return _AdvancedMarkerContentConfiguration(
alpha: marker.alpha,
visible: marker.visible,
rotation: marker.rotation,
iconHash: const DeepCollectionEquality().hash(marker.icon.toJson()),
);
}

final double alpha;
final bool visible;
final double rotation;
final int iconHash;

bool isMatchFor(AdvancedMarker marker) {
return alpha == marker.alpha &&
visible == marker.visible &&
rotation == marker.rotation &&
iconHash == const DeepCollectionEquality().hash(marker.icon.toJson());
}
}

bool _isAdvancedMarkerContentUpdateRequired(
Marker marker,
_AdvancedMarkerContentConfiguration? previousConfiguration,
) {
if (marker is! AdvancedMarker || previousConfiguration == null) {
return true;
}
return !previousConfiguration.isMatchFor(marker);
}

// Computes the options for a new [gmaps.Marker] from an incoming set of options
// [marker], and the existing marker registered with the map: [currentMarker].
Future<O> _markerOptionsFromMarker<T, O>(Marker marker, T? currentMarker) async {
Future<O> _markerOptionsFromMarker<T, O>(
Marker marker,
T? currentMarker, {
bool isAdvancedMarkerContentUpdateRequired = true,
}) async {
if (marker is AdvancedMarker) {
final options = gmaps.AdvancedMarkerElementOptions()
..collisionBehavior = _markerCollisionBehaviorToGmCollisionBehavior(marker.collisionBehavior)
..content = await _advancedMarkerIconFromBitmapDescriptor(
marker.icon,
opacity: marker.alpha,
isVisible: marker.visible,
rotation: marker.rotation,
)
..position = gmaps.LatLng(marker.position.latitude, marker.position.longitude)
..title = sanitizeHtml(marker.infoWindow.title ?? '')
..zIndex = marker.zIndex
..gmpDraggable = marker.draggable;
if (isAdvancedMarkerContentUpdateRequired) {
options.content = await _advancedMarkerIconFromBitmapDescriptor(
marker.icon,
opacity: marker.alpha,
isVisible: marker.visible,
rotation: marker.rotation,
);
}
return options as O;
} else {
final options = gmaps.MarkerOptions()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ abstract class MarkerController<T, O> {
/// Updates the options of the wrapped marker object.
///
/// This cannot be called after [remove].
void update(O options, {web.HTMLElement? newInfoWindowContent});
void update(
O options, {
bool isContentUpdateRequired = true,
web.HTMLElement? newInfoWindowContent,
});

/// Initializes the listener for the wrapped marker object.
void addMarkerListener({
Expand Down Expand Up @@ -193,7 +197,11 @@ class LegacyMarkerController extends MarkerController<gmaps.Marker, gmaps.Marker
}

@override
void update(gmaps.MarkerOptions options, {web.HTMLElement? newInfoWindowContent}) {
void update(
gmaps.MarkerOptions options, {
bool isContentUpdateRequired = true,
web.HTMLElement? newInfoWindowContent,
}) {
assert(_marker != null, 'Cannot `update` Marker after calling `remove`.');
_marker!.options = options;

Expand Down Expand Up @@ -302,12 +310,18 @@ class AdvancedMarkerController
}

@override
void update(gmaps.AdvancedMarkerElementOptions options, {web.HTMLElement? newInfoWindowContent}) {
void update(
gmaps.AdvancedMarkerElementOptions options, {
bool isContentUpdateRequired = true,
web.HTMLElement? newInfoWindowContent,
}) {
assert(_marker != null, 'Cannot `update` Marker after calling `remove`.');

final gmaps.AdvancedMarkerElement marker = _marker!;
marker.collisionBehavior = options.collisionBehavior;
marker.content = options.content;
if (isContentUpdateRequired) {
marker.content = options.content;
}
marker.gmpDraggable = options.gmpDraggable;
marker.position = options.position;
marker.title = options.title ?? '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ abstract class MarkersController<T extends Object, O> extends GeometryController
// A cache of [MarkerController]s indexed by their [MarkerId].
final Map<MarkerId, MarkerController<T, O>> _markerIdToController;

// A cache of the advanced marker content configuration indexed by [MarkerId].
final Map<MarkerId, _AdvancedMarkerContentConfiguration> _contentConfigurationByMarkerId =
<MarkerId, _AdvancedMarkerContentConfiguration>{};

// The stream over which markers broadcast their events
final StreamController<MapEvent<Object?>> _streamController;

Expand Down Expand Up @@ -103,6 +107,7 @@ abstract class MarkersController<T extends Object, O> extends GeometryController
gmInfoWindow,
);
_markerIdToController[marker.markerId] = controller;
_updateAdvancedMarkerContentConfiguration(marker);

return controller;
}
Expand Down Expand Up @@ -136,16 +141,37 @@ abstract class MarkersController<T extends Object, O> extends GeometryController
_removeMarker(marker.markerId);
await _addMarker(marker);
} else {
final O markerOptions = await _markerOptionsFromMarker(marker, markerController.marker);
final _AdvancedMarkerContentConfiguration? previousContentConfiguration =
_contentConfigurationByMarkerId[marker.markerId];
final bool isAdvancedMarkerContentUpdateRequired = _isAdvancedMarkerContentUpdateRequired(
marker,
previousContentConfiguration,
);
final O markerOptions = await _markerOptionsFromMarker(
marker,
markerController.marker,
isAdvancedMarkerContentUpdateRequired: isAdvancedMarkerContentUpdateRequired,
);
final gmaps.InfoWindowOptions? infoWindow = _infoWindowOptionsFromMarker(marker);
markerController.update(
markerOptions,
isContentUpdateRequired: isAdvancedMarkerContentUpdateRequired,
newInfoWindowContent: infoWindow?.content as web.HTMLElement?,
);
_updateAdvancedMarkerContentConfiguration(marker);
}
}
}

void _updateAdvancedMarkerContentConfiguration(Marker marker) {
if (marker is AdvancedMarker) {
_contentConfigurationByMarkerId[marker.markerId] =
_AdvancedMarkerContentConfiguration.fromMarker(marker);
} else {
_contentConfigurationByMarkerId.remove(marker.markerId);
}
}

/// Removes a set of [MarkerId]s from the cache.
void removeMarkers(Set<MarkerId> markerIdsToRemove) {
final List<MapEntry<MarkerId, MarkerController<T, O>?>> markersControllers = markerIdsToRemove
Expand Down Expand Up @@ -182,6 +208,7 @@ abstract class MarkersController<T extends Object, O> extends GeometryController
for (final markerController in markersControllers) {
markerController.value?.remove();
_markerIdToController.remove(markerController.key);
_contentConfigurationByMarkerId.remove(markerController.key);
}
}

Expand All @@ -195,6 +222,7 @@ abstract class MarkersController<T extends Object, O> extends GeometryController
}
markerController?.remove();
_markerIdToController.remove(markerId);
_contentConfigurationByMarkerId.remove(markerId);
}

// InfoWindow...
Expand Down