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 82f2fc94e86..2e0f8c666c6 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.5.13 + +* Adds Advanced markers support. + ## 0.5.12+2 * Fix broken cameraTargetBounds option on web. 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..95ee0f0deed --- /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. All rights reserved. +// 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..61cb1deff60 --- /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. All rights reserved. +// 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 0853359835e..31f2d2115a4 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( @@ -66,8 +66,9 @@ void main() { mapId: mapId, streamController: stream, widgetConfiguration: MapWidgetConfiguration( - initialCameraPosition: initialCameraPosition, - textDirection: TextDirection.ltr), + initialCameraPosition: initialCameraPosition, + textDirection: TextDirection.ltr, + ), mapObjects: mapObjects, mapConfiguration: mapConfiguration, ); @@ -434,7 +435,7 @@ void main() { mapConfiguration: const MapConfiguration( mapType: MapType.satellite, zoomControlsEnabled: true, - cloudMapId: _kCloudMapId, + mapId: _kMapId, fortyFiveDegreeImageryEnabled: false, )); controller.debugSetOverrides( @@ -448,7 +449,7 @@ void main() { expect(capturedOptions, isNotNull); expect(capturedOptions!.mapTypeId, gmaps.MapTypeId.SATELLITE); expect(capturedOptions!.zoomControl, true); - expect(capturedOptions!.mapId, _kCloudMapId); + expect(capturedOptions!.mapId, _kMapId); expect(capturedOptions!.gestureHandling, 'auto', reason: 'by default the map handles zoom/pan gestures internally'); 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 7904b7352cb..76fc44ac034 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. @@ -46,10 +53,7 @@ class MockCirclesController extends _i1.Mock implements _i2.CirclesController { @override set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter( - #googleMap, - _googleMap, - ), + Invocation.setter(#googleMap, _googleMap), returnValueForMissingStub: null, ); @@ -62,54 +66,32 @@ class MockCirclesController extends _i1.Mock implements _i2.CirclesController { @override set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter( - #mapId, - _mapId, - ), + Invocation.setter(#mapId, _mapId), returnValueForMissingStub: null, ); @override void addCircles(Set<_i3.Circle>? circlesToAdd) => super.noSuchMethod( - Invocation.method( - #addCircles, - [circlesToAdd], - ), + Invocation.method(#addCircles, [circlesToAdd]), returnValueForMissingStub: null, ); @override void changeCircles(Set<_i3.Circle>? circlesToChange) => super.noSuchMethod( - Invocation.method( - #changeCircles, - [circlesToChange], - ), + Invocation.method(#changeCircles, [circlesToChange]), returnValueForMissingStub: null, ); @override void removeCircles(Set<_i3.CircleId>? circleIdsToRemove) => super.noSuchMethod( - Invocation.method( - #removeCircles, - [circleIdsToRemove], - ), + Invocation.method(#removeCircles, [circleIdsToRemove]), returnValueForMissingStub: null, ); @override - void bindToMap( - int? mapId, - _i4.Map? googleMap, - ) => - super.noSuchMethod( - Invocation.method( - #bindToMap, - [ - mapId, - googleMap, - ], - ), + void bindToMap(int? mapId, _i4.Map? googleMap) => super.noSuchMethod( + Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null, ); } @@ -135,10 +117,7 @@ class MockHeatmapsController extends _i1.Mock @override set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter( - #googleMap, - _googleMap, - ), + Invocation.setter(#googleMap, _googleMap), returnValueForMissingStub: null, ); @@ -151,54 +130,32 @@ class MockHeatmapsController extends _i1.Mock @override set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter( - #mapId, - _mapId, - ), + Invocation.setter(#mapId, _mapId), returnValueForMissingStub: null, ); @override void addHeatmaps(Set<_i3.Heatmap>? heatmapsToAdd) => super.noSuchMethod( - Invocation.method( - #addHeatmaps, - [heatmapsToAdd], - ), + Invocation.method(#addHeatmaps, [heatmapsToAdd]), returnValueForMissingStub: null, ); @override void changeHeatmaps(Set<_i3.Heatmap>? heatmapsToChange) => super.noSuchMethod( - Invocation.method( - #changeHeatmaps, - [heatmapsToChange], - ), + Invocation.method(#changeHeatmaps, [heatmapsToChange]), returnValueForMissingStub: null, ); @override void removeHeatmaps(Set<_i3.HeatmapId>? heatmapIdsToRemove) => super.noSuchMethod( - Invocation.method( - #removeHeatmaps, - [heatmapIdsToRemove], - ), + Invocation.method(#removeHeatmaps, [heatmapIdsToRemove]), returnValueForMissingStub: null, ); @override - void bindToMap( - int? mapId, - _i4.Map? googleMap, - ) => - super.noSuchMethod( - Invocation.method( - #bindToMap, - [ - mapId, - googleMap, - ], - ), + void bindToMap(int? mapId, _i4.Map? googleMap) => super.noSuchMethod( + Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null, ); } @@ -224,10 +181,7 @@ class MockPolygonsController extends _i1.Mock @override set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter( - #googleMap, - _googleMap, - ), + Invocation.setter(#googleMap, _googleMap), returnValueForMissingStub: null, ); @@ -240,54 +194,32 @@ class MockPolygonsController extends _i1.Mock @override set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter( - #mapId, - _mapId, - ), + Invocation.setter(#mapId, _mapId), returnValueForMissingStub: null, ); @override void addPolygons(Set<_i3.Polygon>? polygonsToAdd) => super.noSuchMethod( - Invocation.method( - #addPolygons, - [polygonsToAdd], - ), + Invocation.method(#addPolygons, [polygonsToAdd]), returnValueForMissingStub: null, ); @override void changePolygons(Set<_i3.Polygon>? polygonsToChange) => super.noSuchMethod( - Invocation.method( - #changePolygons, - [polygonsToChange], - ), + Invocation.method(#changePolygons, [polygonsToChange]), returnValueForMissingStub: null, ); @override void removePolygons(Set<_i3.PolygonId>? polygonIdsToRemove) => super.noSuchMethod( - Invocation.method( - #removePolygons, - [polygonIdsToRemove], - ), + Invocation.method(#removePolygons, [polygonIdsToRemove]), returnValueForMissingStub: null, ); @override - void bindToMap( - int? mapId, - _i4.Map? googleMap, - ) => - super.noSuchMethod( - Invocation.method( - #bindToMap, - [ - mapId, - googleMap, - ], - ), + void bindToMap(int? mapId, _i4.Map? googleMap) => super.noSuchMethod( + Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null, ); } @@ -313,10 +245,7 @@ class MockPolylinesController extends _i1.Mock @override set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter( - #googleMap, - _googleMap, - ), + Invocation.setter(#googleMap, _googleMap), returnValueForMissingStub: null, ); @@ -329,55 +258,33 @@ class MockPolylinesController extends _i1.Mock @override set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter( - #mapId, - _mapId, - ), + Invocation.setter(#mapId, _mapId), returnValueForMissingStub: null, ); @override void addPolylines(Set<_i3.Polyline>? polylinesToAdd) => super.noSuchMethod( - Invocation.method( - #addPolylines, - [polylinesToAdd], - ), + Invocation.method(#addPolylines, [polylinesToAdd]), returnValueForMissingStub: null, ); @override void changePolylines(Set<_i3.Polyline>? polylinesToChange) => super.noSuchMethod( - Invocation.method( - #changePolylines, - [polylinesToChange], - ), + Invocation.method(#changePolylines, [polylinesToChange]), returnValueForMissingStub: null, ); @override void removePolylines(Set<_i3.PolylineId>? polylineIdsToRemove) => super.noSuchMethod( - Invocation.method( - #removePolylines, - [polylineIdsToRemove], - ), + Invocation.method(#removePolylines, [polylineIdsToRemove]), returnValueForMissingStub: null, ); @override - void bindToMap( - int? mapId, - _i4.Map? googleMap, - ) => - super.noSuchMethod( - Invocation.method( - #bindToMap, - [ - mapId, - googleMap, - ], - ), + void bindToMap(int? mapId, _i4.Map? googleMap) => super.noSuchMethod( + Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null, ); } @@ -385,13 +292,16 @@ class MockPolylinesController extends _i1.Mock /// A class which mocks [MarkersController]. /// /// See the documentation for Mockito's code generation for more information. -class MockMarkersController extends _i1.Mock implements _i2.MarkersController { +class MockMarkersController extends _i1.Mock + implements _i2.MarkersController { @override - Map<_i3.MarkerId, _i2.MarkerController> get markers => (super.noSuchMethod( + 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>); + returnValue: <_i3.MarkerId, _i2.MarkerController>{}, + returnValueForMissingStub: <_i3.MarkerId, + _i2.MarkerController>{}, + ) as Map<_i3.MarkerId, _i2.MarkerController>); @override _i4.Map get googleMap => (super.noSuchMethod( @@ -402,10 +312,7 @@ class MockMarkersController extends _i1.Mock implements _i2.MarkersController { @override set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter( - #googleMap, - _googleMap, - ), + Invocation.setter(#googleMap, _googleMap), returnValueForMissingStub: null, ); @@ -418,31 +325,57 @@ class MockMarkersController extends _i1.Mock implements _i2.MarkersController { @override set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter( - #mapId, - _mapId, - ), + Invocation.setter(#mapId, _mapId), returnValueForMissingStub: null, ); @override _i6.Future addMarkers(Set<_i3.Marker>? markersToAdd) => (super.noSuchMethod( - Invocation.method( - #addMarkers, - [markersToAdd], - ), + 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], - ), + Invocation.method(#changeMarkers, [markersToChange]), returnValue: _i6.Future.value(), returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); @@ -450,54 +383,32 @@ class MockMarkersController extends _i1.Mock implements _i2.MarkersController { @override void removeMarkers(Set<_i3.MarkerId>? markerIdsToRemove) => super.noSuchMethod( - Invocation.method( - #removeMarkers, - [markerIdsToRemove], - ), + Invocation.method(#removeMarkers, [markerIdsToRemove]), returnValueForMissingStub: null, ); @override void showMarkerInfoWindow(_i3.MarkerId? markerId) => super.noSuchMethod( - Invocation.method( - #showMarkerInfoWindow, - [markerId], - ), + Invocation.method(#showMarkerInfoWindow, [markerId]), returnValueForMissingStub: null, ); @override void hideMarkerInfoWindow(_i3.MarkerId? markerId) => super.noSuchMethod( - Invocation.method( - #hideMarkerInfoWindow, - [markerId], - ), + Invocation.method(#hideMarkerInfoWindow, [markerId]), returnValueForMissingStub: null, ); @override bool isInfoWindowShown(_i3.MarkerId? markerId) => (super.noSuchMethod( - Invocation.method( - #isInfoWindowShown, - [markerId], - ), + 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, - ], - ), + void bindToMap(int? mapId, _i4.Map? googleMap) => super.noSuchMethod( + Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null, ); } @@ -516,10 +427,7 @@ class MockTileOverlaysController extends _i1.Mock @override set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter( - #googleMap, - _googleMap, - ), + Invocation.setter(#googleMap, _googleMap), returnValueForMissingStub: null, ); @@ -532,65 +440,40 @@ class MockTileOverlaysController extends _i1.Mock @override set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter( - #mapId, - _mapId, - ), + Invocation.setter(#mapId, _mapId), returnValueForMissingStub: null, ); @override void addTileOverlays(Set<_i3.TileOverlay>? tileOverlaysToAdd) => super.noSuchMethod( - Invocation.method( - #addTileOverlays, - [tileOverlaysToAdd], - ), + Invocation.method(#addTileOverlays, [tileOverlaysToAdd]), returnValueForMissingStub: null, ); @override void changeTileOverlays(Set<_i3.TileOverlay>? tileOverlays) => super.noSuchMethod( - Invocation.method( - #changeTileOverlays, - [tileOverlays], - ), + Invocation.method(#changeTileOverlays, [tileOverlays]), returnValueForMissingStub: null, ); @override void removeTileOverlays(Set<_i3.TileOverlayId>? tileOverlayIds) => super.noSuchMethod( - Invocation.method( - #removeTileOverlays, - [tileOverlayIds], - ), + Invocation.method(#removeTileOverlays, [tileOverlayIds]), returnValueForMissingStub: null, ); @override void clearTileCache(_i3.TileOverlayId? tileOverlayId) => super.noSuchMethod( - Invocation.method( - #clearTileCache, - [tileOverlayId], - ), + Invocation.method(#clearTileCache, [tileOverlayId]), returnValueForMissingStub: null, ); @override - void bindToMap( - int? mapId, - _i4.Map? googleMap, - ) => - super.noSuchMethod( - Invocation.method( - #bindToMap, - [ - mapId, - googleMap, - ], - ), + void bindToMap(int? mapId, _i4.Map? googleMap) => super.noSuchMethod( + Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null, ); } @@ -609,10 +492,7 @@ class MockGroundOverlaysController extends _i1.Mock @override set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( - Invocation.setter( - #googleMap, - _googleMap, - ), + Invocation.setter(#googleMap, _googleMap), returnValueForMissingStub: null, ); @@ -625,56 +505,41 @@ class MockGroundOverlaysController extends _i1.Mock @override set mapId(int? _mapId) => super.noSuchMethod( - Invocation.setter( - #mapId, - _mapId, - ), + Invocation.setter(#mapId, _mapId), returnValueForMissingStub: null, ); @override void addGroundOverlays(Set<_i3.GroundOverlay>? groundOverlaysToAdd) => super.noSuchMethod( - Invocation.method( - #addGroundOverlays, - [groundOverlaysToAdd], - ), + Invocation.method(#addGroundOverlays, [groundOverlaysToAdd]), returnValueForMissingStub: null, ); @override void changeGroundOverlays(Set<_i3.GroundOverlay>? groundOverlays) => super.noSuchMethod( - Invocation.method( - #changeGroundOverlays, - [groundOverlays], - ), + Invocation.method(#changeGroundOverlays, [groundOverlays]), returnValueForMissingStub: null, ); @override void removeGroundOverlays(Set<_i3.GroundOverlayId>? groundOverlayIds) => super.noSuchMethod( - Invocation.method( - #removeGroundOverlays, - [groundOverlayIds], - ), + Invocation.method(#removeGroundOverlays, [groundOverlayIds]), returnValueForMissingStub: null, ); @override - void bindToMap( - int? mapId, - _i4.Map? googleMap, - ) => - super.noSuchMethod( - Invocation.method( - #bindToMap, - [ - mapId, - googleMap, - ], - ), + _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, ); } 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 54825075821..411b9c17ca7 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 @@ -27,55 +28,30 @@ 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, - ); + _FakeMapConfiguration_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); } class _FakeStreamController_1 extends _i1.SmartFake implements _i3.StreamController { - _FakeStreamController_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); + _FakeStreamController_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); } class _FakeLatLngBounds_2 extends _i1.SmartFake implements _i2.LatLngBounds { - _FakeLatLngBounds_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); + _FakeLatLngBounds_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); } class _FakeScreenCoordinate_3 extends _i1.SmartFake implements _i2.ScreenCoordinate { - _FakeScreenCoordinate_3( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); + _FakeScreenCoordinate_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); } class _FakeLatLng_4 extends _i1.SmartFake implements _i2.LatLng { - _FakeLatLng_4( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); + _FakeLatLng_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); } /// A class which mocks [GoogleMapController]. @@ -135,198 +111,146 @@ 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, - }, - ), + 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, - [], - ), + Invocation.method(#init, []), returnValueForMissingStub: null, ); @override void updateMapConfiguration(_i2.MapConfiguration? update) => super.noSuchMethod( - Invocation.method( - #updateMapConfiguration, - [update], - ), + Invocation.method(#updateMapConfiguration, [update]), returnValueForMissingStub: null, ); @override void updateStyles(List<_i5.MapTypeStyle>? styles) => super.noSuchMethod( - Invocation.method( - #updateStyles, - [styles], - ), + Invocation.method(#updateStyles, [styles]), returnValueForMissingStub: null, ); @override _i3.Future<_i2.LatLngBounds> getVisibleRegion() => (super.noSuchMethod( - Invocation.method( - #getVisibleRegion, - [], - ), - returnValue: _i3.Future<_i2.LatLngBounds>.value(_FakeLatLngBounds_2( - this, - Invocation.method( - #getVisibleRegion, - [], + 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, - [], + ), + 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], + 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], + ), + 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], + 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], + ), + 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], - ), + Invocation.method(#moveCamera, [cameraUpdate]), returnValue: _i3.Future.value(), returnValueForMissingStub: _i3.Future.value(), ) as _i3.Future); @override _i3.Future getZoomLevel() => (super.noSuchMethod( - Invocation.method( - #getZoomLevel, - [], - ), + 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], - ), + Invocation.method(#updateCircles, [updates]), returnValueForMissingStub: null, ); @override void updateHeatmaps(_i2.HeatmapUpdates? updates) => super.noSuchMethod( - Invocation.method( - #updateHeatmaps, - [updates], - ), + Invocation.method(#updateHeatmaps, [updates]), returnValueForMissingStub: null, ); @override void updatePolygons(_i2.PolygonUpdates? updates) => super.noSuchMethod( - Invocation.method( - #updatePolygons, - [updates], - ), + Invocation.method(#updatePolygons, [updates]), returnValueForMissingStub: null, ); @override void updatePolylines(_i2.PolylineUpdates? updates) => super.noSuchMethod( - Invocation.method( - #updatePolylines, - [updates], - ), + Invocation.method(#updatePolylines, [updates]), returnValueForMissingStub: null, ); @override _i3.Future updateMarkers(_i2.MarkerUpdates? updates) => (super.noSuchMethod( - Invocation.method( - #updateMarkers, - [updates], - ), + Invocation.method(#updateMarkers, [updates]), returnValue: _i3.Future.value(), returnValueForMissingStub: _i3.Future.value(), ) as _i3.Future); @@ -334,76 +258,59 @@ class MockGoogleMapController extends _i1.Mock @override void updateClusterManagers(_i2.ClusterManagerUpdates? updates) => super.noSuchMethod( - Invocation.method( - #updateClusterManagers, - [updates], - ), + Invocation.method(#updateClusterManagers, [updates]), returnValueForMissingStub: null, ); @override void updateGroundOverlays(_i2.GroundOverlayUpdates? updates) => super.noSuchMethod( - Invocation.method( - #updateGroundOverlays, - [updates], - ), + Invocation.method(#updateGroundOverlays, [updates]), returnValueForMissingStub: null, ); @override void updateTileOverlays(Set<_i2.TileOverlay>? newOverlays) => super.noSuchMethod( - Invocation.method( - #updateTileOverlays, - [newOverlays], - ), + Invocation.method(#updateTileOverlays, [newOverlays]), returnValueForMissingStub: null, ); @override void clearTileCache(_i2.TileOverlayId? id) => super.noSuchMethod( - Invocation.method( - #clearTileCache, - [id], - ), + Invocation.method(#clearTileCache, [id]), returnValueForMissingStub: null, ); @override void showInfoWindow(_i2.MarkerId? markerId) => super.noSuchMethod( - Invocation.method( - #showInfoWindow, - [markerId], - ), + Invocation.method(#showInfoWindow, [markerId]), returnValueForMissingStub: null, ); @override void hideInfoWindow(_i2.MarkerId? markerId) => super.noSuchMethod( - Invocation.method( - #hideInfoWindow, - [markerId], - ), + Invocation.method(#hideInfoWindow, [markerId]), returnValueForMissingStub: null, ); @override bool isInfoWindowShown(_i2.MarkerId? markerId) => (super.noSuchMethod( - Invocation.method( - #isInfoWindowShown, - [markerId], - ), + 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, - [], - ), + 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 fbdcbf94946..98f34fef0e4 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 @@ -98,7 +98,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 2859d082b78..cf54abe7179 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( @@ -67,7 +67,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( @@ -80,7 +80,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( @@ -93,7 +93,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( @@ -106,7 +106,8 @@ void main() { }); testWidgets('update', (WidgetTester tester) async { - final MarkerController controller = MarkerController(marker: marker); + final LegacyMarkerController controller = + LegacyMarkerController(marker: marker); final gmaps.MarkerOptions options = gmaps.MarkerOptions() ..draggable = true ..position = gmaps.LatLng(42, 54); @@ -122,7 +123,8 @@ void main() { testWidgets('infoWindow null, showInfoWindow.', (WidgetTester tester) async { - final MarkerController controller = MarkerController(marker: marker); + final LegacyMarkerController controller = + LegacyMarkerController(marker: marker); controller.showInfoWindow(); @@ -133,7 +135,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, ); @@ -148,7 +150,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, ); @@ -160,13 +162,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 39e28509de7..3e5ad6a8f94 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart @@ -25,15 +25,16 @@ void main() { group('MarkersController', () { late StreamController> events; - late MarkersController controller; - late ClusterManagersController clusterManagersController; + late LegacyMarkersController controller; + late ClusterManagersController clusterManagersController; late gmaps.Map map; setUp(() { events = StreamController>(); - clusterManagersController = ClusterManagersController(stream: events); - controller = MarkersController( + clusterManagersController = + ClusterManagersController(stream: events); + controller = LegacyMarkersController( stream: events, clusterManagersController: clusterManagersController); map = gmaps.Map(createDivElement()); clusterManagersController.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 0480e644fe1..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,19 +17,15 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class class _FakeTile_0 extends _i1.SmartFake implements _i2.Tile { - _FakeTile_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); + _FakeTile_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); } /// A class which mocks [TileProvider]. @@ -37,41 +33,14 @@ class _FakeTile_0 extends _i1.SmartFake implements _i2.Tile { /// See the documentation for Mockito's code generation for more information. class MockTileProvider extends _i1.Mock implements _i2.TileProvider { @override - _i3.Future<_i2.Tile> getTile( - int? x, - int? y, - int? zoom, - ) => + _i3.Future<_i2.Tile> getTile(int? x, int? y, int? zoom) => (super.noSuchMethod( - Invocation.method( - #getTile, - [ - x, - y, - zoom, - ], + 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])), ), - 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/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml index 6d404825bc7..76f01d47021 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml @@ -8,7 +8,7 @@ environment: dependencies: flutter: sdk: flutter - google_maps_flutter_platform_interface: ^2.12.1 + google_maps_flutter_platform_interface: ^2.13.0 google_maps_flutter_web: path: ../ web: ^1.0.0 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 169dd5ff7ec..4e921face9c 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 f8b7dd0506b..4e13d2636ef 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,6 +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' as web; import 'package:web/web.dart'; import 'src/dom_window_extension.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 ee62cad2733..e069d400515 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 @@ -104,7 +104,7 @@ gmaps.MapOptions _configurationAndStyleToGmapsOptions( // See updateMapConfiguration for why this is not using configuration.style. options.styles = styles; - options.mapId = configuration.cloudMapId; + options.mapId = configuration.mapId; return options; } @@ -196,7 +196,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), @@ -294,21 +294,32 @@ gmaps.Size? _gmSizeFromIconConfig(List iconConfig, int sizeIndex) { ); } } + return size; } -/// Sets the size of the Google Maps icon. -void _setIconSize({ - required Size size, - required gmaps.Icon icon, +// 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, }) { - final gmaps.Size gmapsSize = gmaps.Size(size.width, size.height); - icon.size = gmapsSize; - icon.scaledSize = gmapsSize; + 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, }) { @@ -319,6 +330,16 @@ 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 @@ -326,12 +347,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, 'URL must not be empty when calculating dimensions.'); @@ -360,7 +381,7 @@ Future _getBitmapSize(MapBitmap mapBitmap, String url) async { } // Return the calculated size. - return Size(targetWidth, targetHeight); + return gmaps.Size(targetWidth, targetHeight); } } @@ -392,19 +413,155 @@ 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 = 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 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 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 Blob blob = + Blob([bytesMapBitmap.byteData.toJS].toJS); + return URL.createObjectURL(blob as JSObject); + }), + (final AssetMapBitmap assetMapBitmap) => + ui_web.assetManager.getAssetUrl(assetMapBitmap.assetName), + _ => throw UnimplementedError(), + }; + + final web.Element icon = 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 = 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 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 = Blob([(bytes as Uint8List).toJS].toJS); + + final web.Element icon = document.createElement('img') + ..setAttribute('src', 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, Offset anchor) async { + BitmapDescriptor bitmapDescriptor, + Offset anchor, +) async { 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 Blob blob = + Blob([bytesMapBitmap.byteData.toJS].toJS); + return 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); @@ -412,6 +569,7 @@ Future _gmIconFromBitmapDescriptor( case MapBitmapScaling.none: break; } + return icon; } @@ -458,27 +616,65 @@ Future _gmIconFromBitmapDescriptor( return icon; } -/// Computes the options for a new [gmaps.Marker] from an incoming set of options -/// [marker], and the existing marker registered with the map: [currentMarker]. -Future _markerOptionsFromMarker( +// Computes the options for a new [gmaps.Marker] from an incoming set of options +// [marker], and the existing marker registered with the map: [currentMarker]. +Future _markerOptionsFromMarker( Marker marker, - gmaps.Marker? currentMarker, + T? currentMarker, ) async { - return gmaps.MarkerOptions() - ..position = gmaps.LatLng( - marker.position.latitude, - marker.position.longitude, - ) - ..title = sanitizeHtml(marker.infoWindow.title ?? '') - // The deprecated parameter is used here to avoid losing precision. - // ignore: deprecated_member_use - ..zIndex = marker.zIndex - ..visible = marker.visible - ..opacity = marker.alpha - ..draggable = marker.draggable - ..icon = await _gmIconFromBitmapDescriptor(marker.icon, marker.anchor); - // TODO(ditman): Compute anchor properly, otherwise infowindows attach to the wrong spot. - // Flat and Rotation are not supported directly on the web. + if (marker is AdvancedMarker) { + final 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; + final gmaps.MVCObject mapObject = marker as gmaps.MVCObject; + if (object.isA()) { + return MarkerId((mapObject.get('markerId')! as JSString).toDart); + } else if (object.isA()) { + return MarkerId((mapObject.get('id')! as JSString).toDart); + } else { + throw ArgumentError( + 'Must be either a gmaps.Marker or a gmaps.AdvancedMarkerElement', + ); + } } gmaps.CircleOptions _circleOptionsFromCircle(Circle circle) { @@ -737,3 +933,15 @@ gmaps.LatLng _pixelToLatLng(gmaps.Map map, int x, int y) { return projection.fromPointToLatLng(point)!; } + +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 4588c4717ba..0d4a90b8571 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 @@ -38,11 +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); @@ -132,8 +175,8 @@ class GoogleMapController { HeatmapsController? _heatmapsController; PolygonsController? _polygonsController; PolylinesController? _polylinesController; - MarkersController? _markersController; - ClusterManagersController? _clusterManagersController; + MarkersController? _markersController; + ClusterManagersController? _clusterManagersController; TileOverlaysController? _tileOverlaysController; GroundOverlaysController? _groundOverlaysController; @@ -145,7 +188,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. @@ -158,12 +201,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, }) { @@ -421,7 +464,7 @@ class GoogleMapController { await Future.value(_googleMap!.bounds) ?? _nullGmapsLatLngBounds; - return gmLatLngBoundsTolatLngBounds(bounds); + return gmLatLngBoundsToLatLngBounds(bounds); } /// Returns the [ScreenCoordinate] for a given viewport [LatLng]. @@ -571,6 +614,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 d205a747690..fc173ec913b 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 @@ -319,6 +319,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 d5a23c434a2..f5b3d27f020 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 = GroundOverlaysController? Function( @@ -105,7 +105,7 @@ class GoogleMapsInspectorWeb extends GoogleMapsInspectorPlatform { Uint8List.fromList([0]), bitmapScaling: MapBitmapScaling.none, ), - bounds: gmLatLngBoundsTolatLngBounds(groundOverlay.bounds), + bounds: gmLatLngBoundsToLatLngBounds(groundOverlay.bounds), transparency: 1.0 - groundOverlay.opacity, visible: groundOverlay.map != null, clickable: clickable != null && (clickable as JSBoolean).toDart); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart index 518dce6de77..8f0fb5f429a 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, @@ -20,32 +24,16 @@ class MarkerController { _infoWindow = infoWindow, _consumeTapEvents = consumeTapEvents, _clusterManagerId = clusterManagerId { - if (onTap != null) { - marker.onClick.listen((gmaps.MapMouseEvent event) { - onTap.call(); - }); - } - if (onDragStart != null) { - marker.onDragstart.listen((gmaps.MapMouseEvent event) { - marker.position = event.latLng; - onDragStart.call(event.latLng ?? _nullGmapsLatLng); - }); - } - if (onDrag != null) { - marker.onDrag.listen((gmaps.MapMouseEvent event) { - marker.position = event.latLng; - onDrag.call(event.latLng ?? _nullGmapsLatLng); - }); - } - if (onDragEnd != null) { - marker.onDragend.listen((gmaps.MapMouseEvent event) { - marker.position = event.latLng; - onDragEnd.call(event.latLng ?? _nullGmapsLatLng); - }); - } + initializeMarkerListener( + marker: marker, + onDragStart: onDragStart, + onDrag: onDrag, + onDragEnd: onDragEnd, + onTap: onTap, + ); } - gmaps.Marker? _marker; + T? _marker; final bool _consumeTapEvents; @@ -64,56 +52,222 @@ 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, { + O options, { 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.gmpClickable = options.gmpClickable; + 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 8dbb4308ba6..0d3d83df4a9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering.dart @@ -17,7 +17,10 @@ import 'types.dart'; /// This class maps [ClusterManager] objects to javascript [MarkerClusterer] /// objects and provides an interface for adding and removing markers from /// clusters. -class ClusterManagersController extends GeometryController { +/// +/// [T] must extend [JSObject]. It's not specified in code because our mocking +/// framework does not support mocking JSObjects. +class ClusterManagersController extends GeometryController { /// Creates a new [ClusterManagersController] instance. /// /// The [stream] parameter is a required [StreamController] used for @@ -26,13 +29,13 @@ class ClusterManagersController extends GeometryController { {required StreamController> stream}) : _streamController = stream, _clusterManagerIdToMarkerClusterer = - {}; + >{}; // The stream over which cluster managers broadcast their events final StreamController> _streamController; // A cache of [MarkerClusterer]s indexed by their [ClusterManagerId]. - final Map + final Map> _clusterManagerIdToMarkerClusterer; /// Adds a set of [ClusterManager] objects to the cache. @@ -41,12 +44,12 @@ class ClusterManagersController extends GeometryController { } void _addClusterManager(ClusterManager clusterManager) { - final MarkerClusterer markerClusterer = createMarkerClusterer( - googleMap, - (gmaps.MapMouseEvent event, MarkerClustererCluster cluster, - gmaps.Map map) => - _clusterClicked( - clusterManager.clusterManagerId, event, cluster, map)); + final MarkerClusterer markerClusterer = createMarkerClusterer( + googleMap, + (gmaps.MapMouseEvent event, MarkerClustererCluster cluster, + gmaps.Map map) => + _clusterClicked(clusterManager.clusterManagerId, event, cluster, map), + ); _clusterManagerIdToMarkerClusterer[clusterManager.clusterManagerId] = markerClusterer; @@ -59,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); @@ -68,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); @@ -79,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); @@ -95,11 +103,11 @@ 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) => + .map((MarkerClustererCluster cluster) => _convertCluster(clusterManagerId, cluster)) .toList(); } @@ -109,7 +117,7 @@ class ClusterManagersController extends GeometryController { void _clusterClicked( ClusterManagerId clusterManagerId, gmaps.MapMouseEvent event, - MarkerClustererCluster markerClustererCluster, + MarkerClustererCluster markerClustererCluster, gmaps.Map map) { if (markerClustererCluster.count > 0 && markerClustererCluster.bounds != null) { @@ -121,15 +129,13 @@ class ClusterManagersController extends GeometryController { /// Converts [MarkerClustererCluster] to [Cluster]. Cluster _convertCluster(ClusterManagerId clusterManagerId, - MarkerClustererCluster markerClustererCluster) { + MarkerClustererCluster markerClustererCluster) { final LatLng position = gmLatLngToLatLng(markerClustererCluster.position); final LatLngBounds bounds = - gmLatLngBoundsTolatLngBounds(markerClustererCluster.bounds!); + gmLatLngBoundsToLatLngBounds(markerClustererCluster.bounds!); + final List markerIds = + markerClustererCluster.markers.map(getMarkerId).toList(); - final List markerIds = markerClustererCluster.markers - .map((gmaps.Marker marker) => - MarkerId((marker.get('markerId')! as JSString).toDart)) - .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 80e7e5fc1c1..130c0774abd 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,9 +13,9 @@ 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( +typedef ClusterClickHandler = void Function( gmaps.MapMouseEvent, - MarkerClustererCluster, + MarkerClustererCluster, gmaps.Map, ); @@ -24,18 +24,18 @@ typedef ClusterClickHandler = void Function( /// 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, + List? markers, + ClusterClickHandler? onClusterClick, }) => - MarkerClustererOptions._js( + MarkerClustererOptions._js( map: map as JSAny?, markers: markers?.cast().toJS ?? JSArray(), onClusterClick: onClusterClick != null - ? ((JSAny event, MarkerClustererCluster cluster, JSAny map) => + ? ((JSAny event, MarkerClustererCluster cluster, JSAny map) => onClusterClick(event as gmaps.MapMouseEvent, cluster, map as gmaps.Map)).toJS : null, @@ -52,13 +52,13 @@ extension type MarkerClustererOptions._(JSObject _) implements JSObject { @JS('map') external JSAny? get _map; - /// Returns the list of [gmaps.Marker] objects. - List? get markers => _markers?.toDart.cast(); + /// Returns the list of marker objects. + List? get markers => _markers?.toDart.cast(); @JS('markers') external JSArray? get _markers; /// Returns the onClusterClick handler. - ClusterClickHandler? get onClusterClick => + ClusterClickHandler? get onClusterClick => _onClusterClick?.toDart as ClusterClickHandler?; @JS('onClusterClick') external JSExportedDartFunction? get _onClusterClick; @@ -68,14 +68,14 @@ extension type MarkerClustererOptions._(JSObject _) implements JSObject { /// /// https://googlemaps.github.io/js-markerclusterer/classes/Cluster.html @JS('markerClusterer.Cluster') -extension type MarkerClustererCluster._(JSObject _) implements JSObject { +extension type MarkerClustererCluster._(JSObject _) implements JSObject { /// Getter for the cluster marker. - gmaps.Marker get marker => _marker as gmaps.Marker; + T get marker => _marker as T; @JS('marker') external JSAny get _marker; /// List of markers in the cluster. - List get markers => _markers.toDart.cast(); + List get markers => _markers.toDart.cast(); @JS('markers') external JSArray get _markers; @@ -96,7 +96,7 @@ extension type MarkerClustererCluster._(JSObject _) implements JSObject { external void delete(); /// Adds a marker to the cluster. - void push(gmaps.Marker marker) => _push(marker as JSAny); + void push(T marker) => _push(marker as JSAny); @JS('push') external void _push(JSAny marker); } @@ -105,30 +105,29 @@ extension type MarkerClustererCluster._(JSObject _) implements JSObject { /// /// https://googlemaps.github.io/js-markerclusterer/classes/MarkerClusterer.html @JS('markerClusterer.MarkerClusterer') -extension type MarkerClusterer._(JSObject _) implements JSObject { +extension type MarkerClusterer._(JSObject _) implements JSObject { /// Constructs a new [MarkerClusterer] object. - external MarkerClusterer(MarkerClustererOptions options); + external MarkerClusterer(MarkerClustererOptions options); /// Adds a marker to be clustered by the [MarkerClusterer]. - void addMarker(gmaps.Marker marker, bool? noDraw) => - _addMarker(marker as JSAny, noDraw); + void addMarker(T marker, bool? noDraw) => _addMarker(marker as JSAny, noDraw); @JS('addMarker') external void _addMarker(JSAny marker, bool? noDraw); /// Adds a list of markers to be clustered by the [MarkerClusterer]. - void addMarkers(List? markers, bool? noDraw) => + void addMarkers(List? markers, bool? noDraw) => _addMarkers(markers?.cast().toJS, noDraw); @JS('addMarkers') external void _addMarkers(JSArray? markers, bool? noDraw); /// Removes a marker from the [MarkerClusterer]. - bool removeMarker(gmaps.Marker marker, bool? noDraw) => + bool removeMarker(T marker, bool? noDraw) => _removeMarker(marker as JSAny, noDraw); @JS('removeMarker') external bool _removeMarker(JSAny marker, bool? noDraw); /// Removes a list of markers from the [MarkerClusterer]. - bool removeMarkers(List? markers, bool? noDraw) => + bool removeMarkers(List? markers, bool? noDraw) => _removeMarkers(markers?.cast().toJS, noDraw); @JS('removeMarkers') external bool _removeMarkers(JSArray? markers, bool? noDraw); @@ -143,8 +142,8 @@ extension type MarkerClusterer._(JSObject _) implements JSObject { external void onRemove(); /// Returns the list of clusters. - List get clusters => - _clusters.toDart.cast(); + List> get clusters => + _clusters.toDart.cast>(); @JS('clusters') external JSArray get _clusters; @@ -154,11 +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 f5204403528..b4599bbecfa 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, + required ClusterManagersController clusterManagersController, }) : _streamController = stream, _clusterManagersController = clusterManagersController, - _markerIdToController = {}; + _markerIdToController = >{}; // A cache of [MarkerController]s indexed by their [MarkerId]. - final Map _markerIdToController; + final Map> _markerIdToController; // The stream over which markers broadcast their events final StreamController> _streamController; - final ClusterManagersController _clusterManagersController; + final ClusterManagersController _clusterManagersController; /// Returns the cache of [MarkerController]s. Test only. @visibleForTesting - Map get markers => _markerIdToController; + Map> get markers => _markerIdToController; /// Adds a set of [Marker] objects to the cache. /// @@ -52,51 +60,36 @@ class MarkersController extends GeometryController { } } - final gmaps.Marker? currentMarker = - _markerIdToController[marker.markerId]?.marker; - - final gmaps.MarkerOptions markerOptions = + final MarkerController? markerController = + _markerIdToController[marker.markerId]; + final T? currentMarker = markerController?.marker; + final O 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 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 = @@ -108,8 +101,7 @@ class MarkersController extends GeometryController { _removeMarker(marker.markerId); await _addMarker(marker); } else { - final gmaps.MarkerOptions markerOptions = - await _markerOptionsFromMarker( + final O markerOptions = await _markerOptionsFromMarker( marker, markerController.marker, ); @@ -129,7 +121,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!, markerController.marker); @@ -145,7 +138,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(); } @@ -153,7 +147,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(); } @@ -161,7 +156,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; } @@ -204,10 +200,106 @@ class MarkersController extends GeometryController { void _hideAllMarkerInfoWindow() { _markerIdToController.values - .where((MarkerController? controller) => + .where((MarkerController? controller) => controller?.infoWindowShown ?? false) - .forEach((MarkerController controller) { + .forEach((MarkerController controller) { controller.hideInfoWindow(); }); } } + +/// A [MarkersController] for the legacy [gmaps.Marker] class. +class LegacyMarkersController + extends MarkersController { + /// Initialize the markers controller for the legacy [gmaps.Marker] class. + LegacyMarkersController({ + required super.stream, + required super.clusterManagersController, + }); + + @override + Future createMarkerController( + Marker marker, + gmaps.MarkerOptions markerOptions, + gmaps.InfoWindow? gmInfoWindow, + ) async { + final 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/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index 195cce79784..c64cbfd5b0b 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.12+2 +version: 0.5.13 environment: sdk: ^3.6.0 @@ -23,10 +23,10 @@ dependencies: flutter_web_plugins: sdk: flutter google_maps: ^8.0.0 - google_maps_flutter_platform_interface: ^2.12.1 + google_maps_flutter_platform_interface: ^2.13.0 sanitize_html: ^2.0.0 stream_transform: ^2.0.0 - web: ">=0.5.1 <2.0.0" + web: ">=1.0.0 <2.0.0" dev_dependencies: flutter_test: