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 9bf48f6ac87..bc2b55cb7e1 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.0 + +* Adds Advanced markers support. + ## 0.5.14+2 * Fixes a bug where using `cloudMapId` for cloud-based styling would fail if the `style` property was also present. 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..2dd51dcaf8d 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 +``` + To request multiple libraries, separate them with commas: ```html ``` 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..f0697dfe13e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/advanced_marker_test.dart @@ -0,0 +1,223 @@ +// 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 AdvancedMarkerController controller = + AdvancedMarkerController(marker: marker); + final gmaps.AdvancedMarkerElementOptions 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 AdvancedMarkerController controller = + AdvancedMarkerController(marker: marker); + + controller.showInfoWindow(); + + expect(controller.infoWindowShown, isFalse); + }); + + testWidgets('showInfoWindow', (WidgetTester tester) async { + final gmaps.InfoWindow infoWindow = gmaps.InfoWindow(); + final gmaps.Map map = gmaps.Map(createDivElement()); + marker.map = map; + final AdvancedMarkerController controller = AdvancedMarkerController( + marker: marker, + infoWindow: infoWindow, + ); + + controller.showInfoWindow(); + + expect(infoWindow.get('map'), map); + expect(controller.infoWindowShown, isTrue); + }); + + testWidgets('hideInfoWindow', (WidgetTester tester) async { + final gmaps.InfoWindow infoWindow = gmaps.InfoWindow(); + final gmaps.Map map = gmaps.Map(createDivElement()); + marker.map = map; + final AdvancedMarkerController controller = AdvancedMarkerController( + marker: marker, + infoWindow: infoWindow, + ); + + controller.hideInfoWindow(); + + expect(infoWindow.get('map'), isNull); + expect(controller.infoWindowShown, isFalse); + }); + + group('remove', () { + late AdvancedMarkerController controller; + + setUp(() { + final gmaps.InfoWindow infoWindow = gmaps.InfoWindow(); + final gmaps.Map 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 gmaps.AdvancedMarkerElementOptions 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(); + }, throwsAssertionError); + }); + + 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..321de172a13 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/advanced_markers_test.dart @@ -0,0 +1,619 @@ +// 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 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 Set 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 Set 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 Set 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 Set 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 Set 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 Set 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 Set 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 Set 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 Set 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 Set 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 HTMLImageElement? 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 Set 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 HTMLImageElement? 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 Set 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 HTMLImageElement? 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 Set 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 HTMLImageElement? 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 Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: BytesMapBitmap( + bytes, + imagePixelRatio: tester.view.devicePixelRatio, + ), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final HTMLImageElement? 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 Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: BytesMapBitmap( + bytes, + imagePixelRatio: 1, + ), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final HTMLImageElement? 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 Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: BytesMapBitmap( + bytes, + width: 20, + height: 30, + ), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final HTMLImageElement? 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 pin config and colored glyph work', + (WidgetTester widgetTester) async { + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: BitmapDescriptor.pinConfig( + backgroundColor: const Color(0xFF00FF00), + borderColor: const Color(0xFFFF0000), + glyph: const CircleGlyph(color: Color(0xFFFFFFFF)), + ), + ), + }; + await controller.addMarkers(markers); + expect(controller.markers.length, 1); + + final HTMLDivElement? icon = controller + .markers[const MarkerId('1')]?.marker?.content as HTMLDivElement?; + expect(icon, isNotNull); + + // Query nodes and check colors. This is a bit fragile as it depends on + // the implementation details of the icon which is not part of the public + // API. + final NodeList backgroundNodes = + icon!.querySelectorAll("[class*='maps-pin-view-background']"); + final NodeList borderNodes = + icon.querySelectorAll("[class*='maps-pin-view-border']"); + final NodeList glyphNodes = + icon.querySelectorAll("[class*='maps-pin-view-default-glyph']"); + + expect(backgroundNodes.length, 1); + expect(borderNodes.length, 1); + expect(glyphNodes.length, 1); + + expect( + (backgroundNodes.item(0)! as dom.Element) + .getAttribute('fill') + ?.toUpperCase(), + '#00FF00', + ); + expect( + (borderNodes.item(0)! as dom.Element) + .getAttribute('fill') + ?.toUpperCase(), + '#FF0000', + ); + expect( + (glyphNodes.item(0)! as dom.Element) + .getAttribute('fill') + ?.toUpperCase(), + '#FFFFFF', + ); + }); + + testWidgets('markers created with text glyph work', + (WidgetTester widgetTester) async { + final Set 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 HTMLDivElement? 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 (int i = 0; i < paragraphs.length; i++) { + final dom.Element? paragraph = paragraphs.item(i) as dom.Element?; + if (paragraph?.innerHTML.toString() == 'Hey') { + paragraphElement = paragraph; + break; + } + } + + expect(paragraphElement, isNotNull); + expect(paragraphElement!.innerHTML.toString(), 'Hey'); + + expect( + paragraphElement.getAttribute('style')?.toLowerCase(), + contains('color: #0000ff'), + ); + }); + + testWidgets('markers created with bitmap glyph work', + (WidgetTester widgetTester) async { + final Set 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 HTMLDivElement? 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 (int i = 0; i < imgElements.length; i++) { + final dom.Element? 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')); + expect( + imgElement.getAttribute('style')?.toLowerCase(), + contains('width: 12.0px; height: 12.0px;'), + ); + }); + + testWidgets('InfoWindow snippet can have links', + (WidgetTester tester) async { + final Set 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 HTMLElement? 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 Set 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 HTMLElement? 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 ac5a78ab0e9..fad415e36b5 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( @@ -488,13 +488,12 @@ void main() { testWidgets('translates initial options', (WidgetTester tester) async { gmaps.MapOptions? capturedOptions; controller = createController( - mapConfiguration: const MapConfiguration( - mapType: MapType.satellite, - zoomControlsEnabled: true, - cloudMapId: _kCloudMapId, - fortyFiveDegreeImageryEnabled: false, - ), - ); + mapConfiguration: const MapConfiguration( + mapType: MapType.satellite, + zoomControlsEnabled: true, + mapId: _kMapId, + fortyFiveDegreeImageryEnabled: false, + )); controller.debugSetOverrides( createMap: (_, gmaps.MapOptions options) { capturedOptions = options; @@ -507,12 +506,10 @@ void main() { expect(capturedOptions, isNotNull); expect(capturedOptions!.mapTypeId, gmaps.MapTypeId.SATELLITE); expect(capturedOptions!.zoomControl, true); - expect(capturedOptions!.mapId, _kCloudMapId); - expect( - capturedOptions!.gestureHandling, - 'auto', - reason: 'by default the map handles zoom/pan gestures internally', - ); + expect(capturedOptions!.mapId, _kMapId); + expect(capturedOptions!.gestureHandling, 'auto', + reason: + 'by default the map handles zoom/pan gestures internally'); expect(capturedOptions!.rotateControl, false); expect(capturedOptions!.tilt, 0); }); 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..2b4f9e10f3d 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.5 from annotations // in google_maps_flutter_web_integration_tests/integration_test/google_maps_controller_test.dart. // Do not manually edit this file. @@ -21,11 +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 +class _FakeMarkerController_0 extends _i1.SmartFake + implements _i2.MarkerController { + _FakeMarkerController_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + /// A class which mocks [CirclesController]. /// /// See the documentation for Mockito's code generation for more information. @@ -50,9 +57,9 @@ class MockCirclesController extends _i1.Mock implements _i2.CirclesController { @override set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null, - ); + Invocation.setter(#googleMap, _googleMap), + returnValueForMissingStub: null, + ); @override int get mapId => @@ -65,21 +72,21 @@ class MockCirclesController extends _i1.Mock implements _i2.CirclesController { @override set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter(#mapId, _mapId), - returnValueForMissingStub: null, - ); + Invocation.setter(#mapId, _mapId), + returnValueForMissingStub: null, + ); @override void addCircles(Set<_i3.Circle>? circlesToAdd) => super.noSuchMethod( - Invocation.method(#addCircles, [circlesToAdd]), - returnValueForMissingStub: null, - ); + Invocation.method(#addCircles, [circlesToAdd]), + returnValueForMissingStub: null, + ); @override void changeCircles(Set<_i3.Circle>? circlesToChange) => super.noSuchMethod( - Invocation.method(#changeCircles, [circlesToChange]), - returnValueForMissingStub: null, - ); + Invocation.method(#changeCircles, [circlesToChange]), + returnValueForMissingStub: null, + ); @override void removeCircles(Set<_i3.CircleId>? circleIdsToRemove) => @@ -90,9 +97,9 @@ class MockCirclesController extends _i1.Mock implements _i2.CirclesController { @override void bindToMap(int? mapId, _i4.Map? googleMap) => super.noSuchMethod( - Invocation.method(#bindToMap, [mapId, googleMap]), - returnValueForMissingStub: null, - ); + Invocation.method(#bindToMap, [mapId, googleMap]), + returnValueForMissingStub: null, + ); } /// A class which mocks [HeatmapsController]. @@ -120,9 +127,9 @@ class MockHeatmapsController extends _i1.Mock @override set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null, - ); + Invocation.setter(#googleMap, _googleMap), + returnValueForMissingStub: null, + ); @override int get mapId => @@ -135,21 +142,21 @@ class MockHeatmapsController extends _i1.Mock @override set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter(#mapId, _mapId), - returnValueForMissingStub: null, - ); + Invocation.setter(#mapId, _mapId), + returnValueForMissingStub: null, + ); @override void addHeatmaps(Set<_i3.Heatmap>? heatmapsToAdd) => super.noSuchMethod( - Invocation.method(#addHeatmaps, [heatmapsToAdd]), - returnValueForMissingStub: null, - ); + Invocation.method(#addHeatmaps, [heatmapsToAdd]), + returnValueForMissingStub: null, + ); @override void changeHeatmaps(Set<_i3.Heatmap>? heatmapsToChange) => super.noSuchMethod( - Invocation.method(#changeHeatmaps, [heatmapsToChange]), - returnValueForMissingStub: null, - ); + Invocation.method(#changeHeatmaps, [heatmapsToChange]), + returnValueForMissingStub: null, + ); @override void removeHeatmaps(Set<_i3.HeatmapId>? heatmapIdsToRemove) => @@ -160,9 +167,9 @@ class MockHeatmapsController extends _i1.Mock @override void bindToMap(int? mapId, _i4.Map? googleMap) => super.noSuchMethod( - Invocation.method(#bindToMap, [mapId, googleMap]), - returnValueForMissingStub: null, - ); + Invocation.method(#bindToMap, [mapId, googleMap]), + returnValueForMissingStub: null, + ); } /// A class which mocks [PolygonsController]. @@ -190,9 +197,9 @@ class MockPolygonsController extends _i1.Mock @override set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null, - ); + Invocation.setter(#googleMap, _googleMap), + returnValueForMissingStub: null, + ); @override int get mapId => @@ -205,21 +212,21 @@ class MockPolygonsController extends _i1.Mock @override set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter(#mapId, _mapId), - returnValueForMissingStub: null, - ); + Invocation.setter(#mapId, _mapId), + returnValueForMissingStub: null, + ); @override void addPolygons(Set<_i3.Polygon>? polygonsToAdd) => super.noSuchMethod( - Invocation.method(#addPolygons, [polygonsToAdd]), - returnValueForMissingStub: null, - ); + Invocation.method(#addPolygons, [polygonsToAdd]), + returnValueForMissingStub: null, + ); @override void changePolygons(Set<_i3.Polygon>? polygonsToChange) => super.noSuchMethod( - Invocation.method(#changePolygons, [polygonsToChange]), - returnValueForMissingStub: null, - ); + Invocation.method(#changePolygons, [polygonsToChange]), + returnValueForMissingStub: null, + ); @override void removePolygons(Set<_i3.PolygonId>? polygonIdsToRemove) => @@ -230,9 +237,9 @@ class MockPolygonsController extends _i1.Mock @override void bindToMap(int? mapId, _i4.Map? googleMap) => super.noSuchMethod( - Invocation.method(#bindToMap, [mapId, googleMap]), - returnValueForMissingStub: null, - ); + Invocation.method(#bindToMap, [mapId, googleMap]), + returnValueForMissingStub: null, + ); } /// A class which mocks [PolylinesController]. @@ -261,9 +268,9 @@ class MockPolylinesController extends _i1.Mock @override set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null, - ); + Invocation.setter(#googleMap, _googleMap), + returnValueForMissingStub: null, + ); @override int get mapId => @@ -276,15 +283,15 @@ class MockPolylinesController extends _i1.Mock @override set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter(#mapId, _mapId), - returnValueForMissingStub: null, - ); + Invocation.setter(#mapId, _mapId), + returnValueForMissingStub: null, + ); @override void addPolylines(Set<_i3.Polyline>? polylinesToAdd) => super.noSuchMethod( - Invocation.method(#addPolylines, [polylinesToAdd]), - returnValueForMissingStub: null, - ); + Invocation.method(#addPolylines, [polylinesToAdd]), + returnValueForMissingStub: null, + ); @override void changePolylines(Set<_i3.Polyline>? polylinesToChange) => @@ -302,23 +309,24 @@ class MockPolylinesController extends _i1.Mock @override void bindToMap(int? mapId, _i4.Map? googleMap) => super.noSuchMethod( - Invocation.method(#bindToMap, [mapId, googleMap]), - returnValueForMissingStub: null, - ); + Invocation.method(#bindToMap, [mapId, googleMap]), + returnValueForMissingStub: null, + ); } /// 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>{}, - ) - as Map<_i3.MarkerId, _i2.MarkerController>); + Invocation.getter(#markers), + returnValue: <_i3.MarkerId, _i2.MarkerController>{}, + returnValueForMissingStub: <_i3.MarkerId, + _i2.MarkerController>{}, + ) as Map<_i3.MarkerId, _i2.MarkerController>); @override _i4.Map get googleMap => @@ -331,9 +339,9 @@ class MockMarkersController extends _i1.Mock implements _i2.MarkersController { @override set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null, - ); + Invocation.setter(#googleMap, _googleMap), + returnValueForMissingStub: null, + ); @override int get mapId => @@ -346,27 +354,60 @@ class MockMarkersController extends _i1.Mock implements _i2.MarkersController { @override set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter(#mapId, _mapId), - returnValueForMissingStub: null, - ); + Invocation.setter(#mapId, _mapId), + returnValueForMissingStub: null, + ); @override _i6.Future addMarkers(Set<_i3.Marker>? markersToAdd) => (super.noSuchMethod( - Invocation.method(#addMarkers, [markersToAdd]), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) - as _i6.Future); + Invocation.method(#addMarkers, [markersToAdd]), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) 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( - Invocation.method(#changeMarkers, [markersToChange]), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) - as _i6.Future); + Invocation.method(#changeMarkers, [markersToChange]), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override void removeMarkers(Set<_i3.MarkerId>? markerIdsToRemove) => @@ -377,30 +418,28 @@ class MockMarkersController extends _i1.Mock implements _i2.MarkersController { @override void showMarkerInfoWindow(_i3.MarkerId? markerId) => super.noSuchMethod( - Invocation.method(#showMarkerInfoWindow, [markerId]), - returnValueForMissingStub: null, - ); + Invocation.method(#showMarkerInfoWindow, [markerId]), + returnValueForMissingStub: null, + ); @override void hideMarkerInfoWindow(_i3.MarkerId? markerId) => super.noSuchMethod( - Invocation.method(#hideMarkerInfoWindow, [markerId]), - returnValueForMissingStub: null, - ); + Invocation.method(#hideMarkerInfoWindow, [markerId]), + returnValueForMissingStub: null, + ); @override - bool isInfoWindowShown(_i3.MarkerId? markerId) => - (super.noSuchMethod( - Invocation.method(#isInfoWindowShown, [markerId]), - returnValue: false, - returnValueForMissingStub: false, - ) - as bool); + bool isInfoWindowShown(_i3.MarkerId? markerId) => (super.noSuchMethod( + Invocation.method(#isInfoWindowShown, [markerId]), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); @override void bindToMap(int? mapId, _i4.Map? googleMap) => super.noSuchMethod( - Invocation.method(#bindToMap, [mapId, googleMap]), - returnValueForMissingStub: null, - ); + Invocation.method(#bindToMap, [mapId, googleMap]), + returnValueForMissingStub: null, + ); } /// A class which mocks [TileOverlaysController]. @@ -419,9 +458,9 @@ class MockTileOverlaysController extends _i1.Mock @override set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null, - ); + Invocation.setter(#googleMap, _googleMap), + returnValueForMissingStub: null, + ); @override int get mapId => @@ -434,9 +473,9 @@ class MockTileOverlaysController extends _i1.Mock @override set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter(#mapId, _mapId), - returnValueForMissingStub: null, - ); + Invocation.setter(#mapId, _mapId), + returnValueForMissingStub: null, + ); @override void addTileOverlays(Set<_i3.TileOverlay>? tileOverlaysToAdd) => @@ -461,15 +500,15 @@ class MockTileOverlaysController extends _i1.Mock @override void clearTileCache(_i3.TileOverlayId? tileOverlayId) => super.noSuchMethod( - Invocation.method(#clearTileCache, [tileOverlayId]), - returnValueForMissingStub: null, - ); + Invocation.method(#clearTileCache, [tileOverlayId]), + returnValueForMissingStub: null, + ); @override void bindToMap(int? mapId, _i4.Map? googleMap) => super.noSuchMethod( - Invocation.method(#bindToMap, [mapId, googleMap]), - returnValueForMissingStub: null, - ); + Invocation.method(#bindToMap, [mapId, googleMap]), + returnValueForMissingStub: null, + ); } /// A class which mocks [GroundOverlaysController]. @@ -488,9 +527,9 @@ class MockGroundOverlaysController extends _i1.Mock @override set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null, - ); + Invocation.setter(#googleMap, _googleMap), + returnValueForMissingStub: null, + ); @override int get mapId => @@ -503,9 +542,9 @@ class MockGroundOverlaysController extends _i1.Mock @override set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter(#mapId, _mapId), - returnValueForMissingStub: null, - ); + Invocation.setter(#mapId, _mapId), + returnValueForMissingStub: null, + ); @override void addGroundOverlays(Set<_i3.GroundOverlay>? groundOverlaysToAdd) => @@ -528,9 +567,16 @@ 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]), - returnValueForMissingStub: null, - ); + Invocation.method(#bindToMap, [mapId, googleMap]), + returnValueForMissingStub: null, + ); } 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..4016913d9a9 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.5 from annotations // in google_maps_flutter_web_integration_tests/integration_test/google_maps_plugin_test.dart. // Do not manually edit this file. @@ -20,6 +20,7 @@ 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 @@ -28,29 +29,29 @@ import 'package:mockito/mockito.dart' as _i1; class _FakeMapConfiguration_0 extends _i1.SmartFake implements _i2.MapConfiguration { _FakeMapConfiguration_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + : super(parent, parentInvocation); } class _FakeStreamController_1 extends _i1.SmartFake implements _i3.StreamController { _FakeStreamController_1(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + : super(parent, parentInvocation); } class _FakeLatLngBounds_2 extends _i1.SmartFake implements _i2.LatLngBounds { _FakeLatLngBounds_2(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + : super(parent, parentInvocation); } class _FakeScreenCoordinate_3 extends _i1.SmartFake implements _i2.ScreenCoordinate { _FakeScreenCoordinate_3(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + : super(parent, parentInvocation); } class _FakeLatLng_4 extends _i1.SmartFake implements _i2.LatLng { _FakeLatLng_4(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + : super(parent, parentInvocation); } /// A class which mocks [GoogleMapController]. @@ -121,35 +122,36 @@ 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( - Invocation.method(#debugSetOverrides, [], { - #createMap: createMap, - #setOptions: setOptions, - #markers: markers, - #circles: circles, - #heatmaps: heatmaps, - #polygons: polygons, - #polylines: polylines, - #clusterManagers: clusterManagers, - #tileOverlays: tileOverlays, - #groundOverlays: groundOverlays, - }), - returnValueForMissingStub: null, - ); + }) => + super.noSuchMethod( + Invocation.method(#debugSetOverrides, [], { + #createMap: createMap, + #setOptions: setOptions, + #markers: markers, + #circles: circles, + #heatmaps: heatmaps, + #polygons: polygons, + #polylines: polylines, + #clusterManagers: clusterManagers, + #tileOverlays: tileOverlays, + #groundOverlays: groundOverlays, + }), + returnValueForMissingStub: null, + ); @override void init() => super.noSuchMethod( - Invocation.method(#init, []), - returnValueForMissingStub: null, - ); + Invocation.method(#init, []), + returnValueForMissingStub: null, + ); @override void updateMapConfiguration(_i2.MapConfiguration? update) => @@ -160,117 +162,109 @@ class MockGoogleMapController extends _i1.Mock @override void updateStyles(List<_i5.MapTypeStyle>? styles) => super.noSuchMethod( - Invocation.method(#updateStyles, [styles]), - returnValueForMissingStub: null, - ); + Invocation.method(#updateStyles, [styles]), + returnValueForMissingStub: null, + ); @override - _i3.Future<_i2.LatLngBounds> getVisibleRegion() => - (super.noSuchMethod( + _i3.Future<_i2.LatLngBounds> getVisibleRegion() => (super.noSuchMethod( + Invocation.method(#getVisibleRegion, []), + returnValue: _i3.Future<_i2.LatLngBounds>.value( + _FakeLatLngBounds_2( + this, Invocation.method(#getVisibleRegion, []), - returnValue: _i3.Future<_i2.LatLngBounds>.value( - _FakeLatLngBounds_2( - this, - Invocation.method(#getVisibleRegion, []), - ), - ), - returnValueForMissingStub: _i3.Future<_i2.LatLngBounds>.value( - _FakeLatLngBounds_2( - this, - Invocation.method(#getVisibleRegion, []), - ), - ), - ) - as _i3.Future<_i2.LatLngBounds>); + ), + ), + returnValueForMissingStub: _i3.Future<_i2.LatLngBounds>.value( + _FakeLatLngBounds_2( + this, + Invocation.method(#getVisibleRegion, []), + ), + ), + ) as _i3.Future<_i2.LatLngBounds>); @override _i3.Future<_i2.ScreenCoordinate> getScreenCoordinate(_i2.LatLng? latLng) => (super.noSuchMethod( + Invocation.method(#getScreenCoordinate, [latLng]), + returnValue: _i3.Future<_i2.ScreenCoordinate>.value( + _FakeScreenCoordinate_3( + this, Invocation.method(#getScreenCoordinate, [latLng]), - returnValue: _i3.Future<_i2.ScreenCoordinate>.value( - _FakeScreenCoordinate_3( - this, - Invocation.method(#getScreenCoordinate, [latLng]), - ), - ), - returnValueForMissingStub: _i3.Future<_i2.ScreenCoordinate>.value( - _FakeScreenCoordinate_3( - this, - Invocation.method(#getScreenCoordinate, [latLng]), - ), - ), - ) - as _i3.Future<_i2.ScreenCoordinate>); + ), + ), + returnValueForMissingStub: _i3.Future<_i2.ScreenCoordinate>.value( + _FakeScreenCoordinate_3( + this, + Invocation.method(#getScreenCoordinate, [latLng]), + ), + ), + ) as _i3.Future<_i2.ScreenCoordinate>); @override _i3.Future<_i2.LatLng> getLatLng(_i2.ScreenCoordinate? screenCoordinate) => (super.noSuchMethod( + Invocation.method(#getLatLng, [screenCoordinate]), + returnValue: _i3.Future<_i2.LatLng>.value( + _FakeLatLng_4( + this, Invocation.method(#getLatLng, [screenCoordinate]), - returnValue: _i3.Future<_i2.LatLng>.value( - _FakeLatLng_4( - this, - Invocation.method(#getLatLng, [screenCoordinate]), - ), - ), - returnValueForMissingStub: _i3.Future<_i2.LatLng>.value( - _FakeLatLng_4( - this, - Invocation.method(#getLatLng, [screenCoordinate]), - ), - ), - ) - as _i3.Future<_i2.LatLng>); + ), + ), + returnValueForMissingStub: _i3.Future<_i2.LatLng>.value( + _FakeLatLng_4( + this, + Invocation.method(#getLatLng, [screenCoordinate]), + ), + ), + ) as _i3.Future<_i2.LatLng>); @override _i3.Future moveCamera(_i2.CameraUpdate? cameraUpdate) => (super.noSuchMethod( - Invocation.method(#moveCamera, [cameraUpdate]), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) - as _i3.Future); + Invocation.method(#moveCamera, [cameraUpdate]), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override - _i3.Future getZoomLevel() => - (super.noSuchMethod( - Invocation.method(#getZoomLevel, []), - returnValue: _i3.Future.value(0.0), - returnValueForMissingStub: _i3.Future.value(0.0), - ) - as _i3.Future); + _i3.Future getZoomLevel() => (super.noSuchMethod( + Invocation.method(#getZoomLevel, []), + returnValue: _i3.Future.value(0.0), + returnValueForMissingStub: _i3.Future.value(0.0), + ) as _i3.Future); @override void updateCircles(_i2.CircleUpdates? updates) => super.noSuchMethod( - Invocation.method(#updateCircles, [updates]), - returnValueForMissingStub: null, - ); + Invocation.method(#updateCircles, [updates]), + returnValueForMissingStub: null, + ); @override void updateHeatmaps(_i2.HeatmapUpdates? updates) => super.noSuchMethod( - Invocation.method(#updateHeatmaps, [updates]), - returnValueForMissingStub: null, - ); + Invocation.method(#updateHeatmaps, [updates]), + returnValueForMissingStub: null, + ); @override void updatePolygons(_i2.PolygonUpdates? updates) => super.noSuchMethod( - Invocation.method(#updatePolygons, [updates]), - returnValueForMissingStub: null, - ); + Invocation.method(#updatePolygons, [updates]), + returnValueForMissingStub: null, + ); @override void updatePolylines(_i2.PolylineUpdates? updates) => super.noSuchMethod( - Invocation.method(#updatePolylines, [updates]), - returnValueForMissingStub: null, - ); + Invocation.method(#updatePolylines, [updates]), + returnValueForMissingStub: null, + ); @override _i3.Future updateMarkers(_i2.MarkerUpdates? updates) => (super.noSuchMethod( - Invocation.method(#updateMarkers, [updates]), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) - as _i3.Future); + Invocation.method(#updateMarkers, [updates]), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override void updateClusterManagers(_i2.ClusterManagerUpdates? updates) => @@ -295,34 +289,39 @@ class MockGoogleMapController extends _i1.Mock @override void clearTileCache(_i2.TileOverlayId? id) => super.noSuchMethod( - Invocation.method(#clearTileCache, [id]), - returnValueForMissingStub: null, - ); + Invocation.method(#clearTileCache, [id]), + returnValueForMissingStub: null, + ); @override void showInfoWindow(_i2.MarkerId? markerId) => super.noSuchMethod( - Invocation.method(#showInfoWindow, [markerId]), - returnValueForMissingStub: null, - ); + Invocation.method(#showInfoWindow, [markerId]), + returnValueForMissingStub: null, + ); @override void hideInfoWindow(_i2.MarkerId? markerId) => super.noSuchMethod( - Invocation.method(#hideInfoWindow, [markerId]), - returnValueForMissingStub: null, - ); + Invocation.method(#hideInfoWindow, [markerId]), + returnValueForMissingStub: null, + ); @override - bool isInfoWindowShown(_i2.MarkerId? markerId) => - (super.noSuchMethod( - Invocation.method(#isInfoWindowShown, [markerId]), - returnValue: false, - returnValueForMissingStub: false, - ) - as bool); + bool isInfoWindowShown(_i2.MarkerId? markerId) => (super.noSuchMethod( + Invocation.method(#isInfoWindowShown, [markerId]), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool isAdvancedMarkersAvailable() => (super.noSuchMethod( + Invocation.method(#isAdvancedMarkersAvailable, []), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); @override void dispose() => super.noSuchMethod( - Invocation.method(#dispose, []), - returnValueForMissingStub: null, - ); + Invocation.method(#dispose, []), + returnValueForMissingStub: null, + ); } 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 0fb3e61b81d..57cc5fe010d 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 @@ -114,7 +114,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 ce86f70ac32..58b405a4e59 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,11 +102,11 @@ void main() { }); testWidgets('update', (WidgetTester tester) async { - final MarkerController controller = MarkerController(marker: marker); - final gmaps.MarkerOptions options = - gmaps.MarkerOptions() - ..draggable = true - ..position = gmaps.LatLng(42, 54); + final LegacyMarkerController controller = + LegacyMarkerController(marker: marker); + final gmaps.MarkerOptions options = gmaps.MarkerOptions() + ..draggable = true + ..position = gmaps.LatLng(42, 54); expect(marker.isDraggableDefined(), isFalse); @@ -117,10 +117,10 @@ void main() { expect(marker.position?.lng, equals(54)); }); - testWidgets('infoWindow null, showInfoWindow.', ( - WidgetTester tester, - ) async { - final MarkerController controller = MarkerController(marker: marker); + testWidgets('infoWindow null, showInfoWindow.', + (WidgetTester tester) async { + final LegacyMarkerController controller = + LegacyMarkerController(marker: marker); controller.showInfoWindow(); @@ -131,7 +131,7 @@ void main() { final gmaps.InfoWindow infoWindow = gmaps.InfoWindow(); final gmaps.Map map = gmaps.Map(createDivElement()); marker.set('map', map); - final MarkerController controller = MarkerController( + final LegacyMarkerController controller = LegacyMarkerController( marker: marker, infoWindow: infoWindow, ); @@ -146,7 +146,7 @@ void main() { final gmaps.InfoWindow infoWindow = gmaps.InfoWindow(); final gmaps.Map map = gmaps.Map(createDivElement()); marker.set('map', map); - final MarkerController controller = MarkerController( + final LegacyMarkerController controller = LegacyMarkerController( marker: marker, infoWindow: infoWindow, ); @@ -158,13 +158,14 @@ void main() { }); group('remove', () { - late MarkerController controller; + late LegacyMarkerController controller; setUp(() { final gmaps.InfoWindow infoWindow = gmaps.InfoWindow(); final gmaps.Map 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 { 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 b8f60ee24cd..d97f93c2ff6 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,18 +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( - stream: events, - clusterManagersController: clusterManagersController, - ); + clusterManagersController = + ClusterManagersController(stream: events); + controller = LegacyMarkersController( + stream: events, clusterManagersController: clusterManagersController); map = gmaps.Map(createDivElement()); clusterManagersController.bindToMap(123, map); controller.bindToMap(123, map); 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..7d00b267d8b 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.5 from annotations // in google_maps_flutter_web_integration_tests/integration_test/overlays_test.dart. // Do not manually edit this file. @@ -17,6 +17,7 @@ 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 @@ -24,7 +25,7 @@ import 'package:mockito/mockito.dart' as _i1; class _FakeTile_0 extends _i1.SmartFake implements _i2.Tile { _FakeTile_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + : super(parent, parentInvocation); } /// A class which mocks [TileProvider]. @@ -34,13 +35,12 @@ class MockTileProvider extends _i1.Mock implements _i2.TileProvider { @override _i3.Future<_i2.Tile> getTile(int? x, int? y, int? zoom) => (super.noSuchMethod( - Invocation.method(#getTile, [x, y, zoom]), - returnValue: _i3.Future<_i2.Tile>.value( - _FakeTile_0(this, Invocation.method(#getTile, [x, y, zoom])), - ), - returnValueForMissingStub: _i3.Future<_i2.Tile>.value( - _FakeTile_0(this, Invocation.method(#getTile, [x, y, zoom])), - ), - ) - as _i3.Future<_i2.Tile>); + Invocation.method(#getTile, [x, y, zoom]), + returnValue: _i3.Future<_i2.Tile>.value( + _FakeTile_0(this, Invocation.method(#getTile, [x, y, zoom])), + ), + returnValueForMissingStub: _i3.Future<_i2.Tile>.value( + _FakeTile_0(this, Invocation.method(#getTile, [x, y, zoom])), + ), + ) as _i3.Future<_i2.Tile>); } 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 f4ca29a478d..7381e3130f6 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. @@ -126,7 +126,7 @@ gmaps.MapOptions _configurationAndStyleToGmapsOptions( options.styles = styles; } - options.mapId = configuration.cloudMapId; + options.mapId = configuration.mapId; return options; } @@ -226,7 +226,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), @@ -267,27 +267,27 @@ 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 = + final web.HTMLElement container = createDivElement() ..id = 'gmaps-marker-${marker.markerId.value}-infowindow'; if (markerTitle.isNotEmpty) { - final HTMLHeadingElement title = - (document.createElement('h3') as HTMLHeadingElement) + final web.HTMLHeadingElement title = + (web.document.createElement('h3') as web.HTMLHeadingElement) ..className = 'infowindow-title' ..innerText = markerTitle; container.appendChild(title); } if (markerSnippet.isNotEmpty) { - final HTMLElement snippet = + 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; @@ -327,18 +327,32 @@ 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 gmaps.Size 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.Element icon, + required gmaps.Size? size, + required double? opacity, + required bool? isVisible, +}) { + icon.setAttribute( + 'style', + [ + if (size != null) ...[ + 'width: ${size.width.toStringAsFixed(1)}px;', + 'height: ${size.height.toStringAsFixed(1)}px;', + ], + if (opacity != null) 'opacity: $opacity;', + if (isVisible != null) 'visibility: ${isVisible ? 'visible' : 'hidden'};', + ].join(' '), + ); } void _setIconAnchor({ - required Size size, + required gmaps.Size size, required Offset anchor, required gmaps.Icon icon, }) { @@ -349,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 gmaps.Size 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 @@ -356,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, @@ -392,7 +413,7 @@ Future _getBitmapSize(MapBitmap mapBitmap, String url) async { } // Return the calculated size. - return Size(targetWidth, targetHeight); + return gmaps.Size(targetWidth, targetHeight); } } @@ -400,10 +421,10 @@ 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 HTMLImageElement image = HTMLImageElement()..src = url; + final web.HTMLImageElement 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. @@ -424,6 +445,148 @@ void _cleanUpBitmapConversionCaches() { _bitmapBlobUrlCache.clear(); } +/// 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) { + final gmaps.PinElementOptions options = + gmaps.PinElementOptions() + ..background = + bitmapDescriptor.backgroundColor != null + ? _getCssColor(bitmapDescriptor.backgroundColor!) + : null + ..borderColor = + bitmapDescriptor.borderColor != null + ? _getCssColor(bitmapDescriptor.borderColor!) + : null; + + final AdvancedMarkerGlyph? glyph = bitmapDescriptor.glyph; + switch (glyph) { + case final CircleGlyph circleGlyph: + options.glyphColor = _getCssColor(circleGlyph.color); + case final TextGlyph textGlyph: + final web.Element element = web.document.createElement('p'); + element.innerHTML = textGlyph.text.toJS; + if (textGlyph.textColor != null) { + element.setAttribute( + '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 gmaps.PinElement pinElement = gmaps.PinElement(options); + final web.HTMLElement htmlElement = pinElement.element; + htmlElement.style + ..visibility = isVisible ? 'visible' : 'hidden' + ..opacity = opacity?.toString() ?? '1.0' + ..transform = rotation != null ? 'rotate(${rotation}deg)' : ''; + return htmlElement; + } + + if (bitmapDescriptor is MapBitmap) { + final String url = switch (bitmapDescriptor) { + (final BytesMapBitmap bytesMapBitmap) => _bitmapBlobUrlCache.putIfAbsent( + bytesMapBitmap.byteData.hashCode, + () { + final web.Blob 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 web.Element icon = web.document.createElement('img') + ..setAttribute('src', url); + + final gmaps.Size? size = switch (bitmapDescriptor.bitmapScaling) { + MapBitmapScaling.auto => await _getBitmapSize(bitmapDescriptor, url), + MapBitmapScaling.none => null, + }; + _setIconStyle( + icon: icon, + size: size, + opacity: opacity, + isVisible: isVisible, + ); + + return icon; + } + + // The following code is for the deprecated BitmapDescriptor.fromBytes + // and BitmapDescriptor.fromAssetImage. + final List iconConfig = bitmapDescriptor.toJson() as List; + if (iconConfig[0] == 'fromAssetImage') { + assert(iconConfig.length >= 2); + // iconConfig[2] contains the DPIs of the screen, but that information is + // already encoded in the iconConfig[1] + final web.Element icon = web.document.createElement('img')..setAttribute( + '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; + } else if (iconConfig[0] == 'fromBytes') { + // Grab the bytes, and put them into a blob. + final List 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 web.Element icon = web.document.createElement('img') + ..setAttribute('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; + } + + return null; +} + // Converts a [BitmapDescriptor] into a [gmaps.Icon] that can be used in Markers. Future _gmIconFromBitmapDescriptor( BitmapDescriptor bitmapDescriptor, @@ -432,13 +595,27 @@ 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, + () { + final web.Blob 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); @@ -446,6 +623,7 @@ Future _gmIconFromBitmapDescriptor( case MapBitmapScaling.none: break; } + return icon; } @@ -470,7 +648,7 @@ Future _gmIconFromBitmapDescriptor( // Grab the bytes, and put them into a blob final List 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, @@ -479,9 +657,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) { @@ -493,27 +671,68 @@ 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 gmaps.AdvancedMarkerElementOptions 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 gmaps.MarkerOptions 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 JSObject object = marker! as JSObject; + if (object.isA()) { + final gmaps.MVCObject mapObject = marker as gmaps.MVCObject; + return MarkerId((mapObject.get('markerId')! as JSString).toDart); + } else if (object.isA()) { + final gmaps.AdvancedMarkerElement 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) { @@ -754,10 +973,10 @@ String urlFromMapBitmap(MapBitmap mapBitmap) { (final BytesMapBitmap bytesMapBitmap) => _bitmapBlobUrlCache.putIfAbsent( bytesMapBitmap.byteData.hashCode, () { - final Blob blob = Blob( + final web.Blob blob = web.Blob( [bytesMapBitmap.byteData.toJS].toJS, ); - return URL.createObjectURL(blob as JSObject); + return web.URL.createObjectURL(blob as JSObject); }, ), (final AssetMapBitmap assetMapBitmap) => ui_web.assetManager.getAssetUrl( @@ -861,3 +1080,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 9a1aedcf639..e2dc151d024 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,54 @@ 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, @@ -99,7 +140,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 { @@ -134,8 +175,8 @@ class GoogleMapController { HeatmapsController? _heatmapsController; PolygonsController? _polygonsController; PolylinesController? _polylinesController; - MarkersController? _markersController; - ClusterManagersController? _clusterManagersController; + MarkersController? _markersController; + ClusterManagersController? _clusterManagersController; TileOverlaysController? _tileOverlaysController; GroundOverlaysController? _groundOverlaysController; @@ -152,7 +193,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. @@ -165,12 +206,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, }) { @@ -189,7 +230,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); } @@ -468,7 +509,7 @@ class GoogleMapController { await Future.value(_googleMap!.bounds) ?? _nullGmapsLatLngBounds; - return gmLatLngBoundsTolatLngBounds(bounds); + return gmLatLngBoundsToLatLngBounds(bounds); } /// Returns the [ScreenCoordinate] for a given viewport [LatLng]. @@ -652,6 +693,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 4f0058b4aec..7fe39488584 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..c23fd2a85db 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 @@ -15,8 +15,8 @@ import 'marker_clustering.dart'; typedef ConfigurationProvider = MapConfiguration Function(int mapId); /// Function that gets the [ClusterManagersController] for a given `mapId`. -typedef ClusterManagersControllerProvider = - ClusterManagersController? Function(int mapId); +typedef ClusterManagersControllerProvider = ClusterManagersController? + Function(int mapId); /// Function that gets the [GroundOverlaysController] for a given `mapId`. typedef GroundOverlaysControllerProvider = @@ -103,16 +103,15 @@ class GoogleMapsInspectorWeb extends GoogleMapsInspectorPlatform { final JSAny? clickable = groundOverlay.get('clickable'); return GroundOverlay.fromBounds( - groundOverlayId: groundOverlayId, - image: BytesMapBitmap( - Uint8List.fromList([0]), - bitmapScaling: MapBitmapScaling.none, - ), - bounds: gmLatLngBoundsTolatLngBounds(groundOverlay.bounds), - transparency: 1.0 - groundOverlay.opacity, - visible: groundOverlay.map != null, - clickable: clickable != null && (clickable as JSBoolean).toDart, - ); + groundOverlayId: groundOverlayId, + image: BytesMapBitmap( + Uint8List.fromList([0]), + bitmapScaling: MapBitmapScaling.none, + ), + bounds: gmLatLngBoundsToLatLngBounds(groundOverlay.bounds), + transparency: 1.0 - groundOverlay.opacity, + visible: groundOverlay.map != null, + clickable: clickable != null && (clickable as JSBoolean).toDart); } @override 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..8a880c2c1cf 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,15 @@ 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. +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, @@ -16,36 +20,20 @@ class MarkerController { LatLngCallback? onDragEnd, VoidCallback? onTap, ClusterManagerId? clusterManagerId, - }) : _marker = marker, - _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); - }); - } + }) : _marker = marker, + _infoWindow = infoWindow, + _consumeTapEvents = consumeTapEvents, + _clusterManagerId = clusterManagerId { + initializeMarkerListener( + marker: marker, + onDragStart: onDragStart, + onDrag: onDrag, + onDragEnd: onDragEnd, + onTap: onTap, + ); } - gmaps.Marker? _marker; + T? _marker; final bool _consumeTapEvents; @@ -64,56 +52,221 @@ 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( - gmaps.MarkerOptions options, { - HTMLElement? newInfoWindowContent, + O options, { + web.HTMLElement? newInfoWindowContent, + }); + + /// Initializes the listener for the wrapped marker object. + void initializeMarkerListener({ + 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, + }); + + @override + void initializeMarkerListener({ + required gmaps.Marker marker, + required LatLngCallback? onDragStart, + required LatLngCallback? onDrag, + required LatLngCallback? onDragEnd, + required VoidCallback? onTap, }) { - assert(_marker != null, 'Cannot `update` Marker after calling `remove`.'); - _marker!.options = options; - if (_infoWindow != null && newInfoWindowContent != null) { - _infoWindow.content = newInfoWindowContent; + 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); + }); } } - /// Disposes of the currently wrapped [gmaps.Marker]. + @override void remove() { if (_marker != null) { _infoWindowShown = false; - _marker!.visible = false; _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.'); + @override + void showInfoWindow() { + assert(_marker != null, 'Cannot `showInfoWindow` on a `remove`d Marker.'); if (_infoWindow != null) { - _infoWindow.close(); + _infoWindow.open(_marker!.map, _marker); + _infoWindowShown = true; + } + } + + @override + void update(gmaps.MarkerOptions options, + {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, + }); + + @override + void initializeMarkerListener({ + required gmaps.AdvancedMarkerElement marker, + required LatLngCallback? onDragStart, + required LatLngCallback? onDrag, + required LatLngCallback? onDragEnd, + required VoidCallback? onTap, + }) { + 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); + }); + } + } + + @override + void remove() { + if (_marker != null) { _infoWindowShown = false; + + _marker!.remove(); + _marker!.map = null; + _marker = null; } } - /// 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 (_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 388648a307b..f2c871ae271 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,23 +17,26 @@ 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 /// emitting map events. - ClusterManagersController({ - required StreamController> stream, - }) : _streamController = stream, - _clusterManagerIdToMarkerClusterer = - {}; + ClusterManagersController( + {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 - _clusterManagerIdToMarkerClusterer; + final Map> + _clusterManagerIdToMarkerClusterer; /// Adds a set of [ClusterManager] objects to the cache. void addClusterManagers(Set clusterManagersToAdd) { @@ -41,13 +44,10 @@ class ClusterManagersController extends GeometryController { } void _addClusterManager(ClusterManager clusterManager) { - final MarkerClusterer markerClusterer = createMarkerClusterer( + final MarkerClusterer markerClusterer = createMarkerClusterer( googleMap, - ( - gmaps.MapMouseEvent event, - MarkerClustererCluster cluster, - gmaps.Map map, - ) => + (gmaps.MapMouseEvent event, MarkerClustererCluster cluster, + gmaps.Map map) => _clusterClicked(clusterManager.clusterManagerId, event, cluster, map), ); @@ -62,7 +62,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 +71,12 @@ 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,14 @@ 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,25 +103,22 @@ 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) => - _convertCluster(clusterManagerId, cluster), - ) + .map((MarkerClustererCluster cluster) => + _convertCluster(clusterManagerId, cluster)) .toList(); } return []; } void _clusterClicked( - ClusterManagerId clusterManagerId, - gmaps.MapMouseEvent event, - MarkerClustererCluster markerClustererCluster, - gmaps.Map map, - ) { + ClusterManagerId clusterManagerId, + gmaps.MapMouseEvent event, + MarkerClustererCluster markerClustererCluster, + gmaps.Map map) { if (markerClustererCluster.count > 0 && markerClustererCluster.bounds != null) { final Cluster cluster = _convertCluster( @@ -128,22 +130,14 @@ class ClusterManagersController extends GeometryController { } /// Converts [MarkerClustererCluster] to [Cluster]. - Cluster _convertCluster( - ClusterManagerId clusterManagerId, - MarkerClustererCluster markerClustererCluster, - ) { + Cluster _convertCluster(ClusterManagerId clusterManagerId, + MarkerClustererCluster markerClustererCluster) { final LatLng position = gmLatLngToLatLng(markerClustererCluster.position); - final LatLngBounds bounds = gmLatLngBoundsTolatLngBounds( - markerClustererCluster.bounds!, - ); - + final LatLngBounds bounds = + gmLatLngBoundsToLatLngBounds(markerClustererCluster.bounds!); final List markerIds = - markerClustererCluster.markers - .map( - (gmaps.Marker marker) => - MarkerId((marker.get('markerId')! as JSString).toDart), - ) - .toList(); + markerClustererCluster.markers.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 bc74bca2492..582237c302c 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,32 +13,31 @@ 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( - map: map as JSAny?, - markers: markers?.cast().toJS ?? JSArray(), - onClusterClick: - onClusterClick != null - ? ((JSAny event, MarkerClustererCluster cluster, JSAny map) => - onClusterClick( - event as gmaps.MapMouseEvent, - cluster, - map as gmaps.Map, - )) - .toJS + List? markers, + ClusterClickHandler? onClusterClick, + }) => + MarkerClustererOptions._js( + map: map as JSAny?, + markers: markers?.cast().toJS ?? JSArray(), + onClusterClick: onClusterClick != null + ? ((JSAny event, MarkerClustererCluster cluster, JSAny map) => + onClusterClick(event as gmaps.MapMouseEvent, cluster, + map as gmaps.Map)).toJS : null, ); @@ -53,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; @@ -69,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; @@ -97,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); } @@ -106,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); @@ -144,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; @@ -155,13 +153,11 @@ extension type MarkerClusterer._(JSObject _) implements JSObject { /// Creates [MarkerClusterer] object with given [gmaps.Map] and /// [ClusterClickHandler]. -MarkerClusterer createMarkerClusterer( - gmaps.Map map, - ClusterClickHandler onClusterClickHandler, -) { - final MarkerClustererOptions options = MarkerClustererOptions( +MarkerClusterer createMarkerClusterer( + gmaps.Map map, ClusterClickHandler onClusterClickHandler) { + final MarkerClustererOptions 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 a066aabba51..4a47a1464fb 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,34 @@ 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. +/// +/// [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, - }) : _streamController = stream, - _clusterManagersController = clusterManagersController, - _markerIdToController = {}; + required ClusterManagersController clusterManagersController, + }) : _streamController = stream, + _clusterManagersController = clusterManagersController, + _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 +51,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 HTMLElement content = infoWindowOptions.content! as HTMLElement; + infoWindowOptions.content is web.HTMLElement) { + final web.HTMLElement content = infoWindowOptions.content! as web.HTMLElement; content.onclick = (JSAny? _) { @@ -53,53 +61,36 @@ class MarkersController extends GeometryController { } } - final gmaps.Marker? currentMarker = - _markerIdToController[marker.markerId]?.marker; - - final gmaps.MarkerOptions markerOptions = await _markerOptionsFromMarker( - marker, - currentMarker, - ); - - final gmaps.Marker 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 MarkerController 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? markerController = + _markerIdToController[marker.markerId]; + final T? currentMarker = markerController?.marker; + final O markerOptions = + await _markerOptionsFromMarker(marker, currentMarker); + 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 = @@ -111,13 +102,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?, ); } } @@ -129,7 +122,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!, @@ -147,7 +141,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(); } @@ -155,7 +150,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(); } @@ -163,7 +159,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,12 +197,106 @@ class MarkersController extends GeometryController { void _hideAllMarkerInfoWindow() { _markerIdToController.values - .where( - (MarkerController? controller) => - controller?.infoWindowShown ?? false, - ) - .forEach((MarkerController controller) { - controller.hideInfoWindow(); - }); + .where((MarkerController? controller) => + controller?.infoWindowShown ?? false) + .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 gmaps.Marker 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 gmaps.AdvancedMarkerElement 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 9a5983e0acf..7c293f1110b 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 @@ -32,17 +32,17 @@ 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 HTMLImageElement img = - ownerDocument!.createElement('img') as HTMLImageElement; + final web.HTMLImageElement img = + ownerDocument!.createElement('img') as web.HTMLImageElement; img.width = img.height = logicalTileSize; img.hidden = true.toJS; img.setAttribute('decoding', 'async'); @@ -54,14 +54,14 @@ 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 672b6d7a069..de1f2fcd5b6 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+2 +version: 0.6.0 environment: sdk: ^3.7.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: