Skip to content

Commit 89d28fd

Browse files
authored
[google_maps_flutter_android] Android changes to support heatmaps (#7313)
Sequel to: - #7312 Prequel to: - #3257
1 parent cfbcc94 commit 89d28fd

23 files changed

+1199
-79
lines changed

packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.13.0
2+
3+
* Adds support for heatmap layers.
4+
15
## 2.12.2
26

37
* Updates the example app to use TLHC mode, per current package guidance.

packages/google_maps_flutter/google_maps_flutter_android/README.md

+10
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ Google Play the latest renderer will not be available and the legacy renderer wi
7878
WARNING: `AndroidMapRenderer.legacy` is known to crash apps and is no longer supported by the Google Maps team
7979
and therefore cannot be supported by the Flutter team.
8080

81+
## Supported Heatmap Options
82+
83+
| Field | Supported |
84+
| ---------------------------- | :-------: |
85+
| Heatmap.dissipating | x |
86+
| Heatmap.maxIntensity ||
87+
| Heatmap.minimumZoomIntensity | x |
88+
| Heatmap.maximumZoomIntensity | x |
89+
| HeatmapGradient.colorMapSize ||
90+
8191
[1]: https://pub.dev/packages/google_maps_flutter
8292
[2]: https://flutter.dev/to/endorsed-federated-plugin
8393
[3]: https://docs.flutter.dev/development/platform-integration/android/platform-views

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

+129
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import com.google.android.gms.maps.model.SquareCap;
3131
import com.google.android.gms.maps.model.Tile;
3232
import com.google.maps.android.clustering.Cluster;
33+
import com.google.maps.android.heatmaps.Gradient;
34+
import com.google.maps.android.heatmaps.WeightedLatLng;
3335
import io.flutter.FlutterInjector;
3436
import java.io.IOException;
3537
import java.io.InputStream;
@@ -41,6 +43,17 @@
4143

4244
/** Conversions between JSON-like values and GoogleMaps data types. */
4345
class Convert {
46+
// These constants must match the corresponding constants in serialization.dart
47+
public static final String HEATMAPS_TO_ADD_KEY = "heatmapsToAdd";
48+
public static final String HEATMAP_ID_KEY = "heatmapId";
49+
public static final String HEATMAP_DATA_KEY = "data";
50+
public static final String HEATMAP_GRADIENT_KEY = "gradient";
51+
public static final String HEATMAP_MAX_INTENSITY_KEY = "maxIntensity";
52+
public static final String HEATMAP_OPACITY_KEY = "opacity";
53+
public static final String HEATMAP_RADIUS_KEY = "radius";
54+
public static final String HEATMAP_GRADIENT_COLORS_KEY = "colors";
55+
public static final String HEATMAP_GRADIENT_START_POINTS_KEY = "startPoints";
56+
public static final String HEATMAP_GRADIENT_COLOR_MAP_SIZE_KEY = "colorMapSize";
4457

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

481+
/**
482+
* Converts a list of serialized weighted lat/lng to a list of WeightedLatLng.
483+
*
484+
* @param o The serialized list of weighted lat/lng.
485+
* @return The list of WeightedLatLng.
486+
*/
487+
static WeightedLatLng toWeightedLatLng(Object o) {
488+
final List<?> data = toList(o);
489+
return new WeightedLatLng(toLatLng(data.get(0)), toDouble(data.get(1)));
490+
}
491+
468492
static Point pointFromPigeon(Messages.PlatformPoint point) {
469493
return new Point(point.getX().intValue(), point.getY().intValue());
470494
}
@@ -842,6 +866,55 @@ static String interpretCircleOptions(Map<String, ?> data, CircleOptionsSink sink
842866
}
843867
}
844868

869+
/**
870+
* Set the options in the given heatmap object to the given sink.
871+
*
872+
* @param o the object expected to be a Map containing the heatmap options. The options map is
873+
* expected to have the following structure:
874+
* <pre>{@code
875+
* {
876+
* "heatmapId": String,
877+
* "data": List, // List of serialized weighted lat/lng
878+
* "gradient": Map, // Serialized heatmap gradient
879+
* "maxIntensity": Double,
880+
* "opacity": Double,
881+
* "radius": Integer
882+
* }
883+
* }</pre>
884+
*
885+
* @param sink the HeatmapOptionsSink where the options will be set.
886+
* @return the heatmapId.
887+
* @throws IllegalArgumentException if heatmapId is null.
888+
*/
889+
static String interpretHeatmapOptions(Map<String, ?> data, HeatmapOptionsSink sink) {
890+
final Object rawWeightedData = data.get(HEATMAP_DATA_KEY);
891+
if (rawWeightedData != null) {
892+
sink.setWeightedData(toWeightedData(rawWeightedData));
893+
}
894+
final Object gradient = data.get(HEATMAP_GRADIENT_KEY);
895+
if (gradient != null) {
896+
sink.setGradient(toGradient(gradient));
897+
}
898+
final Object maxIntensity = data.get(HEATMAP_MAX_INTENSITY_KEY);
899+
if (maxIntensity != null) {
900+
sink.setMaxIntensity(toDouble(maxIntensity));
901+
}
902+
final Object opacity = data.get(HEATMAP_OPACITY_KEY);
903+
if (opacity != null) {
904+
sink.setOpacity(toDouble(opacity));
905+
}
906+
final Object radius = data.get(HEATMAP_RADIUS_KEY);
907+
if (radius != null) {
908+
sink.setRadius(toInt(radius));
909+
}
910+
final String heatmapId = (String) data.get(HEATMAP_ID_KEY);
911+
if (heatmapId == null) {
912+
throw new IllegalArgumentException("heatmapId was null");
913+
} else {
914+
return heatmapId;
915+
}
916+
}
917+
845918
@VisibleForTesting
846919
static List<LatLng> toPoints(Object o) {
847920
final List<?> data = toList(o);
@@ -854,6 +927,62 @@ static List<LatLng> toPoints(Object o) {
854927
return points;
855928
}
856929

930+
/**
931+
* Converts the given object to a list of WeightedLatLng objects.
932+
*
933+
* @param o the object to convert. The object is expected to be a List of serialized weighted
934+
* lat/lng.
935+
* @return a list of WeightedLatLng objects.
936+
*/
937+
@VisibleForTesting
938+
static List<WeightedLatLng> toWeightedData(Object o) {
939+
final List<?> data = toList(o);
940+
final List<WeightedLatLng> weightedData = new ArrayList<>(data.size());
941+
942+
for (Object rawWeightedPoint : data) {
943+
weightedData.add(toWeightedLatLng(rawWeightedPoint));
944+
}
945+
return weightedData;
946+
}
947+
948+
/**
949+
* Converts the given object to a Gradient object.
950+
*
951+
* @param o the object to convert. The object is expected to be a Map containing the gradient
952+
* options. The gradient map is expected to have the following structure:
953+
* <pre>{@code
954+
* {
955+
* "colors": List<Integer>,
956+
* "startPoints": List<Float>,
957+
* "colorMapSize": Integer
958+
* }
959+
* }</pre>
960+
*
961+
* @return a Gradient object.
962+
*/
963+
@VisibleForTesting
964+
static Gradient toGradient(Object o) {
965+
final Map<?, ?> data = toMap(o);
966+
967+
final List<?> colorData = toList(data.get(HEATMAP_GRADIENT_COLORS_KEY));
968+
assert colorData != null;
969+
final int[] colors = new int[colorData.size()];
970+
for (int i = 0; i < colorData.size(); i++) {
971+
colors[i] = toInt(colorData.get(i));
972+
}
973+
974+
final List<?> startPointData = toList(data.get(HEATMAP_GRADIENT_START_POINTS_KEY));
975+
assert startPointData != null;
976+
final float[] startPoints = new float[startPointData.size()];
977+
for (int i = 0; i < startPointData.size(); i++) {
978+
startPoints[i] = toFloat(startPointData.get(i));
979+
}
980+
981+
final int colorMapSize = toInt(data.get(HEATMAP_GRADIENT_COLOR_MAP_SIZE_KEY));
982+
983+
return new Gradient(colors, startPoints, colorMapSize);
984+
}
985+
857986
private static List<List<LatLng>> toHoles(Object o) {
858987
final List<?> data = toList(o);
859988
final List<List<LatLng>> holes = new ArrayList<>(data.size());

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

+7
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class GoogleMapBuilder implements GoogleMapOptionsSink {
2727
private Object initialPolygons;
2828
private Object initialPolylines;
2929
private Object initialCircles;
30+
private Object initialHeatmaps;
3031
private List<Map<String, ?>> initialTileOverlays;
3132
private Rect padding = new Rect(0, 0, 0, 0);
3233
private @Nullable String style;
@@ -50,6 +51,7 @@ GoogleMapController build(
5051
controller.setInitialPolygons(initialPolygons);
5152
controller.setInitialPolylines(initialPolylines);
5253
controller.setInitialCircles(initialCircles);
54+
controller.setInitialHeatmaps(initialHeatmaps);
5355
controller.setPadding(padding.top, padding.left, padding.bottom, padding.right);
5456
controller.setInitialTileOverlays(initialTileOverlays);
5557
controller.setMapStyle(style);
@@ -184,6 +186,11 @@ public void setInitialCircles(Object initialCircles) {
184186
this.initialCircles = initialCircles;
185187
}
186188

189+
@Override
190+
public void setInitialHeatmaps(Object initialHeatmaps) {
191+
this.initialHeatmaps = initialHeatmaps;
192+
}
193+
187194
@Override
188195
public void setInitialTileOverlays(List<Map<String, ?>> initialTileOverlays) {
189196
this.initialTileOverlays = initialTileOverlays;

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

+30
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ class GoogleMapController
9292
private final PolygonsController polygonsController;
9393
private final PolylinesController polylinesController;
9494
private final CirclesController circlesController;
95+
private final HeatmapsController heatmapsController;
9596
private final TileOverlaysController tileOverlaysController;
9697
private MarkerManager markerManager;
9798
private MarkerManager.Collection markerCollection;
@@ -100,6 +101,7 @@ class GoogleMapController
100101
private List<Object> initialPolygons;
101102
private List<Object> initialPolylines;
102103
private List<Object> initialCircles;
104+
private List<Object> initialHeatmaps;
103105
private List<Map<String, ?>> initialTileOverlays;
104106
// Null except between initialization and onMapReady.
105107
private @Nullable String initialMapStyle;
@@ -129,6 +131,7 @@ class GoogleMapController
129131
this.polygonsController = new PolygonsController(flutterApi, density);
130132
this.polylinesController = new PolylinesController(flutterApi, assetManager, density);
131133
this.circlesController = new CirclesController(flutterApi, density);
134+
this.heatmapsController = new HeatmapsController();
132135
this.tileOverlaysController = new TileOverlaysController(flutterApi);
133136
}
134137

@@ -146,6 +149,7 @@ class GoogleMapController
146149
PolygonsController polygonsController,
147150
PolylinesController polylinesController,
148151
CirclesController circlesController,
152+
HeatmapsController heatmapController,
149153
TileOverlaysController tileOverlaysController) {
150154
this.id = id;
151155
this.context = context;
@@ -160,6 +164,7 @@ class GoogleMapController
160164
this.polygonsController = polygonsController;
161165
this.polylinesController = polylinesController;
162166
this.circlesController = circlesController;
167+
this.heatmapsController = heatmapController;
163168
this.tileOverlaysController = tileOverlaysController;
164169
}
165170

@@ -198,6 +203,7 @@ public void onMapReady(@NonNull GoogleMap googleMap) {
198203
polygonsController.setGoogleMap(googleMap);
199204
polylinesController.setGoogleMap(googleMap);
200205
circlesController.setGoogleMap(googleMap);
206+
heatmapsController.setGoogleMap(googleMap);
201207
tileOverlaysController.setGoogleMap(googleMap);
202208
setMarkerCollectionListener(this);
203209
setClusterItemClickListener(this);
@@ -207,6 +213,7 @@ public void onMapReady(@NonNull GoogleMap googleMap) {
207213
updateInitialPolygons();
208214
updateInitialPolylines();
209215
updateInitialCircles();
216+
updateInitialHeatmaps();
210217
updateInitialTileOverlays();
211218
if (initialPadding != null && initialPadding.size() == 4) {
212219
setPadding(
@@ -679,10 +686,23 @@ public void setInitialCircles(Object initialCircles) {
679686
}
680687
}
681688

689+
@Override
690+
public void setInitialHeatmaps(Object initialHeatmaps) {
691+
List<?> heatmaps = (List<?>) initialHeatmaps;
692+
this.initialHeatmaps = heatmaps != null ? new ArrayList<>(heatmaps) : null;
693+
if (googleMap != null) {
694+
updateInitialHeatmaps();
695+
}
696+
}
697+
682698
private void updateInitialCircles() {
683699
circlesController.addJsonCircles(initialCircles);
684700
}
685701

702+
private void updateInitialHeatmaps() {
703+
heatmapsController.addJsonHeatmaps(initialHeatmaps);
704+
}
705+
686706
@Override
687707
public void setInitialTileOverlays(List<Map<String, ?>> initialTileOverlays) {
688708
this.initialTileOverlays = initialTileOverlays;
@@ -802,6 +822,16 @@ public void updateCircles(
802822
circlesController.removeCircles(idsToRemove);
803823
}
804824

825+
@Override
826+
public void updateHeatmaps(
827+
@NonNull List<Messages.PlatformHeatmap> toAdd,
828+
@NonNull List<Messages.PlatformHeatmap> toChange,
829+
@NonNull List<String> idsToRemove) {
830+
heatmapsController.addHeatmaps(toAdd);
831+
heatmapsController.changeHeatmaps(toChange);
832+
heatmapsController.removeHeatmaps(idsToRemove);
833+
}
834+
805835
@Override
806836
public void updateClusterManagers(
807837
@NonNull List<Messages.PlatformClusterManager> toAdd, @NonNull List<String> idsToRemove) {

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

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
package io.flutter.plugins.googlemaps;
66

7+
import static io.flutter.plugins.googlemaps.Convert.HEATMAPS_TO_ADD_KEY;
8+
79
import android.content.Context;
810
import androidx.annotation.NonNull;
911
import androidx.annotation.Nullable;
@@ -58,6 +60,9 @@ public PlatformView create(@NonNull Context context, int id, @Nullable Object ar
5860
if (params.containsKey("circlesToAdd")) {
5961
builder.setInitialCircles(params.get("circlesToAdd"));
6062
}
63+
if (params.containsKey(HEATMAPS_TO_ADD_KEY)) {
64+
builder.setInitialHeatmaps(params.get(HEATMAPS_TO_ADD_KEY));
65+
}
6166
if (params.containsKey("tileOverlaysToAdd")) {
6267
builder.setInitialTileOverlays((List<Map<String, ?>>) params.get("tileOverlaysToAdd"));
6368
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ interface GoogleMapOptionsSink {
5757

5858
void setInitialCircles(Object initialCircles);
5959

60+
void setInitialHeatmaps(Object initialHeatmaps);
61+
6062
void setInitialTileOverlays(List<Map<String, ?>> initialTileOverlays);
6163

6264
void setMapStyle(@Nullable String style);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.googlemaps;
6+
7+
import androidx.annotation.NonNull;
8+
import com.google.maps.android.heatmaps.Gradient;
9+
import com.google.maps.android.heatmaps.HeatmapTileProvider;
10+
import com.google.maps.android.heatmaps.WeightedLatLng;
11+
import java.util.List;
12+
13+
/** Builder of a single Heatmap on the map. */
14+
public class HeatmapBuilder implements HeatmapOptionsSink {
15+
private final HeatmapTileProvider.Builder heatmapOptions;
16+
17+
/** Construct a HeatmapBuilder. */
18+
HeatmapBuilder() {
19+
this.heatmapOptions = new HeatmapTileProvider.Builder();
20+
}
21+
22+
/** Build the HeatmapTileProvider with the given options. */
23+
HeatmapTileProvider build() {
24+
return heatmapOptions.build();
25+
}
26+
27+
@Override
28+
public void setWeightedData(@NonNull List<WeightedLatLng> weightedData) {
29+
heatmapOptions.weightedData(weightedData);
30+
}
31+
32+
@Override
33+
public void setGradient(@NonNull Gradient gradient) {
34+
heatmapOptions.gradient(gradient);
35+
}
36+
37+
@Override
38+
public void setMaxIntensity(double maxIntensity) {
39+
heatmapOptions.maxIntensity(maxIntensity);
40+
}
41+
42+
@Override
43+
public void setOpacity(double opacity) {
44+
heatmapOptions.opacity(opacity);
45+
}
46+
47+
@Override
48+
public void setRadius(int radius) {
49+
heatmapOptions.radius(radius);
50+
}
51+
}

0 commit comments

Comments
 (0)