From 70c314ce53cfd7c85d0ab498c8aaf334a69262d4 Mon Sep 17 00:00:00 2001 From: David Iglesias Date: Fri, 10 Sep 2021 17:36:22 -0700 Subject: [PATCH] [google_maps_flutter_web] Fix getScreenCoordinate, zIndex of Circles (#4298) This commit: * uses the zIndex attribute when converting Circle geometry objects. * ensures that the getScreenCoordinate method works as expected on the web platform. * adds tests that can use a fully-rendered Google Map (see projection_test.dart) * changes the initialization flow of the web Google Map, so the Controller is only returned to the main plugin when it's ready to work. In order to test the getScreenCoordinate method, the Controller of a fully-rendered map must be available on the test, so we can retrieve information from an actual map instance. While working on this, it was observed that the Controller was being sent to the programmer before it was truly ready (while the map was still initializing). Instead of littering the test with imprecise timeouts that may make these tests slower (and flakier) than needed, this PR also changes the initialization process of a GMap slightly so when its Controller is returned to the user of the plugin (onPlatformViewCreated method call), it is truly ready. For this: * Controller.init is immediately called after the controller is created, * The plugin waits for the first onTilesloaded event coming from the JS SDK, and then * The Controller is sent to the user This change happens within "private" sections of the plugin, so programmers using the plugin "normally" shouldn't notice any difference whatsoever (only that the GMap might load slightly faster, and the onPlatformViewCreated callback might be firing a few hundred milliseconds later). --- .../google_maps_flutter_web/CHANGELOG.md | 7 + .../google_maps_flutter_web/LICENSE | 26 ++ .../google_maps_controller_test.dart | 19 +- .../google_maps_controller_test.mocks.dart | 77 ++--- .../google_maps_plugin_test.dart | 60 ++-- .../google_maps_plugin_test.mocks.dart | 120 +++++--- .../integration_test/projection_test.dart | 265 ++++++++++++++++++ .../example/pubspec.yaml | 6 +- .../lib/google_maps_flutter_web.dart | 1 + .../lib/src/convert.dart | 7 +- .../lib/src/google_maps_controller.dart | 46 ++- .../lib/src/google_maps_flutter_web.dart | 14 +- .../third_party/to_screen_location/LICENSE | 21 ++ .../third_party/to_screen_location/README.md | 14 + .../to_screen_location.dart | 57 ++++ .../google_maps_flutter_web/pubspec.yaml | 4 +- .../tool/lib/src/license_check_command.dart | 24 +- 17 files changed, 636 insertions(+), 132 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/LICENSE create mode 100644 packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/README.md create mode 100644 packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/to_screen_location.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md index 83ffe09b357d..4d7ecf74e098 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.3.1 + +* Fix the `getScreenCoordinate(LatLng)` method. [#80710](https://github.com/flutter/flutter/issues/80710) +* Wait until the map tiles have loaded before calling `onPlatformViewCreated`, so +the returned controller is 100% functional (has bounds, a projection, etc...) +* Use zIndex property when initializing Circle objects. [#89374](https://github.com/flutter/flutter/issues/89374) + ## 0.3.0+4 * Add `implements` to pubspec. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/LICENSE b/packages/google_maps_flutter/google_maps_flutter_web/LICENSE index c6823b81eb84..8f8c01d50118 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/LICENSE +++ b/packages/google_maps_flutter/google_maps_flutter_web/LICENSE @@ -1,3 +1,5 @@ +google_maps_flutter_web + Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, @@ -23,3 +25,27 @@ 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. +-------------------------------------------------------------------------------- +to_screen_location + +The MIT License (MIT) + +Copyright (c) 2008 Krasimir Tsonev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart index 1d33eea4c7f3..39aa641b10e4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart @@ -257,13 +257,19 @@ void main() { }); testWidgets('renders initial geometry', (WidgetTester tester) async { - controller = _createController(circles: { - Circle(circleId: CircleId('circle-1')) - }, markers: { + controller = _createController(circles: { + Circle( + circleId: CircleId('circle-1'), + zIndex: 1234, + ), + }, markers: { Marker( - markerId: MarkerId('marker-1'), - infoWindow: InfoWindow( - title: 'title for test', snippet: 'snippet for test')) + markerId: MarkerId('marker-1'), + infoWindow: InfoWindow( + title: 'title for test', + snippet: 'snippet for test', + ), + ), }, polygons: { Polygon(polygonId: PolygonId('polygon-1'), points: [ LatLng(43.355114, -5.851333), @@ -315,6 +321,7 @@ void main() { .captured[0] as Set; expect(capturedCircles.first.circleId.value, 'circle-1'); + expect(capturedCircles.first.zIndex, 1234); expect(capturedMarkers.first.markerId.value, 'marker-1'); expect(capturedMarkers.first.infoWindow.snippet, 'snippet for test'); expect(capturedMarkers.first.infoWindow.title, 'title for test'); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart index 47933285b208..af8ed5420a0c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart @@ -2,26 +2,25 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Mocks generated by Mockito 5.0.2 from annotations +// Mocks generated by Mockito 5.0.15 from annotations // in google_maps_flutter_web_integration_tests/integration_test/google_maps_controller_test.dart. // Do not manually edit this file. -import 'package:google_maps/src/generated/google_maps_core.js.g.dart' as _i2; -import 'package:google_maps_flutter_platform_interface/src/types/circle.dart' +import 'package:google_maps/google_maps.dart' as _i2; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart' as _i4; -import 'package:google_maps_flutter_platform_interface/src/types/marker.dart' - as _i7; -import 'package:google_maps_flutter_platform_interface/src/types/polygon.dart' - as _i5; -import 'package:google_maps_flutter_platform_interface/src/types/polyline.dart' - as _i6; import 'package:google_maps_flutter_web/google_maps_flutter_web.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis -class _FakeGMap extends _i1.Fake implements _i2.GMap {} +class _FakeGMap_0 extends _i1.Fake implements _i2.GMap {} /// A class which mocks [CirclesController]. /// @@ -34,7 +33,7 @@ class MockCirclesController extends _i1.Mock implements _i3.CirclesController { as Map<_i4.CircleId, _i3.CircleController>); @override _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), - returnValue: _FakeGMap()) as _i2.GMap); + returnValue: _FakeGMap_0()) as _i2.GMap); @override set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), @@ -62,6 +61,8 @@ class MockCirclesController extends _i1.Mock implements _i3.CirclesController { void bindToMap(int? mapId, _i2.GMap? googleMap) => super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null); + @override + String toString() => super.toString(); } /// A class which mocks [PolygonsController]. @@ -70,13 +71,13 @@ class MockCirclesController extends _i1.Mock implements _i3.CirclesController { class MockPolygonsController extends _i1.Mock implements _i3.PolygonsController { @override - Map<_i5.PolygonId, _i3.PolygonController> get polygons => + Map<_i4.PolygonId, _i3.PolygonController> get polygons => (super.noSuchMethod(Invocation.getter(#polygons), - returnValue: <_i5.PolygonId, _i3.PolygonController>{}) - as Map<_i5.PolygonId, _i3.PolygonController>); + returnValue: <_i4.PolygonId, _i3.PolygonController>{}) + as Map<_i4.PolygonId, _i3.PolygonController>); @override _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), - returnValue: _FakeGMap()) as _i2.GMap); + returnValue: _FakeGMap_0()) as _i2.GMap); @override set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), @@ -89,21 +90,23 @@ class MockPolygonsController extends _i1.Mock super.noSuchMethod(Invocation.setter(#mapId, _mapId), returnValueForMissingStub: null); @override - void addPolygons(Set<_i5.Polygon>? polygonsToAdd) => + void addPolygons(Set<_i4.Polygon>? polygonsToAdd) => super.noSuchMethod(Invocation.method(#addPolygons, [polygonsToAdd]), returnValueForMissingStub: null); @override - void changePolygons(Set<_i5.Polygon>? polygonsToChange) => + void changePolygons(Set<_i4.Polygon>? polygonsToChange) => super.noSuchMethod(Invocation.method(#changePolygons, [polygonsToChange]), returnValueForMissingStub: null); @override - void removePolygons(Set<_i5.PolygonId>? polygonIdsToRemove) => super + void removePolygons(Set<_i4.PolygonId>? polygonIdsToRemove) => super .noSuchMethod(Invocation.method(#removePolygons, [polygonIdsToRemove]), returnValueForMissingStub: null); @override void bindToMap(int? mapId, _i2.GMap? googleMap) => super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null); + @override + String toString() => super.toString(); } /// A class which mocks [PolylinesController]. @@ -112,13 +115,13 @@ class MockPolygonsController extends _i1.Mock class MockPolylinesController extends _i1.Mock implements _i3.PolylinesController { @override - Map<_i6.PolylineId, _i3.PolylineController> get lines => + Map<_i4.PolylineId, _i3.PolylineController> get lines => (super.noSuchMethod(Invocation.getter(#lines), - returnValue: <_i6.PolylineId, _i3.PolylineController>{}) - as Map<_i6.PolylineId, _i3.PolylineController>); + returnValue: <_i4.PolylineId, _i3.PolylineController>{}) + as Map<_i4.PolylineId, _i3.PolylineController>); @override _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), - returnValue: _FakeGMap()) as _i2.GMap); + returnValue: _FakeGMap_0()) as _i2.GMap); @override set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), @@ -131,21 +134,23 @@ class MockPolylinesController extends _i1.Mock super.noSuchMethod(Invocation.setter(#mapId, _mapId), returnValueForMissingStub: null); @override - void addPolylines(Set<_i6.Polyline>? polylinesToAdd) => + void addPolylines(Set<_i4.Polyline>? polylinesToAdd) => super.noSuchMethod(Invocation.method(#addPolylines, [polylinesToAdd]), returnValueForMissingStub: null); @override - void changePolylines(Set<_i6.Polyline>? polylinesToChange) => super + void changePolylines(Set<_i4.Polyline>? polylinesToChange) => super .noSuchMethod(Invocation.method(#changePolylines, [polylinesToChange]), returnValueForMissingStub: null); @override - void removePolylines(Set<_i6.PolylineId>? polylineIdsToRemove) => super + void removePolylines(Set<_i4.PolylineId>? polylineIdsToRemove) => super .noSuchMethod(Invocation.method(#removePolylines, [polylineIdsToRemove]), returnValueForMissingStub: null); @override void bindToMap(int? mapId, _i2.GMap? googleMap) => super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null); + @override + String toString() => super.toString(); } /// A class which mocks [MarkersController]. @@ -153,13 +158,13 @@ class MockPolylinesController extends _i1.Mock /// See the documentation for Mockito's code generation for more information. class MockMarkersController extends _i1.Mock implements _i3.MarkersController { @override - Map<_i7.MarkerId, _i3.MarkerController> get markers => + Map<_i4.MarkerId, _i3.MarkerController> get markers => (super.noSuchMethod(Invocation.getter(#markers), - returnValue: <_i7.MarkerId, _i3.MarkerController>{}) - as Map<_i7.MarkerId, _i3.MarkerController>); + returnValue: <_i4.MarkerId, _i3.MarkerController>{}) + as Map<_i4.MarkerId, _i3.MarkerController>); @override _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), - returnValue: _FakeGMap()) as _i2.GMap); + returnValue: _FakeGMap_0()) as _i2.GMap); @override set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), @@ -172,31 +177,33 @@ class MockMarkersController extends _i1.Mock implements _i3.MarkersController { super.noSuchMethod(Invocation.setter(#mapId, _mapId), returnValueForMissingStub: null); @override - void addMarkers(Set<_i7.Marker>? markersToAdd) => + void addMarkers(Set<_i4.Marker>? markersToAdd) => super.noSuchMethod(Invocation.method(#addMarkers, [markersToAdd]), returnValueForMissingStub: null); @override - void changeMarkers(Set<_i7.Marker>? markersToChange) => + void changeMarkers(Set<_i4.Marker>? markersToChange) => super.noSuchMethod(Invocation.method(#changeMarkers, [markersToChange]), returnValueForMissingStub: null); @override - void removeMarkers(Set<_i7.MarkerId>? markerIdsToRemove) => + void removeMarkers(Set<_i4.MarkerId>? markerIdsToRemove) => super.noSuchMethod(Invocation.method(#removeMarkers, [markerIdsToRemove]), returnValueForMissingStub: null); @override - void showMarkerInfoWindow(_i7.MarkerId? markerId) => + void showMarkerInfoWindow(_i4.MarkerId? markerId) => super.noSuchMethod(Invocation.method(#showMarkerInfoWindow, [markerId]), returnValueForMissingStub: null); @override - void hideMarkerInfoWindow(_i7.MarkerId? markerId) => + void hideMarkerInfoWindow(_i4.MarkerId? markerId) => super.noSuchMethod(Invocation.method(#hideMarkerInfoWindow, [markerId]), returnValueForMissingStub: null); @override - bool isInfoWindowShown(_i7.MarkerId? markerId) => + bool isInfoWindowShown(_i4.MarkerId? markerId) => (super.noSuchMethod(Invocation.method(#isInfoWindowShown, [markerId]), returnValue: false) as bool); @override void bindToMap(int? mapId, _i2.GMap? googleMap) => super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null); + @override + String toString() => super.toString(); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart index 2de431a5445e..758294f5bb91 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart @@ -28,16 +28,18 @@ void main() { group('GoogleMapsPlugin', () { late MockGoogleMapController controller; late GoogleMapsPlugin plugin; - int? reportedMapId; + late Completer reportedMapIdCompleter; + int numberOnPlatformViewCreatedCalls = 0; void onPlatformViewCreated(int id) { - reportedMapId = id; + reportedMapIdCompleter.complete(id); + numberOnPlatformViewCreatedCalls++; } setUp(() { controller = MockGoogleMapController(); plugin = GoogleMapsPlugin(); - reportedMapId = null; + reportedMapIdCompleter = Completer(); }); group('init/dispose', () { @@ -52,12 +54,6 @@ void main() { 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); @@ -95,17 +91,17 @@ void main() { 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.', ); + expect( + cache[testMapId]!.isInitialized, + isTrue, + reason: 'buildView calls init on the controller', + ); }); testWidgets('returns cached instance if it already exists', @@ -121,11 +117,41 @@ void main() { ); expect(widget, equals(expected)); + }); + + testWidgets( + 'asynchronously reports onPlatformViewCreated the first time it happens', + (WidgetTester tester) async { + final Map cache = {}; + plugin.debugSetMapById(cache); + + plugin.buildView( + testMapId, + onPlatformViewCreated, + initialCameraPosition: initialCameraPosition, + ); + + // Simulate Google Maps JS SDK being "ready" + cache[testMapId]!.stream.add(WebMapReadyEvent(testMapId)); + + expect( + cache[testMapId]!.isInitialized, + isTrue, + reason: 'buildView calls init on the controller', + ); + expect( + await reportedMapIdCompleter.future, + testMapId, + reason: 'Should call onPlatformViewCreated with the mapId', + ); + + // Fire repeated event again... + cache[testMapId]!.stream.add(WebMapReadyEvent(testMapId)); expect( - reportedMapId, - isNull, + numberOnPlatformViewCreatedCalls, + equals(1), reason: - 'onPlatformViewCreated should not be called when returning a cached controller', + 'Should not call onPlatformViewCreated for the same controller multiple times', ); }); }); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart index 43150f63ef93..01908ce777e7 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart @@ -2,41 +2,34 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Mocks generated by Mockito 5.0.2 from annotations +// Mocks generated by Mockito 5.0.15 from annotations // in google_maps_flutter_web_integration_tests/integration_test/google_maps_plugin_test.dart. // Do not manually edit this file. -import 'dart:async' as _i5; +import 'dart:async' as _i2; -import 'package:google_maps_flutter_platform_interface/src/events/map_event.dart' - as _i6; -import 'package:google_maps_flutter_platform_interface/src/types/camera.dart' - as _i7; -import 'package:google_maps_flutter_platform_interface/src/types/circle_updates.dart' - as _i8; -import 'package:google_maps_flutter_platform_interface/src/types/location.dart' - as _i2; -import 'package:google_maps_flutter_platform_interface/src/types/marker.dart' - as _i12; -import 'package:google_maps_flutter_platform_interface/src/types/marker_updates.dart' - as _i11; -import 'package:google_maps_flutter_platform_interface/src/types/polygon_updates.dart' - as _i9; -import 'package:google_maps_flutter_platform_interface/src/types/polyline_updates.dart' - as _i10; -import 'package:google_maps_flutter_platform_interface/src/types/screen_coordinate.dart' +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart' as _i3; import 'package:google_maps_flutter_web/google_maps_flutter_web.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis -class _FakeLatLngBounds extends _i1.Fake implements _i2.LatLngBounds {} +class _FakeStreamController_0 extends _i1.Fake + implements _i2.StreamController {} -class _FakeScreenCoordinate extends _i1.Fake implements _i3.ScreenCoordinate {} +class _FakeLatLngBounds_1 extends _i1.Fake implements _i3.LatLngBounds {} -class _FakeLatLng extends _i1.Fake implements _i2.LatLng {} +class _FakeScreenCoordinate_2 extends _i1.Fake implements _i3.ScreenCoordinate { +} + +class _FakeLatLng_3 extends _i1.Fake implements _i3.LatLng {} /// A class which mocks [GoogleMapController]. /// @@ -44,63 +37,98 @@ class _FakeLatLng extends _i1.Fake implements _i2.LatLng {} class MockGoogleMapController extends _i1.Mock implements _i4.GoogleMapController { @override - _i5.Stream<_i6.MapEvent> get events => + _i2.StreamController<_i3.MapEvent> get stream => + (super.noSuchMethod(Invocation.getter(#stream), + returnValue: _FakeStreamController_0<_i3.MapEvent>()) + as _i2.StreamController<_i3.MapEvent>); + @override + _i2.Stream<_i3.MapEvent> get events => (super.noSuchMethod(Invocation.getter(#events), - returnValue: Stream<_i6.MapEvent>.empty()) - as _i5.Stream<_i6.MapEvent>); + returnValue: Stream<_i3.MapEvent>.empty()) + as _i2.Stream<_i3.MapEvent>); + @override + bool get isInitialized => + (super.noSuchMethod(Invocation.getter(#isInitialized), returnValue: false) + as bool); + @override + void debugSetOverrides( + {_i4.DebugCreateMapFunction? createMap, + _i4.MarkersController? markers, + _i4.CirclesController? circles, + _i4.PolygonsController? polygons, + _i4.PolylinesController? polylines}) => + super.noSuchMethod( + Invocation.method(#debugSetOverrides, [], { + #createMap: createMap, + #markers: markers, + #circles: circles, + #polygons: polygons, + #polylines: polylines + }), + returnValueForMissingStub: null); + @override + void init() => super.noSuchMethod(Invocation.method(#init, []), + returnValueForMissingStub: null); @override void updateRawOptions(Map? optionsUpdate) => super.noSuchMethod(Invocation.method(#updateRawOptions, [optionsUpdate]), returnValueForMissingStub: null); @override - _i5.Future<_i2.LatLngBounds> getVisibleRegion() => - (super.noSuchMethod(Invocation.method(#getVisibleRegion, []), - returnValue: Future.value(_FakeLatLngBounds())) - as _i5.Future<_i2.LatLngBounds>); + _i2.Future<_i3.LatLngBounds> getVisibleRegion() => (super.noSuchMethod( + Invocation.method(#getVisibleRegion, []), + returnValue: Future<_i3.LatLngBounds>.value(_FakeLatLngBounds_1())) + as _i2.Future<_i3.LatLngBounds>); @override - _i5.Future<_i3.ScreenCoordinate> getScreenCoordinate(_i2.LatLng? latLng) => + _i2.Future<_i3.ScreenCoordinate> getScreenCoordinate(_i3.LatLng? latLng) => (super.noSuchMethod(Invocation.method(#getScreenCoordinate, [latLng]), - returnValue: Future.value(_FakeScreenCoordinate())) - as _i5.Future<_i3.ScreenCoordinate>); + returnValue: + Future<_i3.ScreenCoordinate>.value(_FakeScreenCoordinate_2())) + as _i2.Future<_i3.ScreenCoordinate>); @override - _i5.Future<_i2.LatLng> getLatLng(_i3.ScreenCoordinate? screenCoordinate) => + _i2.Future<_i3.LatLng> getLatLng(_i3.ScreenCoordinate? screenCoordinate) => (super.noSuchMethod(Invocation.method(#getLatLng, [screenCoordinate]), - returnValue: Future.value(_FakeLatLng())) as _i5.Future<_i2.LatLng>); + returnValue: Future<_i3.LatLng>.value(_FakeLatLng_3())) + as _i2.Future<_i3.LatLng>); @override - _i5.Future moveCamera(_i7.CameraUpdate? cameraUpdate) => + _i2.Future moveCamera(_i3.CameraUpdate? cameraUpdate) => (super.noSuchMethod(Invocation.method(#moveCamera, [cameraUpdate]), - returnValue: Future.value(null), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i2.Future); @override - _i5.Future getZoomLevel() => + _i2.Future getZoomLevel() => (super.noSuchMethod(Invocation.method(#getZoomLevel, []), - returnValue: Future.value(0.0)) as _i5.Future); + returnValue: Future.value(0.0)) as _i2.Future); @override - void updateCircles(_i8.CircleUpdates? updates) => + void updateCircles(_i3.CircleUpdates? updates) => super.noSuchMethod(Invocation.method(#updateCircles, [updates]), returnValueForMissingStub: null); @override - void updatePolygons(_i9.PolygonUpdates? updates) => + void updatePolygons(_i3.PolygonUpdates? updates) => super.noSuchMethod(Invocation.method(#updatePolygons, [updates]), returnValueForMissingStub: null); @override - void updatePolylines(_i10.PolylineUpdates? updates) => + void updatePolylines(_i3.PolylineUpdates? updates) => super.noSuchMethod(Invocation.method(#updatePolylines, [updates]), returnValueForMissingStub: null); @override - void updateMarkers(_i11.MarkerUpdates? updates) => + void updateMarkers(_i3.MarkerUpdates? updates) => super.noSuchMethod(Invocation.method(#updateMarkers, [updates]), returnValueForMissingStub: null); @override - void showInfoWindow(_i12.MarkerId? markerId) => + void showInfoWindow(_i3.MarkerId? markerId) => super.noSuchMethod(Invocation.method(#showInfoWindow, [markerId]), returnValueForMissingStub: null); @override - void hideInfoWindow(_i12.MarkerId? markerId) => + void hideInfoWindow(_i3.MarkerId? markerId) => super.noSuchMethod(Invocation.method(#hideInfoWindow, [markerId]), returnValueForMissingStub: null); @override - bool isInfoWindowShown(_i12.MarkerId? markerId) => + bool isInfoWindowShown(_i3.MarkerId? markerId) => (super.noSuchMethod(Invocation.method(#isInfoWindowShown, [markerId]), returnValue: false) as bool); + @override + void dispose() => super.noSuchMethod(Invocation.method(#dispose, []), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart new file mode 100644 index 000000000000..8a5a62013538 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart @@ -0,0 +1,265 @@ +// 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. + +// These tests render an app with a small map widget, and use its map controller +// to compute values of the default projection. + +// (Tests methods that can't be mocked in `google_maps_controller_test.dart`) + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart' + show GoogleMap, GoogleMapController; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +// This value is used when comparing long~num, like LatLng values. +const _acceptableLatLngDelta = 0.0000000001; + +// This value is used when comparing pixel measurements, mostly to gloss over +// browser rounding errors. +const _acceptablePixelDelta = 1; + +/// Test Google Map Controller +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Methods that require a proper Projection', () { + final LatLng center = LatLng(43.3078, -5.6958); + final Size size = Size(320, 240); + final CameraPosition initialCamera = CameraPosition( + target: center, + zoom: 14, + ); + + late Completer controllerCompleter; + late void Function(GoogleMapController) onMapCreated; + + setUp(() { + controllerCompleter = Completer(); + onMapCreated = (GoogleMapController mapController) { + controllerCompleter.complete(mapController); + }; + }); + + group('getScreenCoordinate', () { + testWidgets('target of map is in center of widget', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + + final GoogleMapController controller = await controllerCompleter.future; + + final ScreenCoordinate screenPosition = + await controller.getScreenCoordinate(center); + + expect( + screenPosition.x, + closeTo(size.width / 2, _acceptablePixelDelta), + ); + expect( + screenPosition.y, + closeTo(size.height / 2, _acceptablePixelDelta), + ); + }); + + testWidgets('NorthWest of visible region corresponds to x:0, y:0', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + final GoogleMapController controller = await controllerCompleter.future; + + final LatLngBounds bounds = await controller.getVisibleRegion(); + final LatLng northWest = LatLng( + bounds.northeast.latitude, + bounds.southwest.longitude, + ); + + final ScreenCoordinate screenPosition = + await controller.getScreenCoordinate(northWest); + + expect(screenPosition.x, closeTo(0, _acceptablePixelDelta)); + expect(screenPosition.y, closeTo(0, _acceptablePixelDelta)); + }); + + testWidgets( + 'SouthEast of visible region corresponds to x:size.width, y:size.height', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + final GoogleMapController controller = await controllerCompleter.future; + + final LatLngBounds bounds = await controller.getVisibleRegion(); + final LatLng southEast = LatLng( + bounds.southwest.latitude, + bounds.northeast.longitude, + ); + + final ScreenCoordinate screenPosition = + await controller.getScreenCoordinate(southEast); + + expect(screenPosition.x, closeTo(size.width, _acceptablePixelDelta)); + expect(screenPosition.y, closeTo(size.height, _acceptablePixelDelta)); + }); + }); + + group('getLatLng', () { + testWidgets('Center of widget is the target of map', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + + final GoogleMapController controller = await controllerCompleter.future; + + final LatLng coords = await controller.getLatLng( + ScreenCoordinate(x: size.width ~/ 2, y: size.height ~/ 2), + ); + + expect( + coords.latitude, + closeTo(center.latitude, _acceptableLatLngDelta), + ); + expect( + coords.longitude, + closeTo(center.longitude, _acceptableLatLngDelta), + ); + }); + + testWidgets('Top-left of widget is NorthWest bound of map', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + final GoogleMapController controller = await controllerCompleter.future; + + final LatLngBounds bounds = await controller.getVisibleRegion(); + final LatLng northWest = LatLng( + bounds.northeast.latitude, + bounds.southwest.longitude, + ); + + final LatLng coords = await controller.getLatLng( + ScreenCoordinate(x: 0, y: 0), + ); + + expect( + coords.latitude, + closeTo(northWest.latitude, _acceptableLatLngDelta), + ); + expect( + coords.longitude, + closeTo(northWest.longitude, _acceptableLatLngDelta), + ); + }); + + testWidgets('Bottom-right of widget is SouthWest bound of map', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + final GoogleMapController controller = await controllerCompleter.future; + + final LatLngBounds bounds = await controller.getVisibleRegion(); + final LatLng southEast = LatLng( + bounds.southwest.latitude, + bounds.northeast.longitude, + ); + + final LatLng coords = await controller.getLatLng( + ScreenCoordinate(x: size.width.toInt(), y: size.height.toInt()), + ); + + expect( + coords.latitude, + closeTo(southEast.latitude, _acceptableLatLngDelta), + ); + expect( + coords.longitude, + closeTo(southEast.longitude, _acceptableLatLngDelta), + ); + }); + }); + }); +} + +// Pumps a CenteredMap Widget into a given tester, with some parameters +void pumpCenteredMap( + WidgetTester tester, { + required CameraPosition initialCamera, + Size size = const Size(320, 240), + void Function(GoogleMapController)? onMapCreated, +}) async { + await tester.pumpWidget( + CenteredMap( + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ), + ); + + // This is needed to kick-off the rendering of the JS Map flutter widget + await tester.pump(); +} + +/// Renders a Map widget centered on the screen. +/// This depends in `package:google_maps_flutter` to work. +class CenteredMap extends StatelessWidget { + const CenteredMap({ + required this.initialCamera, + required this.size, + required this.onMapCreated, + Key? key, + }) : super(key: key); + + /// A function that receives the [GoogleMapController] of the Map widget once initialized. + final void Function(GoogleMapController)? onMapCreated; + + /// The size of the rendered map widget. + final Size size; + + /// The initial camera position (center + zoom level) of the Map widget. + final CameraPosition initialCamera; + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox.fromSize( + size: size, + child: GoogleMap( + initialCameraPosition: initialCamera, + onMapCreated: onMapCreated, + ), + ), + ), + ), + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml index b0ac9910afc9..249b893d198c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml @@ -13,8 +13,10 @@ dependencies: sdk: flutter dev_dependencies: - build_runner: ^1.11.0 - google_maps: ^5.1.0 + build_runner: ^2.1.1 + google_maps: ^5.2.0 + google_maps_flutter: # Used for projection_test.dart + path: ../../google_maps_flutter http: ^0.13.0 mockito: ^5.0.0 flutter_driver: diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart index 6dc2dab572a6..0355f2923528 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart @@ -25,6 +25,7 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:google_maps/google_maps.dart' as gmaps; +import 'src/third_party/to_screen_location/to_screen_location.dart'; import 'src/types.dart'; part 'src/google_maps_flutter_web.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart index 2e71c795ff0e..c026a03be804 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart @@ -264,7 +264,7 @@ gmaps.MarkerOptions _markerOptionsFromMarker( } gmaps.CircleOptions _circleOptionsFromCircle(Circle circle) { - final populationOptions = gmaps.CircleOptions() + final circleOptions = gmaps.CircleOptions() ..strokeColor = _getCssColor(circle.strokeColor) ..strokeOpacity = _getCssOpacity(circle.strokeColor) ..strokeWeight = circle.strokeWidth @@ -272,8 +272,9 @@ gmaps.CircleOptions _circleOptionsFromCircle(Circle circle) { ..fillOpacity = _getCssOpacity(circle.fillColor) ..center = gmaps.LatLng(circle.center.latitude, circle.center.longitude) ..radius = circle.radius - ..visible = circle.visible; - return populationOptions; + ..visible = circle.visible + ..zIndex = circle.zIndex; + return circleOptions; } gmaps.PolygonOptions _polygonOptionsFromPolygon( diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart index 226268270579..edf47764f346 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart @@ -53,6 +53,10 @@ class GoogleMapController { // The StreamController used by this controller and the geometry ones. final StreamController _streamController; + /// The StreamController for the events of this Map. Only for integration testing. + @visibleForTesting + StreamController get stream => _streamController; + /// The Stream over which this controller broadcasts events. Stream get events => _streamController.stream; @@ -132,10 +136,27 @@ class GoogleMapController { return gmaps.GMap(div, options); } - /// Initializes the [gmaps.GMap] instance from the stored `rawOptions`. + /// A flag that returns true if the controller has been initialized or not. + @visibleForTesting + bool get isInitialized => _googleMap != null; + + /// Starts the JS Maps SDK into the target [_div] with `rawOptions`. + /// + /// (Also initializes the geometry/traffic layers.) + /// + /// The first part of this method starts the rendering of a [gmaps.GMap] inside + /// of the target [_div], with configuration from `rawOptions`. It then stores + /// the created GMap in the [_googleMap] attribute. /// - /// This method actually renders the GMap into the cached `_div`. This is - /// called by the [GoogleMapsPlugin.init] method when appropriate. + /// Not *everything* is rendered with the initial `rawOptions` configuration, + /// geometry and traffic layers (and possibly others in the future) have their + /// own configuration and are rendered on top of a GMap instance later. This + /// happens in the second half of this method. + /// + /// This method is eagerly called from the [GoogleMapsPlugin.buildView] method + /// so the internal [GoogleMapsController] of a Web Map initializes as soon as + /// possible. Check [_attachMapEvents] to see how this controller notifies the + /// plugin of it being fully ready (through the `onTilesloaded.first` event). /// /// 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. @@ -151,6 +172,7 @@ class GoogleMapController { _attachMapEvents(map); _attachGeometryControllers(map); + // Now attach the geometry, traffic and any other layers... _renderInitialGeometry( markers: _markers, circles: _circles, @@ -163,6 +185,10 @@ class GoogleMapController { // Funnels map gmap events into the plugin's stream controller. void _attachMapEvents(gmaps.GMap map) { + map.onTilesloaded.first.then((event) { + // Report the map as ready to go the first time the tiles load + _streamController.add(WebMapReadyEvent(_mapId)); + }); map.onClick.listen((event) { assert(event.latLng != null); _streamController.add( @@ -292,14 +318,8 @@ class GoogleMapController { Future getScreenCoordinate(LatLng latLng) async { assert(_googleMap != null, 'Cannot get the screen coordinates with a null map.'); - assert(_googleMap!.projection != null, - 'Cannot compute screen coordinate with a null map or projection.'); - - final point = - _googleMap!.projection!.fromLatLngToPoint!(_latLngToGmLatLng(latLng))!; - assert(point.x != null && point.y != null, - 'The x and y of a ScreenCoordinate cannot be null.'); + final point = toScreenLocation(_googleMap!, _latLngToGmLatLng(latLng)); return ScreenCoordinate(x: point.x!.toInt(), y: point.y!.toInt()); } @@ -403,3 +423,9 @@ class GoogleMapController { _streamController.close(); } } + +/// An event fired when a [mapId] on web is interactive. +class WebMapReadyEvent extends MapEvent { + /// Build a WebMapReady Event for the map represented by `mapId`. + WebMapReadyEvent(int mapId) : super(mapId, null); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart index 692917fef4da..d03dec93ce3f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart @@ -35,7 +35,10 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { @override Future init(int mapId) async { - _map(mapId).init(); + // The internal instance of our controller is initialized eagerly in `buildView`, + // so we don't have to do anything in this method, which is left intentionally + // blank. + assert(_map(mapId) != null, 'Must call buildWidget before init!'); } /// Updates the options of a given `mapId`. @@ -305,11 +308,16 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { polylines: polylines, circles: circles, mapOptions: mapOptions, - ); + )..init(); // Initialize the controller _mapById[creationId] = mapController; - onPlatformViewCreated.call(creationId); + mapController.events.whereType().first.then((event) { + assert(creationId == event.mapId, + 'Received WebMapReadyEvent for the wrong map'); + // Notify the plugin now that there's a fully initialized controller. + onPlatformViewCreated.call(event.mapId); + }); assert(mapController.widget != null, 'The widget of a GoogleMapController cannot be null before calling dispose on it.'); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/LICENSE b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/LICENSE new file mode 100644 index 000000000000..ab4e163abe54 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2008 Krasimir Tsonev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/README.md b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/README.md new file mode 100644 index 000000000000..8bd4a39c065f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/README.md @@ -0,0 +1,14 @@ +# to_screen_location + +The code in this directory is a Dart re-implementation of Krasimir Tsonev's blog +post: [GoogleMaps API v3: convert LatLng object to actual pixels][blog-post]. + +The blog post describes a way to implement the [`toScreenLocation` method][method] +of the Google Maps Platform SDK for the web. + +Used under license (MIT), [available here][blog-license], and in the accompanying +LICENSE file. + +[blog-license]: https://krasimirtsonev.com/license +[blog-post]: https://krasimirtsonev.com/blog/article/google-maps-api-v3-convert-latlng-object-to-actual-pixels-point-object +[method]: https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/Projection#toScreenLocation(com.google.android.libraries.maps.model.LatLng) diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/to_screen_location.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/to_screen_location.dart new file mode 100644 index 000000000000..2963111fdcc3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/to_screen_location.dart @@ -0,0 +1,57 @@ +// The MIT License (MIT) +// +// Copyright (c) 2008 Krasimir Tsonev +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:google_maps/google_maps.dart' as gmaps; + +/// Returns a screen location that corresponds to a geographical coordinate ([gmaps.LatLng]). +/// +/// The screen location is in pixels relative to the top left of the Map widget +/// (not of the whole screen/app). +/// +/// See: https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/Projection#public-point-toscreenlocation-latlng-location +gmaps.Point toScreenLocation(gmaps.GMap map, gmaps.LatLng coords) { + final zoom = map.zoom; + final bounds = map.bounds; + final projection = map.projection; + + assert( + bounds != null, 'Map Bounds required to compute screen x/y of LatLng.'); + assert(projection != null, + 'Map Projection required to compute screen x/y of LatLng.'); + assert(zoom != null, + 'Current map zoom level required to compute screen x/y of LatLng.'); + + final ne = bounds!.northEast; + final sw = bounds.southWest; + + final topRight = projection!.fromLatLngToPoint!(ne)!; + final bottomLeft = projection.fromLatLngToPoint!(sw)!; + + final scale = 1 << (zoom!.toInt()); // 2 ^ zoom + + final worldPoint = projection.fromLatLngToPoint!(coords)!; + + return gmaps.Point( + ((worldPoint.x! - bottomLeft.x!) * scale).toInt(), + ((worldPoint.y! - topRight.y!) * scale).toInt(), + ); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index 82605f8fd070..8a23916b0e98 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 0.3.0+4 +version: 0.3.1 environment: sdk: ">=2.12.0 <3.0.0" @@ -22,7 +22,7 @@ dependencies: flutter_web_plugins: sdk: flutter google_maps_flutter_platform_interface: ^2.0.1 - google_maps: ^5.1.0 + google_maps: ^5.2.0 meta: ^1.3.0 sanitize_html: ^2.0.0 stream_transform: ^2.0.0 diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index e68585c44bdf..8cee46b45a4c 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -49,16 +49,24 @@ const Set _ignoredFullBasenameList = { // When adding license regexes here, include the copyright info to ensure that // any new additions are flagged for added scrutiny in review. final List _thirdPartyLicenseBlockRegexes = [ -// Third-party code used in url_launcher_web. + // Third-party code used in url_launcher_web. RegExp( - r'^// Copyright 2017 Workiva Inc\..*' - r'^// Licensed under the Apache License, Version 2\.0', - multiLine: true, - dotAll: true), + r'^// Copyright 2017 Workiva Inc\..*' + r'^// Licensed under the Apache License, Version 2\.0', + multiLine: true, + dotAll: true, + ), + // Third-party code used in google_maps_flutter_web. + RegExp( + r'^// The MIT License [^C]+ Copyright \(c\) 2008 Krasimir Tsonev', + multiLine: true, + ), // bsdiff in flutter/packages. - RegExp(r'// Copyright 2003-2005 Colin Percival\. All rights reserved\.\n' - r'// Use of this source code is governed by a BSD-style license that can be\n' - r'// found in the LICENSE file\.\n'), + RegExp( + r'// Copyright 2003-2005 Colin Percival\. All rights reserved\.\n' + r'// Use of this source code is governed by a BSD-style license that can be\n' + r'// found in the LICENSE file\.\n', + ), ]; // The exact format of the BSD license that our license files should contain.