diff --git a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md index 149dba55020..5fe6a9812b0 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,5 +1,11 @@ -## NEXT - +## 0.6.0 + +* **BREAKING CHANGES**: Adds type constraints to generic type parameters: + * `ClusterManagersController` now requires `T extends Object` + * `MarkersController` now requires `T extends Object` +* Adds support for Google Maps JavaScript API Advanced Markers (`AdvancedMarker`), including new `AdvancedMarkerController` and `AdvancedMarkersController` classes, support for `PinConfig` with customizable background, border, and glyph, and custom marker content via `BitmapDescriptor` (including `AssetMapBitmap`, `BytesMapBitmap`, and `PinConfig`). Advanced markers require the `marker` library - add `&libraries=marker` to your Google Maps API script URL in `web/index.html`. +* Adds `isAdvancedMarkersAvailable()` method to check if advanced markers are supported. +* Refactors marker architecture to support both legacy `Marker` and new `AdvancedMarker` types through unified controller interfaces. * Updates minimum supported SDK version to Flutter 3.35/Dart 3.9. ## 0.5.14+3 diff --git a/packages/google_maps_flutter/google_maps_flutter_web/README.md b/packages/google_maps_flutter/google_maps_flutter_web/README.md index 9a92c70bad1..1a0bde9f27a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/README.md @@ -32,7 +32,7 @@ Modify the `` tag of your `web/index.html` to load the Google Maps JavaScr The Google Maps Web SDK splits some of its functionality in [separate libraries](https://developers.google.com/maps/documentation/javascript/libraries#libraries-for-dynamic-library-import). If your app needs the `drawing` library (to draw polygons, rectangles, polylines, -circles or markers on a map), include it like this: +circles or legacy markers on a map), include it like this: ```html ``` Now you should be able to use the Google Maps plugin normally. +## Advanced Markers + +The Google Maps SDK provides Advanced Markers, which replace the older legacy markers. Advanced Markers offer improved performance, richer customization (including scalable pins, custom HTML-like content, and styling options), and better behavior on vector maps such as collision management and altitude control. Legacy Marker APIs are deprecated, and new features will only be available through the Advanced Marker system. + +If your app uses Advanced Markers, include `marker` library like this: +```html + +``` + +For full details, see Google's official documentation: +https://developers.google.com/maps/documentation/javascript/advanced-markers/overview + ## Marker clustering If you need marker clustering support, modify the tag to load the [js-markerclusterer](https://github.com/googlemaps/js-markerclusterer#install) library. Ensure you are using the currently supported version `2.5.3`, like so: diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/advanced_marker_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/advanced_marker_test.dart new file mode 100644 index 00000000000..5b464df10a8 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/advanced_marker_test.dart @@ -0,0 +1,222 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:js_interop'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps/google_maps.dart' as gmaps; +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; +import 'package:google_maps_flutter_web/src/utils.dart'; +import 'package:integration_test/integration_test.dart'; + +/// Test Markers +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Since onTap/DragEnd events happen asynchronously, we need to store when the event + // is fired. We use a completer so the test can wait for the future to be completed. + late Completer methodCalledCompleter; + + /// This is the future value of the [methodCalledCompleter]. Reinitialized + /// in the [setUp] method, and completed (as `true`) by [onTap] and [onDragEnd] + /// when those methods are called from the MarkerController. + late Future methodCalled; + + void onTap() { + methodCalledCompleter.complete(true); + } + + void onDragStart(gmaps.LatLng _) { + methodCalledCompleter.complete(true); + } + + void onDrag(gmaps.LatLng _) { + methodCalledCompleter.complete(true); + } + + void onDragEnd(gmaps.LatLng _) { + methodCalledCompleter.complete(true); + } + + setUp(() { + methodCalledCompleter = Completer(); + methodCalled = methodCalledCompleter.future; + }); + + group('MarkerController', () { + late gmaps.AdvancedMarkerElement marker; + + setUp(() { + marker = gmaps.AdvancedMarkerElement(); + }); + + testWidgets('onTap gets called', (WidgetTester tester) async { + AdvancedMarkerController(marker: marker, onTap: onTap); + + // Trigger a click event... + gmaps.event.trigger(marker, 'click', gmaps.MapMouseEvent()); + + // The event handling is now truly async. Wait for it... + expect(await methodCalled, isTrue); + }); + + testWidgets('onDragStart gets called', (WidgetTester tester) async { + AdvancedMarkerController(marker: marker, onDragStart: onDragStart); + + // Trigger a drag end event... + gmaps.event.trigger( + marker, + 'dragstart', + gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0), + ); + + expect(await methodCalled, isTrue); + }); + + testWidgets('onDrag gets called', (WidgetTester tester) async { + AdvancedMarkerController(marker: marker, onDrag: onDrag); + + // Trigger a drag end event... + gmaps.event.trigger( + marker, + 'drag', + gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0), + ); + + expect(await methodCalled, isTrue); + }); + + testWidgets('onDragEnd gets called', (WidgetTester tester) async { + AdvancedMarkerController(marker: marker, onDragEnd: onDragEnd); + + // Trigger a drag end event... + gmaps.event.trigger( + marker, + 'dragend', + gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0), + ); + + expect(await methodCalled, isTrue); + }); + + testWidgets('update', (WidgetTester tester) async { + final controller = AdvancedMarkerController(marker: marker); + final options = gmaps.AdvancedMarkerElementOptions() + ..collisionBehavior = + gmaps.CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY + ..gmpDraggable = true + ..position = gmaps.LatLng(42, 54); + + expect(marker.collisionBehavior, gmaps.CollisionBehavior.REQUIRED); + expect(marker.gmpDraggable, isFalse); + + controller.update(options); + + expect(marker.gmpDraggable, isTrue); + expect( + marker.collisionBehavior, + gmaps.CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY, + ); + final JSAny? position = marker.position; + expect(position, isNotNull); + expect(position is gmaps.LatLngLiteral, isTrue); + expect((position! as gmaps.LatLngLiteral).lat, equals(42)); + expect((position as gmaps.LatLngLiteral).lng, equals(54)); + }); + + testWidgets('infoWindow null, showInfoWindow.', ( + WidgetTester tester, + ) async { + final controller = AdvancedMarkerController(marker: marker); + + controller.showInfoWindow(); + + expect(controller.infoWindowShown, isFalse); + }); + + testWidgets('showInfoWindow', (WidgetTester tester) async { + final infoWindow = gmaps.InfoWindow(); + final map = gmaps.Map(createDivElement()); + marker.map = map; + final controller = AdvancedMarkerController( + marker: marker, + infoWindow: infoWindow, + ); + + controller.showInfoWindow(); + + expect(infoWindow.get('map'), map); + expect(controller.infoWindowShown, isTrue); + }); + + testWidgets('hideInfoWindow', (WidgetTester tester) async { + final infoWindow = gmaps.InfoWindow(); + final map = gmaps.Map(createDivElement()); + marker.map = map; + final controller = AdvancedMarkerController( + marker: marker, + infoWindow: infoWindow, + ); + + controller.hideInfoWindow(); + + expect(infoWindow.get('map'), isNull); + expect(controller.infoWindowShown, isFalse); + }); + + group('remove', () { + late AdvancedMarkerController controller; + + setUp(() { + final infoWindow = gmaps.InfoWindow(); + final map = gmaps.Map(createDivElement()); + marker.map = map; + controller = AdvancedMarkerController( + marker: marker, + infoWindow: infoWindow, + ); + }); + + testWidgets('drops gmaps instance', (WidgetTester tester) async { + controller.remove(); + + expect(controller.marker, isNull); + }); + + testWidgets('cannot call update after remove', ( + WidgetTester tester, + ) async { + final options = gmaps.AdvancedMarkerElementOptions() + ..gmpDraggable = true; + + controller.remove(); + + expect(() { + controller.update(options); + }, throwsAssertionError); + }); + + testWidgets('cannot call showInfoWindow after remove', ( + WidgetTester tester, + ) async { + controller.remove(); + + expect(() { + controller.showInfoWindow(); + }, throwsStateError); + }); + + testWidgets('cannot call hideInfoWindow after remove', ( + WidgetTester tester, + ) async { + controller.remove(); + + expect(() { + controller.hideInfoWindow(); + }, throwsAssertionError); + }); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/advanced_markers_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/advanced_markers_test.dart new file mode 100644 index 00000000000..11b7b9df35e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/advanced_markers_test.dart @@ -0,0 +1,566 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:js_interop'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps/google_maps.dart' as gmaps; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; +import 'package:google_maps_flutter_web/src/marker_clustering.dart'; +import 'package:google_maps_flutter_web/src/utils.dart'; +import 'package:http/http.dart' as http; +import 'package:integration_test/integration_test.dart'; +import 'package:web/src/dom.dart' as dom; +import 'package:web/web.dart'; + +import 'resources/icon_image_base64.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('MarkersController', () { + late StreamController> events; + late MarkersController< + gmaps.AdvancedMarkerElement, + gmaps.AdvancedMarkerElementOptions + > + controller; + late ClusterManagersController + clusterManagersController; + late gmaps.Map map; + + setUp(() { + events = StreamController>(); + + clusterManagersController = + ClusterManagersController( + stream: events, + ); + controller = AdvancedMarkersController( + stream: events, + clusterManagersController: clusterManagersController, + ); + map = gmaps.Map(createDivElement()); + clusterManagersController.bindToMap(123, map); + controller.bindToMap(123, map); + }); + + testWidgets('addMarkers', (WidgetTester tester) async { + final markers = { + AdvancedMarker(markerId: const MarkerId('1')), + AdvancedMarker(markerId: const MarkerId('2')), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 2); + expect(controller.markers, contains(const MarkerId('1'))); + expect(controller.markers, contains(const MarkerId('2'))); + expect(controller.markers, isNot(contains(const MarkerId('66')))); + }); + + testWidgets('changeMarkers', (WidgetTester tester) async { + gmaps.AdvancedMarkerElement? marker; + gmaps.LatLngLiteral? position; + + final markers = { + AdvancedMarker(markerId: const MarkerId('1')), + }; + await controller.addMarkers(markers); + + marker = controller.markers[const MarkerId('1')]?.marker; + expect(marker, isNotNull); + expect(marker!.gmpDraggable, isFalse); + + // By default, markers fall in LatLng(0, 0). + position = marker.position! as gmaps.LatLngLiteral; + expect(position, isNotNull); + expect(position.lat, equals(0)); + expect(position.lng, equals(0)); + + // Update the marker with draggable and position. + final updatedMarkers = { + AdvancedMarker( + markerId: const MarkerId('1'), + draggable: true, + position: const LatLng(42, 54), + ), + }; + await controller.changeMarkers(updatedMarkers); + expect(controller.markers.length, 1); + + marker = controller.markers[const MarkerId('1')]?.marker; + expect(marker, isNotNull); + expect(marker!.gmpDraggable, isTrue); + + position = marker.position! as gmaps.LatLngLiteral; + expect(position, isNotNull); + expect(position.lat, equals(42)); + expect(position.lng, equals(54)); + }); + + testWidgets( + 'changeMarkers resets marker position if not passed when updating!', + (WidgetTester tester) async { + gmaps.AdvancedMarkerElement? marker; + gmaps.LatLngLiteral? position; + + final markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + position: const LatLng(42, 54), + ), + }; + await controller.addMarkers(markers); + + marker = controller.markers[const MarkerId('1')]?.marker; + expect(marker, isNotNull); + expect(marker!.gmpDraggable, isFalse); + + position = marker.position! as gmaps.LatLngLiteral; + expect(position, isNotNull); + expect(position.lat, equals(42)); + expect(position.lng, equals(54)); + + // Update the marker without position. + final updatedMarkers = { + AdvancedMarker(markerId: const MarkerId('1'), draggable: true), + }; + await controller.changeMarkers(updatedMarkers); + expect(controller.markers.length, 1); + + marker = controller.markers[const MarkerId('1')]?.marker; + expect(marker, isNotNull); + expect(marker!.gmpDraggable, isTrue); + + position = marker.position! as gmaps.LatLngLiteral; + expect(position, isNotNull); + expect(position.lat, equals(0)); + expect(position.lng, equals(0)); + }, + ); + + testWidgets('removeMarkers', (WidgetTester tester) async { + final markers = { + AdvancedMarker(markerId: const MarkerId('1')), + AdvancedMarker(markerId: const MarkerId('2')), + AdvancedMarker(markerId: const MarkerId('3')), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 3); + + // Remove some markers. + final markerIdsToRemove = { + const MarkerId('1'), + const MarkerId('3'), + }; + + controller.removeMarkers(markerIdsToRemove); + + expect(controller.markers.length, 1); + expect(controller.markers, isNot(contains(const MarkerId('1')))); + expect(controller.markers, contains(const MarkerId('2'))); + expect(controller.markers, isNot(contains(const MarkerId('3')))); + }); + + testWidgets('InfoWindow show/hide', (WidgetTester tester) async { + final markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + infoWindow: const InfoWindow(title: 'Title', snippet: 'Snippet'), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isFalse); + + controller.showMarkerInfoWindow(const MarkerId('1')); + + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isTrue); + + controller.hideMarkerInfoWindow(const MarkerId('1')); + + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isFalse); + }); + + testWidgets('only single InfoWindow is visible', ( + WidgetTester tester, + ) async { + final markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + infoWindow: const InfoWindow(title: 'Title', snippet: 'Snippet'), + ), + AdvancedMarker( + markerId: const MarkerId('2'), + infoWindow: const InfoWindow(title: 'Title', snippet: 'Snippet'), + ), + }; + await controller.addMarkers(markers); + + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isFalse); + expect(controller.markers[const MarkerId('2')]?.infoWindowShown, isFalse); + + controller.showMarkerInfoWindow(const MarkerId('1')); + + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isTrue); + expect(controller.markers[const MarkerId('2')]?.infoWindowShown, isFalse); + + controller.showMarkerInfoWindow(const MarkerId('2')); + + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isFalse); + expect(controller.markers[const MarkerId('2')]?.infoWindowShown, isTrue); + }); + + testWidgets('markers with custom asset icon work', ( + WidgetTester tester, + ) async { + final markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: AssetMapBitmap('assets/red_square.png', imagePixelRatio: 1.0), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final icon = + controller.markers[const MarkerId('1')]?.marker?.content + as HTMLImageElement?; + expect(icon, isNotNull); + + final String assetUrl = icon!.src; + expect(assetUrl, endsWith('assets/red_square.png')); + + // Asset size is 48x48 physical pixels. + expect(icon.style.width, '48px'); + expect(icon.style.height, '48px'); + }); + + testWidgets('markers with custom asset icon and pixel ratio work', ( + WidgetTester tester, + ) async { + final markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: AssetMapBitmap('assets/red_square.png', imagePixelRatio: 2.0), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final icon = + controller.markers[const MarkerId('1')]?.marker?.content + as HTMLImageElement?; + expect(icon, isNotNull); + + final String assetUrl = icon!.src; + expect(assetUrl, endsWith('assets/red_square.png')); + + // Asset size is 48x48 physical pixels, and with pixel ratio 2.0 it + // should be drawn with size 24x24 logical pixels. + expect(icon.style.width, '24px'); + expect(icon.style.height, '24px'); + }); + + testWidgets('markers with custom asset icon with width and height work', ( + WidgetTester tester, + ) async { + final markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 2.0, + width: 64, + height: 64, + ), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final icon = + controller.markers[const MarkerId('1')]?.marker?.content + as HTMLImageElement?; + expect(icon, isNotNull); + + final String assetUrl = icon!.src; + expect(assetUrl, endsWith('assets/red_square.png')); + + // Asset size is 48x48 physical pixels, + // and scaled to requested 64x64 size. + expect(icon.style.width, '64px'); + expect(icon.style.height, '64px'); + }); + + testWidgets('markers with missing asset icon should not set size', ( + WidgetTester tester, + ) async { + final markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: AssetMapBitmap( + 'assets/broken_asset_name.png', + imagePixelRatio: 2.0, + ), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final icon = + controller.markers[const MarkerId('1')]?.marker?.content + as HTMLImageElement?; + expect(icon, isNotNull); + + final String assetUrl = icon!.src; + expect(assetUrl, endsWith('assets/broken_asset_name.png')); + + // For invalid assets, the size and scaledSize should be null. + expect(icon.style.width, isEmpty); + expect(icon.style.height, isEmpty); + }); + + testWidgets('markers with custom bitmap icon work', ( + WidgetTester tester, + ) async { + final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); + final markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: BytesMapBitmap( + bytes, + imagePixelRatio: tester.view.devicePixelRatio, + ), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final icon = + controller.markers[const MarkerId('1')]?.marker?.content + as HTMLImageElement?; + expect(icon, isNotNull); + + final String blobUrl = icon!.src; + expect(blobUrl, startsWith('blob:')); + + final http.Response response = await http.get(Uri.parse(blobUrl)); + expect( + response.bodyBytes, + bytes, + reason: + 'Bytes from the Icon blob must match bytes used to create AdvancedMarker', + ); + + // Icon size is 16x16 pixels, this should be automatically read from the + // bitmap and set to the icon size scaled to 8x8 using the + // given imagePixelRatio. + final int expectedSize = 16 ~/ tester.view.devicePixelRatio; + expect(icon.style.width, '${expectedSize}px'); + expect(icon.style.height, '${expectedSize}px'); + }); + + testWidgets('markers with custom bitmap icon and pixel ratio work', ( + WidgetTester tester, + ) async { + final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); + final markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: BytesMapBitmap(bytes, imagePixelRatio: 1), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final icon = + controller.markers[const MarkerId('1')]?.marker?.content + as HTMLImageElement?; + expect(icon, isNotNull); + + // Icon size is 16x16 pixels, this should be automatically read from the + // bitmap and set to the icon size and should not be changed as + // image pixel ratio is set to 1.0. + expect(icon!.style.width, '16px'); + expect(icon.style.height, '16px'); + }); + + testWidgets('markers with custom bitmap icon pass size to sdk', ( + WidgetTester tester, + ) async { + final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); + final markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: BytesMapBitmap(bytes, width: 20, height: 30), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final icon = + controller.markers[const MarkerId('1')]?.marker?.content + as HTMLImageElement?; + expect(icon, isNotNull); + expect(icon!.style.width, '20px'); + expect(icon.style.height, '30px'); + }); + + testWidgets('markers created with text glyph work', ( + WidgetTester widgetTester, + ) async { + final markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: BitmapDescriptor.pinConfig( + backgroundColor: Colors.black, + borderColor: Colors.black, + glyph: const TextGlyph(text: 'Hey', textColor: Color(0xFF0000FF)), + ), + ), + }; + await controller.addMarkers(markers); + expect(controller.markers.length, 1); + + final icon = + controller.markers[const MarkerId('1')]?.marker?.content + as HTMLDivElement?; + expect(icon, isNotNull); + + // Query pin nodes and find text element. This is a bit fragile as it + // depends on the implementation details of the icon which is not part of + // the public API. + dom.Element? paragraphElement; + final NodeList paragraphs = icon!.querySelectorAll('p'); + for (var i = 0; i < paragraphs.length; i++) { + final paragraph = paragraphs.item(i) as dom.Element?; + if (paragraph?.innerHTML.toString() == 'Hey') { + paragraphElement = paragraph; + break; + } + } + + expect(paragraphElement, isNotNull); + expect(paragraphElement!.innerHTML.toString(), 'Hey'); + }); + + testWidgets('markers created with bitmap glyph work', ( + WidgetTester widgetTester, + ) async { + final markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: BitmapDescriptor.pinConfig( + backgroundColor: Colors.black, + borderColor: Colors.black, + glyph: BitmapGlyph( + bitmap: await BitmapDescriptor.asset( + const ImageConfiguration(size: Size.square(12)), + 'assets/red_square.png', + ), + ), + ), + ), + }; + await controller.addMarkers(markers); + expect(controller.markers.length, 1); + + final icon = + controller.markers[const MarkerId('1')]?.marker?.content + as HTMLDivElement?; + expect(icon, isNotNull); + + // Query pin nodes and find text element. This is a bit fragile as it + // depends on the implementation details of the icon which is not part of + // the public API. + HTMLImageElement? imgElement; + final NodeList imgElements = icon!.querySelectorAll('img'); + for (var i = 0; i < imgElements.length; i++) { + final img = imgElements.item(i) as dom.Element?; + final String src = (img! as HTMLImageElement).src; + if (src.endsWith('assets/red_square.png')) { + imgElement = img as HTMLImageElement; + break; + } + } + + expect(imgElement, isNotNull); + expect(imgElement!.src, endsWith('assets/red_square.png')); + }); + + testWidgets('InfoWindow snippet can have links', ( + WidgetTester tester, + ) async { + final markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + infoWindow: const InfoWindow( + title: 'title for test', + snippet: 'Go to Google >>>', + ), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final content = + controller.markers[const MarkerId('1')]?.infoWindow?.content + as HTMLElement?; + expect(content, isNotNull); + + final String innerHtml = (content!.innerHTML as JSString).toDart; + expect(innerHtml, contains('title for test')); + expect( + innerHtml, + contains( + 'Go to Google >>>', + ), + ); + }); + + testWidgets('InfoWindow content is clickable', (WidgetTester tester) async { + final markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + infoWindow: const InfoWindow( + title: 'title for test', + snippet: 'some snippet', + ), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final content = + controller.markers[const MarkerId('1')]?.infoWindow?.content + as HTMLElement?; + + content?.click(); + + final MapEvent event = await events.stream.first; + + expect(event, isA()); + expect((event as InfoWindowTapEvent).value, equals(const MarkerId('1'))); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart index 1649f40e7bb..931465c7b95 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart @@ -18,7 +18,7 @@ import 'google_maps_controller_test.mocks.dart'; // This value is used when comparing long~num, like // LatLng values. -const String _kCloudMapId = '000000000000000'; // Dummy map ID. +const String _kMapId = '000000000000000'; // Dummy map ID. gmaps.Map mapShim() => throw UnimplementedError(); @@ -35,7 +35,7 @@ gmaps.Map mapShim() => throw UnimplementedError(); MockSpec( fallbackGenerators: {#googleMap: mapShim}, ), - MockSpec( + MockSpec>( fallbackGenerators: {#googleMap: mapShim}, ), MockSpec( @@ -487,7 +487,7 @@ void main() { mapConfiguration: const MapConfiguration( mapType: MapType.satellite, zoomControlsEnabled: true, - mapId: _kCloudMapId, + mapId: _kMapId, fortyFiveDegreeImageryEnabled: false, ), ); @@ -503,7 +503,7 @@ void main() { expect(capturedOptions, isNotNull); expect(capturedOptions!.mapTypeId, gmaps.MapTypeId.SATELLITE); expect(capturedOptions!.zoomControl, true); - expect(capturedOptions!.mapId, _kCloudMapId); + expect(capturedOptions!.mapId, _kMapId); expect( capturedOptions!.gestureHandling, 'auto', diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart index a47b2daad9e..c08034d58f1 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.6 from annotations // in google_maps_flutter_web_integration_tests/integration_test/google_maps_controller_test.dart. // Do not manually edit this file. @@ -21,10 +21,18 @@ import 'google_maps_controller_test.dart' as _i5; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member + +class _FakeMarkerController_0 extends _i1.SmartFake + implements _i2.MarkerController { + _FakeMarkerController_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} /// A class which mocks [CirclesController]. /// @@ -48,12 +56,6 @@ class MockCirclesController extends _i1.Mock implements _i2.CirclesController { ) as _i4.Map); - @override - set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null, - ); - @override int get mapId => (super.noSuchMethod( @@ -64,8 +66,14 @@ class MockCirclesController extends _i1.Mock implements _i2.CirclesController { as int); @override - set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter(#mapId, _mapId), + set googleMap(_i4.Map? value) => super.noSuchMethod( + Invocation.setter(#googleMap, value), + returnValueForMissingStub: null, + ); + + @override + set mapId(int? value) => super.noSuchMethod( + Invocation.setter(#mapId, value), returnValueForMissingStub: null, ); @@ -118,12 +126,6 @@ class MockHeatmapsController extends _i1.Mock ) as _i4.Map); - @override - set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null, - ); - @override int get mapId => (super.noSuchMethod( @@ -134,8 +136,14 @@ class MockHeatmapsController extends _i1.Mock as int); @override - set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter(#mapId, _mapId), + set googleMap(_i4.Map? value) => super.noSuchMethod( + Invocation.setter(#googleMap, value), + returnValueForMissingStub: null, + ); + + @override + set mapId(int? value) => super.noSuchMethod( + Invocation.setter(#mapId, value), returnValueForMissingStub: null, ); @@ -188,12 +196,6 @@ class MockPolygonsController extends _i1.Mock ) as _i4.Map); - @override - set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null, - ); - @override int get mapId => (super.noSuchMethod( @@ -204,8 +206,14 @@ class MockPolygonsController extends _i1.Mock as int); @override - set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter(#mapId, _mapId), + set googleMap(_i4.Map? value) => super.noSuchMethod( + Invocation.setter(#googleMap, value), + returnValueForMissingStub: null, + ); + + @override + set mapId(int? value) => super.noSuchMethod( + Invocation.setter(#mapId, value), returnValueForMissingStub: null, ); @@ -259,12 +267,6 @@ class MockPolylinesController extends _i1.Mock ) as _i4.Map); - @override - set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null, - ); - @override int get mapId => (super.noSuchMethod( @@ -275,8 +277,14 @@ class MockPolylinesController extends _i1.Mock as int); @override - set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter(#mapId, _mapId), + set googleMap(_i4.Map? value) => super.noSuchMethod( + Invocation.setter(#googleMap, value), + returnValueForMissingStub: null, + ); + + @override + set mapId(int? value) => super.noSuchMethod( + Invocation.setter(#mapId, value), returnValueForMissingStub: null, ); @@ -310,15 +318,17 @@ class MockPolylinesController extends _i1.Mock /// A class which mocks [MarkersController]. /// /// See the documentation for Mockito's code generation for more information. -class MockMarkersController extends _i1.Mock implements _i2.MarkersController { +class MockMarkersController extends _i1.Mock + implements _i2.MarkersController { @override - Map<_i3.MarkerId, _i2.MarkerController> get markers => + Map<_i3.MarkerId, _i2.MarkerController> get markers => (super.noSuchMethod( Invocation.getter(#markers), - returnValue: <_i3.MarkerId, _i2.MarkerController>{}, - returnValueForMissingStub: <_i3.MarkerId, _i2.MarkerController>{}, + returnValue: <_i3.MarkerId, _i2.MarkerController>{}, + returnValueForMissingStub: + <_i3.MarkerId, _i2.MarkerController>{}, ) - as Map<_i3.MarkerId, _i2.MarkerController>); + as Map<_i3.MarkerId, _i2.MarkerController>); @override _i4.Map get googleMap => @@ -329,12 +339,6 @@ class MockMarkersController extends _i1.Mock implements _i2.MarkersController { ) as _i4.Map); - @override - set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null, - ); - @override int get mapId => (super.noSuchMethod( @@ -345,8 +349,14 @@ class MockMarkersController extends _i1.Mock implements _i2.MarkersController { as int); @override - set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter(#mapId, _mapId), + set googleMap(_i4.Map? value) => super.noSuchMethod( + Invocation.setter(#googleMap, value), + returnValueForMissingStub: null, + ); + + @override + set mapId(int? value) => super.noSuchMethod( + Invocation.setter(#mapId, value), returnValueForMissingStub: null, ); @@ -359,6 +369,42 @@ class MockMarkersController extends _i1.Mock implements _i2.MarkersController { ) as _i6.Future); + @override + _i6.Future<_i2.MarkerController> createMarkerController( + _i3.Marker? marker, + Object? markerOptions, + _i4.InfoWindow? gmInfoWindow, + ) => + (super.noSuchMethod( + Invocation.method(#createMarkerController, [ + marker, + markerOptions, + gmInfoWindow, + ]), + returnValue: _i6.Future<_i2.MarkerController>.value( + _FakeMarkerController_0( + this, + Invocation.method(#createMarkerController, [ + marker, + markerOptions, + gmInfoWindow, + ]), + ), + ), + returnValueForMissingStub: + _i6.Future<_i2.MarkerController>.value( + _FakeMarkerController_0( + this, + Invocation.method(#createMarkerController, [ + marker, + markerOptions, + gmInfoWindow, + ]), + ), + ), + ) + as _i6.Future<_i2.MarkerController>); + @override _i6.Future changeMarkers(Set<_i3.Marker>? markersToChange) => (super.noSuchMethod( @@ -417,12 +463,6 @@ class MockTileOverlaysController extends _i1.Mock ) as _i4.Map); - @override - set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null, - ); - @override int get mapId => (super.noSuchMethod( @@ -433,8 +473,14 @@ class MockTileOverlaysController extends _i1.Mock as int); @override - set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter(#mapId, _mapId), + set googleMap(_i4.Map? value) => super.noSuchMethod( + Invocation.setter(#googleMap, value), + returnValueForMissingStub: null, + ); + + @override + set mapId(int? value) => super.noSuchMethod( + Invocation.setter(#mapId, value), returnValueForMissingStub: null, ); @@ -486,12 +532,6 @@ class MockGroundOverlaysController extends _i1.Mock ) as _i4.Map); - @override - set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null, - ); - @override int get mapId => (super.noSuchMethod( @@ -502,8 +542,14 @@ class MockGroundOverlaysController extends _i1.Mock as int); @override - set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter(#mapId, _mapId), + set googleMap(_i4.Map? value) => super.noSuchMethod( + Invocation.setter(#googleMap, value), + returnValueForMissingStub: null, + ); + + @override + set mapId(int? value) => super.noSuchMethod( + Invocation.setter(#mapId, value), returnValueForMissingStub: null, ); @@ -528,6 +574,14 @@ class MockGroundOverlaysController extends _i1.Mock returnValueForMissingStub: null, ); + @override + _i4.GroundOverlay? getGroundOverlay(_i3.GroundOverlayId? groundOverlayId) => + (super.noSuchMethod( + Invocation.method(#getGroundOverlay, [groundOverlayId]), + returnValueForMissingStub: null, + ) + as _i4.GroundOverlay?); + @override void bindToMap(int? mapId, _i4.Map? googleMap) => super.noSuchMethod( Invocation.method(#bindToMap, [mapId, googleMap]), diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart index 0d38efbf4f8..4a8fa105c25 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.6 from annotations // in google_maps_flutter_web_integration_tests/integration_test/google_maps_plugin_test.dart. // Do not manually edit this file. @@ -20,10 +20,12 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member class _FakeMapConfiguration_0 extends _i1.SmartFake implements _i2.MapConfiguration { @@ -121,12 +123,12 @@ class MockGoogleMapController extends _i1.Mock void debugSetOverrides({ _i4.DebugCreateMapFunction? createMap, _i4.DebugSetOptionsFunction? setOptions, - _i4.MarkersController? markers, + _i4.MarkersController? markers, _i4.CirclesController? circles, _i4.HeatmapsController? heatmaps, _i4.PolygonsController? polygons, _i4.PolylinesController? polylines, - _i6.ClusterManagersController? clusterManagers, + _i6.ClusterManagersController? clusterManagers, _i4.TileOverlaysController? tileOverlays, _i4.GroundOverlaysController? groundOverlays, }) => super.noSuchMethod( @@ -320,6 +322,15 @@ class MockGoogleMapController extends _i1.Mock ) as bool); + @override + bool isAdvancedMarkersAvailable() => + (super.noSuchMethod( + Invocation.method(#isAdvancedMarkersAvailable, []), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + @override void dispose() => super.noSuchMethod( Invocation.method(#dispose, []), diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart index 003934e6168..1a3132e13d4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart @@ -109,7 +109,7 @@ void main() { } // Repeatedly checks an asynchronous value against a test condition, waiting -// one frame between each check, returing the value if it passes the predicate +// one frame between each check, returning the value if it passes the predicate // before [maxTries] is reached. // // Returns null if the predicate is never satisfied. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart index adf8f4e1ee4..f2bc3e69bda 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart @@ -53,7 +53,7 @@ void main() { }); testWidgets('onTap gets called', (WidgetTester tester) async { - MarkerController(marker: marker, onTap: onTap); + LegacyMarkerController(marker: marker, onTap: onTap); // Trigger a click event... gmaps.event.trigger(marker, 'click', gmaps.MapMouseEvent()); @@ -63,7 +63,7 @@ void main() { }); testWidgets('onDragStart gets called', (WidgetTester tester) async { - MarkerController(marker: marker, onDragStart: onDragStart); + LegacyMarkerController(marker: marker, onDragStart: onDragStart); // Trigger a drag end event... gmaps.event.trigger( @@ -76,7 +76,7 @@ void main() { }); testWidgets('onDrag gets called', (WidgetTester tester) async { - MarkerController(marker: marker, onDrag: onDrag); + LegacyMarkerController(marker: marker, onDrag: onDrag); // Trigger a drag end event... gmaps.event.trigger( @@ -89,7 +89,7 @@ void main() { }); testWidgets('onDragEnd gets called', (WidgetTester tester) async { - MarkerController(marker: marker, onDragEnd: onDragEnd); + LegacyMarkerController(marker: marker, onDragEnd: onDragEnd); // Trigger a drag end event... gmaps.event.trigger( @@ -102,7 +102,7 @@ void main() { }); testWidgets('update', (WidgetTester tester) async { - final controller = MarkerController(marker: marker); + final controller = LegacyMarkerController(marker: marker); final options = gmaps.MarkerOptions() ..draggable = true ..position = gmaps.LatLng(42, 54); @@ -119,7 +119,7 @@ void main() { testWidgets('infoWindow null, showInfoWindow.', ( WidgetTester tester, ) async { - final controller = MarkerController(marker: marker); + final controller = LegacyMarkerController(marker: marker); controller.showInfoWindow(); @@ -130,7 +130,7 @@ void main() { final infoWindow = gmaps.InfoWindow(); final map = gmaps.Map(createDivElement()); marker.set('map', map); - final controller = MarkerController( + final controller = LegacyMarkerController( marker: marker, infoWindow: infoWindow, ); @@ -145,7 +145,7 @@ void main() { final infoWindow = gmaps.InfoWindow(); final map = gmaps.Map(createDivElement()); marker.set('map', map); - final controller = MarkerController( + final controller = LegacyMarkerController( marker: marker, infoWindow: infoWindow, ); @@ -157,13 +157,16 @@ void main() { }); group('remove', () { - late MarkerController controller; + late LegacyMarkerController controller; setUp(() { final infoWindow = gmaps.InfoWindow(); final map = gmaps.Map(createDivElement()); marker.set('map', map); - controller = MarkerController(marker: marker, infoWindow: infoWindow); + controller = LegacyMarkerController( + marker: marker, + infoWindow: infoWindow, + ); }); testWidgets('drops gmaps instance', (WidgetTester tester) async { @@ -191,7 +194,7 @@ void main() { expect(() { controller.showInfoWindow(); - }, throwsAssertionError); + }, throwsStateError); }); testWidgets('cannot call hideInfoWindow after remove', ( diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart index 2ddc2137978..4c1be3ce22c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart @@ -25,15 +25,17 @@ void main() { group('MarkersController', () { late StreamController> events; - late MarkersController controller; - late ClusterManagersController clusterManagersController; + late LegacyMarkersController controller; + late ClusterManagersController clusterManagersController; late gmaps.Map map; setUp(() { events = StreamController>(); - clusterManagersController = ClusterManagersController(stream: events); - controller = MarkersController( + clusterManagersController = ClusterManagersController( + stream: events, + ); + controller = LegacyMarkersController( stream: events, clusterManagersController: clusterManagersController, ); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/overlays_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/overlays_test.mocks.dart index 8a13b88eb10..6abd4a780e6 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/overlays_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/overlays_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.6 from annotations // in google_maps_flutter_web_integration_tests/integration_test/overlays_test.dart. // Do not manually edit this file. @@ -17,10 +17,12 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member class _FakeTile_0 extends _i1.SmartFake implements _i2.Tile { _FakeTile_0(Object parent, Invocation parentInvocation) diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html b/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html index 9819866730a..4b4fe18c74b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html @@ -7,7 +7,7 @@ Browser Tests - + diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart index 08588d9ccbf..0413faaf104 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart @@ -20,7 +20,7 @@ import 'package:google_maps/google_maps_visualization.dart' as visualization; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:sanitize_html/sanitize_html.dart'; import 'package:stream_transform/stream_transform.dart'; -import 'package:web/web.dart'; +import 'package:web/web.dart' as web; import 'src/dom_window_extension.dart'; import 'src/google_maps_inspector_web.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart index fc4d0cd0069..ed963650792 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart @@ -12,7 +12,7 @@ final gmaps.LatLngBounds _nullGmapsLatLngBounds = gmaps.LatLngBounds( ); // The TrustedType Policy used by this plugin. Used to sanitize InfoWindow contents. -TrustedTypePolicy? _gmapsTrustedTypePolicy; +web.TrustedTypePolicy? _gmapsTrustedTypePolicy; // A cache for image size Futures to reduce redundant image fetch requests. // This cache should be always cleaned up after marker updates are processed. @@ -228,7 +228,7 @@ LatLng gmLatLngToLatLng(gmaps.LatLng latLng) { } /// Converts a [gmaps.LatLngBounds] into a [LatLngBounds]. -LatLngBounds gmLatLngBoundsTolatLngBounds(gmaps.LatLngBounds latLngBounds) { +LatLngBounds gmLatLngBoundsToLatLngBounds(gmaps.LatLngBounds latLngBounds) { return LatLngBounds( southwest: gmLatLngToLatLng(latLngBounds.southWest), northeast: gmLatLngToLatLng(latLngBounds.northEast), @@ -269,25 +269,25 @@ gmaps.InfoWindowOptions? _infoWindowOptionsFromMarker(Marker marker) { // Add an outer wrapper to the contents of the infowindow, we need it to listen // to click events... - final HTMLElement container = createDivElement() + final web.HTMLElement container = createDivElement() ..id = 'gmaps-marker-${marker.markerId.value}-infowindow'; if (markerTitle.isNotEmpty) { - final title = (document.createElement('h3') as HTMLHeadingElement) + final title = (web.document.createElement('h3') as web.HTMLHeadingElement) ..className = 'infowindow-title' ..innerText = markerTitle; container.appendChild(title); } if (markerSnippet.isNotEmpty) { - final HTMLElement snippet = createDivElement() + final web.HTMLElement snippet = createDivElement() ..className = 'infowindow-snippet'; // Firefox and Safari don't support Trusted Types yet. // See https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicyFactory#browser_compatibility - if (window.nullableTrustedTypes != null) { - _gmapsTrustedTypePolicy ??= window.trustedTypes.createPolicy( + if (web.window.nullableTrustedTypes != null) { + _gmapsTrustedTypePolicy ??= web.window.trustedTypes.createPolicy( 'google_maps_flutter_sanitize', - TrustedTypePolicyOptions( + web.TrustedTypePolicyOptions( createHTML: (String html) { return sanitizeHtml(html).toJS; }.toJS, @@ -326,18 +326,33 @@ gmaps.Size? _gmSizeFromIconConfig(List iconConfig, int sizeIndex) { size = gmaps.Size(rawIconSize[0]! as double, rawIconSize[1]! as double); } } + return size; } -/// Sets the size of the Google Maps icon. -void _setIconSize({required Size size, required gmaps.Icon icon}) { - final gmapsSize = gmaps.Size(size.width, size.height); - icon.size = gmapsSize; - icon.scaledSize = gmapsSize; +/// Sets the size and style of the [icon] element. +void _setIconStyle({ + required web.HTMLImageElement icon, + required gmaps.Size? size, + required double? opacity, + required bool? isVisible, +}) { + final web.CSSStyleDeclaration iconStyle = icon.style; + if (size != null) { + iconStyle + ..width = '${size.width.toStringAsFixed(1)}px' + ..height = '${size.height.toStringAsFixed(1)}px'; + } + if (opacity != null) { + iconStyle.opacity = opacity.toString(); + } + if (isVisible != null) { + iconStyle.visibility = isVisible ? 'visible' : 'hidden'; + } } void _setIconAnchor({ - required Size size, + required gmaps.Size size, required Offset anchor, required gmaps.Icon icon, }) { @@ -348,6 +363,13 @@ void _setIconAnchor({ icon.anchor = gmapsAnchor; } +// Sets the size of the Google Maps icon. +void _setIconSize({required gmaps.Size size, required gmaps.Icon icon}) { + final gmapsSize = gmaps.Size(size.width, size.height); + icon.size = gmapsSize; + icon.scaledSize = gmapsSize; +} + /// Determines the appropriate size for a bitmap based on its descriptor. /// /// This method returns the icon's size based on the provided [width] and @@ -355,12 +377,12 @@ void _setIconAnchor({ /// [imagePixelRatio] based on the actual size of the image fetched from the /// [url]. If only one of the dimensions is provided, the other is calculated to /// maintain the image's original aspect ratio. -Future _getBitmapSize(MapBitmap mapBitmap, String url) async { +Future _getBitmapSize(MapBitmap mapBitmap, String url) async { final double? width = mapBitmap.width; final double? height = mapBitmap.height; if (width != null && height != null) { // If both, width and height are set, return the provided dimensions. - return Size(width, height); + return gmaps.Size(width, height); } else { assert( url.isNotEmpty, @@ -391,7 +413,7 @@ Future _getBitmapSize(MapBitmap mapBitmap, String url) async { } // Return the calculated size. - return Size(targetWidth, targetHeight); + return gmaps.Size(targetWidth, targetHeight); } } @@ -399,10 +421,13 @@ Future _getBitmapSize(MapBitmap mapBitmap, String url) async { /// /// This method attempts to fetch the image size for a given [url]. Future _fetchBitmapSize(String url) async { - final image = HTMLImageElement()..src = url; + final image = web.HTMLImageElement()..src = url; // Wait for the onLoad or onError event. - await Future.any(>[image.onLoad.first, image.onError.first]); + await Future.any(>[ + image.onLoad.first, + image.onError.first, + ]); if (image.width == 0 || image.height == 0) { // Complete with null for invalid images. @@ -423,6 +448,181 @@ void _cleanUpBitmapConversionCaches() { _bitmapBlobUrlCache.clear(); } +Future _advancedMarkerIconFromPinConfig( + PinConfig config, { + required double opacity, + required bool isVisible, + required double rotation, +}) async { + final options = gmaps.PinElementOptions() + ..background = config.backgroundColor != null + ? _getCssColor(config.backgroundColor!) + : null + ..borderColor = config.borderColor != null + ? _getCssColor(config.borderColor!) + : null; + + final AdvancedMarkerGlyph? glyph = config.glyph; + switch (glyph) { + case final CircleGlyph circleGlyph: + options.glyphColor = _getCssColor(circleGlyph.color); + case final TextGlyph textGlyph: + final element = web.HTMLParagraphElement(); + element.text = textGlyph.text; + if (textGlyph.textColor != null) { + element.style.color = _getCssColor(textGlyph.textColor!); + } + options.glyph = element; + case final BitmapGlyph bitmapGlyph: + final web.Node? + glyphBitmap = await _advancedMarkerIconFromBitmapDescriptor( + bitmapGlyph.bitmap, + // Always opaque, opacity is handled by the parent marker. + opacity: 1.0, + // Always visible, as the visibility is handled by the parent marker. + isVisible: true, + rotation: rotation, + ); + options.glyph = glyphBitmap; + case null: + break; + } + + final pinElement = gmaps.PinElement(options); + final web.HTMLElement htmlElement = pinElement.element; + htmlElement.style + ..visibility = isVisible ? 'visible' : 'hidden' + ..opacity = opacity.toString() + ..transform = 'rotate(${rotation}deg)'; + return htmlElement; +} + +Future _advancedMarkerIconFromMapBitmap( + MapBitmap bitmap, { + required double opacity, + required bool isVisible, + required double rotation, +}) async { + final String url = switch (bitmap) { + (final BytesMapBitmap bytesMapBitmap) => _bitmapBlobUrlCache.putIfAbsent( + bytesMapBitmap.byteData.hashCode, + () { + final blob = web.Blob( + [bytesMapBitmap.byteData.toJS].toJS, + ); + return web.URL.createObjectURL(blob as JSObject); + }, + ), + (final AssetMapBitmap assetMapBitmap) => ui_web.assetManager.getAssetUrl( + assetMapBitmap.assetName, + ), + _ => throw UnimplementedError(), + }; + + final icon = web.HTMLImageElement()..src = url; + + final gmaps.Size? size = switch (bitmap.bitmapScaling) { + MapBitmapScaling.auto => await _getBitmapSize(bitmap, url), + MapBitmapScaling.none => null, + }; + _setIconStyle(icon: icon, size: size, opacity: opacity, isVisible: isVisible); + + return icon; +} + +Future _advancedMarkerIconFromAssetImage( + List iconConfig, { + required double opacity, + required bool isVisible, + required double rotation, +}) async { + assert(iconConfig.length >= 2); + // iconConfig[2] contains the DPIs of the screen, but that information is + // already encoded in the iconConfig[1] + final icon = web.HTMLImageElement() + ..src = ui_web.assetManager.getAssetUrl(iconConfig[1]! as String); + + final gmaps.Size? size = _gmSizeFromIconConfig(iconConfig, 3); + _setIconStyle(icon: icon, size: size, opacity: opacity, isVisible: isVisible); + return icon; +} + +Future _advancedMarkerIconFromBytes( + List iconConfig, { + required double opacity, + required bool isVisible, + required double rotation, +}) async { + // Grab the bytes, and put them into a blob. + final bytes = iconConfig[1]! as List; + // Create a Blob from bytes, but let the browser figure out the encoding. + final web.Blob blob; + + assert( + bytes is Uint8List, + 'The bytes for a BitmapDescriptor icon must be a Uint8List', + ); + + // TODO(ditman): Improve this conversion + // See https://github.com/dart-lang/web/issues/180 + blob = web.Blob([(bytes as Uint8List).toJS].toJS); + + final icon = web.HTMLImageElement() + ..src = web.URL.createObjectURL(blob as JSObject); + + final gmaps.Size? size = _gmSizeFromIconConfig(iconConfig, 2); + _setIconStyle(size: size, icon: icon, opacity: opacity, isVisible: isVisible); + return icon; +} + +/// Converts a [BitmapDescriptor] into a [Node] that can be used as +/// [AdvancedMarker]'s icon. +Future _advancedMarkerIconFromBitmapDescriptor( + BitmapDescriptor bitmapDescriptor, { + required double opacity, + required bool isVisible, + required double rotation, +}) async { + if (bitmapDescriptor is PinConfig) { + return _advancedMarkerIconFromPinConfig( + bitmapDescriptor, + rotation: rotation, + opacity: opacity, + isVisible: isVisible, + ); + } + + if (bitmapDescriptor is MapBitmap) { + return _advancedMarkerIconFromMapBitmap( + bitmapDescriptor, + rotation: rotation, + opacity: opacity, + isVisible: isVisible, + ); + } + + // The following code is for the deprecated BitmapDescriptor.fromBytes + // and BitmapDescriptor.fromAssetImage. + final iconConfig = bitmapDescriptor.toJson() as List; + if (iconConfig[0] == 'fromAssetImage') { + return _advancedMarkerIconFromAssetImage( + iconConfig, + opacity: opacity, + isVisible: isVisible, + rotation: rotation, + ); + } else if (iconConfig[0] == 'fromBytes') { + return _advancedMarkerIconFromBytes( + iconConfig, + opacity: opacity, + isVisible: isVisible, + rotation: rotation, + ); + } + + return null; +} + // Converts a [BitmapDescriptor] into a [gmaps.Icon] that can be used in Markers. Future _gmIconFromBitmapDescriptor( BitmapDescriptor bitmapDescriptor, @@ -431,13 +631,29 @@ Future _gmIconFromBitmapDescriptor( gmaps.Icon? icon; if (bitmapDescriptor is MapBitmap) { - final String url = urlFromMapBitmap(bitmapDescriptor); + final String url = switch (bitmapDescriptor) { + (final BytesMapBitmap bytesMapBitmap) => _bitmapBlobUrlCache.putIfAbsent( + bytesMapBitmap.byteData.hashCode, + () { + // TODO(ditman): Improve this conversion + // See https://github.com/dart-lang/web/issues/180 + final blob = web.Blob( + [bytesMapBitmap.byteData.toJS].toJS, + ); + return web.URL.createObjectURL(blob as JSObject); + }, + ), + (final AssetMapBitmap assetMapBitmap) => ui_web.assetManager.getAssetUrl( + assetMapBitmap.assetName, + ), + _ => throw UnimplementedError(), + }; icon = gmaps.Icon()..url = url; switch (bitmapDescriptor.bitmapScaling) { case MapBitmapScaling.auto: - final Size? size = await _getBitmapSize(bitmapDescriptor, url); + final gmaps.Size? size = await _getBitmapSize(bitmapDescriptor, url); if (size != null) { _setIconSize(size: size, icon: icon); _setIconAnchor(size: size, anchor: anchor, icon: icon); @@ -445,6 +661,7 @@ Future _gmIconFromBitmapDescriptor( case MapBitmapScaling.none: break; } + return icon; } @@ -468,7 +685,7 @@ Future _gmIconFromBitmapDescriptor( // Grab the bytes, and put them into a blob final bytes = iconConfig[1]! as List; // Create a Blob from bytes, but let the browser figure out the encoding - final Blob blob; + final web.Blob blob; assert( bytes is Uint8List, @@ -477,9 +694,9 @@ Future _gmIconFromBitmapDescriptor( // TODO(ditman): Improve this conversion // See https://github.com/dart-lang/web/issues/180 - blob = Blob([(bytes as Uint8List).toJS].toJS); + blob = web.Blob([(bytes as Uint8List).toJS].toJS); - icon = gmaps.Icon()..url = URL.createObjectURL(blob as JSObject); + icon = gmaps.Icon()..url = web.URL.createObjectURL(blob as JSObject); final gmaps.Size? size = _gmSizeFromIconConfig(iconConfig, 2); if (size != null) { @@ -491,27 +708,65 @@ Future _gmIconFromBitmapDescriptor( return icon; } -/// 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 _markerOptionsFromMarker( +// 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 _markerOptionsFromMarker( Marker marker, - gmaps.Marker? currentMarker, + T? currentMarker, ) async { - return gmaps.MarkerOptions() - ..position = gmaps.LatLng( - marker.position.latitude, - marker.position.longitude, - ) - ..title = sanitizeHtml(marker.infoWindow.title ?? '') - // The deprecated parameter is used here to avoid losing precision. - // ignore: deprecated_member_use - ..zIndex = marker.zIndex - ..visible = marker.visible - ..opacity = marker.alpha - ..draggable = marker.draggable - ..icon = await _gmIconFromBitmapDescriptor(marker.icon, marker.anchor); - // TODO(ditman): Compute anchor properly, otherwise infowindows attach to the wrong spot. - // Flat and Rotation are not supported directly on the web. + 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; + return options as O; + } else { + final options = gmaps.MarkerOptions() + ..position = gmaps.LatLng( + marker.position.latitude, + marker.position.longitude, + ) + ..icon = await _gmIconFromBitmapDescriptor(marker.icon, marker.anchor) + ..title = sanitizeHtml(marker.infoWindow.title ?? '') + ..zIndex = marker.zIndex + ..visible = marker.visible + ..opacity = marker.alpha + ..draggable = marker.draggable; + + // TODO(ditman): Compute anchor properly, otherwise infowindows attach to the wrong spot. + // Flat and Rotation are not supported directly on the web. + + return options as O; + } +} + +/// Gets marker Id from a [marker] object. +MarkerId getMarkerId(Object marker) { + final object = marker as JSObject; + if (object.isA()) { + final mapObject = marker as gmaps.MVCObject; + return MarkerId((mapObject.get('markerId')! as JSString).toDart); + } else if (object.isA()) { + final element = marker as gmaps.AdvancedMarkerElement; + return MarkerId(element.id); + } else { + throw ArgumentError( + 'Must be either a gmaps.Marker or a gmaps.AdvancedMarkerElement', + ); + } } gmaps.CircleOptions _circleOptionsFromCircle(Circle circle) { @@ -747,8 +1002,10 @@ String urlFromMapBitmap(MapBitmap mapBitmap) { (final BytesMapBitmap bytesMapBitmap) => _bitmapBlobUrlCache.putIfAbsent( bytesMapBitmap.byteData.hashCode, () { - final blob = Blob([bytesMapBitmap.byteData.toJS].toJS); - return URL.createObjectURL(blob as JSObject); + final blob = web.Blob( + [bytesMapBitmap.byteData.toJS].toJS, + ); + return web.URL.createObjectURL(blob as JSObject); }, ), (final AssetMapBitmap assetMapBitmap) => ui_web.assetManager.getAssetUrl( @@ -851,3 +1108,15 @@ gmaps.ControlPosition? _toControlPosition( return gmaps.ControlPosition.TOP_RIGHT; } } + +gmaps.CollisionBehavior _markerCollisionBehaviorToGmCollisionBehavior( + MarkerCollisionBehavior markerCollisionBehavior, +) { + return switch (markerCollisionBehavior) { + MarkerCollisionBehavior.requiredDisplay => gmaps.CollisionBehavior.REQUIRED, + MarkerCollisionBehavior.optionalAndHidesLowerPriority => + gmaps.CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY, + MarkerCollisionBehavior.requiredAndHidesOptional => + gmaps.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL, + }; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart index 8e862f6e3dd..bb8a5c62434 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart @@ -7,7 +7,7 @@ part of '../google_maps_flutter_web.dart'; /// Type used when passing an override to the _createMap function. @visibleForTesting typedef DebugCreateMapFunction = - gmaps.Map Function(HTMLElement div, gmaps.MapOptions options); + gmaps.Map Function(web.HTMLElement div, gmaps.MapOptions options); /// Type used when passing an override to the _setOptions function. @visibleForTesting @@ -38,13 +38,58 @@ class GoogleMapController { _heatmapsController = HeatmapsController(); _polygonsController = PolygonsController(stream: _streamController); _polylinesController = PolylinesController(stream: _streamController); - _clusterManagersController = ClusterManagersController( - stream: _streamController, - ); - _markersController = MarkersController( - stream: _streamController, - clusterManagersController: _clusterManagersController!, - ); + + // Check if all markers are of the same type. Mixing marker types is not + // allowed. + final Set markerTypes = _markers + .map((Marker e) => e.runtimeType) + .toSet(); + if (markerTypes.isNotEmpty) { + assert(markerTypes.length == 1, 'All markers must be of the same type.'); + + switch (mapConfiguration.markerType) { + case null: + case MarkerType.marker: + assert( + markerTypes.first == Marker, + 'All markers must be of type Marker because ' + 'mapConfiguration.markerType is MarkerType.marker', + ); + case MarkerType.advancedMarker: + assert( + markerTypes.first == AdvancedMarker, + 'All markers must be of type AdvancedMarker because ' + 'mapConfiguration.markerType is MarkerType.advanced', + ); + } + } + + // Advanced and legacy markers are handled differently so markers controller + // and cluster manager need be initialized with the correct marker type. + _clusterManagersController = switch (mapConfiguration.markerType) { + null || MarkerType.marker => ClusterManagersController( + stream: _streamController, + ), + MarkerType.advancedMarker => + ClusterManagersController( + stream: _streamController, + ), + }; + _markersController = switch (mapConfiguration.markerType) { + null || MarkerType.marker => LegacyMarkersController( + stream: stream, + clusterManagersController: + _clusterManagersController! + as ClusterManagersController, + ), + MarkerType.advancedMarker => AdvancedMarkersController( + stream: stream, + clusterManagersController: + _clusterManagersController! + as ClusterManagersController, + ), + }; + _tileOverlaysController = TileOverlaysController(); _groundOverlaysController = GroundOverlaysController( stream: _streamController, @@ -98,7 +143,7 @@ class GoogleMapController { // The Flutter widget that contains the rendered Map. HtmlElementView? _widget; - late HTMLElement _div; + late web.HTMLElement _div; /// The Flutter widget that will contain the rendered Map. Used for caching. Widget? get widget { @@ -133,8 +178,8 @@ class GoogleMapController { HeatmapsController? _heatmapsController; PolygonsController? _polygonsController; PolylinesController? _polylinesController; - MarkersController? _markersController; - ClusterManagersController? _clusterManagersController; + MarkersController? _markersController; + ClusterManagersController? _clusterManagersController; TileOverlaysController? _tileOverlaysController; GroundOverlaysController? _groundOverlaysController; @@ -151,7 +196,7 @@ class GoogleMapController { /// The ClusterManagersController of this Map. Only for integration testing. @visibleForTesting - ClusterManagersController? get clusterManagersController => + ClusterManagersController? get clusterManagersController => _clusterManagersController; /// The GroundOverlaysController of this Map. Only for integration testing. @@ -164,12 +209,12 @@ class GoogleMapController { void debugSetOverrides({ DebugCreateMapFunction? createMap, DebugSetOptionsFunction? setOptions, - MarkersController? markers, + MarkersController? markers, CirclesController? circles, HeatmapsController? heatmaps, PolygonsController? polygons, PolylinesController? polylines, - ClusterManagersController? clusterManagers, + ClusterManagersController? clusterManagers, TileOverlaysController? tileOverlays, GroundOverlaysController? groundOverlays, }) { @@ -188,7 +233,7 @@ class GoogleMapController { DebugCreateMapFunction? _overrideCreateMap; DebugSetOptionsFunction? _overrideSetOptions; - gmaps.Map _createMap(HTMLElement div, gmaps.MapOptions options) { + gmaps.Map _createMap(web.HTMLElement div, gmaps.MapOptions options) { if (_overrideCreateMap != null) { return _overrideCreateMap!(div, options); } @@ -467,7 +512,7 @@ class GoogleMapController { await Future.value(_googleMap!.bounds) ?? _nullGmapsLatLngBounds; - return gmLatLngBoundsTolatLngBounds(bounds); + return gmLatLngBoundsToLatLngBounds(bounds); } /// Returns the [ScreenCoordinate] for a given viewport [LatLng]. @@ -650,6 +695,13 @@ class GoogleMapController { return _markersController?.isInfoWindowShown(markerId) ?? false; } + /// Returns true if this map supports [AdvancedMarker]s. + bool isAdvancedMarkersAvailable() { + assert(_googleMap != null, 'Cannot get map capabilities of a null map.'); + + return _googleMap!.mapCapabilities.isAdvancedMarkersAvailable ?? false; + } + // Cleanup /// Disposes of this controller and its resources. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart index e980252e7c8..04982b8d75e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart @@ -314,6 +314,12 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { return _map(mapId).lastStyleError; } + @override + Future isAdvancedMarkersAvailable({required int mapId}) async { + final GoogleMapController map = _map(mapId); + return map.isAdvancedMarkersAvailable(); + } + /// Disposes of the current map. It can't be used afterwards! @override void dispose({required int mapId}) { diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart index b14a8e2812b..9af2c585665 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart @@ -16,7 +16,7 @@ typedef ConfigurationProvider = MapConfiguration Function(int mapId); /// Function that gets the [ClusterManagersController] for a given `mapId`. typedef ClusterManagersControllerProvider = - ClusterManagersController? Function(int mapId); + ClusterManagersController? Function(int mapId); /// Function that gets the [GroundOverlaysController] for a given `mapId`. typedef GroundOverlaysControllerProvider = @@ -108,7 +108,7 @@ class GoogleMapsInspectorWeb extends GoogleMapsInspectorPlatform { Uint8List.fromList([0]), bitmapScaling: MapBitmapScaling.none, ), - bounds: gmLatLngBoundsTolatLngBounds(groundOverlay.bounds), + bounds: gmLatLngBoundsToLatLngBounds(groundOverlay.bounds), transparency: 1.0 - groundOverlay.opacity, visible: groundOverlay.map != null, clickable: clickable != null && (clickable as JSBoolean).toDart, diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart index fadf59fd767..70b99f41721 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart @@ -4,11 +4,20 @@ part of '../google_maps_flutter_web.dart'; -/// The `MarkerController` class wraps a [gmaps.Marker], how it handles events, and its associated (optional) [gmaps.InfoWindow] widget. -class MarkerController { - /// Creates a `MarkerController`, which wraps a [gmaps.Marker] object, its `onTap`/`onDrag` behavior, and its associated [gmaps.InfoWindow]. +/// The `MarkerController` class wraps a [gmaps.AdvancedMarkerElement] +/// or [gmaps.Marker], how it handles events, and its associated (optional) +/// [gmaps.InfoWindow] widget. +/// +/// Type parameters: +/// * [T] - The marker type (e.g., [gmaps.Marker] or [gmaps.AdvancedMarkerElement]) +/// * [O] - The options type used to configure the marker +/// (e.g., [gmaps.MarkerOptions] or [gmaps.AdvancedMarkerElementOptions]) +abstract class MarkerController { + /// Creates a `MarkerController`, which wraps a [gmaps.AdvancedMarkerElement] + /// or [gmaps.Marker] object, its `onTap`/`onDrag` behavior, and its + /// associated [gmaps.InfoWindow]. MarkerController({ - required gmaps.Marker marker, + required T marker, gmaps.InfoWindow? infoWindow, bool consumeTapEvents = false, LatLngCallback? onDragStart, @@ -20,32 +29,16 @@ class MarkerController { _infoWindow = infoWindow, _consumeTapEvents = consumeTapEvents, _clusterManagerId = clusterManagerId { - if (onTap != null) { - marker.onClick.listen((gmaps.MapMouseEvent event) { - onTap.call(); - }); - } - if (onDragStart != null) { - marker.onDragstart.listen((gmaps.MapMouseEvent event) { - marker.position = event.latLng; - onDragStart.call(event.latLng ?? _nullGmapsLatLng); - }); - } - if (onDrag != null) { - marker.onDrag.listen((gmaps.MapMouseEvent event) { - marker.position = event.latLng; - onDrag.call(event.latLng ?? _nullGmapsLatLng); - }); - } - if (onDragEnd != null) { - marker.onDragend.listen((gmaps.MapMouseEvent event) { - marker.position = event.latLng; - onDragEnd.call(event.latLng ?? _nullGmapsLatLng); - }); - } + addMarkerListener( + marker: marker, + onDragStart: onDragStart, + onDrag: onDrag, + onDragEnd: onDragEnd, + onTap: onTap, + ); } - gmaps.Marker? _marker; + T? _marker; final bool _consumeTapEvents; @@ -64,56 +57,271 @@ class MarkerController { /// Returns [ClusterManagerId] if marker belongs to cluster. ClusterManagerId? get clusterManagerId => _clusterManagerId; - /// Returns the [gmaps.Marker] associated to this controller. - gmaps.Marker? get marker => _marker; + /// Returns the marker associated to this controller. + T? get marker => _marker; /// Returns the [gmaps.InfoWindow] associated to the marker. @visibleForTesting gmaps.InfoWindow? get infoWindow => _infoWindow; - /// Updates the options of the wrapped [gmaps.Marker] object. + /// Updates the options of the wrapped marker object. + /// + /// This cannot be called after [remove]. + void update(O options, {web.HTMLElement? newInfoWindowContent}); + + /// Initializes the listener for the wrapped marker object. + void addMarkerListener({ + required T marker, + required LatLngCallback? onDragStart, + required LatLngCallback? onDrag, + required LatLngCallback? onDragEnd, + required VoidCallback? onTap, + }); + + /// Disposes of the currently wrapped marker object. + void remove(); + + /// Hide the associated [gmaps.InfoWindow]. + /// + /// This cannot be called after [remove]. + void hideInfoWindow() { + assert(_marker != null, 'Cannot `hideInfoWindow` on a `remove`d Marker.'); + if (_infoWindow != null) { + _infoWindow.close(); + _infoWindowShown = false; + } + } + + /// Show the associated [gmaps.InfoWindow]. /// /// This cannot be called after [remove]. + void showInfoWindow(); +} + +/// A `MarkerController` that wraps a [gmaps.Marker] object. +/// +/// [gmaps.Marker] is a legacy class that is being replaced +/// by [gmaps.AdvancedMarkerElement]. +class LegacyMarkerController + extends MarkerController { + /// Creates a `LegacyMarkerController`, which wraps a [gmaps.Marker] object. + LegacyMarkerController({ + required super.marker, + super.infoWindow, + super.consumeTapEvents, + super.onDragStart, + super.onDrag, + super.onDragEnd, + super.onTap, + super.clusterManagerId, + }); + + /// List of active stream subscriptions for marker events. + /// + /// This list keeps track of all event subscriptions created for the marker, + /// including taps and different drag events. + /// These subscriptions should be disposed when the controller is disposed. + final List> _subscriptions = + >[]; + + @override + void addMarkerListener({ + required gmaps.Marker marker, + required LatLngCallback? onDragStart, + required LatLngCallback? onDrag, + required LatLngCallback? onDragEnd, + required VoidCallback? onTap, + }) { + if (onTap != null) { + _subscriptions.add( + marker.onClick.listen((gmaps.MapMouseEvent event) { + onTap.call(); + }), + ); + } + if (onDragStart != null) { + _subscriptions.add( + marker.onDragstart.listen((gmaps.MapMouseEvent event) { + marker.position = event.latLng; + onDragStart.call(event.latLng ?? _nullGmapsLatLng); + }), + ); + } + if (onDrag != null) { + _subscriptions.add( + marker.onDrag.listen((gmaps.MapMouseEvent event) { + marker.position = event.latLng; + onDrag.call(event.latLng ?? _nullGmapsLatLng); + }), + ); + } + if (onDragEnd != null) { + _subscriptions.add( + marker.onDragend.listen((gmaps.MapMouseEvent event) { + marker.position = event.latLng; + onDragEnd.call(event.latLng ?? _nullGmapsLatLng); + }), + ); + } + } + + @override + void remove() { + if (_marker != null) { + _infoWindowShown = false; + _marker!.map = null; + _marker = null; + + for (final StreamSubscription sub in _subscriptions) { + sub.cancel(); + } + _subscriptions.clear(); + } + } + + @override + void showInfoWindow() { + if (_marker == null) { + throw StateError('Cannot `showInfoWindow` on a `remove`d Marker.'); + } + + if (_infoWindow != null) { + _infoWindow.open(_marker!.map, _marker); + _infoWindowShown = true; + } + } + + @override void update( gmaps.MarkerOptions options, { - HTMLElement? newInfoWindowContent, + web.HTMLElement? newInfoWindowContent, }) { assert(_marker != null, 'Cannot `update` Marker after calling `remove`.'); _marker!.options = options; + if (_infoWindow != null && newInfoWindowContent != null) { _infoWindow.content = newInfoWindowContent; } } +} + +/// A `MarkerController` that wraps a [gmaps.AdvancedMarkerElement] object. +/// +/// [gmaps.AdvancedMarkerElement] is a new class that is +/// replacing [gmaps.Marker]. +class AdvancedMarkerController + extends + MarkerController< + gmaps.AdvancedMarkerElement, + gmaps.AdvancedMarkerElementOptions + > { + /// Creates a `AdvancedMarkerController`, which wraps + /// a [gmaps.AdvancedMarkerElement] object. + AdvancedMarkerController({ + required super.marker, + super.infoWindow, + super.consumeTapEvents, + super.onDragStart, + super.onDrag, + super.onDragEnd, + super.onTap, + super.clusterManagerId, + }); + + /// List of active stream subscriptions for marker events. + /// + /// This list keeps track of all event subscriptions created for the marker, + /// including taps and different drag events. + /// These subscriptions should be disposed when the controller is disposed. + final List> _subscriptions = + >[]; + + @override + void addMarkerListener({ + required gmaps.AdvancedMarkerElement marker, + required LatLngCallback? onDragStart, + required LatLngCallback? onDrag, + required LatLngCallback? onDragEnd, + required VoidCallback? onTap, + }) { + if (onTap != null) { + _subscriptions.add( + marker.onClick.listen((gmaps.MapMouseEvent event) { + onTap.call(); + }), + ); + } + if (onDragStart != null) { + _subscriptions.add( + marker.onDragstart.listen((gmaps.MapMouseEvent event) { + marker.position = event.latLng; + onDragStart.call(event.latLng ?? _nullGmapsLatLng); + }), + ); + } + if (onDrag != null) { + _subscriptions.add( + marker.onDrag.listen((gmaps.MapMouseEvent event) { + marker.position = event.latLng; + onDrag.call(event.latLng ?? _nullGmapsLatLng); + }), + ); + } + if (onDragEnd != null) { + _subscriptions.add( + marker.onDragend.listen((gmaps.MapMouseEvent event) { + marker.position = event.latLng; + onDragEnd.call(event.latLng ?? _nullGmapsLatLng); + }), + ); + } + } - /// Disposes of the currently wrapped [gmaps.Marker]. + @override void remove() { if (_marker != null) { _infoWindowShown = false; - _marker!.visible = false; + + _marker!.remove(); _marker!.map = null; _marker = null; - } - } - /// Hide the associated [gmaps.InfoWindow]. - /// - /// This cannot be called after [remove]. - void hideInfoWindow() { - assert(_marker != null, 'Cannot `hideInfoWindow` on a `remove`d Marker.'); - if (_infoWindow != null) { - _infoWindow.close(); - _infoWindowShown = false; + for (final StreamSubscription sub in _subscriptions) { + sub.cancel(); + } + _subscriptions.clear(); } } - /// Show the associated [gmaps.InfoWindow]. - /// - /// This cannot be called after [remove]. + @override void showInfoWindow() { - assert(_marker != null, 'Cannot `showInfoWindow` on a `remove`d Marker.'); + if (_marker == null) { + throw StateError('Cannot `showInfoWindow` on a `remove`d Marker.'); + } + if (_infoWindow != null) { _infoWindow.open(_marker!.map, _marker); _infoWindowShown = true; } } + + @override + void update( + gmaps.AdvancedMarkerElementOptions options, { + 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; + marker.gmpDraggable = options.gmpDraggable; + marker.position = options.position; + marker.title = options.title ?? ''; + marker.zIndex = options.zIndex; + + if (_infoWindow != null && newInfoWindowContent != null) { + _infoWindow.content = newInfoWindowContent; + } + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering.dart index 0f235dfc615..a09ca329e82 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering.dart @@ -17,7 +17,10 @@ import 'types.dart'; /// This class maps [ClusterManager] objects to javascript [MarkerClusterer] /// objects and provides an interface for adding and removing markers from /// clusters. -class ClusterManagersController extends GeometryController { +/// +/// [T] must extend [JSObject]. It's not specified in code because our mocking +/// framework does not support mocking JSObjects. +class ClusterManagersController extends GeometryController { /// Creates a new [ClusterManagersController] instance. /// /// The [stream] parameter is a required [StreamController] used for @@ -26,13 +29,13 @@ class ClusterManagersController extends GeometryController { required StreamController> stream, }) : _streamController = stream, _clusterManagerIdToMarkerClusterer = - {}; + >{}; // The stream over which cluster managers broadcast their events final StreamController> _streamController; // A cache of [MarkerClusterer]s indexed by their [ClusterManagerId]. - final Map + final Map> _clusterManagerIdToMarkerClusterer; /// Adds a set of [ClusterManager] objects to the cache. @@ -41,11 +44,11 @@ class ClusterManagersController extends GeometryController { } void _addClusterManager(ClusterManager clusterManager) { - final MarkerClusterer markerClusterer = createMarkerClusterer( + final MarkerClusterer markerClusterer = createMarkerClusterer( googleMap, ( gmaps.MapMouseEvent event, - MarkerClustererCluster cluster, + MarkerClustererCluster cluster, gmaps.Map map, ) => _clusterClicked(clusterManager.clusterManagerId, event, cluster, map), @@ -62,7 +65,7 @@ class ClusterManagersController extends GeometryController { } void _removeClusterManager(ClusterManagerId clusterManagerId) { - final MarkerClusterer? markerClusterer = + final MarkerClusterer? markerClusterer = _clusterManagerIdToMarkerClusterer[clusterManagerId]; if (markerClusterer != null) { markerClusterer.clearMarkers(true); @@ -71,10 +74,9 @@ class ClusterManagersController extends GeometryController { _clusterManagerIdToMarkerClusterer.remove(clusterManagerId); } - /// Adds given [gmaps.Marker] to the [MarkerClusterer] with given - /// [ClusterManagerId]. - void addItem(ClusterManagerId clusterManagerId, gmaps.Marker marker) { - final MarkerClusterer? markerClusterer = + /// Adds given markers to the [MarkerClusterer] with given [ClusterManagerId]. + void addItem(ClusterManagerId clusterManagerId, T marker) { + final MarkerClusterer? markerClusterer = _clusterManagerIdToMarkerClusterer[clusterManagerId]; if (markerClusterer != null) { markerClusterer.addMarker(marker, true); @@ -82,11 +84,11 @@ class ClusterManagersController extends GeometryController { } } - /// Removes given [gmaps.Marker] from the [MarkerClusterer] with given - /// [ClusterManagerId]. - void removeItem(ClusterManagerId clusterManagerId, gmaps.Marker? marker) { + /// Removes given marker from the [MarkerClusterer] with + /// given [ClusterManagerId]. + void removeItem(ClusterManagerId clusterManagerId, T? marker) { if (marker != null) { - final MarkerClusterer? markerClusterer = + final MarkerClusterer? markerClusterer = _clusterManagerIdToMarkerClusterer[clusterManagerId]; if (markerClusterer != null) { markerClusterer.removeMarker(marker, true); @@ -98,12 +100,12 @@ class ClusterManagersController extends GeometryController { /// Returns list of clusters in [MarkerClusterer] with given /// [ClusterManagerId]. List getClusters(ClusterManagerId clusterManagerId) { - final MarkerClusterer? markerClusterer = + final MarkerClusterer? markerClusterer = _clusterManagerIdToMarkerClusterer[clusterManagerId]; if (markerClusterer != null) { return markerClusterer.clusters .map( - (MarkerClustererCluster cluster) => + (MarkerClustererCluster cluster) => _convertCluster(clusterManagerId, cluster), ) .toList(); @@ -114,7 +116,7 @@ class ClusterManagersController extends GeometryController { void _clusterClicked( ClusterManagerId clusterManagerId, gmaps.MapMouseEvent event, - MarkerClustererCluster markerClustererCluster, + MarkerClustererCluster markerClustererCluster, gmaps.Map map, ) { if (markerClustererCluster.count > 0 && @@ -130,19 +132,16 @@ class ClusterManagersController extends GeometryController { /// Converts [MarkerClustererCluster] to [Cluster]. Cluster _convertCluster( ClusterManagerId clusterManagerId, - MarkerClustererCluster markerClustererCluster, + MarkerClustererCluster markerClustererCluster, ) { final LatLng position = gmLatLngToLatLng(markerClustererCluster.position); - final LatLngBounds bounds = gmLatLngBoundsTolatLngBounds( + final LatLngBounds bounds = gmLatLngBoundsToLatLngBounds( markerClustererCluster.bounds!, ); - final List markerIds = markerClustererCluster.markers - .map( - (gmaps.Marker marker) => - MarkerId((marker.get('markerId')! as JSString).toDart), - ) + .map(getMarkerId) .toList(); + return Cluster( clusterManagerId, markerIds, diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering_js_interop.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering_js_interop.dart index d5fca430e76..fc639778706 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering_js_interop.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering_js_interop.dart @@ -13,25 +13,25 @@ import 'dart:js_interop'; import 'package:google_maps/google_maps.dart' as gmaps; /// A typedef representing a callback function for handling cluster tap events. -typedef ClusterClickHandler = - void Function(gmaps.MapMouseEvent, MarkerClustererCluster, gmaps.Map); +typedef ClusterClickHandler = + void Function(gmaps.MapMouseEvent, MarkerClustererCluster, gmaps.Map); /// The [MarkerClustererOptions] object used to initialize [MarkerClusterer]. /// /// See: https://googlemaps.github.io/js-markerclusterer/interfaces/MarkerClustererOptions.html @JS() @anonymous -extension type MarkerClustererOptions._(JSObject _) implements JSObject { +extension type MarkerClustererOptions._(JSObject _) implements JSObject { /// Constructs a new [MarkerClustererOptions] object. factory MarkerClustererOptions({ gmaps.Map? map, - List? markers, - ClusterClickHandler? onClusterClick, - }) => MarkerClustererOptions._js( + List? markers, + ClusterClickHandler? onClusterClick, + }) => MarkerClustererOptions._js( map: map as JSAny?, markers: markers?.cast().toJS ?? JSArray(), onClusterClick: onClusterClick != null - ? ((JSAny event, MarkerClustererCluster cluster, JSAny map) => + ? ((JSAny event, MarkerClustererCluster cluster, JSAny map) => onClusterClick( event as gmaps.MapMouseEvent, cluster, @@ -52,13 +52,13 @@ extension type MarkerClustererOptions._(JSObject _) implements JSObject { @JS('map') external JSAny? get _map; - /// Returns the list of [gmaps.Marker] objects. - List? get markers => _markers?.toDart.cast(); + /// Returns the list of marker objects. + List? get markers => _markers?.toDart.cast(); @JS('markers') external JSArray? get _markers; /// Returns the onClusterClick handler. - ClusterClickHandler? get onClusterClick => + ClusterClickHandler? get onClusterClick => _onClusterClick?.toDart as ClusterClickHandler?; @JS('onClusterClick') external JSExportedDartFunction? get _onClusterClick; @@ -68,14 +68,14 @@ extension type MarkerClustererOptions._(JSObject _) implements JSObject { /// /// https://googlemaps.github.io/js-markerclusterer/classes/Cluster.html @JS('markerClusterer.Cluster') -extension type MarkerClustererCluster._(JSObject _) implements JSObject { +extension type MarkerClustererCluster._(JSObject _) implements JSObject { /// Getter for the cluster marker. - gmaps.Marker get marker => _marker as gmaps.Marker; + T get marker => _marker as T; @JS('marker') external JSAny get _marker; /// List of markers in the cluster. - List get markers => _markers.toDart.cast(); + List get markers => _markers.toDart.cast(); @JS('markers') external JSArray get _markers; @@ -96,7 +96,7 @@ extension type MarkerClustererCluster._(JSObject _) implements JSObject { external void delete(); /// Adds a marker to the cluster. - void push(gmaps.Marker marker) => _push(marker as JSAny); + void push(T marker) => _push(marker as JSAny); @JS('push') external void _push(JSAny marker); } @@ -105,30 +105,29 @@ extension type MarkerClustererCluster._(JSObject _) implements JSObject { /// /// https://googlemaps.github.io/js-markerclusterer/classes/MarkerClusterer.html @JS('markerClusterer.MarkerClusterer') -extension type MarkerClusterer._(JSObject _) implements JSObject { +extension type MarkerClusterer._(JSObject _) implements JSObject { /// Constructs a new [MarkerClusterer] object. - external MarkerClusterer(MarkerClustererOptions options); + external MarkerClusterer(MarkerClustererOptions options); /// Adds a marker to be clustered by the [MarkerClusterer]. - void addMarker(gmaps.Marker marker, bool? noDraw) => - _addMarker(marker as JSAny, noDraw); + void addMarker(T marker, bool? noDraw) => _addMarker(marker as JSAny, noDraw); @JS('addMarker') external void _addMarker(JSAny marker, bool? noDraw); /// Adds a list of markers to be clustered by the [MarkerClusterer]. - void addMarkers(List? markers, bool? noDraw) => + void addMarkers(List? markers, bool? noDraw) => _addMarkers(markers?.cast().toJS, noDraw); @JS('addMarkers') external void _addMarkers(JSArray? markers, bool? noDraw); /// Removes a marker from the [MarkerClusterer]. - bool removeMarker(gmaps.Marker marker, bool? noDraw) => + bool removeMarker(T marker, bool? noDraw) => _removeMarker(marker as JSAny, noDraw); @JS('removeMarker') external bool _removeMarker(JSAny marker, bool? noDraw); /// Removes a list of markers from the [MarkerClusterer]. - bool removeMarkers(List? markers, bool? noDraw) => + bool removeMarkers(List? markers, bool? noDraw) => _removeMarkers(markers?.cast().toJS, noDraw); @JS('removeMarkers') external bool _removeMarkers(JSArray? markers, bool? noDraw); @@ -143,8 +142,8 @@ extension type MarkerClusterer._(JSObject _) implements JSObject { external void onRemove(); /// Returns the list of clusters. - List get clusters => - _clusters.toDart.cast(); + List> get clusters => + _clusters.toDart.cast>(); @JS('clusters') external JSArray get _clusters; @@ -154,13 +153,13 @@ extension type MarkerClusterer._(JSObject _) implements JSObject { /// Creates [MarkerClusterer] object with given [gmaps.Map] and /// [ClusterClickHandler]. -MarkerClusterer createMarkerClusterer( +MarkerClusterer createMarkerClusterer( gmaps.Map map, - ClusterClickHandler onClusterClickHandler, + ClusterClickHandler onClusterClickHandler, ) { - final options = MarkerClustererOptions( + final options = MarkerClustererOptions( map: map, onClusterClick: onClusterClickHandler, ); - return MarkerClusterer(options); + return MarkerClusterer(options); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart index a0dc2e361c4..7dd31d1c544 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart @@ -5,26 +5,40 @@ part of '../google_maps_flutter_web.dart'; /// This class manages a set of [MarkerController]s associated to a [GoogleMapController]. -class MarkersController extends GeometryController { +/// +/// * [LegacyMarkersController] implements the [MarkersController] for the +/// legacy [gmaps.Marker] class. +/// * [AdvancedMarkersController] implements the [MarkersController] for the +/// advanced [gmaps.AdvancedMarkerElement] class. +/// +/// Type parameters: +/// * [T] - The marker type (e.g., [gmaps.Marker] or [gmaps.AdvancedMarkerElement]) +/// * [O] - The options type used to configure the marker +/// (e.g., [gmaps.MarkerOptions] or [gmaps.AdvancedMarkerElementOptions]) +/// +/// [T] must extend [JSObject]. It's not specified in code because our mocking +/// framework does not support mocking JSObjects. +abstract class MarkersController + extends GeometryController { /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. MarkersController({ required StreamController> stream, - required ClusterManagersController clusterManagersController, + required ClusterManagersController clusterManagersController, }) : _streamController = stream, _clusterManagersController = clusterManagersController, - _markerIdToController = {}; + _markerIdToController = >{}; // A cache of [MarkerController]s indexed by their [MarkerId]. - final Map _markerIdToController; + final Map> _markerIdToController; // The stream over which markers broadcast their events final StreamController> _streamController; - final ClusterManagersController _clusterManagersController; + final ClusterManagersController _clusterManagersController; /// Returns the cache of [MarkerController]s. Test only. @visibleForTesting - Map get markers => _markerIdToController; + Map> get markers => _markerIdToController; /// Adds a set of [Marker] objects to the cache. /// @@ -43,8 +57,8 @@ class MarkersController extends GeometryController { // Google Maps' JS SDK does not have a click event on the InfoWindow, so // we make one... if (infoWindowOptions.content != null && - infoWindowOptions.content is HTMLElement) { - final content = infoWindowOptions.content! as HTMLElement; + infoWindowOptions.content is web.HTMLElement) { + final content = infoWindowOptions.content! as web.HTMLElement; content.onclick = (JSAny? _) { _onInfoWindowTap(marker.markerId); @@ -52,53 +66,41 @@ class MarkersController extends GeometryController { } } - final gmaps.Marker? currentMarker = - _markerIdToController[marker.markerId]?.marker; - - final gmaps.MarkerOptions markerOptions = await _markerOptionsFromMarker( + final MarkerController? markerController = + _markerIdToController[marker.markerId]; + final T? currentMarker = markerController?.marker; + final O markerOptions = await _markerOptionsFromMarker( marker, currentMarker, ); - - final gmMarker = gmaps.Marker(markerOptions); - - gmMarker.set('markerId', marker.markerId.value.toJS); - - if (marker.clusterManagerId != null) { - _clusterManagersController.addItem(marker.clusterManagerId!, gmMarker); - } else { - gmMarker.map = googleMap; - } - - final controller = MarkerController( - marker: gmMarker, - clusterManagerId: marker.clusterManagerId, - infoWindow: gmInfoWindow, - consumeTapEvents: marker.consumeTapEvents, - onTap: () { - showMarkerInfoWindow(marker.markerId); - _onMarkerTap(marker.markerId); - }, - onDragStart: (gmaps.LatLng latLng) { - _onMarkerDragStart(marker.markerId, latLng); - }, - onDrag: (gmaps.LatLng latLng) { - _onMarkerDrag(marker.markerId, latLng); - }, - onDragEnd: (gmaps.LatLng latLng) { - _onMarkerDragEnd(marker.markerId, latLng); - }, + final MarkerController controller = await createMarkerController( + marker, + markerOptions, + gmInfoWindow, ); _markerIdToController[marker.markerId] = controller; } + /// Creates a [MarkerController] for the given [marker]. + /// + /// [markerOptions] contains configuration that should be used to create + /// a [gmaps.Marker] or [gmaps.AdvancedMarkerElement] object. [markersOptions] + /// is either [gmaps.MarkerOptions] or [gmaps.AdvancedMarkerElementOptions]. + /// + /// [gmInfoWindow] is marker's info window to show on tap. + Future> createMarkerController( + Marker marker, + O markerOptions, + gmaps.InfoWindow? gmInfoWindow, + ); + /// Updates a set of [Marker] objects with new options. Future changeMarkers(Set markersToChange) async { await Future.wait(markersToChange.map(_changeMarker)); } Future _changeMarker(Marker marker) async { - final MarkerController? markerController = + final MarkerController? markerController = _markerIdToController[marker.markerId]; if (markerController != null) { final ClusterManagerId? oldClusterManagerId = @@ -110,13 +112,15 @@ class MarkersController extends GeometryController { _removeMarker(marker.markerId); await _addMarker(marker); } else { - final gmaps.MarkerOptions markerOptions = - await _markerOptionsFromMarker(marker, markerController.marker); + final O markerOptions = await _markerOptionsFromMarker( + marker, + markerController.marker, + ); final gmaps.InfoWindowOptions? infoWindow = _infoWindowOptionsFromMarker(marker); markerController.update( markerOptions, - newInfoWindowContent: infoWindow?.content as HTMLElement?, + newInfoWindowContent: infoWindow?.content as web.HTMLElement?, ); } } @@ -128,7 +132,8 @@ class MarkersController extends GeometryController { } void _removeMarker(MarkerId markerId) { - final MarkerController? markerController = _markerIdToController[markerId]; + final MarkerController? markerController = + _markerIdToController[markerId]; if (markerController?.clusterManagerId != null) { _clusterManagersController.removeItem( markerController!.clusterManagerId!, @@ -146,7 +151,8 @@ class MarkersController extends GeometryController { /// See also [hideMarkerInfoWindow] and [isInfoWindowShown]. void showMarkerInfoWindow(MarkerId markerId) { _hideAllMarkerInfoWindow(); - final MarkerController? markerController = _markerIdToController[markerId]; + final MarkerController? markerController = + _markerIdToController[markerId]; markerController?.showInfoWindow(); } @@ -154,7 +160,8 @@ class MarkersController extends GeometryController { /// /// See also [showMarkerInfoWindow] and [isInfoWindowShown]. void hideMarkerInfoWindow(MarkerId markerId) { - final MarkerController? markerController = _markerIdToController[markerId]; + final MarkerController? markerController = + _markerIdToController[markerId]; markerController?.hideInfoWindow(); } @@ -162,7 +169,8 @@ class MarkersController extends GeometryController { /// /// See also [showMarkerInfoWindow] and [hideMarkerInfoWindow]. bool isInfoWindowShown(MarkerId markerId) { - final MarkerController? markerController = _markerIdToController[markerId]; + final MarkerController? markerController = + _markerIdToController[markerId]; return markerController?.infoWindowShown ?? false; } @@ -200,11 +208,110 @@ class MarkersController extends GeometryController { void _hideAllMarkerInfoWindow() { _markerIdToController.values .where( - (MarkerController? controller) => + (MarkerController? controller) => controller?.infoWindowShown ?? false, ) - .forEach((MarkerController controller) { + .forEach((MarkerController controller) { controller.hideInfoWindow(); }); } } + +/// A [MarkersController] for the legacy [gmaps.Marker] class. +class LegacyMarkersController + extends MarkersController { + /// Initialize the markers controller for the legacy [gmaps.Marker] class. + LegacyMarkersController({ + required super.stream, + required super.clusterManagersController, + }); + + @override + Future createMarkerController( + Marker marker, + gmaps.MarkerOptions markerOptions, + gmaps.InfoWindow? gmInfoWindow, + ) async { + final gmMarker = gmaps.Marker(markerOptions); + gmMarker.set('markerId', marker.markerId.value.toJS); + + if (marker.clusterManagerId != null) { + _clusterManagersController.addItem(marker.clusterManagerId!, gmMarker); + } else { + gmMarker.map = googleMap; + } + + return LegacyMarkerController( + marker: gmMarker, + clusterManagerId: marker.clusterManagerId, + infoWindow: gmInfoWindow, + consumeTapEvents: marker.consumeTapEvents, + onTap: () { + showMarkerInfoWindow(marker.markerId); + _onMarkerTap(marker.markerId); + }, + onDragStart: (gmaps.LatLng latLng) { + _onMarkerDragStart(marker.markerId, latLng); + }, + onDrag: (gmaps.LatLng latLng) { + _onMarkerDrag(marker.markerId, latLng); + }, + onDragEnd: (gmaps.LatLng latLng) { + _onMarkerDragEnd(marker.markerId, latLng); + }, + ); + } +} + +/// A [MarkersController] for the advanced [gmaps.AdvancedMarkerElement] class. +class AdvancedMarkersController + extends + MarkersController< + gmaps.AdvancedMarkerElement, + gmaps.AdvancedMarkerElementOptions + > { + /// Initialize the markers controller for advanced markers + /// ([gmaps.AdvancedMarkerElement]). + AdvancedMarkersController({ + required super.stream, + required super.clusterManagersController, + }); + + @override + Future createMarkerController( + Marker marker, + gmaps.AdvancedMarkerElementOptions markerOptions, + gmaps.InfoWindow? gmInfoWindow, + ) async { + assert(marker is AdvancedMarker, 'Marker must be an AdvancedMarker.'); + + final gmMarker = gmaps.AdvancedMarkerElement(markerOptions); + gmMarker.setAttribute('id', marker.markerId.value); + + if (marker.clusterManagerId != null) { + _clusterManagersController.addItem(marker.clusterManagerId!, gmMarker); + } else { + gmMarker.map = googleMap; + } + + return AdvancedMarkerController( + marker: gmMarker, + clusterManagerId: marker.clusterManagerId, + infoWindow: gmInfoWindow, + consumeTapEvents: marker.consumeTapEvents, + onTap: () { + showMarkerInfoWindow(marker.markerId); + _onMarkerTap(marker.markerId); + }, + onDragStart: (gmaps.LatLng latLng) { + _onMarkerDragStart(marker.markerId, latLng); + }, + onDrag: (gmaps.LatLng latLng) { + _onMarkerDrag(marker.markerId, latLng); + }, + onDragEnd: (gmaps.LatLng latLng) { + _onMarkerDragEnd(marker.markerId, latLng); + }, + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/overlay.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/overlay.dart index 650562a3097..c8054cd4911 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/overlay.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/overlay.dart @@ -31,16 +31,16 @@ class TileOverlayController { } /// Renders a Tile for gmaps; delegating to the configured [TileProvider]. - HTMLElement? _getTile( + web.HTMLElement? _getTile( gmaps.Point? tileCoord, num? zoom, - Document? ownerDocument, + web.Document? ownerDocument, ) { if (_tileOverlay.tileProvider == null) { return null; } - final img = ownerDocument!.createElement('img') as HTMLImageElement; + final img = ownerDocument!.createElement('img') as web.HTMLImageElement; img.width = img.height = logicalTileSize; img.hidden = true.toJS; img.setAttribute('decoding', 'async'); @@ -52,13 +52,13 @@ class TileOverlayController { return; } // Using img lets us take advantage of native decoding. - final String src = URL.createObjectURL( - Blob([tile.data!.toJS].toJS) as JSObject, + final String src = web.URL.createObjectURL( + web.Blob([tile.data!.toJS].toJS) as JSObject, ); img.src = src; img.onload = (JSAny? _) { img.hidden = false.toJS; - URL.revokeObjectURL(src); + web.URL.revokeObjectURL(src); }.toJS; }); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index f87b3ea66e8..e62af433d3b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 0.5.14+3 +version: 0.6.0 environment: sdk: ^3.9.0 @@ -26,7 +26,7 @@ dependencies: google_maps_flutter_platform_interface: ^2.14.0 sanitize_html: ^2.0.0 stream_transform: ^2.0.0 - web: ">=0.5.1 <2.0.0" + web: ^1.0.0 dev_dependencies: flutter_test: