[google_maps_flutter_platform_interface] Platform interface changes for #3258 (#4478)

See #3258
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md
index c47578b..c785625 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.4.0
+
+* Adds options for gesture handling and tilt controls on web.
+
 ## 2.3.0
 
 * Adds a `cloudMapId` parameter to support cloud-based map styling.
@@ -58,7 +62,7 @@
 
 ## 2.1.5
 
-Removes dependency on `meta`.
+* Removes dependency on `meta`.
 
 ## 2.1.4
 
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_configuration.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_configuration.dart
index 5580e9e..3ec973f 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_configuration.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_configuration.dart
@@ -4,7 +4,7 @@
 
 import 'package:flutter/widgets.dart';
 
-import 'ui.dart';
+import '../../google_maps_flutter_platform_interface.dart';
 
 /// Configuration options for the GoogleMaps user interface.
 @immutable
@@ -15,6 +15,7 @@
   /// as either a full configuration selection, or an update to an existing
   /// configuration where only non-null values are updated.
   const MapConfiguration({
+    this.webGestureHandling,
     this.compassEnabled,
     this.mapToolbarEnabled,
     this.cameraTargetBounds,
@@ -23,6 +24,7 @@
     this.rotateGesturesEnabled,
     this.scrollGesturesEnabled,
     this.tiltGesturesEnabled,
+    this.fortyFiveDegreeImageryEnabled,
     this.trackCameraPosition,
     this.zoomControlsEnabled,
     this.zoomGesturesEnabled,
@@ -36,6 +38,11 @@
     this.cloudMapId,
   });
 
+  /// This setting controls how the API handles gestures on the map. Web only.
+  ///
+  /// See [WebGestureHandling] for more details.
+  final WebGestureHandling? webGestureHandling;
+
   /// True if the compass UI should be shown.
   final bool? compassEnabled;
 
@@ -48,18 +55,25 @@
   /// The type of the map.
   final MapType? mapType;
 
-  /// The prefered zoom range.
+  /// The preferred zoom range.
   final MinMaxZoomPreference? minMaxZoomPreference;
 
   /// True if rotate gestures should be enabled.
   final bool? rotateGesturesEnabled;
 
   /// True if scroll gestures should be enabled.
+  ///
+  /// Android/iOS only. For web, see [webGestureHandling].
   final bool? scrollGesturesEnabled;
 
   /// True if tilt gestures should be enabled.
   final bool? tiltGesturesEnabled;
 
+  /// True if 45 degree imagery should be enabled.
+  ///
+  /// Web only.
+  final bool? fortyFiveDegreeImageryEnabled;
+
   /// True if camera position changes should trigger notifications.
   final bool? trackCameraPosition;
 
@@ -67,6 +81,8 @@
   final bool? zoomControlsEnabled;
 
   /// True if zoom gestures should be enabled.
+  ///
+  /// Android/iOS only. For web, see [webGestureHandling].
   final bool? zoomGesturesEnabled;
 
   /// True if the map should use Lite Mode, showing a limited-interactivity
