diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index c14478638..4c4b3b6e1 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.1.15 + +* Refactor the JavaScript interaction layer by introducing `GoogleMapsJsBridge`, + replacing direct `WebViewController` calls. +* Update `webview_flutter` to ^4.13.1 and `webview_flutter_lwe` to ^0.5.0. +* 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..285e729a5 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.1.15 ``` 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..52ee24d69 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, + required GoogleMapsJsBridge bridge, }) : _circle = circle, _consumeTapEvents = consumeTapEvents, tapEvent = onTap { - _addCircleEvent(controller); + _addCircleEvent(bridge); } util.GCircle? _circle; @@ -25,10 +25,10 @@ class CircleController { /// Circle component's tap event. ui.VoidCallback? tapEvent; - Future _addCircleEvent(WebViewController? controller) async { + Future _addCircleEvent(GoogleMapsJsBridge bridge) async { final String command = "$_circle.addListener('click', (event) => CircleClick.postMessage(JSON.stringify(${_circle?.id})));"; - 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/circles.dart b/packages/google_maps_flutter/lib/src/circles.dart index d2ec6b955..07127827c 100644 --- a/packages/google_maps_flutter/lib/src/circles.dart +++ b/packages/google_maps_flutter/lib/src/circles.dart @@ -8,8 +8,11 @@ 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, + CirclesController({ + required StreamController> stream, + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _bridge = bridge, _circleIdToController = {}, _idToCircleId = {}; @@ -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/google_maps_controller.dart b/packages/google_maps_flutter/lib/src/google_maps_controller.dart index 53e7c4ab8..f2d7559ca 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. @@ -34,18 +31,30 @@ class GoogleMapsController { _clusterManagers = clusterManagers, _groundOverlays = groundOverlays, _rawMapOptions = mapOptions { - _circlesController = CirclesController(stream: _streamController); - _polygonsController = PolygonsController(stream: _streamController); - _polylinesController = PolylinesController(stream: _streamController); + _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 = WebViewGoogleMapsJsBridge(); /// The Flutter widget that will contain the rendered Map. Used for caching. WebViewWidget? get webview => _webview; @@ -76,7 +85,7 @@ class GoogleMapsController { /// Returns min-max zoom levels. Test only. @visibleForTesting Future getMinMaxZoomLevels() async { - final String value = await controller.runJavaScriptReturningResult( + final String value = await _bridge.runJavaScriptReturningResult( 'JSON.stringify([map.minZoom, map.maxZoom])', ) as String; final dynamic bound = json.decode(value); @@ -100,7 +109,7 @@ class GoogleMapsController { /// Returns if zoomGestures property is enabled. Test only. @visibleForTesting Future isZoomGesturesEnabled() async { - final String value = await controller + final String value = await _bridge .runJavaScriptReturningResult('map.gestureHandling') as String; return value != 'none'; } @@ -108,15 +117,15 @@ class GoogleMapsController { /// 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 + final String value = await _bridge .runJavaScriptReturningResult('map.gestureHandling') as String; return value != 'none'; } @@ -128,88 +137,45 @@ class GoogleMapsController { } 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); + _bridge.events.listen(_onJsEvent); + _webview = WebViewWidget(controller: _bridge.controller); + } + + Future _onJsEvent(MapsJsEvent event) async { + switch (event) { + case BoundsChangedJsEvent(): + await _onBoundsChanged(); + case IdleJsEvent(): + _onIdle(); + case TilesLoadedJsEvent(): + _onTilesloaded(); + case ClickJsEvent(message: final String message): + _onClick(message); + case LongPressJsEvent(message: final String message): + _onLongPress(message); + case MarkerClickJsEvent(message: final String message): + _onMarkerClick(message); + case ClusterClickJsEvent(message: final String message): + _onClusterClick(message); + case MarkerDragStartJsEvent(message: final String message): + _onMarkerDragStart(message); + case MarkerDragJsEvent(message: final String message): + _onMarkerDrag(message); + case MarkerDragEndJsEvent(message: final String message): + _onMarkerDragEnd(message); + case PolylineClickJsEvent(message: final String message): + _onPolylineClick(message); + case PolygonClickJsEvent(message: final String message): + _onPolygonClick(message); + case CircleClickJsEvent(message: final String message): + _onCircleClick(message); + case GroundOverlayClickJsEvent(message: final String message): + _onGroundOverlayClick(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 +203,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 +222,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 +239,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 +255,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 +271,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 +294,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 +310,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 +336,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 +362,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 +388,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 +405,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 +421,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 +437,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 +464,7 @@ class GoogleMapsController { Future init() async { if (_webview == null && !_streamController.isClosed) { _getWebview(); - await _pageFinishedCompleter.future; + await _bridge.load(); await _createMap(); } await _attachGeometryControllers(); @@ -551,7 +517,6 @@ class GoogleMapsController { _clusterManagersController!.bindToMap(_mapId, _webview!); _groundOverlaysController!.bindToMap(_mapId, _webview!); - util.webController = controller; _controllersBoundToMap = true; } @@ -604,11 +569,11 @@ 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. @@ -626,39 +591,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 +628,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, ); } @@ -768,7 +728,7 @@ class GoogleMapsController { JSON.stringify(getPixelToLatLng()); '''; - return await controller.runJavaScriptReturningResult(command) as String; + return await _bridge.runJavaScriptReturningResult(command) as String; } Future _latLngToPoint(LatLng latLng) async { @@ -788,12 +748,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 +861,7 @@ class GoogleMapsController { /// You won't be able to call many of the methods on this controller after /// calling `dispose`! void dispose() { + _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..6e0c75e2c --- /dev/null +++ b/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart @@ -0,0 +1,427 @@ +// Copyright 2021 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: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; +} + +/// Base class for events dispatched from the Google Maps JavaScript runtime +/// back into Dart through a [GoogleMapsJsBridge]. +abstract class MapsJsEvent { + /// Const constructor for subclasses. + const MapsJsEvent(); +} + +/// Fired when the map's `bounds_changed` listener fires. +class BoundsChangedJsEvent extends MapsJsEvent { + /// Creates a [BoundsChangedJsEvent]. + const BoundsChangedJsEvent(); +} + +/// Fired when the map's `idle` listener fires. +class IdleJsEvent extends MapsJsEvent { + /// Creates an [IdleJsEvent]. + const IdleJsEvent(); +} + +/// Fired when the map's `tilesloaded` listener fires. +class TilesLoadedJsEvent extends MapsJsEvent { + /// Creates a [TilesLoadedJsEvent]. + const TilesLoadedJsEvent(); +} + +/// Base class for [MapsJsEvent]s that carry a raw JSON [message] payload. +abstract class MessageJsEvent extends MapsJsEvent { + /// Creates a [MessageJsEvent] carrying the raw [message] payload. + const MessageJsEvent(this.message); + + /// The raw (JSON-encoded) message payload posted from the JS side. + final String message; +} + +/// Fired when the map is clicked. +class ClickJsEvent extends MessageJsEvent { + /// Creates a [ClickJsEvent]. + const ClickJsEvent(super.message); +} + +/// Fired when the map is long-pressed. +class LongPressJsEvent extends MessageJsEvent { + /// Creates a [LongPressJsEvent]. + const LongPressJsEvent(super.message); +} + +/// Fired when a marker is clicked. +class MarkerClickJsEvent extends MessageJsEvent { + /// Creates a [MarkerClickJsEvent]. + const MarkerClickJsEvent(super.message); +} + +/// Fired when a marker cluster is clicked. +class ClusterClickJsEvent extends MessageJsEvent { + /// Creates a [ClusterClickJsEvent]. + const ClusterClickJsEvent(super.message); +} + +/// Fired when a marker drag starts. +class MarkerDragStartJsEvent extends MessageJsEvent { + /// Creates a [MarkerDragStartJsEvent]. + const MarkerDragStartJsEvent(super.message); +} + +/// Fired while a marker is being dragged. +class MarkerDragJsEvent extends MessageJsEvent { + /// Creates a [MarkerDragJsEvent]. + const MarkerDragJsEvent(super.message); +} + +/// Fired when a marker drag ends. +class MarkerDragEndJsEvent extends MessageJsEvent { + /// Creates a [MarkerDragEndJsEvent]. + const MarkerDragEndJsEvent(super.message); +} + +/// Fired when a polyline is clicked. +class PolylineClickJsEvent extends MessageJsEvent { + /// Creates a [PolylineClickJsEvent]. + const PolylineClickJsEvent(super.message); +} + +/// Fired when a polygon is clicked. +class PolygonClickJsEvent extends MessageJsEvent { + /// Creates a [PolygonClickJsEvent]. + const PolygonClickJsEvent(super.message); +} + +/// Fired when a circle is clicked. +class CircleClickJsEvent extends MessageJsEvent { + /// Creates a [CircleClickJsEvent]. + const CircleClickJsEvent(super.message); +} + +/// Fired when a ground overlay is clicked. +class GroundOverlayClickJsEvent extends MessageJsEvent { + /// Creates a [GroundOverlayClickJsEvent]. + const GroundOverlayClickJsEvent(super.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. +abstract class GoogleMapsJsBridge { + /// The underlying WebView controller. Exposed so callers can build the + /// [WebViewWidget] that hosts this bridge's JS runtime. + WebViewController get controller; + + /// Broadcasts events received from the JS side. + Stream get events; + + /// Loads the map HTML shell and wires up the JS→Dart event channels. + /// + /// Completes once the page has finished loading. + Future load(); + + /// Creates the top-level `map` JS variable using [optionsJs] (a JS object + /// literal), plus its built-in map-level listeners. + Future createMap(String optionsJs); + + /// 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); + + /// Assigns `ref[property] = value` on the JS side. + Future setProperty(JsRef ref, String property, Object? value); + + /// Reads `ref.property` from the JS side. + Future getProperty(JsRef ref, String property); + + /// Calls `ref.method(...args)` on the JS side, discarding the result. + Future callMethod(JsRef ref, String method, List args); + + /// Calls `ref.method(...args)` on the JS side and returns the result. + Future callMethodReturning( + JsRef ref, + String method, + List args, + ); + + /// 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, + ); + + /// Escape hatch for JS not yet expressed in terms of the methods above. + Future runJavaScript(String script); + + /// Escape hatch for JS not yet expressed in terms of the methods above, + /// returning the raw result. + Future runJavaScriptReturningResult(String script); + + /// Releases the resources held by this bridge. + void dispose(); +} + +/// A [GoogleMapsJsBridge] backed by a [WebViewController]. +class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { + /// Creates a bridge over [controller], or a fresh [WebViewController] if + /// none is provided. + WebViewGoogleMapsJsBridge({WebViewController? controller}) + : controller = controller ?? WebViewController(); + + @override + final WebViewController controller; + + final StreamController _events = + StreamController.broadcast(); + final Completer _pageFinished = Completer(); + + @override + Stream get events => _events.stream; + + @override + 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) + ..addJavaScriptChannel( + 'BoundChanged', + onMessageReceived: (JavaScriptMessage message) { + _events.add(const BoundsChangedJsEvent()); + }, + ) + ..addJavaScriptChannel( + 'Idle', + onMessageReceived: (JavaScriptMessage message) { + _events.add(const IdleJsEvent()); + }, + ) + ..addJavaScriptChannel( + 'Tilesloaded', + onMessageReceived: (JavaScriptMessage message) { + _events.add(const TilesLoadedJsEvent()); + }, + ) + ..addJavaScriptChannel( + 'Click', + onMessageReceived: (JavaScriptMessage message) { + _events.add(ClickJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'LongPress', + onMessageReceived: (JavaScriptMessage message) { + _events.add(LongPressJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'MarkerClick', + onMessageReceived: (JavaScriptMessage message) { + _events.add(MarkerClickJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'ClusterClick', + onMessageReceived: (JavaScriptMessage message) { + _events.add(ClusterClickJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'MarkerDragStart', + onMessageReceived: (JavaScriptMessage message) { + _events.add(MarkerDragStartJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'MarkerDrag', + onMessageReceived: (JavaScriptMessage message) { + _events.add(MarkerDragJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'MarkerDragEnd', + onMessageReceived: (JavaScriptMessage message) { + _events.add(MarkerDragEndJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'PolylineClick', + onMessageReceived: (JavaScriptMessage message) { + _events.add(PolylineClickJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'PolygonClick', + onMessageReceived: (JavaScriptMessage message) { + _events.add(PolygonClickJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'CircleClick', + onMessageReceived: (JavaScriptMessage message) { + _events.add(CircleClickJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'GroundOverlayClick', + onMessageReceived: (JavaScriptMessage message) { + _events.add(GroundOverlayClickJsEvent(message.message)); + }, + ) + ..loadFile(path); + + return _pageFinished.future; + } + + @override + 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); + } + + @override + Future createObject( + String varName, + String constructorExpression, + ) async { + await controller.runJavaScript('var $varName = $constructorExpression;'); + return JsRef(varName); + } + + @override + Future setProperty( + JsRef ref, + String property, + Object? value, + ) async { + await controller.runJavaScript( + "JSON.stringify($ref['$property'] = $value)", + ); + } + + @override + Future getProperty(JsRef ref, String property) async { + return controller.runJavaScriptReturningResult('$ref.$property'); + } + + @override + Future callMethod( + JsRef ref, + String method, + List args, + ) async { + await controller.runJavaScript( + 'JSON.stringify($ref.$method.apply($ref, $args))', + ); + } + + @override + Future callMethodReturning( + JsRef ref, + String method, + List args, + ) async { + return controller.runJavaScriptReturningResult( + '$ref.$method.apply($ref, $args)', + ); + } + + @override + Future addListener( + JsRef ref, + String eventName, + String channel, + String payloadJs, + ) async { + await controller.runJavaScript( + "$ref.addListener('$eventName', (event) => $channel.postMessage($payloadJs));", + ); + } + + @override + Future runJavaScript(String script) async { + await controller.runJavaScript(script); + } + + @override + Future runJavaScriptReturningResult(String script) async { + return controller.runJavaScriptReturningResult(script); + } + + @override + 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..a0f329915 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, + required GoogleMapsJsBridge bridge, }) : _groundOverlay = groundOverlay, tapEvent = onTap { - _addGroundOverlayEvent(controller); + _addGroundOverlayEvent(bridge); } util.GGroundOverlay? _groundOverlay; @@ -24,10 +24,10 @@ class GroundOverlayController { /// Ground overlay component's tap event. ui.VoidCallback? tapEvent; - Future _addGroundOverlayEvent(WebViewController? controller) async { + Future _addGroundOverlayEvent(GoogleMapsJsBridge bridge) async { final String command = "$_groundOverlay.addListener('click', (event) => GroundOverlayClick.postMessage(JSON.stringify(${_groundOverlay?.id})));"; - await controller!.runJavaScript(command); + await bridge.runJavaScript(command); } /// 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..a7b8254e2 100644 --- a/packages/google_maps_flutter/lib/src/ground_overlays.dart +++ b/packages/google_maps_flutter/lib/src/ground_overlays.dart @@ -12,7 +12,9 @@ class GroundOverlaysController extends GeometryController { /// [GoogleMapController], and is shared with other controllers. GroundOverlaysController({ required StreamController> stream, + required GoogleMapsJsBridge bridge, }) : _streamController = stream, + _bridge = bridge, _groundOverlayIdToController = {}, _idToGroundOverlayId = {}; @@ -25,6 +27,8 @@ class GroundOverlaysController extends GeometryController { // 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..d5b23b534 100644 --- a/packages/google_maps_flutter/lib/src/marker.dart +++ b/packages/google_maps_flutter/lib/src/marker.dart @@ -17,7 +17,7 @@ class MarkerController { LatLngCallback? onDragEnd, ui.VoidCallback? onTap, ClusterManagerId? clusterManagerId, - WebViewController? controller, + GoogleMapsJsBridge? bridge, }) : _marker = marker, _infoWindow = infoWindow, _consumeTapEvents = consumeTapEvents, @@ -26,8 +26,8 @@ class MarkerController { dragStartEvent = onDragStart, dragEvent = onDrag, dragEndEvent = onDragEnd { - if (controller != null) { - _addMarkerEvent(controller); + if (bridge != null) { + _addMarkerEvent(bridge); } } @@ -49,13 +49,13 @@ class MarkerController { /// Marker component's drag end event. LatLngCallback? dragEndEvent; - Future _addMarkerEvent(WebViewController? controller) async { + 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..dc3271cf9 100644 --- a/packages/google_maps_flutter/lib/src/marker_clustering.dart +++ b/packages/google_maps_flutter/lib/src/marker_clustering.dart @@ -17,7 +17,9 @@ class ClusterManagersController extends GeometryController { /// emitting map events. ClusterManagersController({ required StreamController> stream, + required GoogleMapsJsBridge bridge, }) : _streamController = stream, + _bridge = bridge, _idToClusterManagerId = {}, _clusterManagerIdToMarkerClusterer = {}; @@ -25,6 +27,8 @@ class ClusterManagersController extends GeometryController { // 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; @@ -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..6595c962a 100644 --- a/packages/google_maps_flutter/lib/src/markers.dart +++ b/packages/google_maps_flutter/lib/src/markers.dart @@ -11,8 +11,10 @@ class MarkersController extends GeometryController { MarkersController({ required StreamController> stream, required ClusterManagersController clusterManagersController, + required GoogleMapsJsBridge bridge, }) : _streamController = stream, _clusterManagersController = clusterManagersController, + _bridge = bridge, _idToMarkerId = {}, _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; diff --git a/packages/google_maps_flutter/lib/src/polygon.dart b/packages/google_maps_flutter/lib/src/polygon.dart index e15d70e43..217a49249 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, + required GoogleMapsJsBridge bridge, }) : _polygon = polygon, _consumeTapEvents = consumeTapEvents, tapEvent = onTap { - _addPolygonEvent(controller); + _addPolygonEvent(bridge); } util.GPolygon? _polygon; @@ -25,10 +25,10 @@ class PolygonController { /// Polygon component's tap event. ui.VoidCallback? tapEvent; - Future _addPolygonEvent(WebViewController? controller) async { + Future _addPolygonEvent(GoogleMapsJsBridge bridge) async { final String command = "$_polygon.addListener('click', (event) => PolygonClick.postMessage(JSON.stringify(${_polygon?.id})));"; - 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/polygons.dart b/packages/google_maps_flutter/lib/src/polygons.dart index baf7aba5f..4fd1f2222 100644 --- a/packages/google_maps_flutter/lib/src/polygons.dart +++ b/packages/google_maps_flutter/lib/src/polygons.dart @@ -8,8 +8,11 @@ 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, + PolygonsController({ + required StreamController> stream, + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _bridge = bridge, _polygonIdToController = {}, _idToPolygonId = {}; @@ -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..9269eadc0 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, + required GoogleMapsJsBridge bridge, }) : _polyline = polyline, _consumeTapEvents = consumeTapEvents, tapEvent = onTap { - _addPolylineEvent(controller); + _addPolylineEvent(bridge); } util.GPolyline? _polyline; @@ -25,10 +25,10 @@ class PolylineController { /// Polyline component's tap event. ui.VoidCallback? tapEvent; - Future _addPolylineEvent(WebViewController? controller) async { + Future _addPolylineEvent(GoogleMapsJsBridge bridge) async { final String command = "$_polyline.addListener('click', (event) => PolylineClick.postMessage(JSON.stringify(${_polyline?.id})));"; - 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/polylines.dart b/packages/google_maps_flutter/lib/src/polylines.dart index 55d55ea0c..94973d36a 100644 --- a/packages/google_maps_flutter/lib/src/polylines.dart +++ b/packages/google_maps_flutter/lib/src/polylines.dart @@ -8,8 +8,11 @@ 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, + PolylinesController({ + required StreamController> stream, + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _bridge = bridge, _polylineIdToController = {}, _idToPolylineId = {}; @@ -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..4031dd5af 100644 --- a/packages/google_maps_flutter/lib/src/util.dart +++ b/packages/google_maps_flutter/lib/src/util.dart @@ -7,7 +7,8 @@ import 'dart:async'; 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); @@ -148,13 +149,18 @@ class GInfoWindowOptions { /// 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)', ); } @@ -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?.toValue(), + ); } } /// This class represents a geographical location on the map as a Marker. class GMarker { /// GMarker Constructor. - GMarker([GMarkerOptions? opts]) - : id = _gid++, + 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,49 @@ class GMarker { } Future _setMap(Object? /*GMap?|StreetViewPanorama?*/ map) async { - await callMethod(this, 'setMap', [map]); + await _bridge.callMethod(JsRef(toString()), 'setMap', [map]); } 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', [ + await _bridge.callMethod(JsRef(toString()), 'setPosition', [ '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,13 +337,18 @@ 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)', ); } @@ -347,15 +373,21 @@ 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]); + await _bridge.callMethod(JsRef(toString()), 'setMap', [map]); } Future _setOptions(GPolylineOptions? options) async { - await callMethod(this, 'setOptions', [options]); + await _bridge.callMethod( + JsRef(toString()), + 'setOptions', + [options], + ); } } @@ -406,14 +438,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 +471,21 @@ 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]); + await _bridge.callMethod(JsRef(toString()), 'setMap', [map]); } Future _setOptions(GPolygonOptions? options) async { - await callMethod(this, 'setOptions', [options]); + await _bridge.callMethod( + JsRef(toString()), + 'setOptions', + [options], + ); } } @@ -506,14 +546,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 +564,7 @@ class GCircle { @override String toString() { - return 'polygon$id'; + return 'circle$id'; } /// Sets if the circle is visible. @@ -540,19 +582,25 @@ 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]); + await _bridge.callMethod(JsRef(toString()), 'setMap', [map]); } Future _setOptions(GCircleOptions? options) async { - await callMethod(this, 'setOptions', [options]); + await _bridge.callMethod( + JsRef(toString()), + 'setOptions', + [options], + ); } } @@ -599,14 +647,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,12 +667,12 @@ 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( + final bool result = await _bridge.runJavaScriptReturningResult( '${toString()}.removeMarker($marker, $noDraw);', ) as bool; @@ -631,42 +683,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 +765,22 @@ class GMarkerClustererOptions { /// against the Earth's surface. class GGroundOverlay { /// GGroundOverlay Constructor. - GGroundOverlay([GGroundOverlayOptions? opts]) - : id = _gid++, + 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 +821,13 @@ class GGroundOverlay { } Future _setMap(Object? map) async { - await callMethod(this, 'setMap', [map]); + await _bridge.callMethod(JsRef(toString()), 'setMap', [map]); } Future _setOpacity(num? opacity) async { - await callMethod(this, 'setOpacity', [opacity]); + await _bridge.callMethod(JsRef(toString()), 'setOpacity', [ + opacity, + ]); } } @@ -802,18 +859,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..820ec89da 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/pubspec.yaml @@ -2,7 +2,7 @@ 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.1.15 environment: sdk: ">=3.4.0 <4.0.0" @@ -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.0