From 174caf4a9dcfcf88658980fb67031ba70d13d1e5 Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Tue, 4 Feb 2025 19:26:09 +0200 Subject: [PATCH 01/10] [google_maps_flutter_ios] Adds support for ground overlay --- .../google_maps_flutter_ios/CHANGELOG.md | 3 +- .../integration_test/google_maps_test.dart | 253 ++++++++++++++ .../ios/Runner.xcodeproj/project.pbxproj | 29 +- .../RunnerTests/ExtractIconFromDataTests.m | 83 ++--- .../GoogleMapsGroundOverlayControllerTests.m | 155 +++++++++ .../ios14/ios/RunnerTests/GoogleMapsTests.m | 3 +- .../example/ios14/lib/main.dart | 2 + .../example/ios14/pubspec.yaml | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 19 + .../ios15/ios/RunnerTests/GoogleMapsTests.m | 3 +- .../example/ios15/lib/main.dart | 2 + .../example/ios15/pubspec.yaml | 2 +- .../lib/example_google_map.dart | 30 ++ .../maps_example_dart/lib/ground_overlay.dart | 328 ++++++++++++++++++ .../shared/maps_example_dart/pubspec.yaml | 2 +- .../fake_google_maps_flutter_platform.dart | 18 + .../ios/Classes/FGMGroundOverlayController.h | 64 ++++ .../ios/Classes/FGMGroundOverlayController.m | 234 +++++++++++++ .../Classes/FGMGroundOverlayController_Test.h | 18 + .../ios/Classes/FGMImageUtils.h | 18 + .../ios/Classes/FGMImageUtils.m | 234 +++++++++++++ .../ios/Classes/FLTGoogleMapJSONConversions.h | 6 + .../ios/Classes/FLTGoogleMapJSONConversions.m | 51 +++ .../ios/Classes/GoogleMapController.m | 24 ++ .../ios/Classes/GoogleMapMarkerController.m | 236 +------------ .../google_maps_flutter_ios-umbrella.h | 3 + .../ios/Classes/messages.g.h | 64 +++- .../ios/Classes/messages.g.m | 211 +++++++++-- .../lib/src/google_map_inspector_ios.dart | 55 +++ .../lib/src/google_maps_flutter_ios.dart | 65 ++++ .../lib/src/messages.g.dart | 236 +++++++++++-- .../pigeons/messages.dart | 42 +++ .../google_maps_flutter_ios/pubspec.yaml | 4 +- .../test/google_maps_flutter_ios_test.dart | 162 +++++++++ .../google_maps_flutter_ios_test.mocks.dart | 22 +- 35 files changed, 2304 insertions(+), 379 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsGroundOverlayControllerTests.m create mode 100644 packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/ground_overlay.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.h create mode 100644 packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.m create mode 100644 packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController_Test.h create mode 100644 packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.h create mode 100644 packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.m diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md index b09f2f375b20..dd58bd9e23b1 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.14.0 +* Adds support for ground overlay. * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. ## 2.13.2 diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart index e2b5941d2618..78e5abd16447 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart @@ -20,6 +20,7 @@ const double _kInitialZoomLevel = 5; const CameraPosition _kInitialCameraPosition = CameraPosition(target: _kInitialMapCenter, zoom: _kInitialZoomLevel); const String _kCloudMapId = '000000000000000'; // Dummy map ID. +const double _floatTolerance = 1e-6; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -1292,6 +1293,258 @@ void main() { )); await controllerCompleter.future; }); + + group('GroundOverlay', () { + final LatLngBounds kGroundOverlayBounds = LatLngBounds( + southwest: const LatLng(37.77483, -122.41942), + northeast: const LatLng(37.78183, -122.39105), + ); + + final GroundOverlay groundOverlayBounds1 = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('bounds_1'), + bounds: kGroundOverlayBounds, + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ), + ); + + final GroundOverlay groundOverlayPosition1 = GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('position_1'), + position: kGroundOverlayBounds.northeast, + width: 100, + height: 100, + anchor: const Offset(0.1, 0.2), + zoomLevel: 14.0, + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + )); + + void expectGroundOverlayEquals( + GroundOverlay source, GroundOverlay response) { + expect(response.groundOverlayId, source.groundOverlayId); + expect( + response.transparency, + moreOrLessEquals(source.transparency, epsilon: _floatTolerance), + ); + expect( + response.bearing, + moreOrLessEquals(source.bearing, epsilon: _floatTolerance), + ); + + // Only test bounds if it was given in the original object + if (source.bounds != null) { + expect(response.bounds, source.bounds); + } + + // Only test position if it was given in the original object + if (source.position != null) { + expect(response.position, source.position); + } + + expect(response.clickable, source.clickable); + expect(response.zIndex, source.zIndex); + expect(response.zoomLevel, source.zoomLevel); + expect( + response.anchor?.dx, + moreOrLessEquals(source.anchor!.dx, epsilon: _floatTolerance), + ); + expect( + response.anchor?.dy, + moreOrLessEquals(source.anchor!.dy, epsilon: _floatTolerance), + ); + } + + testWidgets('set ground overlays correctly', (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final GroundOverlay groundOverlayBounds2 = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('bounds_2'), + bounds: groundOverlayBounds1.bounds!, + image: groundOverlayBounds1.image, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: _kInitialCameraPosition, + groundOverlays: { + groundOverlayBounds1, + groundOverlayBounds2, + groundOverlayPosition1, + }, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + if (inspector.supportsGettingGroundOverlayInfo()) { + final GroundOverlay groundOverlayBoundsInfo1 = (await inspector + .getGroundOverlayInfo(groundOverlayBounds1.mapsId, mapId: mapId))!; + final GroundOverlay groundOverlayBoundsInfo2 = (await inspector + .getGroundOverlayInfo(groundOverlayBounds2.mapsId, mapId: mapId))!; + final GroundOverlay groundOverlayPositionInfo1 = + (await inspector.getGroundOverlayInfo(groundOverlayPosition1.mapsId, + mapId: mapId))!; + + expectGroundOverlayEquals( + groundOverlayBounds1, + groundOverlayBoundsInfo1, + ); + expectGroundOverlayEquals( + groundOverlayBounds2, + groundOverlayBoundsInfo2, + ); + expectGroundOverlayEquals( + groundOverlayPosition1, + groundOverlayPositionInfo1, + ); + } + }); + + testWidgets('update ground overlays correctly', + (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final Key key = GlobalKey(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + groundOverlays: { + groundOverlayBounds1, + groundOverlayPosition1 + }, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + ), + ); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + final GroundOverlay groundOverlayBounds1New = + groundOverlayBounds1.copyWith( + bearingParam: 10, + clickableParam: false, + transparencyParam: 0.5, + visibleParam: false, + zIndexParam: 10, + ); + + final GroundOverlay groundOverlayPosition1New = + groundOverlayPosition1.copyWith( + bearingParam: 10, + clickableParam: false, + transparencyParam: 0.5, + visibleParam: false, + zIndexParam: 10, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + groundOverlays: { + groundOverlayBounds1New, + groundOverlayPosition1New + }, + onMapCreated: (ExampleGoogleMapController controller) { + fail('update: OnMapCreated should get called only once.'); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + if (inspector.supportsGettingGroundOverlayInfo()) { + final GroundOverlay groundOverlayBounds1Info = (await inspector + .getGroundOverlayInfo(groundOverlayBounds1.mapsId, mapId: mapId))!; + final GroundOverlay groundOverlayPosition1Info = + (await inspector.getGroundOverlayInfo(groundOverlayPosition1.mapsId, + mapId: mapId))!; + + expectGroundOverlayEquals( + groundOverlayBounds1New, + groundOverlayBounds1Info, + ); + expectGroundOverlayEquals( + groundOverlayPosition1New, + groundOverlayPosition1Info, + ); + } + }); + + testWidgets('remove ground overlays correctly', + (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final Key key = GlobalKey(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + groundOverlays: { + groundOverlayBounds1, + groundOverlayPosition1 + }, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + ), + ); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + if (inspector.supportsGettingGroundOverlayInfo()) { + final GroundOverlay? groundOverlayBounds1Info = await inspector + .getGroundOverlayInfo(groundOverlayBounds1.mapsId, mapId: mapId); + final GroundOverlay? groundOverlayPositionInfo = await inspector + .getGroundOverlayInfo(groundOverlayPosition1.mapsId, mapId: mapId); + + expect(groundOverlayBounds1Info, isNull); + expect(groundOverlayPositionInfo, isNull); + } + }); + }); } class _DebugTileProvider implements TileProvider { diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/Runner.xcodeproj/project.pbxproj index b59580322983..4db85635bead 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/Runner.xcodeproj/project.pbxproj @@ -3,12 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ 0DD7B6C32B744EEF00E857FD /* FLTTileProviderControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DD7B6C22B744EEF00E857FD /* FLTTileProviderControllerTests.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2A6906C72D263DF4001F8426 /* GoogleMapsGroundOverlayControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A6906C62D263DE7001F8426 /* GoogleMapsGroundOverlayControllerTests.m */; }; 2BDE99378062AE3E60B40021 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3ACE0AFE8D82CD5962486AFD /* Pods_RunnerTests.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 478116522BEF8F47002F593E /* GoogleMapsPolylinesControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 478116512BEF8F47002F593E /* GoogleMapsPolylinesControllerTests.m */; }; @@ -62,6 +63,7 @@ 0DD7B6C22B744EEF00E857FD /* FLTTileProviderControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTTileProviderControllerTests.m; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2A6906C62D263DE7001F8426 /* GoogleMapsGroundOverlayControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleMapsGroundOverlayControllerTests.m; sourceTree = ""; }; 3ACE0AFE8D82CD5962486AFD /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 478116512BEF8F47002F593E /* GoogleMapsPolylinesControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GoogleMapsPolylinesControllerTests.m; sourceTree = ""; }; @@ -211,6 +213,7 @@ 0DD7B6C22B744EEF00E857FD /* FLTTileProviderControllerTests.m */, F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */, 478116512BEF8F47002F593E /* GoogleMapsPolylinesControllerTests.m */, + 2A6906C62D263DE7001F8426 /* GoogleMapsGroundOverlayControllerTests.m */, 982F2A6A27BADE17003C81F4 /* PartiallyMockedMapView.h */, 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */, F7151F14265D7ED70028CB91 /* Info.plist */, @@ -242,6 +245,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, BB6BD9A1101E970BEF85B6D2 /* [CP] Copy Pods Resources */, + 9C5FE6CAF02237D44998DDC0 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -327,7 +331,7 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; @@ -419,6 +423,24 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + 9C5FE6CAF02237D44998DDC0 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; BB6BD9A1101E970BEF85B6D2 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -501,6 +523,7 @@ 6851F3562835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m in Sources */, 982F2A6C27BADE17003C81F4 /* PartiallyMockedMapView.m in Sources */, 478116522BEF8F47002F593E /* GoogleMapsPolylinesControllerTests.m in Sources */, + 2A6906C72D263DF4001F8426 /* GoogleMapsGroundOverlayControllerTests.m in Sources */, 0DD7B6C32B744EEF00E857FD /* FLTTileProviderControllerTests.m in Sources */, 528F16872C62952700148160 /* ExtractIconFromDataTests.m in Sources */, ); @@ -821,7 +844,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/ExtractIconFromDataTests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/ExtractIconFromDataTests.m index 811f09d49b45..bd39d7b3f83a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/ExtractIconFromDataTests.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/ExtractIconFromDataTests.m @@ -5,6 +5,7 @@ @import google_maps_flutter_ios; @import google_maps_flutter_ios.Test; @import XCTest; + #import #import @@ -15,7 +16,6 @@ - (UIImage *)createOnePixelImage; @implementation ExtractIconFromDataTests - (void)testExtractIconFromDataAssetAuto { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); id mockImageClass = OCMClassMock([UIImage class]); @@ -32,9 +32,9 @@ - (void)testExtractIconFromDataAssetAuto { CGFloat screenScale = 3.0; - UIImage *resultImage = [instance iconFromBitmap:[FGMPlatformBitmap makeWithBitmap:bitmap] - registrar:mockRegistrar - screenScale:screenScale]; + UIImage *resultImage = + FGMIconFromBitmap([FGMPlatformBitmap makeWithBitmap:bitmap], mockRegistrar, screenScale); + XCTAssertNotNil(resultImage); XCTAssertEqual(resultImage.scale, 1.0); XCTAssertEqual(resultImage.size.width, 1.0); @@ -42,7 +42,6 @@ - (void)testExtractIconFromDataAssetAuto { } - (void)testExtractIconFromDataAssetAutoWithScale { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); id mockImageClass = OCMClassMock([UIImage class]); @@ -60,9 +59,8 @@ - (void)testExtractIconFromDataAssetAutoWithScale { CGFloat screenScale = 3.0; - UIImage *resultImage = [instance iconFromBitmap:[FGMPlatformBitmap makeWithBitmap:bitmap] - registrar:mockRegistrar - screenScale:screenScale]; + UIImage *resultImage = + FGMIconFromBitmap([FGMPlatformBitmap makeWithBitmap:bitmap], mockRegistrar, screenScale); XCTAssertNotNil(resultImage); XCTAssertEqual(resultImage.scale, 10); @@ -71,7 +69,6 @@ - (void)testExtractIconFromDataAssetAutoWithScale { } - (void)testExtractIconFromDataAssetAutoAndSizeWithSameAspectRatio { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); id mockImageClass = OCMClassMock([UIImage class]); @@ -91,9 +88,8 @@ - (void)testExtractIconFromDataAssetAutoAndSizeWithSameAspectRatio { CGFloat screenScale = 3.0; - UIImage *resultImage = [instance iconFromBitmap:[FGMPlatformBitmap makeWithBitmap:bitmap] - registrar:mockRegistrar - screenScale:screenScale]; + UIImage *resultImage = + FGMIconFromBitmap([FGMPlatformBitmap makeWithBitmap:bitmap], mockRegistrar, screenScale); XCTAssertNotNil(resultImage); XCTAssertEqual(testImage.scale, 1.0); @@ -107,7 +103,6 @@ - (void)testExtractIconFromDataAssetAutoAndSizeWithSameAspectRatio { } - (void)testExtractIconFromDataAssetAutoAndSizeWithDifferentAspectRatio { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); id mockImageClass = OCMClassMock([UIImage class]); @@ -127,9 +122,8 @@ - (void)testExtractIconFromDataAssetAutoAndSizeWithDifferentAspectRatio { CGFloat screenScale = 3.0; - UIImage *resultImage = [instance iconFromBitmap:[FGMPlatformBitmap makeWithBitmap:bitmap] - registrar:mockRegistrar - screenScale:screenScale]; + UIImage *resultImage = + FGMIconFromBitmap([FGMPlatformBitmap makeWithBitmap:bitmap], mockRegistrar, screenScale); XCTAssertNotNil(resultImage); XCTAssertEqual(resultImage.scale, screenScale); XCTAssertEqual(resultImage.size.width, width); @@ -137,7 +131,6 @@ - (void)testExtractIconFromDataAssetAutoAndSizeWithDifferentAspectRatio { } - (void)testExtractIconFromDataAssetNoScaling { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); id mockImageClass = OCMClassMock([UIImage class]); @@ -155,9 +148,8 @@ - (void)testExtractIconFromDataAssetNoScaling { CGFloat screenScale = 3.0; - UIImage *resultImage = [instance iconFromBitmap:[FGMPlatformBitmap makeWithBitmap:bitmap] - registrar:mockRegistrar - screenScale:screenScale]; + UIImage *resultImage = + FGMIconFromBitmap([FGMPlatformBitmap makeWithBitmap:bitmap], mockRegistrar, screenScale); XCTAssertNotNil(resultImage); XCTAssertEqual(resultImage.scale, 1.0); @@ -166,7 +158,6 @@ - (void)testExtractIconFromDataAssetNoScaling { } - (void)testExtractIconFromDataBytesAuto { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); UIImage *testImage = [self createOnePixelImage]; @@ -183,9 +174,8 @@ - (void)testExtractIconFromDataBytesAuto { CGFloat screenScale = 3.0; - UIImage *resultImage = [instance iconFromBitmap:[FGMPlatformBitmap makeWithBitmap:bitmap] - registrar:mockRegistrar - screenScale:screenScale]; + UIImage *resultImage = + FGMIconFromBitmap([FGMPlatformBitmap makeWithBitmap:bitmap], mockRegistrar, screenScale); XCTAssertNotNil(resultImage); XCTAssertEqual(resultImage.scale, 1.0); @@ -194,7 +184,6 @@ - (void)testExtractIconFromDataBytesAuto { } - (void)testExtractIconFromDataBytesAutoWithScaling { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); UIImage *testImage = [self createOnePixelImage]; @@ -211,9 +200,8 @@ - (void)testExtractIconFromDataBytesAutoWithScaling { CGFloat screenScale = 3.0; - UIImage *resultImage = [instance iconFromBitmap:[FGMPlatformBitmap makeWithBitmap:bitmap] - registrar:mockRegistrar - screenScale:screenScale]; + UIImage *resultImage = + FGMIconFromBitmap([FGMPlatformBitmap makeWithBitmap:bitmap], mockRegistrar, screenScale); XCTAssertNotNil(resultImage); XCTAssertEqual(resultImage.scale, 10); XCTAssertEqual(resultImage.size.width, 0.1); @@ -221,7 +209,6 @@ - (void)testExtractIconFromDataBytesAutoWithScaling { } - (void)testExtractIconFromDataBytesAutoAndSizeWithSameAspectRatio { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); UIImage *testImage = [self createOnePixelImage]; @@ -240,9 +227,8 @@ - (void)testExtractIconFromDataBytesAutoAndSizeWithSameAspectRatio { CGFloat screenScale = 3.0; - UIImage *resultImage = [instance iconFromBitmap:[FGMPlatformBitmap makeWithBitmap:bitmap] - registrar:mockRegistrar - screenScale:screenScale]; + UIImage *resultImage = + FGMIconFromBitmap([FGMPlatformBitmap makeWithBitmap:bitmap], mockRegistrar, screenScale); XCTAssertNotNil(resultImage); XCTAssertEqual(testImage.scale, 1.0); @@ -257,7 +243,6 @@ - (void)testExtractIconFromDataBytesAutoAndSizeWithSameAspectRatio { } - (void)testExtractIconFromDataBytesAutoAndSizeWithDifferentAspectRatio { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); UIImage *testImage = [self createOnePixelImage]; @@ -276,9 +261,8 @@ - (void)testExtractIconFromDataBytesAutoAndSizeWithDifferentAspectRatio { CGFloat screenScale = 3.0; - UIImage *resultImage = [instance iconFromBitmap:[FGMPlatformBitmap makeWithBitmap:bitmap] - registrar:mockRegistrar - screenScale:screenScale]; + UIImage *resultImage = + FGMIconFromBitmap([FGMPlatformBitmap makeWithBitmap:bitmap], mockRegistrar, screenScale); XCTAssertNotNil(resultImage); XCTAssertEqual(resultImage.scale, screenScale); XCTAssertEqual(resultImage.size.width, width); @@ -286,7 +270,6 @@ - (void)testExtractIconFromDataBytesAutoAndSizeWithDifferentAspectRatio { } - (void)testExtractIconFromDataBytesNoScaling { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); UIImage *testImage = [self createOnePixelImage]; @@ -303,9 +286,8 @@ - (void)testExtractIconFromDataBytesNoScaling { CGFloat screenScale = 3.0; - UIImage *resultImage = [instance iconFromBitmap:[FGMPlatformBitmap makeWithBitmap:bitmap] - registrar:mockRegistrar - screenScale:screenScale]; + UIImage *resultImage = + FGMIconFromBitmap([FGMPlatformBitmap makeWithBitmap:bitmap], mockRegistrar, screenScale); XCTAssertNotNil(resultImage); XCTAssertEqual(resultImage.scale, 1.0); XCTAssertEqual(resultImage.size.width, 1.0); @@ -315,50 +297,43 @@ - (void)testExtractIconFromDataBytesNoScaling { - (void)testIsScalableWithScaleFactorFromSize100x100to10x100 { CGSize originalSize = CGSizeMake(100.0, 100.0); CGSize targetSize = CGSizeMake(10.0, 100.0); - XCTAssertFalse([FLTGoogleMapMarkerController isScalableWithScaleFactorFromSize:originalSize - toSize:targetSize]); + XCTAssertFalse(FGMIsScalableWithScaleFactorFromSize(originalSize, targetSize)); } - (void)testIsScalableWithScaleFactorFromSize100x100to10x10 { CGSize originalSize = CGSizeMake(100.0, 100.0); CGSize targetSize = CGSizeMake(10.0, 10.0); - XCTAssertTrue([FLTGoogleMapMarkerController isScalableWithScaleFactorFromSize:originalSize - toSize:targetSize]); + XCTAssertTrue(FGMIsScalableWithScaleFactorFromSize(originalSize, targetSize)); } - (void)testIsScalableWithScaleFactorFromSize233x200to23x20 { CGSize originalSize = CGSizeMake(233.0, 200.0); CGSize targetSize = CGSizeMake(23.0, 20.0); - XCTAssertTrue([FLTGoogleMapMarkerController isScalableWithScaleFactorFromSize:originalSize - toSize:targetSize]); + XCTAssertTrue(FGMIsScalableWithScaleFactorFromSize(originalSize, targetSize)); } - (void)testIsScalableWithScaleFactorFromSize233x200to22x20 { CGSize originalSize = CGSizeMake(233.0, 200.0); CGSize targetSize = CGSizeMake(22.0, 20.0); - XCTAssertFalse([FLTGoogleMapMarkerController isScalableWithScaleFactorFromSize:originalSize - toSize:targetSize]); + XCTAssertFalse(FGMIsScalableWithScaleFactorFromSize(originalSize, targetSize)); } - (void)testIsScalableWithScaleFactorFromSize200x233to20x23 { CGSize originalSize = CGSizeMake(200.0, 233.0); CGSize targetSize = CGSizeMake(20.0, 23.0); - XCTAssertTrue([FLTGoogleMapMarkerController isScalableWithScaleFactorFromSize:originalSize - toSize:targetSize]); + XCTAssertTrue(FGMIsScalableWithScaleFactorFromSize(originalSize, targetSize)); } - (void)testIsScalableWithScaleFactorFromSize200x233to20x22 { CGSize originalSize = CGSizeMake(200.0, 233.0); CGSize targetSize = CGSizeMake(20.0, 22.0); - XCTAssertFalse([FLTGoogleMapMarkerController isScalableWithScaleFactorFromSize:originalSize - toSize:targetSize]); + XCTAssertFalse(FGMIsScalableWithScaleFactorFromSize(originalSize, targetSize)); } - (void)testIsScalableWithScaleFactorFromSize1024x768to500x250 { CGSize originalSize = CGSizeMake(1024.0, 768.0); CGSize targetSize = CGSizeMake(500.0, 250.0); - XCTAssertFalse([FLTGoogleMapMarkerController isScalableWithScaleFactorFromSize:originalSize - toSize:targetSize]); + XCTAssertFalse(FGMIsScalableWithScaleFactorFromSize(originalSize, targetSize)); } - (UIImage *)createOnePixelImage { diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsGroundOverlayControllerTests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsGroundOverlayControllerTests.m new file mode 100644 index 000000000000..da3ef509abae --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsGroundOverlayControllerTests.m @@ -0,0 +1,155 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import google_maps_flutter_ios; +@import google_maps_flutter_ios.Test; +@import XCTest; +@import GoogleMaps; + +#import +#import +#import +#import "PartiallyMockedMapView.h" + +@interface GoogleMapsGroundOverlayControllerTests : XCTestCase +@end + +@implementation GoogleMapsGroundOverlayControllerTests + +/// Returns GoogleMapGroundOverlayController object instantiated with position and a mocked map +/// instance. +/// +/// @return An object of FLTGoogleMapGroundOverlayController ++ (FGMGroundOverlayController *)groundOverlayControllerWithPositionWithMockedMap { + NSString *imagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"widegamut" + ofType:@"png" + inDirectory:@"assets"]; + UIImage *wideGamutImage = [UIImage imageWithContentsOfFile:imagePath]; + GMSGroundOverlay *groundOverlay = + [GMSGroundOverlay groundOverlayWithPosition:CLLocationCoordinate2DMake(52.4816, 3.1791) + icon:wideGamutImage + zoomLevel:14.0]; + + GMSCameraPosition *camera = [[GMSCameraPosition alloc] initWithLatitude:0 longitude:0 zoom:0]; + CGRect frame = CGRectMake(0, 0, 100, 100); + GMSMapViewOptions *mapViewOptions = [[GMSMapViewOptions alloc] init]; + mapViewOptions.frame = frame; + mapViewOptions.camera = camera; + + PartiallyMockedMapView *mapView = [[PartiallyMockedMapView alloc] initWithOptions:mapViewOptions]; + + return [[FGMGroundOverlayController alloc] initWithGroundOverlay:groundOverlay + identifier:@"id_1" + mapView:mapView + isCreatedWithBounds:NO]; +} + +/// Returns GoogleMapGroundOverlayController object instantiated with bounds and a mocked map +/// instance. +/// +/// @return An object of FLTGoogleMapGroundOverlayController ++ (FGMGroundOverlayController *)groundOverlayControllerWithBoundsWithMockedMap { + NSString *imagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"widegamut" + ofType:@"png" + inDirectory:@"assets"]; + UIImage *wideGamutImage = [UIImage imageWithContentsOfFile:imagePath]; + GMSGroundOverlay *groundOverlay = [GMSGroundOverlay + groundOverlayWithBounds:[[GMSCoordinateBounds alloc] + initWithCoordinate:CLLocationCoordinate2DMake(10, 20) + coordinate:CLLocationCoordinate2DMake(30, 40)] + icon:wideGamutImage]; + + GMSCameraPosition *camera = [[GMSCameraPosition alloc] initWithLatitude:0 longitude:0 zoom:0]; + CGRect frame = CGRectMake(0, 0, 100, 100); + GMSMapViewOptions *mapViewOptions = [[GMSMapViewOptions alloc] init]; + mapViewOptions.frame = frame; + mapViewOptions.camera = camera; + + PartiallyMockedMapView *mapView = [[PartiallyMockedMapView alloc] initWithOptions:mapViewOptions]; + + return [[FGMGroundOverlayController alloc] initWithGroundOverlay:groundOverlay + identifier:@"id_1" + mapView:mapView + isCreatedWithBounds:YES]; +} + +- (void)testUpdatingGroundOverlayWithPosition { + FGMGroundOverlayController *groundOverlayController = + [GoogleMapsGroundOverlayControllerTests groundOverlayControllerWithPositionWithMockedMap]; + + FGMPlatformLatLng *position = [FGMPlatformLatLng makeWithLatitude:52.4816 longitude:3.1791]; + + FGMPlatformBitmap *bitmap = + [FGMPlatformBitmap makeWithBitmap:[FGMPlatformBitmapDefaultMarker makeWithHue:0]]; + NSObject *mockRegistrar = + OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); + + FGMPlatformGroundOverlay *platformGroundOverlay = + [FGMPlatformGroundOverlay makeWithGroundOverlayId:@"id_1" + image:bitmap + position:position + bounds:nil + anchor:nil + transparency:0.5 + bearing:65.0 + zIndex:2.0 + visible:true + clickable:true + zoomLevel:@14.0]; + + [groundOverlayController updateFromPlatformGroundOverlay:platformGroundOverlay + registrar:mockRegistrar + screenScale:1.0]; + + XCTAssertNotNil(groundOverlayController.groundOverlay.icon); + XCTAssertEqual(groundOverlayController.groundOverlay.position.latitude, position.latitude); + XCTAssertEqual(groundOverlayController.groundOverlay.position.longitude, position.longitude); + XCTAssertEqual(groundOverlayController.groundOverlay.opacity, platformGroundOverlay.transparency); + XCTAssertEqual(groundOverlayController.groundOverlay.bearing, platformGroundOverlay.bearing); +} + +- (void)testUpdatingGroundOverlayWithBounds { + FGMGroundOverlayController *groundOverlayController = + [GoogleMapsGroundOverlayControllerTests groundOverlayControllerWithBoundsWithMockedMap]; + + FGMPlatformLatLngBounds *bounds = [FGMPlatformLatLngBounds + makeWithNortheast:[FGMPlatformLatLng makeWithLatitude:54.4816 longitude:5.1791] + southwest:[FGMPlatformLatLng makeWithLatitude:52.4816 longitude:3.1791]]; + + FGMPlatformBitmap *bitmap = + [FGMPlatformBitmap makeWithBitmap:[FGMPlatformBitmapDefaultMarker makeWithHue:0]]; + NSObject *mockRegistrar = + OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); + + FGMPlatformGroundOverlay *platformGroundOverlay = + [FGMPlatformGroundOverlay makeWithGroundOverlayId:@"id_1" + image:bitmap + position:nil + bounds:bounds + anchor:nil + transparency:0.5 + bearing:65.0 + zIndex:2.0 + visible:true + clickable:true + zoomLevel:nil]; + + [groundOverlayController updateFromPlatformGroundOverlay:platformGroundOverlay + registrar:mockRegistrar + screenScale:1.0]; + + XCTAssertNotNil(groundOverlayController.groundOverlay.icon); + XCTAssertEqual(groundOverlayController.groundOverlay.bounds.northEast.latitude, + bounds.northeast.latitude); + XCTAssertEqual(groundOverlayController.groundOverlay.bounds.northEast.longitude, + bounds.northeast.longitude); + XCTAssertEqual(groundOverlayController.groundOverlay.bounds.southWest.latitude, + bounds.southwest.latitude); + XCTAssertEqual(groundOverlayController.groundOverlay.bounds.southWest.longitude, + bounds.southwest.longitude); + XCTAssertEqual(groundOverlayController.groundOverlay.opacity, platformGroundOverlay.transparency); + XCTAssertEqual(groundOverlayController.groundOverlay.bearing, platformGroundOverlay.bearing); +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsTests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsTests.m index c175550dd2cf..6d821b4d11fe 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsTests.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsTests.m @@ -103,7 +103,8 @@ - (FGMPlatformMapViewCreationParams *)emptyCreationParameters { initialPolylines:@[] initialHeatmaps:@[] initialTileOverlays:@[] - initialClusterManagers:@[]]; + initialClusterManagers:@[] + initialGroundOverlays:@[]]; } @end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/lib/main.dart index 3144c2aff5e6..d1d736a659a3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:maps_example_dart/animate_camera.dart'; import 'package:maps_example_dart/clustering.dart'; +import 'package:maps_example_dart/ground_overlay.dart'; import 'package:maps_example_dart/lite_mode.dart'; import 'package:maps_example_dart/map_click.dart'; import 'package:maps_example_dart/map_coordinates.dart'; @@ -41,6 +42,7 @@ void main() { SnapshotPage(), LiteModePage(), TileOverlayPage(), + GroundOverlayPage(), ClusteringPage(), MapIdPage(), ]))); diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/pubspec.yaml index f0a241192822..5d4f5d3cedda 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../../ - google_maps_flutter_platform_interface: ^2.9.0 + google_maps_flutter_platform_interface: ^2.10.0 maps_example_dart: path: ../shared/maps_example_dart/ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/ios/Runner.xcodeproj/project.pbxproj index cf3ec2ab9f0b..244542c4b0bc 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/ios/Runner.xcodeproj/project.pbxproj @@ -198,6 +198,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, BB6BD9A1101E970BEF85B6D2 /* [CP] Copy Pods Resources */, + 356AF3055B7647C0F00BD257 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -295,6 +296,24 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 356AF3055B7647C0F00BD257 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/ios/RunnerTests/GoogleMapsTests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/ios/RunnerTests/GoogleMapsTests.m index c175550dd2cf..6d821b4d11fe 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/ios/RunnerTests/GoogleMapsTests.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/ios/RunnerTests/GoogleMapsTests.m @@ -103,7 +103,8 @@ - (FGMPlatformMapViewCreationParams *)emptyCreationParameters { initialPolylines:@[] initialHeatmaps:@[] initialTileOverlays:@[] - initialClusterManagers:@[]]; + initialClusterManagers:@[] + initialGroundOverlays:@[]]; } @end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/lib/main.dart index 3144c2aff5e6..d1d736a659a3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:maps_example_dart/animate_camera.dart'; import 'package:maps_example_dart/clustering.dart'; +import 'package:maps_example_dart/ground_overlay.dart'; import 'package:maps_example_dart/lite_mode.dart'; import 'package:maps_example_dart/map_click.dart'; import 'package:maps_example_dart/map_coordinates.dart'; @@ -41,6 +42,7 @@ void main() { SnapshotPage(), LiteModePage(), TileOverlayPage(), + GroundOverlayPage(), ClusteringPage(), MapIdPage(), ]))); diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/pubspec.yaml index f0a241192822..5d4f5d3cedda 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../../ - google_maps_flutter_platform_interface: ^2.9.0 + google_maps_flutter_platform_interface: ^2.10.0 maps_example_dart: path: ../shared/maps_example_dart/ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart index a8e19e29e7d2..378993cb476a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart @@ -82,6 +82,9 @@ class ExampleGoogleMapController { GoogleMapsFlutterPlatform.instance .onCircleTap(mapId: mapId) .listen((CircleTapEvent e) => _googleMapState.onCircleTap(e.value)); + GoogleMapsFlutterPlatform.instance.onGroundOverlayTap(mapId: mapId).listen( + (GroundOverlayTapEvent e) => + _googleMapState.onGroundOverlayTap(e.value)); GoogleMapsFlutterPlatform.instance .onTap(mapId: mapId) .listen((MapTapEvent e) => _googleMapState.onTap(e.position)); @@ -111,6 +114,13 @@ class ExampleGoogleMapController { .updateClusterManagers(clusterManagerUpdates, mapId: mapId); } + /// Updates ground overlay configuration. + Future _updateGroundOverlays( + GroundOverlayUpdates groundOverlayUpdates) { + return GoogleMapsFlutterPlatform.instance + .updateGroundOverlays(groundOverlayUpdates, mapId: mapId); + } + /// Updates polygon configuration. Future _updatePolygons(PolygonUpdates polygonUpdates) { return GoogleMapsFlutterPlatform.instance @@ -248,6 +258,7 @@ class ExampleGoogleMap extends StatefulWidget { this.clusterManagers = const {}, this.onCameraMoveStarted, this.tileOverlays = const {}, + this.groundOverlays = const {}, this.onCameraMove, this.onCameraIdle, this.onTap, @@ -318,6 +329,9 @@ class ExampleGoogleMap extends StatefulWidget { /// Cluster Managers to be placed for the map. final Set clusterManagers; + /// Ground overlays to be initialized for the map. + final Set groundOverlays; + /// Called when the camera starts moving. final VoidCallback? onCameraMoveStarted; @@ -379,6 +393,8 @@ class _ExampleGoogleMapState extends State { Map _circles = {}; Map _clusterManagers = {}; + Map _groundOverlays = + {}; late MapConfiguration _mapConfiguration; @override @@ -399,6 +415,7 @@ class _ExampleGoogleMapState extends State { polylines: widget.polylines, circles: widget.circles, clusterManagers: widget.clusterManagers, + groundOverlays: widget.groundOverlays, ), mapConfiguration: _mapConfiguration, ); @@ -413,6 +430,7 @@ class _ExampleGoogleMapState extends State { _polygons = keyByPolygonId(widget.polygons); _polylines = keyByPolylineId(widget.polylines); _circles = keyByCircleId(widget.circles); + _groundOverlays = keyByGroundOverlayId(widget.groundOverlays); } @override @@ -432,6 +450,7 @@ class _ExampleGoogleMapState extends State { _updatePolylines(); _updateCircles(); _updateTileOverlays(); + _updateGroundOverlays(); } Future _updateOptions() async { @@ -459,6 +478,13 @@ class _ExampleGoogleMapState extends State { _clusterManagers = keyByClusterManagerId(widget.clusterManagers); } + Future _updateGroundOverlays() async { + final ExampleGoogleMapController controller = await _controller.future; + unawaited(controller._updateGroundOverlays(GroundOverlayUpdates.from( + _groundOverlays.values.toSet(), widget.groundOverlays))); + _groundOverlays = keyByGroundOverlayId(widget.groundOverlays); + } + Future _updatePolygons() async { final ExampleGoogleMapController controller = await _controller.future; unawaited(controller._updatePolygons( @@ -525,6 +551,10 @@ class _ExampleGoogleMapState extends State { _circles[circleId]!.onTap?.call(); } + void onGroundOverlayTap(GroundOverlayId groundOverlayId) { + _groundOverlays[groundOverlayId]!.onTap?.call(); + } + void onInfoWindowTap(MarkerId markerId) { _markers[markerId]!.infoWindow.onTap?.call(); } diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/ground_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/ground_overlay.dart new file mode 100644 index 000000000000..055736f396df --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/ground_overlay.dart @@ -0,0 +1,328 @@ +// 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. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +enum _GroundOverlayPlacing { position, bounds } + +class GroundOverlayPage extends GoogleMapExampleAppPage { + const GroundOverlayPage({Key? key}) + : super(const Icon(Icons.map), 'Ground overlay', key: key); + + @override + Widget build(BuildContext context) { + return const GroundOverlayBody(); + } +} + +class GroundOverlayBody extends StatefulWidget { + const GroundOverlayBody({super.key}); + + @override + State createState() => GroundOverlayBodyState(); +} + +class GroundOverlayBodyState extends State { + GroundOverlayBodyState(); + + ExampleGoogleMapController? controller; + GroundOverlay? _groundOverlay; + + final LatLng _mapCenter = const LatLng(37.422026, -122.085329); + + _GroundOverlayPlacing _placingType = _GroundOverlayPlacing.bounds; + + // Positions for demonstranting placing ground overlays with position, and + // changing positions. + final LatLng _groundOverlayPos1 = const LatLng(37.422026, -122.085329); + final LatLng _groundOverlayPos2 = const LatLng(37.42, -122.08); + late LatLng _currentGroundOverlayPos; + + // Bounds for demonstranting placing ground overlays with bounds, and + // changing bounds. + final LatLngBounds _groundOverlayBounds1 = LatLngBounds( + southwest: const LatLng(37.42, -122.09), + northeast: const LatLng(37.423, -122.084)); + final LatLngBounds _groundOverlayBounds2 = LatLngBounds( + southwest: const LatLng(37.421, -122.091), + northeast: const LatLng(37.424, -122.08)); + late LatLngBounds _currentGroundOverlayBounds; + + Offset _anchor = const Offset(0.5, 0.5); + + Offset _dimensions = const Offset(1000, 1000); + + // Index to be used as identifier for the ground overlay. + // If position is changed to bounds and vice versa, the ground overlay will + // be removed and added again with the new type. Also anchor can be given only + // when the ground overlay is created with position and cannot be changed + // after the ground overlay is created. + int _groundOverlayIndex = 0; + + @override + void initState() { + _currentGroundOverlayPos = _groundOverlayPos1; + _currentGroundOverlayBounds = _groundOverlayBounds1; + super.initState(); + } + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + this.controller = controller; + } + + void _removeGroundOverlay() { + setState(() { + _groundOverlay = null; + }); + } + + Future _addGroundOverlay() async { + final AssetMapBitmap assetMapBitmap = await AssetMapBitmap.create( + createLocalImageConfiguration(context), + 'assets/red_square.png', + bitmapScaling: MapBitmapScaling.none, + ); + + _groundOverlayIndex += 1; + + final GroundOverlayId id = + GroundOverlayId('ground_overlay_$_groundOverlayIndex'); + + final GroundOverlay groundOverlay = switch (_placingType) { + _GroundOverlayPlacing.position => GroundOverlay.fromPosition( + groundOverlayId: id, + image: assetMapBitmap, + position: _currentGroundOverlayPos, + width: _dimensions.dx, + height: _dimensions.dy, + anchor: _anchor, + onTap: () { + _onGroundOverlayTapped(); + }, + zoomLevel: 14.0, + ), + _GroundOverlayPlacing.bounds => GroundOverlay.fromBounds( + groundOverlayId: id, + image: assetMapBitmap, + bounds: _currentGroundOverlayBounds, + onTap: () { + _onGroundOverlayTapped(); + }, + ), + }; + + setState(() { + _groundOverlay = groundOverlay; + }); + } + + void _onGroundOverlayTapped() { + _changePosition(); + } + + void _setBearing() { + assert(_groundOverlay != null); + setState(() { + _groundOverlay = _groundOverlay!.copyWith( + bearingParam: _groundOverlay!.bearing >= 350 + ? 0 + : _groundOverlay!.bearing + 10); + }); + } + + void _changeTransparency() { + assert(_groundOverlay != null); + setState(() { + final double transparency = + _groundOverlay!.transparency == 0.0 ? 0.5 : 0.0; + _groundOverlay = + _groundOverlay!.copyWith(transparencyParam: transparency); + }); + } + + Future _changeDimensions() async { + assert(_groundOverlay != null); + assert(_placingType == _GroundOverlayPlacing.position); + setState(() { + _dimensions = _dimensions == const Offset(1000, 1000) + ? const Offset(1500, 500) + : const Offset(1000, 1000); + }); + + // Re-add the ground overlay to apply the new position, as the position + // cannot be changed after the ground overlay is created on all platforms. + await _addGroundOverlay(); + } + + Future _changePosition() async { + assert(_groundOverlay != null); + assert(_placingType == _GroundOverlayPlacing.position); + setState(() { + _currentGroundOverlayPos = _currentGroundOverlayPos == _groundOverlayPos1 + ? _groundOverlayPos2 + : _groundOverlayPos1; + }); + + // Re-add the ground overlay to apply the new position, as the position + // cannot be changed after the ground overlay is created on all platforms. + await _addGroundOverlay(); + } + + Future _changeBounds() async { + assert(_groundOverlay != null); + assert(_placingType == _GroundOverlayPlacing.bounds); + setState(() { + _currentGroundOverlayBounds = + _currentGroundOverlayBounds == _groundOverlayBounds1 + ? _groundOverlayBounds2 + : _groundOverlayBounds1; + }); + // Re-add the ground overlay to apply the new position, as the position + // cannot be changed after the ground overlay is created on all platforms. + await _addGroundOverlay(); + } + + void _toggleVisible() { + assert(_groundOverlay != null); + setState(() { + _groundOverlay = + _groundOverlay!.copyWith(visibleParam: !_groundOverlay!.visible); + }); + } + + void _changeZIndex() { + assert(_groundOverlay != null); + final int current = _groundOverlay!.zIndex; + final int zIndex = current == 12 ? 0 : current + 1; + setState(() { + _groundOverlay = _groundOverlay!.copyWith(zIndexParam: zIndex); + }); + } + + Future _changeType() async { + setState(() { + _placingType = _placingType == _GroundOverlayPlacing.position + ? _GroundOverlayPlacing.bounds + : _GroundOverlayPlacing.position; + }); + + // Re-add the ground overlay to apply the new position, as the position + // cannot be changed after the ground overlay is created on all platforms. + await _addGroundOverlay(); + } + + Future _changeAnchor() async { + assert(_groundOverlay != null); + assert(_placingType == _GroundOverlayPlacing.position); + setState(() { + _anchor = _groundOverlay!.anchor == const Offset(0.5, 0.5) + ? const Offset(1.0, 1.0) + : const Offset(0.5, 0.5); + }); + + // Re-add the ground overlay to apply the new anchor as anchor cannot be + // changed after the ground overlay is created. + await _addGroundOverlay(); + } + + @override + Widget build(BuildContext context) { + final Set overlays = { + if (_groundOverlay != null) _groundOverlay!, + }; + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: ExampleGoogleMap( + initialCameraPosition: CameraPosition( + target: _mapCenter, + zoom: 14.0, + ), + groundOverlays: overlays, + onMapCreated: _onMapCreated, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: _groundOverlay == null ? _addGroundOverlay : null, + child: const Text('Add'), + ), + TextButton( + onPressed: _groundOverlay != null ? _removeGroundOverlay : null, + child: const Text('Remove'), + ), + ], + ), + Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: + _groundOverlay == null ? null : () => _changeTransparency(), + child: const Text('change transparency'), + ), + TextButton( + onPressed: _groundOverlay == null ? null : () => _setBearing(), + child: const Text('change bearing'), + ), + TextButton( + onPressed: _groundOverlay == null ? null : () => _toggleVisible(), + child: const Text('toggle visible'), + ), + TextButton( + onPressed: _groundOverlay == null ? null : () => _changeZIndex(), + child: const Text('change zIndex'), + ), + TextButton( + onPressed: _groundOverlay == null ? null : () => _changeType(), + child: Text(_placingType == _GroundOverlayPlacing.position + ? 'use bounds' + : 'use position'), + ), + TextButton( + onPressed: _placingType != _GroundOverlayPlacing.position || + _groundOverlay == null + ? null + : () => _changePosition(), + child: const Text('change position'), + ), + TextButton( + onPressed: _placingType != _GroundOverlayPlacing.position || + _groundOverlay == null + ? null + : () => _changeDimensions(), + child: const Text('change dimensions'), + ), + TextButton( + onPressed: _placingType != _GroundOverlayPlacing.position || + _groundOverlay == null + ? null + : () => _changeAnchor(), + child: const Text('change anchor'), + ), + TextButton( + onPressed: _placingType != _GroundOverlayPlacing.bounds || + _groundOverlay == null + ? null + : () => _changeBounds(), + child: const Text('change bounds'), + ), + ], + ), + ], + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml index 9e6466288a92..e098970f1ac9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../../../ - google_maps_flutter_platform_interface: ^2.9.0 + google_maps_flutter_platform_interface: ^2.10.0 dev_dependencies: flutter_test: diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/test/fake_google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/test/fake_google_maps_flutter_platform.dart index 9ac70ab760fe..cb3d24fef725 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/test/fake_google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/test/fake_google_maps_flutter_platform.dart @@ -103,6 +103,15 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { await _fakeDelay(); } + @override + Future updateGroundOverlays( + GroundOverlayUpdates groundOverlayUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.groundOverlayUpdates.add(groundOverlayUpdates); + await _fakeDelay(); + } + @override Future clearTileCache( TileOverlayId tileOverlayId, { @@ -255,6 +264,11 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { return mapEventStreamController.stream.whereType(); } + @override + Stream onGroundOverlayTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + @override void dispose({required int mapId}) { disposed = true; @@ -298,6 +312,8 @@ class PlatformMapStateRecorder { }) { clusterManagerUpdates.add(ClusterManagerUpdates.from( const {}, mapObjects.clusterManagers)); + groundOverlayUpdates.add(GroundOverlayUpdates.from( + const {}, mapObjects.groundOverlays)); markerUpdates.add(MarkerUpdates.from(const {}, mapObjects.markers)); polygonUpdates .add(PolygonUpdates.from(const {}, mapObjects.polygons)); @@ -318,4 +334,6 @@ class PlatformMapStateRecorder { final List> tileOverlaySets = >[]; final List clusterManagerUpdates = []; + final List groundOverlayUpdates = + []; } diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.h new file mode 100644 index 000000000000..de875b0fb2dc --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.h @@ -0,0 +1,64 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import +#import +#import + +#import "messages.g.h" + +NS_ASSUME_NONNULL_BEGIN + +/// Controller of a single ground overlay on the map. +@interface FGMGroundOverlayController : NSObject + +/// The ground overlay this controller handles. +@property(strong, nonatomic) GMSGroundOverlay *groundOverlay; + +/// Whether ground overlay is created with bounds or position. +@property(nonatomic, assign, getter=isCreatedWithBounds) BOOL createdWithBounds; + +/// Zoom level when ground overlay is initialized with position. +@property(nonatomic, strong, nullable) NSNumber *zoomLevel; + +/// Initializes an instance of this class with a GMSGroundOverlay, a map view, and identifier. +- (instancetype _Nullable)initWithGroundOverlay:(GMSGroundOverlay *)groundOverlay + identifier:(NSString *)identifier + mapView:(GMSMapView *)mapView + isCreatedWithBounds:(BOOL)isCreatedWithBounds; + +/// Removes this ground overlay from the map. +- (void)removeGroundOverlay; +@end + +/// Controller of multiple ground overlays on the map. +@interface FLTGroundOverlaysController : NSObject + +/// Initializes the controller with a GMSMapView, callback handler and registrar. +- (instancetype _Nullable)initWithMapView:(GMSMapView *)mapView + callbackHandler:(FGMMapsCallbackApi *)callbackHandler + registrar:(NSObject *)registrar; + +/// Adds ground overlays to the map. +- (void)addGroundOverlays:(NSArray *)groundOverlaysToAdd; + +/// Updates ground overlays on the map. +- (void)changeGroundOverlays:(NSArray *)groundOverlaysToChange; + +/// Removes ground overlays from the map. +- (void)removeGroundOverlaysWithIdentifiers:(NSArray *)identifiers; + +/// Called when a ground overlay is tapped on the map. +- (void)didTapGroundOverlayWithIdentifier:(NSString *)identifier; + +/// Returns true if a ground overlay with the given identifier exists on the map. +- (bool)hasGroundOverlaysWithIdentifier:(NSString *)identifier; + +/// Returns the ground overlay with the given identifier. +- (nullable FGMPlatformGroundOverlay *)groundOverlayWithIdentifier:(NSString *)identifier; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.m new file mode 100644 index 000000000000..29d9e2e4c5a0 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.m @@ -0,0 +1,234 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FGMGroundOverlayController.h" + +#import "FGMImageUtils.h" +#import "FLTGoogleMapJSONConversions.h" + +@interface FGMGroundOverlayController () + +/// The GMSMapView to which the ground overlays are added. +@property(weak, nonatomic) GMSMapView *mapView; + +@end + +@implementation FGMGroundOverlayController + +- (instancetype)initWithGroundOverlay:(GMSGroundOverlay *)groundOverlay + identifier:(NSString *)identifier + mapView:(GMSMapView *)mapView + isCreatedWithBounds:(BOOL)isCreatedWithBounds { + self = [super init]; + if (self) { + _groundOverlay = groundOverlay; + _mapView = mapView; + _groundOverlay.userData = @[ identifier ]; + _createdWithBounds = isCreatedWithBounds; + } + return self; +} + +- (void)removeGroundOverlay { + self.groundOverlay.map = nil; +} + +- (void)setConsumeTapEvents:(BOOL)consumes { + self.groundOverlay.tappable = consumes; +} + +- (void)setVisible:(BOOL)visible { + self.groundOverlay.map = visible ? self.mapView : nil; +} + +- (void)setZIndex:(int)zIndex { + self.groundOverlay.zIndex = zIndex; +} + +- (void)setAnchor:(CGPoint)anchor { + self.groundOverlay.anchor = anchor; +} + +- (void)setBearing:(CLLocationDirection)bearing { + self.groundOverlay.bearing = bearing; +} + +- (void)setTransparency:(float)transparency { + float opacity = 1.0 - transparency; + self.groundOverlay.opacity = opacity; +} + +- (void)setPositionFromBounds:(GMSCoordinateBounds *)bounds { + self.groundOverlay.bounds = bounds; +} + +- (void)setPositionFromCoordinates:(CLLocationCoordinate2D)coordinates { + self.groundOverlay.position = coordinates; +} + +- (void)setIcon:(UIImage *)icon { + self.groundOverlay.icon = icon; +} + +- (void)updateFromPlatformGroundOverlay:(FGMPlatformGroundOverlay *)groundOverlay + registrar:(NSObject *)registrar + screenScale:(CGFloat)screenScale { + [self setConsumeTapEvents:groundOverlay.clickable]; + [self setVisible:groundOverlay.visible]; + [self setZIndex:(int)groundOverlay.zIndex]; + [self setAnchor:CGPointMake(groundOverlay.anchor.x, groundOverlay.anchor.y)]; + UIImage *image = FGMIconFromBitmap(groundOverlay.image, registrar, screenScale); + [self setIcon:image]; + [self setBearing:groundOverlay.bearing]; + [self setTransparency:groundOverlay.transparency]; + if ([self isCreatedWithBounds]) { + [self setPositionFromBounds:[[GMSCoordinateBounds alloc] + initWithCoordinate:CLLocationCoordinate2DMake( + groundOverlay.bounds.northeast.latitude, + groundOverlay.bounds.northeast.longitude) + coordinate:CLLocationCoordinate2DMake( + groundOverlay.bounds.southwest.latitude, + groundOverlay.bounds.southwest + .longitude)]]; + } else { + [self setPositionFromCoordinates:CLLocationCoordinate2DMake(groundOverlay.position.latitude, + groundOverlay.position.longitude)]; + } +} + +@end + +@interface FLTGroundOverlaysController () + +/// A map from ground overlay id to the controller that manages it. +@property(strong, nonatomic) NSMutableDictionary + *groundOverlayControllerByIdentifier; + +/// A callback api for the map interactions. +@property(strong, nonatomic) FGMMapsCallbackApi *callbackHandler; + +/// Flutter Plugin Registrar used to load images. +@property(weak, nonatomic) NSObject *registrar; + +/// The map view used to generate the controllers. +@property(weak, nonatomic) GMSMapView *mapView; + +@end + +@implementation FLTGroundOverlaysController + +- (instancetype)initWithMapView:(GMSMapView *)mapView + callbackHandler:(FGMMapsCallbackApi *)callbackHandler + registrar:(NSObject *)registrar { + self = [super init]; + if (self) { + _callbackHandler = callbackHandler; + _mapView = mapView; + _groundOverlayControllerByIdentifier = [[NSMutableDictionary alloc] init]; + _registrar = registrar; + } + return self; +} + +- (void)addGroundOverlays:(NSArray *)groundOverlaysToAdd { + for (FGMPlatformGroundOverlay *groundOverlay in groundOverlaysToAdd) { + NSString *identifier = groundOverlay.groundOverlayId; + GMSGroundOverlay *gmsOverlay; + BOOL isCreatedWithBounds = NO; + if (groundOverlay.position == nil) { + isCreatedWithBounds = YES; + NSAssert(groundOverlay.bounds != nil, + @"If ground overlay is initialized without position, bounds are required"); + gmsOverlay = [GMSGroundOverlay + groundOverlayWithBounds: + [[GMSCoordinateBounds alloc] + initWithCoordinate:CLLocationCoordinate2DMake( + groundOverlay.bounds.northeast.latitude, + groundOverlay.bounds.northeast.longitude) + coordinate:CLLocationCoordinate2DMake( + groundOverlay.bounds.southwest.latitude, + groundOverlay.bounds.southwest.longitude)] + icon:FGMIconFromBitmap(groundOverlay.image, self.registrar, + [self getScreenScale])]; + } else { + NSAssert(groundOverlay.zoomLevel != nil, + @"If ground overlay is initialized with position, zoomLevel is required"); + gmsOverlay = [GMSGroundOverlay + groundOverlayWithPosition:CLLocationCoordinate2DMake(groundOverlay.position.latitude, + groundOverlay.position.longitude) + icon:FGMIconFromBitmap(groundOverlay.image, self.registrar, + [self getScreenScale]) + zoomLevel:[groundOverlay.zoomLevel doubleValue]]; + } + FGMGroundOverlayController *controller = + [[FGMGroundOverlayController alloc] initWithGroundOverlay:gmsOverlay + identifier:identifier + mapView:self.mapView + isCreatedWithBounds:isCreatedWithBounds]; + controller.zoomLevel = groundOverlay.zoomLevel; + [controller updateFromPlatformGroundOverlay:groundOverlay + registrar:self.registrar + screenScale:[self getScreenScale]]; + self.groundOverlayControllerByIdentifier[identifier] = controller; + } +} + +- (void)changeGroundOverlays:(NSArray *)groundOverlaysToChange { + for (FGMPlatformGroundOverlay *groundOverlay in groundOverlaysToChange) { + NSString *identifier = groundOverlay.groundOverlayId; + FGMGroundOverlayController *controller = self.groundOverlayControllerByIdentifier[identifier]; + [controller updateFromPlatformGroundOverlay:groundOverlay + registrar:self.registrar + screenScale:[self getScreenScale]]; + } +} + +- (void)removeGroundOverlaysWithIdentifiers:(NSArray *)identifiers { + for (NSString *identifier in identifiers) { + FGMGroundOverlayController *controller = self.groundOverlayControllerByIdentifier[identifier]; + if (!controller) { + continue; + } + [controller removeGroundOverlay]; + [self.groundOverlayControllerByIdentifier removeObjectForKey:identifier]; + } +} + +- (void)didTapGroundOverlayWithIdentifier:(NSString *)identifier { + if (!identifier) { + return; + } + FGMGroundOverlayController *controller = self.groundOverlayControllerByIdentifier[identifier]; + if (!controller) { + return; + } + [self.callbackHandler didTapGroundOverlayWithIdentifier:identifier + completion:^(FlutterError *_Nullable _){ + }]; +} + +- (bool)hasGroundOverlaysWithIdentifier:(NSString *)identifier { + return self.groundOverlayControllerByIdentifier[identifier] != nil; +} + +- (CGFloat)getScreenScale { + // TODO(jokerttu): This method is called on marker creation, which, for initial markers, is done + // before the view is added to the view hierarchy. This means that the traitCollection values may + // not be matching the right display where the map is finally shown. The solution should be + // revisited after the proper way to fetch the display scale is resolved for platform views. This + // should be done under the context of the following issue: + // https://github.com/flutter/flutter/issues/125496. + return self.mapView.traitCollection.displayScale; +} + +- (nullable FGMPlatformGroundOverlay *)groundOverlayWithIdentifier:(NSString *)identifier { + FGMGroundOverlayController *controller = self.groundOverlayControllerByIdentifier[identifier]; + if (!controller) { + return nil; + } + return FGMGetPigeonGroundOverlay(controller.groundOverlay, identifier, + controller.isCreatedWithBounds, controller.zoomLevel); +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController_Test.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController_Test.h new file mode 100644 index 000000000000..5c4201a7b7ff --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController_Test.h @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FGMGroundOverlayController.h" + +/// Internal APIs exposed for unit testing +@interface FGMGroundOverlayController (Test) + +/// Ground Overlay instance the controller is attached to +@property(strong, nonatomic) GMSGroundOverlay *groundOverlay; + +/// Function to update the gms ground overlay from platform ground overlay. +- (void)updateFromPlatformGroundOverlay:(FGMPlatformGroundOverlay *)groundOverlay + registrar:(NSObject *)registrar + screenScale:(CGFloat)screenScale; + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.h new file mode 100644 index 000000000000..8aa1bcb67934 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.h @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "messages.g.h" + +NS_ASSUME_NONNULL_BEGIN + +/// Creates a UIImage from Pigeon bitmap. +UIImage *FGMIconFromBitmap(FGMPlatformBitmap *platformBitmap, + NSObject *registrar, CGFloat screenScale); +/// Returns a BOOL indicating whether image is considered scalable with the given scale factor from +/// size. +BOOL FGMIsScalableWithScaleFactorFromSize(CGSize originalSize, CGSize targetSize); + +NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.m new file mode 100644 index 000000000000..3830ea28f604 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.m @@ -0,0 +1,234 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FGMImageUtils.h" +#import "Foundation/Foundation.h" + +/// This method is deprecated within the context of `BitmapDescriptor.fromBytes` handling in the +/// flutter google_maps_flutter_platform_interface package which has been replaced by 'bytes' +/// message handling. It will be removed when the deprecated image bitmap description type +/// 'fromBytes' is removed from the platform interface. +static UIImage *scaledImage(UIImage *image, double scale); + +/// Creates a scaled version of the provided UIImage based on a specified scale factor. If the +/// scale factor differs from the image's current scale by more than a small epsilon-delta (to +/// account for minor floating-point inaccuracies), a new UIImage object is created with the +/// specified scale. Otherwise, the original image is returned. +/// +/// @param image The UIImage to scale. +/// @param scale The factor by which to scale the image. +/// @return UIImage Returns the scaled UIImage. +static UIImage *scaledImageWithScale(UIImage *image, CGFloat scale); + +/// Scales an input UIImage to a specified size. If the aspect ratio of the input image +/// closely matches the target size, indicated by a small epsilon-delta, the image's scale +/// property is updated instead of resizing the image. If the aspect ratios differ beyond this +/// threshold, the method redraws the image at the target size. +/// +/// @param image The UIImage to scale. +/// @param size The target CGSize to scale the image to. +/// @return UIImage Returns the scaled UIImage. +static UIImage *scaledImageWithSize(UIImage *image, CGSize size); + +/// Scales an input UIImage to a specified width and height preserving aspect ratio if both +/// widht and height are not given.. +/// +/// @param image The UIImage to scale. +/// @param width The target width to scale the image to. +/// @param height The target height to scale the image to. +/// @param screenScale The current screen scale. +/// @return UIImage Returns the scaled UIImage. +static UIImage *scaledImageWithWidthHeight(UIImage *image, NSNumber *width, NSNumber *height, + CGFloat screenScale); + +UIImage *FGMIconFromBitmap(FGMPlatformBitmap *platformBitmap, + NSObject *registrar, CGFloat screenScale) { + assert(screenScale > 0 && "Screen scale must be greater than 0"); + // See comment in messages.dart for why this is so loosely typed. See also + // https://github.com/flutter/flutter/issues/117819. + id bitmap = platformBitmap.bitmap; + UIImage *image; + if ([bitmap isKindOfClass:[FGMPlatformBitmapDefaultMarker class]]) { + FGMPlatformBitmapDefaultMarker *bitmapDefaultMarker = bitmap; + CGFloat hue = bitmapDefaultMarker.hue.doubleValue; + image = [GMSMarker markerImageWithColor:[UIColor colorWithHue:hue / 360.0 + saturation:1.0 + brightness:0.7 + alpha:1.0]]; + } else if ([bitmap isKindOfClass:[FGMPlatformBitmapAsset class]]) { + // Deprecated: This message handling for 'fromAsset' has been replaced by 'asset'. + // Refer to the flutter google_maps_flutter_platform_interface package for details. + FGMPlatformBitmapAsset *bitmapAsset = bitmap; + if (bitmapAsset.pkg) { + image = [UIImage imageNamed:[registrar lookupKeyForAsset:bitmapAsset.name + fromPackage:bitmapAsset.pkg]]; + } else { + image = [UIImage imageNamed:[registrar lookupKeyForAsset:bitmapAsset.name]]; + } + } else if ([bitmap isKindOfClass:[FGMPlatformBitmapAssetImage class]]) { + // Deprecated: This message handling for 'fromAssetImage' has been replaced by 'asset'. + // Refer to the flutter google_maps_flutter_platform_interface package for details. + FGMPlatformBitmapAssetImage *bitmapAssetImage = bitmap; + image = [UIImage imageNamed:[registrar lookupKeyForAsset:bitmapAssetImage.name]]; + image = scaledImage(image, bitmapAssetImage.scale); + } else if ([bitmap isKindOfClass:[FGMPlatformBitmapBytes class]]) { + // Deprecated: This message handling for 'fromBytes' has been replaced by 'bytes'. + // Refer to the flutter google_maps_flutter_platform_interface package for details. + FGMPlatformBitmapBytes *bitmapBytes = bitmap; + @try { + CGFloat mainScreenScale = [[UIScreen mainScreen] scale]; + image = [UIImage imageWithData:bitmapBytes.byteData.data scale:mainScreenScale]; + } @catch (NSException *exception) { + @throw [NSException exceptionWithName:@"InvalidByteDescriptor" + reason:@"Unable to interpret bytes as a valid image." + userInfo:nil]; + } + } else if ([bitmap isKindOfClass:[FGMPlatformBitmapAssetMap class]]) { + FGMPlatformBitmapAssetMap *bitmapAssetMap = bitmap; + + image = [UIImage imageNamed:[registrar lookupKeyForAsset:bitmapAssetMap.assetName]]; + + if (bitmapAssetMap.bitmapScaling == FGMPlatformMapBitmapScalingAuto) { + NSNumber *width = bitmapAssetMap.width; + NSNumber *height = bitmapAssetMap.height; + if (width || height) { + image = scaledImageWithScale(image, screenScale); + image = scaledImageWithWidthHeight(image, width, height, screenScale); + } else { + image = scaledImageWithScale(image, bitmapAssetMap.imagePixelRatio); + } + } + } else if ([bitmap isKindOfClass:[FGMPlatformBitmapBytesMap class]]) { + FGMPlatformBitmapBytesMap *bitmapBytesMap = bitmap; + FlutterStandardTypedData *bytes = bitmapBytesMap.byteData; + + @try { + image = [UIImage imageWithData:bytes.data scale:screenScale]; + if (bitmapBytesMap.bitmapScaling == FGMPlatformMapBitmapScalingAuto) { + NSNumber *width = bitmapBytesMap.width; + NSNumber *height = bitmapBytesMap.height; + + if (width || height) { + // Before scaling the image, image must be in screenScale. + image = scaledImageWithScale(image, screenScale); + image = scaledImageWithWidthHeight(image, width, height, screenScale); + } else { + image = scaledImageWithScale(image, bitmapBytesMap.imagePixelRatio); + } + } else { + // No scaling, load image from bytes without scale parameter. + image = [UIImage imageWithData:bytes.data]; + } + } @catch (NSException *exception) { + @throw [NSException exceptionWithName:@"InvalidByteDescriptor" + reason:@"Unable to interpret bytes as a valid image." + userInfo:nil]; + } + } + + return image; +} + +UIImage *scaledImage(UIImage *image, double scale) { + if (fabs(scale - 1) > 1e-3) { + return [UIImage imageWithCGImage:[image CGImage] + scale:(image.scale * scale) + orientation:(image.imageOrientation)]; + } + return image; +} + +UIImage *scaledImageWithScale(UIImage *image, CGFloat scale) { + if (fabs(scale - image.scale) > DBL_EPSILON) { + return [UIImage imageWithCGImage:[image CGImage] + scale:scale + orientation:(image.imageOrientation)]; + } + return image; +} + +UIImage *scaledImageWithSize(UIImage *image, CGSize size) { + CGFloat originalPixelWidth = image.size.width * image.scale; + CGFloat originalPixelHeight = image.size.height * image.scale; + + // Return original image if either original image size or target size is so small that + // image cannot be resized or displayed. + if (originalPixelWidth <= 0 || originalPixelHeight <= 0 || size.width <= 0 || size.height <= 0) { + return image; + } + + // Check if the image's size, accounting for scale, matches the target size. + if (fabs(originalPixelWidth - size.width) <= DBL_EPSILON && + fabs(originalPixelHeight - size.height) <= DBL_EPSILON) { + // No need for resizing, return the original image + return image; + } + + // Check if the aspect ratios are approximately equal. + CGSize originalPixelSize = CGSizeMake(originalPixelWidth, originalPixelHeight); + if (FGMIsScalableWithScaleFactorFromSize(originalPixelSize, size)) { + // Scaled image has close to same aspect ratio, + // updating image scale instead of resizing image. + CGFloat factor = originalPixelWidth / size.width; + return scaledImageWithScale(image, image.scale * factor); + } else { + // Aspect ratios differ significantly, resize the image. + UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat]; + format.scale = 1.0; + format.opaque = NO; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size + format:format]; + UIImage *newImage = + [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull context) { + [image drawInRect:CGRectMake(0, 0, size.width, size.height)]; + }]; + + // Return image with proper scaling. + return scaledImageWithScale(newImage, image.scale); + } +} + +UIImage *scaledImageWithWidthHeight(UIImage *image, NSNumber *width, NSNumber *height, + CGFloat screenScale) { + if (!width && !height) { + return image; + } + + CGFloat targetWidth = width ? width.doubleValue : image.size.width; + CGFloat targetHeight = height ? height.doubleValue : image.size.height; + + if (width && !height) { + // Calculate height based on aspect ratio if only width is provided. + double aspectRatio = image.size.height / image.size.width; + targetHeight = round(targetWidth * aspectRatio); + } else if (!width && height) { + // Calculate width based on aspect ratio if only height is provided. + double aspectRatio = image.size.width / image.size.height; + targetWidth = round(targetHeight * aspectRatio); + } + + CGSize targetSize = + CGSizeMake(round(targetWidth * screenScale), round(targetHeight * screenScale)); + return scaledImageWithSize(image, targetSize); +} + +BOOL FGMIsScalableWithScaleFactorFromSize(CGSize originalSize, CGSize targetSize) { + // Select the scaling factor based on the longer side to have good precision. + CGFloat scaleFactor = (originalSize.width > originalSize.height) + ? (targetSize.width / originalSize.width) + : (targetSize.height / originalSize.height); + + // Calculate the scaled dimensions. + CGFloat scaledWidth = originalSize.width * scaleFactor; + CGFloat scaledHeight = originalSize.height * scaleFactor; + + // Check if the scaled dimensions are within a one-pixel + // threshold of the target dimensions. + BOOL widthWithinThreshold = fabs(scaledWidth - targetSize.width) <= 1.0; + BOOL heightWithinThreshold = fabs(scaledHeight - targetSize.height) <= 1.0; + + // The image is considered scalable with scale factor + // if both dimensions are within the threshold. + return widthWithinThreshold && heightWithinThreshold; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h index 2d853e9bfe98..ea5db67f2bec 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h @@ -58,6 +58,12 @@ extern GMSMapViewType FGMGetMapViewTypeForPigeonMapType(FGMPlatformMapType type) extern FGMPlatformCluster *FGMGetPigeonCluster(GMUStaticCluster *cluster, NSString *clusterManagerIdentifier); +/// Converts a GMSGroundOverlay to its Pigeon representation. +extern FGMPlatformGroundOverlay *FGMGetPigeonGroundOverlay(GMSGroundOverlay *groundOverlay, + NSString *overlayId, + BOOL isCreatedWithBounds, + NSNumber *zoomLevel); + /// Creates a GMSCameraUpdate from its Pigeon equivalent. extern GMSCameraUpdate *_Nullable FGMGetCameraUpdateForPigeonCameraUpdate( FGMPlatformCameraUpdate *update); diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.m index 25504291192e..79177348a42b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.m @@ -113,6 +113,57 @@ GMSMapViewType FGMGetMapViewTypeForPigeonMapType(FGMPlatformMapType type) { markerIds:markerIDs]; } +FGMPlatformGroundOverlay *FGMGetPigeonGroundOverlay(GMSGroundOverlay *groundOverlay, + NSString *overlayId, BOOL isCreatedWithBounds, + NSNumber *zoomLevel) { + /// Dummy image is used as image is required field of FGMPlatformGroundOverlay and converting + /// image back to bitmap image is not currently supported. + FGMPlatformBitmap *placeholderImage = + [FGMPlatformBitmap makeWithBitmap:[FGMPlatformBitmapDefaultMarker makeWithHue:0]]; + if (isCreatedWithBounds) { + return [FGMPlatformGroundOverlay + makeWithGroundOverlayId:overlayId + image:placeholderImage + position:nil + bounds:[FGMPlatformLatLngBounds + makeWithNortheast:[FGMPlatformLatLng + makeWithLatitude:groundOverlay.bounds + .northEast.latitude + longitude:groundOverlay.bounds + .northEast.longitude] + southwest:[FGMPlatformLatLng + makeWithLatitude:groundOverlay.bounds + .southWest.latitude + longitude:groundOverlay.bounds + .southWest + .longitude]] + anchor:[FGMPlatformPoint makeWithX:groundOverlay.anchor.x + y:groundOverlay.anchor.y] + transparency:1.0f - groundOverlay.opacity + bearing:groundOverlay.bearing + zIndex:groundOverlay.zIndex + visible:groundOverlay.map != nil + clickable:groundOverlay.isTappable + zoomLevel:zoomLevel]; + } else { + return [FGMPlatformGroundOverlay + makeWithGroundOverlayId:overlayId + image:placeholderImage + position:[FGMPlatformLatLng + makeWithLatitude:groundOverlay.position.latitude + longitude:groundOverlay.position.longitude] + bounds:nil + anchor:[FGMPlatformPoint makeWithX:groundOverlay.anchor.x + y:groundOverlay.anchor.y] + transparency:1.0f - groundOverlay.opacity + bearing:groundOverlay.bearing + zIndex:groundOverlay.zIndex + visible:groundOverlay.map != nil + clickable:groundOverlay.isTappable + zoomLevel:zoomLevel]; + } +} + GMSCameraUpdate *FGMGetCameraUpdateForPigeonCameraUpdate(FGMPlatformCameraUpdate *cameraUpdate) { // See note in messages.dart for why this is so loosely typed. id update = cameraUpdate.cameraUpdate; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m index 0108a3f72b2a..a57a9ff51836 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m @@ -6,6 +6,7 @@ #import "GoogleMapController.h" +#import "FGMGroundOverlayController.h" #import "FGMMarkerUserData.h" #import "FLTGoogleMapHeatmapController.h" #import "FLTGoogleMapJSONConversions.h" @@ -129,6 +130,7 @@ @interface FLTGoogleMapController () // The controller that handles heatmaps @property(nonatomic, strong) FLTHeatmapsController *heatmapsController; @property(nonatomic, strong) FLTTileOverlaysController *tileOverlaysController; +@property(nonatomic, strong) FLTGroundOverlaysController *groundOverlaysController; // The resulting error message, if any, from the last attempt to set the map style. // This is used to provide access to errors after the fact, since the map style is generally set at // creation time and there's no mechanism to return non-fatal error details during platform view @@ -204,6 +206,10 @@ - (instancetype)initWithMapView:(GMSMapView *_Nonnull)mapView [[FLTTileOverlaysController alloc] initWithMapView:_mapView callbackHandler:_dartCallbackHandler registrar:registrar]; + _groundOverlaysController = + [[FLTGroundOverlaysController alloc] initWithMapView:_mapView + callbackHandler:_dartCallbackHandler + registrar:registrar]; [_clusterManagersController addClusterManagers:creationParameters.initialClusterManagers]; [_markersController addMarkers:creationParameters.initialMarkers]; [_polygonsController addPolygons:creationParameters.initialPolygons]; @@ -211,6 +217,7 @@ - (instancetype)initWithMapView:(GMSMapView *_Nonnull)mapView [_circlesController addCircles:creationParameters.initialCircles]; [_heatmapsController addHeatmaps:creationParameters.initialHeatmaps]; [_tileOverlaysController addTileOverlays:creationParameters.initialTileOverlays]; + [_groundOverlaysController addGroundOverlays:creationParameters.initialGroundOverlays]; // Invoke clustering after markers are added. [_clusterManagersController invokeClusteringForEachClusterManager]; @@ -423,6 +430,8 @@ - (void)mapView:(GMSMapView *)mapView didTapOverlay:(GMSOverlay *)overlay { [self.polygonsController didTapPolygonWithIdentifier:overlayId]; } else if ([self.circlesController hasCircleWithIdentifier:overlayId]) { [self.circlesController didTapCircleWithIdentifier:overlayId]; + } else if ([self.groundOverlaysController hasGroundOverlaysWithIdentifier:overlayId]) { + [self.groundOverlaysController didTapGroundOverlayWithIdentifier:overlayId]; } } @@ -602,6 +611,15 @@ - (void)updateTileOverlaysByAdding:(nonnull NSArray *) [self.controller.tileOverlaysController removeTileOverlayWithIdentifiers:idsToRemove]; } +- (void)updateGroundOverlaysByAdding:(nonnull NSArray *)toAdd + changing:(nonnull NSArray *)toChange + removing:(nonnull NSArray *)idsToRemove + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + [self.controller.groundOverlaysController addGroundOverlays:toAdd]; + [self.controller.groundOverlaysController changeGroundOverlays:toChange]; + [self.controller.groundOverlaysController removeGroundOverlaysWithIdentifiers:idsToRemove]; +} + - (nullable FGMPlatformLatLng *) latLngForScreenCoordinate:(nonnull FGMPlatformPoint *)screenCoordinate error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { @@ -822,4 +840,10 @@ - (nullable FGMPlatformZoomRange *)zoomRange: max:@(self.controller.mapView.maxZoom)]; } +- (nullable FGMPlatformGroundOverlay *) + groundOverlayWithIdentifier:(NSString *)groundOverlayId + error:(FlutterError *_Nullable __autoreleasing *)error { + return [self.controller.groundOverlaysController groundOverlayWithIdentifier:groundOverlayId]; +} + @end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.m index cfbc9392159f..a76915d7d20c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.m @@ -4,6 +4,7 @@ #import "GoogleMapMarkerController.h" +#import "FGMImageUtils.h" #import "FGMMarkerUserData.h" #import "FLTGoogleMapJSONConversions.h" @@ -112,9 +113,7 @@ - (void)updateFromPlatformMarker:(FGMPlatformMarker *)platformMarker [self setAlpha:platformMarker.alpha]; [self setAnchor:FGMGetCGPointForPigeonPoint(platformMarker.anchor)]; [self setDraggable:platformMarker.draggable]; - UIImage *image = [self iconFromBitmap:platformMarker.icon - registrar:registrar - screenScale:screenScale]; + UIImage *image = FGMIconFromBitmap(platformMarker.icon, registrar, screenScale); [self setIcon:image]; [self setFlat:platformMarker.flat]; [self setConsumeTapEvents:platformMarker.consumeTapEvents]; @@ -139,237 +138,6 @@ - (void)interpretInfoWindow:(NSDictionary *)data { } } -- (UIImage *)iconFromBitmap:(FGMPlatformBitmap *)platformBitmap - registrar:(NSObject *)registrar - screenScale:(CGFloat)screenScale { - NSAssert(screenScale > 0, @"Screen scale must be greater than 0"); - // See comment in messages.dart for why this is so loosely typed. See also - // https://github.com/flutter/flutter/issues/117819. - id bitmap = platformBitmap.bitmap; - UIImage *image; - if ([bitmap isKindOfClass:[FGMPlatformBitmapDefaultMarker class]]) { - FGMPlatformBitmapDefaultMarker *bitmapDefaultMarker = bitmap; - CGFloat hue = bitmapDefaultMarker.hue.doubleValue; - image = [GMSMarker markerImageWithColor:[UIColor colorWithHue:hue / 360.0 - saturation:1.0 - brightness:0.7 - alpha:1.0]]; - } else if ([bitmap isKindOfClass:[FGMPlatformBitmapAsset class]]) { - // Deprecated: This message handling for 'fromAsset' has been replaced by 'asset'. - // Refer to the flutter google_maps_flutter_platform_interface package for details. - FGMPlatformBitmapAsset *bitmapAsset = bitmap; - if (bitmapAsset.pkg) { - image = [UIImage imageNamed:[registrar lookupKeyForAsset:bitmapAsset.name - fromPackage:bitmapAsset.pkg]]; - } else { - image = [UIImage imageNamed:[registrar lookupKeyForAsset:bitmapAsset.name]]; - } - } else if ([bitmap isKindOfClass:[FGMPlatformBitmapAssetImage class]]) { - // Deprecated: This message handling for 'fromAssetImage' has been replaced by 'asset'. - // Refer to the flutter google_maps_flutter_platform_interface package for details. - FGMPlatformBitmapAssetImage *bitmapAssetImage = bitmap; - image = [UIImage imageNamed:[registrar lookupKeyForAsset:bitmapAssetImage.name]]; - image = [self scaleImage:image by:bitmapAssetImage.scale]; - } else if ([bitmap isKindOfClass:[FGMPlatformBitmapBytes class]]) { - // Deprecated: This message handling for 'fromBytes' has been replaced by 'bytes'. - // Refer to the flutter google_maps_flutter_platform_interface package for details. - FGMPlatformBitmapBytes *bitmapBytes = bitmap; - @try { - CGFloat mainScreenScale = [[UIScreen mainScreen] scale]; - image = [UIImage imageWithData:bitmapBytes.byteData.data scale:mainScreenScale]; - } @catch (NSException *exception) { - @throw [NSException exceptionWithName:@"InvalidByteDescriptor" - reason:@"Unable to interpret bytes as a valid image." - userInfo:nil]; - } - } else if ([bitmap isKindOfClass:[FGMPlatformBitmapAssetMap class]]) { - FGMPlatformBitmapAssetMap *bitmapAssetMap = bitmap; - - image = [UIImage imageNamed:[registrar lookupKeyForAsset:bitmapAssetMap.assetName]]; - - if (bitmapAssetMap.bitmapScaling == FGMPlatformMapBitmapScalingAuto) { - NSNumber *width = bitmapAssetMap.width; - NSNumber *height = bitmapAssetMap.height; - if (width || height) { - image = [FLTGoogleMapMarkerController scaledImage:image withScale:screenScale]; - image = [FLTGoogleMapMarkerController scaledImage:image - withWidth:width - height:height - screenScale:screenScale]; - } else { - image = [FLTGoogleMapMarkerController scaledImage:image - withScale:bitmapAssetMap.imagePixelRatio]; - } - } - } else if ([bitmap isKindOfClass:[FGMPlatformBitmapBytesMap class]]) { - FGMPlatformBitmapBytesMap *bitmapBytesMap = bitmap; - FlutterStandardTypedData *bytes = bitmapBytesMap.byteData; - - @try { - image = [UIImage imageWithData:bytes.data scale:screenScale]; - if (bitmapBytesMap.bitmapScaling == FGMPlatformMapBitmapScalingAuto) { - NSNumber *width = bitmapBytesMap.width; - NSNumber *height = bitmapBytesMap.height; - - if (width || height) { - // Before scaling the image, image must be in screenScale. - image = [FLTGoogleMapMarkerController scaledImage:image withScale:screenScale]; - image = [FLTGoogleMapMarkerController scaledImage:image - withWidth:width - height:height - screenScale:screenScale]; - } else { - image = [FLTGoogleMapMarkerController scaledImage:image - withScale:bitmapBytesMap.imagePixelRatio]; - } - } else { - // No scaling, load image from bytes without scale parameter. - image = [UIImage imageWithData:bytes.data]; - } - } @catch (NSException *exception) { - @throw [NSException exceptionWithName:@"InvalidByteDescriptor" - reason:@"Unable to interpret bytes as a valid image." - userInfo:nil]; - } - } - - return image; -} - -/// This method is deprecated within the context of `BitmapDescriptor.fromBytes` handling in the -/// flutter google_maps_flutter_platform_interface package which has been replaced by 'bytes' -/// message handling. It will be removed when the deprecated image bitmap description type -/// 'fromBytes' is removed from the platform interface. -- (UIImage *)scaleImage:(UIImage *)image by:(double)scale { - if (fabs(scale - 1) > 1e-3) { - return [UIImage imageWithCGImage:[image CGImage] - scale:(image.scale * scale) - orientation:(image.imageOrientation)]; - } - return image; -} - -/// Creates a scaled version of the provided UIImage based on a specified scale factor. If the -/// scale factor differs from the image's current scale by more than a small epsilon-delta (to -/// account for minor floating-point inaccuracies), a new UIImage object is created with the -/// specified scale. Otherwise, the original image is returned. -/// -/// @param image The UIImage to scale. -/// @param scale The factor by which to scale the image. -/// @return UIImage Returns the scaled UIImage. -+ (UIImage *)scaledImage:(UIImage *)image withScale:(CGFloat)scale { - if (fabs(scale - image.scale) > DBL_EPSILON) { - return [UIImage imageWithCGImage:[image CGImage] - scale:scale - orientation:(image.imageOrientation)]; - } - return image; -} - -/// Scales an input UIImage to a specified size. If the aspect ratio of the input image -/// closely matches the target size, indicated by a small epsilon-delta, the image's scale -/// property is updated instead of resizing the image. If the aspect ratios differ beyond this -/// threshold, the method redraws the image at the target size. -/// -/// @param image The UIImage to scale. -/// @param size The target CGSize to scale the image to. -/// @return UIImage Returns the scaled UIImage. -+ (UIImage *)scaledImage:(UIImage *)image withSize:(CGSize)size { - CGFloat originalPixelWidth = image.size.width * image.scale; - CGFloat originalPixelHeight = image.size.height * image.scale; - - // Return original image if either original image size or target size is so small that - // image cannot be resized or displayed. - if (originalPixelWidth <= 0 || originalPixelHeight <= 0 || size.width <= 0 || size.height <= 0) { - return image; - } - - // Check if the image's size, accounting for scale, matches the target size. - if (fabs(originalPixelWidth - size.width) <= DBL_EPSILON && - fabs(originalPixelHeight - size.height) <= DBL_EPSILON) { - // No need for resizing, return the original image - return image; - } - - // Check if the aspect ratios are approximately equal. - CGSize originalPixelSize = CGSizeMake(originalPixelWidth, originalPixelHeight); - if ([FLTGoogleMapMarkerController isScalableWithScaleFactorFromSize:originalPixelSize - toSize:size]) { - // Scaled image has close to same aspect ratio, - // updating image scale instead of resizing image. - CGFloat factor = originalPixelWidth / size.width; - return [FLTGoogleMapMarkerController scaledImage:image withScale:(image.scale * factor)]; - } else { - // Aspect ratios differ significantly, resize the image. - UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat]; - format.scale = 1.0; - format.opaque = NO; - UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size - format:format]; - UIImage *newImage = - [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull context) { - [image drawInRect:CGRectMake(0, 0, size.width, size.height)]; - }]; - - // Return image with proper scaling. - return [FLTGoogleMapMarkerController scaledImage:newImage withScale:image.scale]; - } -} - -/// Scales an input UIImage to a specified width and height preserving aspect ratio if both -/// widht and height are not given.. -/// -/// @param image The UIImage to scale. -/// @param width The target width to scale the image to. -/// @param height The target height to scale the image to. -/// @param screenScale The current screen scale. -/// @return UIImage Returns the scaled UIImage. -+ (UIImage *)scaledImage:(UIImage *)image - withWidth:(NSNumber *)width - height:(NSNumber *)height - screenScale:(CGFloat)screenScale { - if (!width && !height) { - return image; - } - - CGFloat targetWidth = width ? width.doubleValue : image.size.width; - CGFloat targetHeight = height ? height.doubleValue : image.size.height; - - if (width && !height) { - // Calculate height based on aspect ratio if only width is provided. - double aspectRatio = image.size.height / image.size.width; - targetHeight = round(targetWidth * aspectRatio); - } else if (!width && height) { - // Calculate width based on aspect ratio if only height is provided. - double aspectRatio = image.size.width / image.size.height; - targetWidth = round(targetHeight * aspectRatio); - } - - CGSize targetSize = - CGSizeMake(round(targetWidth * screenScale), round(targetHeight * screenScale)); - return [FLTGoogleMapMarkerController scaledImage:image withSize:targetSize]; -} - -+ (BOOL)isScalableWithScaleFactorFromSize:(CGSize)originalSize toSize:(CGSize)targetSize { - // Select the scaling factor based on the longer side to have good precision. - CGFloat scaleFactor = (originalSize.width > originalSize.height) - ? (targetSize.width / originalSize.width) - : (targetSize.height / originalSize.height); - - // Calculate the scaled dimensions. - CGFloat scaledWidth = originalSize.width * scaleFactor; - CGFloat scaledHeight = originalSize.height * scaleFactor; - - // Check if the scaled dimensions are within a one-pixel - // threshold of the target dimensions. - BOOL widthWithinThreshold = fabs(scaledWidth - targetSize.width) <= 1.0; - BOOL heightWithinThreshold = fabs(scaledHeight - targetSize.height) <= 1.0; - - // The image is considered scalable with scale factor - // if both dimensions are within the threshold. - return widthWithinThreshold && heightWithinThreshold; -} - @end @interface FLTMarkersController () diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios-umbrella.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios-umbrella.h index c88ee79c7a0d..af821c5ffdfa 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios-umbrella.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios-umbrella.h @@ -3,6 +3,9 @@ // found in the LICENSE file. #import +#import +#import +#import #import #import #import diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.h index b8c1bb5c35da..1f51686fecbf 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.h @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.6.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.3), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @@ -91,6 +91,7 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @class FGMPlatformLatLng; @class FGMPlatformLatLngBounds; @class FGMPlatformCameraTargetBounds; +@class FGMPlatformGroundOverlay; @class FGMPlatformMapViewCreationParams; @class FGMPlatformMapConfiguration; @class FGMPlatformPoint; @@ -432,6 +433,34 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @property(nonatomic, strong, nullable) FGMPlatformLatLngBounds *bounds; @end +/// Pigeon equivalent of the GroundOverlay class. +@interface FGMPlatformGroundOverlay : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithGroundOverlayId:(NSString *)groundOverlayId + image:(FGMPlatformBitmap *)image + position:(nullable FGMPlatformLatLng *)position + bounds:(nullable FGMPlatformLatLngBounds *)bounds + anchor:(nullable FGMPlatformPoint *)anchor + transparency:(double)transparency + bearing:(double)bearing + zIndex:(NSInteger)zIndex + visible:(BOOL)visible + clickable:(BOOL)clickable + zoomLevel:(nullable NSNumber *)zoomLevel; +@property(nonatomic, copy) NSString *groundOverlayId; +@property(nonatomic, strong) FGMPlatformBitmap *image; +@property(nonatomic, strong, nullable) FGMPlatformLatLng *position; +@property(nonatomic, strong, nullable) FGMPlatformLatLngBounds *bounds; +@property(nonatomic, strong, nullable) FGMPlatformPoint *anchor; +@property(nonatomic, assign) double transparency; +@property(nonatomic, assign) double bearing; +@property(nonatomic, assign) NSInteger zIndex; +@property(nonatomic, assign) BOOL visible; +@property(nonatomic, assign) BOOL clickable; +@property(nonatomic, strong, nullable) NSNumber *zoomLevel; +@end + /// Information passed to the platform view creation. @interface FGMPlatformMapViewCreationParams : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. @@ -445,7 +474,8 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { initialPolylines:(NSArray *)initialPolylines initialHeatmaps:(NSArray *)initialHeatmaps initialTileOverlays:(NSArray *)initialTileOverlays - initialClusterManagers:(NSArray *)initialClusterManagers; + initialClusterManagers:(NSArray *)initialClusterManagers + initialGroundOverlays:(NSArray *)initialGroundOverlays; @property(nonatomic, strong) FGMPlatformCameraPosition *initialCameraPosition; @property(nonatomic, strong) FGMPlatformMapConfiguration *mapConfiguration; @property(nonatomic, copy) NSArray *initialCircles; @@ -455,6 +485,7 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @property(nonatomic, copy) NSArray *initialHeatmaps; @property(nonatomic, copy) NSArray *initialTileOverlays; @property(nonatomic, copy) NSArray *initialClusterManagers; +@property(nonatomic, copy) NSArray *initialGroundOverlays; @end /// Pigeon equivalent of MapConfiguration. @@ -551,15 +582,13 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @property(nonatomic, strong) id bitmap; @end -/// Pigeon equivalent of [DefaultMarker]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#defaultMarker(float) +/// Pigeon equivalent of [DefaultMarker]. @interface FGMPlatformBitmapDefaultMarker : NSObject + (instancetype)makeWithHue:(nullable NSNumber *)hue; @property(nonatomic, strong, nullable) NSNumber *hue; @end -/// Pigeon equivalent of [BytesBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#fromBitmap(android.graphics.Bitmap) +/// Pigeon equivalent of [BytesBitmap]. @interface FGMPlatformBitmapBytes : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -569,8 +598,7 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @property(nonatomic, strong, nullable) FGMPlatformSize *size; @end -/// Pigeon equivalent of [AssetBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#public-static-bitmapdescriptor-fromasset-string-assetname +/// Pigeon equivalent of [AssetBitmap]. @interface FGMPlatformBitmapAsset : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -579,8 +607,7 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @property(nonatomic, copy, nullable) NSString *pkg; @end -/// Pigeon equivalent of [AssetImageBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#public-static-bitmapdescriptor-fromasset-string-assetname +/// Pigeon equivalent of [AssetImageBitmap]. @interface FGMPlatformBitmapAssetImage : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -592,8 +619,7 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @property(nonatomic, strong, nullable) FGMPlatformSize *size; @end -/// Pigeon equivalent of [AssetMapBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#public-static-bitmapdescriptor-fromasset-string-assetname +/// Pigeon equivalent of [AssetMapBitmap]. @interface FGMPlatformBitmapAssetMap : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -609,8 +635,7 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @property(nonatomic, strong, nullable) NSNumber *height; @end -/// Pigeon equivalent of [BytesMapBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#public-static-bitmapdescriptor-frombitmap-bitmap-image +/// Pigeon equivalent of [BytesMapBitmap]. @interface FGMPlatformBitmapBytesMap : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -675,6 +700,11 @@ NSObject *FGMGetMessagesCodec(void); changing:(NSArray *)toChange removing:(NSArray *)idsToRemove error:(FlutterError *_Nullable *_Nonnull)error; +/// Updates the set of ground overlays on the map. +- (void)updateGroundOverlaysByAdding:(NSArray *)toAdd + changing:(NSArray *)toChange + removing:(NSArray *)idsToRemove + error:(FlutterError *_Nullable *_Nonnull)error; /// Gets the screen coordinate for the given map location. /// /// @return `nil` only when `error != nil`. @@ -788,6 +818,9 @@ extern void SetUpFGMMapsApiWithSuffix(id binaryMessenger /// Called when a polyline is tapped. - (void)didTapPolylineWithIdentifier:(NSString *)polylineId completion:(void (^)(FlutterError *_Nullable))completion; +/// Called when a ground overlay is tapped. +- (void)didTapGroundOverlayWithIdentifier:(NSString *)groundOverlayId + completion:(void (^)(FlutterError *_Nullable))completion; /// Called to get data for a map tile. - (void)tileWithOverlayIdentifier:(NSString *)tileOverlayId location:(FGMPlatformPoint *)location @@ -832,6 +865,9 @@ extern void SetUpFGMMapsPlatformViewApiWithSuffix(id bin - (nullable FGMPlatformTileLayer *)tileOverlayWithIdentifier:(NSString *)tileOverlayId error: (FlutterError *_Nullable *_Nonnull)error; +- (nullable FGMPlatformGroundOverlay *) + groundOverlayWithIdentifier:(NSString *)groundOverlayId + error:(FlutterError *_Nullable *_Nonnull)error; - (nullable FGMPlatformHeatmap *)heatmapWithIdentifier:(NSString *)heatmapId error:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.m index 5d3474cf79cc..045a4459330b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.m @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.6.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.3), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "messages.g.h" @@ -233,6 +233,12 @@ + (nullable FGMPlatformCameraTargetBounds *)nullableFromList:(NSArray *)list - (NSArray *)toList; @end +@interface FGMPlatformGroundOverlay () ++ (FGMPlatformGroundOverlay *)fromList:(NSArray *)list; ++ (nullable FGMPlatformGroundOverlay *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @interface FGMPlatformMapViewCreationParams () + (FGMPlatformMapViewCreationParams *)fromList:(NSArray *)list; + (nullable FGMPlatformMapViewCreationParams *)nullableFromList:(NSArray *)list; @@ -1091,6 +1097,67 @@ + (nullable FGMPlatformCameraTargetBounds *)nullableFromList:(NSArray *)list } @end +@implementation FGMPlatformGroundOverlay ++ (instancetype)makeWithGroundOverlayId:(NSString *)groundOverlayId + image:(FGMPlatformBitmap *)image + position:(nullable FGMPlatformLatLng *)position + bounds:(nullable FGMPlatformLatLngBounds *)bounds + anchor:(nullable FGMPlatformPoint *)anchor + transparency:(double)transparency + bearing:(double)bearing + zIndex:(NSInteger)zIndex + visible:(BOOL)visible + clickable:(BOOL)clickable + zoomLevel:(nullable NSNumber *)zoomLevel { + FGMPlatformGroundOverlay *pigeonResult = [[FGMPlatformGroundOverlay alloc] init]; + pigeonResult.groundOverlayId = groundOverlayId; + pigeonResult.image = image; + pigeonResult.position = position; + pigeonResult.bounds = bounds; + pigeonResult.anchor = anchor; + pigeonResult.transparency = transparency; + pigeonResult.bearing = bearing; + pigeonResult.zIndex = zIndex; + pigeonResult.visible = visible; + pigeonResult.clickable = clickable; + pigeonResult.zoomLevel = zoomLevel; + return pigeonResult; +} ++ (FGMPlatformGroundOverlay *)fromList:(NSArray *)list { + FGMPlatformGroundOverlay *pigeonResult = [[FGMPlatformGroundOverlay alloc] init]; + pigeonResult.groundOverlayId = GetNullableObjectAtIndex(list, 0); + pigeonResult.image = GetNullableObjectAtIndex(list, 1); + pigeonResult.position = GetNullableObjectAtIndex(list, 2); + pigeonResult.bounds = GetNullableObjectAtIndex(list, 3); + pigeonResult.anchor = GetNullableObjectAtIndex(list, 4); + pigeonResult.transparency = [GetNullableObjectAtIndex(list, 5) doubleValue]; + pigeonResult.bearing = [GetNullableObjectAtIndex(list, 6) doubleValue]; + pigeonResult.zIndex = [GetNullableObjectAtIndex(list, 7) integerValue]; + pigeonResult.visible = [GetNullableObjectAtIndex(list, 8) boolValue]; + pigeonResult.clickable = [GetNullableObjectAtIndex(list, 9) boolValue]; + pigeonResult.zoomLevel = GetNullableObjectAtIndex(list, 10); + return pigeonResult; +} ++ (nullable FGMPlatformGroundOverlay *)nullableFromList:(NSArray *)list { + return (list) ? [FGMPlatformGroundOverlay fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + self.groundOverlayId ?: [NSNull null], + self.image ?: [NSNull null], + self.position ?: [NSNull null], + self.bounds ?: [NSNull null], + self.anchor ?: [NSNull null], + @(self.transparency), + @(self.bearing), + @(self.zIndex), + @(self.visible), + @(self.clickable), + self.zoomLevel ?: [NSNull null], + ]; +} +@end + @implementation FGMPlatformMapViewCreationParams + (instancetype) makeWithInitialCameraPosition:(FGMPlatformCameraPosition *)initialCameraPosition @@ -1101,7 +1168,8 @@ @implementation FGMPlatformMapViewCreationParams initialPolylines:(NSArray *)initialPolylines initialHeatmaps:(NSArray *)initialHeatmaps initialTileOverlays:(NSArray *)initialTileOverlays - initialClusterManagers:(NSArray *)initialClusterManagers { + initialClusterManagers:(NSArray *)initialClusterManagers + initialGroundOverlays:(NSArray *)initialGroundOverlays { FGMPlatformMapViewCreationParams *pigeonResult = [[FGMPlatformMapViewCreationParams alloc] init]; pigeonResult.initialCameraPosition = initialCameraPosition; pigeonResult.mapConfiguration = mapConfiguration; @@ -1112,6 +1180,7 @@ @implementation FGMPlatformMapViewCreationParams pigeonResult.initialHeatmaps = initialHeatmaps; pigeonResult.initialTileOverlays = initialTileOverlays; pigeonResult.initialClusterManagers = initialClusterManagers; + pigeonResult.initialGroundOverlays = initialGroundOverlays; return pigeonResult; } + (FGMPlatformMapViewCreationParams *)fromList:(NSArray *)list { @@ -1125,6 +1194,7 @@ + (FGMPlatformMapViewCreationParams *)fromList:(NSArray *)list { pigeonResult.initialHeatmaps = GetNullableObjectAtIndex(list, 6); pigeonResult.initialTileOverlays = GetNullableObjectAtIndex(list, 7); pigeonResult.initialClusterManagers = GetNullableObjectAtIndex(list, 8); + pigeonResult.initialGroundOverlays = GetNullableObjectAtIndex(list, 9); return pigeonResult; } + (nullable FGMPlatformMapViewCreationParams *)nullableFromList:(NSArray *)list { @@ -1141,6 +1211,7 @@ + (nullable FGMPlatformMapViewCreationParams *)nullableFromList:(NSArray *)l self.initialHeatmaps ?: [NSNull null], self.initialTileOverlays ?: [NSNull null], self.initialClusterManagers ?: [NSNull null], + self.initialGroundOverlays ?: [NSNull null], ]; } @end @@ -1613,30 +1684,32 @@ - (nullable id)readValueOfType:(UInt8)type { case 157: return [FGMPlatformCameraTargetBounds fromList:[self readValue]]; case 158: - return [FGMPlatformMapViewCreationParams fromList:[self readValue]]; + return [FGMPlatformGroundOverlay fromList:[self readValue]]; case 159: - return [FGMPlatformMapConfiguration fromList:[self readValue]]; + return [FGMPlatformMapViewCreationParams fromList:[self readValue]]; case 160: - return [FGMPlatformPoint fromList:[self readValue]]; + return [FGMPlatformMapConfiguration fromList:[self readValue]]; case 161: - return [FGMPlatformSize fromList:[self readValue]]; + return [FGMPlatformPoint fromList:[self readValue]]; case 162: - return [FGMPlatformTileLayer fromList:[self readValue]]; + return [FGMPlatformSize fromList:[self readValue]]; case 163: - return [FGMPlatformZoomRange fromList:[self readValue]]; + return [FGMPlatformTileLayer fromList:[self readValue]]; case 164: - return [FGMPlatformBitmap fromList:[self readValue]]; + return [FGMPlatformZoomRange fromList:[self readValue]]; case 165: - return [FGMPlatformBitmapDefaultMarker fromList:[self readValue]]; + return [FGMPlatformBitmap fromList:[self readValue]]; case 166: - return [FGMPlatformBitmapBytes fromList:[self readValue]]; + return [FGMPlatformBitmapDefaultMarker fromList:[self readValue]]; case 167: - return [FGMPlatformBitmapAsset fromList:[self readValue]]; + return [FGMPlatformBitmapBytes fromList:[self readValue]]; case 168: - return [FGMPlatformBitmapAssetImage fromList:[self readValue]]; + return [FGMPlatformBitmapAsset fromList:[self readValue]]; case 169: - return [FGMPlatformBitmapAssetMap fromList:[self readValue]]; + return [FGMPlatformBitmapAssetImage fromList:[self readValue]]; case 170: + return [FGMPlatformBitmapAssetMap fromList:[self readValue]]; + case 171: return [FGMPlatformBitmapBytesMap fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -1739,45 +1812,48 @@ - (void)writeValue:(id)value { } else if ([value isKindOfClass:[FGMPlatformCameraTargetBounds class]]) { [self writeByte:157]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FGMPlatformMapViewCreationParams class]]) { + } else if ([value isKindOfClass:[FGMPlatformGroundOverlay class]]) { [self writeByte:158]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FGMPlatformMapConfiguration class]]) { + } else if ([value isKindOfClass:[FGMPlatformMapViewCreationParams class]]) { [self writeByte:159]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FGMPlatformPoint class]]) { + } else if ([value isKindOfClass:[FGMPlatformMapConfiguration class]]) { [self writeByte:160]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FGMPlatformSize class]]) { + } else if ([value isKindOfClass:[FGMPlatformPoint class]]) { [self writeByte:161]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FGMPlatformTileLayer class]]) { + } else if ([value isKindOfClass:[FGMPlatformSize class]]) { [self writeByte:162]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FGMPlatformZoomRange class]]) { + } else if ([value isKindOfClass:[FGMPlatformTileLayer class]]) { [self writeByte:163]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FGMPlatformBitmap class]]) { + } else if ([value isKindOfClass:[FGMPlatformZoomRange class]]) { [self writeByte:164]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FGMPlatformBitmapDefaultMarker class]]) { + } else if ([value isKindOfClass:[FGMPlatformBitmap class]]) { [self writeByte:165]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FGMPlatformBitmapBytes class]]) { + } else if ([value isKindOfClass:[FGMPlatformBitmapDefaultMarker class]]) { [self writeByte:166]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FGMPlatformBitmapAsset class]]) { + } else if ([value isKindOfClass:[FGMPlatformBitmapBytes class]]) { [self writeByte:167]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FGMPlatformBitmapAssetImage class]]) { + } else if ([value isKindOfClass:[FGMPlatformBitmapAsset class]]) { [self writeByte:168]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FGMPlatformBitmapAssetMap class]]) { + } else if ([value isKindOfClass:[FGMPlatformBitmapAssetImage class]]) { [self writeByte:169]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FGMPlatformBitmapBytesMap class]]) { + } else if ([value isKindOfClass:[FGMPlatformBitmapAssetMap class]]) { [self writeByte:170]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FGMPlatformBitmapBytesMap class]]) { + [self writeByte:171]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -2074,6 +2150,37 @@ void SetUpFGMMapsApiWithSuffix(id binaryMessenger, [channel setMessageHandler:nil]; } } + /// Updates the set of ground overlays on the map. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.google_maps_flutter_ios." + @"MapsApi.updateGroundOverlays", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FGMGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(updateGroundOverlaysByAdding: + changing:removing:error:)], + @"FGMMapsApi api (%@) doesn't respond to " + @"@selector(updateGroundOverlaysByAdding:changing:removing:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSArray *arg_toAdd = GetNullableObjectAtIndex(args, 0); + NSArray *arg_toChange = GetNullableObjectAtIndex(args, 1); + NSArray *arg_idsToRemove = GetNullableObjectAtIndex(args, 2); + FlutterError *error; + [api updateGroundOverlaysByAdding:arg_toAdd + changing:arg_toChange + removing:arg_idsToRemove + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } /// Gets the screen coordinate for the given map location. { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] @@ -2771,6 +2878,31 @@ - (void)didTapPolylineWithIdentifier:(NSString *)arg_polylineId } }]; } +- (void)didTapGroundOverlayWithIdentifier:(NSString *)arg_groundOverlayId + completion:(void (^)(FlutterError *_Nullable))completion { + NSString *channelName = [NSString + stringWithFormat: + @"%@%@", @"dev.flutter.pigeon.google_maps_flutter_ios.MapsCallbackApi.onGroundOverlayTap", + _messageChannelSuffix]; + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel messageChannelWithName:channelName + binaryMessenger:self.binaryMessenger + codec:FGMGetMessagesCodec()]; + [channel sendMessage:@[ arg_groundOverlayId ?: [NSNull null] ] + reply:^(NSArray *reply) { + if (reply != nil) { + if (reply.count > 1) { + completion([FlutterError errorWithCode:reply[0] + message:reply[1] + details:reply[2]]); + } else { + completion(nil); + } + } else { + completion(createConnectionError(channelName)); + } + }]; +} - (void)tileWithOverlayIdentifier:(NSString *)arg_tileOverlayId location:(FGMPlatformPoint *)arg_location zoom:(NSInteger)arg_zoom @@ -3052,6 +3184,31 @@ void SetUpFGMMapsInspectorApiWithSuffix(id binaryMesseng [channel setMessageHandler:nil]; } } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.google_maps_flutter_ios." + @"MapsInspectorApi.getGroundOverlayInfo", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FGMGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(groundOverlayWithIdentifier:error:)], + @"FGMMapsInspectorApi api (%@) doesn't respond to " + @"@selector(groundOverlayWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSString *arg_groundOverlayId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + FGMPlatformGroundOverlay *output = [api groundOverlayWithIdentifier:arg_groundOverlayId + error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:[NSString stringWithFormat:@"%@%@", diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart index 823e210ce73b..148d1c73e7b5 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; @@ -103,6 +104,60 @@ class GoogleMapsInspectorIOS extends GoogleMapsInspectorPlatform { ); } + @override + bool supportsGettingGroundOverlayInfo() => true; + + @override + Future getGroundOverlayInfo(GroundOverlayId groundOverlayId, + {required int mapId}) async { + final PlatformGroundOverlay? groundOverlayInfo = + await _inspectorProvider(mapId)! + .getGroundOverlayInfo(groundOverlayId.value); + + if (groundOverlayInfo == null) { + return null; + } + + // Create dummy image to represent the image of the ground overlay. + final BytesMapBitmap dummyImage = BytesMapBitmap( + Uint8List.fromList([0]), + bitmapScaling: MapBitmapScaling.none, + ); + + if (groundOverlayInfo.position != null) { + return GroundOverlay.fromPosition( + groundOverlayId: groundOverlayId, + position: LatLng(groundOverlayInfo.position!.latitude, + groundOverlayInfo.position!.longitude), + image: dummyImage, + zIndex: groundOverlayInfo.zIndex, + bearing: groundOverlayInfo.bearing, + transparency: groundOverlayInfo.transparency, + visible: groundOverlayInfo.visible, + clickable: groundOverlayInfo.clickable, + anchor: + Offset(groundOverlayInfo.anchor!.x, groundOverlayInfo.anchor!.y), + zoomLevel: groundOverlayInfo.zoomLevel, + ); + } else if (groundOverlayInfo.bounds != null) { + return GroundOverlay.fromBounds( + groundOverlayId: groundOverlayId, + bounds: LatLngBounds( + southwest: LatLng(groundOverlayInfo.bounds!.southwest.latitude, + groundOverlayInfo.bounds!.southwest.longitude), + northeast: LatLng(groundOverlayInfo.bounds!.northeast.latitude, + groundOverlayInfo.bounds!.northeast.longitude)), + image: dummyImage, + zIndex: groundOverlayInfo.zIndex, + bearing: groundOverlayInfo.bearing, + transparency: groundOverlayInfo.transparency, + visible: groundOverlayInfo.visible, + clickable: groundOverlayInfo.clickable, + ); + } + return null; + } + @override Future isCompassEnabled({required int mapId}) async { return _inspectorProvider(mapId)!.isCompassEnabled(); diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart index 89c6b90ddc60..475c40bcbadb 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart @@ -190,6 +190,11 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { return _events(mapId).whereType(); } + @override + Stream onGroundOverlayTap({required int mapId}) { + return _events(mapId).whereType(); + } + @override Stream onTap({required int mapId}) { return _events(mapId).whereType(); @@ -334,6 +339,31 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { ); } + @override + Future updateGroundOverlays( + GroundOverlayUpdates groundOverlayUpdates, { + required int mapId, + }) { + assert( + groundOverlayUpdates.groundOverlaysToAdd.every( + (GroundOverlay groundOverlay) => + groundOverlay.position == null || + groundOverlay.zoomLevel != null), + 'On iOS zoom level must be set when position is set for ground overlays.'); + + return _hostApi(mapId).updateGroundOverlays( + groundOverlayUpdates.groundOverlaysToAdd + .map(_platformGroundOverlayFromGroundOverlay) + .toList(), + groundOverlayUpdates.groundOverlaysToChange + .map(_platformGroundOverlayFromGroundOverlay) + .toList(), + groundOverlayUpdates.groundOverlayIdsToRemove + .map((GroundOverlayId id) => id.value) + .toList(), + ); + } + @override Future clearTileCache( TileOverlayId tileOverlayId, { @@ -448,6 +478,11 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { required MapWidgetConfiguration widgetConfiguration, MapObjects mapObjects = const MapObjects(), }) { + assert( + mapObjects.groundOverlays.every((GroundOverlay groundOverlay) => + groundOverlay.position == null || groundOverlay.zoomLevel != null), + 'On iOS zoom level must be set when position is set for ground overlays.'); + final PlatformMapViewCreationParams creationParams = PlatformMapViewCreationParams( initialCameraPosition: _platformCameraPositionFromCameraPosition( @@ -469,6 +504,9 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { initialClusterManagers: mapObjects.clusterManagers .map(_platformClusterManagerFromClusterManager) .toList(), + initialGroundOverlays: mapObjects.groundOverlays + .map(_platformGroundOverlayFromGroundOverlay) + .toList(), ); return UiKitView( @@ -634,6 +672,27 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { ); } + static PlatformGroundOverlay _platformGroundOverlayFromGroundOverlay( + GroundOverlay groundOverlay) { + return PlatformGroundOverlay( + groundOverlayId: groundOverlay.groundOverlayId.value, + anchor: groundOverlay.anchor != null + ? _platformPointFromOffset(groundOverlay.anchor!) + : null, + image: platformBitmapFromBitmapDescriptor(groundOverlay.image), + position: groundOverlay.position != null + ? _platformLatLngFromLatLng(groundOverlay.position!) + : null, + bounds: _platformLatLngBoundsFromLatLngBounds(groundOverlay.bounds), + visible: groundOverlay.visible, + zIndex: groundOverlay.zIndex, + bearing: groundOverlay.bearing, + clickable: groundOverlay.clickable, + transparency: groundOverlay.transparency, + zoomLevel: groundOverlay.zoomLevel, + ); + } + static PlatformPolygon _platformPolygonFromPolygon(Polygon polygon) { final List points = polygon.points.map(_platformLatLngFromLatLng).toList(); @@ -942,6 +1001,12 @@ class HostMapMessageHandler implements MapsCallbackApi { streamController.add(PolylineTapEvent(mapId, PolylineId(polylineId))); } + @override + void onGroundOverlayTap(String groundOverlayId) { + streamController + .add(GroundOverlayTapEvent(mapId, GroundOverlayId(groundOverlayId))); + } + @override void onTap(PlatformLatLng position) { streamController diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/messages.g.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/messages.g.dart index 30a3e4e138df..ade49313866d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/messages.g.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.6.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.3), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -935,6 +935,78 @@ class PlatformCameraTargetBounds { } } +/// Pigeon equivalent of the GroundOverlay class. +class PlatformGroundOverlay { + PlatformGroundOverlay({ + required this.groundOverlayId, + required this.image, + this.position, + this.bounds, + this.anchor, + required this.transparency, + required this.bearing, + required this.zIndex, + required this.visible, + required this.clickable, + this.zoomLevel, + }); + + String groundOverlayId; + + PlatformBitmap image; + + PlatformLatLng? position; + + PlatformLatLngBounds? bounds; + + PlatformPoint? anchor; + + double transparency; + + double bearing; + + int zIndex; + + bool visible; + + bool clickable; + + double? zoomLevel; + + Object encode() { + return [ + groundOverlayId, + image, + position, + bounds, + anchor, + transparency, + bearing, + zIndex, + visible, + clickable, + zoomLevel, + ]; + } + + static PlatformGroundOverlay decode(Object result) { + result as List; + return PlatformGroundOverlay( + groundOverlayId: result[0]! as String, + image: result[1]! as PlatformBitmap, + position: result[2] as PlatformLatLng?, + bounds: result[3] as PlatformLatLngBounds?, + anchor: result[4] as PlatformPoint?, + transparency: result[5]! as double, + bearing: result[6]! as double, + zIndex: result[7]! as int, + visible: result[8]! as bool, + clickable: result[9]! as bool, + zoomLevel: result[10] as double?, + ); + } +} + /// Information passed to the platform view creation. class PlatformMapViewCreationParams { PlatformMapViewCreationParams({ @@ -947,6 +1019,7 @@ class PlatformMapViewCreationParams { required this.initialHeatmaps, required this.initialTileOverlays, required this.initialClusterManagers, + required this.initialGroundOverlays, }); PlatformCameraPosition initialCameraPosition; @@ -967,6 +1040,8 @@ class PlatformMapViewCreationParams { List initialClusterManagers; + List initialGroundOverlays; + Object encode() { return [ initialCameraPosition, @@ -978,6 +1053,7 @@ class PlatformMapViewCreationParams { initialHeatmaps, initialTileOverlays, initialClusterManagers, + initialGroundOverlays, ]; } @@ -995,6 +1071,8 @@ class PlatformMapViewCreationParams { (result[7] as List?)!.cast(), initialClusterManagers: (result[8] as List?)!.cast(), + initialGroundOverlays: + (result[9] as List?)!.cast(), ); } } @@ -1250,8 +1328,7 @@ class PlatformBitmap { } } -/// Pigeon equivalent of [DefaultMarker]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#defaultMarker(float) +/// Pigeon equivalent of [DefaultMarker]. class PlatformBitmapDefaultMarker { PlatformBitmapDefaultMarker({ this.hue, @@ -1273,8 +1350,7 @@ class PlatformBitmapDefaultMarker { } } -/// Pigeon equivalent of [BytesBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#fromBitmap(android.graphics.Bitmap) +/// Pigeon equivalent of [BytesBitmap]. class PlatformBitmapBytes { PlatformBitmapBytes({ required this.byteData, @@ -1301,8 +1377,7 @@ class PlatformBitmapBytes { } } -/// Pigeon equivalent of [AssetBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#public-static-bitmapdescriptor-fromasset-string-assetname +/// Pigeon equivalent of [AssetBitmap]. class PlatformBitmapAsset { PlatformBitmapAsset({ required this.name, @@ -1329,8 +1404,7 @@ class PlatformBitmapAsset { } } -/// Pigeon equivalent of [AssetImageBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#public-static-bitmapdescriptor-fromasset-string-assetname +/// Pigeon equivalent of [AssetImageBitmap]. class PlatformBitmapAssetImage { PlatformBitmapAssetImage({ required this.name, @@ -1362,8 +1436,7 @@ class PlatformBitmapAssetImage { } } -/// Pigeon equivalent of [AssetMapBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#public-static-bitmapdescriptor-fromasset-string-assetname +/// Pigeon equivalent of [AssetMapBitmap]. class PlatformBitmapAssetMap { PlatformBitmapAssetMap({ required this.assetName, @@ -1405,8 +1478,7 @@ class PlatformBitmapAssetMap { } } -/// Pigeon equivalent of [BytesMapBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#public-static-bitmapdescriptor-frombitmap-bitmap-image +/// Pigeon equivalent of [BytesMapBitmap]. class PlatformBitmapBytesMap { PlatformBitmapBytesMap({ required this.byteData, @@ -1542,45 +1614,48 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is PlatformCameraTargetBounds) { buffer.putUint8(157); writeValue(buffer, value.encode()); - } else if (value is PlatformMapViewCreationParams) { + } else if (value is PlatformGroundOverlay) { buffer.putUint8(158); writeValue(buffer, value.encode()); - } else if (value is PlatformMapConfiguration) { + } else if (value is PlatformMapViewCreationParams) { buffer.putUint8(159); writeValue(buffer, value.encode()); - } else if (value is PlatformPoint) { + } else if (value is PlatformMapConfiguration) { buffer.putUint8(160); writeValue(buffer, value.encode()); - } else if (value is PlatformSize) { + } else if (value is PlatformPoint) { buffer.putUint8(161); writeValue(buffer, value.encode()); - } else if (value is PlatformTileLayer) { + } else if (value is PlatformSize) { buffer.putUint8(162); writeValue(buffer, value.encode()); - } else if (value is PlatformZoomRange) { + } else if (value is PlatformTileLayer) { buffer.putUint8(163); writeValue(buffer, value.encode()); - } else if (value is PlatformBitmap) { + } else if (value is PlatformZoomRange) { buffer.putUint8(164); writeValue(buffer, value.encode()); - } else if (value is PlatformBitmapDefaultMarker) { + } else if (value is PlatformBitmap) { buffer.putUint8(165); writeValue(buffer, value.encode()); - } else if (value is PlatformBitmapBytes) { + } else if (value is PlatformBitmapDefaultMarker) { buffer.putUint8(166); writeValue(buffer, value.encode()); - } else if (value is PlatformBitmapAsset) { + } else if (value is PlatformBitmapBytes) { buffer.putUint8(167); writeValue(buffer, value.encode()); - } else if (value is PlatformBitmapAssetImage) { + } else if (value is PlatformBitmapAsset) { buffer.putUint8(168); writeValue(buffer, value.encode()); - } else if (value is PlatformBitmapAssetMap) { + } else if (value is PlatformBitmapAssetImage) { buffer.putUint8(169); writeValue(buffer, value.encode()); - } else if (value is PlatformBitmapBytesMap) { + } else if (value is PlatformBitmapAssetMap) { buffer.putUint8(170); writeValue(buffer, value.encode()); + } else if (value is PlatformBitmapBytesMap) { + buffer.putUint8(171); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1652,30 +1727,32 @@ class _PigeonCodec extends StandardMessageCodec { case 157: return PlatformCameraTargetBounds.decode(readValue(buffer)!); case 158: - return PlatformMapViewCreationParams.decode(readValue(buffer)!); + return PlatformGroundOverlay.decode(readValue(buffer)!); case 159: - return PlatformMapConfiguration.decode(readValue(buffer)!); + return PlatformMapViewCreationParams.decode(readValue(buffer)!); case 160: - return PlatformPoint.decode(readValue(buffer)!); + return PlatformMapConfiguration.decode(readValue(buffer)!); case 161: - return PlatformSize.decode(readValue(buffer)!); + return PlatformPoint.decode(readValue(buffer)!); case 162: - return PlatformTileLayer.decode(readValue(buffer)!); + return PlatformSize.decode(readValue(buffer)!); case 163: - return PlatformZoomRange.decode(readValue(buffer)!); + return PlatformTileLayer.decode(readValue(buffer)!); case 164: - return PlatformBitmap.decode(readValue(buffer)!); + return PlatformZoomRange.decode(readValue(buffer)!); case 165: - return PlatformBitmapDefaultMarker.decode(readValue(buffer)!); + return PlatformBitmap.decode(readValue(buffer)!); case 166: - return PlatformBitmapBytes.decode(readValue(buffer)!); + return PlatformBitmapDefaultMarker.decode(readValue(buffer)!); case 167: - return PlatformBitmapAsset.decode(readValue(buffer)!); + return PlatformBitmapBytes.decode(readValue(buffer)!); case 168: - return PlatformBitmapAssetImage.decode(readValue(buffer)!); + return PlatformBitmapAsset.decode(readValue(buffer)!); case 169: - return PlatformBitmapAssetMap.decode(readValue(buffer)!); + return PlatformBitmapAssetImage.decode(readValue(buffer)!); case 170: + return PlatformBitmapAssetMap.decode(readValue(buffer)!); + case 171: return PlatformBitmapBytesMap.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -1936,6 +2013,32 @@ class MapsApi { } } + /// Updates the set of ground overlays on the map. + Future updateGroundOverlays(List toAdd, + List toChange, List idsToRemove) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_maps_flutter_ios.MapsApi.updateGroundOverlays$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = await pigeonVar_channel + .send([toAdd, toChange, idsToRemove]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + /// Gets the screen coordinate for the given map location. Future getScreenCoordinate(PlatformLatLng latLng) async { final String pigeonVar_channelName = @@ -2343,6 +2446,9 @@ abstract class MapsCallbackApi { /// Called when a polyline is tapped. void onPolylineTap(String polylineId); + /// Called when a ground overlay is tapped. + void onGroundOverlayTap(String groundOverlayId); + /// Called to get data for a map tile. Future getTileOverlayTile( String tileOverlayId, PlatformPoint location, int zoom); @@ -2758,6 +2864,35 @@ abstract class MapsCallbackApi { }); } } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.google_maps_flutter_ios.MapsCallbackApi.onGroundOverlayTap$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.google_maps_flutter_ios.MapsCallbackApi.onGroundOverlayTap was null.'); + final List args = (message as List?)!; + final String? arg_groundOverlayId = (args[0] as String?); + assert(arg_groundOverlayId != null, + 'Argument for dev.flutter.pigeon.google_maps_flutter_ios.MapsCallbackApi.onGroundOverlayTap was null, expected non-null String.'); + try { + api.onGroundOverlayTap(arg_groundOverlayId!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } { final BasicMessageChannel< Object?> pigeonVar_channel = BasicMessageChannel< @@ -3112,6 +3247,31 @@ class MapsInspectorApi { } } + Future getGroundOverlayInfo( + String groundOverlayId) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_maps_flutter_ios.MapsInspectorApi.getGroundOverlayInfo$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = await pigeonVar_channel + .send([groundOverlayId]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return (pigeonVar_replyList[0] as PlatformGroundOverlay?); + } + } + Future getHeatmapInfo(String heatmapId) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.google_maps_flutter_ios.MapsInspectorApi.getHeatmapInfo$pigeonVar_messageChannelSuffix'; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/pigeons/messages.dart b/packages/google_maps_flutter/google_maps_flutter_ios/pigeons/messages.dart index 342bd9ea8019..2509fdee7f2f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/pigeons/messages.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/pigeons/messages.dart @@ -357,6 +357,35 @@ class PlatformCameraTargetBounds { final PlatformLatLngBounds? bounds; } +/// Pigeon equivalent of the GroundOverlay class. +class PlatformGroundOverlay { + PlatformGroundOverlay({ + required this.groundOverlayId, + required this.image, + required this.position, + required this.bounds, + required this.anchor, + required this.transparency, + required this.bearing, + required this.zIndex, + required this.visible, + required this.clickable, + required this.zoomLevel, + }); + + final String groundOverlayId; + final PlatformBitmap image; + final PlatformLatLng? position; + final PlatformLatLngBounds? bounds; + final PlatformPoint? anchor; + final double transparency; + final double bearing; + final int zIndex; + final bool visible; + final bool clickable; + final double? zoomLevel; +} + /// Information passed to the platform view creation. class PlatformMapViewCreationParams { PlatformMapViewCreationParams({ @@ -369,6 +398,7 @@ class PlatformMapViewCreationParams { required this.initialHeatmaps, required this.initialTileOverlays, required this.initialClusterManagers, + required this.initialGroundOverlays, }); final PlatformCameraPosition initialCameraPosition; @@ -380,6 +410,7 @@ class PlatformMapViewCreationParams { final List initialHeatmaps; final List initialTileOverlays; final List initialClusterManagers; + final List initialGroundOverlays; } /// Pigeon equivalent of MapConfiguration. @@ -596,6 +627,11 @@ abstract class MapsApi { void updateTileOverlays(List toAdd, List toChange, List idsToRemove); + /// Updates the set of ground overlays on the map. + @ObjCSelector('updateGroundOverlaysByAdding:changing:removing:') + void updateGroundOverlays(List toAdd, + List toChange, List idsToRemove); + /// Gets the screen coordinate for the given map location. @ObjCSelector('screenCoordinatesForLatLng:') PlatformPoint getScreenCoordinate(PlatformLatLng latLng); @@ -717,6 +753,10 @@ abstract class MapsCallbackApi { @ObjCSelector('didTapPolylineWithIdentifier:') void onPolylineTap(String polylineId); + /// Called when a ground overlay is tapped. + @ObjCSelector('didTapGroundOverlayWithIdentifier:') + void onGroundOverlayTap(String groundOverlayId); + /// Called to get data for a map tile. @async @ObjCSelector('tileWithOverlayIdentifier:location:zoom:') @@ -746,6 +786,8 @@ abstract class MapsInspectorApi { bool isTrafficEnabled(); @ObjCSelector('tileOverlayWithIdentifier:') PlatformTileLayer? getTileOverlayInfo(String tileOverlayId); + @ObjCSelector('groundOverlayWithIdentifier:') + PlatformGroundOverlay? getGroundOverlayInfo(String groundOverlayId); @ObjCSelector('heatmapWithIdentifier:') PlatformHeatmap? getHeatmapInfo(String heatmapId); @ObjCSelector('zoomRange') diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml index 2020ec8940ef..6a0a0ebfed51 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_ios description: iOS implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.13.2 +version: 2.14.0 environment: sdk: ^3.4.0 @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - google_maps_flutter_platform_interface: ^2.9.5 + google_maps_flutter_platform_interface: ^2.10.0 stream_transform: ^2.0.0 dev_dependencies: diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart index 51a5e04cd975..99ee3577962e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart @@ -612,6 +612,150 @@ void main() { expectTileOverlay(toAdd.first, object3); }); + test('updateGroundOverlays passes expected arguments', () async { + const int mapId = 1; + final (GoogleMapsFlutterIOS maps, MockMapsApi api) = + setUpMockMap(mapId: mapId); + + final AssetMapBitmap image = AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ); + + final GroundOverlay object1 = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('1'), + bounds: LatLngBounds( + southwest: const LatLng(10, 20), northeast: const LatLng(30, 40)), + image: image, + ); + final GroundOverlay object2old = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('2'), + bounds: LatLngBounds( + southwest: const LatLng(10, 20), northeast: const LatLng(30, 40)), + image: image, + ); + final GroundOverlay object2new = object2old.copyWith( + visibleParam: false, + bearingParam: 10, + clickableParam: false, + transparencyParam: 0.5, + zIndexParam: 100, + ); + final GroundOverlay object3 = GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('3'), + position: const LatLng(10, 20), + width: 100, + image: image, + zoomLevel: 14.0, + ); + await maps.updateGroundOverlays( + GroundOverlayUpdates.from({object1, object2old}, + {object2new, object3}), + mapId: mapId); + + final VerificationResult verification = + verify(api.updateGroundOverlays(captureAny, captureAny, captureAny)); + + final List toAdd = + verification.captured[0] as List; + final List toChange = + verification.captured[1] as List; + final List toRemove = verification.captured[2] as List; + // Object one should be removed. + expect(toRemove.length, 1); + expect(toRemove.first, object1.groundOverlayId.value); + // Object two should be changed. + { + expect(toChange.length, 1); + final PlatformGroundOverlay firstChanged = toChange.first; + expect(firstChanged.anchor?.x, object2new.anchor?.dx); + expect(firstChanged.anchor?.y, object2new.anchor?.dy); + expect(firstChanged.bearing, object2new.bearing); + expect(firstChanged.bounds?.northeast.latitude, + object2new.bounds?.northeast.latitude); + expect(firstChanged.bounds?.northeast.longitude, + object2new.bounds?.northeast.longitude); + expect(firstChanged.bounds?.southwest.latitude, + object2new.bounds?.southwest.latitude); + expect(firstChanged.bounds?.southwest.longitude, + object2new.bounds?.southwest.longitude); + expect(firstChanged.visible, object2new.visible); + expect(firstChanged.clickable, object2new.clickable); + expect(firstChanged.zIndex, object2new.zIndex); + expect(firstChanged.position?.latitude, object2new.position?.latitude); + expect(firstChanged.position?.longitude, object2new.position?.longitude); + expect(firstChanged.zoomLevel, object2new.zoomLevel); + expect(firstChanged.transparency, object2new.transparency); + expect( + firstChanged.image.bitmap.runtimeType, + GoogleMapsFlutterIOS.platformBitmapFromBitmapDescriptor( + object2new.image) + .bitmap + .runtimeType); + } + // Object three should be added. + { + expect(toAdd.length, 1); + final PlatformGroundOverlay firstAdded = toAdd.first; + expect(firstAdded.anchor?.x, object3.anchor?.dx); + expect(firstAdded.anchor?.y, object3.anchor?.dy); + expect(firstAdded.bearing, object3.bearing); + expect(firstAdded.bounds?.northeast.latitude, + object3.bounds?.northeast.latitude); + expect(firstAdded.bounds?.northeast.longitude, + object3.bounds?.northeast.longitude); + expect(firstAdded.bounds?.southwest.latitude, + object3.bounds?.southwest.latitude); + expect(firstAdded.bounds?.southwest.longitude, + object3.bounds?.southwest.longitude); + expect(firstAdded.visible, object3.visible); + expect(firstAdded.clickable, object3.clickable); + expect(firstAdded.zIndex, object3.zIndex); + expect(firstAdded.position?.latitude, object3.position?.latitude); + expect(firstAdded.position?.longitude, object3.position?.longitude); + expect(firstAdded.zoomLevel, object3.zoomLevel); + expect(firstAdded.transparency, object3.transparency); + expect( + firstAdded.image.bitmap.runtimeType, + GoogleMapsFlutterIOS.platformBitmapFromBitmapDescriptor(object3.image) + .bitmap + .runtimeType); + } + }); + + test( + 'updateGroundOverlays throws assertion error on unsupported ground overlays', + () async { + const int mapId = 1; + final (GoogleMapsFlutterIOS maps, MockMapsApi api) = + setUpMockMap(mapId: mapId); + + final AssetMapBitmap image = AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ); + + final GroundOverlay object3 = GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('1'), + position: const LatLng(10, 20), + // Assert should be thrown because zoomLevel is not set for position-based + // ground overlay on iOS. + // ignore: avoid_redundant_argument_values + zoomLevel: null, + image: image, + ); + + expect( + () async => maps.updateGroundOverlays( + GroundOverlayUpdates.from( + const {}, {object3}), + mapId: mapId), + throwsAssertionError, + ); + }); + test('markers send drag event to correct streams', () async { const int mapId = 1; const String dragStartId = 'drag-start-marker'; @@ -748,6 +892,24 @@ void main() { expect((await stream.next).value.value, equals(objectId)); }); + test('ground overlays send tap events to correct stream', () async { + const int mapId = 1; + const String objectId = 'object-id'; + + final GoogleMapsFlutterIOS maps = GoogleMapsFlutterIOS(); + final HostMapMessageHandler callbackHandler = + maps.ensureHandlerInitialized(mapId); + + final StreamQueue stream = + StreamQueue( + maps.onGroundOverlayTap(mapId: mapId)); + + // Simulate message from the native side. + callbackHandler.onGroundOverlayTap(objectId); + + expect((await stream.next).value.value, equals(objectId)); + }); + test('moveCamera calls through with expected newCameraPosition', () async { const int mapId = 1; final (GoogleMapsFlutterIOS maps, MockMapsApi api) = diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.mocks.dart index e94375876150..abff2a3e6d92 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.5 from annotations // in google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart. // Do not manually edit this file. @@ -18,6 +18,7 @@ import 'package:mockito/src/dummies.dart' as _i3; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types @@ -225,6 +226,25 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + @override + _i4.Future updateGroundOverlays( + List<_i2.PlatformGroundOverlay>? toAdd, + List<_i2.PlatformGroundOverlay>? toChange, + List? idsToRemove, + ) => + (super.noSuchMethod( + Invocation.method( + #updateGroundOverlays, + [ + toAdd, + toChange, + idsToRemove, + ], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override _i4.Future<_i2.PlatformPoint> getScreenCoordinate( _i2.PlatformLatLng? latLng) => From 12792be3602618f2190486f6e35de3ad4e06fd4c Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Tue, 4 Feb 2025 19:29:08 +0200 Subject: [PATCH 02/10] [google_maps_flutter_android] Adds support for ground overlay --- .../google_maps_flutter_android/CHANGELOG.md | 4 + .../flutter/plugins/googlemaps/Convert.java | 137 +++++ .../plugins/googlemaps/GoogleMapBuilder.java | 8 + .../googlemaps/GoogleMapController.java | 55 +- .../plugins/googlemaps/GoogleMapFactory.java | 1 + .../plugins/googlemaps/GoogleMapListener.java | 3 +- .../googlemaps/GoogleMapOptionsSink.java | 3 + .../googlemaps/GroundOverlayBuilder.java | 74 +++ .../googlemaps/GroundOverlayController.java | 88 +++ .../plugins/googlemaps/GroundOverlaySink.java | 32 ++ .../googlemaps/GroundOverlaysController.java | 139 +++++ .../flutter/plugins/googlemaps/Messages.java | 529 +++++++++++++++++- .../googlemaps/GoogleMapControllerTest.java | 4 +- .../GroundOverlaysControllerTest.java | 150 +++++ .../integration_test/google_maps_tests.dart | 259 ++++++++- .../example/lib/example_google_map.dart | 30 + .../example/lib/ground_overlay.dart | 325 +++++++++++ .../example/lib/main.dart | 2 + .../example/pubspec.yaml | 2 +- .../fake_google_maps_flutter_platform.dart | 18 + .../lib/src/google_map_inspector_android.dart | 57 ++ .../lib/src/google_maps_flutter_android.dart | 65 +++ .../lib/src/messages.g.dart | 223 +++++++- .../pigeons/messages.dart | 41 ++ .../google_maps_flutter_android/pubspec.yaml | 4 +- .../google_maps_flutter_android_test.dart | 173 ++++++ ...oogle_maps_flutter_android_test.mocks.dart | 253 +++++---- 27 files changed, 2506 insertions(+), 173 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlayBuilder.java create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlayController.java create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlaySink.java create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlaysController.java create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GroundOverlaysControllerTest.java create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/example/lib/ground_overlay.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md index e18bd40228cf..7b569b801a37 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.15.0 + +* Adds support for ground overlay. + ## 2.14.12 * Updates androidx.annotation:annotation to 1.9.1. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index 5701825f12d9..29355f506885 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java @@ -29,6 +29,7 @@ import com.google.android.gms.maps.model.Dash; import com.google.android.gms.maps.model.Dot; import com.google.android.gms.maps.model.Gap; +import com.google.android.gms.maps.model.GroundOverlay; import com.google.android.gms.maps.model.JointType; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; @@ -849,6 +850,142 @@ static Tile tileFromPigeon(Messages.PlatformTile tile) { return new Tile(tile.getWidth().intValue(), tile.getHeight().intValue(), tile.getData()); } + /** + * Set the options in the given ground overlay object to the given sink. + * + * @param groundOverlay the object expected to be a PlatformGroundOverlay containing the ground + * overlay options. + * @param sink the GroundOverlaySink where the options will be set. + * @param assetManager An instance of Android's AssetManager, which provides access to any raw + * asset files stored in the application's assets directory. + * @param density the density of the display, used to calculate pixel dimensions. + * @param wrapper the BitmapDescriptorFactoryWrapper to create BitmapDescriptor. + * @return the identifier of the ground overlay. + * @throws IllegalArgumentException if required fields are missing or invalid. + */ + static String interpretGroundOverlayOptions( + Messages.PlatformGroundOverlay groundOverlay, + GroundOverlaySink sink, + AssetManager assetManager, + float density, + BitmapDescriptorFactoryWrapper wrapper) { + sink.setTransparency(groundOverlay.getTransparency().floatValue()); + sink.setZIndex(groundOverlay.getZIndex().floatValue()); + sink.setVisible(groundOverlay.getVisible()); + if (groundOverlay.getAnchor() != null) { + sink.setAnchor( + groundOverlay.getAnchor().getX().floatValue(), + groundOverlay.getAnchor().getY().floatValue()); + } + sink.setBearing(groundOverlay.getBearing().floatValue()); + sink.setClickable(groundOverlay.getClickable()); + sink.setImage(toBitmapDescriptor(groundOverlay.getImage(), assetManager, density, wrapper)); + if (groundOverlay.getPosition() != null) { + assert groundOverlay.getWidth() != null; + if (groundOverlay.getHeight() != null) { + sink.setPosition( + latLngFromPigeon(groundOverlay.getPosition()), + groundOverlay.getWidth().floatValue(), + groundOverlay.getHeight().floatValue()); + } else { + sink.setPosition( + latLngFromPigeon(groundOverlay.getPosition()), + groundOverlay.getWidth().floatValue(), + null); + } + } else if (groundOverlay.getBounds() != null) { + sink.setPositionFromBounds(latLngBoundsFromPigeon(groundOverlay.getBounds())); + } + return groundOverlay.getGroundOverlayId(); + } + + /** + * Converts a GroundOverlay object to a PlatformGroundOverlay Pigeon object. + * + * @param groundOverlay the GroundOverlay object to convert. + * @param groundOverlayId the identifier of the GroundOverlay. + * @param isCreatedWithBounds indicates if the GroundOverlay was created with bounds. + * @return the converted PlatformGroundOverlay object. + */ + static @NonNull Messages.PlatformGroundOverlay groundOverlayToPigeon( + @NonNull GroundOverlay groundOverlay, + @NonNull String groundOverlayId, + boolean isCreatedWithBounds) { + + // Dummy image is used as image is required field of PlatformGroundOverlay and converting image + // back to image descriptor is not currently supported. + Messages.PlatformBitmap dummyImage = + new Messages.PlatformBitmap.Builder() + .setBitmap( + new Messages.PlatformBitmapBytesMap.Builder() + .setByteData(new byte[] {0}) + .setImagePixelRatio(1.0) + .setBitmapScaling(Messages.PlatformMapBitmapScaling.NONE) + .build()) + .build(); + + Messages.PlatformGroundOverlay.Builder builder = + new Messages.PlatformGroundOverlay.Builder() + .setGroundOverlayId(groundOverlayId) + .setImage(dummyImage) + .setWidth((double) groundOverlay.getWidth()) + .setHeight((double) groundOverlay.getWidth()) + .setBearing((double) groundOverlay.getBearing()) + .setTransparency((double) groundOverlay.getTransparency()) + .setZIndex((long) groundOverlay.getZIndex()) + .setVisible(groundOverlay.isVisible()) + .setClickable(groundOverlay.isClickable()); + + if (isCreatedWithBounds) { + builder.setBounds(Convert.latLngBoundsToPigeon(groundOverlay.getBounds())); + } else { + builder.setPosition(Convert.latLngToPigeon(groundOverlay.getPosition())); + } + + builder.setAnchor(Convert.buildGroundOverlayAnchorForPigeon(groundOverlay)); + return builder.build(); + } + + /** + * Builds a PlatformDoublePair representing the anchor point for a GroundOverlay. + * + * @param groundOverlay the GroundOverlay object. + * @return the PlatformDoublePair representing the anchor point. + */ + @VisibleForTesting + private static @NonNull Messages.PlatformDoublePair buildGroundOverlayAnchorForPigeon( + GroundOverlay groundOverlay) { + Messages.PlatformDoublePair.Builder anchorBuilder = new Messages.PlatformDoublePair.Builder(); + + // Position is overlays anchor point. Calculate normalized anchor point based on position and bounds. + LatLng position = groundOverlay.getPosition(); + LatLngBounds bounds = groundOverlay.getBounds(); + + // Calculate normalized latitude. + double height = bounds.northeast.latitude - bounds.southwest.latitude; + double normalizedLatitude = 1.0 - ((position.latitude - bounds.southwest.latitude) / height); + + // Calculate normalized longitude. + double west = bounds.southwest.longitude; + double east = bounds.northeast.longitude; + double longitudeOffset = 0; + if (west <= east) { + longitudeOffset = position.longitude - west; + } else { + longitudeOffset = position.longitude - west; + if (position.longitude < west) { + // If bounds cross the antimeridian add 360 to the offset. + longitudeOffset += 360; + } + } + double width = (west <= east) ? east - west : 360.0 - (west - east); + double normalizedLongitude = longitudeOffset / width; + + anchorBuilder.setX(normalizedLongitude); + anchorBuilder.setY(normalizedLatitude); + return anchorBuilder.build(); + } + static class BitmapDescriptorFactoryWrapper { /** * Creates a BitmapDescriptor from the provided asset key using the {@link diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java index c7abcb2ff942..75897778f8dc 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java @@ -29,6 +29,7 @@ class GoogleMapBuilder implements GoogleMapOptionsSink { private List initialCircles; private List initialHeatmaps; private List initialTileOverlays; + private List initialGroundOverlays; private Rect padding = new Rect(0, 0, 0, 0); private @Nullable String style; @@ -54,6 +55,7 @@ GoogleMapController build( controller.setInitialHeatmaps(initialHeatmaps); controller.setPadding(padding.top, padding.left, padding.bottom, padding.right); controller.setInitialTileOverlays(initialTileOverlays); + controller.setInitialGroundOverlays(initialGroundOverlays); controller.setMapStyle(style); return controller; } @@ -197,6 +199,12 @@ public void setInitialTileOverlays( this.initialTileOverlays = initialTileOverlays; } + @Override + public void setInitialGroundOverlays( + @NonNull List initialGroundOverlays) { + this.initialGroundOverlays = initialGroundOverlays; + } + @Override public void setMapStyle(@Nullable String style) { this.style = style; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index 140938ea047c..dc3fe1d5ad59 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -31,6 +31,7 @@ import com.google.android.gms.maps.MapView; import com.google.android.gms.maps.OnMapReadyCallback; import com.google.android.gms.maps.model.Circle; +import com.google.android.gms.maps.model.GroundOverlay; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.gms.maps.model.MapStyleOptions; @@ -93,6 +94,7 @@ class GoogleMapController private final CirclesController circlesController; private final HeatmapsController heatmapsController; private final TileOverlaysController tileOverlaysController; + private final GroundOverlaysController groundOverlaysController; private MarkerManager markerManager; private MarkerManager.Collection markerCollection; private @Nullable List initialMarkers; @@ -102,6 +104,7 @@ class GoogleMapController private @Nullable List initialCircles; private @Nullable List initialHeatmaps; private @Nullable List initialTileOverlays; + private @Nullable List initialGroundOverlays; // Null except between initialization and onMapReady. private @Nullable String initialMapStyle; private boolean lastSetStyleSucceeded; @@ -137,6 +140,7 @@ class GoogleMapController this.circlesController = new CirclesController(flutterApi, density); this.heatmapsController = new HeatmapsController(); this.tileOverlaysController = new TileOverlaysController(flutterApi); + this.groundOverlaysController = new GroundOverlaysController(flutterApi, assetManager, density); } // Constructor for testing purposes only @@ -154,7 +158,8 @@ class GoogleMapController PolylinesController polylinesController, CirclesController circlesController, HeatmapsController heatmapController, - TileOverlaysController tileOverlaysController) { + TileOverlaysController tileOverlaysController, + GroundOverlaysController groundOverlaysController) { this.id = id; this.context = context; this.binaryMessenger = binaryMessenger; @@ -170,6 +175,7 @@ class GoogleMapController this.circlesController = circlesController; this.heatmapsController = heatmapController; this.tileOverlaysController = tileOverlaysController; + this.groundOverlaysController = groundOverlaysController; } @Override @@ -209,6 +215,7 @@ public void onMapReady(@NonNull GoogleMap googleMap) { circlesController.setGoogleMap(googleMap); heatmapsController.setGoogleMap(googleMap); tileOverlaysController.setGoogleMap(googleMap); + groundOverlaysController.setGoogleMap(googleMap); setMarkerCollectionListener(this); setClusterItemClickListener(this); setClusterItemRenderedListener(this); @@ -219,6 +226,7 @@ public void onMapReady(@NonNull GoogleMap googleMap) { updateInitialCircles(); updateInitialHeatmaps(); updateInitialTileOverlays(); + updateInitialGroundOverlays(); if (initialPadding != null && initialPadding.size() == 4) { setPadding( initialPadding.get(0), @@ -369,6 +377,11 @@ public void onCircleClick(Circle circle) { circlesController.onCircleTap(circle.getId()); } + @Override + public void onGroundOverlayClick(@NonNull GroundOverlay groundOverlay) { + groundOverlaysController.onGroundOverlayTap(groundOverlay.getId()); + } + @Override public void dispose() { if (disposed) { @@ -401,6 +414,7 @@ private void setGoogleMapListener(@Nullable GoogleMapListener listener) { googleMap.setOnCircleClickListener(listener); googleMap.setOnMapClickListener(listener); googleMap.setOnMapLongClickListener(listener); + googleMap.setOnGroundOverlayClickListener(listener); } @VisibleForTesting @@ -727,6 +741,21 @@ private void updateInitialTileOverlays() { } } + @Override + public void setInitialGroundOverlays( + @NonNull List initialGroundOverlays) { + this.initialGroundOverlays = initialGroundOverlays; + if (googleMap != null) { + updateInitialGroundOverlays(); + } + } + + private void updateInitialGroundOverlays() { + if (initialGroundOverlays != null) { + groundOverlaysController.addGroundOverlays(initialGroundOverlays); + } + } + @SuppressLint("MissingPermission") private void updateMyLocationSettings() { if (hasLocationPermission()) { @@ -891,6 +920,16 @@ public void updateTileOverlays( tileOverlaysController.removeTileOverlays(idsToRemove); } + @Override + public void updateGroundOverlays( + @NonNull List toAdd, + @NonNull List toChange, + @NonNull List idsToRemove) { + groundOverlaysController.addGroundOverlays(toAdd); + groundOverlaysController.changeGroundOverlays(toChange); + groundOverlaysController.removeGroundOverlays(idsToRemove); + } + @Override public @NonNull Messages.PlatformPoint getScreenCoordinate( @NonNull Messages.PlatformLatLng latLng) { @@ -1075,6 +1114,20 @@ public Boolean isLiteModeEnabled() { .build(); } + @Override + public @Nullable Messages.PlatformGroundOverlay getGroundOverlayInfo( + @NonNull String groundOverlayId) { + GroundOverlay groundOverlay = groundOverlaysController.getGroundOverlay(groundOverlayId); + if (groundOverlay == null) { + return null; + } + + return Convert.groundOverlayToPigeon( + groundOverlay, + groundOverlayId, + groundOverlaysController.isCreatedWithBounds(groundOverlayId)); + } + @Override public @NonNull Messages.PlatformZoomRange getZoomRange() { return new Messages.PlatformZoomRange.Builder() diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java index c1a3496e7f47..c4208f003c66 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java @@ -46,6 +46,7 @@ public PlatformView create(@NonNull Context context, int id, @Nullable Object ar builder.setInitialCircles(params.getInitialCircles()); builder.setInitialHeatmaps(params.getInitialHeatmaps()); builder.setInitialTileOverlays(params.getInitialTileOverlays()); + builder.setInitialGroundOverlays(params.getInitialGroundOverlays()); final String cloudMapId = mapConfig.getCloudMapId(); if (cloudMapId != null) { diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapListener.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapListener.java index 0a5c3ec67e27..6de0dbc94769 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapListener.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapListener.java @@ -17,4 +17,5 @@ interface GoogleMapListener GoogleMap.OnCircleClickListener, GoogleMap.OnMapClickListener, GoogleMap.OnMapLongClickListener, - GoogleMap.OnMarkerDragListener {} + GoogleMap.OnMarkerDragListener, + GoogleMap.OnGroundOverlayClickListener {} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java index 457508e83c5e..fc8b6463162b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java @@ -62,5 +62,8 @@ void setInitialClusterManagers( void setInitialTileOverlays(@NonNull List initialTileOverlays); + void setInitialGroundOverlays( + @NonNull List initialGroundOverlays); + void setMapStyle(@Nullable String style); } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlayBuilder.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlayBuilder.java new file mode 100644 index 000000000000..f5ff95bc10a8 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlayBuilder.java @@ -0,0 +1,74 @@ +// 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. + +package io.flutter.plugins.googlemaps; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.maps.model.BitmapDescriptor; +import com.google.android.gms.maps.model.GroundOverlayOptions; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.LatLngBounds; + +class GroundOverlayBuilder implements GroundOverlaySink { + + private final GroundOverlayOptions groundOverlayOptions; + + GroundOverlayBuilder() { + this.groundOverlayOptions = new GroundOverlayOptions(); + } + + GroundOverlayOptions build() { + return groundOverlayOptions; + } + + @Override + public void setTransparency(float transparency) { + groundOverlayOptions.transparency(transparency); + } + + @Override + public void setZIndex(float zIndex) { + groundOverlayOptions.zIndex(zIndex); + } + + @Override + public void setVisible(boolean visible) { + groundOverlayOptions.visible(visible); + } + + @Override + public void setAnchor(float u, float v) { + groundOverlayOptions.anchor(u, v); + } + + @Override + public void setBearing(float bearing) { + groundOverlayOptions.bearing(bearing); + } + + @Override + public void setClickable(boolean clickable) { + groundOverlayOptions.clickable(clickable); + } + + @Override + public void setPosition(@NonNull LatLng location, @NonNull Float width, @Nullable Float height) { + if (height != null) { + groundOverlayOptions.position(location, width, height); + } else { + groundOverlayOptions.position(location, width); + } + } + + @Override + public void setPositionFromBounds(@NonNull LatLngBounds bounds) { + groundOverlayOptions.positionFromBounds(bounds); + } + + @Override + public void setImage(@NonNull BitmapDescriptor image) { + groundOverlayOptions.image(image); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlayController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlayController.java new file mode 100644 index 000000000000..608071f0cd4b --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlayController.java @@ -0,0 +1,88 @@ +// 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. + +package io.flutter.plugins.googlemaps; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.maps.model.BitmapDescriptor; +import com.google.android.gms.maps.model.GroundOverlay; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.LatLngBounds; + +class GroundOverlayController implements GroundOverlaySink { + private final GroundOverlay groundOverlay; + private final String googleMapsGroundOverlayId; + private final boolean isCreatedWithBounds; + + GroundOverlayController(@NonNull GroundOverlay groundOverlay, boolean isCreatedWithBounds) { + this.groundOverlay = groundOverlay; + this.googleMapsGroundOverlayId = groundOverlay.getId(); + this.isCreatedWithBounds = isCreatedWithBounds; + } + + void remove() { + groundOverlay.remove(); + } + + GroundOverlay getGroundOverlay() { + return groundOverlay; + } + + @Override + public void setTransparency(float transparency) { + groundOverlay.setTransparency(transparency); + } + + @Override + public void setZIndex(float zIndex) { + groundOverlay.setZIndex(zIndex); + } + + @Override + public void setVisible(boolean visible) { + groundOverlay.setVisible(visible); + } + + @Override + public void setAnchor(float u, float v) {} + + @Override + public void setBearing(float bearing) { + groundOverlay.setBearing(bearing); + } + + @Override + public void setClickable(boolean clickable) { + groundOverlay.setClickable(clickable); + } + + @Override + public void setImage(@NonNull BitmapDescriptor imageDescriptor) { + groundOverlay.setImage(imageDescriptor); + } + + @Override + public void setPosition(@NonNull LatLng location, @NonNull Float width, @Nullable Float height) { + groundOverlay.setPosition(location); + if (height == null) { + groundOverlay.setDimensions(width); + } else { + groundOverlay.setDimensions(width, height); + } + } + + @Override + public void setPositionFromBounds(@NonNull LatLngBounds bounds) { + groundOverlay.setPositionFromBounds(bounds); + } + + String getGoogleMapsGroundOverlayId() { + return googleMapsGroundOverlayId; + } + + public boolean isCreatedWithBounds() { + return isCreatedWithBounds; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlaySink.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlaySink.java new file mode 100644 index 000000000000..d56600b3b785 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlaySink.java @@ -0,0 +1,32 @@ +// 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. + +package io.flutter.plugins.googlemaps; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.maps.model.BitmapDescriptor; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.LatLngBounds; + +/** Receiver of GroundOverlayOptions configuration. */ +interface GroundOverlaySink { + void setTransparency(float transparency); + + void setZIndex(float zIndex); + + void setVisible(boolean visible); + + void setAnchor(float u, float v); + + void setBearing(float bearing); + + void setClickable(boolean clickable); + + void setImage(@NonNull BitmapDescriptor imageDescriptor); + + void setPosition(@NonNull LatLng location, @NonNull Float width, @Nullable Float height); + + void setPositionFromBounds(@NonNull LatLngBounds bounds); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlaysController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlaysController.java new file mode 100644 index 000000000000..92b03b1af5b2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlaysController.java @@ -0,0 +1,139 @@ +// 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. + +package io.flutter.plugins.googlemaps; + +import android.content.res.AssetManager; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.GroundOverlay; +import com.google.android.gms.maps.model.GroundOverlayOptions; +import io.flutter.plugins.googlemaps.Messages.MapsCallbackApi; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class GroundOverlaysController { + @VisibleForTesting final Map groundOverlayIdToController; + private final HashMap googleMapsGroundOverlayIdToDartGroundOverlayId; + private final MapsCallbackApi flutterApi; + private GoogleMap googleMap; + private final AssetManager assetManager; + private final float density; + private final Convert.BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper; + + GroundOverlaysController(MapsCallbackApi flutterApi, AssetManager assetManager, float density) { + this(flutterApi, assetManager, density, new Convert.BitmapDescriptorFactoryWrapper()); + } + + @VisibleForTesting + GroundOverlaysController( + MapsCallbackApi flutterApi, + AssetManager assetManager, + float density, + Convert.BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper) { + this.groundOverlayIdToController = new HashMap<>(); + this.googleMapsGroundOverlayIdToDartGroundOverlayId = new HashMap<>(); + this.flutterApi = flutterApi; + this.assetManager = assetManager; + this.density = density; + this.bitmapDescriptorFactoryWrapper = bitmapDescriptorFactoryWrapper; + } + + void setGoogleMap(GoogleMap googleMap) { + this.googleMap = googleMap; + } + + void addGroundOverlays(@NonNull List groundOverlaysToAdd) { + for (Messages.PlatformGroundOverlay groundOverlayToAdd : groundOverlaysToAdd) { + addGroundOverlay(groundOverlayToAdd); + } + } + + void changeGroundOverlays(@NonNull List groundOverlaysToChange) { + for (Messages.PlatformGroundOverlay groundOverlayToChange : groundOverlaysToChange) { + changeGroundOverlay(groundOverlayToChange); + } + } + + void removeGroundOverlays(@NonNull List groundOverlayIdsToRemove) { + for (@NonNull String groundOverlayId : groundOverlayIdsToRemove) { + removeGroundOverlay(groundOverlayId); + } + } + + @Nullable + GroundOverlay getGroundOverlay(@NonNull String groundOverlayId) { + GroundOverlayController groundOverlayController = + groundOverlayIdToController.get(groundOverlayId); + if (groundOverlayController == null) { + return null; + } + return groundOverlayController.getGroundOverlay(); + } + + private void addGroundOverlay(@NonNull Messages.PlatformGroundOverlay platformGroundOverlay) { + GroundOverlayBuilder groundOverlayOptionsBuilder = new GroundOverlayBuilder(); + String groundOverlayId = + Convert.interpretGroundOverlayOptions( + platformGroundOverlay, + groundOverlayOptionsBuilder, + assetManager, + density, + bitmapDescriptorFactoryWrapper); + GroundOverlayOptions options = groundOverlayOptionsBuilder.build(); + final GroundOverlay groundOverlay = googleMap.addGroundOverlay(options); + if (groundOverlay != null) { + GroundOverlayController groundOverlayController = + new GroundOverlayController(groundOverlay, platformGroundOverlay.getBounds() != null); + groundOverlayIdToController.put(groundOverlayId, groundOverlayController); + googleMapsGroundOverlayIdToDartGroundOverlayId.put(groundOverlay.getId(), groundOverlayId); + } + } + + private void changeGroundOverlay(@NonNull Messages.PlatformGroundOverlay platformGroundOverlay) { + String groundOverlayId = platformGroundOverlay.getGroundOverlayId(); + GroundOverlayController groundOverlayController = + groundOverlayIdToController.get(groundOverlayId); + if (groundOverlayController != null) { + Convert.interpretGroundOverlayOptions( + platformGroundOverlay, + groundOverlayController, + assetManager, + density, + bitmapDescriptorFactoryWrapper); + } + } + + private void removeGroundOverlay(@NonNull String groundOverlayId) { + GroundOverlayController groundOverlayController = + groundOverlayIdToController.get(groundOverlayId); + if (groundOverlayController != null) { + groundOverlayController.remove(); + groundOverlayIdToController.remove(groundOverlayId); + googleMapsGroundOverlayIdToDartGroundOverlayId.remove( + groundOverlayController.getGoogleMapsGroundOverlayId()); + } + } + + void onGroundOverlayTap(@NonNull String googleGroundOverlayId) { + String groundOverlayId = + googleMapsGroundOverlayIdToDartGroundOverlayId.get(googleGroundOverlayId); + if (groundOverlayId == null) { + return; + } + flutterApi.onGroundOverlayTap(groundOverlayId, new NoOpVoidResult()); + } + + boolean isCreatedWithBounds(@NonNull String groundOverlayId) { + GroundOverlayController groundOverlayController = + groundOverlayIdToController.get(groundOverlayId); + if (groundOverlayController == null) { + return false; + } + return groundOverlayController.isCreatedWithBounds(); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java index 4a9afeed6d04..bc5ec22f43fe 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.6.0), do not edit directly. +// Autogenerated from Pigeon (v22.7.3), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.googlemaps; @@ -3809,6 +3809,360 @@ ArrayList toList() { } } + /** + * Pigeon equivalent of the GroundOverlay class. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformGroundOverlay { + private @NonNull String groundOverlayId; + + public @NonNull String getGroundOverlayId() { + return groundOverlayId; + } + + public void setGroundOverlayId(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"groundOverlayId\" is null."); + } + this.groundOverlayId = setterArg; + } + + private @NonNull PlatformBitmap image; + + public @NonNull PlatformBitmap getImage() { + return image; + } + + public void setImage(@NonNull PlatformBitmap setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"image\" is null."); + } + this.image = setterArg; + } + + private @Nullable PlatformLatLng position; + + public @Nullable PlatformLatLng getPosition() { + return position; + } + + public void setPosition(@Nullable PlatformLatLng setterArg) { + this.position = setterArg; + } + + private @Nullable PlatformLatLngBounds bounds; + + public @Nullable PlatformLatLngBounds getBounds() { + return bounds; + } + + public void setBounds(@Nullable PlatformLatLngBounds setterArg) { + this.bounds = setterArg; + } + + private @Nullable Double width; + + public @Nullable Double getWidth() { + return width; + } + + public void setWidth(@Nullable Double setterArg) { + this.width = setterArg; + } + + private @Nullable Double height; + + public @Nullable Double getHeight() { + return height; + } + + public void setHeight(@Nullable Double setterArg) { + this.height = setterArg; + } + + private @Nullable PlatformDoublePair anchor; + + public @Nullable PlatformDoublePair getAnchor() { + return anchor; + } + + public void setAnchor(@Nullable PlatformDoublePair setterArg) { + this.anchor = setterArg; + } + + private @NonNull Double transparency; + + public @NonNull Double getTransparency() { + return transparency; + } + + public void setTransparency(@NonNull Double setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"transparency\" is null."); + } + this.transparency = setterArg; + } + + private @NonNull Double bearing; + + public @NonNull Double getBearing() { + return bearing; + } + + public void setBearing(@NonNull Double setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"bearing\" is null."); + } + this.bearing = setterArg; + } + + private @NonNull Long zIndex; + + public @NonNull Long getZIndex() { + return zIndex; + } + + public void setZIndex(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"zIndex\" is null."); + } + this.zIndex = setterArg; + } + + private @NonNull Boolean visible; + + public @NonNull Boolean getVisible() { + return visible; + } + + public void setVisible(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"visible\" is null."); + } + this.visible = setterArg; + } + + private @NonNull Boolean clickable; + + public @NonNull Boolean getClickable() { + return clickable; + } + + public void setClickable(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"clickable\" is null."); + } + this.clickable = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformGroundOverlay() {} + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PlatformGroundOverlay that = (PlatformGroundOverlay) o; + return groundOverlayId.equals(that.groundOverlayId) + && image.equals(that.image) + && Objects.equals(position, that.position) + && Objects.equals(bounds, that.bounds) + && Objects.equals(width, that.width) + && Objects.equals(height, that.height) + && Objects.equals(anchor, that.anchor) + && transparency.equals(that.transparency) + && bearing.equals(that.bearing) + && zIndex.equals(that.zIndex) + && visible.equals(that.visible) + && clickable.equals(that.clickable); + } + + @Override + public int hashCode() { + return Objects.hash( + groundOverlayId, + image, + position, + bounds, + width, + height, + anchor, + transparency, + bearing, + zIndex, + visible, + clickable); + } + + public static final class Builder { + + private @Nullable String groundOverlayId; + + @CanIgnoreReturnValue + public @NonNull Builder setGroundOverlayId(@NonNull String setterArg) { + this.groundOverlayId = setterArg; + return this; + } + + private @Nullable PlatformBitmap image; + + @CanIgnoreReturnValue + public @NonNull Builder setImage(@NonNull PlatformBitmap setterArg) { + this.image = setterArg; + return this; + } + + private @Nullable PlatformLatLng position; + + @CanIgnoreReturnValue + public @NonNull Builder setPosition(@Nullable PlatformLatLng setterArg) { + this.position = setterArg; + return this; + } + + private @Nullable PlatformLatLngBounds bounds; + + @CanIgnoreReturnValue + public @NonNull Builder setBounds(@Nullable PlatformLatLngBounds setterArg) { + this.bounds = setterArg; + return this; + } + + private @Nullable Double width; + + @CanIgnoreReturnValue + public @NonNull Builder setWidth(@Nullable Double setterArg) { + this.width = setterArg; + return this; + } + + private @Nullable Double height; + + @CanIgnoreReturnValue + public @NonNull Builder setHeight(@Nullable Double setterArg) { + this.height = setterArg; + return this; + } + + private @Nullable PlatformDoublePair anchor; + + @CanIgnoreReturnValue + public @NonNull Builder setAnchor(@Nullable PlatformDoublePair setterArg) { + this.anchor = setterArg; + return this; + } + + private @Nullable Double transparency; + + @CanIgnoreReturnValue + public @NonNull Builder setTransparency(@NonNull Double setterArg) { + this.transparency = setterArg; + return this; + } + + private @Nullable Double bearing; + + @CanIgnoreReturnValue + public @NonNull Builder setBearing(@NonNull Double setterArg) { + this.bearing = setterArg; + return this; + } + + private @Nullable Long zIndex; + + @CanIgnoreReturnValue + public @NonNull Builder setZIndex(@NonNull Long setterArg) { + this.zIndex = setterArg; + return this; + } + + private @Nullable Boolean visible; + + @CanIgnoreReturnValue + public @NonNull Builder setVisible(@NonNull Boolean setterArg) { + this.visible = setterArg; + return this; + } + + private @Nullable Boolean clickable; + + @CanIgnoreReturnValue + public @NonNull Builder setClickable(@NonNull Boolean setterArg) { + this.clickable = setterArg; + return this; + } + + public @NonNull PlatformGroundOverlay build() { + PlatformGroundOverlay pigeonReturn = new PlatformGroundOverlay(); + pigeonReturn.setGroundOverlayId(groundOverlayId); + pigeonReturn.setImage(image); + pigeonReturn.setPosition(position); + pigeonReturn.setBounds(bounds); + pigeonReturn.setWidth(width); + pigeonReturn.setHeight(height); + pigeonReturn.setAnchor(anchor); + pigeonReturn.setTransparency(transparency); + pigeonReturn.setBearing(bearing); + pigeonReturn.setZIndex(zIndex); + pigeonReturn.setVisible(visible); + pigeonReturn.setClickable(clickable); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(12); + toListResult.add(groundOverlayId); + toListResult.add(image); + toListResult.add(position); + toListResult.add(bounds); + toListResult.add(width); + toListResult.add(height); + toListResult.add(anchor); + toListResult.add(transparency); + toListResult.add(bearing); + toListResult.add(zIndex); + toListResult.add(visible); + toListResult.add(clickable); + return toListResult; + } + + static @NonNull PlatformGroundOverlay fromList(@NonNull ArrayList pigeonVar_list) { + PlatformGroundOverlay pigeonResult = new PlatformGroundOverlay(); + Object groundOverlayId = pigeonVar_list.get(0); + pigeonResult.setGroundOverlayId((String) groundOverlayId); + Object image = pigeonVar_list.get(1); + pigeonResult.setImage((PlatformBitmap) image); + Object position = pigeonVar_list.get(2); + pigeonResult.setPosition((PlatformLatLng) position); + Object bounds = pigeonVar_list.get(3); + pigeonResult.setBounds((PlatformLatLngBounds) bounds); + Object width = pigeonVar_list.get(4); + pigeonResult.setWidth((Double) width); + Object height = pigeonVar_list.get(5); + pigeonResult.setHeight((Double) height); + Object anchor = pigeonVar_list.get(6); + pigeonResult.setAnchor((PlatformDoublePair) anchor); + Object transparency = pigeonVar_list.get(7); + pigeonResult.setTransparency((Double) transparency); + Object bearing = pigeonVar_list.get(8); + pigeonResult.setBearing((Double) bearing); + Object zIndex = pigeonVar_list.get(9); + pigeonResult.setZIndex((Long) zIndex); + Object visible = pigeonVar_list.get(10); + pigeonResult.setVisible((Boolean) visible); + Object clickable = pigeonVar_list.get(11); + pigeonResult.setClickable((Boolean) clickable); + return pigeonResult; + } + } + /** * Pigeon equivalent of CameraTargetBounds. * @@ -4000,6 +4354,19 @@ public void setInitialClusterManagers(@NonNull List sett this.initialClusterManagers = setterArg; } + private @NonNull List initialGroundOverlays; + + public @NonNull List getInitialGroundOverlays() { + return initialGroundOverlays; + } + + public void setInitialGroundOverlays(@NonNull List setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"initialGroundOverlays\" is null."); + } + this.initialGroundOverlays = setterArg; + } + /** Constructor is non-public to enforce null safety; use Builder. */ PlatformMapViewCreationParams() {} @@ -4020,7 +4387,8 @@ public boolean equals(Object o) { && initialPolylines.equals(that.initialPolylines) && initialHeatmaps.equals(that.initialHeatmaps) && initialTileOverlays.equals(that.initialTileOverlays) - && initialClusterManagers.equals(that.initialClusterManagers); + && initialClusterManagers.equals(that.initialClusterManagers) + && initialGroundOverlays.equals(that.initialGroundOverlays); } @Override @@ -4034,7 +4402,8 @@ public int hashCode() { initialPolylines, initialHeatmaps, initialTileOverlays, - initialClusterManagers); + initialClusterManagers, + initialGroundOverlays); } public static final class Builder { @@ -4112,6 +4481,15 @@ public static final class Builder { return this; } + private @Nullable List initialGroundOverlays; + + @CanIgnoreReturnValue + public @NonNull Builder setInitialGroundOverlays( + @NonNull List setterArg) { + this.initialGroundOverlays = setterArg; + return this; + } + public @NonNull PlatformMapViewCreationParams build() { PlatformMapViewCreationParams pigeonReturn = new PlatformMapViewCreationParams(); pigeonReturn.setInitialCameraPosition(initialCameraPosition); @@ -4123,13 +4501,14 @@ public static final class Builder { pigeonReturn.setInitialHeatmaps(initialHeatmaps); pigeonReturn.setInitialTileOverlays(initialTileOverlays); pigeonReturn.setInitialClusterManagers(initialClusterManagers); + pigeonReturn.setInitialGroundOverlays(initialGroundOverlays); return pigeonReturn; } } @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList<>(9); + ArrayList toListResult = new ArrayList<>(10); toListResult.add(initialCameraPosition); toListResult.add(mapConfiguration); toListResult.add(initialCircles); @@ -4139,6 +4518,7 @@ ArrayList toList() { toListResult.add(initialHeatmaps); toListResult.add(initialTileOverlays); toListResult.add(initialClusterManagers); + toListResult.add(initialGroundOverlays); return toListResult; } @@ -4163,6 +4543,8 @@ ArrayList toList() { pigeonResult.setInitialTileOverlays((List) initialTileOverlays); Object initialClusterManagers = pigeonVar_list.get(8); pigeonResult.setInitialClusterManagers((List) initialClusterManagers); + Object initialGroundOverlays = pigeonVar_list.get(9); + pigeonResult.setInitialGroundOverlays((List) initialGroundOverlays); return pigeonResult; } } @@ -5917,30 +6299,32 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { case (byte) 160: return PlatformCluster.fromList((ArrayList) readValue(buffer)); case (byte) 161: - return PlatformCameraTargetBounds.fromList((ArrayList) readValue(buffer)); + return PlatformGroundOverlay.fromList((ArrayList) readValue(buffer)); case (byte) 162: - return PlatformMapViewCreationParams.fromList((ArrayList) readValue(buffer)); + return PlatformCameraTargetBounds.fromList((ArrayList) readValue(buffer)); case (byte) 163: - return PlatformMapConfiguration.fromList((ArrayList) readValue(buffer)); + return PlatformMapViewCreationParams.fromList((ArrayList) readValue(buffer)); case (byte) 164: - return PlatformPoint.fromList((ArrayList) readValue(buffer)); + return PlatformMapConfiguration.fromList((ArrayList) readValue(buffer)); case (byte) 165: - return PlatformTileLayer.fromList((ArrayList) readValue(buffer)); + return PlatformPoint.fromList((ArrayList) readValue(buffer)); case (byte) 166: - return PlatformZoomRange.fromList((ArrayList) readValue(buffer)); + return PlatformTileLayer.fromList((ArrayList) readValue(buffer)); case (byte) 167: - return PlatformBitmap.fromList((ArrayList) readValue(buffer)); + return PlatformZoomRange.fromList((ArrayList) readValue(buffer)); case (byte) 168: - return PlatformBitmapDefaultMarker.fromList((ArrayList) readValue(buffer)); + return PlatformBitmap.fromList((ArrayList) readValue(buffer)); case (byte) 169: - return PlatformBitmapBytes.fromList((ArrayList) readValue(buffer)); + return PlatformBitmapDefaultMarker.fromList((ArrayList) readValue(buffer)); case (byte) 170: - return PlatformBitmapAsset.fromList((ArrayList) readValue(buffer)); + return PlatformBitmapBytes.fromList((ArrayList) readValue(buffer)); case (byte) 171: - return PlatformBitmapAssetImage.fromList((ArrayList) readValue(buffer)); + return PlatformBitmapAsset.fromList((ArrayList) readValue(buffer)); case (byte) 172: - return PlatformBitmapAssetMap.fromList((ArrayList) readValue(buffer)); + return PlatformBitmapAssetImage.fromList((ArrayList) readValue(buffer)); case (byte) 173: + return PlatformBitmapAssetMap.fromList((ArrayList) readValue(buffer)); + case (byte) 174: return PlatformBitmapBytesMap.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -6045,44 +6429,47 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { } else if (value instanceof PlatformCluster) { stream.write(160); writeValue(stream, ((PlatformCluster) value).toList()); - } else if (value instanceof PlatformCameraTargetBounds) { + } else if (value instanceof PlatformGroundOverlay) { stream.write(161); + writeValue(stream, ((PlatformGroundOverlay) value).toList()); + } else if (value instanceof PlatformCameraTargetBounds) { + stream.write(162); writeValue(stream, ((PlatformCameraTargetBounds) value).toList()); } else if (value instanceof PlatformMapViewCreationParams) { - stream.write(162); + stream.write(163); writeValue(stream, ((PlatformMapViewCreationParams) value).toList()); } else if (value instanceof PlatformMapConfiguration) { - stream.write(163); + stream.write(164); writeValue(stream, ((PlatformMapConfiguration) value).toList()); } else if (value instanceof PlatformPoint) { - stream.write(164); + stream.write(165); writeValue(stream, ((PlatformPoint) value).toList()); } else if (value instanceof PlatformTileLayer) { - stream.write(165); + stream.write(166); writeValue(stream, ((PlatformTileLayer) value).toList()); } else if (value instanceof PlatformZoomRange) { - stream.write(166); + stream.write(167); writeValue(stream, ((PlatformZoomRange) value).toList()); } else if (value instanceof PlatformBitmap) { - stream.write(167); + stream.write(168); writeValue(stream, ((PlatformBitmap) value).toList()); } else if (value instanceof PlatformBitmapDefaultMarker) { - stream.write(168); + stream.write(169); writeValue(stream, ((PlatformBitmapDefaultMarker) value).toList()); } else if (value instanceof PlatformBitmapBytes) { - stream.write(169); + stream.write(170); writeValue(stream, ((PlatformBitmapBytes) value).toList()); } else if (value instanceof PlatformBitmapAsset) { - stream.write(170); + stream.write(171); writeValue(stream, ((PlatformBitmapAsset) value).toList()); } else if (value instanceof PlatformBitmapAssetImage) { - stream.write(171); + stream.write(172); writeValue(stream, ((PlatformBitmapAssetImage) value).toList()); } else if (value instanceof PlatformBitmapAssetMap) { - stream.write(172); + stream.write(173); writeValue(stream, ((PlatformBitmapAssetMap) value).toList()); } else if (value instanceof PlatformBitmapBytesMap) { - stream.write(173); + stream.write(174); writeValue(stream, ((PlatformBitmapBytesMap) value).toList()); } else { super.writeValue(stream, value); @@ -6164,6 +6551,11 @@ void updateTileOverlays( @NonNull List toAdd, @NonNull List toChange, @NonNull List idsToRemove); + /** Updates the set of ground overlays on the map. */ + void updateGroundOverlays( + @NonNull List toAdd, + @NonNull List toChange, + @NonNull List idsToRemove); /** Gets the screen coordinate for the given map location. */ @NonNull PlatformPoint getScreenCoordinate(@NonNull PlatformLatLng latLng); @@ -6466,6 +6858,33 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.google_maps_flutter_android.MapsApi.updateGroundOverlays" + + messageChannelSuffix, + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + ArrayList args = (ArrayList) message; + List toAddArg = (List) args.get(0); + List toChangeArg = (List) args.get(1); + List idsToRemoveArg = (List) args.get(2); + try { + api.updateGroundOverlays(toAddArg, toChangeArg, idsToRemoveArg); + wrapped.add(0, null); + } catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -7157,6 +7576,30 @@ public void onPolylineTap(@NonNull String polylineIdArg, @NonNull VoidResult res } }); } + /** Called when a ground overlay is tapped. */ + public void onGroundOverlayTap(@NonNull String groundOverlayIdArg, @NonNull VoidResult result) { + final String channelName = + "dev.flutter.pigeon.google_maps_flutter_android.MapsCallbackApi.onGroundOverlayTap" + + messageChannelSuffix; + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, channelName, getCodec()); + channel.send( + new ArrayList<>(Collections.singletonList(groundOverlayIdArg)), + channelReply -> { + if (channelReply instanceof List) { + List listReply = (List) channelReply; + if (listReply.size() > 1) { + result.error( + new FlutterError( + (String) listReply.get(0), (String) listReply.get(1), listReply.get(2))); + } else { + result.success(); + } + } else { + result.error(createConnectionError(channelName)); + } + }); + } /** Called to get data for a map tile. */ public void getTileOverlayTile( @NonNull String tileOverlayIdArg, @@ -7357,6 +7800,9 @@ public interface MapsInspectorApi { @Nullable PlatformTileLayer getTileOverlayInfo(@NonNull String tileOverlayId); + @Nullable + PlatformGroundOverlay getGroundOverlayInfo(@NonNull String groundOverlayId); + @NonNull PlatformZoomRange getZoomRange(); @@ -7657,6 +8103,31 @@ static void setUp( channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.google_maps_flutter_android.MapsInspectorApi.getGroundOverlayInfo" + + messageChannelSuffix, + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + ArrayList args = (ArrayList) message; + String groundOverlayIdArg = (String) args.get(0); + try { + PlatformGroundOverlay output = api.getGroundOverlayInfo(groundOverlayIdArg); + wrapped.add(0, output); + } catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java index 48d8b619bebc..65be99a3ff86 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java @@ -51,6 +51,7 @@ public class GoogleMapControllerTest { @Mock CirclesController mockCirclesController; @Mock HeatmapsController mockHeatmapsController; @Mock TileOverlaysController mockTileOverlaysController; + @Mock GroundOverlaysController mockGroundOverlaysController; @Before public void before() { @@ -84,7 +85,8 @@ public GoogleMapController getGoogleMapControllerWithMockedDependencies() { mockPolylinesController, mockCirclesController, mockHeatmapsController, - mockTileOverlaysController); + mockTileOverlaysController, + mockGroundOverlaysController); googleMapController.init(); return googleMapController; } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GroundOverlaysControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GroundOverlaysControllerTest.java new file mode 100644 index 000000000000..bc778af9f58e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GroundOverlaysControllerTest.java @@ -0,0 +1,150 @@ +// 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. + +package io.flutter.plugins.googlemaps; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.os.Build; +import android.util.Base64; +import androidx.annotation.NonNull; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.BitmapDescriptor; +import com.google.android.gms.maps.model.GroundOverlay; +import com.google.android.gms.maps.model.GroundOverlayOptions; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.googlemaps.Convert.BitmapDescriptorFactoryWrapper; +import java.io.ByteArrayOutputStream; +import java.util.Collections; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = Build.VERSION_CODES.LOLLIPOP) +public class GroundOverlaysControllerTest { + @Mock private BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper; + @Mock private BitmapDescriptor mockBitmapDescriptor; + + AutoCloseable mockCloseable; + + private GroundOverlaysController controller; + private GoogleMap googleMap; + + // A 1x1 pixel (#8080ff) PNG image encoded in base64 + private final String base64Image = generateBase64Image(); + + @NonNull + private Messages.PlatformGroundOverlay.Builder defaultGroundOverlayBuilder() { + byte[] bmpData = Base64.decode(base64Image, Base64.DEFAULT); + + return new Messages.PlatformGroundOverlay.Builder() + .setImage( + new Messages.PlatformBitmap.Builder() + .setBitmap( + new Messages.PlatformBitmapBytesMap.Builder() + .setBitmapScaling(Messages.PlatformMapBitmapScaling.AUTO) + .setImagePixelRatio(2.0) + .setByteData(bmpData) + .setWidth(100.0) + .build()) + .build()) + .setBearing(1.0) + .setZIndex(1L) + .setVisible(true) + .setTransparency(1.0) + .setClickable(true); + } + + @Before + public void setUp() { + mockCloseable = MockitoAnnotations.openMocks(this); + Context context = ApplicationProvider.getApplicationContext(); + AssetManager assetManager = context.getAssets(); + Messages.MapsCallbackApi flutterApi = + spy(new Messages.MapsCallbackApi(mock(BinaryMessenger.class))); + controller = + spy( + new GroundOverlaysController( + flutterApi, assetManager, 1.0f, bitmapDescriptorFactoryWrapper)); + googleMap = mock(GoogleMap.class); + controller.setGoogleMap(googleMap); + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + } + + @After + public void tearDown() throws Exception { + mockCloseable.close(); + } + + @Test + public void controller_AddChangeAndRemoveGroundOverlay() { + final GroundOverlay groundOverlay = mock(GroundOverlay.class); + final String googleGroundOverlayId = "abc123"; + final float transparency = 0.1f; + + when(groundOverlay.getId()).thenReturn(googleGroundOverlayId); + when(googleMap.addGroundOverlay(any(GroundOverlayOptions.class))).thenReturn(groundOverlay); + + controller.addGroundOverlays( + Collections.singletonList( + defaultGroundOverlayBuilder() + .setGroundOverlayId(googleGroundOverlayId) + .setTransparency((double) transparency) + .build())); + Mockito.verify(googleMap, times(1)) + .addGroundOverlay(Mockito.argThat(argument -> argument.getTransparency() == transparency)); + + final float newTransparency = 0.2f; + controller.changeGroundOverlays( + Collections.singletonList( + defaultGroundOverlayBuilder() + .setGroundOverlayId(googleGroundOverlayId) + .setTransparency((double) newTransparency) + .build())); + Mockito.verify(groundOverlay, times(1)).setTransparency(newTransparency); + + controller.removeGroundOverlays(Collections.singletonList(googleGroundOverlayId)); + + Mockito.verify(groundOverlay, times(1)).remove(); + } + + // Helper method to generate 1x1 pixel base64 encoded png test image + private String generateBase64Image() { + int width = 1; + int height = 1; + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + // Draw on the Bitmap + Paint paint = new Paint(); + paint.setColor(Color.parseColor("#FF8080FF")); + canvas.drawRect(0, 0, width, height, paint); + + // Convert the Bitmap to PNG format + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); + byte[] pngBytes = outputStream.toByteArray(); + + // Encode the PNG bytes as a base64 string + return Base64.encodeToString(pngBytes, Base64.DEFAULT); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart index 666b48555811..b709438138a7 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; @@ -21,6 +20,7 @@ const double _kInitialZoomLevel = 5; const CameraPosition _kInitialCameraPosition = CameraPosition(target: _kInitialMapCenter, zoom: _kInitialZoomLevel); const String _kCloudMapId = '000000000000000'; // Dummy map ID. +const double _floatTolerance = 1e-8; void googleMapsTests() { GoogleMapsFlutterPlatform.instance.enableDebugInspection(); @@ -995,7 +995,7 @@ void googleMapsTests() { }, // TODO(cyanglaz): un-skip the test when we can test this on CI with API key enabled. // https://github.com/flutter/flutter/issues/57057 - skip: Platform.isAndroid); + skip: true); testWidgets( 'set tileOverlay correctly', @@ -1446,6 +1446,261 @@ void googleMapsTests() { await tester.pumpAndSettle(); }); + + group('GroundOverlay', () { + final LatLngBounds kGroundOverlayBounds = LatLngBounds( + southwest: const LatLng(37.77483, -122.41942), + northeast: const LatLng(37.78183, -122.39105), + ); + + final GroundOverlay groundOverlayBounds1 = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('bounds_1'), + bounds: kGroundOverlayBounds, + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ), + ); + + final GroundOverlay groundOverlayPosition1 = GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('position_1'), + position: kGroundOverlayBounds.northeast, + width: 100, + height: 100, + anchor: const Offset(0.1, 0.2), + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + )); + + void expectGroundOverlayEquals( + GroundOverlay source, GroundOverlay response) { + expect(response.groundOverlayId, source.groundOverlayId); + expect( + response.transparency, + moreOrLessEquals(source.transparency, epsilon: _floatTolerance), + ); + expect( + response.bearing, + moreOrLessEquals(source.bearing, epsilon: _floatTolerance), + ); + + // Only test bounds if it was given in the original object + if (source.bounds != null) { + expect(response.bounds, source.bounds); + } + + // Only test position if it was given in the original object + if (source.position != null) { + expect(response.position, source.position); + } + + expect(response.clickable, source.clickable); + expect(response.zIndex, source.zIndex); + + expect(response.width, source.width); + expect(response.height, source.height); + if (source.position != null) { + expect( + response.anchor?.dx, + moreOrLessEquals(source.anchor!.dx, epsilon: _floatTolerance), + ); + expect( + response.anchor?.dy, + moreOrLessEquals(source.anchor!.dy, epsilon: _floatTolerance), + ); + } + } + + testWidgets('set ground overlays correctly', (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final GroundOverlay groundOverlayBounds2 = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('bounds_2'), + bounds: groundOverlayBounds1.bounds!, + image: groundOverlayBounds1.image, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: _kInitialCameraPosition, + groundOverlays: { + groundOverlayBounds1, + groundOverlayBounds2, + groundOverlayPosition1, + }, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + if (inspector.supportsGettingGroundOverlayInfo()) { + final GroundOverlay groundOverlayBoundsInfo1 = (await inspector + .getGroundOverlayInfo(groundOverlayBounds1.mapsId, mapId: mapId))!; + final GroundOverlay groundOverlayBoundsInfo2 = (await inspector + .getGroundOverlayInfo(groundOverlayBounds2.mapsId, mapId: mapId))!; + final GroundOverlay groundOverlayPositionInfo1 = + (await inspector.getGroundOverlayInfo(groundOverlayPosition1.mapsId, + mapId: mapId))!; + + expectGroundOverlayEquals( + groundOverlayBounds1, + groundOverlayBoundsInfo1, + ); + expectGroundOverlayEquals( + groundOverlayBounds2, + groundOverlayBoundsInfo2, + ); + expectGroundOverlayEquals( + groundOverlayPosition1, + groundOverlayPositionInfo1, + ); + } + }); + + testWidgets('update ground overlays correctly', + (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final Key key = GlobalKey(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + groundOverlays: { + groundOverlayBounds1, + groundOverlayPosition1 + }, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + ), + ); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + final GroundOverlay groundOverlayBounds1New = + groundOverlayBounds1.copyWith( + bearingParam: 10, + clickableParam: false, + transparencyParam: 0.5, + visibleParam: false, + zIndexParam: 10, + ); + + final GroundOverlay groundOverlayPosition1New = + groundOverlayPosition1.copyWith( + bearingParam: 10, + clickableParam: false, + transparencyParam: 0.5, + visibleParam: false, + zIndexParam: 10, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + groundOverlays: { + groundOverlayBounds1New, + groundOverlayPosition1New + }, + onMapCreated: (ExampleGoogleMapController controller) { + fail('update: OnMapCreated should get called only once.'); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + if (inspector.supportsGettingGroundOverlayInfo()) { + final GroundOverlay groundOverlayBounds1Info = (await inspector + .getGroundOverlayInfo(groundOverlayBounds1.mapsId, mapId: mapId))!; + final GroundOverlay groundOverlayPosition1Info = + (await inspector.getGroundOverlayInfo(groundOverlayPosition1.mapsId, + mapId: mapId))!; + + expectGroundOverlayEquals( + groundOverlayBounds1New, + groundOverlayBounds1Info, + ); + expectGroundOverlayEquals( + groundOverlayPosition1New, + groundOverlayPosition1Info, + ); + } + }); + + testWidgets('remove ground overlays correctly', + (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final Key key = GlobalKey(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + groundOverlays: { + groundOverlayBounds1, + groundOverlayPosition1 + }, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + ), + ); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + if (inspector.supportsGettingGroundOverlayInfo()) { + final GroundOverlay? groundOverlayBounds1Info = await inspector + .getGroundOverlayInfo(groundOverlayBounds1.mapsId, mapId: mapId); + final GroundOverlay? groundOverlayPositionInfo = await inspector + .getGroundOverlayInfo(groundOverlayPosition1.mapsId, mapId: mapId); + + expect(groundOverlayBounds1Info, isNull); + expect(groundOverlayPositionInfo, isNull); + } + }); + }); } class _DebugTileProvider implements TileProvider { diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart index fcf24452c875..b0066680b89a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart @@ -82,6 +82,9 @@ class ExampleGoogleMapController { GoogleMapsFlutterPlatform.instance .onCircleTap(mapId: mapId) .listen((CircleTapEvent e) => _googleMapState.onCircleTap(e.value)); + GoogleMapsFlutterPlatform.instance.onGroundOverlayTap(mapId: mapId).listen( + (GroundOverlayTapEvent e) => + _googleMapState.onGroundOverlayTap(e.value)); GoogleMapsFlutterPlatform.instance .onTap(mapId: mapId) .listen((MapTapEvent e) => _googleMapState.onTap(e.position)); @@ -111,6 +114,13 @@ class ExampleGoogleMapController { .updateClusterManagers(clusterManagerUpdates, mapId: mapId); } + /// Updates ground overlay configuration. + Future _updateGroundOverlays( + GroundOverlayUpdates groundOverlayUpdates) { + return GoogleMapsFlutterPlatform.instance + .updateGroundOverlays(groundOverlayUpdates, mapId: mapId); + } + /// Updates polygon configuration. Future _updatePolygons(PolygonUpdates polygonUpdates) { return GoogleMapsFlutterPlatform.instance @@ -250,6 +260,7 @@ class ExampleGoogleMap extends StatefulWidget { this.clusterManagers = const {}, this.onCameraMoveStarted, this.tileOverlays = const {}, + this.groundOverlays = const {}, this.onCameraMove, this.onCameraIdle, this.onTap, @@ -326,6 +337,9 @@ class ExampleGoogleMap extends StatefulWidget { /// Cluster Managers to be placed for the map. final Set clusterManagers; + /// Ground overlays to be initialized for the map. + final Set groundOverlays; + /// Called when the camera starts moving. final VoidCallback? onCameraMoveStarted; @@ -387,6 +401,8 @@ class _ExampleGoogleMapState extends State { Map _circles = {}; Map _clusterManagers = {}; + Map _groundOverlays = + {}; late MapConfiguration _mapConfiguration; @override @@ -407,6 +423,7 @@ class _ExampleGoogleMapState extends State { polylines: widget.polylines, circles: widget.circles, clusterManagers: widget.clusterManagers, + groundOverlays: widget.groundOverlays, ), mapConfiguration: _mapConfiguration, ); @@ -421,6 +438,7 @@ class _ExampleGoogleMapState extends State { _polygons = keyByPolygonId(widget.polygons); _polylines = keyByPolylineId(widget.polylines); _circles = keyByCircleId(widget.circles); + _groundOverlays = keyByGroundOverlayId(widget.groundOverlays); } @override @@ -440,6 +458,7 @@ class _ExampleGoogleMapState extends State { _updatePolylines(); _updateCircles(); _updateTileOverlays(); + _updateGroundOverlays(); } Future _updateOptions() async { @@ -467,6 +486,13 @@ class _ExampleGoogleMapState extends State { _clusterManagers = keyByClusterManagerId(widget.clusterManagers); } + Future _updateGroundOverlays() async { + final ExampleGoogleMapController controller = await _controller.future; + unawaited(controller._updateGroundOverlays(GroundOverlayUpdates.from( + _groundOverlays.values.toSet(), widget.groundOverlays))); + _groundOverlays = keyByGroundOverlayId(widget.groundOverlays); + } + Future _updatePolygons() async { final ExampleGoogleMapController controller = await _controller.future; unawaited(controller._updatePolygons( @@ -533,6 +559,10 @@ class _ExampleGoogleMapState extends State { _circles[circleId]!.onTap?.call(); } + void onGroundOverlayTap(GroundOverlayId groundOverlayId) { + _groundOverlays[groundOverlayId]!.onTap?.call(); + } + void onInfoWindowTap(MarkerId markerId) { _markers[markerId]!.infoWindow.onTap?.call(); } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/ground_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/ground_overlay.dart new file mode 100644 index 000000000000..abc706b409c4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/ground_overlay.dart @@ -0,0 +1,325 @@ +// 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. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +enum _GroundOverlayPlacing { position, bounds } + +class GroundOverlayPage extends GoogleMapExampleAppPage { + const GroundOverlayPage({Key? key}) + : super(const Icon(Icons.map), 'Ground overlay', key: key); + + @override + Widget build(BuildContext context) { + return const GroundOverlayBody(); + } +} + +class GroundOverlayBody extends StatefulWidget { + const GroundOverlayBody({super.key}); + + @override + State createState() => GroundOverlayBodyState(); +} + +class GroundOverlayBodyState extends State { + GroundOverlayBodyState(); + + ExampleGoogleMapController? controller; + GroundOverlay? _groundOverlay; + + final LatLng _mapCenter = const LatLng(37.422026, -122.085329); + + _GroundOverlayPlacing _placingType = _GroundOverlayPlacing.bounds; + + // Positions for demonstranting placing ground overlays with position, and + // changing positions. + final LatLng _groundOverlayPos1 = const LatLng(37.422026, -122.085329); + final LatLng _groundOverlayPos2 = const LatLng(37.42, -122.08); + late LatLng _currentGroundOverlayPos; + + // Bounds for demonstranting placing ground overlays with bounds, and + // changing bounds. + final LatLngBounds _groundOverlayBounds1 = LatLngBounds( + southwest: const LatLng(37.42, -122.09), + northeast: const LatLng(37.423, -122.084)); + final LatLngBounds _groundOverlayBounds2 = LatLngBounds( + southwest: const LatLng(37.421, -122.091), + northeast: const LatLng(37.424, -122.08)); + late LatLngBounds _currentGroundOverlayBounds; + + Offset _anchor = const Offset(0.5, 0.5); + + Offset _dimensions = const Offset(1000, 1000); + + // Index to be used as identifier for the ground overlay. + // If position is changed to bounds and vice versa, the ground overlay will + // be removed and added again with the new type. Also anchor can be given only + // when the ground overlay is created with position and cannot be changed + // after the ground overlay is created. + int _groundOverlayIndex = 0; + + @override + void initState() { + _currentGroundOverlayPos = _groundOverlayPos1; + _currentGroundOverlayBounds = _groundOverlayBounds1; + super.initState(); + } + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + this.controller = controller; + } + + void _removeGroundOverlay() { + setState(() { + _groundOverlay = null; + }); + } + + Future _addGroundOverlay() async { + final AssetMapBitmap assetMapBitmap = await AssetMapBitmap.create( + createLocalImageConfiguration(context), + 'assets/red_square.png', + bitmapScaling: MapBitmapScaling.none, + ); + + _groundOverlayIndex += 1; + + final GroundOverlayId id = + GroundOverlayId('ground_overlay_$_groundOverlayIndex'); + + final GroundOverlay groundOverlay = switch (_placingType) { + _GroundOverlayPlacing.position => GroundOverlay.fromPosition( + groundOverlayId: id, + image: assetMapBitmap, + position: _currentGroundOverlayPos, + width: _dimensions.dx, + height: _dimensions.dy, + anchor: _anchor, + onTap: () { + _onGroundOverlayTapped(); + }, + ), + _GroundOverlayPlacing.bounds => GroundOverlay.fromBounds( + groundOverlayId: id, + image: assetMapBitmap, + bounds: _currentGroundOverlayBounds, + anchor: _anchor, + onTap: () { + _onGroundOverlayTapped(); + }, + ), + }; + + setState(() { + _groundOverlay = groundOverlay; + }); + } + + void _onGroundOverlayTapped() { + _changePosition(); + } + + void _setBearing() { + assert(_groundOverlay != null); + setState(() { + _groundOverlay = _groundOverlay!.copyWith( + bearingParam: _groundOverlay!.bearing >= 350 + ? 0 + : _groundOverlay!.bearing + 10); + }); + } + + void _changeTransparency() { + assert(_groundOverlay != null); + setState(() { + final double transparency = + _groundOverlay!.transparency == 0.0 ? 0.5 : 0.0; + _groundOverlay = + _groundOverlay!.copyWith(transparencyParam: transparency); + }); + } + + Future _changeDimensions() async { + assert(_groundOverlay != null); + assert(_placingType == _GroundOverlayPlacing.position); + setState(() { + _dimensions = _dimensions == const Offset(1000, 1000) + ? const Offset(1500, 500) + : const Offset(1000, 1000); + }); + + // Re-add the ground overlay to apply the new position, as the position + // cannot be changed after the ground overlay is created on all platforms. + await _addGroundOverlay(); + } + + Future _changePosition() async { + assert(_groundOverlay != null); + assert(_placingType == _GroundOverlayPlacing.position); + setState(() { + _currentGroundOverlayPos = _currentGroundOverlayPos == _groundOverlayPos1 + ? _groundOverlayPos2 + : _groundOverlayPos1; + }); + + // Re-add the ground overlay to apply the new position, as the position + // cannot be changed after the ground overlay is created on all platforms. + await _addGroundOverlay(); + } + + Future _changeBounds() async { + assert(_groundOverlay != null); + assert(_placingType == _GroundOverlayPlacing.bounds); + setState(() { + _currentGroundOverlayBounds = + _currentGroundOverlayBounds == _groundOverlayBounds1 + ? _groundOverlayBounds2 + : _groundOverlayBounds1; + }); + + // Re-add the ground overlay to apply the new position, as the position + // cannot be changed after the ground overlay is created on all platforms. + await _addGroundOverlay(); + } + + void _toggleVisible() { + assert(_groundOverlay != null); + setState(() { + _groundOverlay = + _groundOverlay!.copyWith(visibleParam: !_groundOverlay!.visible); + }); + } + + void _changeZIndex() { + assert(_groundOverlay != null); + final int current = _groundOverlay!.zIndex; + final int zIndex = current == 12 ? 0 : current + 1; + setState(() { + _groundOverlay = _groundOverlay!.copyWith(zIndexParam: zIndex); + }); + } + + Future _changeType() async { + setState(() { + _placingType = _placingType == _GroundOverlayPlacing.position + ? _GroundOverlayPlacing.bounds + : _GroundOverlayPlacing.position; + }); + + // Re-add the ground overlay to apply the new position, as the position + // cannot be changed after the ground overlay is created on all platforms. + await _addGroundOverlay(); + } + + Future _changeAnchor() async { + assert(_groundOverlay != null); + setState(() { + _anchor = _groundOverlay!.anchor == const Offset(0.5, 0.5) + ? const Offset(1.0, 1.0) + : const Offset(0.5, 0.5); + }); + + // Re-add the ground overlay to apply the new anchor, as anchor cannot be + // changed after the ground overlay is created. + await _addGroundOverlay(); + } + + @override + Widget build(BuildContext context) { + final Set overlays = { + if (_groundOverlay != null) _groundOverlay!, + }; + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: ExampleGoogleMap( + initialCameraPosition: CameraPosition( + target: _mapCenter, + zoom: 14.0, + ), + groundOverlays: overlays, + onMapCreated: _onMapCreated, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: _groundOverlay == null ? _addGroundOverlay : null, + child: const Text('Add'), + ), + TextButton( + onPressed: _groundOverlay != null ? _removeGroundOverlay : null, + child: const Text('Remove'), + ), + ], + ), + Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: + _groundOverlay == null ? null : () => _changeTransparency(), + child: const Text('change transparency'), + ), + TextButton( + onPressed: _groundOverlay == null ? null : () => _setBearing(), + child: const Text('change bearing'), + ), + TextButton( + onPressed: _groundOverlay == null ? null : () => _toggleVisible(), + child: const Text('toggle visible'), + ), + TextButton( + onPressed: _groundOverlay == null ? null : () => _changeZIndex(), + child: const Text('change zIndex'), + ), + TextButton( + onPressed: _groundOverlay == null ? null : () => _changeAnchor(), + child: const Text('change anchor'), + ), + TextButton( + onPressed: _groundOverlay == null ? null : () => _changeType(), + child: Text(_placingType == _GroundOverlayPlacing.position + ? 'use bounds' + : 'use position'), + ), + TextButton( + onPressed: _placingType != _GroundOverlayPlacing.position || + _groundOverlay == null + ? null + : () => _changePosition(), + child: const Text('change position'), + ), + TextButton( + onPressed: _placingType != _GroundOverlayPlacing.position || + _groundOverlay == null + ? null + : () => _changeDimensions(), + child: const Text('change dimensions'), + ), + TextButton( + onPressed: _placingType != _GroundOverlayPlacing.bounds || + _groundOverlay == null + ? null + : () => _changeBounds(), + child: const Text('change bounds'), + ), + ], + ), + ], + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/main.dart index 30665c1be23d..5261d84beac3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/main.dart @@ -10,6 +10,7 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf import 'animate_camera.dart'; import 'clustering.dart'; +import 'ground_overlay.dart'; import 'lite_mode.dart'; import 'map_click.dart'; import 'map_coordinates.dart'; @@ -43,6 +44,7 @@ final List _allPages = [ const SnapshotPage(), const LiteModePage(), const TileOverlayPage(), + const GroundOverlayPage(), const ClusteringPage(), const MapIdPage(), ]; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml index 4aac17d849b7..8ed76b3155b8 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - google_maps_flutter_platform_interface: ^2.9.2 + google_maps_flutter_platform_interface: ^2.10.0 dev_dependencies: build_runner: ^2.1.10 diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/test/fake_google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/test/fake_google_maps_flutter_platform.dart index 9ac70ab760fe..6b3b759b119e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/test/fake_google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/test/fake_google_maps_flutter_platform.dart @@ -103,6 +103,15 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { await _fakeDelay(); } + @override + Future updateGroundOverlays( + GroundOverlayUpdates groundOverlayUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.groundOverlayUpdates.add(groundOverlayUpdates); + await _fakeDelay(); + } + @override Future clearTileCache( TileOverlayId tileOverlayId, { @@ -240,6 +249,11 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { return mapEventStreamController.stream.whereType(); } + @override + Stream onGroundOverlayTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + @override Stream onTap({required int mapId}) { return mapEventStreamController.stream.whereType(); @@ -298,6 +312,8 @@ class PlatformMapStateRecorder { }) { clusterManagerUpdates.add(ClusterManagerUpdates.from( const {}, mapObjects.clusterManagers)); + groundOverlayUpdates.add(GroundOverlayUpdates.from( + const {}, mapObjects.groundOverlays)); markerUpdates.add(MarkerUpdates.from(const {}, mapObjects.markers)); polygonUpdates .add(PolygonUpdates.from(const {}, mapObjects.polygons)); @@ -318,4 +334,6 @@ class PlatformMapStateRecorder { final List> tileOverlaySets = >[]; final List clusterManagerUpdates = []; + final List groundOverlayUpdates = + []; } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart index 76b8a8ce75d0..3682398b0350 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; + import 'package:flutter/foundation.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; @@ -78,6 +80,61 @@ class GoogleMapsInspectorAndroid extends GoogleMapsInspectorPlatform { @override bool supportsGettingHeatmapInfo() => false; + @override + bool supportsGettingGroundOverlayInfo() => true; + + @override + Future getGroundOverlayInfo(GroundOverlayId groundOverlayId, + {required int mapId}) async { + final PlatformGroundOverlay? groundOverlayInfo = + await _inspectorProvider(mapId)! + .getGroundOverlayInfo(groundOverlayId.value); + + if (groundOverlayInfo == null) { + return null; + } + + // Create dummy image to represent the image of the ground overlay. + final BytesMapBitmap dummyImage = BytesMapBitmap( + Uint8List.fromList([0]), + bitmapScaling: MapBitmapScaling.none, + ); + + if (groundOverlayInfo.position != null) { + return GroundOverlay.fromPosition( + groundOverlayId: groundOverlayId, + position: LatLng(groundOverlayInfo.position!.latitude, + groundOverlayInfo.position!.longitude), + image: dummyImage, + width: groundOverlayInfo.width, + height: groundOverlayInfo.height, + zIndex: groundOverlayInfo.zIndex, + bearing: groundOverlayInfo.bearing, + transparency: groundOverlayInfo.transparency, + visible: groundOverlayInfo.visible, + clickable: groundOverlayInfo.clickable, + anchor: + Offset(groundOverlayInfo.anchor!.x, groundOverlayInfo.anchor!.y), + ); + } else if (groundOverlayInfo.bounds != null) { + return GroundOverlay.fromBounds( + groundOverlayId: groundOverlayId, + bounds: LatLngBounds( + southwest: LatLng(groundOverlayInfo.bounds!.southwest.latitude, + groundOverlayInfo.bounds!.southwest.longitude), + northeast: LatLng(groundOverlayInfo.bounds!.northeast.latitude, + groundOverlayInfo.bounds!.northeast.longitude)), + image: dummyImage, + zIndex: groundOverlayInfo.zIndex, + bearing: groundOverlayInfo.bearing, + transparency: groundOverlayInfo.transparency, + visible: groundOverlayInfo.visible, + clickable: groundOverlayInfo.clickable, + ); + } + return null; + } + @override Future isCompassEnabled({required int mapId}) async { return _inspectorProvider(mapId)!.isCompassEnabled(); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart index dd19bed86468..816327a646c7 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart @@ -204,6 +204,11 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { return _events(mapId).whereType(); } + @override + Stream onGroundOverlayTap({required int mapId}) { + return _events(mapId).whereType(); + } + @override Stream onTap({required int mapId}) { return _events(mapId).whereType(); @@ -348,6 +353,30 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { ); } + @override + Future updateGroundOverlays( + GroundOverlayUpdates groundOverlayUpdates, { + required int mapId, + }) { + assert( + groundOverlayUpdates.groundOverlaysToAdd.every( + (GroundOverlay groundOverlay) => + groundOverlay.position == null || groundOverlay.width != null), + 'On Android width must be set when position is set for ground overlays.'); + + return _hostApi(mapId).updateGroundOverlays( + groundOverlayUpdates.groundOverlaysToAdd + .map(_platformGroundOverlayFromGroundOverlay) + .toList(), + groundOverlayUpdates.groundOverlaysToChange + .map(_platformGroundOverlayFromGroundOverlay) + .toList(), + groundOverlayUpdates.groundOverlayIdsToRemove + .map((GroundOverlayId id) => id.value) + .toList(), + ); + } + @override Future clearTileCache( TileOverlayId tileOverlayId, { @@ -506,6 +535,11 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { required MapWidgetConfiguration widgetConfiguration, MapObjects mapObjects = const MapObjects(), }) { + assert( + mapObjects.groundOverlays.every((GroundOverlay groundOverlay) => + groundOverlay.position == null || groundOverlay.width != null), + 'On Android width must be set when position is set for ground overlays.'); + final PlatformMapViewCreationParams creationParams = PlatformMapViewCreationParams( initialCameraPosition: _platformCameraPositionFromCameraPosition( @@ -527,6 +561,9 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { initialClusterManagers: mapObjects.clusterManagers .map(_platformClusterManagerFromClusterManager) .toList(), + initialGroundOverlays: mapObjects.groundOverlays + .map(_platformGroundOverlayFromGroundOverlay) + .toList(), ); const String viewType = 'plugins.flutter.dev/google_maps_android'; @@ -749,6 +786,28 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { ); } + static PlatformGroundOverlay _platformGroundOverlayFromGroundOverlay( + GroundOverlay groundOverlay) { + return PlatformGroundOverlay( + groundOverlayId: groundOverlay.groundOverlayId.value, + anchor: groundOverlay.anchor != null + ? _platformPairFromOffset(groundOverlay.anchor!) + : null, + image: platformBitmapFromBitmapDescriptor(groundOverlay.image), + position: groundOverlay.position != null + ? _platformLatLngFromLatLng(groundOverlay.position!) + : null, + bounds: _platformLatLngBoundsFromLatLngBounds(groundOverlay.bounds), + visible: groundOverlay.visible, + zIndex: groundOverlay.zIndex, + bearing: groundOverlay.bearing, + clickable: groundOverlay.clickable, + transparency: groundOverlay.transparency, + width: groundOverlay.width, + height: groundOverlay.height, + ); + } + static PlatformPolygon _platformPolygonFromPolygon(Polygon polygon) { final List points = polygon.points.map(_platformLatLngFromLatLng).toList(); @@ -1081,6 +1140,12 @@ class HostMapMessageHandler implements MapsCallbackApi { streamController.add(PolylineTapEvent(mapId, PolylineId(polylineId))); } + @override + void onGroundOverlayTap(String groundOverlayId) { + streamController + .add(GroundOverlayTapEvent(mapId, GroundOverlayId(groundOverlayId))); + } + @override void onTap(PlatformLatLng position) { streamController diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart index d5cc589f22e1..c8f27bd5d677 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.6.0), do not edit directly. +// Autogenerated from Pigeon (v22.7.3), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -999,6 +999,83 @@ class PlatformCluster { } } +/// Pigeon equivalent of the GroundOverlay class. +class PlatformGroundOverlay { + PlatformGroundOverlay({ + required this.groundOverlayId, + required this.image, + this.position, + this.bounds, + this.width, + this.height, + this.anchor, + required this.transparency, + required this.bearing, + required this.zIndex, + required this.visible, + required this.clickable, + }); + + String groundOverlayId; + + PlatformBitmap image; + + PlatformLatLng? position; + + PlatformLatLngBounds? bounds; + + double? width; + + double? height; + + PlatformDoublePair? anchor; + + double transparency; + + double bearing; + + int zIndex; + + bool visible; + + bool clickable; + + Object encode() { + return [ + groundOverlayId, + image, + position, + bounds, + width, + height, + anchor, + transparency, + bearing, + zIndex, + visible, + clickable, + ]; + } + + static PlatformGroundOverlay decode(Object result) { + result as List; + return PlatformGroundOverlay( + groundOverlayId: result[0]! as String, + image: result[1]! as PlatformBitmap, + position: result[2] as PlatformLatLng?, + bounds: result[3] as PlatformLatLngBounds?, + width: result[4] as double?, + height: result[5] as double?, + anchor: result[6] as PlatformDoublePair?, + transparency: result[7]! as double, + bearing: result[8]! as double, + zIndex: result[9]! as int, + visible: result[10]! as bool, + clickable: result[11]! as bool, + ); + } +} + /// Pigeon equivalent of CameraTargetBounds. /// /// As with the Dart version, it exists to distinguish between not setting a @@ -1036,6 +1113,7 @@ class PlatformMapViewCreationParams { required this.initialHeatmaps, required this.initialTileOverlays, required this.initialClusterManagers, + required this.initialGroundOverlays, }); PlatformCameraPosition initialCameraPosition; @@ -1056,6 +1134,8 @@ class PlatformMapViewCreationParams { List initialClusterManagers; + List initialGroundOverlays; + Object encode() { return [ initialCameraPosition, @@ -1067,6 +1147,7 @@ class PlatformMapViewCreationParams { initialHeatmaps, initialTileOverlays, initialClusterManagers, + initialGroundOverlays, ]; } @@ -1084,6 +1165,8 @@ class PlatformMapViewCreationParams { (result[7] as List?)!.cast(), initialClusterManagers: (result[8] as List?)!.cast(), + initialGroundOverlays: + (result[9] as List?)!.cast(), ); } } @@ -1628,45 +1711,48 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is PlatformCluster) { buffer.putUint8(160); writeValue(buffer, value.encode()); - } else if (value is PlatformCameraTargetBounds) { + } else if (value is PlatformGroundOverlay) { buffer.putUint8(161); writeValue(buffer, value.encode()); - } else if (value is PlatformMapViewCreationParams) { + } else if (value is PlatformCameraTargetBounds) { buffer.putUint8(162); writeValue(buffer, value.encode()); - } else if (value is PlatformMapConfiguration) { + } else if (value is PlatformMapViewCreationParams) { buffer.putUint8(163); writeValue(buffer, value.encode()); - } else if (value is PlatformPoint) { + } else if (value is PlatformMapConfiguration) { buffer.putUint8(164); writeValue(buffer, value.encode()); - } else if (value is PlatformTileLayer) { + } else if (value is PlatformPoint) { buffer.putUint8(165); writeValue(buffer, value.encode()); - } else if (value is PlatformZoomRange) { + } else if (value is PlatformTileLayer) { buffer.putUint8(166); writeValue(buffer, value.encode()); - } else if (value is PlatformBitmap) { + } else if (value is PlatformZoomRange) { buffer.putUint8(167); writeValue(buffer, value.encode()); - } else if (value is PlatformBitmapDefaultMarker) { + } else if (value is PlatformBitmap) { buffer.putUint8(168); writeValue(buffer, value.encode()); - } else if (value is PlatformBitmapBytes) { + } else if (value is PlatformBitmapDefaultMarker) { buffer.putUint8(169); writeValue(buffer, value.encode()); - } else if (value is PlatformBitmapAsset) { + } else if (value is PlatformBitmapBytes) { buffer.putUint8(170); writeValue(buffer, value.encode()); - } else if (value is PlatformBitmapAssetImage) { + } else if (value is PlatformBitmapAsset) { buffer.putUint8(171); writeValue(buffer, value.encode()); - } else if (value is PlatformBitmapAssetMap) { + } else if (value is PlatformBitmapAssetImage) { buffer.putUint8(172); writeValue(buffer, value.encode()); - } else if (value is PlatformBitmapBytesMap) { + } else if (value is PlatformBitmapAssetMap) { buffer.putUint8(173); writeValue(buffer, value.encode()); + } else if (value is PlatformBitmapBytesMap) { + buffer.putUint8(174); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1746,30 +1832,32 @@ class _PigeonCodec extends StandardMessageCodec { case 160: return PlatformCluster.decode(readValue(buffer)!); case 161: - return PlatformCameraTargetBounds.decode(readValue(buffer)!); + return PlatformGroundOverlay.decode(readValue(buffer)!); case 162: - return PlatformMapViewCreationParams.decode(readValue(buffer)!); + return PlatformCameraTargetBounds.decode(readValue(buffer)!); case 163: - return PlatformMapConfiguration.decode(readValue(buffer)!); + return PlatformMapViewCreationParams.decode(readValue(buffer)!); case 164: - return PlatformPoint.decode(readValue(buffer)!); + return PlatformMapConfiguration.decode(readValue(buffer)!); case 165: - return PlatformTileLayer.decode(readValue(buffer)!); + return PlatformPoint.decode(readValue(buffer)!); case 166: - return PlatformZoomRange.decode(readValue(buffer)!); + return PlatformTileLayer.decode(readValue(buffer)!); case 167: - return PlatformBitmap.decode(readValue(buffer)!); + return PlatformZoomRange.decode(readValue(buffer)!); case 168: - return PlatformBitmapDefaultMarker.decode(readValue(buffer)!); + return PlatformBitmap.decode(readValue(buffer)!); case 169: - return PlatformBitmapBytes.decode(readValue(buffer)!); + return PlatformBitmapDefaultMarker.decode(readValue(buffer)!); case 170: - return PlatformBitmapAsset.decode(readValue(buffer)!); + return PlatformBitmapBytes.decode(readValue(buffer)!); case 171: - return PlatformBitmapAssetImage.decode(readValue(buffer)!); + return PlatformBitmapAsset.decode(readValue(buffer)!); case 172: - return PlatformBitmapAssetMap.decode(readValue(buffer)!); + return PlatformBitmapAssetImage.decode(readValue(buffer)!); case 173: + return PlatformBitmapAssetMap.decode(readValue(buffer)!); + case 174: return PlatformBitmapBytesMap.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -2030,6 +2118,32 @@ class MapsApi { } } + /// Updates the set of ground overlays on the map. + Future updateGroundOverlays(List toAdd, + List toChange, List idsToRemove) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_maps_flutter_android.MapsApi.updateGroundOverlays$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = await pigeonVar_channel + .send([toAdd, toChange, idsToRemove]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + /// Gets the screen coordinate for the given map location. Future getScreenCoordinate(PlatformLatLng latLng) async { final String pigeonVar_channelName = @@ -2451,6 +2565,9 @@ abstract class MapsCallbackApi { /// Called when a polyline is tapped. void onPolylineTap(String polylineId); + /// Called when a ground overlay is tapped. + void onGroundOverlayTap(String groundOverlayId); + /// Called to get data for a map tile. Future getTileOverlayTile( String tileOverlayId, PlatformPoint location, int zoom); @@ -2866,6 +2983,35 @@ abstract class MapsCallbackApi { }); } } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.google_maps_flutter_android.MapsCallbackApi.onGroundOverlayTap$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.google_maps_flutter_android.MapsCallbackApi.onGroundOverlayTap was null.'); + final List args = (message as List?)!; + final String? arg_groundOverlayId = (args[0] as String?); + assert(arg_groundOverlayId != null, + 'Argument for dev.flutter.pigeon.google_maps_flutter_android.MapsCallbackApi.onGroundOverlayTap was null, expected non-null String.'); + try { + api.onGroundOverlayTap(arg_groundOverlayId!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } { final BasicMessageChannel< Object?> pigeonVar_channel = BasicMessageChannel< @@ -3355,6 +3501,31 @@ class MapsInspectorApi { } } + Future getGroundOverlayInfo( + String groundOverlayId) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_maps_flutter_android.MapsInspectorApi.getGroundOverlayInfo$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = await pigeonVar_channel + .send([groundOverlayId]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return (pigeonVar_replyList[0] as PlatformGroundOverlay?); + } + } + Future getZoomRange() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.google_maps_flutter_android.MapsInspectorApi.getZoomRange$pigeonVar_messageChannelSuffix'; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart b/packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart index 9db93e753140..b6c9a26c050a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart @@ -385,6 +385,37 @@ class PlatformCluster { final List markerIds; } +/// Pigeon equivalent of the GroundOverlay class. +class PlatformGroundOverlay { + PlatformGroundOverlay({ + required this.groundOverlayId, + required this.image, + required this.position, + required this.bounds, + required this.width, + required this.height, + required this.anchor, + required this.transparency, + required this.bearing, + required this.zIndex, + required this.visible, + required this.clickable, + }); + + final String groundOverlayId; + final PlatformBitmap image; + final PlatformLatLng? position; + final PlatformLatLngBounds? bounds; + final double? width; + final double? height; + final PlatformDoublePair? anchor; + final double transparency; + final double bearing; + final int zIndex; + final bool visible; + final bool clickable; +} + /// Pigeon equivalent of CameraTargetBounds. /// /// As with the Dart version, it exists to distinguish between not setting a @@ -407,6 +438,7 @@ class PlatformMapViewCreationParams { required this.initialHeatmaps, required this.initialTileOverlays, required this.initialClusterManagers, + required this.initialGroundOverlays, }); final PlatformCameraPosition initialCameraPosition; @@ -418,6 +450,7 @@ class PlatformMapViewCreationParams { final List initialHeatmaps; final List initialTileOverlays; final List initialClusterManagers; + final List initialGroundOverlays; } /// Pigeon equivalent of MapConfiguration. @@ -631,6 +664,10 @@ abstract class MapsApi { void updateTileOverlays(List toAdd, List toChange, List idsToRemove); + /// Updates the set of ground overlays on the map. + void updateGroundOverlays(List toAdd, + List toChange, List idsToRemove); + /// Gets the screen coordinate for the given map location. PlatformPoint getScreenCoordinate(PlatformLatLng latLng); @@ -726,6 +763,9 @@ abstract class MapsCallbackApi { /// Called when a polyline is tapped. void onPolylineTap(String polylineId); + /// Called when a ground overlay is tapped. + void onGroundOverlayTap(String groundOverlayId); + /// Called to get data for a map tile. @async PlatformTile getTileOverlayTile( @@ -770,6 +810,7 @@ abstract class MapsInspectorApi { bool isMyLocationButtonEnabled(); bool isTrafficEnabled(); PlatformTileLayer? getTileOverlayInfo(String tileOverlayId); + PlatformGroundOverlay? getGroundOverlayInfo(String groundOverlayId); PlatformZoomRange getZoomRange(); List getClusters(String clusterManagerId); } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml index 19db56e19133..10a2517ddf5b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_android description: Android implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.14.12 +version: 2.15.0 environment: sdk: ^3.5.0 @@ -21,7 +21,7 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 - google_maps_flutter_platform_interface: ^2.9.5 + google_maps_flutter_platform_interface: ^2.10.0 stream_transform: ^2.0.0 dev_dependencies: diff --git a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart index a12c7169780f..b9bfc945e37a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart @@ -625,6 +625,161 @@ void main() { expectTileOverlay(toAdd.first, object3); }); + test('updateGroundOverlays passes expected arguments', () async { + const int mapId = 1; + final (GoogleMapsFlutterAndroid maps, MockMapsApi api) = + setUpMockMap(mapId: mapId); + + final AssetMapBitmap image = AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ); + + final GroundOverlay object1 = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('1'), + bounds: LatLngBounds( + southwest: const LatLng(10, 20), northeast: const LatLng(30, 40)), + image: image); + final GroundOverlay object2old = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('2'), + bounds: LatLngBounds( + southwest: const LatLng(10, 20), northeast: const LatLng(30, 40)), + image: image); + final GroundOverlay object2new = object2old.copyWith( + visibleParam: false, + bearingParam: 10, + clickableParam: false, + transparencyParam: 0.5, + zIndexParam: 100, + ); + final GroundOverlay object3 = GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('3'), + position: const LatLng(10, 20), + width: 100, + image: image, + ); + await maps.updateGroundOverlays( + GroundOverlayUpdates.from({object1, object2old}, + {object2new, object3}), + mapId: mapId); + + final VerificationResult verification = + verify(api.updateGroundOverlays(captureAny, captureAny, captureAny)); + + final List toAdd = + verification.captured[0] as List; + final List toChange = + verification.captured[1] as List; + final List toRemove = verification.captured[2] as List; + // Object one should be removed. + expect(toRemove.length, 1); + expect(toRemove.first, object1.groundOverlayId.value); + // Object two should be changed. + { + expect(toChange.length, 1); + final PlatformGroundOverlay firstChanged = toChange.first; + expect(firstChanged.anchor?.x, object2new.anchor?.dx); + expect(firstChanged.anchor?.y, object2new.anchor?.dy); + expect(firstChanged.bearing, object2new.bearing); + expect(firstChanged.bounds?.northeast.latitude, + object2new.bounds?.northeast.latitude); + expect(firstChanged.bounds?.northeast.longitude, + object2new.bounds?.northeast.longitude); + expect(firstChanged.bounds?.southwest.latitude, + object2new.bounds?.southwest.latitude); + expect(firstChanged.bounds?.southwest.longitude, + object2new.bounds?.southwest.longitude); + expect(firstChanged.visible, object2new.visible); + expect(firstChanged.clickable, object2new.clickable); + expect(firstChanged.zIndex, object2new.zIndex); + expect(firstChanged.position?.latitude, object2new.position?.latitude); + expect(firstChanged.position?.longitude, object2new.position?.longitude); + expect(firstChanged.width, object2new.width); + expect(firstChanged.height, object2new.height); + expect(firstChanged.transparency, object2new.transparency); + expect( + firstChanged.image.bitmap.runtimeType, + GoogleMapsFlutterAndroid.platformBitmapFromBitmapDescriptor( + object2new.image) + .bitmap + .runtimeType); + } + // Object three should be added. + { + expect(toAdd.length, 1); + final PlatformGroundOverlay firstAdded = toAdd.first; + expect(firstAdded.anchor?.x, object3.anchor?.dx); + expect(firstAdded.anchor?.y, object3.anchor?.dy); + expect(firstAdded.bearing, object3.bearing); + expect(firstAdded.bounds?.northeast.latitude, + object3.bounds?.northeast.latitude); + expect(firstAdded.bounds?.northeast.longitude, + object3.bounds?.northeast.longitude); + expect(firstAdded.bounds?.southwest.latitude, + object3.bounds?.southwest.latitude); + expect(firstAdded.bounds?.southwest.longitude, + object3.bounds?.southwest.longitude); + expect(firstAdded.visible, object3.visible); + expect(firstAdded.clickable, object3.clickable); + expect(firstAdded.zIndex, object3.zIndex); + expect(firstAdded.position?.latitude, object3.position?.latitude); + expect(firstAdded.position?.longitude, object3.position?.longitude); + expect(firstAdded.width, object3.width); + expect(firstAdded.height, object3.height); + expect(firstAdded.transparency, object3.transparency); + expect( + firstAdded.image.bitmap.runtimeType, + GoogleMapsFlutterAndroid.platformBitmapFromBitmapDescriptor( + object3.image) + .bitmap + .runtimeType); + } + }); + + test( + 'updateGroundOverlays throws assertion error on unsupported ground overlays', + () async { + const int mapId = 1; + final (GoogleMapsFlutterAndroid maps, MockMapsApi api) = + setUpMockMap(mapId: mapId); + + final AssetMapBitmap image = AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ); + + final GroundOverlay groundOverlay = GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('1'), + position: const LatLng(10, 20), + // Assert should be thrown because width is not set for position-based + // ground overlay on Android. + // ignore: avoid_redundant_argument_values + width: null, + image: image, + ); + + expect( + () async => maps.updateGroundOverlays( + GroundOverlayUpdates.from( + const {}, {groundOverlay}), + mapId: mapId), + throwsAssertionError, + ); + + expect( + () async => maps.buildViewWithConfiguration(1, (int _) {}, + widgetConfiguration: const MapWidgetConfiguration( + initialCameraPosition: CameraPosition(target: LatLng(0, 0)), + textDirection: TextDirection.ltr, + ), + mapObjects: + MapObjects(groundOverlays: {groundOverlay})), + throwsAssertionError, + ); + }); + test('markers send drag event to correct streams', () async { const int mapId = 1; const String dragStartId = 'drag-start-marker'; @@ -761,6 +916,24 @@ void main() { expect((await stream.next).value.value, equals(objectId)); }); + test('ground overlays send tap events to correct stream', () async { + const int mapId = 1; + const String objectId = 'object-id'; + + final GoogleMapsFlutterAndroid maps = GoogleMapsFlutterAndroid(); + final HostMapMessageHandler callbackHandler = + maps.ensureHandlerInitialized(mapId); + + final StreamQueue stream = + StreamQueue( + maps.onGroundOverlayTap(mapId: mapId)); + + // Simulate message from the native side. + callbackHandler.onGroundOverlayTap(objectId); + + expect((await stream.next).value.value, equals(objectId)); + }); + test( 'Does not use PlatformViewLink when using TLHC', () async { diff --git a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.mocks.dart index e0fbdd574215..cd9ebcb887b2 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.mocks.dart @@ -3,11 +3,12 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i3; -import 'dart:typed_data' as _i4; +import 'dart:async' as _i4; +import 'dart:typed_data' as _i5; import 'package:google_maps_flutter_android/src/messages.g.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i3; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -59,32 +60,45 @@ class _FakePlatformLatLngBounds_2 extends _i1.SmartFake /// See the documentation for Mockito's code generation for more information. class MockMapsApi extends _i1.Mock implements _i2.MapsApi { @override - _i3.Future waitForMap() => (super.noSuchMethod( + String get pigeonVar_messageChannelSuffix => (super.noSuchMethod( + Invocation.getter(#pigeonVar_messageChannelSuffix), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#pigeonVar_messageChannelSuffix), + ), + returnValueForMissingStub: _i3.dummyValue( + this, + Invocation.getter(#pigeonVar_messageChannelSuffix), + ), + ) as String); + + @override + _i4.Future waitForMap() => (super.noSuchMethod( Invocation.method( #waitForMap, [], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future updateMapConfiguration( + _i4.Future updateMapConfiguration( _i2.PlatformMapConfiguration? configuration) => (super.noSuchMethod( Invocation.method( #updateMapConfiguration, [configuration], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future updateCircles( - List<_i2.PlatformCircle?>? toAdd, - List<_i2.PlatformCircle?>? toChange, - List? idsToRemove, + _i4.Future updateCircles( + List<_i2.PlatformCircle>? toAdd, + List<_i2.PlatformCircle>? toChange, + List? idsToRemove, ) => (super.noSuchMethod( Invocation.method( @@ -95,15 +109,15 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { idsToRemove, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future updateHeatmaps( - List<_i2.PlatformHeatmap?>? toAdd, - List<_i2.PlatformHeatmap?>? toChange, - List? idsToRemove, + _i4.Future updateHeatmaps( + List<_i2.PlatformHeatmap>? toAdd, + List<_i2.PlatformHeatmap>? toChange, + List? idsToRemove, ) => (super.noSuchMethod( Invocation.method( @@ -114,14 +128,14 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { idsToRemove, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future updateClusterManagers( - List<_i2.PlatformClusterManager?>? toAdd, - List? idsToRemove, + _i4.Future updateClusterManagers( + List<_i2.PlatformClusterManager>? toAdd, + List? idsToRemove, ) => (super.noSuchMethod( Invocation.method( @@ -131,15 +145,15 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { idsToRemove, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future updateMarkers( - List<_i2.PlatformMarker?>? toAdd, - List<_i2.PlatformMarker?>? toChange, - List? idsToRemove, + _i4.Future updateMarkers( + List<_i2.PlatformMarker>? toAdd, + List<_i2.PlatformMarker>? toChange, + List? idsToRemove, ) => (super.noSuchMethod( Invocation.method( @@ -150,15 +164,15 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { idsToRemove, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future updatePolygons( - List<_i2.PlatformPolygon?>? toAdd, - List<_i2.PlatformPolygon?>? toChange, - List? idsToRemove, + _i4.Future updatePolygons( + List<_i2.PlatformPolygon>? toAdd, + List<_i2.PlatformPolygon>? toChange, + List? idsToRemove, ) => (super.noSuchMethod( Invocation.method( @@ -169,15 +183,15 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { idsToRemove, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future updatePolylines( - List<_i2.PlatformPolyline?>? toAdd, - List<_i2.PlatformPolyline?>? toChange, - List? idsToRemove, + _i4.Future updatePolylines( + List<_i2.PlatformPolyline>? toAdd, + List<_i2.PlatformPolyline>? toChange, + List? idsToRemove, ) => (super.noSuchMethod( Invocation.method( @@ -188,15 +202,15 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { idsToRemove, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future updateTileOverlays( - List<_i2.PlatformTileOverlay?>? toAdd, - List<_i2.PlatformTileOverlay?>? toChange, - List? idsToRemove, + _i4.Future updateTileOverlays( + List<_i2.PlatformTileOverlay>? toAdd, + List<_i2.PlatformTileOverlay>? toChange, + List? idsToRemove, ) => (super.noSuchMethod( Invocation.method( @@ -207,19 +221,38 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { idsToRemove, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + _i4.Future updateGroundOverlays( + List<_i2.PlatformGroundOverlay>? toAdd, + List<_i2.PlatformGroundOverlay>? toChange, + List? idsToRemove, + ) => + (super.noSuchMethod( + Invocation.method( + #updateGroundOverlays, + [ + toAdd, + toChange, + idsToRemove, + ], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future<_i2.PlatformPoint> getScreenCoordinate( + _i4.Future<_i2.PlatformPoint> getScreenCoordinate( _i2.PlatformLatLng? latLng) => (super.noSuchMethod( Invocation.method( #getScreenCoordinate, [latLng], ), - returnValue: _i3.Future<_i2.PlatformPoint>.value(_FakePlatformPoint_0( + returnValue: _i4.Future<_i2.PlatformPoint>.value(_FakePlatformPoint_0( this, Invocation.method( #getScreenCoordinate, @@ -227,24 +260,24 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { ), )), returnValueForMissingStub: - _i3.Future<_i2.PlatformPoint>.value(_FakePlatformPoint_0( + _i4.Future<_i2.PlatformPoint>.value(_FakePlatformPoint_0( this, Invocation.method( #getScreenCoordinate, [latLng], ), )), - ) as _i3.Future<_i2.PlatformPoint>); + ) as _i4.Future<_i2.PlatformPoint>); @override - _i3.Future<_i2.PlatformLatLng> getLatLng( + _i4.Future<_i2.PlatformLatLng> getLatLng( _i2.PlatformPoint? screenCoordinate) => (super.noSuchMethod( Invocation.method( #getLatLng, [screenCoordinate], ), - returnValue: _i3.Future<_i2.PlatformLatLng>.value(_FakePlatformLatLng_1( + returnValue: _i4.Future<_i2.PlatformLatLng>.value(_FakePlatformLatLng_1( this, Invocation.method( #getLatLng, @@ -252,23 +285,23 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { ), )), returnValueForMissingStub: - _i3.Future<_i2.PlatformLatLng>.value(_FakePlatformLatLng_1( + _i4.Future<_i2.PlatformLatLng>.value(_FakePlatformLatLng_1( this, Invocation.method( #getLatLng, [screenCoordinate], ), )), - ) as _i3.Future<_i2.PlatformLatLng>); + ) as _i4.Future<_i2.PlatformLatLng>); @override - _i3.Future<_i2.PlatformLatLngBounds> getVisibleRegion() => + _i4.Future<_i2.PlatformLatLngBounds> getVisibleRegion() => (super.noSuchMethod( Invocation.method( #getVisibleRegion, [], ), - returnValue: _i3.Future<_i2.PlatformLatLngBounds>.value( + returnValue: _i4.Future<_i2.PlatformLatLngBounds>.value( _FakePlatformLatLngBounds_2( this, Invocation.method( @@ -276,7 +309,7 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { [], ), )), - returnValueForMissingStub: _i3.Future<_i2.PlatformLatLngBounds>.value( + returnValueForMissingStub: _i4.Future<_i2.PlatformLatLngBounds>.value( _FakePlatformLatLngBounds_2( this, Invocation.method( @@ -284,108 +317,108 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { [], ), )), - ) as _i3.Future<_i2.PlatformLatLngBounds>); + ) as _i4.Future<_i2.PlatformLatLngBounds>); @override - _i3.Future moveCamera(_i2.PlatformCameraUpdate? cameraUpdate) => + _i4.Future moveCamera(_i2.PlatformCameraUpdate? cameraUpdate) => (super.noSuchMethod( Invocation.method( #moveCamera, [cameraUpdate], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future animateCamera(_i2.PlatformCameraUpdate? cameraUpdate) => + _i4.Future animateCamera(_i2.PlatformCameraUpdate? cameraUpdate) => (super.noSuchMethod( Invocation.method( #animateCamera, [cameraUpdate], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future getZoomLevel() => (super.noSuchMethod( + _i4.Future getZoomLevel() => (super.noSuchMethod( Invocation.method( #getZoomLevel, [], ), - returnValue: _i3.Future.value(0.0), - returnValueForMissingStub: _i3.Future.value(0.0), - ) as _i3.Future); + returnValue: _i4.Future.value(0.0), + returnValueForMissingStub: _i4.Future.value(0.0), + ) as _i4.Future); @override - _i3.Future showInfoWindow(String? markerId) => (super.noSuchMethod( + _i4.Future showInfoWindow(String? markerId) => (super.noSuchMethod( Invocation.method( #showInfoWindow, [markerId], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future hideInfoWindow(String? markerId) => (super.noSuchMethod( + _i4.Future hideInfoWindow(String? markerId) => (super.noSuchMethod( Invocation.method( #hideInfoWindow, [markerId], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future isInfoWindowShown(String? markerId) => (super.noSuchMethod( + _i4.Future isInfoWindowShown(String? markerId) => (super.noSuchMethod( Invocation.method( #isInfoWindowShown, [markerId], ), - returnValue: _i3.Future.value(false), - returnValueForMissingStub: _i3.Future.value(false), - ) as _i3.Future); + returnValue: _i4.Future.value(false), + returnValueForMissingStub: _i4.Future.value(false), + ) as _i4.Future); @override - _i3.Future setStyle(String? style) => (super.noSuchMethod( + _i4.Future setStyle(String? style) => (super.noSuchMethod( Invocation.method( #setStyle, [style], ), - returnValue: _i3.Future.value(false), - returnValueForMissingStub: _i3.Future.value(false), - ) as _i3.Future); + returnValue: _i4.Future.value(false), + returnValueForMissingStub: _i4.Future.value(false), + ) as _i4.Future); @override - _i3.Future didLastStyleSucceed() => (super.noSuchMethod( + _i4.Future didLastStyleSucceed() => (super.noSuchMethod( Invocation.method( #didLastStyleSucceed, [], ), - returnValue: _i3.Future.value(false), - returnValueForMissingStub: _i3.Future.value(false), - ) as _i3.Future); + returnValue: _i4.Future.value(false), + returnValueForMissingStub: _i4.Future.value(false), + ) as _i4.Future); @override - _i3.Future clearTileCache(String? tileOverlayId) => (super.noSuchMethod( + _i4.Future clearTileCache(String? tileOverlayId) => (super.noSuchMethod( Invocation.method( #clearTileCache, [tileOverlayId], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future<_i4.Uint8List> takeSnapshot() => (super.noSuchMethod( + _i4.Future<_i5.Uint8List> takeSnapshot() => (super.noSuchMethod( Invocation.method( #takeSnapshot, [], ), - returnValue: _i3.Future<_i4.Uint8List>.value(_i4.Uint8List(0)), + returnValue: _i4.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), returnValueForMissingStub: - _i3.Future<_i4.Uint8List>.value(_i4.Uint8List(0)), - ) as _i3.Future<_i4.Uint8List>); + _i4.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), + ) as _i4.Future<_i5.Uint8List>); } From 37d71fb4c80f7c556cd3c1102e6a4cc77ef5c2df Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Tue, 4 Feb 2025 19:32:53 +0200 Subject: [PATCH 03/10] [google_maps_flutter_web] Adds support for ground overlay --- .../google_maps_flutter_web/CHANGELOG.md | 3 +- .../google_maps_controller_test.dart | 82 ++++++++++++++ .../google_maps_controller_test.mocks.dart | 84 ++++++++++++++ .../google_maps_plugin_test.mocks.dart | 12 ++ .../marker_clustering_test.dart | 4 - .../example/pubspec.yaml | 2 +- .../lib/google_maps_flutter_web.dart | 2 + .../lib/src/convert.dart | 34 ++++-- .../lib/src/google_maps_controller.dart | 27 +++++ .../lib/src/google_maps_flutter_web.dart | 14 +++ .../lib/src/google_maps_inspector_web.dart | 49 ++++++++- .../lib/src/ground_overlay.dart | 29 +++++ .../lib/src/ground_overlays.dart | 103 ++++++++++++++++++ .../google_maps_flutter_web/pubspec.yaml | 4 +- 14 files changed, 426 insertions(+), 23 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter_web/lib/src/ground_overlay.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter_web/lib/src/ground_overlays.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 b4239c12870e..eeb309f474e7 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.5.11 +* Adds support for ground overlay. * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. ## 0.5.10 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 be68cdf1c714..dcf5345ae95a 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 @@ -41,6 +41,9 @@ gmaps.Map mapShim() => throw UnimplementedError(); MockSpec( fallbackGenerators: {#googleMap: mapShim}, ), + MockSpec( + fallbackGenerators: {#googleMap: mapShim}, + ), ]) /// Test Google Map Controller @@ -251,6 +254,7 @@ void main() { late MockPolygonsController polygons; late MockPolylinesController polylines; late MockTileOverlaysController tileOverlays; + late MockGroundOverlaysController groundOverlays; late gmaps.Map map; setUp(() { @@ -260,6 +264,7 @@ void main() { polygons = MockPolygonsController(); polylines = MockPolylinesController(); tileOverlays = MockTileOverlaysController(); + groundOverlays = MockGroundOverlaysController(); map = gmaps.Map(createDivElement()); }); @@ -272,6 +277,7 @@ void main() { markers: markers, polygons: polygons, polylines: polylines, + groundOverlays: groundOverlays, ) ..init(); @@ -312,6 +318,7 @@ void main() { polygons: polygons, polylines: polylines, tileOverlays: tileOverlays, + groundOverlays: groundOverlays, ) ..init(); @@ -321,6 +328,7 @@ void main() { verify(polygons.bindToMap(mapId, map)); verify(polylines.bindToMap(mapId, map)); verify(tileOverlays.bindToMap(mapId, map)); + verify(groundOverlays.bindToMap(mapId, map)); }); testWidgets('renders initial geometry', (WidgetTester tester) async { @@ -380,6 +388,22 @@ void main() { ]) }, tileOverlays: { const TileOverlay(tileOverlayId: TileOverlayId('overlay-1')) + }, groundOverlays: { + GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('bounds_1'), + bounds: LatLngBounds( + northeast: const LatLng(100, 0), + southwest: const LatLng(0, 100), + ), + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + ) }); controller = createController(mapObjects: mapObjects) @@ -390,6 +414,7 @@ void main() { polygons: polygons, polylines: polylines, tileOverlays: tileOverlays, + groundOverlays: groundOverlays, ) ..init(); @@ -399,6 +424,7 @@ void main() { verify(polygons.addPolygons(mapObjects.polygons)); verify(polylines.addPolylines(mapObjects.polylines)); verify(tileOverlays.addTileOverlays(mapObjects.tileOverlays)); + verify(groundOverlays.addGroundOverlays(mapObjects.groundOverlays)); }); group('Initialization options', () { @@ -889,6 +915,62 @@ void main() { })); }); + testWidgets('updateGroundOverlays', (WidgetTester tester) async { + final MockGroundOverlaysController mock = + MockGroundOverlaysController(); + controller = createController() + ..debugSetOverrides(groundOverlays: mock); + + final LatLngBounds bounds = LatLngBounds( + northeast: const LatLng(100, 0), + southwest: const LatLng(0, 100), + ); + const LatLng position = LatLng(50, 50); + final AssetMapBitmap image = AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ); + + final GroundOverlay groundOverlayToBeUpdated = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('to-be-updated'), + image: image, + bounds: bounds, + ); + final GroundOverlay groundOverlayToBeRemoved = + GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('to-be-removed'), + image: image, + position: position, + ); + final GroundOverlay groundOverlayToBeAdded = GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('to-be-added'), + image: image, + position: position, + ); + + final Set previous = { + groundOverlayToBeUpdated, + groundOverlayToBeRemoved + }; + + final Set current = { + groundOverlayToBeUpdated.copyWith(visibleParam: false), + groundOverlayToBeAdded + }; + + controller + .updateGroundOverlays(GroundOverlayUpdates.from(previous, current)); + + verify(mock.removeGroundOverlays({ + groundOverlayToBeRemoved.groundOverlayId, + })); + verify(mock.addGroundOverlays({groundOverlayToBeAdded})); + verify(mock.changeGroundOverlays({ + groundOverlayToBeUpdated.copyWith(visibleParam: false), + })); + }); + testWidgets('infoWindow visibility', (WidgetTester tester) async { final MockMarkersController mock = MockMarkersController(); const MarkerId markerId = MarkerId('marker-with-infowindow'); 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 ca64a2a9d7f3..7904b7352cb4 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 @@ -594,3 +594,87 @@ class MockTileOverlaysController extends _i1.Mock returnValueForMissingStub: null, ); } + +/// A class which mocks [GroundOverlaysController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockGroundOverlaysController extends _i1.Mock + implements _i2.GroundOverlaysController { + @override + _i4.Map get googleMap => (super.noSuchMethod( + Invocation.getter(#googleMap), + returnValue: _i5.mapShim(), + returnValueForMissingStub: _i5.mapShim(), + ) as _i4.Map); + + @override + set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( + Invocation.setter( + #googleMap, + _googleMap, + ), + returnValueForMissingStub: null, + ); + + @override + int get mapId => (super.noSuchMethod( + Invocation.getter(#mapId), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + + @override + set mapId(int? _mapId) => super.noSuchMethod( + Invocation.setter( + #mapId, + _mapId, + ), + returnValueForMissingStub: null, + ); + + @override + void addGroundOverlays(Set<_i3.GroundOverlay>? groundOverlaysToAdd) => + super.noSuchMethod( + Invocation.method( + #addGroundOverlays, + [groundOverlaysToAdd], + ), + returnValueForMissingStub: null, + ); + + @override + void changeGroundOverlays(Set<_i3.GroundOverlay>? groundOverlays) => + super.noSuchMethod( + Invocation.method( + #changeGroundOverlays, + [groundOverlays], + ), + returnValueForMissingStub: null, + ); + + @override + void removeGroundOverlays(Set<_i3.GroundOverlayId>? groundOverlayIds) => + super.noSuchMethod( + Invocation.method( + #removeGroundOverlays, + [groundOverlayIds], + ), + returnValueForMissingStub: null, + ); + + @override + void bindToMap( + int? mapId, + _i4.Map? googleMap, + ) => + super.noSuchMethod( + Invocation.method( + #bindToMap, + [ + mapId, + googleMap, + ], + ), + returnValueForMissingStub: null, + ); +} 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 cf5acfcb813c..548250758213 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 @@ -142,6 +142,7 @@ class MockGoogleMapController extends _i1.Mock _i4.PolylinesController? polylines, _i6.ClusterManagersController? clusterManagers, _i4.TileOverlaysController? tileOverlays, + _i4.GroundOverlaysController? groundOverlays, }) => super.noSuchMethod( Invocation.method( @@ -157,6 +158,7 @@ class MockGoogleMapController extends _i1.Mock #polylines: polylines, #clusterManagers: clusterManagers, #tileOverlays: tileOverlays, + #groundOverlays: groundOverlays, }, ), returnValueForMissingStub: null, @@ -339,6 +341,16 @@ class MockGoogleMapController extends _i1.Mock returnValueForMissingStub: null, ); + @override + void updateGroundOverlays(_i2.GroundOverlayUpdates? updates) => + super.noSuchMethod( + Invocation.method( + #updateGroundOverlays, + [updates], + ), + returnValueForMissingStub: null, + ); + @override void updateTileOverlays(Set<_i2.TileOverlay>? newOverlays) => super.noSuchMethod( diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart index c34d9c533117..ce39bea20b77 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart @@ -63,10 +63,6 @@ void main() { addTearDown(() => plugin.dispose(mapId: mapId)); - final LatLng latlon = await plugin - .getLatLng(const ScreenCoordinate(x: 0, y: 0), mapId: mapId); - debugPrint(latlon.toString()); - final List clusters = await waitForValueMatchingPredicate>( tester, 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 e4c0cfabb962..a61f8043548f 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 @@ -8,7 +8,7 @@ environment: dependencies: flutter: sdk: flutter - google_maps_flutter_platform_interface: ^2.9.0 + google_maps_flutter_platform_interface: ^2.10.0 google_maps_flutter_web: path: ../ web: ^1.0.0 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 f56e0b02bd1b..f8b7dd0506b9 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 @@ -35,6 +35,8 @@ part 'src/circles.dart'; part 'src/convert.dart'; part 'src/google_maps_controller.dart'; part 'src/google_maps_flutter_web.dart'; +part 'src/ground_overlay.dart'; +part 'src/ground_overlays.dart'; part 'src/heatmap.dart'; part 'src/heatmaps.dart'; part 'src/marker.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 f6af32046ccd..64ef62cc6d9e 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 @@ -200,6 +200,12 @@ LatLngBounds gmLatLngBoundsTolatLngBounds(gmaps.LatLngBounds latLngBounds) { ); } +/// Converts a [LatLngBounds] into a [gmaps.LatLngBounds]. +gmaps.LatLngBounds latLngBoundsToGmlatLngBounds(LatLngBounds latLngBounds) { + return gmaps.LatLngBounds(_latLngToGmLatLng(latLngBounds.southwest), + _latLngToGmLatLng(latLngBounds.northeast)); +} + CameraPosition _gmViewportToCameraPosition(gmaps.Map map) { return CameraPosition( target: @@ -375,17 +381,7 @@ Future _gmIconFromBitmapDescriptor( gmaps.Icon? icon; if (bitmapDescriptor is MapBitmap) { - final String url = switch (bitmapDescriptor) { - (final BytesMapBitmap bytesMapBitmap) => - _bitmapBlobUrlCache.putIfAbsent(bytesMapBitmap.byteData.hashCode, () { - final Blob blob = - Blob([bytesMapBitmap.byteData.toJS].toJS); - return URL.createObjectURL(blob as JSObject); - }), - (final AssetMapBitmap assetMapBitmap) => - ui_web.assetManager.getAssetUrl(assetMapBitmap.assetName), - _ => throw UnimplementedError(), - }; + final String url = urlFromMapBitmap(bitmapDescriptor); icon = gmaps.Icon()..url = url; @@ -678,6 +674,22 @@ void _applyCameraUpdate(gmaps.Map map, CameraUpdate update) { } } +/// Converts a [MapBitmap] into a URL. +String urlFromMapBitmap(MapBitmap mapBitmap) { + return switch (mapBitmap) { + (final BytesMapBitmap bytesMapBitmap) => + _bitmapBlobUrlCache.putIfAbsent(bytesMapBitmap.byteData.hashCode, () { + final Blob blob = + Blob([bytesMapBitmap.byteData.toJS].toJS); + return URL.createObjectURL(blob as JSObject); + }), + (final AssetMapBitmap assetMapBitmap) => + ui_web.assetManager.getAssetUrl(assetMapBitmap.assetName), + _ => throw UnimplementedError( + 'Only BytesMapBitmap and AssetMapBitmap are supported.'), + }; +} + // original JS by: Byron Singh (https://stackoverflow.com/a/30541162) gmaps.LatLng _pixelToLatLng(gmaps.Map map, int x, int y) { final gmaps.LatLngBounds? bounds = map.bounds; 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 cbc12bf562f2..4588c4717ba0 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 @@ -31,6 +31,7 @@ class GoogleMapController { _circles = mapObjects.circles, _clusterManagers = mapObjects.clusterManagers, _heatmaps = mapObjects.heatmaps, + _groundOverlays = mapObjects.groundOverlays, _tileOverlays = mapObjects.tileOverlays, _lastMapConfiguration = mapConfiguration { _circlesController = CirclesController(stream: _streamController); @@ -43,6 +44,8 @@ class GoogleMapController { stream: _streamController, clusterManagersController: _clusterManagersController!); _tileOverlaysController = TileOverlaysController(); + _groundOverlaysController = + GroundOverlaysController(stream: _streamController); _updateStylesFromConfiguration(mapConfiguration); // Register the view factory that will hold the `_div` that holds the map in the DOM. @@ -70,6 +73,7 @@ class GoogleMapController { final Set _clusterManagers; final Set _heatmaps; Set _tileOverlays; + final Set _groundOverlays; // The configuration passed by the user, before converting to gmaps. // Caching this allows us to re-create the map faithfully when needed. @@ -131,6 +135,7 @@ class GoogleMapController { MarkersController? _markersController; ClusterManagersController? _clusterManagersController; TileOverlaysController? _tileOverlaysController; + GroundOverlaysController? _groundOverlaysController; // Keeps track if _attachGeometryControllers has been called or not. bool _controllersBoundToMap = false; @@ -143,6 +148,11 @@ class GoogleMapController { ClusterManagersController? get clusterManagersController => _clusterManagersController; + /// The GroundOverlaysController of this Map. Only for integration testing. + @visibleForTesting + GroundOverlaysController? get groundOverlayController => + _groundOverlaysController; + /// Overrides certain properties to install mocks defined during testing. @visibleForTesting void debugSetOverrides({ @@ -155,6 +165,7 @@ class GoogleMapController { PolylinesController? polylines, ClusterManagersController? clusterManagers, TileOverlaysController? tileOverlays, + GroundOverlaysController? groundOverlays, }) { _overrideCreateMap = createMap; _overrideSetOptions = setOptions; @@ -165,6 +176,7 @@ class GoogleMapController { _polylinesController = polylines ?? _polylinesController; _clusterManagersController = clusterManagers ?? _clusterManagersController; _tileOverlaysController = tileOverlays ?? _tileOverlaysController; + _groundOverlaysController = groundOverlays ?? _groundOverlaysController; } DebugCreateMapFunction? _overrideCreateMap; @@ -282,6 +294,8 @@ class GoogleMapController { 'Cannot attach a map to a null ClusterManagersController instance.'); assert(_tileOverlaysController != null, 'Cannot attach a map to a null TileOverlaysController instance.'); + assert(_groundOverlaysController != null, + 'Cannot attach a map to a null GroundOverlaysController instance.'); _circlesController!.bindToMap(_mapId, map); _heatmapsController!.bindToMap(_mapId, map); @@ -290,6 +304,7 @@ class GoogleMapController { _markersController!.bindToMap(_mapId, map); _clusterManagersController!.bindToMap(_mapId, map); _tileOverlaysController!.bindToMap(_mapId, map); + _groundOverlaysController!.bindToMap(_mapId, map); _controllersBoundToMap = true; } @@ -315,6 +330,7 @@ class GoogleMapController { _polygonsController!.addPolygons(_polygons); _polylinesController!.addPolylines(_polylines); _tileOverlaysController!.addTileOverlays(_tileOverlays); + _groundOverlaysController!.addGroundOverlays(_groundOverlays); } // Merges new options coming from the plugin into _lastConfiguration. @@ -507,6 +523,16 @@ class GoogleMapController { ?.removeClusterManagers(updates.clusterManagerIdsToRemove); } + /// Updates the set of [GroundOverlay]s. + void updateGroundOverlays(GroundOverlayUpdates updates) { + assert(_groundOverlaysController != null, + 'Cannot update tile overlays after dispose().'); + _groundOverlaysController?.addGroundOverlays(updates.objectsToAdd); + _groundOverlaysController?.changeGroundOverlays(updates.objectsToChange); + _groundOverlaysController?.removeGroundOverlays( + updates.objectIdsToRemove.cast()); + } + /// Updates the set of [TileOverlay]s. void updateTileOverlays(Set newOverlays) { final MapsObjectUpdates updates = @@ -561,6 +587,7 @@ class GoogleMapController { _markersController = null; _clusterManagersController = null; _tileOverlaysController = null; + _groundOverlaysController = null; _streamController.close(); } } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart index c49b5ed67392..d205a747690e 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 @@ -115,6 +115,14 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { _map(mapId).updateClusterManagers(clusterManagerUpdates); } + @override + Future updateGroundOverlays( + GroundOverlayUpdates groundOverlayUpdates, { + required int mapId, + }) async { + _map(mapId).updateGroundOverlays(groundOverlayUpdates); + } + @override Future clearTileCache( TileOverlayId tileOverlayId, { @@ -301,6 +309,11 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { return _events(mapId).whereType(); } + @override + Stream onGroundOverlayTap({required int mapId}) { + return _events(mapId).whereType(); + } + @override Future getStyleError({required int mapId}) async { return _map(mapId).lastStyleError; @@ -362,6 +375,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { GoogleMapsInspectorPlatform.instance = GoogleMapsInspectorWeb( (int mapId) => _map(mapId).configuration, (int mapId) => _map(mapId).clusterManagersController, + (int mapId) => _map(mapId).groundOverlayController, ); } } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart index 98b474309584..2304979dcabd 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart @@ -2,7 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:js_interop'; +import 'dart:typed_data'; + +import 'package:google_maps/google_maps.dart' as gmaps; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import '../google_maps_flutter_web.dart'; import 'marker_clustering.dart'; /// Function that gets the [MapConfiguration] for a given `mapId`. @@ -12,16 +18,24 @@ typedef ConfigurationProvider = MapConfiguration Function(int mapId); typedef ClusterManagersControllerProvider = ClusterManagersController? Function( int mapId); +/// Function that gets the [GroundOverlaysController] for a given `mapId`. +typedef GroundOverlaysControllerProvider = GroundOverlaysController? Function( + int mapId); + /// This platform implementation allows inspecting the running maps. class GoogleMapsInspectorWeb extends GoogleMapsInspectorPlatform { /// Build an "inspector" that is able to look into maps. - GoogleMapsInspectorWeb(ConfigurationProvider configurationProvider, - ClusterManagersControllerProvider clusterManagersControllerProvider) - : _configurationProvider = configurationProvider, - _clusterManagersControllerProvider = clusterManagersControllerProvider; + GoogleMapsInspectorWeb( + ConfigurationProvider configurationProvider, + ClusterManagersControllerProvider clusterManagersControllerProvider, + GroundOverlaysControllerProvider groundOverlaysControllerProvider, + ) : _configurationProvider = configurationProvider, + _clusterManagersControllerProvider = clusterManagersControllerProvider, + _groundOverlaysControllerProvider = groundOverlaysControllerProvider; final ConfigurationProvider _configurationProvider; final ClusterManagersControllerProvider _clusterManagersControllerProvider; + final GroundOverlaysControllerProvider _groundOverlaysControllerProvider; @override Future areBuildingsEnabled({required int mapId}) async { @@ -69,6 +83,33 @@ class GoogleMapsInspectorWeb extends GoogleMapsInspectorPlatform { return null; // Custom tiles not supported on the web } + @override + bool supportsGettingGroundOverlayInfo() => true; + + @override + Future getGroundOverlayInfo(GroundOverlayId groundOverlayId, + {required int mapId}) async { + final gmaps.GroundOverlay? groundOverlay = + _groundOverlaysControllerProvider(mapId)! + .getGroundOverlay(groundOverlayId); + + if (groundOverlay == null) { + return null; + } + + return GroundOverlay.fromBounds( + groundOverlayId: groundOverlayId, + image: BytesMapBitmap( + Uint8List.fromList([0]), + bitmapScaling: MapBitmapScaling.none, + ), + bounds: gmLatLngBoundsTolatLngBounds(groundOverlay.bounds), + transparency: 1.0 - groundOverlay.opacity, + visible: groundOverlay.map != null, + clickable: groundOverlay.get('clickable') is JSAny && + (groundOverlay.get('clickable')! as JSBoolean).toDart); + } + @override Future isCompassEnabled({required int mapId}) async { return false; // There's no compass on the web diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/ground_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/ground_overlay.dart new file mode 100644 index 000000000000..fb4193f5a18c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/ground_overlay.dart @@ -0,0 +1,29 @@ +// 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. + +part of '../google_maps_flutter_web.dart'; + +/// This wraps a [GroundOverlay] in a [gmaps.MapType]. +class GroundOverlayController { + /// Creates a [GroundOverlayController] that wraps a + /// [gmaps.GroundOverlay] object. + GroundOverlayController({ + required gmaps.GroundOverlay groundOverlay, + required VoidCallback onTap, + }) { + _groundOverlay = groundOverlay; + _groundOverlay.onClick.listen((gmaps.MapMouseEvent event) { + onTap.call(); + }); + } + + /// The [GroundOverlay] providing data for this controller. + gmaps.GroundOverlay get groundOverlay => _groundOverlay; + late gmaps.GroundOverlay _groundOverlay; + + /// Removes the [GroundOverlay] from the map. + void remove() { + _groundOverlay.map = null; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/ground_overlays.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/ground_overlays.dart new file mode 100644 index 000000000000..2b5ccdc4bae3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/ground_overlays.dart @@ -0,0 +1,103 @@ +// 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. + +part of '../google_maps_flutter_web.dart'; + +/// This class manages all the [GroundOverlayController]s associated to a [GoogleMapController]. +class GroundOverlaysController extends GeometryController { + /// Creates a new [GroundOverlaysController] instance. + /// + /// The [stream] parameter is a required [StreamController] used for + /// emitting ground overlay tap events. + GroundOverlaysController({ + required StreamController> stream, + }) : _streamController = stream, + _groundOverlayIdToController = + {}; + + final Map + _groundOverlayIdToController; + + // The stream over which ground overlays broadcast their events + final StreamController> _streamController; + + /// Adds new [GroundOverlay]s to this controller. + /// + /// Wraps the [GroundOverlay]s in corresponding [GroundOverlayController]s. + void addGroundOverlays(Set groundOverlaysToAdd) { + groundOverlaysToAdd.forEach(_addGroundOverlay); + } + + void _addGroundOverlay(GroundOverlay groundOverlay) { + assert(groundOverlay.bounds != null, + 'On Web platform, bounds must be provided for GroundOverlay'); + + final gmaps.LatLngBounds bounds = + latLngBoundsToGmlatLngBounds(groundOverlay.bounds!); + + final gmaps.GroundOverlayOptions groundOverlayOptions = + gmaps.GroundOverlayOptions() + ..opacity = 1.0 - groundOverlay.transparency + ..clickable = groundOverlay.clickable + ..map = groundOverlay.visible ? googleMap : null; + + final gmaps.GroundOverlay overlay = gmaps.GroundOverlay( + urlFromMapBitmap(groundOverlay.image), bounds, groundOverlayOptions); + + final GroundOverlayController controller = GroundOverlayController( + groundOverlay: overlay, + onTap: () { + _onGroundOverlayTap(groundOverlay.groundOverlayId); + }, + ); + + _groundOverlayIdToController[groundOverlay.groundOverlayId] = controller; + } + + /// Updates [GroundOverlay]s with new options. + void changeGroundOverlays(Set groundOverlays) { + groundOverlays.forEach(_changeGroundOverlay); + } + + void _changeGroundOverlay(GroundOverlay groundOverlay) { + final GroundOverlayController? controller = + _groundOverlayIdToController[groundOverlay.groundOverlayId]; + + if (controller == null) { + return; + } + + assert(groundOverlay.bounds != null, + 'On Web platform, bounds must be provided for GroundOverlay'); + + controller.groundOverlay.set('clickable', groundOverlay.clickable.toJS); + controller.groundOverlay.map = groundOverlay.visible ? googleMap : null; + controller.groundOverlay.opacity = 1.0 - groundOverlay.transparency; + } + + /// Removes the ground overlays associated with the given [GroundOverlayId]s. + void removeGroundOverlays(Set groundOverlayIds) { + groundOverlayIds.forEach(_removeGroundOverlay); + } + + void _removeGroundOverlay(GroundOverlayId groundOverlayId) { + final GroundOverlayController? controller = + _groundOverlayIdToController.remove(groundOverlayId); + if (controller != null) { + controller.remove(); + } + } + + void _onGroundOverlayTap(GroundOverlayId groundOverlayId) { + _streamController.add(GroundOverlayTapEvent(mapId, groundOverlayId)); + } + + /// Returns the [GroundOverlay] with the given [GroundOverlayId]. + /// Only used for testing. + gmaps.GroundOverlay? getGroundOverlay(GroundOverlayId groundOverlayId) { + final GroundOverlayController? controller = + _groundOverlayIdToController.remove(groundOverlayId); + return controller?.groundOverlay; + } +} 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 7ed3d99fed5e..496fec5be0f8 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/packages/tree/main/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.5.10 +version: 0.5.11 environment: sdk: ^3.4.0 @@ -23,7 +23,7 @@ dependencies: flutter_web_plugins: sdk: flutter google_maps: ^8.0.0 - google_maps_flutter_platform_interface: ^2.9.0 + google_maps_flutter_platform_interface: ^2.10.0 sanitize_html: ^2.0.0 stream_transform: ^2.0.0 web: ">=0.5.1 <2.0.0" From b84b657ac20cdb9cc7e3c25617691fdd4a14cb09 Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Wed, 5 Feb 2025 18:53:46 +0200 Subject: [PATCH 04/10] [google_maps_flutter_android] Improve convert functions --- .../flutter/plugins/googlemaps/Convert.java | 25 ++-- .../plugins/googlemaps/ConvertTest.java | 114 ++++++++++++++++++ 2 files changed, 125 insertions(+), 14 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index 29355f506885..a4d54abed922 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java @@ -929,7 +929,7 @@ static String interpretGroundOverlayOptions( .setGroundOverlayId(groundOverlayId) .setImage(dummyImage) .setWidth((double) groundOverlay.getWidth()) - .setHeight((double) groundOverlay.getWidth()) + .setHeight((double) groundOverlay.getHeight()) .setBearing((double) groundOverlay.getBearing()) .setTransparency((double) groundOverlay.getTransparency()) .setZIndex((long) groundOverlay.getZIndex()) @@ -953,7 +953,7 @@ static String interpretGroundOverlayOptions( * @return the PlatformDoublePair representing the anchor point. */ @VisibleForTesting - private static @NonNull Messages.PlatformDoublePair buildGroundOverlayAnchorForPigeon( + public static @NonNull Messages.PlatformDoublePair buildGroundOverlayAnchorForPigeon( GroundOverlay groundOverlay) { Messages.PlatformDoublePair.Builder anchorBuilder = new Messages.PlatformDoublePair.Builder(); @@ -966,20 +966,17 @@ static String interpretGroundOverlayOptions( double normalizedLatitude = 1.0 - ((position.latitude - bounds.southwest.latitude) / height); // Calculate normalized longitude. + // For longitude, if the bounds cross the antimeridian (west > east), + // adjust the width accordingly. double west = bounds.southwest.longitude; double east = bounds.northeast.longitude; - double longitudeOffset = 0; - if (west <= east) { - longitudeOffset = position.longitude - west; - } else { - longitudeOffset = position.longitude - west; - if (position.longitude < west) { - // If bounds cross the antimeridian add 360 to the offset. - longitudeOffset += 360; - } - } - double width = (west <= east) ? east - west : 360.0 - (west - east); - double normalizedLongitude = longitudeOffset / width; + double width = (west <= east) ? (east - west) : (360.0 - (west - east)); + + // Adjust the position longitude if it is less than west by adding 360, + // then compute the normalized value. + double normalizedLongitude = + ((position.longitude < west ? position.longitude + 360.0 : position.longitude) - west) + / width; anchorBuilder.setX(normalizedLongitude); anchorBuilder.setY(normalizedLatitude); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java index a2733980559d..6df04d136f99 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java @@ -16,6 +16,7 @@ import static io.flutter.plugins.googlemaps.Convert.HEATMAP_RADIUS_KEY; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -31,6 +32,7 @@ import android.util.Base64; import androidx.annotation.NonNull; import com.google.android.gms.maps.model.BitmapDescriptor; +import com.google.android.gms.maps.model.GroundOverlay; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; import com.google.maps.android.clustering.algo.StaticCluster; @@ -665,6 +667,118 @@ public void ConvertInterpretHeatmapOptionsReturnsCorrectData() { Assert.assertEquals(idData, id); } + @Test + public void buildGroundOverlayAnchorForPigeonWithNonCrossingMeridian() { + LatLng position = new LatLng(10, 20); + LatLng southwest = new LatLng(5, 15); + LatLng northeast = new LatLng(15, 25); + LatLngBounds bounds = new LatLngBounds(southwest, northeast); + GroundOverlay groundOverlay = mock(GroundOverlay.class); + when(groundOverlay.getPosition()).thenReturn(position); + when(groundOverlay.getBounds()).thenReturn(bounds); + + Messages.PlatformDoublePair anchor = Convert.buildGroundOverlayAnchorForPigeon(groundOverlay); + + Assert.assertEquals(0.5, anchor.getX(), 1e-15); + Assert.assertEquals(0.5, anchor.getY(), 1e-15); + } + + @Test + public void buildGroundOverlayAnchorForPigeonWithCrossingMeridian() { + LatLng position = new LatLng(10, -175); + LatLng southwest = new LatLng(5, 170); + LatLng northeast = new LatLng(15, -160); + LatLngBounds bounds = new LatLngBounds(southwest, northeast); + GroundOverlay groundOverlay = mock(GroundOverlay.class); + when(groundOverlay.getPosition()).thenReturn(position); + when(groundOverlay.getBounds()).thenReturn(bounds); + + Messages.PlatformDoublePair anchor = Convert.buildGroundOverlayAnchorForPigeon(groundOverlay); + + Assert.assertEquals(0.5, anchor.getX(), 1e-15); + Assert.assertEquals(0.5, anchor.getY(), 1e-15); + } + + @Test + public void groundOverlayToPigeonWithPosition() { + GroundOverlay mockGroundOverlay = mock(GroundOverlay.class); + LatLng position = new LatLng(10, 20); + LatLng southwest = new LatLng(5, 15); + LatLng northeast = new LatLng(15, 25); + LatLngBounds bounds = new LatLngBounds(southwest, northeast); + when(mockGroundOverlay.getPosition()).thenReturn(position); + when(mockGroundOverlay.getBounds()).thenReturn(bounds); + when(mockGroundOverlay.getWidth()).thenReturn(30f); + when(mockGroundOverlay.getHeight()).thenReturn(40f); + when(mockGroundOverlay.getBearing()).thenReturn(50f); + when(mockGroundOverlay.getTransparency()).thenReturn(0.6f); + when(mockGroundOverlay.getZIndex()).thenReturn(7f); + when(mockGroundOverlay.isVisible()).thenReturn(true); + when(mockGroundOverlay.isClickable()).thenReturn(false); + + Messages.PlatformGroundOverlay result = + Convert.groundOverlayToPigeon(mockGroundOverlay, "overlay_1", false); + + Assert.assertEquals("overlay_1", result.getGroundOverlayId()); + Assert.assertEquals(position.latitude, result.getPosition().getLatitude(), 1e-15); + Assert.assertEquals(position.longitude, result.getPosition().getLongitude(), 1e-15); + Assert.assertEquals(30.0, result.getWidth(), 1e-15); + Assert.assertEquals(40.0, result.getHeight(), 1e-15); + Assert.assertEquals(50.0, result.getBearing(), 1e-15); + Assert.assertEquals(0.6, result.getTransparency(), 1e-6); + Assert.assertEquals(7, result.getZIndex().intValue()); + Assert.assertTrue(result.getVisible()); + Assert.assertFalse(result.getClickable()); + Assert.assertNull(result.getBounds()); + + Messages.PlatformDoublePair anchor = result.getAnchor(); + Assert.assertEquals(0.5, anchor.getX(), 1e-6); + Assert.assertEquals(0.5, anchor.getY(), 1e-6); + } + + @Test + public void groundOverlayToPigeonWithBounds() { + GroundOverlay mockGroundOverlay = mock(GroundOverlay.class); + LatLng position = new LatLng(10, 20); + LatLng southwest = new LatLng(5, 15); + LatLng northeast = new LatLng(15, 25); + LatLngBounds bounds = new LatLngBounds(southwest, northeast); + when(mockGroundOverlay.getPosition()).thenReturn(position); + when(mockGroundOverlay.getBounds()).thenReturn(bounds); + when(mockGroundOverlay.getWidth()).thenReturn(30f); + when(mockGroundOverlay.getHeight()).thenReturn(40f); + when(mockGroundOverlay.getBearing()).thenReturn(50f); + when(mockGroundOverlay.getTransparency()).thenReturn(0.6f); + when(mockGroundOverlay.getZIndex()).thenReturn(7f); + when(mockGroundOverlay.isVisible()).thenReturn(true); + when(mockGroundOverlay.isClickable()).thenReturn(false); + + Messages.PlatformGroundOverlay result = + Convert.groundOverlayToPigeon(mockGroundOverlay, "overlay_2", true); + + Assert.assertEquals("overlay_2", result.getGroundOverlayId()); + Assert.assertEquals( + bounds.southwest.latitude, result.getBounds().getSouthwest().getLatitude(), 1e-15); + Assert.assertEquals( + bounds.southwest.longitude, result.getBounds().getSouthwest().getLongitude(), 1e-15); + Assert.assertEquals( + bounds.northeast.latitude, result.getBounds().getNortheast().getLatitude(), 1e-15); + Assert.assertEquals( + bounds.northeast.longitude, result.getBounds().getNortheast().getLongitude(), 1e-15); + Assert.assertEquals(30.0, result.getWidth(), 1e-15); + Assert.assertEquals(40.0, result.getHeight(), 1e-15); + Assert.assertEquals(50.0, result.getBearing(), 1e-15); + Assert.assertEquals(0.6, result.getTransparency(), 1e-6); + Assert.assertEquals(7, result.getZIndex().intValue()); + Assert.assertTrue(result.getVisible()); + Assert.assertFalse(result.getClickable()); + Assert.assertNull(result.getPosition()); + + Messages.PlatformDoublePair anchor = result.getAnchor(); + Assert.assertEquals(0.5, anchor.getX(), 1e-6); + Assert.assertEquals(0.5, anchor.getY(), 1e-6); + } + private InputStream buildImageInputStream() { Bitmap fakeBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); From 43522ae3186dd6c135a2c53a7802b6ad1649abf4 Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Wed, 5 Feb 2025 19:14:55 +0200 Subject: [PATCH 05/10] [google_maps_flutter_ios] Add missing tests and address review comments --- .../GoogleMapsGroundOverlayControllerTests.m | 63 +++++++++++++++---- .../ios/Classes/FGMGroundOverlayController.m | 5 +- .../ios/Classes/FLTGoogleMapJSONConversions.h | 2 +- 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsGroundOverlayControllerTests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsGroundOverlayControllerTests.m index da3ef509abae..705895103020 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsGroundOverlayControllerTests.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsGroundOverlayControllerTests.m @@ -90,7 +90,7 @@ - (void)testUpdatingGroundOverlayWithPosition { image:bitmap position:position bounds:nil - anchor:nil + anchor:[FGMPlatformPoint makeWithX:0.5 y:0.5] transparency:0.5 bearing:65.0 zIndex:2.0 @@ -103,10 +103,29 @@ - (void)testUpdatingGroundOverlayWithPosition { screenScale:1.0]; XCTAssertNotNil(groundOverlayController.groundOverlay.icon); - XCTAssertEqual(groundOverlayController.groundOverlay.position.latitude, position.latitude); - XCTAssertEqual(groundOverlayController.groundOverlay.position.longitude, position.longitude); + XCTAssertEqualWithAccuracy(groundOverlayController.groundOverlay.position.latitude, + position.latitude, DBL_EPSILON); + XCTAssertEqualWithAccuracy(groundOverlayController.groundOverlay.position.longitude, + position.longitude, DBL_EPSILON); XCTAssertEqual(groundOverlayController.groundOverlay.opacity, platformGroundOverlay.transparency); XCTAssertEqual(groundOverlayController.groundOverlay.bearing, platformGroundOverlay.bearing); + XCTAssertEqualWithAccuracy(groundOverlayController.groundOverlay.anchor.x, 0.5, DBL_EPSILON); + XCTAssertEqualWithAccuracy(groundOverlayController.groundOverlay.anchor.y, 0.5, DBL_EPSILON); + XCTAssertEqual(groundOverlayController.groundOverlay.zIndex, platformGroundOverlay.zIndex); + + FGMPlatformGroundOverlay *convertedPlatformGroundOverlay = + FGMGetPigeonGroundOverlay(groundOverlayController.groundOverlay, @"id_1", NO, @14.0); + XCTAssertEqualObjects(convertedPlatformGroundOverlay.groundOverlayId, @"id_1"); + XCTAssertEqualWithAccuracy(convertedPlatformGroundOverlay.position.latitude, position.latitude, + DBL_EPSILON); + XCTAssertEqualWithAccuracy(convertedPlatformGroundOverlay.position.longitude, position.longitude, + DBL_EPSILON); + XCTAssertEqual(convertedPlatformGroundOverlay.zoomLevel.doubleValue, 14.0); + XCTAssertEqual(convertedPlatformGroundOverlay.transparency, platformGroundOverlay.transparency); + XCTAssertEqual(convertedPlatformGroundOverlay.bearing, platformGroundOverlay.bearing); + XCTAssertEqualWithAccuracy(convertedPlatformGroundOverlay.anchor.x, 0.5, DBL_EPSILON); + XCTAssertEqualWithAccuracy(convertedPlatformGroundOverlay.anchor.y, 0.5, DBL_EPSILON); + XCTAssertEqual(convertedPlatformGroundOverlay.zIndex, platformGroundOverlay.zIndex); } - (void)testUpdatingGroundOverlayWithBounds { @@ -127,7 +146,7 @@ - (void)testUpdatingGroundOverlayWithBounds { image:bitmap position:nil bounds:bounds - anchor:nil + anchor:[FGMPlatformPoint makeWithX:0.5 y:0.5] transparency:0.5 bearing:65.0 zIndex:2.0 @@ -140,16 +159,36 @@ - (void)testUpdatingGroundOverlayWithBounds { screenScale:1.0]; XCTAssertNotNil(groundOverlayController.groundOverlay.icon); - XCTAssertEqual(groundOverlayController.groundOverlay.bounds.northEast.latitude, - bounds.northeast.latitude); - XCTAssertEqual(groundOverlayController.groundOverlay.bounds.northEast.longitude, - bounds.northeast.longitude); - XCTAssertEqual(groundOverlayController.groundOverlay.bounds.southWest.latitude, - bounds.southwest.latitude); - XCTAssertEqual(groundOverlayController.groundOverlay.bounds.southWest.longitude, - bounds.southwest.longitude); + XCTAssertEqualWithAccuracy(groundOverlayController.groundOverlay.bounds.northEast.latitude, + bounds.northeast.latitude, DBL_EPSILON); + XCTAssertEqualWithAccuracy(groundOverlayController.groundOverlay.bounds.northEast.longitude, + bounds.northeast.longitude, DBL_EPSILON); + XCTAssertEqualWithAccuracy(groundOverlayController.groundOverlay.bounds.southWest.latitude, + bounds.southwest.latitude, DBL_EPSILON); + XCTAssertEqualWithAccuracy(groundOverlayController.groundOverlay.bounds.southWest.longitude, + bounds.southwest.longitude, DBL_EPSILON); XCTAssertEqual(groundOverlayController.groundOverlay.opacity, platformGroundOverlay.transparency); XCTAssertEqual(groundOverlayController.groundOverlay.bearing, platformGroundOverlay.bearing); + XCTAssertEqualWithAccuracy(groundOverlayController.groundOverlay.anchor.x, 0.5, DBL_EPSILON); + XCTAssertEqualWithAccuracy(groundOverlayController.groundOverlay.anchor.y, 0.5, DBL_EPSILON); + XCTAssertEqual(groundOverlayController.groundOverlay.zIndex, platformGroundOverlay.zIndex); + + FGMPlatformGroundOverlay *convertedPlatformGroundOverlay = + FGMGetPigeonGroundOverlay(groundOverlayController.groundOverlay, @"id_1", YES, nil); + XCTAssertEqualObjects(convertedPlatformGroundOverlay.groundOverlayId, @"id_1"); + XCTAssertEqualWithAccuracy(convertedPlatformGroundOverlay.bounds.northeast.latitude, + bounds.northeast.latitude, DBL_EPSILON); + XCTAssertEqualWithAccuracy(convertedPlatformGroundOverlay.bounds.northeast.longitude, + bounds.northeast.longitude, DBL_EPSILON); + XCTAssertEqualWithAccuracy(convertedPlatformGroundOverlay.bounds.southwest.latitude, + bounds.southwest.latitude, DBL_EPSILON); + XCTAssertEqualWithAccuracy(convertedPlatformGroundOverlay.bounds.southwest.longitude, + bounds.southwest.longitude, DBL_EPSILON); + XCTAssertEqual(convertedPlatformGroundOverlay.transparency, platformGroundOverlay.transparency); + XCTAssertEqual(convertedPlatformGroundOverlay.bearing, platformGroundOverlay.bearing); + XCTAssertEqualWithAccuracy(convertedPlatformGroundOverlay.anchor.x, 0.5, DBL_EPSILON); + XCTAssertEqualWithAccuracy(convertedPlatformGroundOverlay.anchor.y, 0.5, DBL_EPSILON); + XCTAssertEqual(convertedPlatformGroundOverlay.zIndex, platformGroundOverlay.zIndex); } @end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.m index 29d9e2e4c5a0..8f5edc8c2f04 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.m @@ -75,7 +75,6 @@ - (void)updateFromPlatformGroundOverlay:(FGMPlatformGroundOverlay *)groundOverla registrar:(NSObject *)registrar screenScale:(CGFloat)screenScale { [self setConsumeTapEvents:groundOverlay.clickable]; - [self setVisible:groundOverlay.visible]; [self setZIndex:(int)groundOverlay.zIndex]; [self setAnchor:CGPointMake(groundOverlay.anchor.x, groundOverlay.anchor.y)]; UIImage *image = FGMIconFromBitmap(groundOverlay.image, registrar, screenScale); @@ -95,6 +94,7 @@ - (void)updateFromPlatformGroundOverlay:(FGMPlatformGroundOverlay *)groundOverla [self setPositionFromCoordinates:CLLocationCoordinate2DMake(groundOverlay.position.latitude, groundOverlay.position.longitude)]; } + [self setVisible:groundOverlay.visible]; } @end @@ -196,9 +196,6 @@ - (void)removeGroundOverlaysWithIdentifiers:(NSArray *)identifiers { } - (void)didTapGroundOverlayWithIdentifier:(NSString *)identifier { - if (!identifier) { - return; - } FGMGroundOverlayController *controller = self.groundOverlayControllerByIdentifier[identifier]; if (!controller) { return; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h index ea5db67f2bec..c26e6242e555 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h @@ -62,7 +62,7 @@ extern FGMPlatformCluster *FGMGetPigeonCluster(GMUStaticCluster *cluster, extern FGMPlatformGroundOverlay *FGMGetPigeonGroundOverlay(GMSGroundOverlay *groundOverlay, NSString *overlayId, BOOL isCreatedWithBounds, - NSNumber *zoomLevel); + NSNumber *_Nullable zoomLevel); /// Creates a GMSCameraUpdate from its Pigeon equivalent. extern GMSCameraUpdate *_Nullable FGMGetCameraUpdateForPigeonCameraUpdate( From 8f1be2bf52b23b784a1a16c0792df48297e8ae3e Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Fri, 7 Feb 2025 15:29:54 +0200 Subject: [PATCH 06/10] Improve tests and null annotations --- .../flutter/plugins/googlemaps/Convert.java | 12 ++--- .../googlemaps/GroundOverlaysController.java | 11 ++-- .../plugins/googlemaps/ConvertTest.java | 54 +++++-------------- .../GroundOverlaysControllerTest.java | 28 +--------- .../plugins/googlemaps/TestImageUtils.java | 43 +++++++++++++++ .../lib/src/google_map_inspector_android.dart | 18 ++++--- .../ios/Classes/FGMGroundOverlayController.h | 14 ++--- .../lib/src/google_map_inspector_ios.dart | 18 ++++--- 8 files changed, 96 insertions(+), 102 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/TestImageUtils.java diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index a4d54abed922..6f0f25c043e4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java @@ -863,12 +863,12 @@ static Tile tileFromPigeon(Messages.PlatformTile tile) { * @return the identifier of the ground overlay. * @throws IllegalArgumentException if required fields are missing or invalid. */ - static String interpretGroundOverlayOptions( - Messages.PlatformGroundOverlay groundOverlay, - GroundOverlaySink sink, - AssetManager assetManager, + static @NonNull String interpretGroundOverlayOptions( + @NonNull Messages.PlatformGroundOverlay groundOverlay, + @NonNull GroundOverlaySink sink, + @NonNull AssetManager assetManager, float density, - BitmapDescriptorFactoryWrapper wrapper) { + @NonNull BitmapDescriptorFactoryWrapper wrapper) { sink.setTransparency(groundOverlay.getTransparency().floatValue()); sink.setZIndex(groundOverlay.getZIndex().floatValue()); sink.setVisible(groundOverlay.getVisible()); @@ -954,7 +954,7 @@ static String interpretGroundOverlayOptions( */ @VisibleForTesting public static @NonNull Messages.PlatformDoublePair buildGroundOverlayAnchorForPigeon( - GroundOverlay groundOverlay) { + @NonNull GroundOverlay groundOverlay) { Messages.PlatformDoublePair.Builder anchorBuilder = new Messages.PlatformDoublePair.Builder(); // Position is overlays anchor point. Calculate normalized anchor point based on position and bounds. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlaysController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlaysController.java index 92b03b1af5b2..04857bfb7de3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlaysController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GroundOverlaysController.java @@ -17,7 +17,7 @@ import java.util.Map; class GroundOverlaysController { - @VisibleForTesting final Map groundOverlayIdToController; + private final Map groundOverlayIdToController; private final HashMap googleMapsGroundOverlayIdToDartGroundOverlayId; private final MapsCallbackApi flutterApi; private GoogleMap googleMap; @@ -25,16 +25,17 @@ class GroundOverlaysController { private final float density; private final Convert.BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper; - GroundOverlaysController(MapsCallbackApi flutterApi, AssetManager assetManager, float density) { + GroundOverlaysController( + @NonNull MapsCallbackApi flutterApi, @NonNull AssetManager assetManager, float density) { this(flutterApi, assetManager, density, new Convert.BitmapDescriptorFactoryWrapper()); } @VisibleForTesting GroundOverlaysController( - MapsCallbackApi flutterApi, - AssetManager assetManager, + @NonNull MapsCallbackApi flutterApi, + @NonNull AssetManager assetManager, float density, - Convert.BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper) { + @NonNull Convert.BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper) { this.groundOverlayIdToController = new HashMap<>(); this.googleMapsGroundOverlayIdToDartGroundOverlayId = new HashMap<>(); this.flutterApi = flutterApi; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java index 6df04d136f99..4af04f75c52a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java @@ -24,10 +24,6 @@ import static org.mockito.Mockito.when; import android.content.res.AssetManager; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; import android.os.Build; import android.util.Base64; import androidx.annotation.NonNull; @@ -42,9 +38,6 @@ import com.google.maps.android.projection.SphericalMercatorProjection; import io.flutter.plugins.googlemaps.Convert.BitmapDescriptorFactoryWrapper; import io.flutter.plugins.googlemaps.Convert.FlutterInjectorWrapper; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; import java.util.Collections; import java.util.List; import java.util.Map; @@ -74,7 +67,7 @@ public class ConvertTest { AutoCloseable mockCloseable; // A 1x1 pixel (#8080ff) PNG image encoded in base64 - private final String base64Image = generateBase64Image(); + private final String base64Image = TestImageUtils.generateBase64Image(); @Before public void before() { @@ -144,7 +137,7 @@ public void GetBitmapFromAssetAuto() throws Exception { when(flutterInjectorWrapper.getLookupKeyForAsset(fakeAssetName)).thenReturn(fakeAssetKey); - when(assetManager.open(fakeAssetKey)).thenReturn(buildImageInputStream()); + when(assetManager.open(fakeAssetKey)).thenReturn(TestImageUtils.buildImageInputStream()); when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); Messages.PlatformBitmapAssetMap bitmap = @@ -170,7 +163,7 @@ public void GetBitmapFromAssetAutoAndWidth() throws Exception { when(flutterInjectorWrapper.getLookupKeyForAsset(fakeAssetName)).thenReturn(fakeAssetKey); - when(assetManager.open(fakeAssetKey)).thenReturn(buildImageInputStream()); + when(assetManager.open(fakeAssetKey)).thenReturn(TestImageUtils.buildImageInputStream()); when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); Messages.PlatformBitmapAssetMap bitmap = @@ -195,7 +188,7 @@ public void GetBitmapFromAssetAutoAndHeight() throws Exception { when(flutterInjectorWrapper.getLookupKeyForAsset(fakeAssetName)).thenReturn(fakeAssetKey); - when(assetManager.open(fakeAssetKey)).thenReturn(buildImageInputStream()); + when(assetManager.open(fakeAssetKey)).thenReturn(TestImageUtils.buildImageInputStream()); when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); Messages.PlatformBitmapAssetMap bitmap = @@ -220,7 +213,7 @@ public void GetBitmapFromAssetNoScaling() throws Exception { when(flutterInjectorWrapper.getLookupKeyForAsset(fakeAssetName)).thenReturn(fakeAssetKey); - when(assetManager.open(fakeAssetKey)).thenReturn(buildImageInputStream()); + when(assetManager.open(fakeAssetKey)).thenReturn(TestImageUtils.buildImageInputStream()); when(bitmapDescriptorFactoryWrapper.fromAsset(any())).thenReturn(mockBitmapDescriptor); @@ -720,8 +713,11 @@ public void groundOverlayToPigeonWithPosition() { Convert.groundOverlayToPigeon(mockGroundOverlay, "overlay_1", false); Assert.assertEquals("overlay_1", result.getGroundOverlayId()); + Assert.assertNotNull(result.getPosition()); Assert.assertEquals(position.latitude, result.getPosition().getLatitude(), 1e-15); Assert.assertEquals(position.longitude, result.getPosition().getLongitude(), 1e-15); + Assert.assertNotNull(result.getWidth()); + Assert.assertNotNull(result.getHeight()); Assert.assertEquals(30.0, result.getWidth(), 1e-15); Assert.assertEquals(40.0, result.getHeight(), 1e-15); Assert.assertEquals(50.0, result.getBearing(), 1e-15); @@ -732,6 +728,7 @@ public void groundOverlayToPigeonWithPosition() { Assert.assertNull(result.getBounds()); Messages.PlatformDoublePair anchor = result.getAnchor(); + Assert.assertNotNull(anchor); Assert.assertEquals(0.5, anchor.getX(), 1e-6); Assert.assertEquals(0.5, anchor.getY(), 1e-6); } @@ -757,6 +754,7 @@ public void groundOverlayToPigeonWithBounds() { Convert.groundOverlayToPigeon(mockGroundOverlay, "overlay_2", true); Assert.assertEquals("overlay_2", result.getGroundOverlayId()); + Assert.assertNotNull(result.getBounds()); Assert.assertEquals( bounds.southwest.latitude, result.getBounds().getSouthwest().getLatitude(), 1e-15); Assert.assertEquals( @@ -765,6 +763,8 @@ public void groundOverlayToPigeonWithBounds() { bounds.northeast.latitude, result.getBounds().getNortheast().getLatitude(), 1e-15); Assert.assertEquals( bounds.northeast.longitude, result.getBounds().getNortheast().getLongitude(), 1e-15); + Assert.assertNotNull(result.getWidth()); + Assert.assertNotNull(result.getHeight()); Assert.assertEquals(30.0, result.getWidth(), 1e-15); Assert.assertEquals(40.0, result.getHeight(), 1e-15); Assert.assertEquals(50.0, result.getBearing(), 1e-15); @@ -775,38 +775,10 @@ public void groundOverlayToPigeonWithBounds() { Assert.assertNull(result.getPosition()); Messages.PlatformDoublePair anchor = result.getAnchor(); + Assert.assertNotNull(anchor); Assert.assertEquals(0.5, anchor.getX(), 1e-6); Assert.assertEquals(0.5, anchor.getY(), 1e-6); } - - private InputStream buildImageInputStream() { - Bitmap fakeBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - fakeBitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream); - byte[] byteArray = byteArrayOutputStream.toByteArray(); - return new ByteArrayInputStream(byteArray); - } - - // Helper method to generate 1x1 pixel base64 encoded png test image - private String generateBase64Image() { - int width = 1; - int height = 1; - Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - - // Draw on the Bitmap - Paint paint = new Paint(); - paint.setColor(Color.parseColor("#FF8080FF")); - canvas.drawRect(0, 0, width, height, paint); - - // Convert the Bitmap to PNG format - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); - byte[] pngBytes = outputStream.toByteArray(); - - // Encode the PNG bytes as a base64 string - return Base64.encodeToString(pngBytes, Base64.DEFAULT); - } } class MockHeatmapBuilder implements HeatmapOptionsSink { diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GroundOverlaysControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GroundOverlaysControllerTest.java index bc778af9f58e..d6ba8b0a76b8 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GroundOverlaysControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GroundOverlaysControllerTest.java @@ -12,10 +12,6 @@ import android.content.Context; import android.content.res.AssetManager; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; import android.os.Build; import android.util.Base64; import androidx.annotation.NonNull; @@ -26,7 +22,6 @@ import com.google.android.gms.maps.model.GroundOverlayOptions; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.googlemaps.Convert.BitmapDescriptorFactoryWrapper; -import java.io.ByteArrayOutputStream; import java.util.Collections; import org.junit.After; import org.junit.Before; @@ -50,7 +45,7 @@ public class GroundOverlaysControllerTest { private GoogleMap googleMap; // A 1x1 pixel (#8080ff) PNG image encoded in base64 - private final String base64Image = generateBase64Image(); + private final String base64Image = TestImageUtils.generateBase64Image(); @NonNull private Messages.PlatformGroundOverlay.Builder defaultGroundOverlayBuilder() { @@ -126,25 +121,4 @@ public void controller_AddChangeAndRemoveGroundOverlay() { Mockito.verify(groundOverlay, times(1)).remove(); } - - // Helper method to generate 1x1 pixel base64 encoded png test image - private String generateBase64Image() { - int width = 1; - int height = 1; - Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - - // Draw on the Bitmap - Paint paint = new Paint(); - paint.setColor(Color.parseColor("#FF8080FF")); - canvas.drawRect(0, 0, width, height, paint); - - // Convert the Bitmap to PNG format - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); - byte[] pngBytes = outputStream.toByteArray(); - - // Encode the PNG bytes as a base64 string - return Base64.encodeToString(pngBytes, Base64.DEFAULT); - } } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/TestImageUtils.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/TestImageUtils.java new file mode 100644 index 000000000000..628b38192702 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/TestImageUtils.java @@ -0,0 +1,43 @@ +package io.flutter.plugins.googlemaps; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.Base64; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; + +// Collection of helper methods for generating test images. +public class TestImageUtils { + // Helper method to generate 1x1 pixel base64 encoded png test image. + public static String generateBase64Image() { + int width = 1; + int height = 1; + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + // Draw on the Bitmap + Paint paint = new Paint(); + paint.setColor(Color.parseColor("#FF8080FF")); + canvas.drawRect(0, 0, width, height, paint); + + // Convert the Bitmap to PNG format + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); + byte[] pngBytes = outputStream.toByteArray(); + + // Encode the PNG bytes as a base64 string + return Base64.encodeToString(pngBytes, Base64.DEFAULT); + } + + // Helper method to generate input stream for 1x1 pixel test image. + public static InputStream buildImageInputStream() { + Bitmap fakeBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + fakeBitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream); + byte[] byteArray = byteArrayOutputStream.toByteArray(); + return new ByteArrayInputStream(byteArray); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart index 3682398b0350..801a0bd50996 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart @@ -100,11 +100,13 @@ class GoogleMapsInspectorAndroid extends GoogleMapsInspectorPlatform { bitmapScaling: MapBitmapScaling.none, ); - if (groundOverlayInfo.position != null) { + final PlatformLatLng? position = groundOverlayInfo.position; + final PlatformLatLngBounds? bounds = groundOverlayInfo.bounds; + + if (position != null) { return GroundOverlay.fromPosition( groundOverlayId: groundOverlayId, - position: LatLng(groundOverlayInfo.position!.latitude, - groundOverlayInfo.position!.longitude), + position: LatLng(position.latitude, position.longitude), image: dummyImage, width: groundOverlayInfo.width, height: groundOverlayInfo.height, @@ -116,14 +118,14 @@ class GoogleMapsInspectorAndroid extends GoogleMapsInspectorPlatform { anchor: Offset(groundOverlayInfo.anchor!.x, groundOverlayInfo.anchor!.y), ); - } else if (groundOverlayInfo.bounds != null) { + } else if (bounds != null) { return GroundOverlay.fromBounds( groundOverlayId: groundOverlayId, bounds: LatLngBounds( - southwest: LatLng(groundOverlayInfo.bounds!.southwest.latitude, - groundOverlayInfo.bounds!.southwest.longitude), - northeast: LatLng(groundOverlayInfo.bounds!.northeast.latitude, - groundOverlayInfo.bounds!.northeast.longitude)), + southwest: + LatLng(bounds.southwest.latitude, bounds.southwest.longitude), + northeast: + LatLng(bounds.northeast.latitude, bounds.northeast.longitude)), image: dummyImage, zIndex: groundOverlayInfo.zIndex, bearing: groundOverlayInfo.bearing, diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.h index de875b0fb2dc..b01fade21b43 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.h @@ -25,10 +25,10 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, strong, nullable) NSNumber *zoomLevel; /// Initializes an instance of this class with a GMSGroundOverlay, a map view, and identifier. -- (instancetype _Nullable)initWithGroundOverlay:(GMSGroundOverlay *)groundOverlay - identifier:(NSString *)identifier - mapView:(GMSMapView *)mapView - isCreatedWithBounds:(BOOL)isCreatedWithBounds; +- (instancetype)initWithGroundOverlay:(GMSGroundOverlay *)groundOverlay + identifier:(NSString *)identifier + mapView:(GMSMapView *)mapView + isCreatedWithBounds:(BOOL)isCreatedWithBounds; /// Removes this ground overlay from the map. - (void)removeGroundOverlay; @@ -38,9 +38,9 @@ NS_ASSUME_NONNULL_BEGIN @interface FLTGroundOverlaysController : NSObject /// Initializes the controller with a GMSMapView, callback handler and registrar. -- (instancetype _Nullable)initWithMapView:(GMSMapView *)mapView - callbackHandler:(FGMMapsCallbackApi *)callbackHandler - registrar:(NSObject *)registrar; +- (instancetype)initWithMapView:(GMSMapView *)mapView + callbackHandler:(FGMMapsCallbackApi *)callbackHandler + registrar:(NSObject *)registrar; /// Adds ground overlays to the map. - (void)addGroundOverlays:(NSArray *)groundOverlaysToAdd; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart index 148d1c73e7b5..7461a4f2aa45 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart @@ -124,11 +124,13 @@ class GoogleMapsInspectorIOS extends GoogleMapsInspectorPlatform { bitmapScaling: MapBitmapScaling.none, ); - if (groundOverlayInfo.position != null) { + final PlatformLatLng? position = groundOverlayInfo.position; + final PlatformLatLngBounds? bounds = groundOverlayInfo.bounds; + + if (position != null) { return GroundOverlay.fromPosition( groundOverlayId: groundOverlayId, - position: LatLng(groundOverlayInfo.position!.latitude, - groundOverlayInfo.position!.longitude), + position: LatLng(position.latitude, position.longitude), image: dummyImage, zIndex: groundOverlayInfo.zIndex, bearing: groundOverlayInfo.bearing, @@ -139,14 +141,14 @@ class GoogleMapsInspectorIOS extends GoogleMapsInspectorPlatform { Offset(groundOverlayInfo.anchor!.x, groundOverlayInfo.anchor!.y), zoomLevel: groundOverlayInfo.zoomLevel, ); - } else if (groundOverlayInfo.bounds != null) { + } else if (bounds != null) { return GroundOverlay.fromBounds( groundOverlayId: groundOverlayId, bounds: LatLngBounds( - southwest: LatLng(groundOverlayInfo.bounds!.southwest.latitude, - groundOverlayInfo.bounds!.southwest.longitude), - northeast: LatLng(groundOverlayInfo.bounds!.northeast.latitude, - groundOverlayInfo.bounds!.northeast.longitude)), + southwest: + LatLng(bounds.southwest.latitude, bounds.southwest.longitude), + northeast: + LatLng(bounds.northeast.latitude, bounds.northeast.longitude)), image: dummyImage, zIndex: groundOverlayInfo.zIndex, bearing: groundOverlayInfo.bearing, From 323326ba94b3372039659ae4f5820be500dc0f35 Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Mon, 10 Feb 2025 11:55:28 +0200 Subject: [PATCH 07/10] [google_maps_flutter_android] Add missing license block. --- .../java/io/flutter/plugins/googlemaps/TestImageUtils.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/TestImageUtils.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/TestImageUtils.java index 628b38192702..5b7cc32700a4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/TestImageUtils.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/TestImageUtils.java @@ -1,3 +1,7 @@ +// 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. + package io.flutter.plugins.googlemaps; import android.graphics.Bitmap; From 26d8c9353f323e6812801dd239f3908ca0669057 Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Mon, 10 Feb 2025 14:00:40 +0200 Subject: [PATCH 08/10] [google_maps_flutter_ios] Improve example app --- .../maps_example_dart/lib/ground_overlay.dart | 41 ++++--------------- 1 file changed, 7 insertions(+), 34 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/ground_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/ground_overlay.dart index 055736f396df..0dfac4c9a512 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/ground_overlay.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/ground_overlay.dart @@ -57,8 +57,6 @@ class GroundOverlayBodyState extends State { Offset _anchor = const Offset(0.5, 0.5); - Offset _dimensions = const Offset(1000, 1000); - // Index to be used as identifier for the ground overlay. // If position is changed to bounds and vice versa, the ground overlay will // be removed and added again with the new type. Also anchor can be given only @@ -101,8 +99,6 @@ class GroundOverlayBodyState extends State { groundOverlayId: id, image: assetMapBitmap, position: _currentGroundOverlayPos, - width: _dimensions.dx, - height: _dimensions.dy, anchor: _anchor, onTap: () { _onGroundOverlayTapped(); @@ -113,6 +109,7 @@ class GroundOverlayBodyState extends State { groundOverlayId: id, image: assetMapBitmap, bounds: _currentGroundOverlayBounds, + anchor: _anchor, onTap: () { _onGroundOverlayTapped(); }, @@ -148,20 +145,6 @@ class GroundOverlayBodyState extends State { }); } - Future _changeDimensions() async { - assert(_groundOverlay != null); - assert(_placingType == _GroundOverlayPlacing.position); - setState(() { - _dimensions = _dimensions == const Offset(1000, 1000) - ? const Offset(1500, 500) - : const Offset(1000, 1000); - }); - - // Re-add the ground overlay to apply the new position, as the position - // cannot be changed after the ground overlay is created on all platforms. - await _addGroundOverlay(); - } - Future _changePosition() async { assert(_groundOverlay != null); assert(_placingType == _GroundOverlayPlacing.position); @@ -185,6 +168,7 @@ class GroundOverlayBodyState extends State { ? _groundOverlayBounds2 : _groundOverlayBounds1; }); + // Re-add the ground overlay to apply the new position, as the position // cannot be changed after the ground overlay is created on all platforms. await _addGroundOverlay(); @@ -221,14 +205,13 @@ class GroundOverlayBodyState extends State { Future _changeAnchor() async { assert(_groundOverlay != null); - assert(_placingType == _GroundOverlayPlacing.position); setState(() { _anchor = _groundOverlay!.anchor == const Offset(0.5, 0.5) ? const Offset(1.0, 1.0) : const Offset(0.5, 0.5); }); - // Re-add the ground overlay to apply the new anchor as anchor cannot be + // Re-add the ground overlay to apply the new anchor, as anchor cannot be // changed after the ground overlay is created. await _addGroundOverlay(); } @@ -286,6 +269,10 @@ class GroundOverlayBodyState extends State { onPressed: _groundOverlay == null ? null : () => _changeZIndex(), child: const Text('change zIndex'), ), + TextButton( + onPressed: _groundOverlay == null ? null : () => _changeAnchor(), + child: const Text('change anchor'), + ), TextButton( onPressed: _groundOverlay == null ? null : () => _changeType(), child: Text(_placingType == _GroundOverlayPlacing.position @@ -299,20 +286,6 @@ class GroundOverlayBodyState extends State { : () => _changePosition(), child: const Text('change position'), ), - TextButton( - onPressed: _placingType != _GroundOverlayPlacing.position || - _groundOverlay == null - ? null - : () => _changeDimensions(), - child: const Text('change dimensions'), - ), - TextButton( - onPressed: _placingType != _GroundOverlayPlacing.position || - _groundOverlay == null - ? null - : () => _changeAnchor(), - child: const Text('change anchor'), - ), TextButton( onPressed: _placingType != _GroundOverlayPlacing.bounds || _groundOverlay == null From cdf4d25920131a6bce4ed5f21f2c1b3883c20dfe Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Tue, 11 Feb 2025 13:46:26 +0200 Subject: [PATCH 09/10] =?UTF-8?q?[google=5Fmaps=5Fflutter=5Fandroid]=C2=A0?= =?UTF-8?q?Improve=20ground=20overlay=20conversions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flutter/plugins/googlemaps/Convert.java | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index 6f0f25c043e4..e7e57521ec0a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java @@ -881,18 +881,12 @@ static Tile tileFromPigeon(Messages.PlatformTile tile) { sink.setClickable(groundOverlay.getClickable()); sink.setImage(toBitmapDescriptor(groundOverlay.getImage(), assetManager, density, wrapper)); if (groundOverlay.getPosition() != null) { - assert groundOverlay.getWidth() != null; - if (groundOverlay.getHeight() != null) { - sink.setPosition( - latLngFromPigeon(groundOverlay.getPosition()), - groundOverlay.getWidth().floatValue(), - groundOverlay.getHeight().floatValue()); - } else { - sink.setPosition( - latLngFromPigeon(groundOverlay.getPosition()), - groundOverlay.getWidth().floatValue(), - null); - } + assert groundOverlay.getWidth() != null + : "Width is required when using a ground overlay with a position."; + sink.setPosition( + latLngFromPigeon(groundOverlay.getPosition()), + groundOverlay.getWidth().floatValue(), + groundOverlay.getHeight() != null ? groundOverlay.getHeight().floatValue() : null); } else if (groundOverlay.getBounds() != null) { sink.setPositionFromBounds(latLngBoundsFromPigeon(groundOverlay.getBounds())); } @@ -965,17 +959,21 @@ static Tile tileFromPigeon(Messages.PlatformTile tile) { double height = bounds.northeast.latitude - bounds.southwest.latitude; double normalizedLatitude = 1.0 - ((position.latitude - bounds.southwest.latitude) / height); + // Constant for full circle degrees. + final double FULL_CIRCLE_DEGREES = 360.0; + // Calculate normalized longitude. // For longitude, if the bounds cross the antimeridian (west > east), // adjust the width accordingly. double west = bounds.southwest.longitude; double east = bounds.northeast.longitude; - double width = (west <= east) ? (east - west) : (360.0 - (west - east)); + double width = (west <= east) ? (east - west) : (FULL_CIRCLE_DEGREES - (west - east)); - // Adjust the position longitude if it is less than west by adding 360, - // then compute the normalized value. + // Normalize the longitude of the anchor position relative to the western boundary. + // Handles cases where the ground overlay crosses the antimeridian. double normalizedLongitude = - ((position.longitude < west ? position.longitude + 360.0 : position.longitude) - west) + ((position.longitude < west ? position.longitude + FULL_CIRCLE_DEGREES : position.longitude) + - west) / width; anchorBuilder.setX(normalizedLongitude); From 29a8e41713956c3fa781d237e87ab1fc8ce134e7 Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Fri, 14 Feb 2025 16:26:37 +0200 Subject: [PATCH 10/10] [google_maps_flutter_android] Address android review comments --- .../flutter/plugins/googlemaps/Convert.java | 13 ++- .../plugins/googlemaps/ConvertTest.java | 103 ++++++++++-------- .../integration_test/google_maps_tests.dart | 8 +- .../example/lib/ground_overlay.dart | 2 + .../integration_test/google_maps_test.dart | 5 + .../maps_example_dart/lib/ground_overlay.dart | 2 + 6 files changed, 82 insertions(+), 51 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index e7e57521ec0a..f52d3be5b4b2 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java @@ -41,6 +41,7 @@ import com.google.maps.android.heatmaps.Gradient; import com.google.maps.android.heatmaps.WeightedLatLng; import io.flutter.FlutterInjector; +import io.flutter.plugins.googlemaps.Messages.FlutterError; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -881,8 +882,12 @@ static Tile tileFromPigeon(Messages.PlatformTile tile) { sink.setClickable(groundOverlay.getClickable()); sink.setImage(toBitmapDescriptor(groundOverlay.getImage(), assetManager, density, wrapper)); if (groundOverlay.getPosition() != null) { - assert groundOverlay.getWidth() != null - : "Width is required when using a ground overlay with a position."; + if (groundOverlay.getWidth() == null) { + throw new FlutterError( + "Invalid GroundOverlay", + "Width is required when using a ground overlay with a position.", + null); + } sink.setPosition( latLngFromPigeon(groundOverlay.getPosition()), groundOverlay.getWidth().floatValue(), @@ -906,8 +911,8 @@ static Tile tileFromPigeon(Messages.PlatformTile tile) { @NonNull String groundOverlayId, boolean isCreatedWithBounds) { - // Dummy image is used as image is required field of PlatformGroundOverlay and converting image - // back to image descriptor is not currently supported. + // Dummy image is used as image is a required field of PlatformGroundOverlay and converting + // BitmapDescriptor used by Google Maps back to PlatformImageDescriptor is not currently supported. Messages.PlatformBitmap dummyImage = new Messages.PlatformBitmap.Builder() .setBitmap( diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java index 4af04f75c52a..7590eb53d364 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java @@ -692,6 +692,57 @@ public void buildGroundOverlayAnchorForPigeonWithCrossingMeridian() { Assert.assertEquals(0.5, anchor.getY(), 1e-15); } + private void assertGroundOverlayEquals( + Messages.PlatformGroundOverlay result, + GroundOverlay expectedOverlay, + String expectedId, + LatLng expectedPosition, + LatLngBounds expectedBounds) { + Assert.assertEquals(expectedId, result.getGroundOverlayId()); + if (expectedPosition != null) { + Assert.assertNotNull(result.getPosition()); + Assert.assertEquals(expectedPosition.latitude, result.getPosition().getLatitude(), 1e-15); + Assert.assertEquals(expectedPosition.longitude, result.getPosition().getLongitude(), 1e-15); + Assert.assertNotNull(result.getWidth()); + Assert.assertNotNull(result.getHeight()); + Assert.assertEquals(expectedOverlay.getWidth(), result.getWidth(), 1e-15); + Assert.assertEquals(expectedOverlay.getHeight(), result.getHeight(), 1e-15); + } else { + Assert.assertNull(result.getPosition()); + } + if (expectedBounds != null) { + Assert.assertNotNull(result.getBounds()); + Assert.assertEquals( + expectedBounds.southwest.latitude, + result.getBounds().getSouthwest().getLatitude(), + 1e-15); + Assert.assertEquals( + expectedBounds.southwest.longitude, + result.getBounds().getSouthwest().getLongitude(), + 1e-15); + Assert.assertEquals( + expectedBounds.northeast.latitude, + result.getBounds().getNortheast().getLatitude(), + 1e-15); + Assert.assertEquals( + expectedBounds.northeast.longitude, + result.getBounds().getNortheast().getLongitude(), + 1e-15); + } else { + Assert.assertNull(result.getBounds()); + } + + Assert.assertEquals(expectedOverlay.getBearing(), result.getBearing(), 1e-15); + Assert.assertEquals(expectedOverlay.getTransparency(), result.getTransparency(), 1e-6); + Assert.assertEquals(expectedOverlay.getZIndex(), result.getZIndex().intValue(), 1e-6); + Assert.assertEquals(expectedOverlay.isVisible(), result.getVisible()); + Assert.assertEquals(expectedOverlay.isClickable(), result.getClickable()); + Messages.PlatformDoublePair anchor = result.getAnchor(); + Assert.assertNotNull(anchor); + Assert.assertEquals(0.5, anchor.getX(), 1e-6); + Assert.assertEquals(0.5, anchor.getY(), 1e-6); + } + @Test public void groundOverlayToPigeonWithPosition() { GroundOverlay mockGroundOverlay = mock(GroundOverlay.class); @@ -709,28 +760,11 @@ public void groundOverlayToPigeonWithPosition() { when(mockGroundOverlay.isVisible()).thenReturn(true); when(mockGroundOverlay.isClickable()).thenReturn(false); + String overlayId = "overlay_1"; Messages.PlatformGroundOverlay result = - Convert.groundOverlayToPigeon(mockGroundOverlay, "overlay_1", false); - - Assert.assertEquals("overlay_1", result.getGroundOverlayId()); - Assert.assertNotNull(result.getPosition()); - Assert.assertEquals(position.latitude, result.getPosition().getLatitude(), 1e-15); - Assert.assertEquals(position.longitude, result.getPosition().getLongitude(), 1e-15); - Assert.assertNotNull(result.getWidth()); - Assert.assertNotNull(result.getHeight()); - Assert.assertEquals(30.0, result.getWidth(), 1e-15); - Assert.assertEquals(40.0, result.getHeight(), 1e-15); - Assert.assertEquals(50.0, result.getBearing(), 1e-15); - Assert.assertEquals(0.6, result.getTransparency(), 1e-6); - Assert.assertEquals(7, result.getZIndex().intValue()); - Assert.assertTrue(result.getVisible()); - Assert.assertFalse(result.getClickable()); - Assert.assertNull(result.getBounds()); + Convert.groundOverlayToPigeon(mockGroundOverlay, overlayId, false); - Messages.PlatformDoublePair anchor = result.getAnchor(); - Assert.assertNotNull(anchor); - Assert.assertEquals(0.5, anchor.getX(), 1e-6); - Assert.assertEquals(0.5, anchor.getY(), 1e-6); + assertGroundOverlayEquals(result, mockGroundOverlay, overlayId, position, null); } @Test @@ -750,34 +784,11 @@ public void groundOverlayToPigeonWithBounds() { when(mockGroundOverlay.isVisible()).thenReturn(true); when(mockGroundOverlay.isClickable()).thenReturn(false); + String overlayId = "overlay_2"; Messages.PlatformGroundOverlay result = - Convert.groundOverlayToPigeon(mockGroundOverlay, "overlay_2", true); - - Assert.assertEquals("overlay_2", result.getGroundOverlayId()); - Assert.assertNotNull(result.getBounds()); - Assert.assertEquals( - bounds.southwest.latitude, result.getBounds().getSouthwest().getLatitude(), 1e-15); - Assert.assertEquals( - bounds.southwest.longitude, result.getBounds().getSouthwest().getLongitude(), 1e-15); - Assert.assertEquals( - bounds.northeast.latitude, result.getBounds().getNortheast().getLatitude(), 1e-15); - Assert.assertEquals( - bounds.northeast.longitude, result.getBounds().getNortheast().getLongitude(), 1e-15); - Assert.assertNotNull(result.getWidth()); - Assert.assertNotNull(result.getHeight()); - Assert.assertEquals(30.0, result.getWidth(), 1e-15); - Assert.assertEquals(40.0, result.getHeight(), 1e-15); - Assert.assertEquals(50.0, result.getBearing(), 1e-15); - Assert.assertEquals(0.6, result.getTransparency(), 1e-6); - Assert.assertEquals(7, result.getZIndex().intValue()); - Assert.assertTrue(result.getVisible()); - Assert.assertFalse(result.getClickable()); - Assert.assertNull(result.getPosition()); + Convert.groundOverlayToPigeon(mockGroundOverlay, overlayId, true); - Messages.PlatformDoublePair anchor = result.getAnchor(); - Assert.assertNotNull(anchor); - Assert.assertEquals(0.5, anchor.getX(), 1e-6); - Assert.assertEquals(0.5, anchor.getY(), 1e-6); + assertGroundOverlayEquals(result, mockGroundOverlay, overlayId, null, bounds); } } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart index b709438138a7..618c4181df3b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; @@ -20,6 +21,11 @@ const double _kInitialZoomLevel = 5; const CameraPosition _kInitialCameraPosition = CameraPosition(target: _kInitialMapCenter, zoom: _kInitialZoomLevel); const String _kCloudMapId = '000000000000000'; // Dummy map ID. + +// The tolerance value for floating-point comparisons in the tests. +// This value was selected as the minimum possible value that the test passes. +// There are multiple float conversions and calculations when data is converted +// between Dart and platform implementations. const double _floatTolerance = 1e-8; void googleMapsTests() { @@ -995,7 +1001,7 @@ void googleMapsTests() { }, // TODO(cyanglaz): un-skip the test when we can test this on CI with API key enabled. // https://github.com/flutter/flutter/issues/57057 - skip: true); + skip: Platform.isAndroid); testWidgets( 'set tileOverlay correctly', diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/ground_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/ground_overlay.dart index abc706b409c4..a7029b643f52 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/ground_overlay.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/ground_overlay.dart @@ -131,6 +131,8 @@ class GroundOverlayBodyState extends State { void _setBearing() { assert(_groundOverlay != null); setState(() { + // Adjusts the bearing by 10 degrees, wrapping around at 360 degrees. + // 10 is the increment, 350 degrees of the full circle -10. _groundOverlay = _groundOverlay!.copyWith( bearingParam: _groundOverlay!.bearing >= 350 ? 0 diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart index 78e5abd16447..f80acd15ddd2 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart @@ -20,6 +20,11 @@ const double _kInitialZoomLevel = 5; const CameraPosition _kInitialCameraPosition = CameraPosition(target: _kInitialMapCenter, zoom: _kInitialZoomLevel); const String _kCloudMapId = '000000000000000'; // Dummy map ID. + +// The tolerance value for floating-point comparisons in the tests. +// This value was selected as the minimum possible value that the test passes. +// There are multiple float conversions and calculations when data is converted +// between Dart and platform implementations. const double _floatTolerance = 1e-6; void main() { diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/ground_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/ground_overlay.dart index 0dfac4c9a512..245634500316 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/ground_overlay.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/ground_overlay.dart @@ -128,6 +128,8 @@ class GroundOverlayBodyState extends State { void _setBearing() { assert(_groundOverlay != null); setState(() { + // Adjusts the bearing by 10 degrees, wrapping around at 360 degrees. + // 10 is the increment, 350 degrees of the full circle -10. _groundOverlay = _groundOverlay!.copyWith( bearingParam: _groundOverlay!.bearing >= 350 ? 0