@@ -101,6 +117,9 @@
   /// that are different from [other].
   MapConfiguration diffFrom(MapConfiguration other) {
     return MapConfiguration(
+      webGestureHandling: webGestureHandling != other.webGestureHandling
+          ? webGestureHandling
+          : null,
       compassEnabled:
           compassEnabled != other.compassEnabled ? compassEnabled : null,
       mapToolbarEnabled: mapToolbarEnabled != other.mapToolbarEnabled
@@ -124,6 +143,10 @@
       tiltGesturesEnabled: tiltGesturesEnabled != other.tiltGesturesEnabled
           ? tiltGesturesEnabled
           : null,
+      fortyFiveDegreeImageryEnabled:
+          fortyFiveDegreeImageryEnabled != other.fortyFiveDegreeImageryEnabled
+              ? fortyFiveDegreeImageryEnabled
+              : null,
       trackCameraPosition: trackCameraPosition != other.trackCameraPosition
           ? trackCameraPosition
           : null,
@@ -158,6 +181,7 @@
   /// replacing the previous values.
   MapConfiguration applyDiff(MapConfiguration diff) {
     return MapConfiguration(
+      webGestureHandling: diff.webGestureHandling ?? webGestureHandling,
       compassEnabled: diff.compassEnabled ?? compassEnabled,
       mapToolbarEnabled: diff.mapToolbarEnabled ?? mapToolbarEnabled,
       cameraTargetBounds: diff.cameraTargetBounds ?? cameraTargetBounds,
@@ -168,6 +192,8 @@
       scrollGesturesEnabled:
           diff.scrollGesturesEnabled ?? scrollGesturesEnabled,
       tiltGesturesEnabled: diff.tiltGesturesEnabled ?? tiltGesturesEnabled,
+      fortyFiveDegreeImageryEnabled:
+          diff.fortyFiveDegreeImageryEnabled ?? fortyFiveDegreeImageryEnabled,
       trackCameraPosition: diff.trackCameraPosition ?? trackCameraPosition,
       zoomControlsEnabled: diff.zoomControlsEnabled ?? zoomControlsEnabled,
       zoomGesturesEnabled: diff.zoomGesturesEnabled ?? zoomGesturesEnabled,
@@ -185,6 +211,7 @@
 
   /// True if no options are set.
   bool get isEmpty =>
+      webGestureHandling == null &&
       compassEnabled == null &&
       mapToolbarEnabled == null &&
       cameraTargetBounds == null &&
@@ -193,6 +220,7 @@
       rotateGesturesEnabled == null &&
       scrollGesturesEnabled == null &&
       tiltGesturesEnabled == null &&
+      fortyFiveDegreeImageryEnabled == null &&
       trackCameraPosition == null &&
       zoomControlsEnabled == null &&
       zoomGesturesEnabled == null &&
@@ -214,6 +242,7 @@
       return false;
     }
     return other is MapConfiguration &&
+        webGestureHandling == other.webGestureHandling &&
         compassEnabled == other.compassEnabled &&
         mapToolbarEnabled == other.mapToolbarEnabled &&
         cameraTargetBounds == other.cameraTargetBounds &&
@@ -222,6 +251,7 @@
         rotateGesturesEnabled == other.rotateGesturesEnabled &&
         scrollGesturesEnabled == other.scrollGesturesEnabled &&
         tiltGesturesEnabled == other.tiltGesturesEnabled &&
+        fortyFiveDegreeImageryEnabled == other.fortyFiveDegreeImageryEnabled &&
         trackCameraPosition == other.trackCameraPosition &&
         zoomControlsEnabled == other.zoomControlsEnabled &&
         zoomGesturesEnabled == other.zoomGesturesEnabled &&
@@ -236,7 +266,8 @@
   }
 
   @override
-  int get hashCode => Object.hash(
+  int get hashCode => Object.hashAll(<Object?>[
+        webGestureHandling,
         compassEnabled,
         mapToolbarEnabled,
         cameraTargetBounds,
@@ -245,6 +276,7 @@
         rotateGesturesEnabled,
         scrollGesturesEnabled,
         tiltGesturesEnabled,
+        fortyFiveDegreeImageryEnabled,
         trackCameraPosition,
         zoomControlsEnabled,
         zoomGesturesEnabled,
@@ -256,5 +288,5 @@
         trafficEnabled,
         buildingsEnabled,
         cloudMapId,
-      );
+      ]);
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart
index 0beb7d7..1f1916b 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart
@@ -34,3 +34,4 @@
 export 'utils/polygon.dart';
 export 'utils/polyline.dart';
 export 'utils/tile_overlay.dart';
+export 'web_gesture_handling.dart';
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/web_gesture_handling.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/web_gesture_handling.dart
new file mode 100644
index 0000000..c978f91
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/web_gesture_handling.dart
@@ -0,0 +1,22 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// This setting controls how the API handles gestures on the map
+enum WebGestureHandling {
+  /// Scroll events and one-finger touch gestures scroll the page, and do not
+  /// zoom or pan the map. Two-finger touch gestures pan and zoom the map.
+  /// Scroll events with a ctrl key or ⌘ key pressed zoom the map. In this mode
+  /// the map cooperates with the page.
+  cooperative,
+
+  /// All touch gestures and scroll events pan or zoom the map.
+  greedy,
+
+  /// The map cannot be panned or zoomed by user gestures.
+  none,
+
+  /// (default) Gesture handling is either cooperative or greedy, depending on
+  /// whether the page is scrollable or in an iframe.
+  auto,
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml
index a8d7b12..9b79a53 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml
@@ -4,7 +4,7 @@
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22
 # NOTE: We strongly prefer non-breaking changes, even at the expense of a
 # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
-version: 2.3.0
+version: 2.4.0
 
 environment:
   sdk: ">=2.18.0 <4.0.0"
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/map_configuration_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/map_configuration_test.dart
index e57d2c7..2a53b8c 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/map_configuration_test.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/map_configuration_test.dart
@@ -12,6 +12,7 @@
   group('diffs', () {
     // A options instance with every field set, to test diffs against.
     final MapConfiguration diffBase = MapConfiguration(
+      webGestureHandling: WebGestureHandling.auto,
       compassEnabled: false,
       mapToolbarEnabled: false,
       cameraTargetBounds: CameraTargetBounds(LatLngBounds(
@@ -21,6 +22,7 @@
       rotateGesturesEnabled: false,
       scrollGesturesEnabled: false,
       tiltGesturesEnabled: false,
+      fortyFiveDegreeImageryEnabled: false,
       trackCameraPosition: false,
       zoomControlsEnabled: false,
       zoomGesturesEnabled: false,
@@ -58,6 +60,23 @@
       expect(updated.cloudMapId, null);
     });
 
+    test('handle webGestureHandling', () async {
+      const MapConfiguration diff =
+          MapConfiguration(webGestureHandling: WebGestureHandling.none);
+
+      const MapConfiguration empty = MapConfiguration();
+      final MapConfiguration updated = diffBase.applyDiff(diff);
+
+      // A diff applied to empty options should be the diff itself.
+      expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
+      // A diff applied to non-empty options should update that field.
+      expect(updated.webGestureHandling, WebGestureHandling.none);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
+    });
+
     test('handle compassEnabled', () async {
       const MapConfiguration diff = MapConfiguration(compassEnabled: true);
 
@@ -66,8 +85,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.compassEnabled, true);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle mapToolbarEnabled', () async {
@@ -78,8 +101,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.mapToolbarEnabled, true);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle cameraTargetBounds', () async {
@@ -93,8 +120,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.cameraTargetBounds, newBounds);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle mapType', () async {
@@ -106,8 +137,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.mapType, MapType.satellite);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle minMaxZoomPreference', () async {
@@ -120,8 +155,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.minMaxZoomPreference, newZoomPref);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle rotateGesturesEnabled', () async {
@@ -133,8 +172,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.rotateGesturesEnabled, true);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle scrollGesturesEnabled', () async {
@@ -146,8 +189,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.scrollGesturesEnabled, true);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle tiltGesturesEnabled', () async {
@@ -158,8 +205,29 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.tiltGesturesEnabled, true);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
+    });
+
+    test('handle fortyFiveDegreeImageryEnabled', () async {
+      const MapConfiguration diff =
+          MapConfiguration(fortyFiveDegreeImageryEnabled: true);
+
+      const MapConfiguration empty = MapConfiguration();
+      final MapConfiguration updated = diffBase.applyDiff(diff);
+
+      // A diff applied to empty options should be the diff itself.
+      expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
+      // A diff applied to non-empty options should update that field.
+      expect(updated.fortyFiveDegreeImageryEnabled, true);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle trackCameraPosition', () async {
@@ -170,8 +238,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.trackCameraPosition, true);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle zoomControlsEnabled', () async {
@@ -182,8 +254,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.zoomControlsEnabled, true);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle zoomGesturesEnabled', () async {
@@ -194,8 +270,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.zoomGesturesEnabled, true);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle liteModeEnabled', () async {
@@ -206,8 +286,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.liteModeEnabled, true);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle myLocationEnabled', () async {
@@ -218,8 +302,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.myLocationEnabled, true);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle myLocationButtonEnabled', () async {
@@ -231,8 +319,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.myLocationButtonEnabled, true);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle padding', () async {
@@ -245,8 +337,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.padding, newPadding);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle indoorViewEnabled', () async {
@@ -257,8 +353,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.indoorViewEnabled, true);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle trafficEnabled', () async {
@@ -269,8 +369,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.trafficEnabled, true);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle buildingsEnabled', () async {
@@ -281,8 +385,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.buildingsEnabled, true);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
 
     test('handle cloudMapId', () async {
@@ -293,8 +401,12 @@
 
       // A diff applied to empty options should be the diff itself.
       expect(empty.applyDiff(diff), diff);
+      // The diff from empty options should be the diff itself.
+      expect(diff.diffFrom(empty), diff);
       // A diff applied to non-empty options should update that field.
       expect(updated.cloudMapId, _kCloudMapId);
+      // The hash code should change.
+      expect(empty.hashCode, isNot(diff.hashCode));
     });
   });