Skip to content

[google_maps_flutter_android] Android changes to support heatmaps #7313

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

Merged
merged 8 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.13.0

* Adds support for heatmap layers.

## 2.12.2

* Updates the example app to use TLHC mode, per current package guidance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ Google Play the latest renderer will not be available and the legacy renderer wi
WARNING: `AndroidMapRenderer.legacy` is known to crash apps and is no longer supported by the Google Maps team
and therefore cannot be supported by the Flutter team.

## Supported Heatmap Options

| Field | Supported |
| ---------------------------- | :-------: |
| Heatmap.dissipating | x |
| Heatmap.maxIntensity | ✓ |
| Heatmap.minimumZoomIntensity | x |
| Heatmap.maximumZoomIntensity | x |
| HeatmapGradient.colorMapSize | ✓ |

[1]: https://pub.dev/packages/google_maps_flutter
[2]: https://flutter.dev/to/endorsed-federated-plugin
[3]: https://docs.flutter.dev/development/platform-integration/android/platform-views
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import com.google.android.gms.maps.model.SquareCap;
import com.google.android.gms.maps.model.Tile;
import com.google.maps.android.clustering.Cluster;
import com.google.maps.android.heatmaps.Gradient;
import com.google.maps.android.heatmaps.WeightedLatLng;
import io.flutter.FlutterInjector;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -41,6 +43,17 @@

