diff --git a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md new file mode 100644 index 000000000000..d989279aac42 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +* First open-source version diff --git a/packages/google_maps_flutter/google_maps_flutter_web/LICENSE b/packages/google_maps_flutter/google_maps_flutter_web/LICENSE new file mode 100644 index 000000000000..282a0f51aa4a --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/LICENSE @@ -0,0 +1,26 @@ +Copyright 2017, the Flutter project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/README.md b/packages/google_maps_flutter/google_maps_flutter_web/README.md new file mode 100644 index 000000000000..e1c1a5330c56 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/README.md @@ -0,0 +1,51 @@ +# google_maps_flutter_web + +This is an implementation of the [google_maps_flutter](https://pub.dev/packages/google_maps_flutter) plugin for web. Behind the scenes, it uses a14n's [google_maps](https://pub.dev/packages/google_maps) dart JS interop layer. + +## Usage + +### Depend on the package + +This package is not an endorsed implementation of the google_maps_flutter plugin yet, so you'll need to modify the `pubspec.yaml` file of your app to depend on this package: + +```yaml +dependencies: + google_maps_flutter: ^0.5.28 + google_maps_flutter_web: ^0.1.0 +``` + +### Modify web/index.html + +Get an API Key for Google Maps JavaScript API. Get started [here](https://developers.google.com/maps/documentation/javascript/get-api-key). + +Modify the `` tag of your `web/index.html` to load the Google Maps JavaScript API, like so: + +```html + + + + + + +``` + +Now you should be able to use the Google Maps plugin normally. + +## Limitations of the web version + +The following map options are not available in web, because the map doesn't rotate there: + +* `compassEnabled` +* `rotateGesturesEnabled` +* `tiltGesturesEnabled` + +There's no "Map Toolbar" in web, so the `mapToolbarEnabled` option is unused. + +There's no "My Location" widget in web ([tracking issue](https://github.com/flutter/flutter/issues/64073)), so the following options are ignored, for now: + +* `myLocationButtonEnabled` +* `myLocationEnabled` + +There's no `defaultMarkerWithHue` in web. If you need colored pins/markers, you may need to use your own asset images. + +Indoor and building layers are still not available on the web. Traffic is. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/analysis_options.yaml b/packages/google_maps_flutter/google_maps_flutter_web/analysis_options.yaml new file mode 100644 index 000000000000..443b16551ec9 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/analysis_options.yaml @@ -0,0 +1,10 @@ +# This is a temporary file to allow us to unblock the flutter/plugins repo CI. +# It disables some of lints that were disabled inline. Disabling lints inline +# is no longer possible, so this file is required. +# TODO(ditman) https://github.com/flutter/flutter/issues/55000 (clean this up) + +include: ../../../analysis_options.yaml + +analyzer: + errors: + undefined_prefixed_name: ignore diff --git a/packages/google_maps_flutter/google_maps_flutter_web/ios/google_maps_flutter_web.podspec b/packages/google_maps_flutter/google_maps_flutter_web/ios/google_maps_flutter_web.podspec new file mode 100644 index 000000000000..18db6ced01b6 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/ios/google_maps_flutter_web.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint google_maps_flutter_web.podspec' to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'google_maps_flutter_web' + s.version = '0.1.0' + s.summary = 'No-op implementation of google maps flutter web plugin to avoid build issues on iOS' + s.description = <<-DESC +temp fake google_maps_flutter_web plugin + DESC + s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_web' + s.license = { :file => '../LICENSE' } + s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '8.0' + + # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + s.swift_version = '5.0' +end diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart new file mode 100644 index 000000000000..cf133fb9e533 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart @@ -0,0 +1,39 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +library google_maps_flutter_web; + +import 'dart:async'; +import 'dart:html'; +import 'dart:ui' as ui; +import 'dart:convert'; + +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/gestures.dart'; + +import 'package:sanitize_html/sanitize_html.dart'; + +import 'package:stream_transform/stream_transform.dart'; + +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:google_maps/google_maps.dart' as gmaps; + +import 'src/types.dart'; + +part 'src/google_maps_flutter_web.dart'; +part 'src/google_maps_controller.dart'; +part 'src/circle.dart'; +part 'src/circles.dart'; +part 'src/polygon.dart'; +part 'src/polygons.dart'; +part 'src/polyline.dart'; +part 'src/polylines.dart'; +part 'src/marker.dart'; +part 'src/markers.dart'; +part 'src/convert.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circle.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circle.dart new file mode 100644 index 000000000000..96f9be7aa001 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circle.dart @@ -0,0 +1,46 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of google_maps_flutter_web; + +/// The `CircleController` class wraps a [gmaps.Circle] and its `onTap` behavior. +class CircleController { + gmaps.Circle _circle; + + final bool _consumeTapEvents; + + /// Creates a `CircleController`, which wraps a [gmaps.Circle] object and its `onTap` behavior. + CircleController({ + @required gmaps.Circle circle, + bool consumeTapEvents = false, + ui.VoidCallback onTap, + }) : _circle = circle, + _consumeTapEvents = consumeTapEvents { + if (onTap != null) { + circle.onClick.listen((_) { + onTap.call(); + }); + } + } + + /// Returns the wrapped [gmaps.Circle]. Only used for testing. + @visibleForTesting + gmaps.Circle get circle => _circle; + + /// Returns `true` if this Controller will use its own `onTap` handler to consume events. + bool get consumeTapEvents => _consumeTapEvents; + + /// Updates the options of the wrapped [gmaps.Circle] object. + void update(gmaps.CircleOptions options) { + _circle.options = options; + } + + /// Disposes of the currently wrapped [gmaps.Circle]. + void remove() { + _circle.visible = false; + _circle.radius = 0; + _circle.map = null; + _circle = null; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circles.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circles.dart new file mode 100644 index 000000000000..c7c33ed1811f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circles.dart @@ -0,0 +1,79 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of google_maps_flutter_web; + +/// This class manages all the [CircleController]s associated to a [GoogleMapController]. +class CirclesController extends GeometryController { + // A cache of [CircleController]s indexed by their [CircleId]. + final Map _circleIdToController; + + // The stream over which circles broadcast their events + StreamController _streamController; + + /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. + CirclesController({ + @required StreamController stream, + }) : _streamController = stream, + _circleIdToController = Map(); + + /// Returns the cache of [CircleController]s. Test only. + @visibleForTesting + Map get circles => _circleIdToController; + + /// Adds a set of [Circle] objects to the cache. + /// + /// Wraps each [Circle] into its corresponding [CircleController]. + void addCircles(Set circlesToAdd) { + circlesToAdd?.forEach((circle) { + _addCircle(circle); + }); + } + + void _addCircle(Circle circle) { + if (circle == null) { + return; + } + + final populationOptions = _circleOptionsFromCircle(circle); + gmaps.Circle gmCircle = gmaps.Circle(populationOptions); + gmCircle.map = googleMap; + CircleController controller = CircleController( + circle: gmCircle, + consumeTapEvents: circle.consumeTapEvents, + onTap: () { + _onCircleTap(circle.circleId); + }); + _circleIdToController[circle.circleId] = controller; + } + + /// Updates a set of [Circle] objects with new options. + void changeCircles(Set circlesToChange) { + circlesToChange?.forEach((circleToChange) { + _changeCircle(circleToChange); + }); + } + + void _changeCircle(Circle circle) { + final circleController = _circleIdToController[circle?.circleId]; + circleController?.update(_circleOptionsFromCircle(circle)); + } + + /// Removes a set of [CircleId]s from the cache. + void removeCircles(Set circleIdsToRemove) { + circleIdsToRemove?.forEach((circleId) { + final CircleController circleController = _circleIdToController[circleId]; + circleController?.remove(); + _circleIdToController.remove(circleId); + }); + } + + // Handles the global onCircleTap function to funnel events from circles into the stream. + bool _onCircleTap(CircleId circleId) { + // Have you ended here on your debugging? Is this wrong? + // Comment here: https://github.com/flutter/flutter/issues/64084 + _streamController.add(CircleTapEvent(mapId, circleId)); + return _circleIdToController[circleId]?.consumeTapEvents ?? false; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart new file mode 100644 index 000000000000..2eeaa0202995 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart @@ -0,0 +1,544 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of google_maps_flutter_web; + +final _nullLatLng = LatLng(0, 0); +final _nullLatLngBounds = LatLngBounds( + northeast: _nullLatLng, + southwest: _nullLatLng, +); + +// Defaults taken from the Google Maps Platform SDK documentation. +final _defaultStrokeColor = Colors.black.value; +final _defaultFillColor = Colors.transparent.value; + +// Indices in the plugin side don't match with the ones +// in the gmaps lib. This translates from plugin -> gmaps. +final _mapTypeToMapTypeId = { + 0: gmaps.MapTypeId.ROADMAP, // "none" in the plugin + 1: gmaps.MapTypeId.ROADMAP, + 2: gmaps.MapTypeId.SATELLITE, + 3: gmaps.MapTypeId.TERRAIN, + 4: gmaps.MapTypeId.HYBRID, +}; + +// Converts options from the plugin into gmaps.MapOptions that can be used by the JS SDK. +// The following options are not handled here, for various reasons: +// The following are not available in web, because the map doesn't rotate there: +// compassEnabled +// rotateGesturesEnabled +// tiltGesturesEnabled +// mapToolbarEnabled is unused in web, there's no "map toolbar" +// myLocationButtonEnabled Widget not available in web yet, it needs to be built on top of the maps widget +// See: https://developers.google.com/maps/documentation/javascript/examples/control-custom +// myLocationEnabled needs to be built through dart:html navigator.geolocation +// See: https://api.dart.dev/stable/2.8.4/dart-html/Geolocation-class.html +// trafficEnabled is handled when creating the GMap object, since it needs to be added as a layer. +// trackCameraPosition is just a boolan value that indicates if the map has an onCameraMove handler. +// indoorViewEnabled seems to not have an equivalent in web +// buildingsEnabled seems to not have an equivalent in web +// padding seems to behave differently in web than mobile. You can't move UI elements in web. +gmaps.MapOptions _rawOptionsToGmapsOptions(Map rawOptions) { + Map optionsUpdate = rawOptions['options'] ?? {}; + + gmaps.MapOptions options = gmaps.MapOptions(); + + if (_mapTypeToMapTypeId.containsKey(optionsUpdate['mapType'])) { + options.mapTypeId = _mapTypeToMapTypeId[optionsUpdate['mapType']]; + } + + if (optionsUpdate['minMaxZoomPreference'] != null) { + options + ..minZoom = optionsUpdate['minMaxZoomPreference'][0] + ..maxZoom = optionsUpdate['minMaxZoomPreference'][1]; + } + + if (optionsUpdate['cameraTargetBounds'] != null) { + // Needs gmaps.MapOptions.restriction and gmaps.MapRestriction + // see: https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions.restriction + } + + if (optionsUpdate['zoomControlsEnabled'] != null) { + options.zoomControl = optionsUpdate['zoomControlsEnabled']; + } + + if (optionsUpdate['styles'] != null) { + options.styles = optionsUpdate['styles']; + } + + if (optionsUpdate['scrollGesturesEnabled'] == false || + optionsUpdate['zoomGesturesEnabled'] == false) { + options.gestureHandling = 'none'; + } else { + options.gestureHandling = 'auto'; + } + + // These don't have any optionUpdate entry, but they seem to be off in the native maps. + options.mapTypeControl = false; + options.fullscreenControl = false; + options.streetViewControl = false; + + return options; +} + +gmaps.MapOptions _applyInitialPosition( + Map rawOptions, + gmaps.MapOptions options, +) { + // Adjust the initial position, if passed... + Map initialPosition = rawOptions['initialCameraPosition']; + if (initialPosition != null) { + final position = CameraPosition.fromMap(initialPosition); + options.zoom = position.zoom; + options.center = + gmaps.LatLng(position.target.latitude, position.target.longitude); + } + return options; +} + +// Extracts the status of the traffic layer from the rawOptions map. +bool _isTrafficLayerEnabled(Map rawOptions) { + if (rawOptions['options'] == null) { + return false; + } + return rawOptions['options']['trafficEnabled'] ?? false; +} + +// Coverts the incoming JSON object into a List of MapTypeStyler objects. +List _parseStylers(List stylerJsons) { + return stylerJsons?.map((styler) { + return gmaps.MapTypeStyler() + ..color = styler['color'] + ..gamma = styler['gamma'] + ..hue = styler['hue'] + ..invertLightness = styler['invertLightness'] + ..lightness = styler['lightness'] + ..saturation = styler['saturation'] + ..visibility = styler['visibility'] + ..weight = styler['weight']; + })?.toList(); +} + +// Converts a String to its corresponding MapTypeStyleElementType enum value. +final _elementTypeToEnum = { + 'all': gmaps.MapTypeStyleElementType.ALL, + 'geometry': gmaps.MapTypeStyleElementType.GEOMETRY, + 'geometry.fill': gmaps.MapTypeStyleElementType.GEOMETRY_FILL, + 'geometry.stroke': gmaps.MapTypeStyleElementType.GEOMETRY_STROKE, + 'labels': gmaps.MapTypeStyleElementType.LABELS, + 'labels.icon': gmaps.MapTypeStyleElementType.LABELS_ICON, + 'labels.text': gmaps.MapTypeStyleElementType.LABELS_TEXT, + 'labels.text.fill': gmaps.MapTypeStyleElementType.LABELS_TEXT_FILL, + 'labels.text.stroke': gmaps.MapTypeStyleElementType.LABELS_TEXT_STROKE, +}; + +// Converts a String to its corresponding MapTypeStyleFeatureType enum value. +final _featureTypeToEnum = { + 'administrative': gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE, + 'administrative.country': + gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE_COUNTRY, + 'administrative.land_parcel': + gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE_LAND_PARCEL, + 'administrative.locality': + gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE_LOCALITY, + 'administrative.neighborhood': + gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE_NEIGHBORHOOD, + 'administrative.province': + gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE_PROVINCE, + 'all': gmaps.MapTypeStyleFeatureType.ALL, + 'landscape': gmaps.MapTypeStyleFeatureType.LANDSCAPE, + 'landscape.man_made': gmaps.MapTypeStyleFeatureType.LANDSCAPE_MAN_MADE, + 'landscape.natural': gmaps.MapTypeStyleFeatureType.LANDSCAPE_NATURAL, + 'landscape.natural.landcover': + gmaps.MapTypeStyleFeatureType.LANDSCAPE_NATURAL_LANDCOVER, + 'landscape.natural.terrain': + gmaps.MapTypeStyleFeatureType.LANDSCAPE_NATURAL_TERRAIN, + 'poi': gmaps.MapTypeStyleFeatureType.POI, + 'poi.attraction': gmaps.MapTypeStyleFeatureType.POI_ATTRACTION, + 'poi.business': gmaps.MapTypeStyleFeatureType.POI_BUSINESS, + 'poi.government': gmaps.MapTypeStyleFeatureType.POI_GOVERNMENT, + 'poi.medical': gmaps.MapTypeStyleFeatureType.POI_MEDICAL, + 'poi.park': gmaps.MapTypeStyleFeatureType.POI_PARK, + 'poi.place_of_worship': gmaps.MapTypeStyleFeatureType.POI_PLACE_OF_WORSHIP, + 'poi.school': gmaps.MapTypeStyleFeatureType.POI_SCHOOL, + 'poi.sports_complex': gmaps.MapTypeStyleFeatureType.POI_SPORTS_COMPLEX, + 'road': gmaps.MapTypeStyleFeatureType.ROAD, + 'road.arterial': gmaps.MapTypeStyleFeatureType.ROAD_ARTERIAL, + 'road.highway': gmaps.MapTypeStyleFeatureType.ROAD_HIGHWAY, + 'road.highway.controlled_access': + gmaps.MapTypeStyleFeatureType.ROAD_HIGHWAY_CONTROLLED_ACCESS, + 'road.local': gmaps.MapTypeStyleFeatureType.ROAD_LOCAL, + 'transit': gmaps.MapTypeStyleFeatureType.TRANSIT, + 'transit.line': gmaps.MapTypeStyleFeatureType.TRANSIT_LINE, + 'transit.station': gmaps.MapTypeStyleFeatureType.TRANSIT_STATION, + 'transit.station.airport': + gmaps.MapTypeStyleFeatureType.TRANSIT_STATION_AIRPORT, + 'transit.station.bus': gmaps.MapTypeStyleFeatureType.TRANSIT_STATION_BUS, + 'transit.station.rail': gmaps.MapTypeStyleFeatureType.TRANSIT_STATION_RAIL, + 'water': gmaps.MapTypeStyleFeatureType.WATER, +}; + +// The keys we'd expect to see in a serialized MapTypeStyle JSON object. +final _mapStyleKeys = { + 'elementType', + 'featureType', + 'stylers', +}; + +// Checks if the passed in Map contains some of the _mapStyleKeys. +bool _isJsonMapStyle(Map value) { + return _mapStyleKeys.intersection(value.keys.toSet()).isNotEmpty; +} + +// Converts an incoming JSON-encoded Style info, into the correct gmaps array. +List _mapStyles(String mapStyleJson) { + List styles = []; + if (mapStyleJson != null) { + styles = json.decode(mapStyleJson, reviver: (key, value) { + if (value is Map && _isJsonMapStyle(value)) { + return gmaps.MapTypeStyle() + ..elementType = _elementTypeToEnum[value['elementType']] + ..featureType = _featureTypeToEnum[value['featureType']] + ..stylers = _parseStylers(value['stylers']); + } + return value; + }).cast(); + } + return styles; +} + +gmaps.LatLng _latLngToGmLatLng(LatLng latLng) { + if (latLng == null) return null; + return gmaps.LatLng(latLng.latitude, latLng.longitude); +} + +LatLng _gmLatLngToLatLng(gmaps.LatLng latLng) { + if (latLng == null) return _nullLatLng; + return LatLng(latLng.lat, latLng.lng); +} + +LatLngBounds _gmLatLngBoundsTolatLngBounds(gmaps.LatLngBounds latLngBounds) { + if (latLngBounds == null) { + return _nullLatLngBounds; + } + + return LatLngBounds( + southwest: _gmLatLngToLatLng(latLngBounds.southWest), + northeast: _gmLatLngToLatLng(latLngBounds.northEast), + ); +} + +CameraPosition _gmViewportToCameraPosition(gmaps.GMap map) { + return CameraPosition( + target: _gmLatLngToLatLng(map.center), + bearing: map.heading ?? 0, + tilt: map.tilt ?? 0, + zoom: map.zoom?.toDouble() ?? 10, + ); +} + +Set _rawOptionsToInitialMarkers(Map rawOptions) { + final List> list = rawOptions['markersToAdd']; + Set markers = {}; + markers.addAll(list?.map((rawMarker) { + Offset offset; + LatLng position; + InfoWindow infoWindow; + if (rawMarker['anchor'] != null) { + offset = Offset((rawMarker['anchor'][0]), (rawMarker['anchor'][1])); + } + if (rawMarker['position'] != null) { + position = LatLng.fromJson(rawMarker['position']); + } + if (rawMarker['infoWindow'] != null || rawMarker['snippet'] != null) { + String title = rawMarker['infoWindow'] != null + ? rawMarker['infoWindow']['title'] + : null; + infoWindow = InfoWindow( + title: title ?? '', + snippet: rawMarker['snippet'] ?? '', + ); + } + return Marker( + markerId: MarkerId(rawMarker['markerId']), + alpha: rawMarker['alpha'], + anchor: offset, + consumeTapEvents: rawMarker['consumeTapEvents'], + draggable: rawMarker['draggable'], + flat: rawMarker['flat'], + // TODO: Doesn't this support custom icons? + icon: BitmapDescriptor.defaultMarker, + infoWindow: infoWindow, + position: position ?? _nullLatLng, + rotation: rawMarker['rotation'], + visible: rawMarker['visible'], + zIndex: rawMarker['zIndex'], + ); + }) ?? + []); + return markers; +} + +Set _rawOptionsToInitialCircles(Map rawOptions) { + final List> list = rawOptions['circlesToAdd']; + Set circles = {}; + circles.addAll(list?.map((rawCircle) { + LatLng center; + if (rawCircle['center'] != null) { + center = LatLng.fromJson(rawCircle['center']); + } + return Circle( + circleId: CircleId(rawCircle['circleId']), + consumeTapEvents: rawCircle['consumeTapEvents'], + fillColor: Color(rawCircle['fillColor'] ?? _defaultFillColor), + center: center ?? _nullLatLng, + radius: rawCircle['radius'], + strokeColor: Color(rawCircle['strokeColor'] ?? _defaultStrokeColor), + strokeWidth: rawCircle['strokeWidth'], + visible: rawCircle['visible'], + zIndex: rawCircle['zIndex'], + ); + }) ?? + []); + return circles; +} + +// Unsupported on the web: endCap, jointType, patterns and startCap. +Set _rawOptionsToInitialPolylines(Map rawOptions) { + final List> list = rawOptions['polylinesToAdd']; + Set polylines = {}; + polylines.addAll(list?.map((rawPolyline) { + return Polyline( + polylineId: PolylineId(rawPolyline['polylineId']), + consumeTapEvents: rawPolyline['consumeTapEvents'], + color: Color(rawPolyline['color'] ?? _defaultStrokeColor), + geodesic: rawPolyline['geodesic'], + visible: rawPolyline['visible'], + zIndex: rawPolyline['zIndex'], + width: rawPolyline['width'], + points: rawPolyline['points'] + ?.map((rawPoint) => LatLng.fromJson(rawPoint)) + ?.toList(), + ); + }) ?? + []); + return polylines; +} + +Set _rawOptionsToInitialPolygons(Map rawOptions) { + final List> list = rawOptions['polygonsToAdd']; + Set polygons = {}; + + polygons.addAll(list?.map((rawPolygon) { + return Polygon( + polygonId: PolygonId(rawPolygon['polygonId']), + consumeTapEvents: rawPolygon['consumeTapEvents'], + fillColor: Color(rawPolygon['fillColor'] ?? _defaultFillColor), + geodesic: rawPolygon['geodesic'], + strokeColor: Color(rawPolygon['strokeColor'] ?? _defaultStrokeColor), + strokeWidth: rawPolygon['strokeWidth'], + visible: rawPolygon['visible'], + zIndex: rawPolygon['zIndex'], + points: rawPolygon['points'] + ?.map((rawPoint) => LatLng.fromJson(rawPoint)) + ?.toList(), + ); + }) ?? + []); + return polygons; +} + +// Convert plugin objects to gmaps.Options objects +// TODO: Move to their appropriate objects, maybe make these copy constructors: +// Marker.fromMarker(anotherMarker, moreOptions); + +gmaps.InfoWindowOptions _infoWindowOptionsFromMarker(Marker marker) { + if ((marker.infoWindow?.title?.isEmpty ?? true) && + (marker.infoWindow?.snippet?.isEmpty ?? true)) { + return null; + } + + final content = '

