diff --git a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md index fa0e4a06a86..e48f90c9073 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md @@ -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. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/README.md b/packages/google_maps_flutter/google_maps_flutter_android/README.md index ccef3a0b127..a4f0ff5a348 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/README.md @@ -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 diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index d9caf66d688..825bb504abb 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java @@ -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; @@ -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) { @@ -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()); } @@ -842,6 +866,55 @@ static String interpretCircleOptions(Map 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: + *
{@code
+   * {
+   *   "heatmapId": String,
+   *   "data": List, // List of serialized weighted lat/lng
+   *   "gradient": Map, // Serialized heatmap gradient
+   *   "maxIntensity": Double,
+   *   "opacity": Double,
+   *   "radius": Integer
+   * }
+   * }
+ * + * @param sink the HeatmapOptionsSink where the options will be set. + * @return the heatmapId. + * @throws IllegalArgumentException if heatmapId is null. + */ + static String interpretHeatmapOptions(Map 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 toPoints(Object o) { final List data = toList(o); @@ -854,6 +927,62 @@ static List 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 toWeightedData(Object o) { + final List data = toList(o); + final List 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: + *
{@code
+   * {
+   *   "colors": List,
+   *   "startPoints": List,
+   *   "colorMapSize": Integer
+   * }
+   * }
+ * + * @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> toHoles(Object o) { final List data = toList(o); final List> holes = new ArrayList<>(data.size()); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java index 02477418d42..1273a21f664 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java @@ -27,6 +27,7 @@ class GoogleMapBuilder implements GoogleMapOptionsSink { private Object initialPolygons; private Object initialPolylines; private Object initialCircles; + private Object initialHeatmaps; private List> initialTileOverlays; private Rect padding = new Rect(0, 0, 0, 0); private @Nullable String style; @@ -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); @@ -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> initialTileOverlays) { this.initialTileOverlays = initialTileOverlays; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index 3e8aaffe97c..4cb5e3b57af 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -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; @@ -100,6 +101,7 @@ class GoogleMapController private List initialPolygons; private List initialPolylines; private List initialCircles; + private List initialHeatmaps; private List> initialTileOverlays; // Null except between initialization and onMapReady. private @Nullable String initialMapStyle; @@ -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); } @@ -146,6 +149,7 @@ class GoogleMapController PolygonsController polygonsController, PolylinesController polylinesController, CirclesController circlesController, + HeatmapsController heatmapController, TileOverlaysController tileOverlaysController) { this.id = id; this.context = context; @@ -160,6 +164,7 @@ class GoogleMapController this.polygonsController = polygonsController; this.polylinesController = polylinesController; this.circlesController = circlesController; + this.heatmapsController = heatmapController; this.tileOverlaysController = tileOverlaysController; } @@ -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); @@ -207,6 +213,7 @@ public void onMapReady(@NonNull GoogleMap googleMap) { updateInitialPolygons(); updateInitialPolylines(); updateInitialCircles(); + updateInitialHeatmaps(); updateInitialTileOverlays(); if (initialPadding != null && initialPadding.size() == 4) { setPadding( @@ -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> initialTileOverlays) { this.initialTileOverlays = initialTileOverlays; @@ -802,6 +822,16 @@ public void updateCircles( circlesController.removeCircles(idsToRemove); } + @Override + public void updateHeatmaps( + @NonNull List toAdd, + @NonNull List toChange, + @NonNull List idsToRemove) { + heatmapsController.addHeatmaps(toAdd); + heatmapsController.changeHeatmaps(toChange); + heatmapsController.removeHeatmaps(idsToRemove); + } + @Override public void updateClusterManagers( @NonNull List toAdd, @NonNull List idsToRemove) { diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java index b8d6485d35e..81ab3ec7207 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java @@ -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; @@ -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>) params.get("tileOverlaysToAdd")); } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java index 9f744a653b3..b353da6f65f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java @@ -57,6 +57,8 @@ interface GoogleMapOptionsSink { void setInitialCircles(Object initialCircles); + void setInitialHeatmaps(Object initialHeatmaps); + void setInitialTileOverlays(List> initialTileOverlays); void setMapStyle(@Nullable String style); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapBuilder.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapBuilder.java new file mode 100644 index 00000000000..8b335951958 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapBuilder.java @@ -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 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); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapController.java new file mode 100644 index 00000000000..a2990e23429 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapController.java @@ -0,0 +1,59 @@ +// 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.android.gms.maps.model.TileOverlay; +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; + +/** Controller of a single Heatmap on the map. */ +public class HeatmapController implements HeatmapOptionsSink { + private final @NonNull HeatmapTileProvider heatmap; + private final @NonNull TileOverlay heatmapTileOverlay; + + /** Construct a HeatmapController with the given heatmap and heatmapTileOverlay. */ + HeatmapController(@NonNull HeatmapTileProvider heatmap, @NonNull TileOverlay heatmapTileOverlay) { + this.heatmap = heatmap; + this.heatmapTileOverlay = heatmapTileOverlay; + } + + /** Remove the heatmap from the map. */ + void remove() { + heatmapTileOverlay.remove(); + } + + /** Clear the tile cache of the heatmap in order to update the heatmap. */ + void clearTileCache() { + heatmapTileOverlay.clearTileCache(); + } + + @Override + public void setWeightedData(@NonNull List weightedData) { + heatmap.setWeightedData(weightedData); + } + + @Override + public void setGradient(@NonNull Gradient gradient) { + heatmap.setGradient(gradient); + } + + @Override + public void setMaxIntensity(double maxIntensity) { + heatmap.setMaxIntensity(maxIntensity); + } + + @Override + public void setOpacity(double opacity) { + heatmap.setOpacity(opacity); + } + + @Override + public void setRadius(int radius) { + heatmap.setRadius(radius); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapOptionsSink.java new file mode 100644 index 00000000000..fc571665c7d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapOptionsSink.java @@ -0,0 +1,28 @@ +// 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.WeightedLatLng; +import java.util.List; + +/** Receiver of Heatmap configuration options. */ +interface HeatmapOptionsSink { + /** Set the weighted data to be used to generate the heatmap. */ + void setWeightedData(@NonNull List weightedData); + + /** Set the gradient to be used to color the heatmap. */ + void setGradient(@NonNull Gradient gradient); + + /** Set the maximum intensity for the heatmap. */ + void setMaxIntensity(double maxIntensity); + + /** Set the opacity of the heatmap. */ + void setOpacity(double opacity); + + /** Set the radius of the heatmap. */ + void setRadius(int radius); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapsController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapsController.java new file mode 100644 index 00000000000..2bbc685365c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapsController.java @@ -0,0 +1,114 @@ +// 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 static io.flutter.plugins.googlemaps.Convert.HEATMAP_ID_KEY; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.TileOverlay; +import com.google.android.gms.maps.model.TileOverlayOptions; +import com.google.maps.android.heatmaps.HeatmapTileProvider; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Controller of multiple Heatmaps on the map. */ +public class HeatmapsController { + /** Mapping from Heatmap ID to HeatmapController. */ + private final Map heatmapIdToController; + /** The GoogleMap to which the heatmaps are added. */ + private GoogleMap googleMap; + + /** Constructs a HeatmapsController. */ + HeatmapsController() { + this.heatmapIdToController = new HashMap<>(); + } + + /** Sets the GoogleMap to which the heatmaps are added. */ + void setGoogleMap(GoogleMap googleMap) { + this.googleMap = googleMap; + } + + /** Adds heatmaps to the map from json data. */ + void addJsonHeatmaps(List heatmapsToAdd) { + if (heatmapsToAdd != null) { + for (Object heatmapToAdd : heatmapsToAdd) { + @SuppressWarnings("unchecked") + Map heatmapMap = (Map) heatmapToAdd; + addJsonHeatmap(heatmapMap); + } + } + } + + /** Adds heatmaps to the map. */ + void addHeatmaps(@NonNull List heatmapsToAdd) { + for (Messages.PlatformHeatmap heatmapToAdd : heatmapsToAdd) { + addJsonHeatmap(heatmapToAdd.getJson()); + } + } + + /** Updates the given heatmaps on the map. */ + void changeHeatmaps(@NonNull List heatmapsToChange) { + for (Messages.PlatformHeatmap heatmapToChange : heatmapsToChange) { + changeJsonHeatmap(heatmapToChange.getJson()); + } + } + + /** Removes heatmaps with the given ids from the map. */ + void removeHeatmaps(@NonNull List heatmapIdsToRemove) { + for (String heatmapId : heatmapIdsToRemove) { + HeatmapController heatmapController = heatmapIdToController.remove(heatmapId); + if (heatmapController != null) { + heatmapController.remove(); + heatmapIdToController.remove(heatmapId); + } + } + } + + /** Builds the heatmap. This method exists to allow mocking the HeatmapTileProvider in tests. */ + @VisibleForTesting + public @NonNull HeatmapTileProvider buildHeatmap(@NonNull HeatmapBuilder builder) { + return builder.build(); + } + + /** Adds a heatmap to the map from json data. */ + private void addJsonHeatmap(Map heatmap) { + if (heatmap == null) { + return; + } + HeatmapBuilder heatmapBuilder = new HeatmapBuilder(); + String heatmapId = Convert.interpretHeatmapOptions(heatmap, heatmapBuilder); + HeatmapTileProvider options = buildHeatmap(heatmapBuilder); + addHeatmap(heatmapId, options); + } + + /** Adds a heatmap to the map. */ + private void addHeatmap(String heatmapId, HeatmapTileProvider options) { + TileOverlay heatmapTileOverlay = + googleMap.addTileOverlay(new TileOverlayOptions().tileProvider(options)); + HeatmapController heatmapController = new HeatmapController(options, heatmapTileOverlay); + heatmapIdToController.put(heatmapId, heatmapController); + } + + /** Updates the given heatmap on the map. */ + private void changeJsonHeatmap(Map heatmap) { + if (heatmap == null) { + return; + } + String heatmapId = getHeatmapId(heatmap); + HeatmapController heatmapController = heatmapIdToController.get(heatmapId); + if (heatmapController != null) { + Convert.interpretHeatmapOptions(heatmap, heatmapController); + heatmapController.clearTileCache(); + } + } + + /** Returns the heatmap id from the given heatmap data. */ + private static String getHeatmapId(Map heatmap) { + return (String) heatmap.get(HEATMAP_ID_KEY); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java index 2e5a61cb075..32fc6eaf6cb 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java @@ -242,7 +242,7 @@ ArrayList toList() { public static final class PlatformCameraUpdate { /** * The update data, as JSON. This should only be set from CameraUpdate.toJson, and the native - * code must intepret it according to the internal implementation details of the CameraUpdate + * code must interpret it according to the internal implementation details of the CameraUpdate * class. */ private @NonNull Object json; @@ -318,7 +318,7 @@ ArrayList toList() { public static final class PlatformCircle { /** * The circle data, as JSON. This should only be set from Circle.toJson, and the native code - * must intepret it according to the internal implementation details of that method. + * must interpret it according to the internal implementation details of that method. */ private @NonNull Map json; @@ -385,6 +385,81 @@ ArrayList toList() { } } + /** + * Pigeon equivalent of the Heatmap class. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformHeatmap { + /** + * The heatmap data, as JSON. This should only be set from Heatmap.toJson, and the native code + * must interpret it according to the internal implementation details of that method. + */ + private @NonNull Map json; + + public @NonNull Map getJson() { + return json; + } + + public void setJson(@NonNull Map setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"json\" is null."); + } + this.json = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformHeatmap() {} + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PlatformHeatmap that = (PlatformHeatmap) o; + return json.equals(that.json); + } + + @Override + public int hashCode() { + return Objects.hash(json); + } + + public static final class Builder { + + private @Nullable Map json; + + @CanIgnoreReturnValue + public @NonNull Builder setJson(@NonNull Map setterArg) { + this.json = setterArg; + return this; + } + + public @NonNull PlatformHeatmap build() { + PlatformHeatmap pigeonReturn = new PlatformHeatmap(); + pigeonReturn.setJson(json); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(1); + toListResult.add(json); + return toListResult; + } + + static @NonNull PlatformHeatmap fromList(@NonNull ArrayList __pigeon_list) { + PlatformHeatmap pigeonResult = new PlatformHeatmap(); + Object json = __pigeon_list.get(0); + pigeonResult.setJson((Map) json); + return pigeonResult; + } + } + /** * Pigeon equivalent of the ClusterManager class. * @@ -464,7 +539,7 @@ ArrayList toList() { public static final class PlatformMarker { /** * The marker data, as JSON. This should only be set from Marker.toJson, and the native code - * must intepret it according to the internal implementation details of that method. + * must interpret it according to the internal implementation details of that method. */ private @NonNull Map json; @@ -539,7 +614,7 @@ ArrayList toList() { public static final class PlatformPolygon { /** * The polygon data, as JSON. This should only be set from Polygon.toJson, and the native code - * must intepret it according to the internal implementation details of that method. + * must interpret it according to the internal implementation details of that method. */ private @NonNull Map json; @@ -614,7 +689,7 @@ ArrayList toList() { public static final class PlatformPolyline { /** * The polyline data, as JSON. This should only be set from Polyline.toJson, and the native code - * must intepret it according to the internal implementation details of that method. + * must interpret it according to the internal implementation details of that method. */ private @NonNull Map json; @@ -815,7 +890,8 @@ ArrayList toList() { public static final class PlatformTileOverlay { /** * The tile overlay data, as JSON. This should only be set from TileOverlay.toJson, and the - * native code must intepret it according to the internal implementation details of that method. + * native code must interpret it according to the internal implementation details of that + * method. */ private @NonNull Map json; @@ -1231,8 +1307,8 @@ ArrayList toList() { public static final class PlatformMapConfiguration { /** * The configuration options, as JSON. This should only be set from _jsonForMapConfiguration, - * and the native code must intepret it according to the internal implementation details of that - * method. + * and the native code must interpret it according to the internal implementation details of + * that method. */ private @NonNull Map json; @@ -1655,32 +1731,34 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { case (byte) 131: return PlatformCircle.fromList((ArrayList) readValue(buffer)); case (byte) 132: - return PlatformClusterManager.fromList((ArrayList) readValue(buffer)); + return PlatformHeatmap.fromList((ArrayList) readValue(buffer)); case (byte) 133: - return PlatformMarker.fromList((ArrayList) readValue(buffer)); + return PlatformClusterManager.fromList((ArrayList) readValue(buffer)); case (byte) 134: - return PlatformPolygon.fromList((ArrayList) readValue(buffer)); + return PlatformMarker.fromList((ArrayList) readValue(buffer)); case (byte) 135: - return PlatformPolyline.fromList((ArrayList) readValue(buffer)); + return PlatformPolygon.fromList((ArrayList) readValue(buffer)); case (byte) 136: - return PlatformTile.fromList((ArrayList) readValue(buffer)); + return PlatformPolyline.fromList((ArrayList) readValue(buffer)); case (byte) 137: - return PlatformTileOverlay.fromList((ArrayList) readValue(buffer)); + return PlatformTile.fromList((ArrayList) readValue(buffer)); case (byte) 138: - return PlatformLatLng.fromList((ArrayList) readValue(buffer)); + return PlatformTileOverlay.fromList((ArrayList) readValue(buffer)); case (byte) 139: - return PlatformLatLngBounds.fromList((ArrayList) readValue(buffer)); + return PlatformLatLng.fromList((ArrayList) readValue(buffer)); case (byte) 140: - return PlatformCluster.fromList((ArrayList) readValue(buffer)); + return PlatformLatLngBounds.fromList((ArrayList) readValue(buffer)); case (byte) 141: - return PlatformMapConfiguration.fromList((ArrayList) readValue(buffer)); + return PlatformCluster.fromList((ArrayList) readValue(buffer)); case (byte) 142: - return PlatformPoint.fromList((ArrayList) readValue(buffer)); + return PlatformMapConfiguration.fromList((ArrayList) readValue(buffer)); case (byte) 143: - return PlatformTileLayer.fromList((ArrayList) readValue(buffer)); + return PlatformPoint.fromList((ArrayList) readValue(buffer)); case (byte) 144: - return PlatformZoomRange.fromList((ArrayList) readValue(buffer)); + return PlatformTileLayer.fromList((ArrayList) readValue(buffer)); case (byte) 145: + return PlatformZoomRange.fromList((ArrayList) readValue(buffer)); + case (byte) 146: Object value = readValue(buffer); return value == null ? null : PlatformRendererType.values()[(int) value]; default: @@ -1699,47 +1777,50 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { } else if (value instanceof PlatformCircle) { stream.write(131); writeValue(stream, ((PlatformCircle) value).toList()); - } else if (value instanceof PlatformClusterManager) { + } else if (value instanceof PlatformHeatmap) { stream.write(132); + writeValue(stream, ((PlatformHeatmap) value).toList()); + } else if (value instanceof PlatformClusterManager) { + stream.write(133); writeValue(stream, ((PlatformClusterManager) value).toList()); } else if (value instanceof PlatformMarker) { - stream.write(133); + stream.write(134); writeValue(stream, ((PlatformMarker) value).toList()); } else if (value instanceof PlatformPolygon) { - stream.write(134); + stream.write(135); writeValue(stream, ((PlatformPolygon) value).toList()); } else if (value instanceof PlatformPolyline) { - stream.write(135); + stream.write(136); writeValue(stream, ((PlatformPolyline) value).toList()); } else if (value instanceof PlatformTile) { - stream.write(136); + stream.write(137); writeValue(stream, ((PlatformTile) value).toList()); } else if (value instanceof PlatformTileOverlay) { - stream.write(137); + stream.write(138); writeValue(stream, ((PlatformTileOverlay) value).toList()); } else if (value instanceof PlatformLatLng) { - stream.write(138); + stream.write(139); writeValue(stream, ((PlatformLatLng) value).toList()); } else if (value instanceof PlatformLatLngBounds) { - stream.write(139); + stream.write(140); writeValue(stream, ((PlatformLatLngBounds) value).toList()); } else if (value instanceof PlatformCluster) { - stream.write(140); + stream.write(141); writeValue(stream, ((PlatformCluster) value).toList()); } else if (value instanceof PlatformMapConfiguration) { - stream.write(141); + stream.write(142); writeValue(stream, ((PlatformMapConfiguration) value).toList()); } else if (value instanceof PlatformPoint) { - stream.write(142); + stream.write(143); writeValue(stream, ((PlatformPoint) value).toList()); } else if (value instanceof PlatformTileLayer) { - stream.write(143); + stream.write(144); writeValue(stream, ((PlatformTileLayer) value).toList()); } else if (value instanceof PlatformZoomRange) { - stream.write(144); + stream.write(145); writeValue(stream, ((PlatformZoomRange) value).toList()); } else if (value instanceof PlatformRendererType) { - stream.write(145); + stream.write(146); writeValue(stream, value == null ? null : ((PlatformRendererType) value).index); } else { super.writeValue(stream, value); @@ -1793,6 +1874,11 @@ void updateCircles( @NonNull List toAdd, @NonNull List toChange, @NonNull List idsToRemove); + /** Updates the set of heatmaps on the map. */ + void updateHeatmaps( + @NonNull List toAdd, + @NonNull List toChange, + @NonNull List idsToRemove); /** Updates the set of custer managers for clusters on the map. */ void updateClusterManagers( @NonNull List toAdd, @NonNull List idsToRemove); @@ -1959,6 +2045,34 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.google_maps_flutter_android.MapsApi.updateHeatmaps" + + messageChannelSuffix, + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + List toAddArg = (List) args.get(0); + List toChangeArg = (List) args.get(1); + List idsToRemoveArg = (List) args.get(2); + try { + api.updateHeatmaps(toAddArg, toChangeArg, idsToRemoveArg); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java index 8e00a67f23b..156b8ce60e8 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java @@ -4,6 +4,15 @@ package io.flutter.plugins.googlemaps; +import static io.flutter.plugins.googlemaps.Convert.HEATMAP_DATA_KEY; +import static io.flutter.plugins.googlemaps.Convert.HEATMAP_GRADIENT_COLORS_KEY; +import static io.flutter.plugins.googlemaps.Convert.HEATMAP_GRADIENT_COLOR_MAP_SIZE_KEY; +import static io.flutter.plugins.googlemaps.Convert.HEATMAP_GRADIENT_KEY; +import static io.flutter.plugins.googlemaps.Convert.HEATMAP_GRADIENT_START_POINTS_KEY; +import static io.flutter.plugins.googlemaps.Convert.HEATMAP_ID_KEY; +import static io.flutter.plugins.googlemaps.Convert.HEATMAP_MAX_INTENSITY_KEY; +import static io.flutter.plugins.googlemaps.Convert.HEATMAP_OPACITY_KEY; +import static io.flutter.plugins.googlemaps.Convert.HEATMAP_RADIUS_KEY; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; @@ -17,9 +26,14 @@ import android.graphics.Paint; import android.os.Build; import android.util.Base64; +import androidx.annotation.NonNull; import com.google.android.gms.maps.model.BitmapDescriptor; import com.google.android.gms.maps.model.LatLng; import com.google.maps.android.clustering.algo.StaticCluster; +import com.google.maps.android.geometry.Point; +import com.google.maps.android.heatmaps.Gradient; +import com.google.maps.android.heatmaps.WeightedLatLng; +import com.google.maps.android.projection.SphericalMercatorProjection; import io.flutter.plugins.googlemaps.Convert.BitmapDescriptorFactoryWrapper; import io.flutter.plugins.googlemaps.Convert.FlutterInjectorWrapper; import java.io.ByteArrayInputStream; @@ -324,6 +338,128 @@ public void GetBitmapFromBytesThrowsErrorIfInvalidImageData() throws Exception { fail("Expected an IllegalArgumentException to be thrown"); } + private static final SphericalMercatorProjection sProjection = new SphericalMercatorProjection(1); + + @Test() + public void ConvertToWeightedLatLngReturnsCorrectData() { + final double intensity = 3.3; + final Object data = List.of(List.of(1.1, 2.2), intensity); + final Point point = sProjection.toPoint(new LatLng(1.1, 2.2)); + + final WeightedLatLng result = Convert.toWeightedLatLng(data); + + Assert.assertEquals(point.x, result.getPoint().x, 0); + Assert.assertEquals(point.y, result.getPoint().y, 0); + Assert.assertEquals(intensity, result.getIntensity(), 0); + } + + @Test() + public void ConvertToWeightedDataReturnsCorrectData() { + final double intensity = 3.3; + final List data = List.of(List.of(List.of(1.1, 2.2), intensity)); + final Point point = sProjection.toPoint(new LatLng(1.1, 2.2)); + + final List result = Convert.toWeightedData(data); + + Assert.assertEquals(1, result.size()); + Assert.assertEquals(point.x, result.get(0).getPoint().x, 0); + Assert.assertEquals(point.y, result.get(0).getPoint().y, 0); + Assert.assertEquals(intensity, result.get(0).getIntensity(), 0); + } + + @Test() + public void ConvertToGradientReturnsCorrectData() { + final int color1 = 0; + final int color2 = 1; + final int color3 = 2; + final List colorData = List.of(color1, color2, color3); + final double startPoint1 = 0.0; + final double startPoint2 = 1.0; + final double startPoint3 = 2.0; + List startPointData = List.of(startPoint1, startPoint2, startPoint3); + final int colorMapSize = 3; + final Map data = + Map.of( + HEATMAP_GRADIENT_COLORS_KEY, colorData, + HEATMAP_GRADIENT_START_POINTS_KEY, startPointData, + HEATMAP_GRADIENT_COLOR_MAP_SIZE_KEY, colorMapSize); + + final Gradient result = Convert.toGradient(data); + + Assert.assertEquals(3, result.mColors.length); + Assert.assertEquals(color1, result.mColors[0]); + Assert.assertEquals(color2, result.mColors[1]); + Assert.assertEquals(color3, result.mColors[2]); + Assert.assertEquals(3, result.mStartPoints.length); + Assert.assertEquals(startPoint1, result.mStartPoints[0], 0); + Assert.assertEquals(startPoint2, result.mStartPoints[1], 0); + Assert.assertEquals(startPoint3, result.mStartPoints[2], 0); + Assert.assertEquals(colorMapSize, result.mColorMapSize); + } + + @Test() + public void ConvertInterpretHeatmapOptionsReturnsCorrectData() { + final double intensity = 3.3; + final List dataData = List.of(List.of(List.of(1.1, 2.2), intensity)); + final Point point = sProjection.toPoint(new LatLng(1.1, 2.2)); + + final int color1 = 0; + final int color2 = 1; + final int color3 = 2; + final List colorData = List.of(color1, color2, color3); + final double startPoint1 = 0.0; + final double startPoint2 = 1.0; + final double startPoint3 = 2.0; + List startPointData = List.of(startPoint1, startPoint2, startPoint3); + final int colorMapSize = 3; + final Map gradientData = + Map.of( + HEATMAP_GRADIENT_COLORS_KEY, colorData, + HEATMAP_GRADIENT_START_POINTS_KEY, startPointData, + HEATMAP_GRADIENT_COLOR_MAP_SIZE_KEY, colorMapSize); + + final double maxIntensity = 4.4; + final double opacity = 5.5; + final int radius = 6; + final String idData = "heatmap_1"; + + final Map data = + Map.of( + HEATMAP_DATA_KEY, + dataData, + HEATMAP_GRADIENT_KEY, + gradientData, + HEATMAP_MAX_INTENSITY_KEY, + maxIntensity, + HEATMAP_OPACITY_KEY, + opacity, + HEATMAP_RADIUS_KEY, + radius, + HEATMAP_ID_KEY, + idData); + + final MockHeatmapBuilder builder = new MockHeatmapBuilder(); + final String id = Convert.interpretHeatmapOptions(data, builder); + + Assert.assertEquals(1, builder.getWeightedData().size()); + Assert.assertEquals(point.x, builder.getWeightedData().get(0).getPoint().x, 0); + Assert.assertEquals(point.y, builder.getWeightedData().get(0).getPoint().y, 0); + Assert.assertEquals(intensity, builder.getWeightedData().get(0).getIntensity(), 0); + Assert.assertEquals(3, builder.getGradient().mColors.length); + Assert.assertEquals(color1, builder.getGradient().mColors[0]); + Assert.assertEquals(color2, builder.getGradient().mColors[1]); + Assert.assertEquals(color3, builder.getGradient().mColors[2]); + Assert.assertEquals(3, builder.getGradient().mStartPoints.length); + Assert.assertEquals(startPoint1, builder.getGradient().mStartPoints[0], 0); + Assert.assertEquals(startPoint2, builder.getGradient().mStartPoints[1], 0); + Assert.assertEquals(startPoint3, builder.getGradient().mStartPoints[2], 0); + Assert.assertEquals(colorMapSize, builder.getGradient().mColorMapSize); + Assert.assertEquals(maxIntensity, builder.getMaxIntensity(), 0); + Assert.assertEquals(opacity, builder.getOpacity(), 0); + Assert.assertEquals(radius, builder.getRadius()); + Assert.assertEquals(idData, id); + } + private InputStream buildImageInputStream() { Bitmap fakeBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); @@ -356,3 +492,56 @@ private String generateBase64Image() { return base64Image; } } + +class MockHeatmapBuilder implements HeatmapOptionsSink { + private List weightedData; + private Gradient gradient; + private double maxIntensity; + private double opacity; + private int radius; + + public List getWeightedData() { + return weightedData; + } + + public Gradient getGradient() { + return gradient; + } + + public double getMaxIntensity() { + return maxIntensity; + } + + public double getOpacity() { + return opacity; + } + + public int getRadius() { + return radius; + } + + @Override + public void setWeightedData(@NonNull List weightedData) { + this.weightedData = weightedData; + } + + @Override + public void setGradient(@NonNull Gradient gradient) { + this.gradient = gradient; + } + + @Override + public void setMaxIntensity(double maxIntensity) { + this.maxIntensity = maxIntensity; + } + + @Override + public void setOpacity(double opacity) { + this.opacity = opacity; + } + + @Override + public void setRadius(int radius) { + this.radius = radius; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java index 865920ee631..2295ec1a757 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java @@ -52,6 +52,7 @@ public class GoogleMapControllerTest { @Mock PolygonsController mockPolygonsController; @Mock PolylinesController mockPolylinesController; @Mock CirclesController mockCirclesController; + @Mock HeatmapsController mockHeatmapsController; @Mock TileOverlaysController mockTileOverlaysController; @Before @@ -85,6 +86,7 @@ public GoogleMapController getGoogleMapControllerWithMockedDependencies() { mockPolygonsController, mockPolylinesController, mockCirclesController, + mockHeatmapsController, mockTileOverlaysController); googleMapController.init(); return googleMapController; @@ -222,4 +224,31 @@ public void OnClusterItemClickCallsMarkersController() { googleMapController.onClusterItemClick(markerBuilder); verify(mockMarkersController, times(1)).onMarkerTap(markerBuilder.markerId()); } + + @Test + public void SetInitialHeatmaps() { + GoogleMapController googleMapController = getGoogleMapControllerWithMockedDependencies(); + + List initialHeatmaps = List.of(Map.of("heatmapId", "hm_1")); + googleMapController.setInitialHeatmaps(initialHeatmaps); + googleMapController.onMapReady(mockGoogleMap); + + // Verify if the HeatmapsController.addHeatmaps method is called with initial heatmaps. + verify(mockHeatmapsController, times(1)).addJsonHeatmaps(initialHeatmaps); + } + + @Test + public void UpdateHeatmaps() { + GoogleMapController googleMapController = getGoogleMapControllerWithMockedDependencies(); + + final List toAdd = List.of(new Messages.PlatformHeatmap()); + final List toChange = List.of(new Messages.PlatformHeatmap()); + final List idsToRemove = List.of("hm_1"); + + googleMapController.updateHeatmaps(toAdd, toChange, idsToRemove); + + verify(mockHeatmapsController, times(1)).addHeatmaps(toAdd); + verify(mockHeatmapsController, times(1)).changeHeatmaps(toChange); + verify(mockHeatmapsController, times(1)).removeHeatmaps(idsToRemove); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/HeatmapsControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/HeatmapsControllerTest.java new file mode 100644 index 00000000000..e087ba38683 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/HeatmapsControllerTest.java @@ -0,0 +1,108 @@ +// 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 static io.flutter.plugins.googlemaps.Convert.HEATMAP_DATA_KEY; +import static io.flutter.plugins.googlemaps.Convert.HEATMAP_ID_KEY; +import static io.flutter.plugins.googlemaps.Convert.HEATMAP_OPACITY_KEY; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import android.os.Build; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.TileOverlay; +import com.google.android.gms.maps.model.TileOverlayOptions; +import com.google.maps.android.heatmaps.HeatmapTileProvider; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Build.VERSION_CODES.P) +public class HeatmapsControllerTest { + private HeatmapsController controller; + private GoogleMap googleMap; + + @Before + public void setUp() { + controller = spy(new HeatmapsController()); + googleMap = mock(GoogleMap.class); + controller.setGoogleMap(googleMap); + } + + @Test(expected = IllegalArgumentException.class) + public void controller_AddHeatmapThrowsErrorIfHeatmapIdIsNull() { + final Map heatmapOptions = new HashMap<>(); + + final List heatmaps = Collections.singletonList(heatmapOptions); + try { + controller.addJsonHeatmaps(heatmaps); + } catch (IllegalArgumentException e) { + assertEquals("heatmapId was null", e.getMessage()); + throw e; + } + } + + @Test + public void controller_AddChangeAndRemoveHeatmap() { + final TileOverlay tileOverlay = mock(TileOverlay.class); + final HeatmapTileProvider heatmap = mock(HeatmapTileProvider.class); + + final String googleHeatmapId = "abc123"; + final Object heatmapData = + Collections.singletonList(Arrays.asList(Arrays.asList(1.1, 2.2), 3.3)); + + when(googleMap.addTileOverlay(any(TileOverlayOptions.class))).thenReturn(tileOverlay); + doReturn(heatmap).when(controller).buildHeatmap(any(HeatmapBuilder.class)); + + final Map heatmapOptions1 = new HashMap<>(); + heatmapOptions1.put(HEATMAP_ID_KEY, googleHeatmapId); + heatmapOptions1.put(HEATMAP_DATA_KEY, heatmapData); + + final List heatmaps = Collections.singletonList(heatmapOptions1); + controller.addJsonHeatmaps(heatmaps); + + Mockito.verify(googleMap, times(1)) + .addTileOverlay( + Mockito.argThat(argument -> argument.getTileProvider() instanceof HeatmapTileProvider)); + + final float opacity = 0.1f; + final Map heatmapOptions2 = new HashMap<>(); + heatmapOptions2.put(HEATMAP_ID_KEY, googleHeatmapId); + heatmapOptions2.put(HEATMAP_DATA_KEY, heatmapData); + heatmapOptions2.put(HEATMAP_OPACITY_KEY, opacity); + + final List heatmapUpdates = + Collections.singletonList(heatmapOptions2) + .stream() + .map( + json -> { + final Messages.PlatformHeatmap platformHeatmap = new Messages.PlatformHeatmap(); + platformHeatmap.setJson(json); + return platformHeatmap; + }) + .toList(); + + controller.changeHeatmaps(heatmapUpdates); + Mockito.verify(heatmap, times(1)).setOpacity(opacity); + + controller.removeHeatmaps(Collections.singletonList(googleHeatmapId)); + + Mockito.verify(tileOverlay, times(1)).remove(); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml index ddac3d7b231..9f3931140f6 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - google_maps_flutter_platform_interface: ^2.7.0 + google_maps_flutter_platform_interface: ^2.9.0 dev_dependencies: build_runner: ^2.1.10 diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart index abe361566b6..76b8a8ce75d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart @@ -75,6 +75,9 @@ class GoogleMapsInspectorAndroid extends GoogleMapsInspectorPlatform { ); } + @override + bool supportsGettingHeatmapInfo() => false; + @override Future isCompassEnabled({required int mapId}) async { return _inspectorProvider(mapId)!.isCompassEnabled(); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart index 30bc4b7c9cd..214acc41ed2 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart @@ -14,6 +14,7 @@ import 'package:stream_transform/stream_transform.dart'; import 'google_map_inspector_android.dart'; import 'messages.g.dart'; +import 'serialization.dart'; import 'utils/cluster_manager_utils.dart'; // TODO(stuartmorgan): Remove the dependency on platform interface toJson @@ -293,6 +294,20 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { ); } + @override + Future updateHeatmaps( + HeatmapUpdates heatmapUpdates, { + required int mapId, + }) { + return _hostApi(mapId).updateHeatmaps( + heatmapUpdates.heatmapsToAdd.map(_platformHeatmapFromHeatmap).toList(), + heatmapUpdates.heatmapsToChange.map(_platformHeatmapFromHeatmap).toList(), + heatmapUpdates.heatmapIdsToRemove + .map((HeatmapId id) => id.value) + .toList(), + ); + } + @override Future updateTileOverlays({ required Set newTileOverlays, @@ -502,6 +517,7 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { 'polygonsToAdd': serializePolygonSet(mapObjects.polygons), 'polylinesToAdd': serializePolylineSet(mapObjects.polylines), 'circlesToAdd': serializeCircleSet(mapObjects.circles), + 'heatmapsToAdd': mapObjects.heatmaps.map(serializeHeatmap).toList(), 'tileOverlaysToAdd': serializeTileOverlaySet(mapObjects.tileOverlays), 'clusterManagersToAdd': serializeClusterManagerSet(mapObjects.clusterManagers), @@ -675,6 +691,13 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { return PlatformCircle(json: circle.toJson() as Map); } + static PlatformHeatmap _platformHeatmapFromHeatmap(Heatmap heatmap) { + // This cast is not ideal, but the Java code already assumes this format. + // See the TODOs at the top of this file and on the 'json' field in + // messages.dart. + return PlatformHeatmap(json: heatmap.toJson() as Map); + } + static PlatformClusterManager _platformClusterManagerFromClusterManager( ClusterManager clusterManager) { return PlatformClusterManager( diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart index c54b01c196e..676b8f48b86 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart @@ -78,7 +78,7 @@ class PlatformCameraUpdate { }); /// The update data, as JSON. This should only be set from - /// CameraUpdate.toJson, and the native code must intepret it according to the + /// CameraUpdate.toJson, and the native code must interpret it according to the /// internal implementation details of the CameraUpdate class. Object json; @@ -103,7 +103,7 @@ class PlatformCircle { }); /// The circle data, as JSON. This should only be set from - /// Circle.toJson, and the native code must intepret it according to the + /// Circle.toJson, and the native code must interpret it according to the /// internal implementation details of that method. Map json; @@ -121,6 +121,31 @@ class PlatformCircle { } } +/// Pigeon equivalent of the Heatmap class. +class PlatformHeatmap { + PlatformHeatmap({ + required this.json, + }); + + /// The heatmap data, as JSON. This should only be set from + /// Heatmap.toJson, and the native code must interpret it according to the + /// internal implementation details of that method. + Map json; + + Object encode() { + return [ + json, + ]; + } + + static PlatformHeatmap decode(Object result) { + result as List; + return PlatformHeatmap( + json: (result[0] as Map?)!.cast(), + ); + } +} + /// Pigeon equivalent of the ClusterManager class. class PlatformClusterManager { PlatformClusterManager({ @@ -150,7 +175,7 @@ class PlatformMarker { }); /// The marker data, as JSON. This should only be set from - /// Marker.toJson, and the native code must intepret it according to the + /// Marker.toJson, and the native code must interpret it according to the /// internal implementation details of that method. Map json; @@ -175,7 +200,7 @@ class PlatformPolygon { }); /// The polygon data, as JSON. This should only be set from - /// Polygon.toJson, and the native code must intepret it according to the + /// Polygon.toJson, and the native code must interpret it according to the /// internal implementation details of that method. Map json; @@ -200,7 +225,7 @@ class PlatformPolyline { }); /// The polyline data, as JSON. This should only be set from - /// Polyline.toJson, and the native code must intepret it according to the + /// Polyline.toJson, and the native code must interpret it according to the /// internal implementation details of that method. Map json; @@ -257,7 +282,7 @@ class PlatformTileOverlay { }); /// The tile overlay data, as JSON. This should only be set from - /// TileOverlay.toJson, and the native code must intepret it according to the + /// TileOverlay.toJson, and the native code must interpret it according to the /// internal implementation details of that method. Map json; @@ -373,7 +398,7 @@ class PlatformMapConfiguration { }); /// The configuration options, as JSON. This should only be set from - /// _jsonForMapConfiguration, and the native code must intepret it according + /// _jsonForMapConfiguration, and the native code must interpret it according /// to the internal implementation details of that method. Map json; @@ -495,47 +520,50 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is PlatformCircle) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is PlatformClusterManager) { + } else if (value is PlatformHeatmap) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is PlatformMarker) { + } else if (value is PlatformClusterManager) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is PlatformPolygon) { + } else if (value is PlatformMarker) { buffer.putUint8(134); writeValue(buffer, value.encode()); - } else if (value is PlatformPolyline) { + } else if (value is PlatformPolygon) { buffer.putUint8(135); writeValue(buffer, value.encode()); - } else if (value is PlatformTile) { + } else if (value is PlatformPolyline) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is PlatformTileOverlay) { + } else if (value is PlatformTile) { buffer.putUint8(137); writeValue(buffer, value.encode()); - } else if (value is PlatformLatLng) { + } else if (value is PlatformTileOverlay) { buffer.putUint8(138); writeValue(buffer, value.encode()); - } else if (value is PlatformLatLngBounds) { + } else if (value is PlatformLatLng) { buffer.putUint8(139); writeValue(buffer, value.encode()); - } else if (value is PlatformCluster) { + } else if (value is PlatformLatLngBounds) { buffer.putUint8(140); writeValue(buffer, value.encode()); - } else if (value is PlatformMapConfiguration) { + } else if (value is PlatformCluster) { buffer.putUint8(141); writeValue(buffer, value.encode()); - } else if (value is PlatformPoint) { + } else if (value is PlatformMapConfiguration) { buffer.putUint8(142); writeValue(buffer, value.encode()); - } else if (value is PlatformTileLayer) { + } else if (value is PlatformPoint) { buffer.putUint8(143); writeValue(buffer, value.encode()); - } else if (value is PlatformZoomRange) { + } else if (value is PlatformTileLayer) { buffer.putUint8(144); writeValue(buffer, value.encode()); - } else if (value is PlatformRendererType) { + } else if (value is PlatformZoomRange) { buffer.putUint8(145); + writeValue(buffer, value.encode()); + } else if (value is PlatformRendererType) { + buffer.putUint8(146); writeValue(buffer, value.index); } else { super.writeValue(buffer, value); @@ -552,32 +580,34 @@ class _PigeonCodec extends StandardMessageCodec { case 131: return PlatformCircle.decode(readValue(buffer)!); case 132: - return PlatformClusterManager.decode(readValue(buffer)!); + return PlatformHeatmap.decode(readValue(buffer)!); case 133: - return PlatformMarker.decode(readValue(buffer)!); + return PlatformClusterManager.decode(readValue(buffer)!); case 134: - return PlatformPolygon.decode(readValue(buffer)!); + return PlatformMarker.decode(readValue(buffer)!); case 135: - return PlatformPolyline.decode(readValue(buffer)!); + return PlatformPolygon.decode(readValue(buffer)!); case 136: - return PlatformTile.decode(readValue(buffer)!); + return PlatformPolyline.decode(readValue(buffer)!); case 137: - return PlatformTileOverlay.decode(readValue(buffer)!); + return PlatformTile.decode(readValue(buffer)!); case 138: - return PlatformLatLng.decode(readValue(buffer)!); + return PlatformTileOverlay.decode(readValue(buffer)!); case 139: - return PlatformLatLngBounds.decode(readValue(buffer)!); + return PlatformLatLng.decode(readValue(buffer)!); case 140: - return PlatformCluster.decode(readValue(buffer)!); + return PlatformLatLngBounds.decode(readValue(buffer)!); case 141: - return PlatformMapConfiguration.decode(readValue(buffer)!); + return PlatformCluster.decode(readValue(buffer)!); case 142: - return PlatformPoint.decode(readValue(buffer)!); + return PlatformMapConfiguration.decode(readValue(buffer)!); case 143: - return PlatformTileLayer.decode(readValue(buffer)!); + return PlatformPoint.decode(readValue(buffer)!); case 144: - return PlatformZoomRange.decode(readValue(buffer)!); + return PlatformTileLayer.decode(readValue(buffer)!); case 145: + return PlatformZoomRange.decode(readValue(buffer)!); + case 146: final int? value = readValue(buffer) as int?; return value == null ? null : PlatformRendererType.values[value]; default: @@ -683,6 +713,32 @@ class MapsApi { } } + /// Updates the set of heatmaps on the map. + Future updateHeatmaps(List toAdd, + List toChange, List idsToRemove) async { + final String __pigeon_channelName = + 'dev.flutter.pigeon.google_maps_flutter_android.MapsApi.updateHeatmaps$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = await __pigeon_channel + .send([toAdd, toChange, idsToRemove]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else { + return; + } + } + /// Updates the set of custer managers for clusters on the map. Future updateClusterManagers( List toAdd, List idsToRemove) async { diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/serialization.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/serialization.dart new file mode 100644 index 00000000000..8858a6e481b --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/serialization.dart @@ -0,0 +1,121 @@ +// 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. + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +// These constants must match the corresponding constants in Convert.java +const String _heatmapIdKey = 'heatmapId'; +const String _heatmapDataKey = 'data'; +const String _heatmapGradientKey = 'gradient'; +const String _heatmapMaxIntensityKey = 'maxIntensity'; +const String _heatmapOpacityKey = 'opacity'; +const String _heatmapRadiusKey = 'radius'; +const String _heatmapGradientColorsKey = 'colors'; +const String _heatmapGradientStartPointsKey = 'startPoints'; +const String _heatmapGradientColorMapSizeKey = 'colorMapSize'; + +void _addIfNonNull(Map map, String fieldName, Object? value) { + if (value != null) { + map[fieldName] = value; + } +} + +/// Serialize [Heatmap] +Object serializeHeatmap(Heatmap heatmap) { + final Map json = {}; + + _addIfNonNull(json, _heatmapIdKey, heatmap.heatmapId.value); + _addIfNonNull( + json, + _heatmapDataKey, + heatmap.data.map(serializeWeightedLatLng).toList(), + ); + + final HeatmapGradient? gradient = heatmap.gradient; + if (gradient != null) { + _addIfNonNull( + json, _heatmapGradientKey, serializeHeatmapGradient(gradient)); + } + _addIfNonNull(json, _heatmapMaxIntensityKey, heatmap.maxIntensity); + _addIfNonNull(json, _heatmapOpacityKey, heatmap.opacity); + _addIfNonNull(json, _heatmapRadiusKey, heatmap.radius.radius); + + return json; +} + +/// Serialize [WeightedLatLng] +Object serializeWeightedLatLng(WeightedLatLng wll) { + return [serializeLatLng(wll.point), wll.weight]; +} + +/// Deserialize [WeightedLatLng] +WeightedLatLng? deserializeWeightedLatLng(Object? json) { + if (json == null) { + return null; + } + assert(json is List && json.length == 2); + final List list = json as List; + final LatLng latLng = deserializeLatLng(list[0])!; + return WeightedLatLng(latLng, weight: list[1] as double); +} + +/// Serialize [LatLng] +Object serializeLatLng(LatLng latLng) { + return [latLng.latitude, latLng.longitude]; +} + +/// Deserialize [LatLng] +LatLng? deserializeLatLng(Object? json) { + if (json == null) { + return null; + } + assert(json is List && json.length == 2); + final List list = json as List; + return LatLng(list[0]! as double, list[1]! as double); +} + +/// Serialize [HeatmapGradient] +Object serializeHeatmapGradient(HeatmapGradient gradient) { + final Map json = {}; + + _addIfNonNull( + json, + _heatmapGradientColorsKey, + gradient.colors.map((HeatmapGradientColor e) => e.color.value).toList(), + ); + _addIfNonNull( + json, + _heatmapGradientStartPointsKey, + gradient.colors.map((HeatmapGradientColor e) => e.startPoint).toList(), + ); + _addIfNonNull(json, _heatmapGradientColorMapSizeKey, gradient.colorMapSize); + + return json; +} + +/// Deserialize [HeatmapGradient] +HeatmapGradient? deserializeHeatmapGradient(Object? json) { + if (json == null) { + return null; + } + assert(json is Map); + final Map map = (json as Map).cast(); + final List colors = (map[_heatmapGradientColorsKey]! as List) + .whereType() + .map((int e) => Color(e)) + .toList(); + final List startPoints = + (map[_heatmapGradientStartPointsKey]! as List) + .whereType() + .toList(); + final List gradientColors = []; + for (int i = 0; i < colors.length; i++) { + gradientColors.add(HeatmapGradientColor(colors[i], startPoints[i])); + } + return HeatmapGradient( + gradientColors, + colorMapSize: map[_heatmapGradientColorMapSizeKey] as int? ?? 256, + ); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart b/packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart index 3bd8cbf36d3..7cf0afbea28 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart @@ -34,7 +34,7 @@ class PlatformCameraUpdate { PlatformCameraUpdate(this.json); /// The update data, as JSON. This should only be set from - /// CameraUpdate.toJson, and the native code must intepret it according to the + /// CameraUpdate.toJson, and the native code must interpret it according to the /// internal implementation details of the CameraUpdate class. // TODO(stuartmorgan): Update the google_maps_platform_interface CameraUpdate // class to provide a structured representation of an update. Currently it @@ -49,7 +49,19 @@ class PlatformCircle { PlatformCircle(this.json); /// The circle data, as JSON. This should only be set from - /// Circle.toJson, and the native code must intepret it according to the + /// Circle.toJson, and the native code must interpret it according to the + /// internal implementation details of that method. + // TODO(stuartmorgan): Replace this with structured data. This exists only to + // allow incremental migration to Pigeon. + final Map json; +} + +/// Pigeon equivalent of the Heatmap class. +class PlatformHeatmap { + PlatformHeatmap(this.json); + + /// The heatmap data, as JSON. This should only be set from + /// Heatmap.toJson, and the native code must interpret it according to the /// internal implementation details of that method. // TODO(stuartmorgan): Replace this with structured data. This exists only to // allow incremental migration to Pigeon. @@ -68,7 +80,7 @@ class PlatformMarker { PlatformMarker(this.json); /// The marker data, as JSON. This should only be set from - /// Marker.toJson, and the native code must intepret it according to the + /// Marker.toJson, and the native code must interpret it according to the /// internal implementation details of that method. // TODO(stuartmorgan): Replace this with structured data. This exists only to // allow incremental migration to Pigeon. @@ -80,7 +92,7 @@ class PlatformPolygon { PlatformPolygon(this.json); /// The polygon data, as JSON. This should only be set from - /// Polygon.toJson, and the native code must intepret it according to the + /// Polygon.toJson, and the native code must interpret it according to the /// internal implementation details of that method. // TODO(stuartmorgan): Replace this with structured data. This exists only to // allow incremental migration to Pigeon. @@ -92,7 +104,7 @@ class PlatformPolyline { PlatformPolyline(this.json); /// The polyline data, as JSON. This should only be set from - /// Polyline.toJson, and the native code must intepret it according to the + /// Polyline.toJson, and the native code must interpret it according to the /// internal implementation details of that method. // TODO(stuartmorgan): Replace this with structured data. This exists only to // allow incremental migration to Pigeon. @@ -113,7 +125,7 @@ class PlatformTileOverlay { PlatformTileOverlay(this.json); /// The tile overlay data, as JSON. This should only be set from - /// TileOverlay.toJson, and the native code must intepret it according to the + /// TileOverlay.toJson, and the native code must interpret it according to the /// internal implementation details of that method. // TODO(stuartmorgan): Replace this with structured data. This exists only to // allow incremental migration to Pigeon. @@ -159,7 +171,7 @@ class PlatformMapConfiguration { PlatformMapConfiguration({required this.json}); /// The configuration options, as JSON. This should only be set from - /// _jsonForMapConfiguration, and the native code must intepret it according + /// _jsonForMapConfiguration, and the native code must interpret it according /// to the internal implementation details of that method. // TODO(stuartmorgan): Replace this with structured data. This exists only to // allow incremental migration to Pigeon. @@ -219,6 +231,13 @@ abstract class MapsApi { void updateCircles(List toAdd, List toChange, List idsToRemove); + /// Updates the set of heatmaps on the map. + // TODO(stuartmorgan): Make the generic type non-nullable once supported. + // https://github.com/flutter/flutter/issues/97848 + // The consuming code treats the entries as non-nullable. + void updateHeatmaps(List toAdd, + List toChange, List idsToRemove); + /// Updates the set of custer managers for clusters on the map. // TODO(stuartmorgan): Make the generic type non-nullable once supported. // https://github.com/flutter/flutter/issues/97848 diff --git a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml index f4924aef5f4..82068018bc7 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_android description: Android implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.12.2 +version: 2.13.0 environment: sdk: ^3.4.0 @@ -21,7 +21,7 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 - google_maps_flutter_platform_interface: ^2.7.0 + google_maps_flutter_platform_interface: ^2.9.0 stream_transform: ^2.0.0 dev_dependencies: diff --git a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.mocks.dart index c49d6268926..e0fbdd57421 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.mocks.dart @@ -99,6 +99,25 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { returnValueForMissingStub: _i3.Future.value(), ) as _i3.Future); + @override + _i3.Future updateHeatmaps( + List<_i2.PlatformHeatmap?>? toAdd, + List<_i2.PlatformHeatmap?>? toChange, + List? idsToRemove, + ) => + (super.noSuchMethod( + Invocation.method( + #updateHeatmaps, + [ + toAdd, + toChange, + idsToRemove, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override _i3.Future updateClusterManagers( List<_i2.PlatformClusterManager?>? toAdd,