diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index c14478638..a64d776e1 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,17 @@ +## 0.2.0 + +* Refactor the JavaScript interaction layer by introducing `GoogleMapsJsBridge`, + replacing direct `WebViewController` calls. This removes the public + `GoogleMapsController.controller` field; use the plugin's public API + instead of reaching into the WebView controller directly. +* Update `webview_flutter_lwe` to ^0.5.3. +* Raise the SDK constraint to match `webview_flutter_lwe` 0.5.3 + (Dart ^3.8.0, Flutter >=3.32.0). +* Fix info window content being rendered with stray quote characters. +* Cancel the bridge event subscription on dispose to avoid a late event + throwing a `StateError` on an already-closed stream. +* Verify integration tests pass against upstream google_maps_flutter v2.17.0. + ## 0.1.14 * Update google_maps_flutter to 2.16.0. diff --git a/packages/google_maps_flutter/README.md b/packages/google_maps_flutter/README.md index 0cbdb4161..67e3411d7 100644 --- a/packages/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/README.md @@ -21,7 +21,7 @@ This package is not an _endorsed_ implementation of `google_maps_flutter`. There ```yaml dependencies: google_maps_flutter: ^2.16.0 - google_maps_flutter_tizen: ^0.1.14 + google_maps_flutter_tizen: ^0.2.0 ``` For detailed usage, see https://pub.dev/packages/google_maps_flutter#sample-usage. diff --git a/packages/google_maps_flutter/lib/google_maps_flutter_tizen.dart b/packages/google_maps_flutter/lib/google_maps_flutter_tizen.dart index eb6c1ca62..c15a05427 100644 --- a/packages/google_maps_flutter/lib/google_maps_flutter_tizen.dart +++ b/packages/google_maps_flutter/lib/google_maps_flutter_tizen.dart @@ -7,7 +7,6 @@ library google_maps_flutter_tizen; import 'dart:async'; import 'dart:convert'; -import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; @@ -19,6 +18,7 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf import 'package:stream_transform/stream_transform.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import 'src/google_maps_js_bridge.dart'; import 'src/util.dart' as util; part 'src/circle.dart'; diff --git a/packages/google_maps_flutter/lib/src/circle.dart b/packages/google_maps_flutter/lib/src/circle.dart index 197f3e09a..4a7cea7f3 100644 --- a/packages/google_maps_flutter/lib/src/circle.dart +++ b/packages/google_maps_flutter/lib/src/circle.dart @@ -12,11 +12,11 @@ class CircleController { required util.GCircle circle, bool consumeTapEvents = false, ui.VoidCallback? onTap, - WebViewController? controller, - }) : _circle = circle, - _consumeTapEvents = consumeTapEvents, - tapEvent = onTap { - _addCircleEvent(controller); + required GoogleMapsJsBridge bridge, + }) : _circle = circle, + _consumeTapEvents = consumeTapEvents, + tapEvent = onTap { + _addCircleEvent(bridge); } util.GCircle? _circle; @@ -25,10 +25,13 @@ class CircleController { /// Circle component's tap event. ui.VoidCallback? tapEvent; - Future _addCircleEvent(WebViewController? controller) async { - final String command = - "$_circle.addListener('click', (event) => CircleClick.postMessage(JSON.stringify(${_circle?.id})));"; - await controller!.runJavaScript(command); + Future _addCircleEvent(GoogleMapsJsBridge bridge) async { + await bridge.addListener( + JsRef(_circle.toString()), + 'click', + 'CircleClick', + 'JSON.stringify(${_circle?.id})', + ); } /// Returns `true` if this Controller will use its own `onTap` handler to consume events. diff --git a/packages/google_maps_flutter/lib/src/circles.dart b/packages/google_maps_flutter/lib/src/circles.dart index d2ec6b955..9f0a22a4a 100644 --- a/packages/google_maps_flutter/lib/src/circles.dart +++ b/packages/google_maps_flutter/lib/src/circles.dart @@ -8,10 +8,13 @@ part of '../google_maps_flutter_tizen.dart'; /// This class manages all the [CircleController]s associated to a [GoogleMapController]. class CirclesController extends GeometryController { /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. - CirclesController({required StreamController> stream}) - : _streamController = stream, - _circleIdToController = {}, - _idToCircleId = {}; + CirclesController({ + required StreamController> stream, + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _bridge = bridge, + _circleIdToController = {}, + _idToCircleId = {}; // A cache of [CircleController]s indexed by their [CircleId]. final Map _circleIdToController; @@ -20,6 +23,8 @@ class CirclesController extends GeometryController { // The stream over which circles broadcast their events final StreamController> _streamController; + final GoogleMapsJsBridge _bridge; + /// Adds a set of [Circle] objects to the cache. /// /// Wraps each [Circle] into its corresponding [CircleController]. @@ -35,14 +40,14 @@ class CirclesController extends GeometryController { final util.GCircleOptions populationOptions = _circleOptionsFromCircle( circle, ); - final util.GCircle gCircle = util.GCircle(populationOptions); + final util.GCircle gCircle = util.GCircle(_bridge, populationOptions); final CircleController controller = CircleController( circle: gCircle, consumeTapEvents: circle.consumeTapEvents, onTap: () { _onCircleTap(circle.circleId); }, - controller: util.webController, + bridge: _bridge, ); _idToCircleId[gCircle.id] = circle.circleId; _circleIdToController[circle.circleId] = controller; diff --git a/packages/google_maps_flutter/lib/src/convert.dart b/packages/google_maps_flutter/lib/src/convert.dart index 0e7d3b37a..82388f4e8 100644 --- a/packages/google_maps_flutter/lib/src/convert.dart +++ b/packages/google_maps_flutter/lib/src/convert.dart @@ -216,23 +216,24 @@ String _mapStyles(String? mapStyleJson) { if (mapStyleJson != null) { try { json - .decode( - mapStyleJson, - reviver: (Object? key, Object? value) { - if (value is Map && - _isJsonMapStyle(value as Map)) { - return MapTypeStyle() - ..elementType = value['elementType'] as String? - ..featureType = value['featureType'] as String? - ..stylers = (value['stylers']! as List) - .map((dynamic e) => e) - .toList(); - } - return value; - }, - ) - .cast() - .toList() as List; + .decode( + mapStyleJson, + reviver: (Object? key, Object? value) { + if (value is Map && + _isJsonMapStyle(value as Map)) { + return MapTypeStyle() + ..elementType = value['elementType'] as String? + ..featureType = value['featureType'] as String? + ..stylers = (value['stylers']! as List) + .map((dynamic e) => e) + .toList(); + } + return value; + }, + ) + .cast() + .toList() + as List; } catch (e) { throw MapStyleException('Invalid Map Style JSON: $e'); } @@ -306,9 +307,11 @@ util.GInfoWindowOptions? _infoWindowOptionsFromMarker(Marker marker) { return null; } - // Add an outer wrapper to the contents of the infowindow + // Add an outer wrapper to the contents of the infowindow. The content is + // JSON-encoded by its consumers (GInfoWindowOptions.toString and + // GInfoWindow._setContent), so it must be raw, unquoted HTML here. final StringBuffer buffer = StringBuffer(); - buffer.write('\'
'); + buffer.write('
'); if (markerTitle.isNotEmpty) { buffer.write('

'); buffer.write(markerTitle); @@ -319,7 +322,7 @@ util.GInfoWindowOptions? _infoWindowOptionsFromMarker(Marker marker) { buffer.write(markerSnippet); buffer.write('

