[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).
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 83ffe09..4d7ecf7 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 c6823b8..8f8c01d 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 @@
 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 1d33eea..39aa641 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 @@
       });
 
       testWidgets('renders initial geometry', (WidgetTester tester) async {
-        controller = _createController(circles: <Circle>{
-          Circle(circleId: CircleId('circle-1'))
-        }, markers: <Marker>{
+        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 @@
             .captured[0] as Set<Polyline>;
 
         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 4793328..af8ed54 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 @@
           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 @@
   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 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 @@
       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 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 @@
       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 @@
 /// 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 @@
       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 2de431a..758294f 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 @@
   group('GoogleMapsPlugin', () {
     late MockGoogleMapController controller;
     late GoogleMapsPlugin plugin;
-    int? reportedMapId;
+    late Completer<int> reportedMapIdCompleter;
+    int numberOnPlatformViewCreatedCalls = 0;
 
     void onPlatformViewCreated(int id) {
-      reportedMapId = id;
+      reportedMapIdCompleter.complete(id);
+      numberOnPlatformViewCreatedCalls++;
     }
 
     setUp(() {
       controller = MockGoogleMapController();
       plugin = GoogleMapsPlugin();
-      reportedMapId = null;
+      reportedMapIdCompleter = Completer<int>();
     });
 
     group('init/dispose', () {
@@ -52,12 +54,6 @@
           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 @@
           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 @@
         );
 
         expect(widget, equals(expected));
+      });
+
+      testWidgets(
+          'asynchronously reports onPlatformViewCreated the first time it happens',
+          (WidgetTester tester) async {
+        final Map<int, GoogleMapController> cache = {};
+        plugin.debugSetMapById(cache);
+
+        plugin.buildView(
+          testMapId,
+          onPlatformViewCreated,
+          initialCameraPosition: initialCameraPosition,
+        );
+
+        // Simulate Google Maps JS SDK being "ready"
+        cache[testMapId]!.stream.add(WebMapReadyEvent(testMapId));
+
         expect(
-          reportedMapId,
-          isNull,
+          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(
+          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 43150f6..01908ce 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<T> extends _i1.Fake
+    implements _i2.StreamController<T> {}
 
-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 MockGoogleMapController extends _i1.Mock
     implements _i4.GoogleMapController {
   @override
-  _i5.Stream<_i6.MapEvent<dynamic>> get events =>
+  _i2.StreamController<_i3.MapEvent<dynamic>> get stream =>
+      (super.noSuchMethod(Invocation.getter(#stream),
+              returnValue: _FakeStreamController_0<_i3.MapEvent<dynamic>>())
+          as _i2.StreamController<_i3.MapEvent<dynamic>>);
+  @override
+  _i2.Stream<_i3.MapEvent<dynamic>> get events =>
       (super.noSuchMethod(Invocation.getter(#events),
-              returnValue: Stream<_i6.MapEvent<dynamic>>.empty())
-          as _i5.Stream<_i6.MapEvent<dynamic>>);
+              returnValue: Stream<_i3.MapEvent<dynamic>>.empty())
+          as _i2.Stream<_i3.MapEvent<dynamic>>);
+  @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<String, dynamic>? 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<void> moveCamera(_i7.CameraUpdate? cameraUpdate) =>
+  _i2.Future<void> moveCamera(_i3.CameraUpdate? cameraUpdate) =>
       (super.noSuchMethod(Invocation.method(#moveCamera, [cameraUpdate]),
-          returnValue: Future.value(null),
-          returnValueForMissingStub: Future.value()) as _i5.Future<void>);
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i2.Future<void>);
   @override
-  _i5.Future<double> getZoomLevel() =>
+  _i2.Future<double> getZoomLevel() =>
       (super.noSuchMethod(Invocation.method(#getZoomLevel, []),
-          returnValue: Future.value(0.0)) as _i5.Future<double>);
+          returnValue: Future<double>.value(0.0)) as _i2.Future<double>);
   @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 0000000..8a5a620
--- /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<GoogleMapController> controllerCompleter;
+    late void Function(GoogleMapController) onMapCreated;
+
+    setUp(() {
+      controllerCompleter = Completer<GoogleMapController>();
+      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 b0ac991..249b893 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 @@
     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 6dc2dab..0355f29 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: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 2e71c79..c026a03 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.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 @@
     ..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 2262682..edf4776 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 @@
   // The StreamController used by this controller and the geometry ones.
   final StreamController<MapEvent> _streamController;
 
+  /// The StreamController for the events of this Map. Only for integration testing.
+  @visibleForTesting
+  StreamController<MapEvent> get stream => _streamController;
+
   /// The Stream over which this controller broadcasts events.
   Stream<MapEvent> get events => _streamController.stream;
 
@@ -132,10 +136,27 @@
     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`.
   ///
-  /// This method actually renders the GMap into the cached `_div`. This is
-  /// called by the [GoogleMapsPlugin.init] method when appropriate.
+  /// (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.
+  ///
+  /// 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 @@
     _attachMapEvents(map);
     _attachGeometryControllers(map);
 
+    // Now attach the geometry, traffic and any other layers...
     _renderInitialGeometry(
       markers: _markers,
       circles: _circles,
@@ -163,6 +185,10 @@
 
   // 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 @@
   Future<ScreenCoordinate> 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 @@
     _streamController.close();
   }
 }
+
+/// An event fired when a [mapId] on web is interactive.
+class WebMapReadyEvent extends MapEvent<void> {
+  /// 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 692917f..d03dec9 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 @@
 
   @override
   Future<void> 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 @@
       polylines: polylines,
       circles: circles,
       mapOptions: mapOptions,
-    );
+    )..init(); // Initialize the controller
 
     _mapById[creationId] = mapController;
 
-    onPlatformViewCreated.call(creationId);
+    mapController.events.whereType<WebMapReadyEvent>().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 0000000..ab4e163
--- /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 0000000..8bd4a39
--- /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 0000000..2963111
--- /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 82605f8..8a23916 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 @@
 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 @@
   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 e68585c..8cee46b 100644
--- a/script/tool/lib/src/license_check_command.dart
+++ b/script/tool/lib/src/license_check_command.dart
@@ -49,16 +49,24 @@
 // When adding license regexes here, include the copyright info to ensure that
 // any new additions are flagged for added scrutiny in review.
 final List<RegExp> _thirdPartyLicenseBlockRegexes = <RegExp>[
-// 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.