/** Conversions between JSON-like values and GoogleMaps data types. */
class Convert {
// These constants must match the corresponding constants in serialization.dart
public static final String HEATMAPS_TO_ADD_KEY = "heatmapsToAdd";
public static final String HEATMAP_ID_KEY = "heatmapId";
public static final String HEATMAP_DATA_KEY = "data";
public static final String HEATMAP_GRADIENT_KEY = "gradient";
public static final String HEATMAP_MAX_INTENSITY_KEY = "maxIntensity";
public static final String HEATMAP_OPACITY_KEY = "opacity";
public static final String HEATMAP_RADIUS_KEY = "radius";
public static final String HEATMAP_GRADIENT_COLORS_KEY = "colors";
public static final String HEATMAP_GRADIENT_START_POINTS_KEY = "startPoints";
public static final String HEATMAP_GRADIENT_COLOR_MAP_SIZE_KEY = "colorMapSize";

private static BitmapDescriptor toBitmapDescriptor(
Object o, AssetManager assetManager, float density) {
Expand Down Expand Up @@ -465,6 +478,17 @@ static LatLng toLatLng(Object o) {
return new LatLng(toDouble(data.get(0)), toDouble(data.get(1)));
}

/**
* Converts a list of serialized weighted lat/lng to a list of WeightedLatLng.
*
* @param o The serialized list of weighted lat/lng.
* @return The list of WeightedLatLng.
*/
static WeightedLatLng toWeightedLatLng(Object o) {
final List<?> data = toList(o);
return new WeightedLatLng(toLatLng(data.get(0)), toDouble(data.get(1)));
}

static Point pointFromPigeon(Messages.PlatformPoint point) {
return new Point(point.getX().intValue(), point.getY().intValue());
}
Expand Down Expand Up @@ -842,6 +866,55 @@ static String interpretCircleOptions(Map<String, ?> data, CircleOptionsSink sink
}
}

/**
* Set the options in the given heatmap object to the given sink.
*
* @param o the object expected to be a Map containing the heatmap options. The options map is
* expected to have the following structure:
* <pre>{@code
* {
* "heatmapId": String,
* "data": List, // List of serialized weighted lat/lng
* "gradient": Map, // Serialized heatmap gradient
* "maxIntensity": Double,
* "opacity": Double,
* "radius": Integer
* }
* }</pre>
*
* @param sink the HeatmapOptionsSink where the options will be set.
* @return the heatmapId.
* @throws IllegalArgumentException if heatmapId is null.
*/
static String interpretHeatmapOptions(Map<String, ?> data, HeatmapOptionsSink sink) {
final Object rawWeightedData = data.get(HEATMAP_DATA_KEY);
if (rawWeightedData != null) {
sink.setWeightedData(toWeightedData(rawWeightedData));
}
final Object gradient = data.get(HEATMAP_GRADIENT_KEY);
if (gradient != null) {
sink.setGradient(toGradient(gradient));
}
final Object maxIntensity = data.get(HEATMAP_MAX_INTENSITY_KEY);
if (maxIntensity != null) {
sink.setMaxIntensity(toDouble(maxIntensity));
}
final Object opacity = data.get(HEATMAP_OPACITY_KEY);
if (opacity != null) {
sink.setOpacity(toDouble(opacity));
}
final Object radius = data.get(HEATMAP_RADIUS_KEY);
if (radius != null) {
sink.setRadius(toInt(radius));
}
final String heatmapId = (String) data.get(HEATMAP_ID_KEY);
if (heatmapId == null) {
throw new IllegalArgumentException("heatmapId was null");
} else {
return heatmapId;
}
}

@VisibleForTesting
static List<LatLng> toPoints(Object o) {
final List<?> data = toList(o);
Expand All @@ -854,6 +927,62 @@ static List<LatLng> toPoints(Object o) {
return points;
}

/**
* Converts the given object to a list of WeightedLatLng objects.
*
* @param o the object to convert. The object is expected to be a List of serialized weighted
* lat/lng.
* @return a list of WeightedLatLng objects.
*/
@VisibleForTesting
static List<WeightedLatLng> toWeightedData(Object o) {
final List<?> data = toList(o);
final List<WeightedLatLng> weightedData = new ArrayList<>(data.size());

for (Object rawWeightedPoint : data) {
weightedData.add(toWeightedLatLng(rawWeightedPoint));
}
return weightedData;
}

/**
* Converts the given object to a Gradient object.
*
* @param o the object to convert. The object is expected to be a Map containing the gradient
* options. The gradient map is expected to have the following structure:
* <pre>{@code
* {
* "colors": List<Integer>,
* "startPoints": List<Float>,
* "colorMapSize": Integer
* }
* }</pre>
*
* @return a Gradient object.
*/
@VisibleForTesting
static Gradient toGradient(Object o) {
final Map<?, ?> data = toMap(o);

final List<?> colorData = toList(data.get(HEATMAP_GRADIENT_COLORS_KEY));
assert colorData != null;
final int[] colors = new int[colorData.size()];
for (int i = 0; i < colorData.size(); i++) {
colors[i] = toInt(colorData.get(i));
}

final List<?> startPointData = toList(data.get(HEATMAP_GRADIENT_START_POINTS_KEY));
assert startPointData != null;
final float[] startPoints = new float[startPointData.size()];
for (int i = 0; i < startPointData.size(); i++) {
startPoints[i] = toFloat(startPointData.get(i));
}

final int colorMapSize = toInt(data.get(HEATMAP_GRADIENT_COLOR_MAP_SIZE_KEY));

return new Gradient(colors, startPoints, colorMapSize);
}

private static List<List<LatLng>> toHoles(Object o) {
final List<?> data = toList(o);
final List<List<LatLng>> holes = new ArrayList<>(data.size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class GoogleMapBuilder implements GoogleMapOptionsSink {
private Object initialPolygons;
private Object initialPolylines;
private Object initialCircles;
private Object initialHeatmaps;
private List<Map<String, ?>> initialTileOverlays;
private Rect padding = new Rect(0, 0, 0, 0);
private @Nullable String style;
Expand All @@ -50,6 +51,7 @@ GoogleMapController build(
controller.setInitialPolygons(initialPolygons);
controller.setInitialPolylines(initialPolylines);
controller.setInitialCircles(initialCircles);
controller.setInitialHeatmaps(initialHeatmaps);
controller.setPadding(padding.top, padding.left, padding.bottom, padding.right);
controller.setInitialTileOverlays(initialTileOverlays);
controller.setMapStyle(style);
Expand Down Expand Up @@ -184,6 +186,11 @@ public void setInitialCircles(Object initialCircles) {
this.initialCircles = initialCircles;
}

@Override
public void setInitialHeatmaps(Object initialHeatmaps) {
this.initialHeatmaps = initialHeatmaps;
}

@Override
public void setInitialTileOverlays(List<Map<String, ?>> initialTileOverlays) {
this.initialTileOverlays = initialTileOverlays;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class GoogleMapController
private final PolygonsController polygonsController;
private final PolylinesController polylinesController;
private final CirclesController circlesController;
private final HeatmapsController heatmapsController;
private final TileOverlaysController tileOverlaysController;
private MarkerManager markerManager;
private MarkerManager.Collection markerCollection;
Expand All @@ -100,6 +101,7 @@ class GoogleMapController
private List<Object> initialPolygons;
private List<Object> initialPolylines;
private List<Object> initialCircles;
private List<Object> initialHeatmaps;
private List<Map<String, ?>> initialTileOverlays;
// Null except between initialization and onMapReady.
private @Nullable String initialMapStyle;
Expand Down Expand Up @@ -129,6 +131,7 @@ class GoogleMapController
this.polygonsController = new PolygonsController(flutterApi, density);
this.polylinesController = new PolylinesController(flutterApi, assetManager, density);
this.circlesController = new CirclesController(flutterApi, density);
this.heatmapsController = new HeatmapsController();
this.tileOverlaysController = new TileOverlaysController(flutterApi);
}

Expand All @@ -146,6 +149,7 @@ class GoogleMapController
PolygonsController polygonsController,
PolylinesController polylinesController,
CirclesController circlesController,
HeatmapsController heatmapController,
TileOverlaysController tileOverlaysController) {
this.id = id;
this.context = context;
Expand All @@ -160,6 +164,7 @@ class GoogleMapController
this.polygonsController = polygonsController;
this.polylinesController = polylinesController;
this.circlesController = circlesController;
this.heatmapsController = heatmapController;
this.tileOverlaysController = tileOverlaysController;
}

Expand Down Expand Up @@ -198,6 +203,7 @@ public void onMapReady(@NonNull GoogleMap googleMap) {
polygonsController.setGoogleMap(googleMap);
polylinesController.setGoogleMap(googleMap);
circlesController.setGoogleMap(googleMap);
heatmapsController.setGoogleMap(googleMap);
tileOverlaysController.setGoogleMap(googleMap);
setMarkerCollectionListener(this);
setClusterItemClickListener(this);
Expand All @@ -207,6 +213,7 @@ public void onMapReady(@NonNull GoogleMap googleMap) {
updateInitialPolygons();
updateInitialPolylines();
updateInitialCircles();
updateInitialHeatmaps();
updateInitialTileOverlays();
if (initialPadding != null && initialPadding.size() == 4) {
setPadding(
Expand Down Expand Up @@ -679,10 +686,23 @@ public void setInitialCircles(Object initialCircles) {
}
}

@Override
public void setInitialHeatmaps(Object initialHeatmaps) {
List<?> heatmaps = (List<?>) initialHeatmaps;
this.initialHeatmaps = heatmaps != null ? new ArrayList<>(heatmaps) : null;
if (googleMap != null) {
updateInitialHeatmaps();
}
}

private void updateInitialCircles() {
circlesController.addJsonCircles(initialCircles);
}

private void updateInitialHeatmaps() {
heatmapsController.addJsonHeatmaps(initialHeatmaps);
}

@Override
public void setInitialTileOverlays(List<Map<String, ?>> initialTileOverlays) {
this.initialTileOverlays = initialTileOverlays;
Expand Down Expand Up @@ -802,6 +822,16 @@ public void updateCircles(
circlesController.removeCircles(idsToRemove);
}

@Override
public void updateHeatmaps(
@NonNull List<Messages.PlatformHeatmap> toAdd,
@NonNull List<Messages.PlatformHeatmap> toChange,
@NonNull List<String> idsToRemove) {
heatmapsController.addHeatmaps(toAdd);
heatmapsController.changeHeatmaps(toChange);
heatmapsController.removeHeatmaps(idsToRemove);
}

@Override
public void updateClusterManagers(
@NonNull List<Messages.PlatformClusterManager> toAdd, @NonNull List<String> idsToRemove) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

package io.flutter.plugins.googlemaps;

import static io.flutter.plugins.googlemaps.Convert.HEATMAPS_TO_ADD_KEY;

import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand Down Expand Up @@ -58,6 +60,9 @@ public PlatformView create(@NonNull Context context, int id, @Nullable Object ar
if (params.containsKey("circlesToAdd")) {
builder.setInitialCircles(params.get("circlesToAdd"));
}
if (params.containsKey(HEATMAPS_TO_ADD_KEY)) {
builder.setInitialHeatmaps(params.get(HEATMAPS_TO_ADD_KEY));
}
if (params.containsKey("tileOverlaysToAdd")) {
builder.setInitialTileOverlays((List<Map<String, ?>>) params.get("tileOverlaysToAdd"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ interface GoogleMapOptionsSink {

void setInitialCircles(Object initialCircles);

void setInitialHeatmaps(Object initialHeatmaps);

void setInitialTileOverlays(List<Map<String, ?>> initialTileOverlays);

void setMapStyle(@Nullable String style);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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 com.google.maps.android.heatmaps.Gradient;
import com.google.maps.android.heatmaps.HeatmapTileProvider;
import com.google.maps.android.heatmaps.WeightedLatLng;
import java.util.List;

/** Builder of a single Heatmap on the map. */
public class HeatmapBuilder implements HeatmapOptionsSink {
private final HeatmapTileProvider.Builder heatmapOptions;

/** Construct a HeatmapBuilder. */
HeatmapBuilder() {
this.heatmapOptions = new HeatmapTileProvider.Builder();
}

/** Build the HeatmapTileProvider with the given options. */
HeatmapTileProvider build() {
return heatmapOptions.build();
}

@Override
public void setWeightedData(@NonNull List<WeightedLatLng> weightedData) {
heatmapOptions.weightedData(weightedData);
}

@Override
public void setGradient(@NonNull Gradient gradient) {
heatmapOptions.gradient(gradient);
}

@Override
public void setMaxIntensity(double maxIntensity) {
heatmapOptions.maxIntensity(maxIntensity);
}

@Override
public void setOpacity(double opacity) {
heatmapOptions.opacity(opacity);
}

@Override
public void setRadius(int radius) {
heatmapOptions.radius(radius);
}
}
Loading