'); } - buffer.write("
'"); + buffer.write(''); // Need to add Click Event to infoWindow's content return util.GInfoWindowOptions() @@ -461,7 +464,8 @@ util.GPolygonOptions _polygonOptionsFromPolygon(Polygon polygon) { bool _isPolygonClockwise(List path) { double direction = 0.0; for (int i = 0; i < path.length; i++) { - direction = direction + + direction = + direction + ((path[(i + 1) % path.length].latitude - path[i].latitude) * (path[(i + 1) % path.length].longitude + path[i].longitude)); } @@ -506,7 +510,8 @@ util.GGroundOverlayOptions? _groundOverlayOptionsFromGroundOverlay( } return util.GGroundOverlayOptions() ..url = "'$imageUrl'" - ..bounds = '{south:${bounds.southwest.latitude},' + ..bounds = + '{south:${bounds.southwest.latitude},' ' west:${bounds.southwest.longitude},' ' north:${bounds.northeast.latitude},' ' east:${bounds.northeast.longitude}}' diff --git a/packages/google_maps_flutter/lib/src/google_maps_controller.dart b/packages/google_maps_flutter/lib/src/google_maps_controller.dart index 53e7c4ab8..9fd427800 100644 --- a/packages/google_maps_flutter/lib/src/google_maps_controller.dart +++ b/packages/google_maps_flutter/lib/src/google_maps_controller.dart @@ -7,9 +7,6 @@ part of '../google_maps_flutter_tizen.dart'; -/// The duration of MapLongPressEvent. -const int kGoogleMapsControllerLongPressDuration = 1000; - /// This class implements a Map Controller and its events class GoogleMapsController { /// Initializes the GoogleMapsController. @@ -24,28 +21,40 @@ class GoogleMapsController { Set clusterManagers = const {}, Set groundOverlays = const {}, Map mapOptions = const {}, - }) : _mapId = mapId, - _streamController = streamController, - _initialCameraPosition = initialCameraPosition, - _markers = markers, - _polygons = polygons, - _polylines = polylines, - _circles = circles, - _clusterManagers = clusterManagers, - _groundOverlays = groundOverlays, - _rawMapOptions = mapOptions { - _circlesController = CirclesController(stream: _streamController); - _polygonsController = PolygonsController(stream: _streamController); - _polylinesController = PolylinesController(stream: _streamController); + }) : _mapId = mapId, + _streamController = streamController, + _initialCameraPosition = initialCameraPosition, + _markers = markers, + _polygons = polygons, + _polylines = polylines, + _circles = circles, + _clusterManagers = clusterManagers, + _groundOverlays = groundOverlays, + _rawMapOptions = mapOptions { + _circlesController = CirclesController( + stream: _streamController, + bridge: _bridge, + ); + _polygonsController = PolygonsController( + stream: _streamController, + bridge: _bridge, + ); + _polylinesController = PolylinesController( + stream: _streamController, + bridge: _bridge, + ); _clusterManagersController = ClusterManagersController( stream: _streamController, + bridge: _bridge, ); _markersController = MarkersController( stream: _streamController, clusterManagersController: _clusterManagersController!, + bridge: _bridge, ); _groundOverlaysController = GroundOverlaysController( stream: _streamController, + bridge: _bridge, ); } @@ -61,14 +70,14 @@ class GoogleMapsController { final Set _circles; final Set _clusterManagers; final Set _groundOverlays; - final Completer _pageFinishedCompleter = Completer(); WebViewWidget? _webview; // The raw options passed by the user, before converting to maps. // Caching this allows us to re-create the map faithfully when needed. Map _rawMapOptions = {}; - /// Webview controller instance. - final WebViewController controller = WebViewController(); + /// The bridge mediating all interaction with the Google Maps JavaScript + /// API running inside the WebView. + final GoogleMapsJsBridge _bridge = GoogleMapsJsBridge(); /// The Flutter widget that will contain the rendered Map. Used for caching. WebViewWidget? get webview => _webview; @@ -76,9 +85,11 @@ class GoogleMapsController { /// Returns min-max zoom levels. Test only. @visibleForTesting Future getMinMaxZoomLevels() async { - final String value = await controller.runJavaScriptReturningResult( - 'JSON.stringify([map.minZoom, map.maxZoom])', - ) as String; + final String value = + await _bridge.runJavaScriptReturningResult( + 'JSON.stringify([map.minZoom, map.maxZoom])', + ) + as String; final dynamic bound = json.decode(value); double min = 0, max = 0; if (bound is List) { @@ -100,24 +111,26 @@ class GoogleMapsController { /// Returns if zoomGestures property is enabled. Test only. @visibleForTesting Future isZoomGesturesEnabled() async { - final String value = await controller - .runJavaScriptReturningResult('map.gestureHandling') as String; + final String value = + await _bridge.runJavaScriptReturningResult('map.gestureHandling') + as String; return value != 'none'; } /// Returns if zoomControls property is enabled. Test only. @visibleForTesting Future isZoomControlsEnabled() async { - final String value = await controller - .runJavaScriptReturningResult('map.zoomControl') as String; + final String value = + await _bridge.runJavaScriptReturningResult('map.zoomControl') as String; return value != 'false'; } /// Returns if scrollGestures property is enabled. Test only. @visibleForTesting Future isScrollGesturesEnabled() async { - final String value = await controller - .runJavaScriptReturningResult('map.gestureHandling') as String; + final String value = + await _bridge.runJavaScriptReturningResult('map.gestureHandling') + as String; return value != 'none'; } @@ -127,89 +140,48 @@ class GoogleMapsController { return _isTrafficLayerEnabled(_rawMapOptions); } + StreamSubscription? _bridgeEventsSubscription; + void _getWebview() { - // If the variable does not exist, we must find other alternatives. - String path = Platform.environment['AUL_ROOT_PATH'] ?? ''; - path += '/res/flutter_assets/assets/map.html'; - controller - ..setNavigationDelegate( - NavigationDelegate( - onPageFinished: (String url) { - _pageFinishedCompleter.complete(true); - }, - ), - ) - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..addJavaScriptChannel( - 'BoundChanged', - onMessageReceived: _onBoundsChanged, - ) - ..addJavaScriptChannel('Idle', onMessageReceived: _onIdle) - ..addJavaScriptChannel('Tilesloaded', onMessageReceived: _onTilesloaded) - ..addJavaScriptChannel('Click', onMessageReceived: _onClick) - ..addJavaScriptChannel('LongPress', onMessageReceived: _onLongPress) - ..addJavaScriptChannel('MarkerClick', onMessageReceived: _onMarkerClick) - ..addJavaScriptChannel('ClusterClick', onMessageReceived: _onClusterClick) - ..addJavaScriptChannel( - 'MarkerDragStart', - onMessageReceived: _onMarkerDragStart, - ) - ..addJavaScriptChannel('MarkerDrag', onMessageReceived: _onMarkerDrag) - ..addJavaScriptChannel( - 'MarkerDragEnd', - onMessageReceived: _onMarkerDragEnd, - ) - ..addJavaScriptChannel( - 'PolylineClick', - onMessageReceived: _onPolylineClick, - ) - ..addJavaScriptChannel('PolygonClick', onMessageReceived: _onPolygonClick) - ..addJavaScriptChannel('CircleClick', onMessageReceived: _onCircleClick) - ..addJavaScriptChannel( - 'GroundOverlayClick', - onMessageReceived: _onGroundOverlayClick, - ) - ..loadFile(path); - - _webview = WebViewWidget(controller: controller); + _bridgeEventsSubscription = _bridge.events.listen(_onJsEvent); + _webview = WebViewWidget(controller: _bridge.controller); + } + + Future _onJsEvent(MapsJsEvent event) async { + switch (event.type) { + case MapsJsEventType.boundsChanged: + await _onBoundsChanged(); + case MapsJsEventType.idle: + _onIdle(); + case MapsJsEventType.tilesLoaded: + _onTilesloaded(); + case MapsJsEventType.click: + _onClick(event.message!); + case MapsJsEventType.longPress: + _onLongPress(event.message!); + case MapsJsEventType.markerClick: + _onMarkerClick(event.message!); + case MapsJsEventType.clusterClick: + _onClusterClick(event.message!); + case MapsJsEventType.markerDragStart: + _onMarkerDragStart(event.message!); + case MapsJsEventType.markerDrag: + _onMarkerDrag(event.message!); + case MapsJsEventType.markerDragEnd: + _onMarkerDragEnd(event.message!); + case MapsJsEventType.polylineClick: + _onPolylineClick(event.message!); + case MapsJsEventType.polygonClick: + _onPolygonClick(event.message!); + case MapsJsEventType.circleClick: + _onCircleClick(event.message!); + case MapsJsEventType.groundOverlayClick: + _onGroundOverlayClick(event.message!); + } } Future _createMap() async { - final String options = _createOptions(); - final String command = ''' - map = new google.maps.Map(document.getElementById('map'), $options); - map.addListener('bounds_changed', (event) => { BoundChanged.postMessage(''); }); - map.addListener('idle', (event) => { Idle.postMessage(''); }); - map.addListener('click', (event) => { Click.postMessage(JSON.stringify(event)); }); - map.addListener('tilesloaded', (evnet) => { Tilesloaded.postMessage(''); }); - - let longPressTimeout; - map.addListener('mousedown', (e) => { - longPressTimeout = setTimeout(() => { - LongPress.postMessage(JSON.stringify(e)); - }, $kGoogleMapsControllerLongPressDuration); - }); - map.addListener('mouseup', () => { clearTimeout(longPressTimeout); }); - map.addListener('mouseout', () => { clearTimeout(longPressTimeout); }); - - const makeClusterEvent = function(clusterManagerId, event, cluster) { - var result = '{"id": "' + clusterManagerId +'"'; - result += ', "cluster": {"count":' + cluster.count - result += ', "position":' + JSON.stringify(cluster.position) - result += ', "bounds":' + JSON.stringify(cluster.bounds); - result += ', "markers": ['; - var i = 0; - for (; i < cluster.markers.length - 1; i++) { - result += cluster.markers[i].id; - result += ', '; - } - result += cluster.markers[i].id; - result += ']}}'; - - return result; - } - '''; - await controller.runJavaScript(command); + await _bridge.createMap(_createOptions()); } String _createOptions() { @@ -237,7 +209,7 @@ class GoogleMapsController { // Keeps track if the map is moving or not. bool _mapIsMoving = false; - Future _onBoundsChanged(JavaScriptMessage message) async { + Future _onBoundsChanged() async { final LatLng center = await getCenter(); final num zoom = await getZoomLevel(); @@ -256,12 +228,12 @@ class GoogleMapsController { } } - void _onIdle(JavaScriptMessage message) { + void _onIdle() { _mapIsMoving = false; _streamController.add(CameraIdleEvent(_mapId)); } - void _onTilesloaded(JavaScriptMessage message) { + void _onTilesloaded() { try { if (_isFirst) { return; @@ -273,9 +245,9 @@ class GoogleMapsController { } } - void _onClick(JavaScriptMessage message) { + void _onClick(String message) { try { - final dynamic event = json.decode(message.message); + final dynamic event = json.decode(message); if (event is Map) { assert(event['latLng'] != null); final LatLng position = LatLng( @@ -289,9 +261,9 @@ class GoogleMapsController { } } - void _onLongPress(JavaScriptMessage message) { + void _onLongPress(String message) { try { - final dynamic event = json.decode(message.message); + final dynamic event = json.decode(message); if (event is Map) { assert(event['latLng'] != null); final LatLng position = LatLng( @@ -305,9 +277,9 @@ class GoogleMapsController { } } - void _onClusterClick(JavaScriptMessage message) { + void _onClusterClick(String message) { try { - final dynamic result = json.decode(message.message); + final dynamic result = json.decode(message); final String id = result['id'] as String; final ClusterManagerId? clusterManagerId = @@ -328,9 +300,9 @@ class GoogleMapsController { } } - void _onMarkerClick(JavaScriptMessage message) { + void _onMarkerClick(String message) { try { - final dynamic id = json.decode(message.message); + final dynamic id = json.decode(message); if (_markersController != null && id is int) { final MarkerId? markerId = _markersController!._idToMarkerId[id]; final MarkerController? marker = @@ -344,9 +316,9 @@ class GoogleMapsController { } } - void _onMarkerDragStart(JavaScriptMessage message) { + void _onMarkerDragStart(String message) { try { - final dynamic result = json.decode(message.message); + final dynamic result = json.decode(message); if (result is Map) { assert(result['id'] != null && result['event'] != null); if (_markersController != null && result['id'] is int) { @@ -370,9 +342,9 @@ class GoogleMapsController { } } - void _onMarkerDrag(JavaScriptMessage message) { + void _onMarkerDrag(String message) { try { - final dynamic result = json.decode(message.message); + final dynamic result = json.decode(message); if (result is Map) { assert(result['id'] != null && result['event'] != null); if (_markersController != null && result['id'] is int) { @@ -396,9 +368,9 @@ class GoogleMapsController { } } - void _onMarkerDragEnd(JavaScriptMessage message) { + void _onMarkerDragEnd(String message) { try { - final dynamic result = json.decode(message.message); + final dynamic result = json.decode(message); if (result is Map) { assert(result['id'] != null && result['event'] != null); if (_markersController != null && result['id'] is int) { @@ -422,9 +394,9 @@ class GoogleMapsController { } } - void _onPolylineClick(JavaScriptMessage message) { + void _onPolylineClick(String message) { try { - final dynamic id = json.decode(message.message); + final dynamic id = json.decode(message); if (_polylinesController != null && id is int) { final PolylineId? polylineId = _polylinesController!._idToPolylineId[id]; @@ -439,9 +411,9 @@ class GoogleMapsController { } } - void _onPolygonClick(JavaScriptMessage message) { + void _onPolygonClick(String message) { try { - final dynamic id = json.decode(message.message); + final dynamic id = json.decode(message); if (_polygonsController != null && id is int) { final PolygonId? polygonId = _polygonsController!._idToPolygonId[id]; final PolygonController? polygon = @@ -455,9 +427,9 @@ class GoogleMapsController { } } - void _onCircleClick(JavaScriptMessage message) { + void _onCircleClick(String message) { try { - final dynamic id = json.decode(message.message); + final dynamic id = json.decode(message); if (_polygonsController != null && id is int) { final CircleId? circleId = _circlesController!._idToCircleId[id]; final CircleController? circle = @@ -471,9 +443,9 @@ class GoogleMapsController { } } - void _onGroundOverlayClick(JavaScriptMessage message) { + void _onGroundOverlayClick(String message) { try { - final dynamic id = json.decode(message.message); + final dynamic id = json.decode(message); if (_groundOverlaysController != null && id is int) { final GroundOverlayId? groundOverlayId = _groundOverlaysController!._idToGroundOverlayId[id]; @@ -498,7 +470,7 @@ class GoogleMapsController { Future init() async { if (_webview == null && !_streamController.isClosed) { _getWebview(); - await _pageFinishedCompleter.future; + await _bridge.load(); await _createMap(); } await _attachGeometryControllers(); @@ -551,7 +523,6 @@ class GoogleMapsController { _clusterManagersController!.bindToMap(_mapId, _webview!); _groundOverlaysController!.bindToMap(_mapId, _webview!); - util.webController = controller; _controllersBoundToMap = true; } @@ -604,16 +575,17 @@ class GoogleMapsController { } Future _setOptions(String options) async { - await _callMethod(controller, 'setOptions', [options]); + await _callMethod('setOptions', [options]); } Future _setZoom(String options) async { - await _callMethod(controller, 'setZoom', [options]); + await _callMethod('setZoom', [options]); } // Attaches/detaches a Traffic Layer on the `map` if `attach` is true/false. Future _setTrafficLayer(bool attach) async { - final String command = ''' + final String command = + ''' var trafficLayer; if ($attach == true && trafficLayer == null) { trafficLayer = new google.maps.TrafficLayer(); @@ -626,39 +598,34 @@ class GoogleMapsController { console.log('trafficLayer detached!!'); } '''; - await controller.runJavaScript(command); + await _bridge.runJavaScript(command); } Future _setMoveCamera(String options) async { - await _callMethod(controller, 'moveCamera', [options]); + await _callMethod('moveCamera', [options]); } Future _setPanTo(String options) async { - await _callMethod(controller, 'panTo', [options]); + await _callMethod('panTo', [options]); } Future _setPanBy(String options) async { - await _callMethod(controller, 'panBy', [options]); + await _callMethod('panBy', [options]); } Future _setFitBounds(String options) async { - await _callMethod(controller, 'fitBounds', [options]); + await _callMethod('fitBounds', [options]); } - Future _callMethod( - WebViewController controller, - String method, - List args, - ) async { - return controller.runJavaScriptReturningResult( + Future _callMethod(String method, List args) async { + return _bridge.runJavaScriptReturningResult( 'JSON.stringify(map.$method.apply(map, $args))', ); } - Future _getZoom(WebViewController controller) async { + Future _getZoom() async { try { - return (await _callMethod(controller, 'getZoom', []) as num) + - 0.0; + return (await _callMethod('getZoom', []) as num) + 0.0; } catch (e) { debugPrint('JavaScript Error: $e'); return 0.0; @@ -668,14 +635,14 @@ class GoogleMapsController { /// Returns the [LatLngBounds] of the current viewport. Future getVisibleRegion() async { return _convertToBounds( - await _callMethod(controller, 'getBounds', []) as String, + await _callMethod('getBounds', []) as String, ); } /// Returns the [LatLng] at the center of the map. Future getCenter() async { return _convertToLatLng( - await _callMethod(controller, 'getCenter', []) as String, + await _callMethod('getCenter', []) as String, ); } @@ -725,8 +692,9 @@ class GoogleMapsController { zoomDelta = (json[1] as num) + 0.0; } // Web only supports integer changes... - final int newZoomDelta = - zoomDelta < 0 ? zoomDelta.floor() : zoomDelta.ceil(); + final int newZoomDelta = zoomDelta < 0 + ? zoomDelta.floor() + : zoomDelta.ceil(); if (json.length == 3) { // With focus try { @@ -754,7 +722,8 @@ class GoogleMapsController { } Future _pixelToLatLng(double x, double y) async { - final String command = ''' + final String command = + ''' function getPixelToLatLng() { var projection = map.getProjection(); var ne = map.getBounds().getNorthEast(); @@ -768,11 +737,12 @@ class GoogleMapsController { JSON.stringify(getPixelToLatLng()); '''; - return await controller.runJavaScriptReturningResult(command) as String; + return await _bridge.runJavaScriptReturningResult(command) as String; } Future _latLngToPoint(LatLng latLng) async { - final String command = ''' + final String command = + ''' function getLatLngToPixel() { var ne = map.getBounds().getNorthEast(); var sw = map.getBounds().getSouthWest(); @@ -788,12 +758,12 @@ class GoogleMapsController { JSON.stringify(getLatLngToPixel()); '''; - return await controller.runJavaScriptReturningResult(command) as String; + return await _bridge.runJavaScriptReturningResult(command) as String; } /// Returns the zoom level of the current viewport. Future getZoomLevel() async { - return _getZoom(controller); + return _getZoom(); } // Geometry manipulation @@ -901,6 +871,8 @@ class GoogleMapsController { /// You won't be able to call many of the methods on this controller after /// calling `dispose`! void dispose() { + unawaited(_bridgeEventsSubscription?.cancel()); + _bridge.dispose(); _webview = null; _circlesController = null; _polygonsController = null; diff --git a/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart b/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart new file mode 100644 index 000000000..ad8bb4149 --- /dev/null +++ b/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart @@ -0,0 +1,327 @@ +// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved. +// 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:io'; + +import 'package:webview_flutter/webview_flutter.dart'; + +/// The duration (in milliseconds) of mouse-down before it is treated as a +/// long press by the JS side of the map. +const int kGoogleMapsControllerLongPressDuration = 1000; + +/// A handle to a JavaScript object living inside the Google Maps WebView. +/// +/// Wraps the JS-side variable name so it can be interpolated into further +/// JavaScript snippets (via [toString]) without callers hand-building +/// variable names. +class JsRef { + /// Creates a handle to the JS-side variable called [name]. + JsRef(this.name); + + /// The JS-side variable name this handle refers to. + final String name; + + @override + String toString() => name; +} + +/// A piece of raw JavaScript code to be evaluated literally rather than +/// encoded as a string literal. +class JsExpression { + /// Creates a [JsExpression] wrapping the raw [code]. + const JsExpression(this.code); + + /// The raw JavaScript code. + final String code; + + @override + String toString() => code; +} + +/// Identifies the kind of a [MapsJsEvent] dispatched from the Google Maps +/// JavaScript runtime. +enum MapsJsEventType { + /// The map's `bounds_changed` listener fired. + boundsChanged, + + /// The map's `idle` listener fired. + idle, + + /// The map's `tilesloaded` listener fired. + tilesLoaded, + + /// The map was clicked. + click, + + /// The map was long-pressed. + longPress, + + /// A marker was clicked. + markerClick, + + /// A marker cluster was clicked. + clusterClick, + + /// A marker drag started. + markerDragStart, + + /// A marker is being dragged. + markerDrag, + + /// A marker drag ended. + markerDragEnd, + + /// A polyline was clicked. + polylineClick, + + /// A polygon was clicked. + polygonClick, + + /// A circle was clicked. + circleClick, + + /// A ground overlay was clicked. + groundOverlayClick, +} + +/// An event dispatched from the Google Maps JavaScript runtime back into +/// Dart through a [GoogleMapsJsBridge]. +/// +/// [message] carries the raw (JSON-encoded) payload posted from the JS side, +/// or `null` for a [type] that carries no payload. +typedef MapsJsEvent = ({MapsJsEventType type, String? message}); + +/// Mediates all interaction between Dart and the Google Maps JavaScript API +/// running inside a WebView. +/// +/// This is the single seam through which JS commands are built and JS→Dart +/// events are dispatched, replacing ad hoc `runJavaScript` calls and +/// hand-built JS strings scattered across the plugin. +class GoogleMapsJsBridge { + /// Creates a bridge over [controller], or a fresh [WebViewController] if + /// none is provided. + GoogleMapsJsBridge({WebViewController? controller}) + : controller = controller ?? WebViewController(); + + /// The JS-side channel name and payload-less-ness for each event type, + /// registered in [load]. + static const Map _channelEventTypes = + { + 'BoundChanged': MapsJsEventType.boundsChanged, + 'Idle': MapsJsEventType.idle, + 'Tilesloaded': MapsJsEventType.tilesLoaded, + 'Click': MapsJsEventType.click, + 'LongPress': MapsJsEventType.longPress, + 'MarkerClick': MapsJsEventType.markerClick, + 'ClusterClick': MapsJsEventType.clusterClick, + 'MarkerDragStart': MapsJsEventType.markerDragStart, + 'MarkerDrag': MapsJsEventType.markerDrag, + 'MarkerDragEnd': MapsJsEventType.markerDragEnd, + 'PolylineClick': MapsJsEventType.polylineClick, + 'PolygonClick': MapsJsEventType.polygonClick, + 'CircleClick': MapsJsEventType.circleClick, + 'GroundOverlayClick': MapsJsEventType.groundOverlayClick, + }; + + /// Event types whose JS side posts an empty payload, so [MapsJsEvent] is + /// created with a `null` [MapsJsEvent.message] instead of `''`. + static const Set _payloadlessEventTypes = { + MapsJsEventType.boundsChanged, + MapsJsEventType.idle, + MapsJsEventType.tilesLoaded, + }; + + /// The underlying WebView controller. Exposed so callers can build the + /// [WebViewWidget] that hosts this bridge's JS runtime. + final WebViewController controller; + + final StreamController _events = + StreamController.broadcast(); + final Completer _pageFinished = Completer(); + + /// Broadcasts events received from the JS side. + Stream get events => _events.stream; + + /// Adds [event] to [_events], unless this bridge has already been + /// disposed. + /// + /// JS-side timers and in-flight `postMessage` calls can still invoke the + /// channel callbacks below after [dispose] closes [_events], so emission + /// must be guarded rather than left to throw on a closed controller. + void _emit(MapsJsEvent event) { + if (!_events.isClosed) { + _events.add(event); + } + } + + /// Loads the map HTML shell and wires up the JS→Dart event channels. + /// + /// Completes once the page has finished loading. + Future load() { + String path = Platform.environment['AUL_ROOT_PATH'] ?? ''; + path += '/res/flutter_assets/assets/map.html'; + controller + ..setNavigationDelegate( + NavigationDelegate( + onPageFinished: (String url) { + if (!_pageFinished.isCompleted) { + _pageFinished.complete(true); + } + }, + ), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted); + + for (final MapEntry entry + in _channelEventTypes.entries) { + final MapsJsEventType type = entry.value; + controller.addJavaScriptChannel( + entry.key, + onMessageReceived: (JavaScriptMessage message) { + _emit(( + type: type, + message: _payloadlessEventTypes.contains(type) + ? null + : message.message, + )); + }, + ); + } + + controller.loadFile(path); + + return _pageFinished.future; + } + + /// Creates the top-level `map` JS variable using [optionsJs] (a JS object + /// literal), plus its built-in map-level listeners. + Future createMap(String optionsJs) async { + final String command = + ''' + map = new google.maps.Map(document.getElementById('map'), $optionsJs); + map.addListener('bounds_changed', (event) => { BoundChanged.postMessage(''); }); + map.addListener('idle', (event) => { Idle.postMessage(''); }); + map.addListener('click', (event) => { Click.postMessage(JSON.stringify(event)); }); + map.addListener('tilesloaded', (evnet) => { Tilesloaded.postMessage(''); }); + + let longPressTimeout; + map.addListener('mousedown', (e) => { + longPressTimeout = setTimeout(() => { + LongPress.postMessage(JSON.stringify(e)); + }, $kGoogleMapsControllerLongPressDuration); + }); + map.addListener('mouseup', () => { clearTimeout(longPressTimeout); }); + map.addListener('mouseout', () => { clearTimeout(longPressTimeout); }); + + const makeClusterEvent = function(clusterManagerId, event, cluster) { + var result = '{"id": "' + clusterManagerId +'"'; + result += ', "cluster": {"count":' + cluster.count + result += ', "position":' + JSON.stringify(cluster.position) + result += ', "bounds":' + JSON.stringify(cluster.bounds); + result += ', "markers": ['; + var i = 0; + for (; i < cluster.markers.length - 1; i++) { + result += cluster.markers[i].id; + result += ', '; + } + result += cluster.markers[i].id; + result += ']}}'; + + return result; + } + '''; + await controller.runJavaScript(command); + } + + /// Creates a JS object via `new `, assigns it to + /// the JS-side variable [varName], and returns a [JsRef] handle to it. + Future createObject( + String varName, + String constructorExpression, + ) async { + await controller.runJavaScript('var $varName = $constructorExpression;'); + return JsRef(varName); + } + + /// Serializes [arg] for interpolation into a JavaScript snippet. + /// + /// [JsRef]s and [JsExpression]s are emitted as raw JS code via their + /// [toString], so they refer to JS-side variables/expressions. Plain + /// [String]s are JSON-encoded so they are safely quoted and escaped as JS + /// string literals rather than being mistaken for raw code. + String _serializeArg(Object? arg) { + if (arg is JsRef || arg is JsExpression) { + return arg.toString(); + } + if (arg is String) { + return jsonEncode(arg); + } + return arg.toString(); + } + + /// Assigns `ref[property] = value` on the JS side. + Future setProperty(JsRef ref, String property, Object? value) async { + await controller.runJavaScript( + "JSON.stringify($ref['$property'] = ${_serializeArg(value)})", + ); + } + + /// Reads `ref.property` from the JS side. + Future getProperty(JsRef ref, String property) async { + return controller.runJavaScriptReturningResult('$ref.$property'); + } + + /// Calls `ref.method(...args)` on the JS side, discarding the result. + Future callMethod(JsRef ref, String method, List args) async { + final String serializedArgs = '[${args.map(_serializeArg).join(', ')}]'; + await controller.runJavaScript( + 'JSON.stringify($ref.$method.apply($ref, $serializedArgs))', + ); + } + + /// Calls `ref.method(...args)` on the JS side and returns the result. + Future callMethodReturning( + JsRef ref, + String method, + List args, + ) async { + final String serializedArgs = '[${args.map(_serializeArg).join(', ')}]'; + return controller.runJavaScriptReturningResult( + '$ref.$method.apply($ref, $serializedArgs)', + ); + } + + /// Registers `ref.addListener(eventName, ...)` on the JS side, so that + /// [payloadJs] (a JS expression, evaluated with `event` bound to the + /// listener's callback argument) is posted to [channel] whenever it fires. + Future addListener( + JsRef ref, + String eventName, + String channel, + String payloadJs, + ) async { + await controller.runJavaScript( + "$ref.addListener('$eventName', (event) => $channel.postMessage($payloadJs));", + ); + } + + /// Escape hatch for JS not yet expressed in terms of the methods above. + Future runJavaScript(String script) async { + await controller.runJavaScript(script); + } + + /// Escape hatch for JS not yet expressed in terms of the methods above, + /// returning the raw result. + Future runJavaScriptReturningResult(String script) async { + return controller.runJavaScriptReturningResult(script); + } + + /// Releases the resources held by this bridge. + void dispose() { + _events.close(); + } +} diff --git a/packages/google_maps_flutter/lib/src/ground_overlay.dart b/packages/google_maps_flutter/lib/src/ground_overlay.dart index 94d4a7f59..0414e909b 100644 --- a/packages/google_maps_flutter/lib/src/ground_overlay.dart +++ b/packages/google_maps_flutter/lib/src/ground_overlay.dart @@ -13,10 +13,10 @@ class GroundOverlayController { GroundOverlayController({ required util.GGroundOverlay groundOverlay, ui.VoidCallback? onTap, - WebViewController? controller, - }) : _groundOverlay = groundOverlay, - tapEvent = onTap { - _addGroundOverlayEvent(controller); + required GoogleMapsJsBridge bridge, + }) : _groundOverlay = groundOverlay, + tapEvent = onTap { + _addGroundOverlayEvent(bridge); } util.GGroundOverlay? _groundOverlay; @@ -24,10 +24,13 @@ class GroundOverlayController { /// Ground overlay component's tap event. ui.VoidCallback? tapEvent; - Future _addGroundOverlayEvent(WebViewController? controller) async { - final String command = - "$_groundOverlay.addListener('click', (event) => GroundOverlayClick.postMessage(JSON.stringify(${_groundOverlay?.id})));"; - await controller!.runJavaScript(command); + Future _addGroundOverlayEvent(GoogleMapsJsBridge bridge) async { + await bridge.addListener( + JsRef(_groundOverlay.toString()), + 'click', + 'GroundOverlayClick', + 'JSON.stringify(${_groundOverlay?.id})', + ); } /// Updates the options of the wrapped [GGroundOverlay] object. diff --git a/packages/google_maps_flutter/lib/src/ground_overlays.dart b/packages/google_maps_flutter/lib/src/ground_overlays.dart index 470a50021..2d89d16e7 100644 --- a/packages/google_maps_flutter/lib/src/ground_overlays.dart +++ b/packages/google_maps_flutter/lib/src/ground_overlays.dart @@ -12,19 +12,23 @@ class GroundOverlaysController extends GeometryController { /// [GoogleMapController], and is shared with other controllers. GroundOverlaysController({ required StreamController> stream, - }) : _streamController = stream, - _groundOverlayIdToController = - {}, - _idToGroundOverlayId = {}; + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _bridge = bridge, + _groundOverlayIdToController = + {}, + _idToGroundOverlayId = {}; // A cache of [GroundOverlayController]s indexed by their [GroundOverlayId]. final Map - _groundOverlayIdToController; + _groundOverlayIdToController; final Map _idToGroundOverlayId; // The stream over which ground overlays broadcast events. final StreamController> _streamController; + final GoogleMapsJsBridge _bridge; + /// Adds a set of [GroundOverlay] objects to the cache. /// /// Wraps each [GroundOverlay] into its corresponding @@ -45,6 +49,7 @@ class GroundOverlaysController extends GeometryController { } final util.GGroundOverlay gGroundOverlay = util.GGroundOverlay( + _bridge, populationOptions, ); final GroundOverlayController controller = GroundOverlayController( @@ -52,7 +57,7 @@ class GroundOverlaysController extends GeometryController { onTap: () { _onGroundOverlayTap(groundOverlay.groundOverlayId); }, - controller: util.webController, + bridge: _bridge, ); _idToGroundOverlayId[gGroundOverlay.id] = groundOverlay.groundOverlayId; _groundOverlayIdToController[groundOverlay.groundOverlayId] = controller; diff --git a/packages/google_maps_flutter/lib/src/marker.dart b/packages/google_maps_flutter/lib/src/marker.dart index 04c3e3ce8..bbc24b3ff 100644 --- a/packages/google_maps_flutter/lib/src/marker.dart +++ b/packages/google_maps_flutter/lib/src/marker.dart @@ -17,17 +17,17 @@ class MarkerController { LatLngCallback? onDragEnd, ui.VoidCallback? onTap, ClusterManagerId? clusterManagerId, - WebViewController? controller, - }) : _marker = marker, - _infoWindow = infoWindow, - _consumeTapEvents = consumeTapEvents, - _clusterManagerId = clusterManagerId, - tapEvent = onTap, - dragStartEvent = onDragStart, - dragEvent = onDrag, - dragEndEvent = onDragEnd { - if (controller != null) { - _addMarkerEvent(controller); + GoogleMapsJsBridge? bridge, + }) : _marker = marker, + _infoWindow = infoWindow, + _consumeTapEvents = consumeTapEvents, + _clusterManagerId = clusterManagerId, + tapEvent = onTap, + dragStartEvent = onDragStart, + dragEvent = onDrag, + dragEndEvent = onDragEnd { + if (bridge != null) { + _addMarkerEvent(bridge); } } @@ -49,13 +49,14 @@ class MarkerController { /// Marker component's drag end event. LatLngCallback? dragEndEvent; - Future _addMarkerEvent(WebViewController? controller) async { - final String command = ''' + Future _addMarkerEvent(GoogleMapsJsBridge bridge) async { + final String command = + ''' $marker.addListener("click", (event) => MarkerClick.postMessage(JSON.stringify(${marker?.id}))); $marker.addListener("dragstart", (event) => MarkerDragStart.postMessage(JSON.stringify({id:${marker?.id}, event:event}))); $marker.addListener("drag", (event) => MarkerDrag.postMessage(JSON.stringify({id:${marker?.id}, event:event}))); $marker.addListener("dragend", (event) => MarkerDragEnd.postMessage(JSON.stringify({id:${marker?.id}, event:event})));'''; - await controller!.runJavaScript(command); + await bridge.runJavaScript(command); } /// Returns `true` if this Controller will use its own `onTap` handler to consume events. diff --git a/packages/google_maps_flutter/lib/src/marker_clustering.dart b/packages/google_maps_flutter/lib/src/marker_clustering.dart index 9d7cf674c..4e1d4f88d 100644 --- a/packages/google_maps_flutter/lib/src/marker_clustering.dart +++ b/packages/google_maps_flutter/lib/src/marker_clustering.dart @@ -17,17 +17,21 @@ class ClusterManagersController extends GeometryController { /// emitting map events. ClusterManagersController({ required StreamController> stream, - }) : _streamController = stream, - _idToClusterManagerId = {}, - _clusterManagerIdToMarkerClusterer = - {}; + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _bridge = bridge, + _idToClusterManagerId = {}, + _clusterManagerIdToMarkerClusterer = + {}; // The stream over which cluster managers broadcast their events final StreamController> _streamController; + final GoogleMapsJsBridge _bridge; + // A cache of [MarkerClusterer]s indexed by their [ClusterManagerId]. final Map - _clusterManagerIdToMarkerClusterer; + _clusterManagerIdToMarkerClusterer; final Map _idToClusterManagerId; /// A cache of [ClusterManagerId]s indexed by [GMarkerClusterer.id]. @@ -48,6 +52,7 @@ class ClusterManagersController extends GeometryController { ); final util.GMarkerClusterer markerClusterer = util.GMarkerClusterer( + _bridge, options, ); diff --git a/packages/google_maps_flutter/lib/src/markers.dart b/packages/google_maps_flutter/lib/src/markers.dart index 41b5e7450..e09dbe2fd 100644 --- a/packages/google_maps_flutter/lib/src/markers.dart +++ b/packages/google_maps_flutter/lib/src/markers.dart @@ -11,10 +11,12 @@ class MarkersController extends GeometryController { MarkersController({ required StreamController> stream, required ClusterManagersController clusterManagersController, - }) : _streamController = stream, - _clusterManagersController = clusterManagersController, - _idToMarkerId = {}, - _markerIdToController = {}; + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _clusterManagersController = clusterManagersController, + _bridge = bridge, + _idToMarkerId = {}, + _markerIdToController = {}; // A cache of [MarkerController]s indexed by their [MarkerId]. final Map _markerIdToController; @@ -25,6 +27,8 @@ class MarkersController extends GeometryController { final ClusterManagersController _clusterManagersController; + final GoogleMapsJsBridge _bridge; + /// Adds a set of [Marker] objects to the cache. /// /// Wraps each [Marker] into its corresponding [MarkerController]. @@ -42,14 +46,14 @@ class MarkersController extends GeometryController { util.GInfoWindow? infoWindow; if (infoWindowOptions != null) { - infoWindow = util.GInfoWindow(infoWindowOptions); + infoWindow = util.GInfoWindow(_bridge, infoWindowOptions); } final util.GMarkerOptions populationOptions = _markerOptionsFromMarker( marker, _markerIdToController[marker.markerId]?.marker, ); - final util.GMarker gMarker = util.GMarker(populationOptions); + final util.GMarker gMarker = util.GMarker(_bridge, populationOptions); if (marker.clusterManagerId != null) { _clusterManagersController.addItem(marker.clusterManagerId!, gMarker); @@ -73,7 +77,7 @@ class MarkersController extends GeometryController { onDragEnd: (LatLng latLng) { _onMarkerDragEnd(marker.markerId, latLng); }, - controller: util.webController, + bridge: _bridge, ); _idToMarkerId[gMarker.id] = marker.markerId; _markerIdToController[marker.markerId] = markerController; @@ -174,10 +178,11 @@ class MarkersController extends GeometryController { void _hideAllMarkerInfoWindow() { _markerIdToController.values .where( - (MarkerController? controller) => controller?.infoWindowShown ?? false, - ) + (MarkerController? controller) => + controller?.infoWindowShown ?? false, + ) .forEach((MarkerController controller) { - controller.hideInfoWindow(); - }); + controller.hideInfoWindow(); + }); } } diff --git a/packages/google_maps_flutter/lib/src/polygon.dart b/packages/google_maps_flutter/lib/src/polygon.dart index e15d70e43..82b43a0e7 100644 --- a/packages/google_maps_flutter/lib/src/polygon.dart +++ b/packages/google_maps_flutter/lib/src/polygon.dart @@ -12,11 +12,11 @@ class PolygonController { required util.GPolygon polygon, bool consumeTapEvents = false, ui.VoidCallback? onTap, - WebViewController? controller, - }) : _polygon = polygon, - _consumeTapEvents = consumeTapEvents, - tapEvent = onTap { - _addPolygonEvent(controller); + required GoogleMapsJsBridge bridge, + }) : _polygon = polygon, + _consumeTapEvents = consumeTapEvents, + tapEvent = onTap { + _addPolygonEvent(bridge); } util.GPolygon? _polygon; @@ -25,10 +25,13 @@ class PolygonController { /// Polygon component's tap event. ui.VoidCallback? tapEvent; - Future _addPolygonEvent(WebViewController? controller) async { - final String command = - "$_polygon.addListener('click', (event) => PolygonClick.postMessage(JSON.stringify(${_polygon?.id})));"; - await controller!.runJavaScript(command); + Future _addPolygonEvent(GoogleMapsJsBridge bridge) async { + await bridge.addListener( + JsRef(_polygon.toString()), + 'click', + 'PolygonClick', + 'JSON.stringify(${_polygon?.id})', + ); } /// Returns `true` if this Controller will use its own `onTap` handler to consume events. diff --git a/packages/google_maps_flutter/lib/src/polygons.dart b/packages/google_maps_flutter/lib/src/polygons.dart index baf7aba5f..5690f53c4 100644 --- a/packages/google_maps_flutter/lib/src/polygons.dart +++ b/packages/google_maps_flutter/lib/src/polygons.dart @@ -8,10 +8,13 @@ part of '../google_maps_flutter_tizen.dart'; /// This class manages a set of [PolygonController]s associated to a [GoogleMapController]. class PolygonsController extends GeometryController { /// Initializes the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. - PolygonsController({required StreamController> stream}) - : _streamController = stream, - _polygonIdToController = {}, - _idToPolygonId = {}; + PolygonsController({ + required StreamController> stream, + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _bridge = bridge, + _polygonIdToController = {}, + _idToPolygonId = {}; // A cache of [PolygonController]s indexed by their [PolygonId]. final Map _polygonIdToController; @@ -20,6 +23,8 @@ class PolygonsController extends GeometryController { // The stream over which polygons broadcast events final StreamController> _streamController; + final GoogleMapsJsBridge _bridge; + /// Adds a set of [Polygon] objects to the cache. /// /// Wraps each Polygon into its corresponding [PolygonController]. @@ -35,14 +40,14 @@ class PolygonsController extends GeometryController { final util.GPolygonOptions populationOptions = _polygonOptionsFromPolygon( polygon, ); - final util.GPolygon gPolygon = util.GPolygon(populationOptions); + final util.GPolygon gPolygon = util.GPolygon(_bridge, populationOptions); final PolygonController controller = PolygonController( polygon: gPolygon, consumeTapEvents: polygon.consumeTapEvents, onTap: () { _onPolygonTap(polygon.polygonId); }, - controller: util.webController, + bridge: _bridge, ); _idToPolygonId[gPolygon.id] = polygon.polygonId; _polygonIdToController[polygon.polygonId] = controller; diff --git a/packages/google_maps_flutter/lib/src/polyline.dart b/packages/google_maps_flutter/lib/src/polyline.dart index b6324761e..489a99fb1 100644 --- a/packages/google_maps_flutter/lib/src/polyline.dart +++ b/packages/google_maps_flutter/lib/src/polyline.dart @@ -12,11 +12,11 @@ class PolylineController { required util.GPolyline polyline, bool consumeTapEvents = false, ui.VoidCallback? onTap, - WebViewController? controller, - }) : _polyline = polyline, - _consumeTapEvents = consumeTapEvents, - tapEvent = onTap { - _addPolylineEvent(controller); + required GoogleMapsJsBridge bridge, + }) : _polyline = polyline, + _consumeTapEvents = consumeTapEvents, + tapEvent = onTap { + _addPolylineEvent(bridge); } util.GPolyline? _polyline; @@ -25,10 +25,13 @@ class PolylineController { /// Polyline component's tap event. ui.VoidCallback? tapEvent; - Future _addPolylineEvent(WebViewController? controller) async { - final String command = - "$_polyline.addListener('click', (event) => PolylineClick.postMessage(JSON.stringify(${_polyline?.id})));"; - await controller!.runJavaScript(command); + Future _addPolylineEvent(GoogleMapsJsBridge bridge) async { + await bridge.addListener( + JsRef(_polyline.toString()), + 'click', + 'PolylineClick', + 'JSON.stringify(${_polyline?.id})', + ); } /// Returns `true` if this Controller will use its own `onTap` handler to consume events. diff --git a/packages/google_maps_flutter/lib/src/polylines.dart b/packages/google_maps_flutter/lib/src/polylines.dart index 55d55ea0c..6d9cfe3b0 100644 --- a/packages/google_maps_flutter/lib/src/polylines.dart +++ b/packages/google_maps_flutter/lib/src/polylines.dart @@ -8,10 +8,13 @@ part of '../google_maps_flutter_tizen.dart'; /// This class manages a set of [PolylinesController]s associated to a [GoogleMapController]. class PolylinesController extends GeometryController { /// Initializes the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. - PolylinesController({required StreamController> stream}) - : _streamController = stream, - _polylineIdToController = {}, - _idToPolylineId = {}; + PolylinesController({ + required StreamController> stream, + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _bridge = bridge, + _polylineIdToController = {}, + _idToPolylineId = {}; // A cache of [PolylineController]s indexed by their [PolylineId]. final Map _polylineIdToController; @@ -20,6 +23,8 @@ class PolylinesController extends GeometryController { // The stream over which polylines broadcast their events final StreamController> _streamController; + final GoogleMapsJsBridge _bridge; + /// Adds a set of [Polyline] objects to the cache. /// /// Wraps each line into its corresponding [PolylineController]. @@ -35,14 +40,14 @@ class PolylinesController extends GeometryController { final util.GPolylineOptions polylineOptions = _polylineOptionsFromPolyline( polyline, ); - final util.GPolyline gPolyline = util.GPolyline(polylineOptions); + final util.GPolyline gPolyline = util.GPolyline(_bridge, polylineOptions); final PolylineController controller = PolylineController( polyline: gPolyline, consumeTapEvents: polyline.consumeTapEvents, onTap: () { _onPolylineTap(polyline.polylineId); }, - controller: util.webController, + bridge: _bridge, ); _idToPolylineId[gPolyline.id] = polyline.polylineId; _polylineIdToController[polyline.polylineId] = controller; diff --git a/packages/google_maps_flutter/lib/src/util.dart b/packages/google_maps_flutter/lib/src/util.dart index 4e8b7fdc7..daff8d4c2 100644 --- a/packages/google_maps_flutter/lib/src/util.dart +++ b/packages/google_maps_flutter/lib/src/util.dart @@ -6,8 +6,11 @@ // ignore_for_file: avoid_setters_without_getters import 'dart:async'; +import 'dart:convert'; + import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'package:webview_flutter/webview_flutter.dart'; + +import 'google_maps_js_bridge.dart'; /// Default LatLng. const LatLng nullLatLng = LatLng(0, 0); @@ -141,21 +144,24 @@ class GInfoWindowOptions { final String pos = position != null ? '{lat:${position?.latitude}, lng:${position?.longitude}}' : 'null'; - return '{content:$content, pixelOffset:null , position:$pos, zIndex:$zIndex}'; + final String contentJs = content != null ? jsonEncode(content) : 'null'; + return '{content:$contentJs, pixelOffset:null , position:$pos, zIndex:$zIndex}'; } } /// This class represents GMarker's InfoWindow. class GInfoWindow { /// GInfoWindow Constructor. - GInfoWindow(GInfoWindowOptions? opts) : _id = _gid++ { + GInfoWindow(GoogleMapsJsBridge bridge, GInfoWindowOptions? opts) + : _bridge = bridge, + _id = _gid++ { _createInfoWindow(opts); } + final GoogleMapsJsBridge _bridge; + Future _createInfoWindow(GInfoWindowOptions? opts) async { - await webController!.runJavaScript( - 'var ${toString()} = new google.maps.InfoWindow($opts);', - ); + await _bridge.createObject(toString(), 'new google.maps.InfoWindow($opts)'); } final int _id; @@ -167,7 +173,7 @@ class GInfoWindow { } Future _callCloseInfoWindow() async { - await webController!.runJavaScript('${toString()}.close();'); + await _bridge.runJavaScript('${toString()}.close();'); } /// Opens InfoWindow on the given map. @@ -176,9 +182,7 @@ class GInfoWindow { } Future _callOpenInfoWindow(GMarker? anchor) async { - await webController!.runJavaScript( - '${toString()}.open({anchor: $anchor, map});', - ); + await _bridge.runJavaScript('${toString()}.open({anchor: $anchor, map});'); } @override @@ -193,27 +197,36 @@ class GInfoWindow { set pixelOffset(GSize? size) => _setPixelOffset(size); Future _setContent(Object? /*String?|Node?*/ content) async { - await callMethod(this, 'setContent', [content]); + await _bridge.callMethod(JsRef(toString()), 'setContent', [ + content, + ]); } Future _setPixelOffset(GSize? size) async { - await setProperty(this, 'pixelOffset', size?.toValue()); + await _bridge.setProperty( + JsRef(toString()), + 'pixelOffset', + size != null ? JsExpression(size.toValue()) : null, + ); } } /// This class represents a geographical location on the map as a Marker. class GMarker { /// GMarker Constructor. - GMarker([GMarkerOptions? opts]) - : id = _gid++, - _options = opts { + GMarker(GoogleMapsJsBridge bridge, [GMarkerOptions? opts]) + : _bridge = bridge, + id = _gid++, + _options = opts { _createMarker(opts); } + final GoogleMapsJsBridge _bridge; + Future _createMarker(GMarkerOptions? opts) async { final String command = 'var ${toString()} = new google.maps.Marker($opts); ${toString()}.id = $id;'; - await webController!.runJavaScript(command); + await _bridge.runJavaScript(command); } /// GMarker id. @@ -274,41 +287,48 @@ class GMarker { } Future _setMap(Object? /*GMap?|StreetViewPanorama?*/ map) async { - await callMethod(this, 'setMap', [map]); + final Object? mapArg = map is String ? JsExpression(map) : map; + await _bridge.callMethod(JsRef(toString()), 'setMap', [mapArg]); } Future _setOptions(GMarkerOptions? options) async { - await callMethod(this, 'setOptions', [options]); + await _bridge.callMethod(JsRef(toString()), 'setOptions', [ + options, + ]); } Future _setVisible(bool? visible) async { - await callMethod(this, 'setVisible', [visible]); + await _bridge.callMethod(JsRef(toString()), 'setVisible', [visible]); } Future _setDraggable(bool? visible) async { - await callMethod(this, 'setDraggable', [visible]); + await _bridge.callMethod(JsRef(toString()), 'setDraggable', [ + visible, + ]); } Future _setIcon(Object? icon) async { - await callMethod(this, 'setIcon', [icon]); + await _bridge.callMethod(JsRef(toString()), 'setIcon', [icon]); } Future _setOpacity(num? opacity) async { - await callMethod(this, 'setOpacity', [opacity]); + await _bridge.callMethod(JsRef(toString()), 'setOpacity', [opacity]); } Future _setPosition(LatLng? position) async { - await callMethod(this, 'setPosition', [ - 'new google.maps.LatLng(${position!.latitude},${position.longitude})', + await _bridge.callMethod(JsRef(toString()), 'setPosition', [ + JsExpression( + 'new google.maps.LatLng(${position!.latitude},${position.longitude})', + ), ]); } Future _setTitle(String? title) async { - await callMethod(this, 'setTitle', [title]); + await _bridge.callMethod(JsRef(toString()), 'setTitle', [title]); } Future _setZIndex(num? zIndex) async { - await callMethod(this, 'setZIndex', [zIndex]); + await _bridge.callMethod(JsRef(toString()), 'setZIndex', [zIndex]); } } @@ -316,14 +336,16 @@ class GMarker { /// map. class GPolyline { /// GPolyline Constructor. - GPolyline([GPolylineOptions? opts]) : id = _gid++ { + GPolyline(GoogleMapsJsBridge bridge, [GPolylineOptions? opts]) + : _bridge = bridge, + id = _gid++ { _createPolyline(opts); } + final GoogleMapsJsBridge _bridge; + Future _createPolyline(GPolylineOptions? opts) async { - await webController!.runJavaScript( - 'var ${toString()} = new google.maps.Polyline($opts);', - ); + await _bridge.createObject(toString(), 'new google.maps.Polyline($opts)'); } /// GPolyline id. @@ -347,15 +369,20 @@ class GPolyline { } Future _setVisible(bool? visible) async { - await callMethod(this, 'setVisible', [visible]); + await _bridge.callMethod(JsRef(toString()), 'setVisible', [visible]); } Future _setMap(Object? /*GMap?|StreetViewPanorama?*/ map) async { - await callMethod(this, 'setMap', [map]); + final Object? mapArg = map is String ? JsExpression(map) : map; + await _bridge.callMethod(JsRef(toString()), 'setMap', [mapArg]); } Future _setOptions(GPolylineOptions? options) async { - await callMethod(this, 'setOptions', [options]); + await _bridge.callMethod( + JsRef(toString()), + 'setOptions', + [options], + ); } } @@ -406,14 +433,16 @@ class GPolylineOptions { /// connected coordinates in an ordered sequence. class GPolygon { /// GPolygon Constructor. - GPolygon([GPolygonOptions? opts]) : id = _gid++ { + GPolygon(GoogleMapsJsBridge bridge, [GPolygonOptions? opts]) + : _bridge = bridge, + id = _gid++ { _createPolygon(opts); } + final GoogleMapsJsBridge _bridge; + Future _createPolygon(GPolygonOptions? opts) async { - await webController!.runJavaScript( - 'var ${toString()} = new google.maps.Polygon($opts);', - ); + await _bridge.createObject(toString(), 'new google.maps.Polygon($opts)'); } /// GPolygon id. @@ -437,15 +466,20 @@ class GPolygon { } Future _setVisible(bool? visible) async { - await callMethod(this, 'setVisible', [visible]); + await _bridge.callMethod(JsRef(toString()), 'setVisible', [visible]); } Future _setMap(Object? /*GMap?|StreetViewPanorama?*/ map) async { - await callMethod(this, 'setMap', [map]); + final Object? mapArg = map is String ? JsExpression(map) : map; + await _bridge.callMethod(JsRef(toString()), 'setMap', [mapArg]); } Future _setOptions(GPolygonOptions? options) async { - await callMethod(this, 'setOptions', [options]); + await _bridge.callMethod( + JsRef(toString()), + 'setOptions', + [options], + ); } } @@ -506,14 +540,16 @@ class GPolygonOptions { /// This class represents a circle using the passed GCircleOptions. class GCircle { /// GCircle Constructor. - GCircle([GCircleOptions? opts]) : id = _gid++ { + GCircle(GoogleMapsJsBridge bridge, [GCircleOptions? opts]) + : _bridge = bridge, + id = _gid++ { _createCircle(opts); } + final GoogleMapsJsBridge _bridge; + Future _createCircle(GCircleOptions? opts) async { - await webController!.runJavaScript( - 'var ${toString()} = new google.maps.Circle($opts);', - ); + await _bridge.createObject(toString(), 'new google.maps.Circle($opts)'); } /// GCircle id. @@ -522,7 +558,7 @@ class GCircle { @override String toString() { - return 'polygon$id'; + return 'circle$id'; } /// Sets if the circle is visible. @@ -540,19 +576,22 @@ class GCircle { } Future _setVisible(bool? visible) async { - await callMethod(this, 'setVisible', [visible]); + await _bridge.callMethod(JsRef(toString()), 'setVisible', [visible]); } Future _setRadius(num? radius) async { - await callMethod(this, 'setRadius', [radius]); + await _bridge.callMethod(JsRef(toString()), 'setRadius', [radius]); } Future _setMap(Object? /*GMap?|StreetViewPanorama?*/ map) async { - await callMethod(this, 'setMap', [map]); + final Object? mapArg = map is String ? JsExpression(map) : map; + await _bridge.callMethod(JsRef(toString()), 'setMap', [mapArg]); } Future _setOptions(GCircleOptions? options) async { - await callMethod(this, 'setOptions', [options]); + await _bridge.callMethod(JsRef(toString()), 'setOptions', [ + options, + ]); } } @@ -599,14 +638,18 @@ class GCircleOptions { /// The [GMarkerClusterer] object used to cluster markers on the map. class GMarkerClusterer { /// GMarkerCluster Constructor. - GMarkerClusterer([GMarkerClustererOptions? opts]) : id = _gid++ { + GMarkerClusterer(GoogleMapsJsBridge bridge, [GMarkerClustererOptions? opts]) + : _bridge = bridge, + id = _gid++ { _createMarkerClusterer(opts); } + final GoogleMapsJsBridge _bridge; + void _createMarkerClusterer(GMarkerClustererOptions? opts) { final String command = 'var ${toString()} = new markerClusterer.MarkerClusterer($opts);'; - webController!.runJavaScript(command); + _bridge.runJavaScript(command); } /// GCircle id. @@ -615,14 +658,16 @@ class GMarkerClusterer { /// Adds a marker to be clustered by the [GMarkerClusterer]. void addMarker(GMarker marker, bool? noDraw) { - webController!.runJavaScript('${toString()}.addMarker($marker, $noDraw);'); + _bridge.runJavaScript('${toString()}.addMarker($marker, $noDraw);'); } /// Removes a marker from the [GMarkerClusterer]. Future removeMarker(GMarker marker, bool? noDraw) async { - final bool result = await webController!.runJavaScriptReturningResult( - '${toString()}.removeMarker($marker, $noDraw);', - ) as bool; + final bool result = + await _bridge.runJavaScriptReturningResult( + '${toString()}.removeMarker($marker, $noDraw);', + ) + as bool; return result; } @@ -631,42 +676,42 @@ class GMarkerClusterer { void addMarkers(List? markers, bool? noDraw) { final String command = 'JSON.stringify($this.addMarkers.call($this, $markers, $noDraw))'; - webController!.runJavaScript(command); + _bridge.runJavaScript(command); } /// Removes a list of markers from the [GMarkerClusterer]. bool removeMarkers(List? markers, bool? noDraw) { final String command = 'JSON.stringify($this.removeMarkers.call($this, $markers, $noDraw))'; - return webController!.runJavaScriptReturningResult(command) as bool; + return _bridge.runJavaScriptReturningResult(command) as bool; } /// Clears all the markers from the [GMarkerClusterer]. void clearMarkers(bool? noDraw) { - webController!.runJavaScript('${toString()}.clearMarkers($noDraw);'); + _bridge.runJavaScript('${toString()}.clearMarkers($noDraw);'); } /// Returns the list of clusters. List> get clusters { final List> results = - webController!.runJavaScriptReturningResult('${toString()}.clusters') + _bridge.runJavaScriptReturningResult('${toString()}.clusters') as List>; return results; } /// Called when the [GMarkerClusterer] is added to the map. void onAdd() { - webController!.runJavaScript('${toString()}.onAdd();'); + _bridge.runJavaScript('${toString()}.onAdd();'); } /// Called when the [MarkerClusterer] is removed from the map. void onRemove() { - webController!.runJavaScript('${toString()}.onRemove();'); + _bridge.runJavaScript('${toString()}.onRemove();'); } /// Recalculates and draws all the marker clusters. void render() { - webController!.runJavaScript('${toString()}.render();'); + _bridge.runJavaScript('${toString()}.render();'); } @override @@ -713,19 +758,22 @@ class GMarkerClustererOptions { /// against the Earth's surface. class GGroundOverlay { /// GGroundOverlay Constructor. - GGroundOverlay([GGroundOverlayOptions? opts]) - : id = _gid++, - _options = opts { + GGroundOverlay(GoogleMapsJsBridge bridge, [GGroundOverlayOptions? opts]) + : _bridge = bridge, + id = _gid++, + _options = opts { _createGroundOverlay(opts); } + final GoogleMapsJsBridge _bridge; + Future _createGroundOverlay(GGroundOverlayOptions? opts) async { final String url = opts?.url ?? "''"; final String bounds = opts?.bounds ?? '{}'; final String command = 'var ${toString()} = new google.maps.GroundOverlay($url, $bounds, $opts);' ' ${toString()}.id = $id;'; - await webController!.runJavaScript(command); + await _bridge.runJavaScript(command); } /// GGroundOverlay id. @@ -766,11 +814,12 @@ class GGroundOverlay { } Future _setMap(Object? map) async { - await callMethod(this, 'setMap', [map]); + final Object? mapArg = map is String ? JsExpression(map) : map; + await _bridge.callMethod(JsRef(toString()), 'setMap', [mapArg]); } Future _setOpacity(num? opacity) async { - await callMethod(this, 'setOpacity', [opacity]); + await _bridge.callMethod(JsRef(toString()), 'setOpacity', [opacity]); } } @@ -802,18 +851,3 @@ class GGroundOverlayOptions { ' map: ${visible == false ? 'null' : 'map'}}'; } } - -/// Returns webview controller instance -WebViewController? webController; - -/// Sets the value to property of the object. -Future setProperty(Object o, String property, Object? value) async { - final String command = "JSON.stringify($o['$property'] = $value)"; - await webController!.runJavaScript(command); -} - -/// Calls the method of the object with the args. -Future callMethod(Object o, String method, List args) async { - final String command = 'JSON.stringify($o.$method.apply($o, $args))'; - await webController!.runJavaScript(command); -} diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index 75b2e7220..6a04dd2a6 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/pubspec.yaml @@ -2,11 +2,11 @@ name: google_maps_flutter_tizen description: Tizen implementation of the google_maps_flutter plugin. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/google_maps_flutter -version: 0.1.14 +version: 0.2.0 environment: - sdk: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + sdk: ^3.8.0 + flutter: ">=3.32.0" flutter: plugin: @@ -19,5 +19,5 @@ dependencies: sdk: flutter google_maps_flutter_platform_interface: ^2.15.0 stream_transform: ^2.0.0 - webview_flutter: ^4.10.0 - webview_flutter_lwe: ^0.3.7 + webview_flutter: ^4.13.1 + webview_flutter_lwe: ^0.5.3