Skip to content

Commit a726c23

Browse files
jokerttuandroidseb
authored andcommitted
[google_maps_flutter] Ground overlay support - platform impls (flutter#8563)
This PR contains platform implementations for the ground overlays support (flutter#8432). Linked issue: flutter/flutter#26479
1 parent c44ebde commit a726c23

File tree

78 files changed

+5480
-608
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+5480
-608
lines changed

packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.15.0
2+
3+
* Adds support for ground overlay.
4+
15
## 2.14.14
26

37
* Updates compileSdk 34 to flutter.compileSdkVersion.

packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.google.android.gms.maps.model.Dash;
3030
import com.google.android.gms.maps.model.Dot;
3131
import com.google.android.gms.maps.model.Gap;
32+
import com.google.android.gms.maps.model.GroundOverlay;
3233
import com.google.android.gms.maps.model.JointType;
3334
import com.google.android.gms.maps.model.LatLng;
3435
import com.google.android.gms.maps.model.LatLngBounds;
@@ -40,6 +41,7 @@
4041
import com.google.maps.android.heatmaps.Gradient;
4142
import com.google.maps.android.heatmaps.WeightedLatLng;
4243
import io.flutter.FlutterInjector;
44+
import io.flutter.plugins.googlemaps.Messages.FlutterError;
4345
import java.io.IOException;
4446
import java.io.InputStream;
4547
import java.util.ArrayList;
@@ -849,6 +851,146 @@ static Tile tileFromPigeon(Messages.PlatformTile tile) {
849851
return new Tile(tile.getWidth().intValue(), tile.getHeight().intValue(), tile.getData());
850852
}
851853

854+
/**
855+
* Set the options in the given ground overlay object to the given sink.
856+
*
857+
* @param groundOverlay the object expected to be a PlatformGroundOverlay containing the ground
858+
* overlay options.
859+
* @param sink the GroundOverlaySink where the options will be set.
860+
* @param assetManager An instance of Android's AssetManager, which provides access to any raw
861+
* asset files stored in the application's assets directory.
862+
* @param density the density of the display, used to calculate pixel dimensions.
863+
* @param wrapper the BitmapDescriptorFactoryWrapper to create BitmapDescriptor.
864+
* @return the identifier of the ground overlay. The identifier is valid as long as the ground
865+
* overlay exists.
866+
* @throws IllegalArgumentException if required fields are missing or invalid.
867+
*/
868+
static @NonNull String interpretGroundOverlayOptions(
869+
@NonNull Messages.PlatformGroundOverlay groundOverlay,
870+
@NonNull GroundOverlaySink sink,
871+
@NonNull AssetManager assetManager,
872+
float density,
873+
@NonNull BitmapDescriptorFactoryWrapper wrapper) {
874+
sink.setTransparency(groundOverlay.getTransparency().floatValue());
875+
sink.setZIndex(groundOverlay.getZIndex().floatValue());
876+
sink.setVisible(groundOverlay.getVisible());
877+
if (groundOverlay.getAnchor() != null) {
878+
sink.setAnchor(
879+
groundOverlay.getAnchor().getX().floatValue(),
880+
groundOverlay.getAnchor().getY().floatValue());
881+
}
882+
sink.setBearing(groundOverlay.getBearing().floatValue());
883+
sink.setClickable(groundOverlay.getClickable());
884+
sink.setImage(toBitmapDescriptor(groundOverlay.getImage(), assetManager, density, wrapper));
885+
if (groundOverlay.getPosition() != null) {
886+
if (groundOverlay.getWidth() == null) {
887+
throw new FlutterError(
888+
"Invalid GroundOverlay",
889+
"Width is required when using a ground overlay with a position.",
890+
null);
891+
}
892+
sink.setPosition(
893+
latLngFromPigeon(groundOverlay.getPosition()),
894+
groundOverlay.getWidth().floatValue(),
895+
groundOverlay.getHeight() != null ? groundOverlay.getHeight().floatValue() : null);
896+
} else if (groundOverlay.getBounds() != null) {
897+
sink.setPositionFromBounds(latLngBoundsFromPigeon(groundOverlay.getBounds()));
898+
}
899+
return groundOverlay.getGroundOverlayId();
900+
}
901+
902+
/**
903+
* Converts a GroundOverlay object to a PlatformGroundOverlay Pigeon object.
904+
*
905+
* @param groundOverlay the GroundOverlay object to convert.
906+
* @param groundOverlayId the identifier of the GroundOverlay.
907+
* @param isCreatedWithBounds indicates if the GroundOverlay was created with bounds.
908+
* @return the converted PlatformGroundOverlay object.
909+
*/
910+
static @NonNull Messages.PlatformGroundOverlay groundOverlayToPigeon(
911+
@NonNull GroundOverlay groundOverlay,
912+
@NonNull String groundOverlayId,
913+
boolean isCreatedWithBounds) {
914+
915+
// Image is mandatory field on PlatformGroundOverlay (and it should be kept
916+
// non-nullable), therefore image must be set for the object. The image is
917+
// description either contains set of bytes, or path to asset. This info is
918+
// converted to format google maps uses (BitmapDescription), and the original
919+
// data is not stored on native code. Therefore placeholder image is used for
920+
// the image field.
921+
Messages.PlatformBitmap dummyImage =
922+
new Messages.PlatformBitmap.Builder()
923+
.setBitmap(
924+
new Messages.PlatformBitmapBytesMap.Builder()
925+
.setByteData(new byte[] {0})
926+
.setImagePixelRatio(1.0)
927+
.setBitmapScaling(Messages.PlatformMapBitmapScaling.NONE)
928+
.build())
929+
.build();
930+
931+
Messages.PlatformGroundOverlay.Builder builder =
932+
new Messages.PlatformGroundOverlay.Builder()
933+
.setGroundOverlayId(groundOverlayId)
934+
.setImage(dummyImage)
935+
.setWidth((double) groundOverlay.getWidth())
936+
.setHeight((double) groundOverlay.getHeight())
937+
.setBearing((double) groundOverlay.getBearing())
938+
.setTransparency((double) groundOverlay.getTransparency())
939+
.setZIndex((long) groundOverlay.getZIndex())
940+
.setVisible(groundOverlay.isVisible())
941+
.setClickable(groundOverlay.isClickable());
942+
943+
if (isCreatedWithBounds) {
944+
builder.setBounds(Convert.latLngBoundsToPigeon(groundOverlay.getBounds()));
945+
} else {
946+
builder.setPosition(Convert.latLngToPigeon(groundOverlay.getPosition()));
947+
}
948+
949+
builder.setAnchor(Convert.buildGroundOverlayAnchorForPigeon(groundOverlay));
950+
return builder.build();
951+
}
952+
953+
/**
954+
* Builds a PlatformDoublePair representing the anchor point for a GroundOverlay.
955+
*
956+
* @param groundOverlay the GroundOverlay object.
957+
* @return the PlatformDoublePair representing the anchor point.
958+
*/
959+
@VisibleForTesting
960+
public static @NonNull Messages.PlatformDoublePair buildGroundOverlayAnchorForPigeon(
961+
@NonNull GroundOverlay groundOverlay) {
962+
Messages.PlatformDoublePair.Builder anchorBuilder = new Messages.PlatformDoublePair.Builder();
963+
964+
// Position is overlays anchor point. Calculate normalized anchor point based on position and bounds.
965+
LatLng position = groundOverlay.getPosition();
966+
LatLngBounds bounds = groundOverlay.getBounds();
967+
968+
// Calculate normalized latitude.
969+
double height = bounds.northeast.latitude - bounds.southwest.latitude;
970+
double normalizedLatitude = 1.0 - ((position.latitude - bounds.southwest.latitude) / height);
971+
972+
// Constant for full circle degrees.
973+
final double FULL_CIRCLE_DEGREES = 360.0;
974+
975+
// Calculate normalized longitude.
976+
// For longitude, if the bounds cross the antimeridian (west > east),
977+
// adjust the width accordingly.
978+
double west = bounds.southwest.longitude;
979+
double east = bounds.northeast.longitude;
980+
double width = (west <= east) ? (east - west) : (FULL_CIRCLE_DEGREES - (west - east));
981+
982+
// Normalize the longitude of the anchor position relative to the western boundary.
983+
// Handles cases where the ground overlay crosses the antimeridian.
984+
double normalizedLongitude =
985+
((position.longitude < west ? position.longitude + FULL_CIRCLE_DEGREES : position.longitude)
986+
- west)
987+
/ width;
988+
989+
anchorBuilder.setX(normalizedLongitude);
990+
anchorBuilder.setY(normalizedLatitude);
991+
return anchorBuilder.build();
992+
}
993+
852994
static class BitmapDescriptorFactoryWrapper {
853995
/**
854996
* Creates a BitmapDescriptor from the provided asset key using the {@link

packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class GoogleMapBuilder implements GoogleMapOptionsSink {
2929
private List<Messages.PlatformCircle> initialCircles;
3030
private List<Messages.PlatformHeatmap> initialHeatmaps;
3131
private List<Messages.PlatformTileOverlay> initialTileOverlays;
32+
private List<Messages.PlatformGroundOverlay> initialGroundOverlays;
3233
private Rect padding = new Rect(0, 0, 0, 0);
3334
private @Nullable String style;
3435

@@ -54,6 +55,7 @@ GoogleMapController build(
5455
controller.setInitialHeatmaps(initialHeatmaps);
5556
controller.setPadding(padding.top, padding.left, padding.bottom, padding.right);
5657
controller.setInitialTileOverlays(initialTileOverlays);
58+
controller.setInitialGroundOverlays(initialGroundOverlays);
5759
controller.setMapStyle(style);
5860
return controller;
5961
}
@@ -197,6 +199,12 @@ public void setInitialTileOverlays(
197199
this.initialTileOverlays = initialTileOverlays;
198200
}
199201

202+
@Override
203+
public void setInitialGroundOverlays(
204+
@NonNull List<Messages.PlatformGroundOverlay> initialGroundOverlays) {
205+
this.initialGroundOverlays = initialGroundOverlays;
206+
}
207+
200208
@Override
201209
public void setMapStyle(@Nullable String style) {
202210
this.style = style;

packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.google.android.gms.maps.MapView;
3232
import com.google.android.gms.maps.OnMapReadyCallback;
3333
import com.google.android.gms.maps.model.Circle;
34+
import com.google.android.gms.maps.model.GroundOverlay;
3435
import com.google.android.gms.maps.model.LatLng;
3536
import com.google.android.gms.maps.model.LatLngBounds;
3637
import com.google.android.gms.maps.model.MapStyleOptions;
@@ -93,6 +94,7 @@ class GoogleMapController
9394
private final CirclesController circlesController;
9495
private final HeatmapsController heatmapsController;
9596
private final TileOverlaysController tileOverlaysController;
97+
private final GroundOverlaysController groundOverlaysController;
9698
private MarkerManager markerManager;
9799
private MarkerManager.Collection markerCollection;
98100
private @Nullable List<Messages.PlatformMarker> initialMarkers;
@@ -102,6 +104,7 @@ class GoogleMapController
102104
private @Nullable List<Messages.PlatformCircle> initialCircles;
103105
private @Nullable List<Messages.PlatformHeatmap> initialHeatmaps;
104106
private @Nullable List<Messages.PlatformTileOverlay> initialTileOverlays;
107+
private @Nullable List<Messages.PlatformGroundOverlay> initialGroundOverlays;
105108
// Null except between initialization and onMapReady.
106109
private @Nullable String initialMapStyle;
107110
private boolean lastSetStyleSucceeded;
@@ -137,6 +140,7 @@ class GoogleMapController
137140
this.circlesController = new CirclesController(flutterApi, density);
138141
this.heatmapsController = new HeatmapsController();
139142
this.tileOverlaysController = new TileOverlaysController(flutterApi);
143+
this.groundOverlaysController = new GroundOverlaysController(flutterApi, assetManager, density);
140144
}
141145

142146
// Constructor for testing purposes only
@@ -154,7 +158,8 @@ class GoogleMapController
154158
PolylinesController polylinesController,
155159
CirclesController circlesController,
156160
HeatmapsController heatmapController,
157-
TileOverlaysController tileOverlaysController) {
161+
TileOverlaysController tileOverlaysController,
162+
GroundOverlaysController groundOverlaysController) {
158163
this.id = id;
159164
this.context = context;
160165
this.binaryMessenger = binaryMessenger;
@@ -170,6 +175,7 @@ class GoogleMapController
170175
this.circlesController = circlesController;
171176
this.heatmapsController = heatmapController;
172177
this.tileOverlaysController = tileOverlaysController;
178+
this.groundOverlaysController = groundOverlaysController;
173179
}
174180

175181
@Override
@@ -209,6 +215,7 @@ public void onMapReady(@NonNull GoogleMap googleMap) {
209215
circlesController.setGoogleMap(googleMap);
210216
heatmapsController.setGoogleMap(googleMap);
211217
tileOverlaysController.setGoogleMap(googleMap);
218+
groundOverlaysController.setGoogleMap(googleMap);
212219
setMarkerCollectionListener(this);
213220
setClusterItemClickListener(this);
214221
setClusterItemRenderedListener(this);
@@ -219,6 +226,7 @@ public void onMapReady(@NonNull GoogleMap googleMap) {
219226
updateInitialCircles();
220227
updateInitialHeatmaps();
221228
updateInitialTileOverlays();
229+
updateInitialGroundOverlays();
222230
if (initialPadding != null && initialPadding.size() == 4) {
223231
setPadding(
224232
initialPadding.get(0),
@@ -369,6 +377,11 @@ public void onCircleClick(Circle circle) {
369377
circlesController.onCircleTap(circle.getId());
370378
}
371379

380+
@Override
381+
public void onGroundOverlayClick(@NonNull GroundOverlay groundOverlay) {
382+
groundOverlaysController.onGroundOverlayTap(groundOverlay.getId());
383+
}
384+
372385
@Override
373386
public void dispose() {
374387
if (disposed) {
@@ -401,6 +414,7 @@ private void setGoogleMapListener(@Nullable GoogleMapListener listener) {
401414
googleMap.setOnCircleClickListener(listener);
402415
googleMap.setOnMapClickListener(listener);
403416
googleMap.setOnMapLongClickListener(listener);
417+
googleMap.setOnGroundOverlayClickListener(listener);
404418
}
405419

406420
@VisibleForTesting
@@ -727,6 +741,21 @@ private void updateInitialTileOverlays() {
727741
}
728742
}
729743

744+
@Override
745+
public void setInitialGroundOverlays(
746+
@NonNull List<Messages.PlatformGroundOverlay> initialGroundOverlays) {
747+
this.initialGroundOverlays = initialGroundOverlays;
748+
if (googleMap != null) {
749+
updateInitialGroundOverlays();
750+
}
751+
}
752+
753+
private void updateInitialGroundOverlays() {
754+
if (initialGroundOverlays != null) {
755+
groundOverlaysController.addGroundOverlays(initialGroundOverlays);
756+
}
757+
}
758+
730759
@SuppressLint("MissingPermission")
731760
private void updateMyLocationSettings() {
732761
if (hasLocationPermission()) {
@@ -891,6 +920,16 @@ public void updateTileOverlays(
891920
tileOverlaysController.removeTileOverlays(idsToRemove);
892921
}
893922

923+
@Override
924+
public void updateGroundOverlays(
925+
@NonNull List<Messages.PlatformGroundOverlay> toAdd,
926+
@NonNull List<Messages.PlatformGroundOverlay> toChange,
927+
@NonNull List<String> idsToRemove) {
928+
groundOverlaysController.addGroundOverlays(toAdd);
929+
groundOverlaysController.changeGroundOverlays(toChange);
930+
groundOverlaysController.removeGroundOverlays(idsToRemove);
931+
}
932+
894933
@Override
895934
public @NonNull Messages.PlatformPoint getScreenCoordinate(
896935
@NonNull Messages.PlatformLatLng latLng) {
@@ -1075,6 +1114,20 @@ public Boolean isLiteModeEnabled() {
10751114
.build();
10761115
}
10771116

1117+
@Override
1118+
public @Nullable Messages.PlatformGroundOverlay getGroundOverlayInfo(
1119+
@NonNull String groundOverlayId) {
1120+
GroundOverlay groundOverlay = groundOverlaysController.getGroundOverlay(groundOverlayId);
1121+
if (groundOverlay == null) {
1122+
return null;
1123+
}
1124+
1125+
return Convert.groundOverlayToPigeon(
1126+
groundOverlay,
1127+
groundOverlayId,
1128+
groundOverlaysController.isCreatedWithBounds(groundOverlayId));
1129+
}
1130+
10781131
@Override
10791132
public @NonNull Messages.PlatformZoomRange getZoomRange() {
10801133
return new Messages.PlatformZoomRange.Builder()

packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public PlatformView create(@NonNull Context context, int id, @Nullable Object ar
4646
builder.setInitialCircles(params.getInitialCircles());
4747
builder.setInitialHeatmaps(params.getInitialHeatmaps());
4848
builder.setInitialTileOverlays(params.getInitialTileOverlays());
49+
builder.setInitialGroundOverlays(params.getInitialGroundOverlays());
4950

5051
final String cloudMapId = mapConfig.getCloudMapId();
5152
if (cloudMapId != null) {

packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapListener.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ interface GoogleMapListener
1717
GoogleMap.OnCircleClickListener,
1818
GoogleMap.OnMapClickListener,
1919
GoogleMap.OnMapLongClickListener,
20-
GoogleMap.OnMarkerDragListener {}
20+
GoogleMap.OnMarkerDragListener,
21+
GoogleMap.OnGroundOverlayClickListener {}

packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,8 @@ void setInitialClusterManagers(
6262

6363
void setInitialTileOverlays(@NonNull List<Messages.PlatformTileOverlay> initialTileOverlays);
6464

65+
void setInitialGroundOverlays(
66+
@NonNull List<Messages.PlatformGroundOverlay> initialGroundOverlays);
67+
6568
void setMapStyle(@Nullable String style);
6669
}

0 commit comments

Comments
 (0)