' + + sanitizeHtml(marker.infoWindow.title ?? "") + + '

' + + sanitizeHtml(marker.infoWindow.snippet ?? ""); + + return gmaps.InfoWindowOptions() + ..content = content + ..zIndex = marker.zIndex; + // TODO: Compute the pixelOffset of the infoWindow, from the size of the Marker, + // and the marker.infoWindow.anchor property. +} + +// Computes the options for a new [gmaps.Marker] from an incoming set of options +// [marker], and the existing marker registered with the map: [currentMarker]. +// Preserves the position from the [currentMarker], if set. +gmaps.MarkerOptions _markerOptionsFromMarker( + Marker marker, + gmaps.Marker currentMarker, +) { + final iconConfig = marker.icon.toJson() as List; + gmaps.Icon icon; + + if (iconConfig[0] == 'fromAssetImage') { + // iconConfig[2] contains the DPIs of the screen, but that information is + // already encoded in the iconConfig[1] + + icon = gmaps.Icon() + ..url = ui.webOnlyAssetManager.getAssetUrl(iconConfig[1]); + + // iconConfig[3] may contain the [width, height] of the image, if passed! + if (iconConfig.length >= 4 && iconConfig[3] != null) { + final size = gmaps.Size(iconConfig[3][0], iconConfig[3][1]); + icon + ..size = size + ..scaledSize = size; + } + } + return gmaps.MarkerOptions() + ..position = currentMarker?.position ?? + gmaps.LatLng( + marker.position.latitude, + marker.position.longitude, + ) + ..title = sanitizeHtml(marker.infoWindow?.title ?? "") + ..zIndex = marker.zIndex + ..visible = marker.visible + ..opacity = marker.alpha + ..draggable = marker.draggable + ..icon = icon; + // TODO: Compute anchor properly, otherwise infowindows attach to the wrong spot. + // Flat and Rotation are not supported directly on the web. +} + +gmaps.CircleOptions _circleOptionsFromCircle(Circle circle) { + final populationOptions = gmaps.CircleOptions() + ..strokeColor = '#' + circle.strokeColor.value.toRadixString(16) + ..strokeOpacity = 0.8 + ..strokeWeight = circle.strokeWidth + ..fillColor = '#' + circle.fillColor.value.toRadixString(16) + ..fillOpacity = 0.6 + ..center = gmaps.LatLng(circle.center.latitude, circle.center.longitude) + ..radius = circle.radius + ..visible = circle.visible; + return populationOptions; +} + +gmaps.PolygonOptions _polygonOptionsFromPolygon( + gmaps.GMap googleMap, Polygon polygon) { + List paths = []; + polygon.points.forEach((point) { + paths.add(_latLngToGmLatLng(point)); + }); + return gmaps.PolygonOptions() + ..paths = paths + ..strokeColor = '#' + polygon.strokeColor.value.toRadixString(16) + ..strokeOpacity = 0.8 + ..strokeWeight = polygon.strokeWidth + ..fillColor = '#' + polygon.fillColor.value.toRadixString(16) + ..fillOpacity = 0.35 + ..visible = polygon.visible + ..zIndex = polygon.zIndex + ..geodesic = polygon.geodesic; +} + +gmaps.PolylineOptions _polylineOptionsFromPolyline( + gmaps.GMap googleMap, Polyline polyline) { + List paths = []; + polyline.points.forEach((point) { + paths.add(_latLngToGmLatLng(point)); + }); + + return gmaps.PolylineOptions() + ..path = paths + ..strokeOpacity = 1.0 + ..strokeWeight = polyline.width + ..strokeColor = '#' + polyline.color.value.toRadixString(16).substring(0, 6) + ..visible = polyline.visible + ..zIndex = polyline.zIndex + ..geodesic = polyline.geodesic; +// this.endCap = Cap.buttCap, +// this.jointType = JointType.mitered, +// this.patterns = const [], +// this.startCap = Cap.buttCap, +// this.width = 10, +} + +// Translates a [CameraUpdate] into operations on a [gmaps.GMap]. +void _applyCameraUpdate(gmaps.GMap map, CameraUpdate update) { + final json = update.toJson(); + switch (json[0]) { + case 'newCameraPosition': + map.heading = json[1]['bearing']; + map.zoom = json[1]['zoom']; + map.panTo(gmaps.LatLng(json[1]['target'][0], json[1]['target'][1])); + map.tilt = json[1]['tilt']; + break; + case 'newLatLng': + map.panTo(gmaps.LatLng(json[1][0], json[1][1])); + break; + case 'newLatLngZoom': + map.zoom = json[2]; + map.panTo(gmaps.LatLng(json[1][0], json[1][1])); + break; + case 'newLatLngBounds': + map.fitBounds(gmaps.LatLngBounds( + gmaps.LatLng(json[1][0][0], json[1][0][1]), + gmaps.LatLng(json[1][1][0], json[1][1][1]))); + // padding = json[2]; + // Needs package:google_maps ^4.0.0 to adjust the padding in fitBounds + break; + case 'scrollBy': + map.panBy(json[1], json[2]); + break; + case 'zoomBy': + gmaps.LatLng focusLatLng; + double zoomDelta = json[1] ?? 0; + // Web only supports integer changes... + int newZoomDelta = zoomDelta < 0 ? zoomDelta.floor() : zoomDelta.ceil(); + if (json.length == 3) { + // With focus + try { + focusLatLng = _pixelToLatLng(map, json[2][0], json[2][1]); + } catch (e) { + // https://github.com/a14n/dart-google-maps/issues/87 + // print('Error computing new focus LatLng. JS Error: ' + e.toString()); + } + } + map.zoom = map.zoom + newZoomDelta; + if (focusLatLng != null) { + map.panTo(focusLatLng); + } + break; + case 'zoomIn': + map.zoom++; + break; + case 'zoomOut': + map.zoom--; + break; + case 'zoomTo': + map.zoom = json[1]; + break; + default: + throw UnimplementedError('Unimplemented CameraMove: ${json[0]}.'); + } +} + +// original JS by: Byron Singh (https://stackoverflow.com/a/30541162) +gmaps.LatLng _pixelToLatLng(gmaps.GMap map, int x, int y) { + final ne = map.bounds.northEast; + final sw = map.bounds.southWest; + final projection = map.projection; + + final topRight = projection.fromLatLngToPoint(ne); + final bottomLeft = projection.fromLatLngToPoint(sw); + + final scale = 1 << map.zoom; // 2 ^ zoom + + final point = + gmaps.Point((x / scale) + bottomLeft.x, (y / scale) + topRight.y); + + return projection.fromPointToLatLng(point); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart new file mode 100644 index 000000000000..707af828e2c6 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart @@ -0,0 +1,330 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of google_maps_flutter_web; + +/// Type used when passing an override to the _createMap function. +@visibleForTesting +typedef DebugCreateMapFunction = gmaps.GMap Function( + HtmlElement div, gmaps.MapOptions options); + +/// Encapsulates a [gmaps.GMap], its events, and where in the DOM it's rendered. +class GoogleMapController { + // The internal ID of the map. Used to broadcast events, DOM IDs and everything where a unique ID is needed. + final int _mapId; + + // The raw options passed by the user, before converting to gmaps. + // Caching this allows us to re-create the map faithfully when needed. + Map _rawOptions = { + 'options': {}, + }; + + // Creates the 'viewType' for the _widget + String _getViewType(int mapId) => 'plugins.flutter.io/google_maps_$mapId'; + + // The Flutter widget that contains the rendered Map. + HtmlElementView _widget; + HtmlElement _div; + + /// The Flutter widget that will contain the rendered Map. Used for caching. + HtmlElementView get widget { + if (_widget == null && !_streamController.isClosed) { + _widget = HtmlElementView( + viewType: _getViewType(_mapId), + ); + } + return _widget; + } + + // The currently-enabled traffic layer. + gmaps.TrafficLayer _trafficLayer; + + /// A getter for the current traffic layer. Only for tests. + @visibleForTesting + gmaps.TrafficLayer get trafficLayer => _trafficLayer; + + // The underlying GMap instance. This is the interface with the JS SDK. + gmaps.GMap _googleMap; + + // The StreamController used by this controller and the geometry ones. + final StreamController _streamController; + + /// The Stream over which this controller broadcasts events. + Stream get events => _streamController.stream; + + // Geometry controllers, for different features of the map. + CirclesController _circlesController; + PolygonsController _polygonsController; + PolylinesController _polylinesController; + MarkersController _markersController; + // Keeps track if _attachGeometryControllers has been called or not. + bool _controllersBoundToMap = false; + + // Keeps track if the map is moving or not. + bool _mapIsMoving = false; + + /// Initializes the GMap, and the sub-controllers related to it. Wires events. + GoogleMapController({ + @required int mapId, + @required StreamController streamController, + @required Map rawOptions, + }) : this._mapId = mapId, + this._streamController = streamController, + this._rawOptions = rawOptions { + _circlesController = CirclesController(stream: this._streamController); + _polygonsController = PolygonsController(stream: this._streamController); + _polylinesController = PolylinesController(stream: this._streamController); + _markersController = MarkersController(stream: this._streamController); + + // Register the view factory that will hold the `_div` that holds the map in the DOM. + // The `_div` needs to be created outside of the ViewFactory (and cached!) so we can + // use it to create the [gmaps.GMap] in the `init()` method of this class. + _div = DivElement()..id = _getViewType(mapId); + + ui.platformViewRegistry.registerViewFactory( + _getViewType(mapId), + (int viewId) => _div, + ); + } + + /// Overrides certain properties to install mocks defined during testing. + @visibleForTesting + void debugSetOverrides({ + DebugCreateMapFunction createMap, + MarkersController markers, + CirclesController circles, + PolygonsController polygons, + PolylinesController polylines, + }) { + _overrideCreateMap = createMap; + _markersController = markers ?? _markersController; + _circlesController = circles ?? _circlesController; + _polygonsController = polygons ?? _polygonsController; + _polylinesController = polylines ?? _polylinesController; + } + + DebugCreateMapFunction _overrideCreateMap; + + gmaps.GMap _createMap(HtmlElement div, gmaps.MapOptions options) { + if (_overrideCreateMap != null) { + return _overrideCreateMap(div, options); + } + return gmaps.GMap(div, options); + } + + /// Initializes the [gmaps.GMap] instance from the stored `rawOptions`. + /// + /// This method actually renders the GMap into the cached `_div`. This is + /// called by the [GoogleMapsPlugin.init] method when appropriate. + /// + /// Failure to call this method would result in the GMap not rendering at all, + /// and most of the public methods on this class no-op'ing. + void init() { + var options = _rawOptionsToGmapsOptions(_rawOptions); + // Initial position can only to be set here! + options = _applyInitialPosition(_rawOptions, options); + + // Create the map... + _googleMap = _createMap(_div, options); + + _attachMapEvents(_googleMap); + _attachGeometryControllers(_googleMap); + + _renderInitialGeometry( + markers: _rawOptionsToInitialMarkers(_rawOptions), + circles: _rawOptionsToInitialCircles(_rawOptions), + polygons: _rawOptionsToInitialPolygons(_rawOptions), + polylines: _rawOptionsToInitialPolylines(_rawOptions), + ); + + _setTrafficLayer(_googleMap, _isTrafficLayerEnabled(_rawOptions)); + } + + // Funnels map gmap events into the plugin's stream controller. + void _attachMapEvents(gmaps.GMap map) { + map.onClick.listen((event) { + _streamController.add( + MapTapEvent(_mapId, _gmLatLngToLatLng(event.latLng)), + ); + }); + map.onRightclick.listen((event) { + _streamController.add( + MapLongPressEvent(_mapId, _gmLatLngToLatLng(event.latLng)), + ); + }); + map.onBoundsChanged.listen((event) { + if (!_mapIsMoving) { + _mapIsMoving = true; + _streamController.add(CameraMoveStartedEvent(_mapId)); + } + _streamController.add( + CameraMoveEvent(_mapId, _gmViewportToCameraPosition(map)), + ); + }); + map.onIdle.listen((event) { + _mapIsMoving = false; + _streamController.add(CameraIdleEvent(_mapId)); + }); + } + + // Binds the Geometry controllers to a map instance + void _attachGeometryControllers(gmaps.GMap map) { + // Now we can add the initial geometry. + // And bind the (ready) map instance to the other geometry controllers. + _circlesController.bindToMap(_mapId, map); + _polygonsController.bindToMap(_mapId, map); + _polylinesController.bindToMap(_mapId, map); + _markersController.bindToMap(_mapId, map); + _controllersBoundToMap = true; + } + + // Renders the initial sets of geometry. + void _renderInitialGeometry({ + Set markers, + Set circles, + Set polygons, + Set polylines, + }) { + assert( + _controllersBoundToMap, + 'Geometry controllers must be bound to a map before any geometry can ' + + 'be added to them. Ensure _attachGeometryControllers is called first.'); + _markersController.addMarkers(markers); + _circlesController.addCircles(circles); + _polygonsController.addPolygons(polygons); + _polylinesController.addPolylines(polylines); + } + + // Merges new options coming from the plugin into the `key` entry of the _rawOptions map. + // + // By default: `key` is 'options'. + // + // Returns the updated _rawOptions object. + Map _mergeRawOptions( + Map newOptions, { + String key = 'options', + }) { + _rawOptions[key] = { + ...(_rawOptions[key] ?? {}), + ...newOptions, + }; + return _rawOptions; + } + + /// Updates the map options from a `Map`. + /// + /// This method converts the map into the proper [gmaps.MapOptions] + void updateRawOptions(Map optionsUpdate) { + final newOptions = _mergeRawOptions(optionsUpdate); + + _setOptions(_rawOptionsToGmapsOptions(newOptions)); + _setTrafficLayer(_googleMap, _isTrafficLayerEnabled(newOptions)); + } + + // Sets new [gmaps.MapOptions] on the wrapped map. + void _setOptions(gmaps.MapOptions options) { + _googleMap?.options = options; + } + + // Attaches/detaches a Traffic Layer on the passed `map` if `attach` is true/false. + void _setTrafficLayer(gmaps.GMap map, bool attach) { + if (attach && _trafficLayer == null) { + _trafficLayer = gmaps.TrafficLayer(); + _trafficLayer.set('map', map); + } + if (!attach && _trafficLayer != null) { + _trafficLayer.set('map', null); + _trafficLayer = null; + } + } + + // _googleMap manipulation + // Viewport + + /// Returns the [LatLngBounds] of the current viewport. + Future getVisibleRegion() async { + return _gmLatLngBoundsTolatLngBounds(await _googleMap.bounds); + } + + /// Returns the [ScreenCoordinate] for a given viewport [LatLng]. + Future getScreenCoordinate(LatLng latLng) async { + final point = + _googleMap.projection.fromLatLngToPoint(_latLngToGmLatLng(latLng)); + return ScreenCoordinate(x: point.x, y: point.y); + } + + /// Returns the [LatLng] for a `screenCoordinate` (in pixels) of the viewport. + Future getLatLng(ScreenCoordinate screenCoordinate) async { + final latLng = _googleMap.projection.fromPointToLatLng( + gmaps.Point(screenCoordinate.x, screenCoordinate.y), + ); + return _gmLatLngToLatLng(latLng); + } + + /// Applies a `cameraUpdate` to the current viewport. + Future moveCamera(CameraUpdate cameraUpdate) async { + return _applyCameraUpdate(_googleMap, cameraUpdate); + } + + /// Returns the zoom level of the current viewport. + Future getZoomLevel() async => _googleMap.zoom.toDouble(); + + // Geometry manipulation + + /// Applies [CircleUpdates] to the currently managed circles. + void updateCircles(CircleUpdates updates) { + _circlesController?.addCircles(updates.circlesToAdd); + _circlesController?.changeCircles(updates.circlesToChange); + _circlesController?.removeCircles(updates.circleIdsToRemove); + } + + /// Applies [PolygonUpdates] to the currently managed polygons. + void updatePolygons(PolygonUpdates updates) { + _polygonsController?.addPolygons(updates.polygonsToAdd); + _polygonsController?.changePolygons(updates.polygonsToChange); + _polygonsController?.removePolygons(updates.polygonIdsToRemove); + } + + /// Applies [PolylineUpdates] to the currently managed lines. + void updatePolylines(PolylineUpdates updates) { + _polylinesController?.addPolylines(updates.polylinesToAdd); + _polylinesController?.changePolylines(updates.polylinesToChange); + _polylinesController?.removePolylines(updates.polylineIdsToRemove); + } + + /// Applies [MarkerUpdates] to the currently managed markers. + void updateMarkers(MarkerUpdates updates) { + _markersController?.addMarkers(updates.markersToAdd); + _markersController?.changeMarkers(updates.markersToChange); + _markersController?.removeMarkers(updates.markerIdsToRemove); + } + + /// Shows the [InfoWindow] of the marker identified by its [MarkerId]. + void showInfoWindow(MarkerId markerId) { + _markersController?.showMarkerInfoWindow(markerId); + } + + /// Hides the [InfoWindow] of the marker identified by its [MarkerId]. + void hideInfoWindow(MarkerId markerId) { + _markersController?.hideMarkerInfoWindow(markerId); + } + + /// Returns true if the [InfoWindow] of the marker identified by [MarkerId] is shown. + bool isInfoWindowShown(MarkerId markerId) { + return _markersController?.isInfoWindowShown(markerId); + } + + // Cleanup + + /// Disposes of this controller and its resources. + void dispose() { + _widget = null; + _googleMap = null; + _circlesController = null; + _polygonsController = null; + _polylinesController = null; + _markersController = null; + _streamController.close(); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart new file mode 100644 index 000000000000..cf549e8e375e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart @@ -0,0 +1,291 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of google_maps_flutter_web; + +/// The web implementation of [GoogleMapsFlutterPlatform]. +/// +/// This class implements the `package:google_maps_flutter` functionality for the web. +class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { + /// Registers this class as the default instance of [GoogleMapsFlutterPlatform]. + static void registerWith(Registrar registrar) { + GoogleMapsFlutterPlatform.instance = GoogleMapsPlugin(); + } + + // A cache of map controllers by map Id. + Map _mapById = Map(); + + /// Allows tests to inject controllers without going through the buildView flow. + @visibleForTesting + void debugSetMapById(Map mapById) { + _mapById = mapById; + } + + // Convenience getter for a stream of events filtered by their mapId. + Stream _events(int mapId) => _map(mapId).events; + + // Convenience getter for a map controller by its mapId. + GoogleMapController _map(int mapId) { + final controller = _mapById[mapId]; + assert(controller != null, + 'Maps cannot be retrieved before calling buildView!'); + return controller; + } + + @override + Future init(int mapId) async { + _map(mapId).init(); + } + + /// Updates the options of a given `mapId`. + /// + /// This attempts to merge the new `optionsUpdate` passed in, with the previous + /// options passed to the map (in other updates, or when creating it). + @override + Future updateMapOptions( + Map optionsUpdate, { + @required int mapId, + }) async { + _map(mapId).updateRawOptions(optionsUpdate); + } + + /// Applies the passed in `markerUpdates` to the `mapId`. + @override + Future updateMarkers( + MarkerUpdates markerUpdates, { + @required int mapId, + }) async { + _map(mapId).updateMarkers(markerUpdates); + } + + /// Applies the passed in `polygonUpdates` to the `mapId`. + @override + Future updatePolygons( + PolygonUpdates polygonUpdates, { + @required int mapId, + }) async { + _map(mapId).updatePolygons(polygonUpdates); + } + + /// Applies the passed in `polylineUpdates` to the `mapId`. + @override + Future updatePolylines( + PolylineUpdates polylineUpdates, { + @required int mapId, + }) async { + _map(mapId).updatePolylines(polylineUpdates); + } + + /// Applies the passed in `circleUpdates` to the `mapId`. + @override + Future updateCircles( + CircleUpdates circleUpdates, { + @required int mapId, + }) async { + _map(mapId).updateCircles(circleUpdates); + } + + /// Applies the given `cameraUpdate` to the current viewport (with animation). + @override + Future animateCamera( + CameraUpdate cameraUpdate, { + @required int mapId, + }) async { + return moveCamera(cameraUpdate, mapId: mapId); + } + + /// Applies the given `cameraUpdate` to the current viewport. + @override + Future moveCamera( + CameraUpdate cameraUpdate, { + @required int mapId, + }) async { + return _map(mapId).moveCamera(cameraUpdate); + } + + /// Sets the passed-in `mapStyle` to the map. + /// + /// This function just adds a 'styles' option to the current map options. + /// + /// Subsequent calls to this method override previous calls, you need to + /// pass full styles. + @override + Future setMapStyle( + String mapStyle, { + @required int mapId, + }) async { + _map(mapId).updateRawOptions({ + 'styles': _mapStyles(mapStyle), + }); + } + + /// Returns the bounds of the current viewport. + @override + Future getVisibleRegion({ + @required int mapId, + }) { + return _map(mapId).getVisibleRegion(); + } + + /// Returns the screen coordinate (in pixels) of a given `latLng`. + @override + Future getScreenCoordinate( + LatLng latLng, { + @required int mapId, + }) { + return _map(mapId).getScreenCoordinate(latLng); + } + + /// Returns the [LatLng] of a [ScreenCoordinate] of the viewport. + @override + Future getLatLng( + ScreenCoordinate screenCoordinate, { + @required int mapId, + }) { + return _map(mapId).getLatLng(screenCoordinate); + } + + /// Shows the [InfoWindow] (if any) of the [Marker] identified by `markerId`. + /// + /// See also: + /// * [hideMarkerInfoWindow] to hide the info window. + /// * [isMarkerInfoWindowShown] to check if the info window is visible/hidden. + @override + Future showMarkerInfoWindow( + MarkerId markerId, { + @required int mapId, + }) async { + _map(mapId).showInfoWindow(markerId); + } + + /// Hides the [InfoWindow] (if any) of the [Marker] identified by `markerId`. + /// + /// See also: + /// * [showMarkerInfoWindow] to show the info window. + /// * [isMarkerInfoWindowShown] to check if the info window is shown. + @override + Future hideMarkerInfoWindow( + MarkerId markerId, { + @required int mapId, + }) async { + _map(mapId).hideInfoWindow(markerId); + } + + /// Returns true if the [InfoWindow] of the [Marker] identified by `markerId` is shown. + /// + /// See also: + /// * [showMarkerInfoWindow] to show the info window. + /// * [hideMarkerInfoWindow] to hide the info window. + @override + Future isMarkerInfoWindowShown( + MarkerId markerId, { + @required int mapId, + }) async { + return _map(mapId).isInfoWindowShown(markerId); + } + + /// Returns the zoom level of the `mapId`. + @override + Future getZoomLevel({ + @required int mapId, + }) { + return _map(mapId).getZoomLevel(); + } + + // The following are the 11 possible streams of data from the native side + // into the plugin + + @override + Stream onCameraMoveStarted({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onCameraMove({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onCameraIdle({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerTap({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onInfoWindowTap({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerDragEnd({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onPolylineTap({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onPolygonTap({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onCircleTap({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onTap({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onLongPress({@required int mapId}) { + return _events(mapId).whereType(); + } + + /// Disposes of the current map. It can't be used afterwards! + @override + void dispose({@required int mapId}) { + _map(mapId)?.dispose(); + _mapById.remove(mapId); + } + + @override + Widget buildView( + Map creationParams, + Set> gestureRecognizers, + PlatformViewCreatedCallback onPlatformViewCreated) { + int mapId = creationParams.remove('_webOnlyMapCreationId'); + + assert(mapId != null, + 'buildView needs a `_webOnlyMapCreationId` in its creationParams to prevent widget reloads in web.'); + + // Bail fast if we've already rendered this mapId... + if (_mapById[mapId]?.widget != null) { + return _mapById[mapId].widget; + } + + final StreamController controller = + StreamController.broadcast(); + + final mapController = GoogleMapController( + mapId: mapId, + streamController: controller, + rawOptions: creationParams, + ); + + _mapById[mapId] = mapController; + + onPlatformViewCreated.call(mapId); + + return mapController.widget; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart new file mode 100644 index 000000000000..a067e352732f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart @@ -0,0 +1,82 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of google_maps_flutter_web; + +/// The `MarkerController` class wraps a [gmaps.Marker], how it handles events, and its associated (optional) [gmaps.InfoWindow] widget. +class MarkerController { + gmaps.Marker _marker; + + final bool _consumeTapEvents; + + final gmaps.InfoWindow _infoWindow; + + bool _infoWindowShown = false; + + /// Creates a `MarkerController`, which wraps a [gmaps.Marker] object, its `onTap`/`onDrag` behavior, and its associated [gmaps.InfoWindow]. + MarkerController({ + @required gmaps.Marker marker, + gmaps.InfoWindow infoWindow, + bool consumeTapEvents = false, + LatLngCallback onDragEnd, + ui.VoidCallback onTap, + }) : _marker = marker, + _infoWindow = infoWindow, + _consumeTapEvents = consumeTapEvents { + if (onTap != null) { + _marker.onClick.listen((event) { + onTap.call(); + }); + } + if (onDragEnd != null) { + _marker.onDragend.listen((event) { + _marker.position = event.latLng; + onDragEnd.call(event.latLng); + }); + } + } + + /// Returns `true` if this Controller will use its own `onTap` handler to consume events. + bool get consumeTapEvents => _consumeTapEvents; + + /// Returns `true` if the [gmaps.InfoWindow] associated to this marker is being shown. + bool get infoWindowShown => _infoWindowShown; + + /// Returns the [gmaps.Marker] associated to this controller. + gmaps.Marker get marker => _marker; + + /// Updates the options of the wrapped [gmaps.Marker] object. + void update( + gmaps.MarkerOptions options, { + String newInfoWindowContent, + }) { + _marker.options = options; + if (_infoWindow != null && newInfoWindowContent != null) { + _infoWindow.content = newInfoWindowContent; + } + } + + /// Disposes of the currently wrapped [gmaps.Marker]. + void remove() { + _marker.visible = false; + _marker.map = null; + _marker = null; + } + + /// Hide the associated [gmaps.InfoWindow]. + void hideInfoWindow() { + if (_infoWindow != null) { + _infoWindow.close(); + _infoWindowShown = false; + } + } + + /// Show the associated [gmaps.InfoWindow]. + void showInfoWindow() { + if (_infoWindow != null) { + _infoWindow.open(_marker.map, _marker); + _infoWindowShown = true; + } + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart new file mode 100644 index 000000000000..ebb478d20b06 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart @@ -0,0 +1,144 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of google_maps_flutter_web; + +/// This class manages a set of [MarkerController]s associated to a [GoogleMapController]. +class MarkersController extends GeometryController { + // A cache of [MarkerController]s indexed by their [MarkerId]. + final Map _markerIdToController; + + // The stream over which markers broadcast their events + StreamController _streamController; + + /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. + MarkersController({ + @required StreamController stream, + }) : _streamController = stream, + _markerIdToController = Map(); + + /// Returns the cache of [MarkerController]s. Test only. + @visibleForTesting + Map get markers => _markerIdToController; + + /// Adds a set of [Marker] objects to the cache. + /// + /// Wraps each [Marker] into its corresponding [MarkerController]. + void addMarkers(Set markersToAdd) { + markersToAdd?.forEach(_addMarker); + } + + void _addMarker(Marker marker) { + if (marker == null) { + return; + } + + final infoWindowOptions = _infoWindowOptionsFromMarker(marker); + gmaps.InfoWindow gmInfoWindow; + + if (infoWindowOptions != null) { + gmInfoWindow = gmaps.InfoWindow(infoWindowOptions) + ..addListener('click', () { + _onInfoWindowTap(marker.markerId); + }); + } + + final currentMarker = _markerIdToController[marker.markerId]?.marker; + + final populationOptions = _markerOptionsFromMarker(marker, currentMarker); + gmaps.Marker gmMarker = gmaps.Marker(populationOptions); + gmMarker.map = googleMap; + MarkerController controller = MarkerController( + marker: gmMarker, + infoWindow: gmInfoWindow, + consumeTapEvents: marker.consumeTapEvents, + onTap: () { + this.showMarkerInfoWindow(marker.markerId); + _onMarkerTap(marker.markerId); + }, + onDragEnd: (gmaps.LatLng latLng) { + _onMarkerDragEnd(marker.markerId, latLng); + }, + ); + _markerIdToController[marker.markerId] = controller; + } + + /// Updates a set of [Marker] objects with new options. + void changeMarkers(Set markersToChange) { + markersToChange?.forEach(_changeMarker); + } + + void _changeMarker(Marker marker) { + MarkerController markerController = _markerIdToController[marker?.markerId]; + if (markerController != null) { + final markerOptions = _markerOptionsFromMarker( + marker, + markerController.marker, + ); + final infoWindow = _infoWindowOptionsFromMarker(marker); + markerController.update( + markerOptions, + newInfoWindowContent: infoWindow?.content, + ); + } + } + + /// Removes a set of [MarkerId]s from the cache. + void removeMarkers(Set markerIdsToRemove) { + markerIdsToRemove?.forEach(_removeMarker); + } + + void _removeMarker(MarkerId markerId) { + final MarkerController markerController = _markerIdToController[markerId]; + markerController?.remove(); + _markerIdToController.remove(markerId); + } + + // InfoWindow... + + /// Shows the [InfoWindow] of a [MarkerId]. + /// + /// See also [hideMarkerInfoWindow] and [isInfoWindowShown]. + void showMarkerInfoWindow(MarkerId markerId) { + MarkerController markerController = _markerIdToController[markerId]; + markerController?.showInfoWindow(); + } + + /// Hides the [InfoWindow] of a [MarkerId]. + /// + /// See also [showMarkerInfoWindow] and [isInfoWindowShown]. + void hideMarkerInfoWindow(MarkerId markerId) { + MarkerController markerController = _markerIdToController[markerId]; + markerController?.hideInfoWindow(); + } + + /// Returns whether or not the [InfoWindow] of a [MarkerId] is shown. + /// + /// See also [showMarkerInfoWindow] and [hideMarkerInfoWindow]. + bool isInfoWindowShown(MarkerId markerId) { + MarkerController markerController = _markerIdToController[markerId]; + return markerController?.infoWindowShown ?? false; + } + + // Handle internal events + + bool _onMarkerTap(MarkerId markerId) { + // Have you ended here on your debugging? Is this wrong? + // Comment here: https://github.com/flutter/flutter/issues/64084 + _streamController.add(MarkerTapEvent(mapId, markerId)); + return _markerIdToController[markerId]?.consumeTapEvents ?? false; + } + + void _onInfoWindowTap(MarkerId markerId) { + _streamController.add(InfoWindowTapEvent(mapId, markerId)); + } + + void _onMarkerDragEnd(MarkerId markerId, gmaps.LatLng latLng) { + _streamController.add(MarkerDragEndEvent( + mapId, + _gmLatLngToLatLng(latLng), + markerId, + )); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygon.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygon.dart new file mode 100644 index 000000000000..f4c692d2ee83 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygon.dart @@ -0,0 +1,45 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of google_maps_flutter_web; + +/// The `PolygonController` class wraps a [gmaps.Polygon] and its `onTap` behavior. +class PolygonController { + gmaps.Polygon _polygon; + + final bool _consumeTapEvents; + + /// Creates a `PolygonController` that wraps a [gmaps.Polygon] object and its `onTap` behavior. + PolygonController({ + @required gmaps.Polygon polygon, + bool consumeTapEvents = false, + ui.VoidCallback onTap, + }) : _polygon = polygon, + _consumeTapEvents = consumeTapEvents { + if (onTap != null) { + polygon.onClick.listen((event) { + onTap.call(); + }); + } + } + + /// Returns the wrapped [gmaps.Polygon]. Only used for testing. + @visibleForTesting + gmaps.Polygon get polygon => _polygon; + + /// Returns `true` if this Controller will use its own `onTap` handler to consume events. + bool get consumeTapEvents => _consumeTapEvents; + + /// Updates the options of the wrapped [gmaps.Polygon] object. + void update(gmaps.PolygonOptions options) { + _polygon.options = options; + } + + /// Disposes of the currently wrapped [gmaps.Polygon]. + void remove() { + _polygon.visible = false; + _polygon.map = null; + _polygon = null; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart new file mode 100644 index 000000000000..5c078ea0aa7a --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart @@ -0,0 +1,85 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of google_maps_flutter_web; + +/// This class manages a set of [PolygonController]s associated to a [GoogleMapController]. +class PolygonsController extends GeometryController { + // A cache of [PolygonController]s indexed by their [PolygonId]. + final Map _polygonIdToController; + + // The stream over which polygons broadcast events + StreamController _streamController; + + /// Initializes the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. + PolygonsController({ + @required StreamController stream, + }) : _streamController = stream, + _polygonIdToController = Map(); + + /// Returns the cache of [PolygonController]s. Test only. + @visibleForTesting + Map get polygons => _polygonIdToController; + + /// Adds a set of [Polygon] objects to the cache. + /// + /// Wraps each Polygon into its corresponding [PolygonController]. + void addPolygons(Set polygonsToAdd) { + if (polygonsToAdd != null) { + polygonsToAdd.forEach((polygon) { + _addPolygon(polygon); + }); + } + } + + void _addPolygon(Polygon polygon) { + if (polygon == null) { + return; + } + + final populationOptions = _polygonOptionsFromPolygon(googleMap, polygon); + gmaps.Polygon gmPolygon = gmaps.Polygon(populationOptions); + gmPolygon.map = googleMap; + PolygonController controller = PolygonController( + polygon: gmPolygon, + consumeTapEvents: polygon.consumeTapEvents, + onTap: () { + _onPolygonTap(polygon.polygonId); + }); + _polygonIdToController[polygon.polygonId] = controller; + } + + /// Updates a set of [Polygon] objects with new options. + void changePolygons(Set polygonsToChange) { + if (polygonsToChange != null) { + polygonsToChange.forEach((polygonToChange) { + _changePolygon(polygonToChange); + }); + } + } + + void _changePolygon(Polygon polygon) { + PolygonController polygonController = + _polygonIdToController[polygon?.polygonId]; + polygonController?.update(_polygonOptionsFromPolygon(googleMap, polygon)); + } + + /// Removes a set of [PolygonId]s from the cache. + void removePolygons(Set polygonIdsToRemove) { + polygonIdsToRemove?.forEach((polygonId) { + final PolygonController polygonController = + _polygonIdToController[polygonId]; + polygonController?.remove(); + _polygonIdToController.remove(polygonId); + }); + } + + // Handle internal events + bool _onPolygonTap(PolygonId polygonId) { + // Have you ended here on your debugging? Is this wrong? + // Comment here: https://github.com/flutter/flutter/issues/64084 + _streamController.add(PolygonTapEvent(mapId, polygonId)); + return _polygonIdToController[polygonId]?.consumeTapEvents ?? false; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polyline.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polyline.dart new file mode 100644 index 000000000000..f8ff2c62191d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polyline.dart @@ -0,0 +1,45 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of google_maps_flutter_web; + +/// The `PolygonController` class wraps a [gmaps.Polyline] and its `onTap` behavior. +class PolylineController { + gmaps.Polyline _polyline; + + final bool _consumeTapEvents; + + /// Creates a `PolylineController` that wraps a [gmaps.Polyline] object and its `onTap` behavior. + PolylineController({ + @required gmaps.Polyline polyline, + bool consumeTapEvents = false, + ui.VoidCallback onTap, + }) : _polyline = polyline, + _consumeTapEvents = consumeTapEvents { + if (onTap != null) { + polyline.onClick.listen((event) { + onTap.call(); + }); + } + } + + /// Returns the wrapped [gmaps.Polyline]. Only used for testing. + @visibleForTesting + gmaps.Polyline get line => _polyline; + + /// Returns `true` if this Controller will use its own `onTap` handler to consume events. + bool get consumeTapEvents => _consumeTapEvents; + + /// Updates the options of the wrapped [gmaps.Polyline] object. + void update(gmaps.PolylineOptions options) { + _polyline.options = options; + } + + /// Disposes of the currently wrapped [gmaps.Polyline]. + void remove() { + _polyline.visible = false; + _polyline.map = null; + _polyline = null; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart new file mode 100644 index 000000000000..f24ca4b1bb42 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart @@ -0,0 +1,83 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of google_maps_flutter_web; + +/// This class manages a set of [PolylinesController]s associated to a [GoogleMapController]. +class PolylinesController extends GeometryController { + // A cache of [PolylineController]s indexed by their [PolylineId]. + final Map _polylineIdToController; + + // The stream over which polylines broadcast their events + StreamController _streamController; + + /// Initializes the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. + PolylinesController({ + @required StreamController stream, + }) : _streamController = stream, + _polylineIdToController = Map(); + + /// Returns the cache of [PolylineContrller]s. Test only. + @visibleForTesting + Map get lines => _polylineIdToController; + + /// Adds a set of [Polyline] objects to the cache. + /// + /// Wraps each line into its corresponding [PolylineController]. + void addPolylines(Set polylinesToAdd) { + polylinesToAdd?.forEach((polyline) { + _addPolyline(polyline); + }); + } + + void _addPolyline(Polyline polyline) { + if (polyline == null) { + return; + } + + final polylineOptions = _polylineOptionsFromPolyline(googleMap, polyline); + gmaps.Polyline gmPolyline = gmaps.Polyline(polylineOptions); + gmPolyline.map = googleMap; + PolylineController controller = PolylineController( + polyline: gmPolyline, + consumeTapEvents: polyline.consumeTapEvents, + onTap: () { + _onPolylineTap(polyline.polylineId); + }); + _polylineIdToController[polyline.polylineId] = controller; + } + + /// Updates a set of [Polyline] objects with new options. + void changePolylines(Set polylinesToChange) { + polylinesToChange?.forEach((polylineToChange) { + _changePolyline(polylineToChange); + }); + } + + void _changePolyline(Polyline polyline) { + PolylineController polylineController = + _polylineIdToController[polyline?.polylineId]; + polylineController + ?.update(_polylineOptionsFromPolyline(googleMap, polyline)); + } + + /// Removes a set of [PolylineId]s from the cache. + void removePolylines(Set polylineIdsToRemove) { + polylineIdsToRemove?.forEach((polylineId) { + final PolylineController polylineController = + _polylineIdToController[polylineId]; + polylineController?.remove(); + _polylineIdToController.remove(polylineId); + }); + } + + // Handle internal events + + bool _onPolylineTap(PolylineId polylineId) { + // Have you ended here on your debugging? Is this wrong? + // Comment here: https://github.com/flutter/flutter/issues/64084 + _streamController.add(PolylineTapEvent(mapId, polylineId)); + return _polylineIdToController[polylineId]?.consumeTapEvents ?? false; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/types.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/types.dart new file mode 100644 index 000000000000..039cc473db5e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/types.dart @@ -0,0 +1,30 @@ +// Copyright 2017 The Chromium 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 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; +import 'package:google_maps/google_maps.dart' as gmaps; + +/// A void function that handles a [gmaps.LatLng] as a parameter. +/// +/// Similar to [ui.VoidCallback], but specific for Marker drag events. +typedef LatLngCallback = void Function(gmaps.LatLng latLng); + +/// The base class for all "geometry" group controllers. +/// +/// This lets all Geometry controllers ([MarkersController], [CirclesController], +/// [PolygonsController], [PolylinesController]) to be bound to a [gmaps.GMap] +/// instance and our internal `mapId` value. +abstract class GeometryController { + /// The GMap instance that this controller operates on. + gmaps.GMap googleMap; + + /// The map ID for events. + int mapId; + + /// Binds a `mapId` and the [gmaps.GMap] instance to this controller. + void bindToMap(int mapId, gmaps.GMap googleMap) { + this.mapId = mapId; + this.googleMap = googleMap; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml new file mode 100644 index 000000000000..a4a734ac773e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -0,0 +1,34 @@ +name: google_maps_flutter_web +description: Web platform implementation of google_maps_flutter +homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter +version: 0.1.0 + +flutter: + plugin: + platforms: + web: + pluginClass: GoogleMapsPlugin + fileName: google_maps_flutter_web.dart + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + meta: ^1.1.7 + google_maps_flutter_platform_interface: ^1.0.4 + google_maps: ^3.0.0 + stream_transform: ^1.2.0 + sanitize_html: ^1.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + url_launcher: ^5.2.5 + pedantic: ^1.8.0 + mockito: ^4.1.1 + e2e: ^0.6.1 + +environment: + sdk: ">=2.3.0 <3.0.0" + flutter: ">=1.10.0 <2.0.0" diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/README.md b/packages/google_maps_flutter/google_maps_flutter_web/test/README.md new file mode 100644 index 000000000000..6eae799ce11c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/README.md @@ -0,0 +1,17 @@ +# Running browser_tests + +Make sure you have updated to the latest Flutter master. + +1. Check what version of Chrome is running on the machine you're running tests on. + +2. Download and install driver for that version from here: + * + +3. Start the driver using `chromedriver --port=4444` + +4. Change into the `test` directory of your clone. + +5. Run tests: `flutter drive -d web-server --browser-name=chrome --target=test_driver/TEST_NAME_e2e.dart`, or (in Linux): + + * Single: `./run_test test_driver/TEST_NAME_e2e.dart` + * All: `./run_test` diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/lib/main.dart new file mode 100644 index 000000000000..10415204570c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/lib/main.dart @@ -0,0 +1,22 @@ +// 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 'package:flutter/material.dart'; + +void main() { + runApp(MyApp()); +} + +/// App for testing +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + return Text('Testing... Look at the console output for results!'); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/test/pubspec.yaml new file mode 100644 index 000000000000..ec091f00a56f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/pubspec.yaml @@ -0,0 +1,22 @@ +name: regular_integration_tests +publish_to: none + +environment: + sdk: ">=2.2.2 <3.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + google_maps_flutter_web: + path: ../ + google_maps: ^3.4.4 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + e2e: ^0.6.1 + http: ^0.12.2 + mockito: ^4.1.1 + diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/run_test b/packages/google_maps_flutter/google_maps_flutter_web/test/run_test new file mode 100755 index 000000000000..4b43cf0947dc --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/run_test @@ -0,0 +1,17 @@ +#!/usr/bin/bash +if pgrep -lf chromedriver > /dev/null; then + echo "chromedriver is running." + + if [ $# -eq 0 ]; then + echo "No target specified, running all tests..." + find test_driver/ -iname *_e2e.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --target='{}' + else + echo "Running test target: $1..." + set -x + flutter drive -d web-server --web-port=7357 --browser-name=chrome --target=$1 + fi + + else + echo "chromedriver is not running." +fi + diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_controller_e2e.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_controller_e2e.dart new file mode 100644 index 000000000000..fac1956bb9f8 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_controller_e2e.dart @@ -0,0 +1,513 @@ +// Copyright 2017 The Chromium 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 'package:e2e/e2e.dart'; +import 'package:google_maps/google_maps.dart' as gmaps; +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +class _MockCirclesController extends Mock implements CirclesController {} + +class _MockPolygonsController extends Mock implements PolygonsController {} + +class _MockPolylinesController extends Mock implements PolylinesController {} + +class _MockMarkersController extends Mock implements MarkersController {} + +class _MockGMap extends Mock implements gmaps.GMap { + final onClickController = StreamController.broadcast(); + @override + Stream get onClick => onClickController.stream; + + final onRightclickController = StreamController.broadcast(); + @override + Stream get onRightclick => onRightclickController.stream; + + final onBoundsChangedController = StreamController.broadcast(); + @override + Stream get onBoundsChanged => onBoundsChangedController.stream; + + final onIdleController = StreamController.broadcast(); + @override + Stream get onIdle => onIdleController.stream; +} + +/// Test Google Map Controller +void main() { + E2EWidgetsFlutterBinding.ensureInitialized() as E2EWidgetsFlutterBinding; + + group('GoogleMapController', () { + final int mapId = 33930; + GoogleMapController controller; + StreamController stream; + + // Creates a controller with the default mapId and stream controller, and any `options` needed. + GoogleMapController _createController({Map options}) { + return GoogleMapController( + mapId: mapId, + streamController: stream, + rawOptions: options ?? {}); + } + + setUp(() { + stream = StreamController.broadcast(); + }); + + group('construct/dispose', () { + setUp(() { + controller = _createController(); + }); + + testWidgets('constructor creates widget', (WidgetTester tester) async { + expect(controller.widget, isNotNull); + expect(controller.widget.viewType, endsWith('$mapId')); + }); + + testWidgets('widget is cached when reused', (WidgetTester tester) async { + final first = controller.widget; + final again = controller.widget; + expect(identical(first, again), isTrue); + }); + + testWidgets('dispose closes the stream and removes the widget', + (WidgetTester tester) async { + controller.dispose(); + expect(stream.isClosed, isTrue); + expect(controller.widget, isNull); + }); + }); + + group('init', () { + _MockCirclesController circles; + _MockMarkersController markers; + _MockPolygonsController polygons; + _MockPolylinesController polylines; + _MockGMap map; + + setUp(() { + circles = _MockCirclesController(); + markers = _MockMarkersController(); + polygons = _MockPolygonsController(); + polylines = _MockPolylinesController(); + map = _MockGMap(); + }); + + testWidgets('listens to map events', (WidgetTester tester) async { + controller = _createController(); + controller.debugSetOverrides( + createMap: (_, __) => map, + circles: circles, + markers: markers, + polygons: polygons, + polylines: polylines, + ); + + expect(map.onClickController.hasListener, isFalse); + expect(map.onRightclickController.hasListener, isFalse); + expect(map.onBoundsChangedController.hasListener, isFalse); + expect(map.onIdleController.hasListener, isFalse); + + controller.init(); + + expect(map.onClickController.hasListener, isTrue); + expect(map.onRightclickController.hasListener, isTrue); + expect(map.onBoundsChangedController.hasListener, isTrue); + expect(map.onIdleController.hasListener, isTrue); + }); + + testWidgets('binds geometry controllers to map\'s', + (WidgetTester tester) async { + controller = _createController(); + controller.debugSetOverrides( + createMap: (_, __) => map, + circles: circles, + markers: markers, + polygons: polygons, + polylines: polylines, + ); + + controller.init(); + + verify(circles.bindToMap(mapId, map)); + verify(markers.bindToMap(mapId, map)); + verify(polygons.bindToMap(mapId, map)); + verify(polylines.bindToMap(mapId, map)); + }); + + testWidgets('renders initial geometry', (WidgetTester tester) async { + controller = _createController(options: { + 'circlesToAdd': [ + {'circleId': 'circle-1'} + ], + 'markersToAdd': [ + {'markerId': 'marker-1'} + ], + 'polygonsToAdd': [ + {'polygonId': 'polygon-1'} + ], + 'polylinesToAdd': [ + {'polylineId': 'polyline-1'} + ], + }); + controller.debugSetOverrides( + circles: circles, + markers: markers, + polygons: polygons, + polylines: polylines, + ); + + controller.init(); + + final capturedCircles = + verify(circles.addCircles(captureAny)).captured[0] as Set; + final capturedMarkers = + verify(markers.addMarkers(captureAny)).captured[0] as Set; + final capturedPolygons = verify(polygons.addPolygons(captureAny)) + .captured[0] as Set; + final capturedPolylines = verify(polylines.addPolylines(captureAny)) + .captured[0] as Set; + + expect(capturedCircles.first.circleId.value, 'circle-1'); + expect(capturedMarkers.first.markerId.value, 'marker-1'); + expect(capturedPolygons.first.polygonId.value, 'polygon-1'); + expect(capturedPolylines.first.polylineId.value, 'polyline-1'); + }); + + group('Initialization options', () { + gmaps.MapOptions capturedOptions; + setUp(() { + capturedOptions = null; + }); + testWidgets('translates initial options', (WidgetTester tester) async { + controller = _createController(options: { + 'options': { + 'mapType': 2, + 'zoomControlsEnabled': true, + } + }); + controller.debugSetOverrides(createMap: (_, options) { + capturedOptions = options; + return map; + }); + + controller.init(); + + expect(capturedOptions, isNotNull); + expect(capturedOptions.mapTypeId, gmaps.MapTypeId.SATELLITE); + expect(capturedOptions.zoomControl, true); + expect(capturedOptions.gestureHandling, 'auto', + reason: + 'by default the map handles zoom/pan gestures internally'); + }); + + testWidgets('disables gestureHandling with scrollGesturesEnabled false', + (WidgetTester tester) async { + controller = _createController(options: { + 'options': { + 'scrollGesturesEnabled': false, + } + }); + controller.debugSetOverrides(createMap: (_, options) { + capturedOptions = options; + return map; + }); + + controller.init(); + + expect(capturedOptions, isNotNull); + expect(capturedOptions.gestureHandling, 'none', + reason: + 'disabling scroll gestures disables all gesture handling'); + }); + + testWidgets('disables gestureHandling with zoomGesturesEnabled false', + (WidgetTester tester) async { + controller = _createController(options: { + 'options': { + 'zoomGesturesEnabled': false, + } + }); + controller.debugSetOverrides(createMap: (_, options) { + capturedOptions = options; + return map; + }); + + controller.init(); + + expect(capturedOptions, isNotNull); + expect(capturedOptions.gestureHandling, 'none', + reason: + 'disabling scroll gestures disables all gesture handling'); + }); + + testWidgets('does not set initial position if absent', + (WidgetTester tester) async { + controller = _createController(); + controller.debugSetOverrides(createMap: (_, options) { + capturedOptions = options; + return map; + }); + + controller.init(); + + expect(capturedOptions, isNotNull); + expect(capturedOptions.zoom, isNull); + expect(capturedOptions.center, isNull); + }); + + testWidgets('sets initial position when passed', + (WidgetTester tester) async { + controller = _createController(options: { + 'initialCameraPosition': { + 'target': [43.308, -5.6910], + 'zoom': 12, + 'bearing': 0, + 'tilt': 0, + } + }); + controller.debugSetOverrides(createMap: (_, options) { + capturedOptions = options; + return map; + }); + + controller.init(); + + expect(capturedOptions, isNotNull); + expect(capturedOptions.zoom, 12); + expect(capturedOptions.center, isNotNull); + }); + }); + + group('Traffic Layer', () { + testWidgets('by default is disabled', (WidgetTester tester) async { + controller = _createController(); + controller.init(); + expect(controller.trafficLayer, isNull); + }); + + testWidgets('initializes with traffic layer', + (WidgetTester tester) async { + controller = _createController(options: { + 'options': { + 'trafficEnabled': true, + } + }); + controller.debugSetOverrides(createMap: (_, __) => map); + controller.init(); + expect(controller.trafficLayer, isNotNull); + }); + }); + }); + + // These are the methods that are delegated to the gmaps.GMap object, that we can mock... + group('Map control methods', () { + _MockGMap map; + + setUp(() { + map = _MockGMap(); + controller = _createController(); + controller.debugSetOverrides(createMap: (_, __) => map); + controller.init(); + }); + + group('updateRawOptions', () { + testWidgets('can update `options`', (WidgetTester tester) async { + controller.updateRawOptions({ + 'mapType': 2, + }); + final options = verify(map.options = captureAny).captured[0]; + + expect(options.mapTypeId, gmaps.MapTypeId.SATELLITE); + }); + + testWidgets('can turn on/off traffic', (WidgetTester tester) async { + expect(controller.trafficLayer, isNull); + + controller.updateRawOptions({ + 'trafficEnabled': true, + }); + + expect(controller.trafficLayer, isNotNull); + + controller.updateRawOptions({ + 'trafficEnabled': false, + }); + + expect(controller.trafficLayer, isNull); + }); + }); + + group('viewport getters', () { + testWidgets('getVisibleRegion', (WidgetTester tester) async { + await controller.getVisibleRegion(); + + verify(map.bounds); + }); + + testWidgets('getZoomLevel', (WidgetTester tester) async { + when(map.zoom).thenReturn(10); + + await controller.getZoomLevel(); + + verify(map.zoom); + }); + }); + + group('moveCamera', () { + testWidgets('newLatLngZoom', (WidgetTester tester) async { + await (controller + .moveCamera(CameraUpdate.newLatLngZoom(LatLng(19, 26), 12))); + + verify(map.zoom = 12); + final captured = verify(map.panTo(captureAny)).captured[0]; + expect(captured.lat, 19); + expect(captured.lng, 26); + }); + }); + + group('map.projection methods', () { + // These are too much for dart mockito, can't mock: + // map.projection.method() (in Javascript ;) ) + }); + }); + + // These are the methods that get forwarded to other controllers, so we just verify calls. + group('Pass-through methods', () { + setUp(() { + controller = _createController(); + }); + + testWidgets('updateCircles', (WidgetTester tester) async { + final mock = _MockCirclesController(); + controller.debugSetOverrides(circles: mock); + + final previous = { + Circle(circleId: CircleId('to-be-updated')), + Circle(circleId: CircleId('to-be-removed')), + }; + + final current = { + Circle(circleId: CircleId('to-be-updated'), visible: false), + Circle(circleId: CircleId('to-be-added')), + }; + + controller.updateCircles(CircleUpdates.from(previous, current)); + + verify(mock.removeCircles({ + CircleId('to-be-removed'), + })); + verify(mock.addCircles({ + Circle(circleId: CircleId('to-be-added')), + })); + verify(mock.changeCircles({ + Circle(circleId: CircleId('to-be-updated'), visible: false), + })); + }); + + testWidgets('updateMarkers', (WidgetTester tester) async { + final mock = _MockMarkersController(); + controller.debugSetOverrides(markers: mock); + + final previous = { + Marker(markerId: MarkerId('to-be-updated')), + Marker(markerId: MarkerId('to-be-removed')), + }; + + final current = { + Marker(markerId: MarkerId('to-be-updated'), visible: false), + Marker(markerId: MarkerId('to-be-added')), + }; + + controller.updateMarkers(MarkerUpdates.from(previous, current)); + + verify(mock.removeMarkers({ + MarkerId('to-be-removed'), + })); + verify(mock.addMarkers({ + Marker(markerId: MarkerId('to-be-added')), + })); + verify(mock.changeMarkers({ + Marker(markerId: MarkerId('to-be-updated'), visible: false), + })); + }); + + testWidgets('updatePolygons', (WidgetTester tester) async { + final mock = _MockPolygonsController(); + controller.debugSetOverrides(polygons: mock); + + final previous = { + Polygon(polygonId: PolygonId('to-be-updated')), + Polygon(polygonId: PolygonId('to-be-removed')), + }; + + final current = { + Polygon(polygonId: PolygonId('to-be-updated'), visible: false), + Polygon(polygonId: PolygonId('to-be-added')), + }; + + controller.updatePolygons(PolygonUpdates.from(previous, current)); + + verify(mock.removePolygons({ + PolygonId('to-be-removed'), + })); + verify(mock.addPolygons({ + Polygon(polygonId: PolygonId('to-be-added')), + })); + verify(mock.changePolygons({ + Polygon(polygonId: PolygonId('to-be-updated'), visible: false), + })); + }); + + testWidgets('updatePolylines', (WidgetTester tester) async { + final mock = _MockPolylinesController(); + controller.debugSetOverrides(polylines: mock); + + final previous = { + Polyline(polylineId: PolylineId('to-be-updated')), + Polyline(polylineId: PolylineId('to-be-removed')), + }; + + final current = { + Polyline(polylineId: PolylineId('to-be-updated'), visible: false), + Polyline(polylineId: PolylineId('to-be-added')), + }; + + controller.updatePolylines(PolylineUpdates.from(previous, current)); + + verify(mock.removePolylines({ + PolylineId('to-be-removed'), + })); + verify(mock.addPolylines({ + Polyline(polylineId: PolylineId('to-be-added')), + })); + verify(mock.changePolylines({ + Polyline(polylineId: PolylineId('to-be-updated'), visible: false), + })); + }); + + testWidgets('infoWindow visibility', (WidgetTester tester) async { + final mock = _MockMarkersController(); + controller.debugSetOverrides(markers: mock); + final markerId = MarkerId('marker-with-infowindow'); + + controller.showInfoWindow(markerId); + + verify(mock.showMarkerInfoWindow(markerId)); + + controller.hideInfoWindow(markerId); + + verify(mock.hideMarkerInfoWindow(markerId)); + + controller.isInfoWindowShown(markerId); + + verify(mock.isInfoWindowShown(markerId)); + }); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_controller_e2e_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_controller_e2e_test.dart new file mode 100644 index 000000000000..efa93a120e78 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_controller_e2e_test.dart @@ -0,0 +1,7 @@ +// Copyright 2017 The Chromium 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 'package:e2e/e2e_driver.dart' as e2e; + +Future main() async => e2e.main(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_plugin_e2e.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_plugin_e2e.dart new file mode 100644 index 000000000000..80ec5400bdd7 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_plugin_e2e.dart @@ -0,0 +1,392 @@ +// Copyright 2017 The Chromium 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 'package:e2e/e2e.dart'; +import 'package:flutter/widgets.dart'; +import 'package:google_maps/google_maps.dart' as gmaps; +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +class _MockGoogleMapController extends Mock implements GoogleMapController {} + +/// Test GoogleMapsPlugin +void main() { + E2EWidgetsFlutterBinding.ensureInitialized() as E2EWidgetsFlutterBinding; + + group('GoogleMapsPlugin', () { + _MockGoogleMapController controller; + GoogleMapsPlugin plugin; + int reportedMapId; + + void onPlatformViewCreated(int id) { + reportedMapId = id; + } + + setUp(() { + controller = _MockGoogleMapController(); + plugin = GoogleMapsPlugin(); + reportedMapId = null; + }); + + group('init/dispose', () { + group('before buildWidget', () { + testWidgets('init throws assertion', (WidgetTester tester) async { + expect(() => plugin.init(0), throwsAssertionError); + }); + }); + + group('after buildWidget', () { + setUp(() { + plugin.debugSetMapById({0: controller}); + }); + + testWidgets('init initializes controller', (WidgetTester tester) async { + await plugin.init(0); + + verify(controller.init()); + }); + + testWidgets('cannot call methods after dispose', + (WidgetTester tester) async { + plugin.dispose(mapId: 0); + + verify(controller.dispose()); + expect( + () => plugin.init(0), + throwsAssertionError, + reason: 'Method calls should fail after dispose.', + ); + }); + }); + }); + + group('buildView', () { + final testMapId = 33930; + + testWidgets('throws without _webOnlyMapCreationId', + (WidgetTester tester) async { + expect( + () => plugin.buildView({}, null, onPlatformViewCreated), + throwsAssertionError, + reason: + '_webOnlyMapCreationId is mandatory to prevent unnecessary reloads in web.', + ); + }); + + testWidgets( + 'returns an HtmlElementView and caches the controller for later', + (WidgetTester tester) async { + final Map cache = {}; + plugin.debugSetMapById(cache); + + final HtmlElementView widget = plugin.buildView({ + '_webOnlyMapCreationId': testMapId, + }, null, onPlatformViewCreated); + + expect( + widget.viewType, + contains('$testMapId'), + reason: + 'view type should contain the mapId passed when creating the map.', + ); + expect( + reportedMapId, + testMapId, + reason: 'Should call onPlatformViewCreated with the mapId', + ); + expect(cache, contains(testMapId)); + expect( + cache[testMapId], + isNotNull, + reason: 'cached controller cannot be null.', + ); + }); + + testWidgets('returns cached instance if it already exists', + (WidgetTester tester) async { + final expected = HtmlElementView(viewType: 'only-for-testing'); + when(controller.widget).thenReturn(expected); + plugin.debugSetMapById({testMapId: controller}); + + final widget = plugin.buildView({ + '_webOnlyMapCreationId': testMapId, + }, null, onPlatformViewCreated); + + expect(widget, equals(expected)); + expect( + reportedMapId, + isNull, + reason: + 'onPlatformViewCreated should not be called when returning a cached controller', + ); + }); + }); + + group('setMapStyles', () { + String mapStyle = '''[{ + "featureType": "poi.park", + "elementType": "labels.text.fill", + "stylers": [{"color": "#6b9a76"}] + }]'''; + + testWidgets('translates styles for controller', + (WidgetTester tester) async { + plugin.debugSetMapById({0: controller}); + + await plugin.setMapStyle(mapStyle, mapId: 0); + + var captured = + verify(controller.updateRawOptions(captureThat(isMap))).captured[0]; + + expect(captured, contains('styles')); + var styles = captured['styles']; + expect(styles.length, 1); + // Let's peek inside the styles... + var style = styles[0] as gmaps.MapTypeStyle; + expect(style.featureType, gmaps.MapTypeStyleFeatureType.POI_PARK); + expect( + style.elementType, gmaps.MapTypeStyleElementType.LABELS_TEXT_FILL); + expect(style.stylers.length, 1); + expect(style.stylers[0].color, '#6b9a76'); + }); + }); + + // These methods only pass-through values from the plugin to the controller + // so we verify them all together here... + group('Pass-through methods:', () { + int mapId = 0; + setUp(() { + plugin.debugSetMapById({mapId: controller}); + }); + // Options + testWidgets('updateMapOptions', (WidgetTester tester) async { + final expectedMapOptions = {'someOption': 12345}; + + await plugin.updateMapOptions(expectedMapOptions, mapId: mapId); + + verify(controller.updateRawOptions(expectedMapOptions)); + }); + // Geometry + testWidgets('updateMarkers', (WidgetTester tester) async { + final expectedUpdates = MarkerUpdates.from(null, null); + + await plugin.updateMarkers(expectedUpdates, mapId: mapId); + + verify(controller.updateMarkers(expectedUpdates)); + }); + testWidgets('updatePolygons', (WidgetTester tester) async { + final expectedUpdates = PolygonUpdates.from(null, null); + + await plugin.updatePolygons(expectedUpdates, mapId: mapId); + + verify(controller.updatePolygons(expectedUpdates)); + }); + testWidgets('updatePolylines', (WidgetTester tester) async { + final expectedUpdates = PolylineUpdates.from(null, null); + + await plugin.updatePolylines(expectedUpdates, mapId: mapId); + + verify(controller.updatePolylines(expectedUpdates)); + }); + testWidgets('updateCircles', (WidgetTester tester) async { + final expectedUpdates = CircleUpdates.from(null, null); + + await plugin.updateCircles(expectedUpdates, mapId: mapId); + + verify(controller.updateCircles(expectedUpdates)); + }); + // Camera + testWidgets('animateCamera', (WidgetTester tester) async { + final expectedUpdates = + CameraUpdate.newLatLng(LatLng(43.3626, -5.8433)); + + await plugin.animateCamera(expectedUpdates, mapId: mapId); + + verify(controller.moveCamera(expectedUpdates)); + }); + testWidgets('moveCamera', (WidgetTester tester) async { + final expectedUpdates = + CameraUpdate.newLatLng(LatLng(43.3628, -5.8478)); + + await plugin.moveCamera(expectedUpdates, mapId: mapId); + + verify(controller.moveCamera(expectedUpdates)); + }); + // Viewport + testWidgets('getVisibleRegion', (WidgetTester tester) async { + await plugin.getVisibleRegion(mapId: mapId); + + verify(controller.getVisibleRegion()); + }); + testWidgets('getZoomLevel', (WidgetTester tester) async { + await plugin.getZoomLevel(mapId: mapId); + + verify(controller.getZoomLevel()); + }); + testWidgets('getScreenCoordinate', (WidgetTester tester) async { + final latLng = LatLng(43.3613, -5.8499); + + await plugin.getScreenCoordinate(latLng, mapId: mapId); + + verify(controller.getScreenCoordinate(latLng)); + }); + testWidgets('getLatLng', (WidgetTester tester) async { + final coordinates = ScreenCoordinate(x: 19, y: 26); + + await plugin.getLatLng(coordinates, mapId: mapId); + + verify(controller.getLatLng(coordinates)); + }); + // InfoWindows + testWidgets('showMarkerInfoWindow', (WidgetTester tester) async { + final markerId = MarkerId('testing-123'); + + await plugin.showMarkerInfoWindow(markerId, mapId: mapId); + + verify(controller.showInfoWindow(markerId)); + }); + testWidgets('hideMarkerInfoWindow', (WidgetTester tester) async { + final markerId = MarkerId('testing-123'); + + await plugin.hideMarkerInfoWindow(markerId, mapId: mapId); + + verify(controller.hideInfoWindow(markerId)); + }); + testWidgets('isMarkerInfoWindowShown', (WidgetTester tester) async { + final markerId = MarkerId('testing-123'); + + await plugin.isMarkerInfoWindowShown(markerId, mapId: mapId); + + verify(controller.isInfoWindowShown(markerId)); + }); + }); + + // Verify all event streams are filtered correctly from the main one... + group('Event Streams', () { + int mapId = 0; + StreamController streamController; + setUp(() { + streamController = StreamController.broadcast(); + when(controller.events) + .thenAnswer((realInvocation) => streamController.stream); + plugin.debugSetMapById({mapId: controller}); + }); + + // Dispatches a few events in the global streamController, and expects *only* the passed event to be there. + void _testStreamFiltering(Stream stream, MapEvent event) async { + Timer.run(() { + streamController.add(_OtherMapEvent(mapId)); + streamController.add(event); + streamController.add(_OtherMapEvent(mapId)); + streamController.close(); + }); + + final events = await stream.toList(); + + expect(events.length, 1); + expect(events[0], event); + } + + // Camera events + testWidgets('onCameraMoveStarted', (WidgetTester tester) async { + final event = CameraMoveStartedEvent(mapId); + + final stream = plugin.onCameraMoveStarted(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onCameraMoveStarted', (WidgetTester tester) async { + final event = CameraMoveEvent( + mapId, + CameraPosition( + target: LatLng(43.3790, -5.8660), + ), + ); + + final stream = plugin.onCameraMove(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onCameraIdle', (WidgetTester tester) async { + final event = CameraIdleEvent(mapId); + + final stream = plugin.onCameraIdle(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + // Marker events + testWidgets('onMarkerTap', (WidgetTester tester) async { + final event = MarkerTapEvent(mapId, MarkerId('test-123')); + + final stream = plugin.onMarkerTap(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onInfoWindowTap', (WidgetTester tester) async { + final event = InfoWindowTapEvent(mapId, MarkerId('test-123')); + + final stream = plugin.onInfoWindowTap(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onMarkerDragEnd', (WidgetTester tester) async { + final event = MarkerDragEndEvent( + mapId, + LatLng(43.3677, -5.8372), + MarkerId('test-123'), + ); + + final stream = plugin.onMarkerDragEnd(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + // Geometry + testWidgets('onPolygonTap', (WidgetTester tester) async { + final event = PolygonTapEvent(mapId, PolygonId('test-123')); + + final stream = plugin.onPolygonTap(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onPolylineTap', (WidgetTester tester) async { + final event = PolylineTapEvent(mapId, PolylineId('test-123')); + + final stream = plugin.onPolylineTap(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onCircleTap', (WidgetTester tester) async { + final event = CircleTapEvent(mapId, CircleId('test-123')); + + final stream = plugin.onCircleTap(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + // Map taps + testWidgets('onTap', (WidgetTester tester) async { + final event = MapTapEvent(mapId, LatLng(43.3597, -5.8458)); + + final stream = plugin.onTap(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onLongPress', (WidgetTester tester) async { + final event = MapLongPressEvent(mapId, LatLng(43.3608, -5.8425)); + + final stream = plugin.onLongPress(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + }); + }); +} + +class _OtherMapEvent extends MapEvent { + _OtherMapEvent(int mapId) : super(mapId, null); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_plugin_e2e_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_plugin_e2e_test.dart new file mode 100644 index 000000000000..efa93a120e78 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_plugin_e2e_test.dart @@ -0,0 +1,7 @@ +// Copyright 2017 The Chromium 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 'package:e2e/e2e_driver.dart' as e2e; + +Future main() async => e2e.main(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/marker_e2e.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/marker_e2e.dart new file mode 100644 index 000000000000..7f9d9ba409e0 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/marker_e2e.dart @@ -0,0 +1,99 @@ +// Copyright 2017 The Chromium 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 'package:e2e/e2e.dart'; +import 'package:google_maps/google_maps.dart' as gmaps; +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +class _MockMarker extends Mock implements gmaps.Marker { + final onClickController = StreamController(); + final onDragEndController = StreamController(); + + @override + Stream get onClick => onClickController.stream; + + @override + Stream get onDragend => onDragEndController.stream; +} + +class _MockMouseEvent extends Mock implements gmaps.MouseEvent {} + +class _MockInfoWindow extends Mock implements gmaps.InfoWindow {} + +/// Test Markers +void main() { + E2EWidgetsFlutterBinding.ensureInitialized() as E2EWidgetsFlutterBinding; + + bool called = false; + void onTap() { + called = true; + } + + void onDragEnd(gmaps.LatLng _) { + called = true; + } + + setUp(() { + called = false; + }); + + group('MarkerController', () { + _MockMarker marker; + + setUp(() { + marker = _MockMarker(); + }); + + testWidgets('onTap gets called', (WidgetTester tester) async { + MarkerController(marker: marker, onTap: onTap); + // Simulate a click + await marker.onClickController.add(null); + expect(called, isTrue); + }); + + testWidgets('onDragEnd gets called', (WidgetTester tester) async { + when(marker.draggable).thenReturn(true); + MarkerController(marker: marker, onDragEnd: onDragEnd); + // Simulate a drag end + await marker.onDragEndController.add(_MockMouseEvent()); + expect(called, isTrue); + }); + + testWidgets('update', (WidgetTester tester) async { + final controller = MarkerController(marker: marker); + final options = gmaps.MarkerOptions()..draggable = false; + controller.update(options); + verify(marker.options = options); + }); + + testWidgets('infoWindow null, showInfoWindow.', + (WidgetTester tester) async { + final controller = MarkerController(marker: marker); + controller.showInfoWindow(); + expect(controller.infoWindowShown, isFalse); + }); + + testWidgets('showInfoWindow', (WidgetTester tester) async { + final infoWindow = _MockInfoWindow(); + final controller = + MarkerController(marker: marker, infoWindow: infoWindow); + controller.showInfoWindow(); + verify(infoWindow.open(any, any)).called(1); + expect(controller.infoWindowShown, isTrue); + }); + + testWidgets('hideInfoWindow', (WidgetTester tester) async { + final infoWindow = _MockInfoWindow(); + final controller = + MarkerController(marker: marker, infoWindow: infoWindow); + controller.hideInfoWindow(); + verify(infoWindow.close()).called(1); + expect(controller.infoWindowShown, isFalse); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/marker_e2e_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/marker_e2e_test.dart new file mode 100644 index 000000000000..efa93a120e78 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/marker_e2e_test.dart @@ -0,0 +1,7 @@ +// Copyright 2017 The Chromium 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 'package:e2e/e2e_driver.dart' as e2e; + +Future main() async => e2e.main(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/markers_e2e.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/markers_e2e.dart new file mode 100644 index 000000000000..53c34e9fa6eb --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/markers_e2e.dart @@ -0,0 +1,102 @@ +// Copyright 2017 The Chromium 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 'package:e2e/e2e.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + E2EWidgetsFlutterBinding.ensureInitialized() as E2EWidgetsFlutterBinding; + + group('MarkersController', () { + StreamController stream; + MarkersController controller; + + setUp(() { + stream = StreamController(); + controller = MarkersController(stream: stream); + }); + + testWidgets('addMarkers', (WidgetTester tester) async { + final markers = { + Marker(markerId: MarkerId('1')), + Marker(markerId: MarkerId('2')), + }; + + controller.addMarkers(markers); + + expect(controller.markers.length, 2); + expect(controller.markers, contains(MarkerId('1'))); + expect(controller.markers, contains(MarkerId('2'))); + expect(controller.markers, isNot(contains(MarkerId('66')))); + }); + + testWidgets('changeMarkers', (WidgetTester tester) async { + final markers = { + Marker(markerId: MarkerId('1')), + }; + controller.addMarkers(markers); + + expect(controller.markers[MarkerId('1')].marker.draggable, isFalse); + + // Update the marker with radius 10 + final updatedMarkers = { + Marker(markerId: MarkerId('1'), draggable: true), + }; + controller.changeMarkers(updatedMarkers); + + expect(controller.markers.length, 1); + expect(controller.markers[MarkerId('1')].marker.draggable, isTrue); + }); + + testWidgets('removeMarkers', (WidgetTester tester) async { + final markers = { + Marker(markerId: MarkerId('1')), + Marker(markerId: MarkerId('2')), + Marker(markerId: MarkerId('3')), + }; + + controller.addMarkers(markers); + + expect(controller.markers.length, 3); + + // Remove some markers... + final markerIdsToRemove = { + MarkerId('1'), + MarkerId('3'), + }; + + controller.removeMarkers(markerIdsToRemove); + + expect(controller.markers.length, 1); + expect(controller.markers, isNot(contains(MarkerId('1')))); + expect(controller.markers, contains(MarkerId('2'))); + expect(controller.markers, isNot(contains(MarkerId('3')))); + }); + + testWidgets('InfoWindow show/hide', (WidgetTester tester) async { + final markers = { + Marker( + markerId: MarkerId('1'), + infoWindow: InfoWindow(title: "Title", snippet: "Snippet"), + ), + }; + + controller.addMarkers(markers); + + expect(controller.markers[MarkerId('1')].infoWindowShown, isFalse); + + controller.showMarkerInfoWindow(MarkerId('1')); + + expect(controller.markers[MarkerId('1')].infoWindowShown, isTrue); + + controller.hideMarkerInfoWindow(MarkerId('1')); + + expect(controller.markers[MarkerId('1')].infoWindowShown, isFalse); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/markers_e2e_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/markers_e2e_test.dart new file mode 100644 index 000000000000..efa93a120e78 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/markers_e2e_test.dart @@ -0,0 +1,7 @@ +// Copyright 2017 The Chromium 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 'package:e2e/e2e_driver.dart' as e2e; + +Future main() async => e2e.main(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shape_e2e.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shape_e2e.dart new file mode 100644 index 000000000000..8a0e1d84facc --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shape_e2e.dart @@ -0,0 +1,113 @@ +// Copyright 2017 The Chromium 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 'package:e2e/e2e.dart'; +import 'package:google_maps/google_maps.dart' as gmaps; +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +class _MockCircle extends Mock implements gmaps.Circle { + final onClickController = StreamController(); + @override + Stream get onClick => onClickController.stream; +} + +class _MockPolygon extends Mock implements gmaps.Polygon { + final onClickController = StreamController(); + @override + Stream get onClick => onClickController.stream; +} + +class _MockPolyline extends Mock implements gmaps.Polyline { + final onClickController = StreamController(); + @override + Stream get onClick => onClickController.stream; +} + +/// Test Shapes (Circle, Polygon, Polyline) +void main() { + E2EWidgetsFlutterBinding.ensureInitialized() as E2EWidgetsFlutterBinding; + + bool called = false; + void onTap() { + called = true; + } + + setUp(() { + called = false; + }); + + group('CircleController', () { + _MockCircle circle; + + setUp(() { + circle = _MockCircle(); + }); + + testWidgets('onTap gets called', (WidgetTester tester) async { + CircleController(circle: circle, consumeTapEvents: true, onTap: onTap); + expect(circle.onClickController.hasListener, isTrue); + // Simulate a click + await circle.onClickController.add(null); + expect(called, isTrue); + }); + + testWidgets('update', (WidgetTester tester) async { + final controller = CircleController(circle: circle); + final options = gmaps.CircleOptions()..draggable = false; + controller.update(options); + verify(circle.options = options); + }); + }); + + group('PolygonController', () { + _MockPolygon polygon; + + setUp(() { + polygon = _MockPolygon(); + }); + + testWidgets('onTap gets called', (WidgetTester tester) async { + PolygonController(polygon: polygon, consumeTapEvents: true, onTap: onTap); + expect(polygon.onClickController.hasListener, isTrue); + // Simulate a click + await polygon.onClickController.add(null); + expect(called, isTrue); + }); + + testWidgets('update', (WidgetTester tester) async { + final controller = PolygonController(polygon: polygon); + final options = gmaps.PolygonOptions()..draggable = false; + controller.update(options); + verify(polygon.options = options); + }); + }); + + group('PolylineController', () { + _MockPolyline polyline; + + setUp(() { + polyline = _MockPolyline(); + }); + + testWidgets('onTap gets called', (WidgetTester tester) async { + PolylineController( + polyline: polyline, consumeTapEvents: true, onTap: onTap); + expect(polyline.onClickController.hasListener, isTrue); + // Simulate a click + await polyline.onClickController.add(null); + expect(called, isTrue); + }); + + testWidgets('update', (WidgetTester tester) async { + final controller = PolylineController(polyline: polyline); + final options = gmaps.PolylineOptions()..draggable = false; + controller.update(options); + verify(polyline.options = options); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shape_e2e_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shape_e2e_test.dart new file mode 100644 index 000000000000..efa93a120e78 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shape_e2e_test.dart @@ -0,0 +1,7 @@ +// Copyright 2017 The Chromium 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 'package:e2e/e2e_driver.dart' as e2e; + +Future main() async => e2e.main(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shapes_e2e.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shapes_e2e.dart new file mode 100644 index 000000000000..9ff3ad9455bb --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shapes_e2e.dart @@ -0,0 +1,214 @@ +// Copyright 2017 The Chromium 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 'package:e2e/e2e.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Test Shapes (Circle, Polygon, Polyline) +void main() { + E2EWidgetsFlutterBinding.ensureInitialized() as E2EWidgetsFlutterBinding; + + group('CirclesController', () { + StreamController stream; + CirclesController controller; + + setUp(() { + stream = StreamController(); + controller = CirclesController(stream: stream); + }); + + testWidgets('addCircles', (WidgetTester tester) async { + final circles = { + Circle(circleId: CircleId('1')), + Circle(circleId: CircleId('2')), + }; + + controller.addCircles(circles); + + expect(controller.circles.length, 2); + expect(controller.circles, contains(CircleId('1'))); + expect(controller.circles, contains(CircleId('2'))); + expect(controller.circles, isNot(contains(CircleId('66')))); + }); + + testWidgets('changeCircles', (WidgetTester tester) async { + final circles = { + Circle(circleId: CircleId('1')), + }; + controller.addCircles(circles); + + expect(controller.circles[CircleId('1')].circle.visible, isTrue); + + final updatedCircles = { + Circle(circleId: CircleId('1'), visible: false), + }; + controller.changeCircles(updatedCircles); + + expect(controller.circles.length, 1); + expect(controller.circles[CircleId('1')].circle.visible, isFalse); + }); + + testWidgets('removeCircles', (WidgetTester tester) async { + final circles = { + Circle(circleId: CircleId('1')), + Circle(circleId: CircleId('2')), + Circle(circleId: CircleId('3')), + }; + + controller.addCircles(circles); + + expect(controller.circles.length, 3); + + // Remove some circles... + final circleIdsToRemove = { + CircleId('1'), + CircleId('3'), + }; + + controller.removeCircles(circleIdsToRemove); + + expect(controller.circles.length, 1); + expect(controller.circles, isNot(contains(CircleId('1')))); + expect(controller.circles, contains(CircleId('2'))); + expect(controller.circles, isNot(contains(CircleId('3')))); + }); + }); + + group('PolygonsController', () { + StreamController stream; + PolygonsController controller; + + setUp(() { + stream = StreamController(); + controller = PolygonsController(stream: stream); + }); + + testWidgets('addPolygons', (WidgetTester tester) async { + final polygons = { + Polygon(polygonId: PolygonId('1')), + Polygon(polygonId: PolygonId('2')), + }; + + controller.addPolygons(polygons); + + expect(controller.polygons.length, 2); + expect(controller.polygons, contains(PolygonId('1'))); + expect(controller.polygons, contains(PolygonId('2'))); + expect(controller.polygons, isNot(contains(PolygonId('66')))); + }); + + testWidgets('changePolygons', (WidgetTester tester) async { + final polygons = { + Polygon(polygonId: PolygonId('1')), + }; + controller.addPolygons(polygons); + + expect(controller.polygons[PolygonId('1')].polygon.visible, isTrue); + + // Update the polygon + final updatedPolygons = { + Polygon(polygonId: PolygonId('1'), visible: false), + }; + controller.changePolygons(updatedPolygons); + + expect(controller.polygons.length, 1); + expect(controller.polygons[PolygonId('1')].polygon.visible, isFalse); + }); + + testWidgets('removePolygons', (WidgetTester tester) async { + final polygons = { + Polygon(polygonId: PolygonId('1')), + Polygon(polygonId: PolygonId('2')), + Polygon(polygonId: PolygonId('3')), + }; + + controller.addPolygons(polygons); + + expect(controller.polygons.length, 3); + + // Remove some polygons... + final polygonIdsToRemove = { + PolygonId('1'), + PolygonId('3'), + }; + + controller.removePolygons(polygonIdsToRemove); + + expect(controller.polygons.length, 1); + expect(controller.polygons, isNot(contains(PolygonId('1')))); + expect(controller.polygons, contains(PolygonId('2'))); + expect(controller.polygons, isNot(contains(PolygonId('3')))); + }); + }); + + group('PolylinesController', () { + StreamController stream; + PolylinesController controller; + + setUp(() { + stream = StreamController(); + controller = PolylinesController(stream: stream); + }); + + testWidgets('addPolylines', (WidgetTester tester) async { + final polylines = { + Polyline(polylineId: PolylineId('1')), + Polyline(polylineId: PolylineId('2')), + }; + + controller.addPolylines(polylines); + + expect(controller.lines.length, 2); + expect(controller.lines, contains(PolylineId('1'))); + expect(controller.lines, contains(PolylineId('2'))); + expect(controller.lines, isNot(contains(PolylineId('66')))); + }); + + testWidgets('changePolylines', (WidgetTester tester) async { + final polylines = { + Polyline(polylineId: PolylineId('1')), + }; + controller.addPolylines(polylines); + + expect(controller.lines[PolylineId('1')].line.visible, isTrue); + + final updatedPolylines = { + Polyline(polylineId: PolylineId('1'), visible: false), + }; + controller.changePolylines(updatedPolylines); + + expect(controller.lines.length, 1); + expect(controller.lines[PolylineId('1')].line.visible, isFalse); + }); + + testWidgets('removePolylines', (WidgetTester tester) async { + final polylines = { + Polyline(polylineId: PolylineId('1')), + Polyline(polylineId: PolylineId('2')), + Polyline(polylineId: PolylineId('3')), + }; + + controller.addPolylines(polylines); + + expect(controller.lines.length, 3); + + // Remove some polylines... + final polylineIdsToRemove = { + PolylineId('1'), + PolylineId('3'), + }; + + controller.removePolylines(polylineIdsToRemove); + + expect(controller.lines.length, 1); + expect(controller.lines, isNot(contains(PolylineId('1')))); + expect(controller.lines, contains(PolylineId('2'))); + expect(controller.lines, isNot(contains(PolylineId('3')))); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shapes_e2e_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shapes_e2e_test.dart new file mode 100644 index 000000000000..efa93a120e78 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shapes_e2e_test.dart @@ -0,0 +1,7 @@ +// Copyright 2017 The Chromium 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 'package:e2e/e2e_driver.dart' as e2e; + +Future main() async => e2e.main(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/web/index.html b/packages/google_maps_flutter/google_maps_flutter_web/test/web/index.html new file mode 100644 index 000000000000..3b7e4edc3df1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/web/index.html @@ -0,0 +1,14 @@ + + + + + Browser Tests + + + + + + + diff --git a/script/incremental_build.sh b/script/incremental_build.sh index ba8bf938d593..5351926b0051 100755 --- a/script/incremental_build.sh +++ b/script/incremental_build.sh @@ -17,6 +17,7 @@ CUSTOM_ANALYSIS_PLUGINS=( "in_app_purchase" "camera" "video_player/video_player_web" + "google_maps_flutter/google_maps_flutter_web" ) # Comma-separated string of the list above readonly CUSTOM_FLAG=$(IFS=, ; echo "${CUSTOM_ANALYSIS_PLUGINS[*]}")