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 All @@ -40,6 +41,7 @@
import com.google.maps.android.heatmaps.Gradient;
import com.google.maps.android.heatmaps.WeightedLatLng;
import io.flutter.FlutterInjector;
import io.flutter.plugins.googlemaps.Messages.FlutterError;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
Expand Down Expand Up @@ -849,6 +851,141 @@ 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 @NonNull String interpretGroundOverlayOptions(
@NonNull Messages.PlatformGroundOverlay groundOverlay,
@NonNull GroundOverlaySink sink,
@NonNull AssetManager assetManager,
float density,
@NonNull 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) {
if (groundOverlay.getWidth() == null) {
throw new FlutterError(
"Invalid GroundOverlay",
"Width is required when using a ground overlay with a position.",
null);
}
sink.setPosition(
latLngFromPigeon(groundOverlay.getPosition()),
groundOverlay.getWidth().floatValue(),
groundOverlay.getHeight() != null ? groundOverlay.getHeight().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 a required field of PlatformGroundOverlay and converting
// BitmapDescriptor used by Google Maps back to PlatformImageDescriptor is not currently supported.
Messages.PlatformBitmap dummyImage =
new Messages.PlatformBitmap.Builder()
.setBitmap(
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(
@NonNull GroundOverlay groundOverlay) {
Messages.PlatformDoublePair.Builder anchorBuilder = new Messages.PlatformDoublePair.Builder();

// Position is overlays anchor point. Calculate normalized anchor point based on position and bounds.
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);

// Constant for full circle degrees.
final double FULL_CIRCLE_DEGREES = 360.0;

// Calculate normalized longitude.
// For longitude, if the bounds cross the antimeridian (west > east),
// adjust the width accordingly.
double west = bounds.southwest.longitude;
double east = bounds.northeast.longitude;
double width = (west <= east) ? (east - west) : (FULL_CIRCLE_DEGREES - (west - east));

// Normalize the longitude of the anchor position relative to the western boundary.
// Handles cases where the ground overlay crosses the antimeridian.
double normalizedLongitude =
((position.longitude < west ? position.longitude + FULL_CIRCLE_DEGREES : 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