Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[google_maps_flutter] Ground overlay support - platform impls #8563

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.15.0

* Adds support for ground overlay.

## 2.14.12

* Updates androidx.annotation:annotation to 1.9.1.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -849,6 +850,139 @@ 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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return and the parameters should get nullability annotations.

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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have an assertion message indicating why it is required or at least log an error saying that width is required.

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a link to what layer of the stack does not support this functionality? Also why is it safe to use a dummy image?

Copy link
Contributor Author

@jokerttu jokerttu Feb 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used only by inpector api, for testing purposes. App facing widget stores groundoverlays on dart implementation, and there is no real reason to fetch the data back, other than testing.
Image is mandatory field on PlatformGroundOverlay (and it should be kept non-nullable), therefore image must be set for the object.
The image is description either contains set of bytes, or path to asset. This info is converted to format google maps uses (BitmapDescription), and the original data is not stored on native code.
So converting read and possibly rescaled asset bitmap back orginal instruction with asset url is not currently possible without storing the original PlatformImageDescriptor to memory; which is not optimal.
Therefore using dummy image here should be fine?

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.getHeight())
.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
public static @NonNull Messages.PlatformDoublePair buildGroundOverlayAnchorForPigeon(
GroundOverlay groundOverlay) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NonNull

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.
// For longitude, if the bounds cross the antimeridian (west > east),
// adjust the width accordingly.
double west = bounds.southwest.longitude;
double east = bounds.northeast.longitude;
double width = (west <= east) ? (east - west) : (360.0 - (west - east));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we extract these constants into local constants with good naming so it's easier to understand why we need these magic numbers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ash-google is this what you were looking for? cdf4d25


// Adjust the position longitude if it is less than west by adding 360,
// then compute the normalized value.
double normalizedLongitude =
((position.longitude < west ? position.longitude + 360.0 : position.longitude) - west)
/ width;

anchorBuilder.setX(normalizedLongitude);
anchorBuilder.setY(normalizedLatitude);
return anchorBuilder.build();
}

static class BitmapDescriptorFactoryWrapper {
/**
* Creates a BitmapDescriptor from the provided asset key using the {@link
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class GoogleMapBuilder implements GoogleMapOptionsSink {
private List<Messages.PlatformCircle> initialCircles;
private List<Messages.PlatformHeatmap> initialHeatmaps;
private List<Messages.PlatformTileOverlay> initialTileOverlays;
private List<Messages.PlatformGroundOverlay> initialGroundOverlays;
private Rect padding = new Rect(0, 0, 0, 0);
private @Nullable String style;

Expand All @@ -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;
}
Expand Down Expand Up @@ -197,6 +199,12 @@ public void setInitialTileOverlays(
this.initialTileOverlays = initialTileOverlays;
}

@Override
public void setInitialGroundOverlays(
@NonNull List<Messages.PlatformGroundOverlay> initialGroundOverlays) {
this.initialGroundOverlays = initialGroundOverlays;
}

@Override
public void setMapStyle(@Nullable String style) {
this.style = style;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Messages.PlatformMarker> initialMarkers;
Expand All @@ -102,6 +104,7 @@ class GoogleMapController
private @Nullable List<Messages.PlatformCircle> initialCircles;
private @Nullable List<Messages.PlatformHeatmap> initialHeatmaps;
private @Nullable List<Messages.PlatformTileOverlay> initialTileOverlays;
private @Nullable List<Messages.PlatformGroundOverlay> initialGroundOverlays;
// Null except between initialization and onMapReady.
private @Nullable String initialMapStyle;
private boolean lastSetStyleSucceeded;
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -170,6 +175,7 @@ class GoogleMapController
this.circlesController = circlesController;
this.heatmapsController = heatmapController;
this.tileOverlaysController = tileOverlaysController;
this.groundOverlaysController = groundOverlaysController;
}

@Override
Expand Down Expand Up @@ -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);
Expand All @@ -219,6 +226,7 @@ public void onMapReady(@NonNull GoogleMap googleMap) {
updateInitialCircles();
updateInitialHeatmaps();
updateInitialTileOverlays();
updateInitialGroundOverlays();
if (initialPadding != null && initialPadding.size() == 4) {
setPadding(
initialPadding.get(0),
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -401,6 +414,7 @@ private void setGoogleMapListener(@Nullable GoogleMapListener listener) {
googleMap.setOnCircleClickListener(listener);
googleMap.setOnMapClickListener(listener);
googleMap.setOnMapLongClickListener(listener);
googleMap.setOnGroundOverlayClickListener(listener);
}

@VisibleForTesting
Expand Down Expand Up @@ -727,6 +741,21 @@ private void updateInitialTileOverlays() {
}
}

@Override
public void setInitialGroundOverlays(
@NonNull List<Messages.PlatformGroundOverlay> initialGroundOverlays) {
this.initialGroundOverlays = initialGroundOverlays;
if (googleMap != null) {
updateInitialGroundOverlays();
}
}

private void updateInitialGroundOverlays() {
if (initialGroundOverlays != null) {
groundOverlaysController.addGroundOverlays(initialGroundOverlays);
}
}

@SuppressLint("MissingPermission")
private void updateMyLocationSettings() {
if (hasLocationPermission()) {
Expand Down Expand Up @@ -891,6 +920,16 @@ public void updateTileOverlays(
tileOverlaysController.removeTileOverlays(idsToRemove);
}

@Override
public void updateGroundOverlays(
@NonNull List<Messages.PlatformGroundOverlay> toAdd,
@NonNull List<Messages.PlatformGroundOverlay> toChange,
@NonNull List<String> idsToRemove) {
groundOverlaysController.addGroundOverlays(toAdd);
groundOverlaysController.changeGroundOverlays(toChange);
groundOverlaysController.removeGroundOverlays(idsToRemove);
}

@Override
public @NonNull Messages.PlatformPoint getScreenCoordinate(
@NonNull Messages.PlatformLatLng latLng) {
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ interface GoogleMapListener
GoogleMap.OnCircleClickListener,
GoogleMap.OnMapClickListener,
GoogleMap.OnMapLongClickListener,
GoogleMap.OnMarkerDragListener {}
GoogleMap.OnMarkerDragListener,
GoogleMap.OnGroundOverlayClickListener {}
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,8 @@ void setInitialClusterManagers(

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

void setInitialGroundOverlays(
@NonNull List<Messages.PlatformGroundOverlay> initialGroundOverlays);

void setMapStyle(@Nullable String style);
}
Loading