diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/tiles_inspector.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/tiles_inspector.dart index d115257c07b4..b9b20bf9c0ff 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/tiles_inspector.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/tiles_inspector.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; @@ -22,7 +21,7 @@ void main() { } void runTests() { - const double floatTolerance = 1e-8; + const double floatTolerance = 1e-6; GoogleMapsFlutterPlatform.instance.enableDebugInspection(); @@ -208,269 +207,574 @@ void runTests() { ); }, skip: isWeb /* Tiles not supported on the web */); - /// Check that two lists of [WeightedLatLng] are more or less equal. - void expectHeatmapDataMoreOrLessEquals( - List data1, - List data2, - ) { - expect(data1.length, data2.length); - for (int i = 0; i < data1.length; i++) { - final WeightedLatLng wll1 = data1[i]; - final WeightedLatLng wll2 = data2[i]; - expect(wll1.weight, wll2.weight); - expect(wll1.point.latitude, moreOrLessEquals(wll2.point.latitude)); - expect(wll1.point.longitude, moreOrLessEquals(wll2.point.longitude)); + group('Heatmaps', () { + /// Check that two lists of [WeightedLatLng] are more or less equal. + void expectHeatmapDataMoreOrLessEquals( + List data1, + List data2, + ) { + expect(data1.length, data2.length); + for (int i = 0; i < data1.length; i++) { + final WeightedLatLng wll1 = data1[i]; + final WeightedLatLng wll2 = data2[i]; + expect(wll1.weight, wll2.weight); + expect(wll1.point.latitude, moreOrLessEquals(wll2.point.latitude)); + expect(wll1.point.longitude, moreOrLessEquals(wll2.point.longitude)); + } } - } - /// Check that two [HeatmapGradient]s are more or less equal. - void expectHeatmapGradientMoreOrLessEquals( - HeatmapGradient? gradient1, - HeatmapGradient? gradient2, - ) { - if (gradient1 == null || gradient2 == null) { - expect(gradient1, gradient2); - return; + /// Check that two [HeatmapGradient]s are more or less equal. + void expectHeatmapGradientMoreOrLessEquals( + HeatmapGradient? gradient1, + HeatmapGradient? gradient2, + ) { + if (gradient1 == null || gradient2 == null) { + expect(gradient1, gradient2); + return; + } + expect(gradient2, isNotNull); + + expect(gradient1.colors.length, gradient2.colors.length); + for (int i = 0; i < gradient1.colors.length; i++) { + final HeatmapGradientColor color1 = gradient1.colors[i]; + final HeatmapGradientColor color2 = gradient2.colors[i]; + expect(color1.color, color2.color); + expect( + color1.startPoint, + moreOrLessEquals(color2.startPoint, epsilon: floatTolerance), + ); + } + + expect(gradient1.colorMapSize, gradient2.colorMapSize); } - expect(gradient2, isNotNull); - expect(gradient1.colors.length, gradient2.colors.length); - for (int i = 0; i < gradient1.colors.length; i++) { - final HeatmapGradientColor color1 = gradient1.colors[i]; - final HeatmapGradientColor color2 = gradient2.colors[i]; - expect(color1.color, color2.color); + void expectHeatmapEquals(Heatmap heatmap1, Heatmap heatmap2) { + expectHeatmapDataMoreOrLessEquals(heatmap1.data, heatmap2.data); + expectHeatmapGradientMoreOrLessEquals( + heatmap1.gradient, heatmap2.gradient); + + // Only Android supports `maxIntensity` + // so the platform value is undefined on others. + bool canHandleMaxIntensity() { + return isAndroid; + } + + // Only iOS supports `minimumZoomIntensity` and `maximumZoomIntensity` + // so the platform value is undefined on others. + bool canHandleZoomIntensity() { + return isIOS; + } + + if (canHandleMaxIntensity()) { + expect(heatmap1.maxIntensity, heatmap2.maxIntensity); + } expect( - color1.startPoint, - moreOrLessEquals(color2.startPoint, epsilon: floatTolerance), + heatmap1.opacity, + moreOrLessEquals(heatmap2.opacity, epsilon: floatTolerance), ); + expect(heatmap1.radius, heatmap2.radius); + if (canHandleZoomIntensity()) { + expect(heatmap1.minimumZoomIntensity, heatmap2.minimumZoomIntensity); + expect(heatmap1.maximumZoomIntensity, heatmap2.maximumZoomIntensity); + } } - expect(gradient1.colorMapSize, gradient2.colorMapSize); - } - - void expectHeatmapEquals(Heatmap heatmap1, Heatmap heatmap2) { - expectHeatmapDataMoreOrLessEquals(heatmap1.data, heatmap2.data); - expectHeatmapGradientMoreOrLessEquals(heatmap1.gradient, heatmap2.gradient); - - // Only Android supports `maxIntensity` - // so the platform value is undefined on others. - bool canHandleMaxIntensity() { - return Platform.isAndroid; - } - - // Only iOS supports `minimumZoomIntensity` and `maximumZoomIntensity` - // so the platform value is undefined on others. - bool canHandleZoomIntensity() { - return Platform.isIOS; - } - - if (canHandleMaxIntensity()) { - expect(heatmap1.maxIntensity, heatmap2.maxIntensity); - } - expect( - heatmap1.opacity, - moreOrLessEquals(heatmap2.opacity, epsilon: floatTolerance), + const Heatmap heatmap1 = Heatmap( + heatmapId: HeatmapId('heatmap_1'), + data: [ + WeightedLatLng(LatLng(37.782, -122.447)), + WeightedLatLng(LatLng(37.782, -122.445)), + WeightedLatLng(LatLng(37.782, -122.443)), + WeightedLatLng(LatLng(37.782, -122.441)), + WeightedLatLng(LatLng(37.782, -122.439)), + WeightedLatLng(LatLng(37.782, -122.437)), + WeightedLatLng(LatLng(37.782, -122.435)), + WeightedLatLng(LatLng(37.785, -122.447)), + WeightedLatLng(LatLng(37.785, -122.445)), + WeightedLatLng(LatLng(37.785, -122.443)), + WeightedLatLng(LatLng(37.785, -122.441)), + WeightedLatLng(LatLng(37.785, -122.439)), + WeightedLatLng(LatLng(37.785, -122.437)), + WeightedLatLng(LatLng(37.785, -122.435), weight: 2) + ], + dissipating: false, + gradient: HeatmapGradient( + [ + HeatmapGradientColor( + Color.fromARGB(255, 0, 255, 255), + 0.2, + ), + HeatmapGradientColor( + Color.fromARGB(255, 0, 63, 255), + 0.4, + ), + HeatmapGradientColor( + Color.fromARGB(255, 0, 0, 191), + 0.6, + ), + HeatmapGradientColor( + Color.fromARGB(255, 63, 0, 91), + 0.8, + ), + HeatmapGradientColor( + Color.fromARGB(255, 255, 0, 0), + 1, + ), + ], + ), + maxIntensity: 1, + opacity: 0.5, + radius: HeatmapRadius.fromPixels(40), + minimumZoomIntensity: 1, + maximumZoomIntensity: 20, ); - expect(heatmap1.radius, heatmap2.radius); - if (canHandleZoomIntensity()) { - expect(heatmap1.minimumZoomIntensity, heatmap2.minimumZoomIntensity); - expect(heatmap1.maximumZoomIntensity, heatmap2.maximumZoomIntensity); - } - } - const Heatmap heatmap1 = Heatmap( - heatmapId: HeatmapId('heatmap_1'), - data: [ - WeightedLatLng(LatLng(37.782, -122.447)), - WeightedLatLng(LatLng(37.782, -122.445)), - WeightedLatLng(LatLng(37.782, -122.443)), - WeightedLatLng(LatLng(37.782, -122.441)), - WeightedLatLng(LatLng(37.782, -122.439)), - WeightedLatLng(LatLng(37.782, -122.437)), - WeightedLatLng(LatLng(37.782, -122.435)), - WeightedLatLng(LatLng(37.785, -122.447)), - WeightedLatLng(LatLng(37.785, -122.445)), - WeightedLatLng(LatLng(37.785, -122.443)), - WeightedLatLng(LatLng(37.785, -122.441)), - WeightedLatLng(LatLng(37.785, -122.439)), - WeightedLatLng(LatLng(37.785, -122.437)), - WeightedLatLng(LatLng(37.785, -122.435), weight: 2) - ], - dissipating: false, - gradient: HeatmapGradient( - [ - HeatmapGradientColor( - Color.fromARGB(255, 0, 255, 255), - 0.2, + testWidgets('set heatmap correctly', (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final Heatmap heatmap2 = Heatmap( + heatmapId: const HeatmapId('heatmap_2'), + data: heatmap1.data, + dissipating: heatmap1.dissipating, + gradient: heatmap1.gradient, + maxIntensity: heatmap1.maxIntensity, + opacity: heatmap1.opacity - 0.1, + radius: heatmap1.radius, + minimumZoomIntensity: heatmap1.minimumZoomIntensity, + maximumZoomIntensity: heatmap1.maximumZoomIntensity, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: kInitialCameraPosition, + heatmaps: {heatmap1, heatmap2}, + onMapCreated: (GoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), ), - HeatmapGradientColor( - Color.fromARGB(255, 0, 63, 255), - 0.4, + ); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + if (inspector.supportsGettingHeatmapInfo()) { + final Heatmap heatmapInfo1 = + (await inspector.getHeatmapInfo(heatmap1.mapsId, mapId: mapId))!; + final Heatmap heatmapInfo2 = + (await inspector.getHeatmapInfo(heatmap2.mapsId, mapId: mapId))!; + + expectHeatmapEquals(heatmap1, heatmapInfo1); + expectHeatmapEquals(heatmap2, heatmapInfo2); + } + }); + + testWidgets('update heatmaps correctly', (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final Key key = GlobalKey(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: kInitialCameraPosition, + heatmaps: {heatmap1}, + onMapCreated: (GoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), ), - HeatmapGradientColor( - Color.fromARGB(255, 0, 0, 191), - 0.6, + ); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + final Heatmap heatmap1New = heatmap1.copyWith( + dataParam: heatmap1.data.sublist(5), + dissipatingParam: !heatmap1.dissipating, + gradientParam: heatmap1.gradient, + maxIntensityParam: heatmap1.maxIntensity! + 1, + opacityParam: heatmap1.opacity - 0.1, + radiusParam: HeatmapRadius.fromPixels(heatmap1.radius.radius + 1), + minimumZoomIntensityParam: heatmap1.minimumZoomIntensity + 1, + maximumZoomIntensityParam: heatmap1.maximumZoomIntensity + 1, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: kInitialCameraPosition, + heatmaps: {heatmap1New}, + onMapCreated: (GoogleMapController controller) { + fail('update: OnMapCreated should get called only once.'); + }, + ), ), - HeatmapGradientColor( - Color.fromARGB(255, 63, 0, 91), - 0.8, + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + if (inspector.supportsGettingHeatmapInfo()) { + final Heatmap heatmapInfo1 = + (await inspector.getHeatmapInfo(heatmap1.mapsId, mapId: mapId))!; + + expectHeatmapEquals(heatmap1New, heatmapInfo1); + } + }); + + testWidgets('remove heatmaps correctly', (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final Key key = GlobalKey(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: kInitialCameraPosition, + heatmaps: {heatmap1}, + onMapCreated: (GoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), ), - HeatmapGradientColor( - Color.fromARGB(255, 255, 0, 0), - 1, + ); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), ), - ], - ), - maxIntensity: 1, - opacity: 0.5, - radius: HeatmapRadius.fromPixels(40), - minimumZoomIntensity: 1, - maximumZoomIntensity: 20, - ); + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); - testWidgets('set heatmap correctly', (WidgetTester tester) async { - final Completer mapIdCompleter = Completer(); - final Heatmap heatmap2 = Heatmap( - heatmapId: const HeatmapId('heatmap_2'), - data: heatmap1.data, - dissipating: heatmap1.dissipating, - gradient: heatmap1.gradient, - maxIntensity: heatmap1.maxIntensity, - opacity: heatmap1.opacity - 0.1, - radius: heatmap1.radius, - minimumZoomIntensity: heatmap1.minimumZoomIntensity, - maximumZoomIntensity: heatmap1.maximumZoomIntensity, + if (inspector.supportsGettingHeatmapInfo()) { + final Heatmap? heatmapInfo1 = + await inspector.getHeatmapInfo(heatmap1.mapsId, mapId: mapId); + + expect(heatmapInfo1, isNull); + } + }); + }); + + group('GroundOverlay', () { + final LatLngBounds kGroundOverlayBounds = LatLngBounds( + southwest: const LatLng(37.77483, -122.41942), + northeast: const LatLng(37.78183, -122.39105), ); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - initialCameraPosition: kInitialCameraPosition, - heatmaps: {heatmap1, heatmap2}, - onMapCreated: (GoogleMapController controller) { - mapIdCompleter.complete(controller.mapId); - }, - ), + final GroundOverlay groundOverlayBounds1 = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('bounds_1'), + bounds: kGroundOverlayBounds, + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, ), + transparency: 0.7, + bearing: 10, + zIndex: 10, ); - await tester.pumpAndSettle(const Duration(seconds: 3)); - final int mapId = await mapIdCompleter.future; - final GoogleMapsInspectorPlatform inspector = - GoogleMapsInspectorPlatform.instance!; + 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, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + zoomLevel: 14.0, + ); - if (inspector.supportsGettingHeatmapInfo()) { - final Heatmap heatmapInfo1 = - (await inspector.getHeatmapInfo(heatmap1.mapsId, mapId: mapId))!; - final Heatmap heatmapInfo2 = - (await inspector.getHeatmapInfo(heatmap2.mapsId, mapId: mapId))!; + void expectGroundOverlayEquals( + GroundOverlay source, GroundOverlay response) { + expect(response.groundOverlayId, source.groundOverlayId); + expect( + response.transparency, + moreOrLessEquals(source.transparency, epsilon: floatTolerance), + ); - expectHeatmapEquals(heatmap1, heatmapInfo1); - expectHeatmapEquals(heatmap2, heatmapInfo2); + // Web does not support bearing. + if (!isWeb) { + 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); + + // Web does not support zIndex. + if (!isWeb) { + expect(response.zIndex, source.zIndex); + } + + // Only Android supports width and height. + if (isAndroid) { + expect(response.width, source.width); + expect(response.height, source.height); + } + + // Only iOS supports zoomLevel. + if (isIOS) { + expect(response.zoomLevel, source.zoomLevel); + } + + // Only Android (using position) and iOS supports `anchor`. + if ((isAndroid && source.position != null) || isIOS) { + expect( + response.anchor?.dx, + moreOrLessEquals(source.anchor!.dx, epsilon: floatTolerance), + ); + expect( + response.anchor?.dy, + moreOrLessEquals(source.anchor!.dy, epsilon: floatTolerance), + ); + } } - }); - - testWidgets('update heatmaps correctly', (WidgetTester tester) async { - final Completer mapIdCompleter = Completer(); - final Key key = GlobalKey(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: kInitialCameraPosition, - heatmaps: {heatmap1}, - onMapCreated: (GoogleMapController controller) { - mapIdCompleter.complete(controller.mapId); - }, - ), - ), - ); - final int mapId = await mapIdCompleter.future; - final GoogleMapsInspectorPlatform inspector = - GoogleMapsInspectorPlatform.instance!; - - final Heatmap heatmap1New = heatmap1.copyWith( - dataParam: heatmap1.data.sublist(5), - dissipatingParam: !heatmap1.dissipating, - gradientParam: heatmap1.gradient, - maxIntensityParam: heatmap1.maxIntensity! + 1, - opacityParam: heatmap1.opacity - 0.1, - radiusParam: HeatmapRadius.fromPixels(heatmap1.radius.radius + 1), - minimumZoomIntensityParam: heatmap1.minimumZoomIntensity + 1, - maximumZoomIntensityParam: heatmap1.maximumZoomIntensity + 1, - ); + 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: GoogleMap( - key: key, - initialCameraPosition: kInitialCameraPosition, - heatmaps: {heatmap1New}, - onMapCreated: (GoogleMapController controller) { - fail('update: OnMapCreated should get called only once.'); - }, + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: kInitialCameraPosition, + groundOverlays: { + groundOverlayBounds1, + groundOverlayBounds2, + // Web does not support position-based ground overlays. + if (!isWeb) groundOverlayPosition1, + }, + onMapCreated: (GoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), ), - ), - ); + ); + await tester.pumpAndSettle(const Duration(seconds: 3)); - await tester.pumpAndSettle(const Duration(seconds: 3)); + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; - if (inspector.supportsGettingHeatmapInfo()) { - final Heatmap heatmapInfo1 = - (await inspector.getHeatmapInfo(heatmap1.mapsId, mapId: mapId))!; + if (inspector.supportsGettingGroundOverlayInfo()) { + final GroundOverlay groundOverlayBoundsInfo1 = (await inspector + .getGroundOverlayInfo(groundOverlayBounds1.mapsId, mapId: mapId))!; + final GroundOverlay groundOverlayBoundsInfo2 = (await inspector + .getGroundOverlayInfo(groundOverlayBounds2.mapsId, mapId: mapId))!; - expectHeatmapEquals(heatmap1New, heatmapInfo1); - } - }); + expectGroundOverlayEquals( + groundOverlayBounds1, + groundOverlayBoundsInfo1, + ); + expectGroundOverlayEquals( + groundOverlayBounds2, + groundOverlayBoundsInfo2, + ); - testWidgets('remove heatmaps correctly', (WidgetTester tester) async { - final Completer mapIdCompleter = Completer(); - final Key key = GlobalKey(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: kInitialCameraPosition, - heatmaps: {heatmap1}, - onMapCreated: (GoogleMapController controller) { - mapIdCompleter.complete(controller.mapId); - }, + // Web does not support position-based ground overlays. + if (!isWeb) { + final GroundOverlay groundOverlayPositionInfo1 = (await inspector + .getGroundOverlayInfo(groundOverlayPosition1.mapsId, + mapId: mapId))!; + 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: GoogleMap( + key: key, + initialCameraPosition: kInitialCameraPosition, + groundOverlays: { + groundOverlayBounds1, + // Web does not support position-based ground overlays. + if (!isWeb) groundOverlayPosition1 + }, + onMapCreated: (GoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), ), - ), - ); + ); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + final GroundOverlay groundOverlayBounds1New = + groundOverlayBounds1.copyWith( + bearingParam: 10, + clickableParam: false, + visibleParam: false, + transparencyParam: 0.5, + zIndexParam: 10, + ); - final int mapId = await mapIdCompleter.future; - final GoogleMapsInspectorPlatform inspector = - GoogleMapsInspectorPlatform.instance!; - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: kInitialCameraPosition, - onMapCreated: (GoogleMapController controller) { - fail('OnMapCreated should get called only once.'); - }, + final GroundOverlay groundOverlayPosition1New = + groundOverlayPosition1.copyWith( + bearingParam: 10, + clickableParam: false, + visibleParam: false, + transparencyParam: 0.5, + zIndexParam: 10, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: kInitialCameraPosition, + groundOverlays: { + groundOverlayBounds1New, + // Web does not support position-based ground overlays. + if (!isWeb) groundOverlayPosition1New + }, + onMapCreated: (GoogleMapController controller) { + fail('update: OnMapCreated should get called only once.'); + }, + ), ), - ), - ); + ); - await tester.pumpAndSettle(const Duration(seconds: 3)); + await tester.pumpAndSettle(const Duration(seconds: 3)); - if (inspector.supportsGettingHeatmapInfo()) { - final Heatmap? heatmapInfo1 = - await inspector.getHeatmapInfo(heatmap1.mapsId, mapId: mapId); + if (inspector.supportsGettingGroundOverlayInfo()) { + final GroundOverlay groundOverlayBounds1Info = (await inspector + .getGroundOverlayInfo(groundOverlayBounds1.mapsId, mapId: mapId))!; - expect(heatmapInfo1, isNull); - } + expectGroundOverlayEquals( + groundOverlayBounds1New, + groundOverlayBounds1Info, + ); + + // Web does not support position-based ground overlays. + if (!isWeb) { + final GroundOverlay groundOverlayPosition1Info = (await inspector + .getGroundOverlayInfo(groundOverlayPosition1.mapsId, + mapId: mapId))!; + + 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: GoogleMap( + key: key, + initialCameraPosition: kInitialCameraPosition, + groundOverlays: { + groundOverlayBounds1, + // Web does not support position-based ground overlays. + if (!isWeb) groundOverlayPosition1 + }, + onMapCreated: (GoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: kInitialCameraPosition, + onMapCreated: (GoogleMapController 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); + expect(groundOverlayBounds1Info, isNull); + + // Web does not support position-based ground overlays. + if (!isWeb) { + final GroundOverlay? groundOverlayPositionInfo = await inspector + .getGroundOverlayInfo(groundOverlayPosition1.mapsId, + mapId: mapId); + expect(groundOverlayPositionInfo, isNull); + } + } + }); }); } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/ground_overlay.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/ground_overlay.dart new file mode 100644 index 000000000000..748eb04590ca --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/ground_overlay.dart @@ -0,0 +1,333 @@ +// 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/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.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(); + + GoogleMapController? 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(GoogleMapController 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, // Android only + height: _dimensions.dy, // Android only + zoomLevel: 14.0, // iOS only + 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 bounds as the bounds 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 change the positioning type. + 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 the 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: GoogleMap( + 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'), + ), + if (!kIsWeb) + TextButton( + onPressed: _groundOverlay == null ? null : () => _setBearing(), + child: const Text('change bearing'), + ), + TextButton( + onPressed: _groundOverlay == null ? null : () => _toggleVisible(), + child: const Text('toggle visible'), + ), + if (!kIsWeb) + TextButton( + onPressed: + _groundOverlay == null ? null : () => _changeZIndex(), + child: const Text('change zIndex'), + ), + if (!kIsWeb) + TextButton( + onPressed: + _groundOverlay == null ? null : () => _changeAnchor(), + child: const Text('change anchor'), + ), + if (!kIsWeb) + TextButton( + onPressed: _groundOverlay == null ? null : () => _changeType(), + child: Text(_placingType == _GroundOverlayPlacing.position + ? 'use bounds' + : 'use position'), + ), + if (!kIsWeb) + TextButton( + onPressed: _placingType != _GroundOverlayPlacing.position || + _groundOverlay == null + ? null + : () => _changePosition(), + child: const Text('change position'), + ), + if (defaultTargetPlatform == TargetPlatform.android) + 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/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart index 73a47db723e8..db7f38f9f8ab 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter/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 'heatmap.dart'; import 'lite_mode.dart'; import 'map_click.dart'; @@ -44,6 +45,7 @@ final List _allPages = [ const SnapshotPage(), const LiteModePage(), const TileOverlayPage(), + const GroundOverlayPage(), const ClusteringPage(), const MapIdPage(), const HeatmapPage(), diff --git a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml index 0a949e262bbe..07292b8ea83b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -33,3 +33,25 @@ flutter: uses-material-design: true assets: - assets/ + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + { + google_maps_flutter_android: + { + path: ../../../../packages/google_maps_flutter/google_maps_flutter_android, + }, + google_maps_flutter_ios: + { + path: ../../../../packages/google_maps_flutter/google_maps_flutter_ios, + }, + google_maps_flutter_platform_interface: + { + path: ../../../../packages/google_maps_flutter/google_maps_flutter_platform_interface, + }, + google_maps_flutter_web: + { + path: ../../../../packages/google_maps_flutter/google_maps_flutter_web, + }, + } diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart index 66952bbe062d..d24f6f0995ff 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart @@ -29,6 +29,8 @@ export 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf Cluster, ClusterManager, ClusterManagerId, + GroundOverlay, + GroundOverlayId, Heatmap, HeatmapGradient, HeatmapGradientColor, diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart index 5dd1cfdfd2ad..589058989989 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart @@ -74,6 +74,9 @@ class GoogleMapController { 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)); @@ -118,6 +121,18 @@ class GoogleMapController { .updateClusterManagers(clusterManagerUpdates, mapId: mapId); } + /// Updates ground overlay configuration. + /// + /// Change listeners are notified once the update has been made on the + /// platform side. + /// + /// The returned [Future] completes after listeners have been notified. + Future _updateGroundOverlays( + GroundOverlayUpdates groundOverlayUpdates) { + return GoogleMapsFlutterPlatform.instance + .updateGroundOverlays(groundOverlayUpdates, mapId: mapId); + } + /// Updates polygon configuration. /// /// Change listeners are notified once the update has been made on the diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart index 888195ca36d2..3f63f8bbfaad 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart @@ -124,6 +124,7 @@ class GoogleMap extends StatefulWidget { this.heatmaps = const {}, this.onCameraMoveStarted, this.tileOverlays = const {}, + this.groundOverlays = const {}, this.onCameraMove, this.onCameraIdle, this.onTap, @@ -225,6 +226,34 @@ class GoogleMap extends StatefulWidget { /// Cluster Managers to be initialized for the map. final Set clusterManagers; + /// Ground overlays to be initialized for the map. + /// + /// Support table for Ground Overlay features: + /// | Feature | Android | iOS | Web | + /// |-----------------------------|--------------------------|--------------------------|-----| + /// | [GroundOverlay.image] | Yes | Yes | Yes | + /// | [GroundOverlay.bounds] | Yes | Yes | Yes | + /// | [GroundOverlay.position] | Yes | Yes | No | + /// | [GroundOverlay.width] | Yes (with position only) | No | No | + /// | [GroundOverlay.height] | Yes (with position only) | No | No | + /// | [GroundOverlay.anchor] | Yes | Yes | No | + /// | [GroundOverlay.zoomLevel] | No | Yes (with position only) | No | + /// | [GroundOverlay.bearing] | Yes | Yes | No | + /// | [GroundOverlay.transparency]| Yes | Yes | Yes | + /// | [GroundOverlay.zIndex] | Yes | Yes | No | + /// | [GroundOverlay.visible] | Yes | Yes | Yes | + /// | [GroundOverlay.clickable] | Yes | Yes | Yes | + /// | [GroundOverlay.onTap] | Yes | Yes | Yes | + /// + /// - On Android, [GroundOverlay.width] is required if + /// [GroundOverlay.position] is set. + /// - On iOS, [GroundOverlay.zoomLevel] is required if + /// [GroundOverlay.position] is set. + /// - [GroundOverlay.image] must be a [MapBitmap]. See [AssetMapBitmap] and + /// [BytesMapBitmap]. [MapBitmap.bitmapScaling] must be set to + /// [MapBitmapScaling.none]. + final Set groundOverlays; + /// Called when the camera starts moving. /// /// This can be initiated by the following: @@ -339,6 +368,8 @@ class _GoogleMapState extends State { Map _clusterManagers = {}; Map _heatmaps = {}; + Map _groundOverlays = + {}; late MapConfiguration _mapConfiguration; @override @@ -360,6 +391,7 @@ class _GoogleMapState extends State { circles: widget.circles, clusterManagers: widget.clusterManagers, heatmaps: widget.heatmaps, + groundOverlays: widget.groundOverlays, ), mapConfiguration: _mapConfiguration, ); @@ -375,6 +407,7 @@ class _GoogleMapState extends State { _polylines = keyByPolylineId(widget.polylines); _circles = keyByCircleId(widget.circles); _heatmaps = keyByHeatmapId(widget.heatmaps); + _groundOverlays = keyByGroundOverlayId(widget.groundOverlays); } @override @@ -399,6 +432,7 @@ class _GoogleMapState extends State { _updateCircles(); _updateHeatmaps(); _updateTileOverlays(); + _updateGroundOverlays(); } Future _updateOptions() async { @@ -426,6 +460,13 @@ class _GoogleMapState extends State { _clusterManagers = keyByClusterManagerId(widget.clusterManagers); } + Future _updateGroundOverlays() async { + final GoogleMapController controller = await _controller.future; + unawaited(controller._updateGroundOverlays(GroundOverlayUpdates.from( + _groundOverlays.values.toSet(), widget.groundOverlays))); + _groundOverlays = keyByGroundOverlayId(widget.groundOverlays); + } + Future _updatePolygons() async { final GoogleMapController controller = await _controller.future; unawaited(controller._updatePolygons( @@ -553,6 +594,17 @@ class _GoogleMapState extends State { } } + void onGroundOverlayTap(GroundOverlayId groundOverlayId) { + final GroundOverlay? groundOverlay = _groundOverlays[groundOverlayId]; + if (groundOverlay == null) { + throw UnknownMapObjectIdError('groundOverlay', groundOverlayId, 'onTap'); + } + final VoidCallback? onTap = groundOverlay.onTap; + if (onTap != null) { + onTap(); + } + } + void onInfoWindowTap(MarkerId markerId) { final Marker? marker = _markers[markerId]; if (marker == null) { diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 6afe27288aaf..209ee5d9b106 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -41,3 +41,21 @@ topics: # The example deliberately includes limited-use secrets. false_secrets: - /example/web/index.html + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + { + google_maps_flutter_android: + { + path: ../../../packages/google_maps_flutter/google_maps_flutter_android, + }, + google_maps_flutter_ios: + { path: ../../../packages/google_maps_flutter/google_maps_flutter_ios }, + google_maps_flutter_platform_interface: + { + path: ../../../packages/google_maps_flutter/google_maps_flutter_platform_interface, + }, + google_maps_flutter_web: + { path: ../../../packages/google_maps_flutter/google_maps_flutter_web }, + } diff --git a/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart index df83400c416b..07c99b53807d 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart @@ -112,6 +112,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, { @@ -264,6 +273,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; @@ -307,6 +321,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)); @@ -330,4 +346,6 @@ class PlatformMapStateRecorder { final List> tileOverlaySets = >[]; final List clusterManagerUpdates = []; + final List groundOverlayUpdates = + []; } diff --git a/packages/google_maps_flutter/google_maps_flutter/test/groundoverlay_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/groundoverlay_updates_test.dart new file mode 100644 index 000000000000..79b551021086 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/test/groundoverlay_updates_test.dart @@ -0,0 +1,473 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'fake_google_maps_flutter_platform.dart'; + +Widget _mapWithMarkers(Set groundOverlays) { + return Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + groundOverlays: groundOverlays, + ), + ); +} + +void main() { + final LatLngBounds kGroundOverlayBounds = LatLngBounds( + southwest: const LatLng(37.77483, -122.41942), + northeast: const LatLng(37.78183, -122.39105), + ); + + late FakeGoogleMapsFlutterPlatform platform; + + setUp(() { + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; + }); + + testWidgets('Initializing a groundOverlay', (WidgetTester tester) async { + final GroundOverlay go1 = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('go_1'), + bounds: kGroundOverlayBounds, + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + ); + + final GroundOverlay go2 = GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('go_2'), + 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, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + zoomLevel: 14.0, + ); + + await tester.pumpWidget(_mapWithMarkers({go1, go2})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.groundOverlayUpdates.last.groundOverlaysToAdd.length, 2); + + final Set initializedGroundOverlays = + map.groundOverlayUpdates.last.groundOverlaysToAdd; + + expect(initializedGroundOverlays.first, equals(go1)); + expect(initializedGroundOverlays.last, equals(go2)); + expect( + map.groundOverlayUpdates.last.groundOverlayIdsToRemove.isEmpty, true); + expect(map.groundOverlayUpdates.last.groundOverlaysToChange.isEmpty, true); + }); + + testWidgets('Adding a groundOverlay', (WidgetTester tester) async { + final GroundOverlay go1 = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('go_1'), + bounds: kGroundOverlayBounds, + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + ); + + final GroundOverlay go2 = GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('go_2'), + 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, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + zoomLevel: 14.0, + ); + + await tester.pumpWidget(_mapWithMarkers({go1})); + await tester.pumpWidget(_mapWithMarkers({go1, go2})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.groundOverlayUpdates.last.groundOverlaysToAdd.length, 1); + + final GroundOverlay addedMarker = + map.groundOverlayUpdates.last.groundOverlaysToAdd.first; + expect(addedMarker, equals(go2)); + + expect( + map.groundOverlayUpdates.last.groundOverlayIdsToRemove.isEmpty, true); + + expect(map.groundOverlayUpdates.last.groundOverlaysToChange.isEmpty, true); + }); + + testWidgets('Removing a groundOverlay', (WidgetTester tester) async { + final GroundOverlay go1 = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('go_1'), + bounds: kGroundOverlayBounds, + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + ); + + await tester.pumpWidget(_mapWithMarkers({go1})); + await tester.pumpWidget(_mapWithMarkers({})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.groundOverlayUpdates.last.groundOverlayIdsToRemove.length, 1); + expect(map.groundOverlayUpdates.last.groundOverlayIdsToRemove.first, + equals(go1.groundOverlayId)); + + expect(map.groundOverlayUpdates.last.groundOverlaysToChange.isEmpty, true); + expect(map.groundOverlayUpdates.last.groundOverlaysToAdd.isEmpty, true); + }); + + testWidgets('Updating a groundOverlay', (WidgetTester tester) async { + final GroundOverlay go1 = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('go_1'), + bounds: kGroundOverlayBounds, + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + ); + + final GroundOverlay go2 = go1.copyWith(visibleParam: false); + + await tester.pumpWidget(_mapWithMarkers({go1})); + await tester.pumpWidget(_mapWithMarkers({go2})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.groundOverlayUpdates.last.groundOverlaysToChange.length, 1); + expect(map.groundOverlayUpdates.last.groundOverlaysToChange.first, + equals(go2)); + + expect( + map.groundOverlayUpdates.last.groundOverlayIdsToRemove.isEmpty, true); + expect(map.groundOverlayUpdates.last.groundOverlaysToAdd.isEmpty, true); + }); + + testWidgets('Multi Update', (WidgetTester tester) async { + GroundOverlay go1 = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('go_1'), + bounds: kGroundOverlayBounds, + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + ); + + GroundOverlay go2 = GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('go_2'), + 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, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + zoomLevel: 14.0, + ); + + final Set prev = {go1, go2}; + go1 = go1.copyWith(visibleParam: false); + go2 = go2.copyWith(clickableParam: false); + final Set cur = {go1, go2}; + + await tester.pumpWidget(_mapWithMarkers(prev)); + await tester.pumpWidget(_mapWithMarkers(cur)); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.groundOverlayUpdates.last.groundOverlaysToChange, cur); + expect( + map.groundOverlayUpdates.last.groundOverlayIdsToRemove.isEmpty, true); + expect(map.groundOverlayUpdates.last.groundOverlaysToAdd.isEmpty, true); + }); + + testWidgets('Multi Update', (WidgetTester tester) async { + final GroundOverlay go1 = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('go_1'), + bounds: kGroundOverlayBounds, + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + ); + + GroundOverlay go2 = GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('go_2'), + 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, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + zoomLevel: 14.0, + ); + + final GroundOverlay go3 = GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('go_3'), + position: kGroundOverlayBounds.southwest, + width: 100, + height: 100, + anchor: const Offset(0.1, 0.2), + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + zoomLevel: 14.0, + ); + + final Set prev = {go2, go3}; + + // go1 is added, go2 is updated, go3 is removed. + go2 = go2.copyWith(clickableParam: false); + final Set cur = {go1, go2}; + + await tester.pumpWidget(_mapWithMarkers(prev)); + await tester.pumpWidget(_mapWithMarkers(cur)); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.groundOverlayUpdates.last.groundOverlaysToChange.length, 1); + expect(map.groundOverlayUpdates.last.groundOverlaysToAdd.length, 1); + expect(map.groundOverlayUpdates.last.groundOverlayIdsToRemove.length, 1); + + expect(map.groundOverlayUpdates.last.groundOverlaysToChange.first, + equals(go2)); + expect( + map.groundOverlayUpdates.last.groundOverlaysToAdd.first, equals(go1)); + expect(map.groundOverlayUpdates.last.groundOverlayIdsToRemove.first, + equals(go3.groundOverlayId)); + }); + + testWidgets('Partial Update', (WidgetTester tester) async { + final GroundOverlay go1 = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('go_1'), + bounds: kGroundOverlayBounds, + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + ); + + final GroundOverlay go2 = GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('go_2'), + 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, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + zoomLevel: 14.0, + ); + + GroundOverlay go3 = GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('go_3'), + position: kGroundOverlayBounds.southwest, + width: 100, + height: 100, + anchor: const Offset(0.1, 0.2), + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + zoomLevel: 14.0, + ); + final Set prev = {go1, go2, go3}; + go3 = go3.copyWith(visibleParam: false); + final Set cur = {go1, go2, go3}; + + await tester.pumpWidget(_mapWithMarkers(prev)); + await tester.pumpWidget(_mapWithMarkers(cur)); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.groundOverlayUpdates.last.groundOverlaysToChange, + {go3}); + expect( + map.groundOverlayUpdates.last.groundOverlayIdsToRemove.isEmpty, true); + expect(map.groundOverlayUpdates.last.groundOverlaysToAdd.isEmpty, true); + }); + + testWidgets('Update non platform related attr', (WidgetTester tester) async { + GroundOverlay go1 = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('go_1'), + bounds: kGroundOverlayBounds, + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + ); + final Set prev = {go1}; + go1 = go1.copyWith( + onTapParam: () {}, + ); + final Set cur = {go1}; + + await tester.pumpWidget(_mapWithMarkers(prev)); + await tester.pumpWidget(_mapWithMarkers(cur)); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.groundOverlayUpdates.last.groundOverlaysToChange.isEmpty, true); + expect( + map.groundOverlayUpdates.last.groundOverlayIdsToRemove.isEmpty, true); + expect(map.groundOverlayUpdates.last.groundOverlaysToAdd.isEmpty, true); + }); + + testWidgets('multi-update with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + final GroundOverlay go1 = GroundOverlay.fromBounds( + groundOverlayId: const GroundOverlayId('go_1'), + bounds: kGroundOverlayBounds, + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + ); + + final GroundOverlay go2 = GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('go_2'), + 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, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + zoomLevel: 14.0, + ); + + final GroundOverlay go3 = GroundOverlay.fromPosition( + groundOverlayId: const GroundOverlayId('go_3'), + position: kGroundOverlayBounds.southwest, + width: 100, + height: 100, + anchor: const Offset(0.1, 0.2), + image: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ), + transparency: 0.7, + bearing: 10, + zIndex: 10, + zoomLevel: 14.0, + ); + + final GroundOverlay go3updated = go3.copyWith(visibleParam: false); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithMarkers({go1, go2})); + await tester.pumpWidget(_mapWithMarkers({go1, go3})); + await tester.pumpWidget(_mapWithMarkers({go1, go3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.groundOverlayUpdates.length, 3); + + expect(map.groundOverlayUpdates[0].groundOverlaysToChange.isEmpty, true); + expect(map.groundOverlayUpdates[0].groundOverlaysToAdd, + {go1, go2}); + expect(map.groundOverlayUpdates[0].groundOverlayIdsToRemove.isEmpty, true); + + expect(map.groundOverlayUpdates[1].groundOverlaysToChange.isEmpty, true); + expect( + map.groundOverlayUpdates[1].groundOverlaysToAdd, {go3}); + expect(map.groundOverlayUpdates[1].groundOverlayIdsToRemove, + {go2.groundOverlayId}); + + expect(map.groundOverlayUpdates[2].groundOverlaysToChange, + {go3updated}); + expect(map.groundOverlayUpdates[2].groundOverlaysToAdd.isEmpty, true); + expect(map.groundOverlayUpdates[2].groundOverlayIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); +} 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..d389c2ff458a 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 @@ -33,3 +33,13 @@ flutter: uses-material-design: true assets: - assets/ + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + { + google_maps_flutter_platform_interface: + { + path: ../../../../packages/google_maps_flutter/google_maps_flutter_platform_interface, + }, + } 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..cc03d165d221 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml @@ -37,3 +37,13 @@ topics: - google-maps - google-maps-flutter - map + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + { + google_maps_flutter_platform_interface: + { + path: ../../../packages/google_maps_flutter/google_maps_flutter_platform_interface, + }, + } 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>); } 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..73e9e11575c0 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 @@ -15,7 +15,6 @@ - (UIImage *)createOnePixelImage; @implementation ExtractIconFromDataTests - (void)testExtractIconFromDataAssetAuto { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); id mockImageClass = OCMClassMock([UIImage class]); @@ -32,9 +31,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 +41,6 @@ - (void)testExtractIconFromDataAssetAuto { } - (void)testExtractIconFromDataAssetAutoWithScale { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); id mockImageClass = OCMClassMock([UIImage class]); @@ -60,9 +58,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 +68,6 @@ - (void)testExtractIconFromDataAssetAutoWithScale { } - (void)testExtractIconFromDataAssetAutoAndSizeWithSameAspectRatio { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); id mockImageClass = OCMClassMock([UIImage class]); @@ -91,9 +87,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 +102,6 @@ - (void)testExtractIconFromDataAssetAutoAndSizeWithSameAspectRatio { } - (void)testExtractIconFromDataAssetAutoAndSizeWithDifferentAspectRatio { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); id mockImageClass = OCMClassMock([UIImage class]); @@ -127,9 +121,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 +130,6 @@ - (void)testExtractIconFromDataAssetAutoAndSizeWithDifferentAspectRatio { } - (void)testExtractIconFromDataAssetNoScaling { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); id mockImageClass = OCMClassMock([UIImage class]); @@ -155,9 +147,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 +157,6 @@ - (void)testExtractIconFromDataAssetNoScaling { } - (void)testExtractIconFromDataBytesAuto { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); UIImage *testImage = [self createOnePixelImage]; @@ -183,9 +173,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 +183,6 @@ - (void)testExtractIconFromDataBytesAuto { } - (void)testExtractIconFromDataBytesAutoWithScaling { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); UIImage *testImage = [self createOnePixelImage]; @@ -211,9 +199,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 +208,6 @@ - (void)testExtractIconFromDataBytesAutoWithScaling { } - (void)testExtractIconFromDataBytesAutoAndSizeWithSameAspectRatio { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); UIImage *testImage = [self createOnePixelImage]; @@ -240,9 +226,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 +242,6 @@ - (void)testExtractIconFromDataBytesAutoAndSizeWithSameAspectRatio { } - (void)testExtractIconFromDataBytesAutoAndSizeWithDifferentAspectRatio { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); UIImage *testImage = [self createOnePixelImage]; @@ -276,9 +260,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 +269,6 @@ - (void)testExtractIconFromDataBytesAutoAndSizeWithDifferentAspectRatio { } - (void)testExtractIconFromDataBytesNoScaling { - FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; NSObject *mockRegistrar = OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); UIImage *testImage = [self createOnePixelImage]; @@ -303,9 +285,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 +296,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..7c2a7d76163d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsGroundOverlayControllerTests.m @@ -0,0 +1,156 @@ +// 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..cfe342ce871d 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 @@ -32,3 +32,13 @@ flutter: uses-material-design: true assets: - assets/ + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + { + google_maps_flutter_platform_interface: + { + path: ../../../../../packages/google_maps_flutter/google_maps_flutter_platform_interface, + }, + } 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..cfe342ce871d 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 @@ -32,3 +32,13 @@ flutter: uses-material-design: true assets: - assets/ + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + { + google_maps_flutter_platform_interface: + { + path: ../../../../../packages/google_maps_flutter/google_maps_flutter_platform_interface, + }, + } 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..c0cf5ed056b8 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 @@ -27,3 +27,13 @@ dev_dependencies: flutter: uses-material-design: true + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + { + google_maps_flutter_platform_interface: + { + path: ../../../../../../packages/google_maps_flutter/google_maps_flutter_platform_interface, + }, + } 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..c455de651041 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMGroundOverlayController.h @@ -0,0 +1,61 @@ +// 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 + +/// 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 FGMPlatformGroundOverlay for 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..5ad77a94e1ab --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.m @@ -0,0 +1,229 @@ +// 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" + +static UIImage *scaleImage(UIImage *image, double scale); +static UIImage *scaledImageWithScale(UIImage *image, CGFloat scale); +static UIImage *scaledImageWithSize(UIImage *image, CGSize size); +static UIImage *scaledImage(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 = scaleImage(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 = scaledImage(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 = scaledImage(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; +} + +/// 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, 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 *scaledImageWithScale(UIImage *image, 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 *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); + } +} + +/// 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, 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..8b3a644eec6f 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 *mockImage = + [FGMPlatformBitmap makeWithBitmap:[FGMPlatformBitmapDefaultMarker makeWithHue:0]]; + if (isCreatedWithBounds) { + return [FGMPlatformGroundOverlay + makeWithGroundOverlayId:overlayId + image:mockImage + 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:mockImage + 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..094405b46080 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml @@ -35,3 +35,13 @@ topics: - google-maps - google-maps-flutter - map + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + { + google_maps_flutter_platform_interface: + { + path: ../../../packages/google_maps_flutter/google_maps_flutter_platform_interface, + }, + } 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) => diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart index 67a026d90557..eda4dc198f4c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart @@ -152,6 +152,14 @@ class CircleTapEvent extends MapEvent { CircleTapEvent(super.mapId, super.circleId); } +/// An event fired when a [GroundOverlay] is tapped. +class GroundOverlayTapEvent extends MapEvent { + /// Build an GroundOverlayTap Event triggered from the map represented by `mapId`. + /// + /// The `value` of this event is a [GroundOverlayId] object that represents the tapped GroundOverlay. + GroundOverlayTapEvent(super.mapId, super.croundOverlayId); +} + /// An event fired when a Map is tapped. class MapTapEvent extends _PositionedMapEvent { /// Build an MapTap Event triggered from the map represented by `mapId`. diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart index a8f8e6d8b329..b8a74bd0b607 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart @@ -169,6 +169,18 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { 'updateClusterManagers() has not been implemented.'); } + /// Updates ground overlay configuration. + /// + /// The returned [Future] completes once the update has been made on the + /// platform side. + Future updateGroundOverlays( + GroundOverlayUpdates groundOverlayUpdates, { + required int mapId, + }) { + throw UnimplementedError( + 'updateGroundOverlays() has not been implemented.'); + } + /// Clears the tile cache so that all tiles will be requested again from the /// [TileProvider]. /// @@ -389,6 +401,11 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { throw UnimplementedError('onClusterTap() has not been implemented.'); } + /// A [GroundOverlay] has been tapped. + Stream onGroundOverlayTap({required int mapId}) { + throw UnimplementedError('onGroundOverlayTap() has not been implemented.'); + } + /// Dispose of whatever resources the `mapId` is holding on to. void dispose({required int mapId}) { throw UnimplementedError('dispose() has not been implemented.'); diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_inspector_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_inspector_platform.dart index 8bf6f6f89baf..c5acde75d389 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_inspector_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_inspector_platform.dart @@ -117,11 +117,7 @@ abstract class GoogleMapsInspectorPlatform extends PlatformInterface { } /// If the platform supports getting information about heatmaps. - bool supportsGettingHeatmapInfo() { - throw UnimplementedError( - 'supportsGettingHeatmapInfo() has not been implemented.', - ); - } + bool supportsGettingHeatmapInfo() => false; /// Returns information about the heatmap with the given ID. /// @@ -132,6 +128,20 @@ abstract class GoogleMapsInspectorPlatform extends PlatformInterface { throw UnimplementedError('getHeatmapInfo() has not been implemented.'); } + /// If the platform supports getting information about ground overlays. + bool supportsGettingGroundOverlayInfo() => false; + + /// Returns information about the ground overlay with the given ID. + /// + /// The returned object will be synthesized from platform data, so will not + /// be the same Dart object as the original [GroundOverlay] provided to the + /// platform interface with that ID, and not all fields will be populated. + Future getGroundOverlayInfo(GroundOverlayId groundOverlayId, + {required int mapId}) { + throw UnimplementedError( + 'getGroundOverlayInfo() has not been implemented.'); + } + /// Returns current clusters from [ClusterManager]. Future> getClusters( {required int mapId, required ClusterManagerId clusterManagerId}) { diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ground_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ground_overlay.dart new file mode 100644 index 000000000000..b06112a190f9 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ground_overlay.dart @@ -0,0 +1,385 @@ +// 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 'dart:ui'; + +import 'package:flutter/foundation.dart' show immutable; + +import 'types.dart'; + +/// Uniquely identifies a [GroundOverlay] among [GoogleMap] ground overlays. +@immutable +class GroundOverlayId extends MapsObjectId { + /// Creates an immutable identifier for a [GroundOverlay]. + const GroundOverlayId(super.value); +} + +/// Ground overlay to be drawn on the map. +/// +/// A ground overlay is an image that is fixed to a map. Unlike markers, ground +/// overlays are oriented against the Earth's surface rather than the screen, +/// so rotating, tilting, or zooming the map will change the orientation of the +/// image. Ground overlays are useful for fixing a single image at one area on +/// the map. For adding extensive imagery that covers a large portion of the +/// map, a [TileOverlay] should be considered. +/// +/// Because the overlay is automatically scaled to fit either a specified +/// [bounds] or a [position] (combined with [width], [height], and [zoomLevel]), +/// the [image].bitmapScaling must be set to [MapBitmapScaling.none]. +/// +/// Sizing and positioning can be defined in the following ways: +/// - Using [bounds] for exact corners in [LatLngBounds]. +/// Recommended for precise placement. +/// - Using [position] with [width] and [height] in meters. If [height] +/// is omitted, the image aspect ratio is preserved. +/// - Using [position] with [zoomLevel] to scale the image according to +/// a chosen zoom level. +/// +/// The [anchor] parameter defines the anchor’s relative location within the +/// overlay. For example, an anchor of (0.5, 0.5) corresponds to the image’s +/// center. When [position] is used, the overlay shifts so that this anchor +/// aligns with the given position. When [bounds] is used, the anchor specifies +/// the internal anchor position inside the bounds. If [bearing] is set, the +/// image rotates around this anchor. +/// +/// Platform behavior for sizing can vary, and not all sizing or positioning +/// options may be supported equally across all platforms. Combining both +/// [width] and [zoomLevel] can help achieve the desired effect across +/// platforms. Using [bounds] is the most reliable way to position an ground +/// overlay precisely. +/// +/// Use either [GroundOverlay.fromBounds] or [GroundOverlay.fromPosition] to +/// create a ground overlay. +/// +/// Example of [GroundOverlay.fromBounds] method: +/// ```dart +/// GroundOverlay.bounds( +/// groundOverlayId: const GroundOverlayId('overlay_id'), +/// image: await AssetMapBitmap.create( +/// createLocalImageConfiguration(context), +/// 'assets/images/ground_overlay.png', +/// bitmapScaling: MapBitmapScaling.none, +/// ), +/// bounds: LatLngBounds( +/// southwest: LatLng(37.42, -122.08), +/// northeast: LatLng(37.43, -122.09), +/// ), +/// ); +/// ``` +/// +/// Example of [GroundOverlay.fromPosition] method: +/// ```dart +/// GroundOverlay.position( +/// groundOverlayId: const GroundOverlayId('overlay_id'), +/// image: await AssetMapBitmap.create( +/// createLocalImageConfiguration(context), +/// 'assets/images/ground_overlay.png', +/// bitmapScaling: MapBitmapScaling.none, +/// ), +/// position: LatLng(37.42, -122.08), +/// width: 100, +/// height: 100, +/// zoomLevel: 14, +/// ); +/// ``` +@immutable +class GroundOverlay implements MapsObject { + /// Creates an immutable representation of a [GroundOverlay] to + /// draw on [GoogleMap]. + GroundOverlay._({ + required this.groundOverlayId, + required this.image, + this.position, + this.bounds, + this.width, + this.height, + this.anchor = const Offset(0.5, 0.5), + this.transparency = 0.0, + this.bearing = 0.0, + this.zIndex = 0, + this.visible = true, + this.clickable = true, + this.onTap, + this.zoomLevel, + }) : assert(transparency >= 0.0 && transparency <= 1.0), + assert(bearing >= 0.0 && bearing <= 360.0), + assert((position == null) != (bounds == null), + 'Either position or bounds must be given, but not both'), + assert(position == null || (width == null || width > 0), + 'Width must be null or greater than 0 when position is used'), + assert(position == null || (height == null || height > 0), + 'Height must be null or greater than 0 when position is used'), + assert(image.bitmapScaling == MapBitmapScaling.none, + 'The provided image must have its bitmapScaling property set to MapBitmapScaling.none.'); + + /// Creates a [GroundOverlay] fitted to the specified [bounds] with the + /// provided [image]. + /// + /// Example: + /// ```dart + /// GroundOverlay.fromBounds( + /// groundOverlayId: const GroundOverlayId('overlay_id'), + /// image: await AssetMapBitmap.create( + /// createLocalImageConfiguration(context), + /// 'assets/images/ground_overlay.png', + /// bitmapScaling: MapBitmapScaling.none, + /// ), + /// bounds: LatLngBounds( + /// southwest: LatLng(37.42, -122.08), + /// northeast: LatLng(37.43, -122.09), + /// ), + /// ); + factory GroundOverlay.fromBounds({ + required GroundOverlayId groundOverlayId, + required MapBitmap image, + required LatLngBounds bounds, + Offset anchor = const Offset(0.5, 0.5), + double bearing = 0.0, + double transparency = 0.0, + int zIndex = 0, + bool visible = true, + bool clickable = true, + VoidCallback? onTap, + }) { + return GroundOverlay._( + groundOverlayId: groundOverlayId, + image: image, + bounds: bounds, + anchor: anchor, + bearing: bearing, + transparency: transparency, + zIndex: zIndex, + visible: visible, + clickable: clickable, + onTap: onTap, + ); + } + + /// Creates a [GroundOverlay] to given [position] with the given [image]. + /// + /// Example: + /// ```dart + /// GroundOverlay.fromPosition( + /// groundOverlayId: const GroundOverlayId('overlay_id'), + /// image: await AssetMapBitmap.create( + /// createLocalImageConfiguration(context), + /// 'assets/images/ground_overlay.png', + /// bitmapScaling: MapBitmapScaling.none, + /// ), + /// position: LatLng(37.42, -122.08), + /// width: 100, + /// height: 100, + /// zoomLevel: 14, + /// ); + /// ``` + factory GroundOverlay.fromPosition({ + required GroundOverlayId groundOverlayId, + required MapBitmap image, + required LatLng position, + double? width, + double? height, + Offset anchor = const Offset(0.5, 0.5), + double bearing = 0.0, + double transparency = 0.0, + int zIndex = 0, + bool visible = true, + bool clickable = true, + VoidCallback? onTap, + double? zoomLevel, + }) { + return GroundOverlay._( + groundOverlayId: groundOverlayId, + image: image, + position: position, + width: width, + height: height, + anchor: anchor, + bearing: bearing, + transparency: transparency, + zIndex: zIndex, + visible: visible, + clickable: clickable, + onTap: onTap, + zoomLevel: zoomLevel, + ); + } + + /// Uniquely identifies a [GroundOverlay]. + final GroundOverlayId groundOverlayId; + + @override + GroundOverlayId get mapsId => groundOverlayId; + + /// A description of the bitmap used to draw the ground overlay. + /// + /// To create ground overlay from assets, use [AssetMapBitmap], + /// [AssetMapBitmap.create] or [BitmapDescriptor.asset]. + /// + /// To create ground overlay from raw PNG data use [BytesMapBitmap] + /// or [BitmapDescriptor.bytes]. + /// + /// [MapBitmap.bitmapScaling] must be set to [MapBitmapScaling.none]. + final MapBitmap image; + + /// Geographical location to which the anchor will be fixed. The relative + /// location of the [position] on the overlay can be changed with the [anchor] + /// parameter, which is by default (0.5, 0.5) meaning that the [position] is + /// in the middle of the overlay image. + final LatLng? position; + + /// Width of the ground overlay (in meters). This parameter is only available + /// with [position]. + final double? width; + + /// Height of the ground overlay (in meters). This parameter is only available + /// with [position]. If not provided, the image aspect ratio is automatically + /// preserved. + final double? height; + + /// Bounds which will contain the image. If [bounds] is specified, [position] + /// must be null. + final LatLngBounds? bounds; + + /// The [anchor] in normalized coordinates specifying the anchor point of the + /// overlay. When [position] is used, the overlay shifts so that this anchor + /// aligns with the given position. If [bounds] is specified, the anchor is + /// the internal anchor position inside the bounds. + /// + /// * An anchor of (0.0, 0.0) is the top-left corner. + /// * An anchor of (1.0, 1.0) is the bottom-right corner. + /// + /// Defaults to `Offset(0.5, 0.5)`, i.e., the center of the image. + /// If [bearing] is set, the image rotates around this anchor. + final Offset? anchor; + + /// The amount that the image should be rotated in a clockwise direction. The + /// center of the rotation will be the image's [anchor]. + /// The default bearing is 0, i.e., the image is aligned so that up is north. + final double bearing; + + /// The transparency of the ground overlay. Defaults to 0 (opaque). + final double transparency; + + /// The ground overlay's zIndex, i.e., the order in which it will be drawn + /// where overlays with larger values are drawn above those with lower values. + /// Defaults to 0. + final int zIndex; + + /// Whether the ground overlay is visible (true) or hidden (false). + /// Defaults to true. + final bool visible; + + /// Controls if click events are handled for this ground overlay. + /// Defaults to true. + final bool clickable; + + /// Callbacks to receive tap events for ground overlay placed on this map. + final VoidCallback? onTap; + + /// The map zoom level used when setting a ground overlay with a [position]. + /// + /// This parameter determines how the [GroundOverlay.image] is rendered on the + /// map when using [GroundOverlay.position]. The image is scaled as if its + /// actual size corresponds to the camera pixels at the specified `zoomLevel`. + /// Usage of this parameter can differ between platforms. + final double? zoomLevel; + + /// Converts this object to something serializable in JSON. + @override + Object toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, Object? value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('groundOverlayId', groundOverlayId.value); + addIfPresent('image', image.toJson()); + addIfPresent('position', position?.toJson()); + addIfPresent('bounds', bounds?.toJson()); + addIfPresent('width', width); + addIfPresent('height', height); + addIfPresent( + 'anchor', anchor != null ? [anchor!.dx, anchor!.dy] : null); + addIfPresent('bearing', bearing); + addIfPresent('transparency', transparency); + addIfPresent('zIndex', zIndex); + addIfPresent('visible', visible); + addIfPresent('clickable', clickable); + addIfPresent('zoomLevel', zoomLevel); + + return json; + } + + /// Creates a new [GroundOverlay] object whose values are the same as this + /// instance, unless overwritten by the specified parameters. + GroundOverlay copyWith({ + double? bearingParam, + double? transparencyParam, + int? zIndexParam, + bool? visibleParam, + bool? clickableParam, + VoidCallback? onTapParam, + }) { + return GroundOverlay._( + groundOverlayId: groundOverlayId, + bearing: bearingParam ?? bearing, + transparency: transparencyParam ?? transparency, + zIndex: zIndexParam ?? zIndex, + visible: visibleParam ?? visible, + clickable: clickableParam ?? clickable, + onTap: onTapParam ?? onTap, + image: image, + position: position, + bounds: bounds, + width: width, + height: height, + anchor: anchor, + zoomLevel: zoomLevel, + ); + } + + @override + GroundOverlay clone() => copyWith(); + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is GroundOverlay && + groundOverlayId == other.groundOverlayId && + image == other.image && + position == other.position && + bounds == other.bounds && + width == other.width && + height == other.height && + anchor == other.anchor && + bearing == other.bearing && + transparency == other.transparency && + zIndex == other.zIndex && + visible == other.visible && + clickable == other.clickable && + zoomLevel == other.zoomLevel; + } + + @override + int get hashCode => Object.hash( + groundOverlayId, + image, + position, + bounds, + width, + height, + anchor, + bearing, + transparency, + zIndex, + visible, + clickable, + zoomLevel, + ); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ground_overlay_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ground_overlay_updates.dart new file mode 100644 index 000000000000..152352e78aeb --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ground_overlay_updates.dart @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'types.dart'; + +/// Update specification for a set of [GroundOverlay]s. +class GroundOverlayUpdates extends MapsObjectUpdates { + /// Computes [GroundOverlayUpdates] given previous and current [GroundOverlay]s. + GroundOverlayUpdates.from(super.previous, super.current) + : super.from(objectName: 'groundOverlay'); + + /// Set of GroundOverlays to be added in this update. + Set get groundOverlaysToAdd => objectsToAdd; + + /// Set of GroundOverlayIds to be removed in this update. + Set get groundOverlayIdsToRemove => + objectIdsToRemove.cast(); + + /// Set of GroundOverlays to be changed in this update. + Set get groundOverlaysToChange => objectsToChange; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_objects.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_objects.dart index 23d605c43eff..66ec35f86f59 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_objects.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_objects.dart @@ -23,6 +23,7 @@ class MapObjects { this.heatmaps = const {}, this.tileOverlays = const {}, this.clusterManagers = const {}, + this.groundOverlays = const {}, }); final Set markers; @@ -32,4 +33,5 @@ class MapObjects { final Set heatmaps; final Set tileOverlays; final Set clusterManagers; + final Set groundOverlays; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart index 4df0fe97e42b..43699f820556 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart @@ -70,7 +70,7 @@ class TileOverlay implements MapsObject { final double transparency; /// The tile overlay's zIndex, i.e., the order in which it will be drawn where - /// overlays with larger values are drawn above those with lower values + /// overlays with larger values are drawn above those with lower values. final int zIndex; /// The visibility for the tile overlay. The default visibility is true. diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart index 745e300ff05c..95c27d5bf951 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart @@ -12,6 +12,8 @@ export 'circle_updates.dart'; export 'cluster.dart'; export 'cluster_manager.dart'; export 'cluster_manager_updates.dart'; +export 'ground_overlay.dart'; +export 'ground_overlay_updates.dart'; export 'heatmap.dart'; export 'heatmap_updates.dart'; export 'joint_type.dart'; @@ -36,6 +38,7 @@ export 'ui.dart'; // Export the utils used by the Widget export 'utils/circle.dart'; export 'utils/cluster_manager.dart'; +export 'utils/ground_overlay.dart'; export 'utils/heatmap.dart'; export 'utils/marker.dart'; export 'utils/polygon.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/ground_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/ground_overlay.dart new file mode 100644 index 000000000000..22ee01d86fcb --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/ground_overlay.dart @@ -0,0 +1,13 @@ +// 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 '../types.dart'; +import 'maps_object.dart'; + +/// Converts an [Iterable] of GroundOverlay in a Map of GroundOverlayId -> GroundOverlay. +Map keyByGroundOverlayId( + Iterable groundOverlays) { + return keyByMapsObjectId(groundOverlays) + .cast(); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/ground_overlay_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/ground_overlay_test.dart new file mode 100644 index 000000000000..04cacda98228 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/ground_overlay_test.dart @@ -0,0 +1,403 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$GroundOverlay', () { + const GroundOverlayId kID = GroundOverlayId('groundOverlay'); + final LatLngBounds kBounds = LatLngBounds( + southwest: const LatLng(37.42, -122.08), + northeast: const LatLng(37.43, -122.09), + ); + const LatLng kPosition = LatLng(37.42, -122.08); + final MapBitmap kMapBitmap = AssetMapBitmap( + 'assets/asset.png', + imagePixelRatio: 1.0, + bitmapScaling: MapBitmapScaling.none, + ); + const Offset kAnchor = Offset(0.3, 0.7); + const double kBearing = 45.0; + const double kTransparency = 0.5; + const int kZIndex = 1; + const bool kVisible = false; + const bool kClickable = false; + const double kWidth = 200; + const double kHeight = 300; + const double kZoomLevel = 10.0; + + test('fromBounds constructor defaults', () { + final GroundOverlay groundOverlay = GroundOverlay.fromBounds( + groundOverlayId: kID, + image: kMapBitmap, + bounds: kBounds, + ); + + expect(groundOverlay.groundOverlayId, kID); + expect(groundOverlay.bounds, kBounds); + expect(groundOverlay.image, kMapBitmap); + expect(groundOverlay.anchor, const Offset(0.5, 0.5)); + expect(groundOverlay.bearing, 0.0); + expect(groundOverlay.transparency, 0.0); + expect(groundOverlay.zIndex, 0.0); + expect(groundOverlay.visible, true); + expect(groundOverlay.clickable, true); + expect(groundOverlay.onTap, null); + }); + + test('fromBounds construct with values', () { + final GroundOverlay groundOverlay = GroundOverlay.fromBounds( + groundOverlayId: kID, + image: kMapBitmap, + bounds: kBounds, + anchor: kAnchor, + bearing: kBearing, + transparency: kTransparency, + zIndex: kZIndex, + visible: kVisible, + clickable: kClickable, + ); + + expect(groundOverlay.groundOverlayId, kID); + expect(groundOverlay.bounds, kBounds); + expect(groundOverlay.image, kMapBitmap); + expect(groundOverlay.anchor, kAnchor); + expect(groundOverlay.bearing, kBearing); + expect(groundOverlay.transparency, kTransparency); + expect(groundOverlay.zIndex, kZIndex); + expect(groundOverlay.visible, kVisible); + expect(groundOverlay.clickable, kClickable); + }); + + test('fromPosition constructor defaults', () { + final GroundOverlay groundOverlay = GroundOverlay.fromPosition( + groundOverlayId: kID, + image: kMapBitmap, + position: kPosition, + width: 100, + height: 100, + ); + + expect(groundOverlay.groundOverlayId, kID); + expect(groundOverlay.position, kPosition); + expect(groundOverlay.image, kMapBitmap); + expect(groundOverlay.width, 100); + expect(groundOverlay.height, 100); + expect(groundOverlay.anchor, const Offset(0.5, 0.5)); + expect(groundOverlay.bearing, 0.0); + expect(groundOverlay.transparency, 0.0); + expect(groundOverlay.zIndex, 0.0); + expect(groundOverlay.visible, true); + expect(groundOverlay.clickable, true); + expect(groundOverlay.onTap, null); + }); + + test('fromPosition construct with values', () { + final GroundOverlay groundOverlay = GroundOverlay.fromPosition( + groundOverlayId: kID, + image: kMapBitmap, + position: kPosition, + width: kWidth, + height: kHeight, + anchor: kAnchor, + bearing: kBearing, + transparency: kTransparency, + zIndex: kZIndex, + visible: kVisible, + clickable: kClickable, + zoomLevel: kZoomLevel, + ); + + expect(groundOverlay.groundOverlayId, kID); + expect(groundOverlay.position, kPosition); + expect(groundOverlay.image, kMapBitmap); + expect(groundOverlay.width, kWidth); + expect(groundOverlay.height, kHeight); + expect(groundOverlay.anchor, kAnchor); + expect(groundOverlay.bearing, kBearing); + expect(groundOverlay.transparency, kTransparency); + expect(groundOverlay.zIndex, kZIndex); + expect(groundOverlay.visible, kVisible); + expect(groundOverlay.clickable, kClickable); + expect(groundOverlay.zoomLevel, kZoomLevel); + }); + + test('copyWith fromPosition', () { + final GroundOverlay groundOverlay1 = GroundOverlay.fromPosition( + groundOverlayId: kID, + image: kMapBitmap, + position: kPosition, + width: 100, + height: 100, + ); + + final GroundOverlay groundOverlay2 = groundOverlay1.copyWith( + bearingParam: kBearing, + transparencyParam: kTransparency, + zIndexParam: kZIndex, + visibleParam: kVisible, + clickableParam: kClickable, + onTapParam: () {}, + ); + + expect(groundOverlay2.groundOverlayId, groundOverlay1.groundOverlayId); + expect(groundOverlay2.image, groundOverlay1.image); + expect(groundOverlay2.position, groundOverlay1.position); + expect(groundOverlay2.width, groundOverlay1.width); + expect(groundOverlay2.height, groundOverlay1.height); + expect(groundOverlay2.anchor, groundOverlay1.anchor); + expect(groundOverlay2.bearing, kBearing); + expect(groundOverlay2.transparency, kTransparency); + expect(groundOverlay2.zIndex, kZIndex); + expect(groundOverlay2.visible, kVisible); + expect(groundOverlay2.clickable, kClickable); + expect(groundOverlay2.zoomLevel, groundOverlay1.zoomLevel); + }); + + test('copyWith fromBounds', () { + final GroundOverlay groundOverlay1 = GroundOverlay.fromBounds( + groundOverlayId: kID, + image: kMapBitmap, + bounds: kBounds, + ); + + final GroundOverlay groundOverlay2 = groundOverlay1.copyWith( + bearingParam: kBearing, + transparencyParam: kTransparency, + zIndexParam: kZIndex, + visibleParam: kVisible, + clickableParam: kClickable, + onTapParam: () {}, + ); + + expect(groundOverlay2.groundOverlayId, groundOverlay1.groundOverlayId); + expect(groundOverlay2.image, groundOverlay1.image); + expect(groundOverlay2.position, groundOverlay1.position); + expect(groundOverlay2.width, groundOverlay1.width); + expect(groundOverlay2.height, groundOverlay1.height); + expect(groundOverlay2.anchor, groundOverlay1.anchor); + expect(groundOverlay2.bearing, kBearing); + expect(groundOverlay2.transparency, kTransparency); + expect(groundOverlay2.zIndex, kZIndex); + expect(groundOverlay2.visible, kVisible); + expect(groundOverlay2.clickable, kClickable); + expect(groundOverlay2.zoomLevel, groundOverlay1.zoomLevel); + }); + + test('fromPosition clone', () { + final GroundOverlay groundOverlay1 = GroundOverlay.fromPosition( + groundOverlayId: kID, + image: kMapBitmap, + position: kPosition, + width: 100, + height: 100, + ); + + final GroundOverlay groundOverlay2 = groundOverlay1.clone(); + + expect(groundOverlay2, groundOverlay1); + }); + + test('fromBounds clone', () { + final GroundOverlay groundOverlay1 = GroundOverlay.fromBounds( + groundOverlayId: kID, + image: kMapBitmap, + bounds: kBounds, + ); + + final GroundOverlay groundOverlay2 = groundOverlay1.clone(); + + expect(groundOverlay2, groundOverlay1); + }); + + test('==', () { + final GroundOverlay groundOverlayPosition1 = GroundOverlay.fromPosition( + groundOverlayId: kID, + image: kMapBitmap, + position: kPosition, + width: kWidth, + height: kHeight, + anchor: kAnchor, + bearing: kBearing, + transparency: kTransparency, + zIndex: kZIndex, + visible: kVisible, + clickable: kClickable, + zoomLevel: kZoomLevel, + ); + + final GroundOverlay groundOverlayPosition2 = GroundOverlay.fromPosition( + groundOverlayId: kID, + image: kMapBitmap, + position: kPosition, + width: kWidth, + height: kHeight, + anchor: kAnchor, + bearing: kBearing, + transparency: kTransparency, + zIndex: kZIndex, + visible: kVisible, + clickable: kClickable, + zoomLevel: kZoomLevel, + ); + + final GroundOverlay groundOverlayPosition3 = GroundOverlay.fromPosition( + groundOverlayId: kID, + image: kMapBitmap, + position: kPosition, + width: kWidth, + height: kHeight, + anchor: kAnchor, + bearing: kBearing, + transparency: kTransparency, + zIndex: kZIndex, + visible: kVisible, + clickable: kClickable, + zoomLevel: kZoomLevel + 1, + ); + + final GroundOverlay groundOverlayBounds1 = GroundOverlay.fromBounds( + groundOverlayId: kID, + image: kMapBitmap, + bounds: kBounds, + anchor: kAnchor, + bearing: kBearing, + transparency: kTransparency, + zIndex: kZIndex, + visible: kVisible, + clickable: kClickable, + ); + + final GroundOverlay groundOverlayBounds2 = GroundOverlay.fromBounds( + groundOverlayId: kID, + image: kMapBitmap, + bounds: kBounds, + anchor: kAnchor, + bearing: kBearing, + transparency: kTransparency, + zIndex: kZIndex, + visible: kVisible, + clickable: kClickable, + ); + + final GroundOverlay groundOverlayBounds3 = GroundOverlay.fromBounds( + groundOverlayId: kID, + image: kMapBitmap, + bounds: kBounds, + anchor: kAnchor, + bearing: kBearing, + transparency: kTransparency, + zIndex: kZIndex + 1, + visible: kVisible, + clickable: kClickable, + ); + + expect(groundOverlayPosition1, groundOverlayPosition2); + expect(groundOverlayPosition1, isNot(groundOverlayPosition3)); + expect(groundOverlayBounds1, groundOverlayBounds2); + expect(groundOverlayBounds1, isNot(groundOverlayBounds3)); + expect(groundOverlayPosition1, isNot(groundOverlayBounds1)); + }); + + test('hashCode', () { + final GroundOverlay groundOverlay = GroundOverlay.fromPosition( + groundOverlayId: kID, + image: kMapBitmap, + position: kPosition, + ); + + expect(groundOverlay.hashCode, groundOverlay.clone().hashCode); + }); + + test('asserts in constructor', () { + // Transparency must be between 0.0 and 1.0. + expect( + () => GroundOverlay.fromBounds( + groundOverlayId: kID, + image: kMapBitmap, + bounds: kBounds, + transparency: -0.1, + ), + throwsAssertionError, + ); + + // Transparency must be between 0.0 and 1.0. + expect( + () => GroundOverlay.fromBounds( + groundOverlayId: kID, + image: kMapBitmap, + bounds: kBounds, + transparency: 1.1, + ), + throwsAssertionError, + ); + + // Bearing must be between 0.0 and 360.0. + expect( + () => GroundOverlay.fromBounds( + groundOverlayId: kID, + image: kMapBitmap, + bounds: kBounds, + bearing: -1.0, + ), + throwsAssertionError, + ); + + // Bearing must be between 0.0 and 360.0. + expect( + () => GroundOverlay.fromBounds( + groundOverlayId: kID, + image: kMapBitmap, + bounds: kBounds, + bearing: 361.0, + ), + throwsAssertionError, + ); + + // Height must be greater than 0. + expect( + () => GroundOverlay.fromPosition( + groundOverlayId: kID, + image: kMapBitmap, + position: kPosition, + width: 100, + height: -1, + ), + throwsAssertionError, + ); + + // Width must be greater than 0. + expect( + () => GroundOverlay.fromPosition( + groundOverlayId: kID, + image: kMapBitmap, + position: kPosition, + width: -1, + height: 100, + ), + throwsAssertionError, + ); + + // Image bitMapScaling must be MapBitmapScaling.none. + expect( + () => GroundOverlay.fromPosition( + groundOverlayId: kID, + image: AssetMapBitmap( + 'assets/asset.png', + imagePixelRatio: 1.0, + // ignore: avoid_redundant_argument_values + bitmapScaling: MapBitmapScaling.auto, + ), + position: kPosition, + width: 100, + height: 100, + ), + throwsAssertionError, + ); + }); + }); +} 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..72b935835c08 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 @@ -29,6 +29,10 @@ flutter: - assets/ dependency_overrides: + # FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. + # See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins + google_maps_flutter_platform_interface: + path: ../../../../packages/google_maps_flutter/google_maps_flutter_platform_interface # Override the google_maps_flutter dependency on google_maps_flutter_web. # TODO(ditman): Unwind the circular dependency. This will create problems # if we need to make a breaking change to google_maps_flutter_web. 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..c70252915d75 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -40,3 +40,13 @@ topics: # The example deliberately includes limited-use secrets. false_secrets: - /example/web/index.html + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + { + google_maps_flutter_platform_interface: + { + path: ../../../packages/google_maps_flutter/google_maps_flutter_platform_interface, + }, + }