From 6875ceedf9cd40fabce2af1f5bbb929af5008dfd Mon Sep 17 00:00:00 2001 From: Rexios Date: Mon, 5 Aug 2024 11:45:39 -0400 Subject: [PATCH 1/6] Platform interface changes to support heatmaps --- .../CHANGELOG.md | 4 + .../method_channel_google_maps_flutter.dart | 16 + .../lib/src/method_channel/serialization.dart | 159 +++++++ .../google_maps_flutter_platform.dart | 16 + .../google_maps_inspector_platform.dart | 16 + .../lib/src/types/heatmap.dart | 372 ++++++++++++++++ .../lib/src/types/heatmap_updates.dart | 26 ++ .../lib/src/types/map_objects.dart | 2 + .../lib/src/types/types.dart | 3 + .../lib/src/types/utils/heatmap.dart | 19 + .../pubspec.yaml | 2 +- .../google_maps_flutter_platform_test.dart | 1 + .../test/types/heatmap_test.dart | 402 ++++++++++++++++++ 13 files changed, 1037 insertions(+), 1 deletion(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/serialization.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/heatmap.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/heatmap_updates.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/heatmap.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/heatmap_test.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md index a137b29112c7..62ba5277b898 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.9.0 + +* Adds support for heatmap layers. + ## 2.8.0 * Deprecates `BitmapDescriptor.fromAssetImage` in favor of `BitmapDescriptor.asset` and `AssetMapBitmap.create`. diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart index 2f162bf50462..53cea1a363d1 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart @@ -14,6 +14,7 @@ import 'package:stream_transform/stream_transform.dart'; import '../../google_maps_flutter_platform_interface.dart'; import '../types/tile_overlay_updates.dart'; import '../types/utils/map_configuration_serialization.dart'; +import 'serialization.dart'; /// Error thrown when an unknown map ID is provided to a method channel API. class UnknownMapIDError extends Error { @@ -363,6 +364,17 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { ); } + @override + Future updateHeatmaps( + HeatmapUpdates heatmapUpdates, { + required int mapId, + }) { + return channel(mapId).invokeMethod( + 'heatmaps#update', + serializeMapsObjectUpdates(heatmapUpdates, serializeHeatmap), + ); + } + @override Future updateTileOverlays({ required Set newTileOverlays, @@ -542,6 +554,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { 'polygonsToAdd': serializePolygonSet(mapObjects.polygons), 'polylinesToAdd': serializePolylineSet(mapObjects.polylines), 'circlesToAdd': serializeCircleSet(mapObjects.circles), + 'heatmapsToAdd': serializeHeatmapSet(mapObjects.heatmaps), 'tileOverlaysToAdd': serializeTileOverlaySet(mapObjects.tileOverlays), }; @@ -630,6 +643,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, + Set heatmaps = const {}, Set tileOverlays = const {}, Set clusterManagers = const {}, Set>? gestureRecognizers, @@ -661,6 +675,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, + Set heatmaps = const {}, Set tileOverlays = const {}, Set clusterManagers = const {}, Set>? gestureRecognizers, @@ -675,6 +690,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { polygons: polygons, polylines: polylines, circles: circles, + heatmaps: heatmaps, tileOverlays: tileOverlays, clusterManagers: clusterManagers, gestureRecognizers: gestureRecognizers, diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/serialization.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/serialization.dart new file mode 100644 index 000000000000..2a82a05565f9 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/serialization.dart @@ -0,0 +1,159 @@ +// 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 '../../google_maps_flutter_platform_interface.dart'; + +String _objectsToAddKey(String name) => '${name}sToAdd'; +String _objectsToChangeKey(String name) => '${name}sToChange'; +String _objectIdsToRemoveKey(String name) => '${name}IdsToRemove'; +const String _heatmapIdKey = 'heatmapId'; +const String _heatmapDataKey = 'data'; +const String _heatmapDissipatingKey = 'dissipating'; +const String _heatmapGradientKey = 'gradient'; +const String _heatmapMaxIntensityKey = 'maxIntensity'; +const String _heatmapOpacityKey = 'opacity'; +const String _heatmapRadiusKey = 'radius'; +const String _heatmapMinimumZoomIntensityKey = 'minimumZoomIntensity'; +const String _heatmapMaximumZoomIntensityKey = 'maximumZoomIntensity'; +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 [MapsObjectUpdates] +Object serializeMapsObjectUpdates>( + MapsObjectUpdates updates, + Object Function(T) serialize, +) { + final Map json = {}; + + _addIfNonNull( + json, + _objectsToAddKey(updates.objectName), + updates.objectsToAdd.map(serialize).toList(), + ); + _addIfNonNull( + json, + _objectsToChangeKey(updates.objectName), + updates.objectsToChange.map(serialize).toList(), + ); + _addIfNonNull( + json, + _objectIdsToRemoveKey(updates.objectName), + updates.objectIdsToRemove + .map((MapsObjectId m) => m.value) + .toList(), + ); + + return json; +} + +/// Serialize [Heatmap] +Object serializeHeatmap(Heatmap heatmap) { + final Map json = {}; + + _addIfNonNull(json, _heatmapIdKey, heatmap.heatmapId.value); + _addIfNonNull( + json, + _heatmapDataKey, + heatmap.data.map(serializeWeightedLatLng).toList(), + ); + _addIfNonNull(json, _heatmapDissipatingKey, heatmap.dissipating); + + 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); + _addIfNonNull( + json, _heatmapMinimumZoomIntensityKey, heatmap.minimumZoomIntensity); + _addIfNonNull( + json, _heatmapMaximumZoomIntensityKey, heatmap.maximumZoomIntensity); + + 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_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart index 648c6a162d68..e38912a7a585 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart @@ -129,6 +129,19 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { throw UnimplementedError('updateCircles() has not been implemented.'); } + /// Updates heatmap configuration. + /// + /// Change listeners are notified once the update has been made on the + /// platform side. + /// + /// The returned [Future] completes after listeners have been notified. + Future updateHeatmaps( + HeatmapUpdates heatmapUpdates, { + required int mapId, + }) { + throw UnimplementedError('updateHeatmaps() has not been implemented.'); + } + /// Updates tile overlay configuration. /// /// Change listeners are notified once the update has been made on the @@ -397,6 +410,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, + Set heatmaps = const {}, Set tileOverlays = const {}, Set>? gestureRecognizers = const >{}, @@ -427,6 +441,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, + Set heatmaps = const {}, Set tileOverlays = const {}, Map mapOptions = const {}, }) { @@ -438,6 +453,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { polygons: polygons, polylines: polylines, circles: circles, + heatmaps: heatmaps, tileOverlays: tileOverlays, gestureRecognizers: gestureRecognizers, mapOptions: mapOptions, diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_inspector_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_inspector_platform.dart index 461d38a61616..8bf6f6f89baf 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_inspector_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_inspector_platform.dart @@ -116,6 +116,22 @@ abstract class GoogleMapsInspectorPlatform extends PlatformInterface { throw UnimplementedError('getTileOverlayInfo() has not been implemented.'); } + /// If the platform supports getting information about heatmaps. + bool supportsGettingHeatmapInfo() { + throw UnimplementedError( + 'supportsGettingHeatmapInfo() has not been implemented.', + ); + } + + /// Returns information about the heatmap with the given ID. + /// + /// The returned object will be synthesized from platform data, so will not + /// be the same Dart object as the original [Heatmap] provided to the + /// platform interface with that ID, and not all fields will be populated. + Future getHeatmapInfo(HeatmapId heatmapId, {required int mapId}) { + throw UnimplementedError('getHeatmapInfo() has not been implemented.'); + } + /// Returns current clusters from [ClusterManager]. Future> getClusters( {required int mapId, required ClusterManagerId clusterManagerId}) { diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/heatmap.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/heatmap.dart new file mode 100644 index 000000000000..41b4dcb2abc3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/heatmap.dart @@ -0,0 +1,372 @@ +// 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/foundation.dart' + show immutable, listEquals, objectRuntimeType; +import 'package:flutter/material.dart' show Color; + +import 'types.dart'; + +/// Uniquely identifies a [Heatmap] among [GoogleMap] heatmaps. +/// +/// This does not have to be globally unique, only unique among the list. +@immutable +class HeatmapId extends MapsObjectId { + /// Creates an immutable identifier for a [Heatmap]. + const HeatmapId(super.value); +} + +/// A wrapper around platform specific behavior for the radius of a [Heatmap]. +/// +/// Currently this class only handles platform-specific values, but in the +/// future it may provide alternate constructors that abstract platform +/// differences in handling of heatmap radius values. +/// +/// See https://github.com/flutter/flutter/issues/145411 +/// +// TODO(stuartmorgan): Add constructor and enum field that informs the platform how to perform the conversion. +@immutable +class HeatmapRadius { + /// Create a [HeatmapRadius] with a radius in pixels. + /// + /// This value will be used verbatim on all platforms. It is up to the + /// developer to ensure that the value is appropriate for the platform. + const HeatmapRadius.fromPixels(this.radius); + + /// The platform-independent value of the radius. + final int radius; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is HeatmapRadius && other.radius == radius; + } + + @override + int get hashCode => radius.hashCode; +} + +/// Draws a heatmap on the map. +@immutable +class Heatmap implements MapsObject { + /// Creates an immutable representation of a [Heatmap] to draw on + /// [GoogleMap]. + const Heatmap({ + required this.heatmapId, + required this.data, + this.dissipating = true, + this.gradient, + this.maxIntensity, + this.opacity = _defaultOpacity, + required this.radius, + this.minimumZoomIntensity = _defaultMinimumZoomIntensity, + this.maximumZoomIntensity = _defaultMaximumZoomIntensity, + }) : assert(opacity >= 0 && opacity <= 1); + + /// The default heatmap opacity as seen in the Google Maps SDKS: + /// - https://github.com/googlemaps/google-maps-ios-utils/blob/0e7ed81f1bbd9d29e4529c40ae39b0791b0a0eb8/src/Heatmap/GMUHeatmapTileLayer.m#L66 + /// - https://github.com/googlemaps/android-maps-utils/blob/2883d5d471bc04fa0e74f286b7c5beeac634df84/library/src/main/java/com/google/maps/android/heatmaps/HeatmapTileProvider.java#L49 + /// - https://developers.google.com/maps/documentation/javascript/reference/visualization#HeatmapLayerOptions.opacity + /// + /// The default on web is actually 0.6, but to maintain consistency with + /// iOS and Android, we use 0.7. + static const double _defaultOpacity = 0.7; + + /// The minimum and maximum zoom intensity values required to get iOS + /// heatmap rendering to match the other platforms. + /// + /// See: + /// - https://github.com/googlemaps/google-maps-ios-utils/issues/419 + /// - https://github.com/googlemaps/google-maps-ios-utils/issues/371 + /// + /// The values used are respectively the minimum and maximum zoom levels + /// supported by Google Maps. + static const int _defaultMinimumZoomIntensity = 0; + static const int _defaultMaximumZoomIntensity = 21; + + /// Uniquely identifies a [Heatmap]. + final HeatmapId heatmapId; + + @override + HeatmapId get mapsId => heatmapId; + + /// The data points to display. + /// + /// This list must not be empty. + final List data; + + /// Specifies whether the heatmap dissipate on zoom. + /// + /// By default, the radius of influence of a data point is specified by the + /// radius option only. When dissipating is disabled, the radius option is + /// interpreted as a radius at zoom level 0. + final bool dissipating; + + /// The color gradient of the heatmap + final HeatmapGradient? gradient; + + /// The maximum intensity of the heatmap. + /// + /// By default, heatmap colors are dynamically scaled according to the + /// greatest concentration of points at any particular pixel on the map. + /// This property allows you to specify a fixed maximum. + final double? maxIntensity; + + /// The opacity of the heatmap, expressed as a number between 0 and 1. + final double opacity; + + /// The radius of influence for each data point, in pixels. + final HeatmapRadius radius; + + /// The minimum zoom intensity used for normalizing intensities. + final int minimumZoomIntensity; + + /// The maximum zoom intensity used for normalizing intensities. + final int maximumZoomIntensity; + + /// Creates a new [Heatmap] object whose values are the same as this + /// instance, unless overwritten by the specified parameters. + Heatmap copyWith({ + List? dataParam, + bool? dissipatingParam, + HeatmapGradient? gradientParam, + double? maxIntensityParam, + double? opacityParam, + HeatmapRadius? radiusParam, + int? minimumZoomIntensityParam, + int? maximumZoomIntensityParam, + }) { + return Heatmap( + heatmapId: heatmapId, + data: dataParam ?? data, + dissipating: dissipatingParam ?? dissipating, + gradient: gradientParam ?? gradient, + maxIntensity: maxIntensityParam ?? maxIntensity, + opacity: opacityParam ?? opacity, + radius: radiusParam ?? radius, + minimumZoomIntensity: minimumZoomIntensityParam ?? minimumZoomIntensity, + maximumZoomIntensity: maximumZoomIntensityParam ?? maximumZoomIntensity, + ); + } + + /// Creates a new [Heatmap] object whose values are the same as this + /// instance. + @override + Heatmap clone() => copyWith( + dataParam: List.of(data), + gradientParam: gradient?.clone(), + ); + + /// Converts this object to something serializable in JSON. + @override + Object toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, Object? value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('heatmapId', heatmapId.value); + addIfPresent('data', data.map((WeightedLatLng e) => e.toJson()).toList()); + addIfPresent('dissipating', dissipating); + addIfPresent('gradient', gradient?.toJson()); + addIfPresent('maxIntensity', maxIntensity); + addIfPresent('opacity', opacity); + addIfPresent('radius', radius.radius); + addIfPresent('minimumZoomIntensity', minimumZoomIntensity); + addIfPresent('maximumZoomIntensity', maximumZoomIntensity); + + return json; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is Heatmap && + heatmapId == other.heatmapId && + listEquals(data, other.data) && + dissipating == other.dissipating && + gradient == other.gradient && + maxIntensity == other.maxIntensity && + opacity == other.opacity && + radius == other.radius && + minimumZoomIntensity == other.minimumZoomIntensity && + maximumZoomIntensity == other.maximumZoomIntensity; + } + + @override + int get hashCode => heatmapId.hashCode; +} + +/// A data point entry for a heatmap. +/// +/// This is a geographical data point with a weight attribute. +@immutable +class WeightedLatLng { + /// Creates a [WeightedLatLng] with the specified [weight] + const WeightedLatLng(this.point, {this.weight = 1.0}); + + /// The geographical data point. + final LatLng point; + + /// The weighting value of the data point. + final double weight; + + /// Converts this object to something serializable in JSON. + Object toJson() { + return [point.toJson(), weight]; + } + + @override + String toString() { + return '${objectRuntimeType(this, 'WeightedLatLng')}($point, $weight)'; + } + + @override + bool operator ==(Object other) { + return other is WeightedLatLng && + other.point == point && + other.weight == weight; + } + + @override + int get hashCode => Object.hash(point, weight); +} + +/// Represents a mapping of intensity to color. +/// +/// Interpolates between given set of intensity and color values to produce a +/// full mapping for the range [0, 1]. +@immutable +class HeatmapGradient { + /// Creates a new [HeatmapGradient] object. + const HeatmapGradient( + this.colors, { + this.colorMapSize = 256, + }); + + /// The gradient colors. + /// + /// Distributed along [startPoint]s or uniformly depending on the platform. + /// Must contain at least one color. + final List colors; + + /// Number of entries in the generated color map. + final int colorMapSize; + + /// Creates a new [HeatmapGradient] object whose values are the same as this + /// instance, unless overwritten by the specified parameters. + HeatmapGradient copyWith({ + List? colorsParam, + int? colorMapSizeParam, + }) { + return HeatmapGradient( + colorsParam ?? colors, + colorMapSize: colorMapSizeParam ?? colorMapSize, + ); + } + + /// Creates a new [HeatmapGradient] object whose values are the same as this + /// instance. + HeatmapGradient clone() => copyWith( + colorsParam: List.of(colors), + ); + + /// Converts this object to something serializable in JSON. + Object toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, Object? value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('colors', + colors.map((HeatmapGradientColor e) => e.color.value).toList()); + addIfPresent('startPoints', + colors.map((HeatmapGradientColor e) => e.startPoint).toList()); + addIfPresent('colorMapSize', colorMapSize); + + return json; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is HeatmapGradient && + listEquals(colors, other.colors) && + colorMapSize == other.colorMapSize; + } + + @override + int get hashCode => Object.hash(colors, colorMapSize); +} + +/// A [Color] with a [startPoint] for use in a [HeatmapGradient]. +@immutable +class HeatmapGradientColor { + /// Creates a new [HeatmapGradientColor] object. + const HeatmapGradientColor(this.color, this.startPoint); + + /// The color for this portion of the gradient. + final Color color; + + /// The start point of this color. + final double startPoint; + + /// Creates a new [HeatmapGradientColor] object whose values are the same as + /// this instance, unless overwritten by the specified parameters. + HeatmapGradientColor copyWith({ + Color? colorParam, + double? startPointParam, + }) { + return HeatmapGradientColor( + colorParam ?? color, + startPointParam ?? startPoint, + ); + } + + /// Creates a new [HeatmapGradientColor] object whose values are the same as + /// this instance. + HeatmapGradientColor clone() => copyWith(); + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is HeatmapGradientColor && + color == other.color && + startPoint == other.startPoint; + } + + @override + int get hashCode => Object.hash(color, startPoint); + + @override + String toString() { + return '${objectRuntimeType(this, 'HeatmapGradientColor')}($color, $startPoint)'; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/heatmap_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/heatmap_updates.dart new file mode 100644 index 000000000000..bd74c6301fc3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/heatmap_updates.dart @@ -0,0 +1,26 @@ +// 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 'types.dart'; + +/// [Heatmap] update events to be applied to the [GoogleMap]. +/// +/// Used in [GoogleMapController] when the map is updated. +// (Do not re-export) +class HeatmapUpdates extends MapsObjectUpdates { + /// Computes [HeatmapUpdates] given previous and current [Heatmap]s. + HeatmapUpdates.from( + super.previous, + super.current, + ) : super.from(objectName: 'heatmap'); + + /// Set of Heatmaps to be added in this update. + Set get heatmapsToAdd => objectsToAdd; + + /// Set of Heatmaps to be removed in this update. + Set get heatmapIdsToRemove => objectIdsToRemove.cast(); + + /// Set of Heatmaps to be changed in this update. + Set get heatmapsToChange => objectsToChange; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_objects.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_objects.dart index 009a6a078268..23d605c43eff 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_objects.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_objects.dart @@ -20,6 +20,7 @@ class MapObjects { this.polygons = const {}, this.polylines = const {}, this.circles = const {}, + this.heatmaps = const {}, this.tileOverlays = const {}, this.clusterManagers = const {}, }); @@ -28,6 +29,7 @@ class MapObjects { final Set polygons; final Set polylines; final Set circles; + final Set heatmaps; final Set tileOverlays; final Set clusterManagers; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart index 3ef0e4ab18b5..745e300ff05c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart @@ -12,6 +12,8 @@ export 'circle_updates.dart'; export 'cluster.dart'; export 'cluster_manager.dart'; export 'cluster_manager_updates.dart'; +export 'heatmap.dart'; +export 'heatmap_updates.dart'; export 'joint_type.dart'; export 'location.dart'; export 'map_configuration.dart'; @@ -34,6 +36,7 @@ export 'ui.dart'; // Export the utils used by the Widget export 'utils/circle.dart'; export 'utils/cluster_manager.dart'; +export 'utils/heatmap.dart'; export 'utils/marker.dart'; export 'utils/polygon.dart'; export 'utils/polyline.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/heatmap.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/heatmap.dart new file mode 100644 index 000000000000..ff6e7944601f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/heatmap.dart @@ -0,0 +1,19 @@ +// 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 '../types.dart'; +import 'maps_object.dart'; + +/// Converts an [Iterable] of Heatmaps in a Map of +/// HeatmapId -> Heatmap. +Map keyByHeatmapId( + Iterable heatmaps, +) { + return keyByMapsObjectId(heatmaps).cast(); +} + +/// Converts a Set of Heatmaps into something serializable in JSON. +Object serializeHeatmapSet(Set heatmaps) { + return serializeMapsObjectSet(heatmaps); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml index e9d672efb1c2..a01bed82ff40 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/google_maps_f issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.8.0 +version: 2.9.0 environment: sdk: ^3.2.0 diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart index b0e48fd474bb..7cfc867aa73a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart @@ -143,6 +143,7 @@ class BuildViewGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, + Set heatmaps = const {}, Set tileOverlays = const {}, Set clusterManagers = const {}, Set>? gestureRecognizers = diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/heatmap_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/heatmap_test.dart new file mode 100644 index 000000000000..bc7d779d00ad --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/heatmap_test.dart @@ -0,0 +1,402 @@ +// 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:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$HeatmapRadius', () { + test('fromPixels', () { + const HeatmapRadius radius = HeatmapRadius.fromPixels(10); + expect(radius.radius, 10); + }); + + test('==', () { + const HeatmapRadius radius1 = HeatmapRadius.fromPixels(10); + const HeatmapRadius radius2 = HeatmapRadius.fromPixels(10); + const HeatmapRadius radius3 = HeatmapRadius.fromPixels(20); + expect(radius1, radius2); + expect(radius1, isNot(radius3)); + }); + + test('hashCode', () { + const int radius = 10; + const HeatmapRadius heatmapRadius = HeatmapRadius.fromPixels(radius); + expect(heatmapRadius.hashCode, radius.hashCode); + }); + }); + + group('$Heatmap', () { + test('constructor defaults', () { + const HeatmapId id = HeatmapId('heatmap'); + const List data = [ + WeightedLatLng(LatLng(1, 1)), + ]; + const HeatmapRadius radius = HeatmapRadius.fromPixels(10); + const Heatmap heatmap = Heatmap( + heatmapId: id, + data: data, + radius: radius, + ); + + expect(heatmap.heatmapId, id); + expect(heatmap.data, data); + expect(heatmap.dissipating, true); + expect(heatmap.gradient, null); + expect(heatmap.maxIntensity, null); + expect(heatmap.opacity, 0.7); + expect(heatmap.radius, radius); + expect(heatmap.minimumZoomIntensity, 0); + expect(heatmap.maximumZoomIntensity, 21); + + expect(heatmap.heatmapId, heatmap.mapsId); + }); + + test('construct with values', () { + const HeatmapId id = HeatmapId('heatmap'); + const List data = [ + WeightedLatLng(LatLng(1, 1)), + ]; + const HeatmapGradient gradient = HeatmapGradient([ + HeatmapGradientColor(Colors.red, 0.0), + ]); + const double maxIntensity = 1.0; + const double opacity = 0.5; + const HeatmapRadius radius = HeatmapRadius.fromPixels(10); + const int minimumZoomIntensity = 1; + const int maximumZoomIntensity = 20; + const Heatmap heatmap = Heatmap( + heatmapId: id, + data: data, + dissipating: false, + gradient: gradient, + maxIntensity: maxIntensity, + opacity: opacity, + radius: radius, + minimumZoomIntensity: minimumZoomIntensity, + maximumZoomIntensity: maximumZoomIntensity, + ); + + expect(heatmap.heatmapId, id); + expect(heatmap.data, data); + expect(heatmap.dissipating, false); + expect(heatmap.gradient, gradient); + expect(heatmap.maxIntensity, maxIntensity); + expect(heatmap.opacity, opacity); + expect(heatmap.radius, radius); + expect(heatmap.minimumZoomIntensity, minimumZoomIntensity); + expect(heatmap.maximumZoomIntensity, maximumZoomIntensity); + }); + + test('copyWith', () { + const Heatmap heatmap1 = Heatmap( + heatmapId: HeatmapId('heatmap'), + data: [], + radius: HeatmapRadius.fromPixels(10), + ); + + const List data = [ + WeightedLatLng(LatLng(1, 1)), + ]; + const HeatmapGradient gradient = HeatmapGradient([ + HeatmapGradientColor(Colors.red, 0.0), + ]); + const double maxIntensity = 1.0; + const double opacity = 0.5; + const HeatmapRadius radius = HeatmapRadius.fromPixels(20); + const int minimumZoomIntensity = 1; + const int maximumZoomIntensity = 20; + + final Heatmap heatmap2 = heatmap1.copyWith( + dataParam: data, + dissipatingParam: false, + gradientParam: gradient, + maxIntensityParam: maxIntensity, + opacityParam: opacity, + radiusParam: radius, + minimumZoomIntensityParam: minimumZoomIntensity, + maximumZoomIntensityParam: maximumZoomIntensity, + ); + + expect(heatmap2.heatmapId, heatmap1.heatmapId); + expect(heatmap2.data, data); + expect(heatmap2.dissipating, false); + expect(heatmap2.gradient, gradient); + expect(heatmap2.maxIntensity, maxIntensity); + expect(heatmap2.opacity, opacity); + expect(heatmap2.radius, radius); + expect(heatmap2.minimumZoomIntensity, minimumZoomIntensity); + }); + + test('clone', () { + const Heatmap heatmap1 = Heatmap( + heatmapId: HeatmapId('heatmap'), + data: [], + radius: HeatmapRadius.fromPixels(10), + ); + + final Heatmap heatmap2 = heatmap1.clone(); + + expect(heatmap2, heatmap1); + }); + + test('==', () { + const HeatmapId id = HeatmapId('heatmap'); + const List data = [ + WeightedLatLng(LatLng(1, 1)), + ]; + const HeatmapRadius radius = HeatmapRadius.fromPixels(10); + const Heatmap heatmap1 = Heatmap( + heatmapId: id, + data: data, + radius: radius, + ); + const Heatmap heatmap2 = Heatmap( + heatmapId: id, + data: data, + radius: radius, + ); + const Heatmap heatmap3 = Heatmap( + heatmapId: id, + data: data, + radius: HeatmapRadius.fromPixels(20), + ); + + expect(heatmap1, heatmap2); + expect(heatmap1, isNot(heatmap3)); + }); + + test('hashCode', () { + const HeatmapId id = HeatmapId('heatmap'); + const Heatmap heatmap = Heatmap( + heatmapId: id, + data: [], + radius: HeatmapRadius.fromPixels(10), + ); + + expect(heatmap.hashCode, id.hashCode); + }); + }); + + group('$WeightedLatLng', () { + test('constructor defaults', () { + const LatLng point = LatLng(1, 1); + const WeightedLatLng wll = WeightedLatLng(point); + + expect(wll.point, point); + expect(wll.weight, 1.0); + }); + + test('construct with values', () { + const LatLng point = LatLng(1, 1); + const double weight = 2.0; + const WeightedLatLng wll = WeightedLatLng(point, weight: weight); + + expect(wll.point, point); + expect(wll.weight, weight); + }); + + test('toJson', () { + const LatLng point = LatLng(1, 1); + const double weight = 2.0; + const WeightedLatLng wll = WeightedLatLng(point, weight: weight); + + expect(wll.toJson(), [ + [point.latitude, point.longitude], + weight, + ]); + }); + + test('toString', () { + const LatLng point = LatLng(1, 1); + const double weight = 2.0; + const WeightedLatLng wll = WeightedLatLng(point, weight: weight); + + expect(wll.toString(), 'WeightedLatLng($point, $weight)'); + }); + + test('==', () { + const LatLng point = LatLng(1, 1); + const double weight = 2.0; + const WeightedLatLng wll1 = WeightedLatLng(point, weight: weight); + const WeightedLatLng wll2 = WeightedLatLng(point, weight: weight); + const WeightedLatLng wll3 = WeightedLatLng(point, weight: 3.0); + + expect(wll1, wll2); + expect(wll1, isNot(wll3)); + }); + + test('hashCode', () { + const LatLng point = LatLng(1, 1); + const double weight = 2.0; + const WeightedLatLng wll = WeightedLatLng(point, weight: weight); + + expect(wll.hashCode, Object.hash(point, weight)); + }); + }); + + group('$HeatmapGradient', () { + test('constructor defaults', () { + const List colors = [ + HeatmapGradientColor(Colors.red, 0.0), + ]; + const HeatmapGradient gradient = HeatmapGradient(colors); + + expect(gradient.colors, colors); + expect(gradient.colorMapSize, 256); + }); + + test('construct with values', () { + const List colors = [ + HeatmapGradientColor(Colors.red, 0.0), + ]; + const int colorMapSize = 512; + const HeatmapGradient gradient = + HeatmapGradient(colors, colorMapSize: colorMapSize); + + expect(gradient.colors, colors); + expect(gradient.colorMapSize, colorMapSize); + }); + + test('copyWith', () { + const HeatmapGradient gradient1 = HeatmapGradient([ + HeatmapGradientColor(Colors.red, 0.0), + ]); + + const List colors = [ + HeatmapGradientColor(Colors.blue, 0.0), + ]; + const int colorMapSize = 512; + final HeatmapGradient gradient2 = gradient1.copyWith( + colorsParam: colors, + colorMapSizeParam: colorMapSize, + ); + + expect(gradient2.colors, colors); + expect(gradient2.colorMapSize, colorMapSize); + }); + + test('clone', () { + const HeatmapGradient gradient1 = HeatmapGradient( + [HeatmapGradientColor(Colors.red, 0.0)], + colorMapSize: 512, + ); + + final HeatmapGradient gradient2 = gradient1.clone(); + expect(gradient2, gradient1); + }); + + test('toJson', () { + const List colors = [ + HeatmapGradientColor(Colors.red, 0.0), + ]; + const int colorMapSize = 512; + const HeatmapGradient gradient = + HeatmapGradient(colors, colorMapSize: colorMapSize); + + expect(gradient.toJson(), { + 'colors': + colors.map((HeatmapGradientColor e) => e.color.value).toList(), + 'startPoints': + colors.map((HeatmapGradientColor e) => e.startPoint).toList(), + 'colorMapSize': colorMapSize, + }); + }); + + test('==', () { + const List colors = [ + HeatmapGradientColor(Colors.red, 0.0), + ]; + const HeatmapGradient gradient1 = HeatmapGradient(colors); + const HeatmapGradient gradient2 = HeatmapGradient(colors); + const HeatmapGradient gradient3 = HeatmapGradient( + [HeatmapGradientColor(Colors.blue, 0.0)], + colorMapSize: 512); + + expect(gradient1, gradient2); + expect(gradient1, isNot(gradient3)); + }); + + test('hashCode', () { + const List colors = [ + HeatmapGradientColor(Colors.red, 0.0), + ]; + const int colorMapSize = 512; + const HeatmapGradient gradient = + HeatmapGradient(colors, colorMapSize: colorMapSize); + + expect(gradient.hashCode, Object.hash(colors, colorMapSize)); + }); + }); + + group('$HeatmapGradientColor', () { + test('construct with values', () { + const Color color = Colors.red; + const double startPoint = 0.0; + const HeatmapGradientColor gradientColor = + HeatmapGradientColor(color, startPoint); + + expect(gradientColor.color, color); + expect(gradientColor.startPoint, startPoint); + }); + + test('copyWith', () { + const HeatmapGradientColor gradientColor1 = + HeatmapGradientColor(Colors.red, 0.0); + + const Color color = Colors.blue; + const double startPoint = 0.5; + final HeatmapGradientColor gradientColor2 = gradientColor1.copyWith( + colorParam: color, + startPointParam: startPoint, + ); + + expect(gradientColor2.color, color); + expect(gradientColor2.startPoint, startPoint); + }); + + test('clone', () { + const HeatmapGradientColor gradientColor1 = + HeatmapGradientColor(Colors.red, 0.0); + + final HeatmapGradientColor gradientColor2 = gradientColor1.clone(); + expect(gradientColor2, gradientColor1); + }); + + test('==', () { + const HeatmapGradientColor gradientColor1 = + HeatmapGradientColor(Colors.red, 0.0); + const HeatmapGradientColor gradientColor2 = + HeatmapGradientColor(Colors.red, 0.0); + const HeatmapGradientColor gradientColor3 = + HeatmapGradientColor(Colors.blue, 0.0); + + expect(gradientColor1, gradientColor2); + expect(gradientColor1, isNot(gradientColor3)); + }); + + test('hashCode', () { + const HeatmapGradientColor gradientColor = + HeatmapGradientColor(Colors.red, 0.0); + + expect( + gradientColor.hashCode, + Object.hash(gradientColor.color, gradientColor.startPoint), + ); + }); + + test('toString', () { + const HeatmapGradientColor gradientColor = + HeatmapGradientColor(Colors.red, 0.0); + + expect( + gradientColor.toString(), + 'HeatmapGradientColor(${gradientColor.color}, ${gradientColor.startPoint})', + ); + }); + }); +} From 57a8062fd0af34828613932f36e5bb1d3304e1d8 Mon Sep 17 00:00:00 2001 From: Rexios Date: Mon, 5 Aug 2024 11:48:03 -0400 Subject: [PATCH 2/6] Android changes to support heatmaps --- .../google_maps_flutter_android/CHANGELOG.md | 4 + .../google_maps_flutter_android/README.md | 10 + .../flutter/plugins/googlemaps/Convert.java | 129 ++++++++++++ .../plugins/googlemaps/GoogleMapBuilder.java | 7 + .../googlemaps/GoogleMapController.java | 30 +++ .../plugins/googlemaps/GoogleMapFactory.java | 5 + .../googlemaps/GoogleMapOptionsSink.java | 2 + .../plugins/googlemaps/HeatmapBuilder.java | 51 +++++ .../plugins/googlemaps/HeatmapController.java | 59 ++++++ .../googlemaps/HeatmapOptionsSink.java | 28 +++ .../googlemaps/HeatmapsController.java | 114 +++++++++++ .../flutter/plugins/googlemaps/Messages.java | 184 +++++++++++++---- .../plugins/googlemaps/ConvertTest.java | 189 ++++++++++++++++++ .../googlemaps/GoogleMapControllerTest.java | 29 +++ .../googlemaps/HeatmapsControllerTest.java | 108 ++++++++++ .../example/pubspec.yaml | 8 + .../lib/src/google_map_inspector_android.dart | 3 + .../lib/src/google_maps_flutter_android.dart | 27 +++ .../lib/src/messages.g.dart | 124 ++++++++---- .../lib/src/serialization.dart | 121 +++++++++++ .../pigeons/messages.dart | 33 ++- .../google_maps_flutter_android/pubspec.yaml | 8 +- ...oogle_maps_flutter_android_test.mocks.dart | 19 ++ 23 files changed, 1215 insertions(+), 77 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapBuilder.java create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapController.java create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapOptionsSink.java create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapsController.java create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/HeatmapsControllerTest.java create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/lib/src/serialization.dart 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 fa0e4a06a869..e48f90c9073c 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 ccef3a0b1277..a4f0ff5a3486 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 d9caf66d688c..825bb504abbf 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 02477418d425..1273a21f6647 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 3e8aaffe97c0..4cb5e3b57af7 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 b8d6485d35eb..81ab3ec72079 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 9f744a653b3c..b353da6f65ff 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 000000000000..8b335951958b --- /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 000000000000..a2990e234293 --- /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 000000000000..fc571665c7d2 --- /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 000000000000..2bbc685365c2 --- /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 2e5a61cb0755..32fc6eaf6cb4 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 8e00a67f23b0..156b8ce60e88 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 865920ee631d..2295ec1a757a 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 000000000000..3e12d4b8681a --- /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(minSdk = Build.VERSION_CODES.LOLLIPOP) +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 ddac3d7b231c..c89ea86d11d6 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 @@ -33,3 +33,11 @@ flutter: uses-material-design: true assets: - assets/ + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_android: + path: ../../../google_maps_flutter/google_maps_flutter_android + google_maps_flutter_platform_interface: + path: ../../../google_maps_flutter/google_maps_flutter_platform_interface 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 abe361566b67..76b8a8ce75d0 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 30bc4b7c9cd6..0c4fbc511c25 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), @@ -581,6 +597,7 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, + Set heatmaps = const {}, Set tileOverlays = const {}, Set clusterManagers = const {}, Set>? gestureRecognizers, @@ -598,6 +615,7 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { polylines: polylines, circles: circles, clusterManagers: clusterManagers, + heatmaps: heatmaps, tileOverlays: tileOverlays), mapOptions: mapOptions, ); @@ -612,6 +630,7 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, + Set heatmaps = const {}, Set tileOverlays = const {}, Set clusterManagers = const {}, Set>? gestureRecognizers, @@ -626,6 +645,7 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { polygons: polygons, polylines: polylines, circles: circles, + heatmaps: heatmaps, tileOverlays: tileOverlays, clusterManagers: clusterManagers, gestureRecognizers: gestureRecognizers, @@ -675,6 +695,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 c54b01c196ea..676b8f48b866 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 000000000000..8858a6e481bb --- /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 3bd8cbf36d36..7cf0afbea281 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 f4924aef5f48..e0022bcc2402 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 @@ -33,6 +33,12 @@ dev_dependencies: pigeon: ^20.0.2 plugin_platform_interface: ^2.1.7 + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_platform_interface: + path: ../../google_maps_flutter/google_maps_flutter_platform_interface + topics: - google-maps - google-maps-flutter 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 c49d62689264..e0fbdd574215 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, From c57e4ebf2f17357547163eb3e276e5042d9e73be Mon Sep 17 00:00:00 2001 From: Rexios Date: Mon, 5 Aug 2024 17:00:35 -0400 Subject: [PATCH 3/6] Merge with google_maps_flutter PR --- .../google_maps_flutter_android/example/pubspec.yaml | 4 +--- .../lib/src/google_maps_flutter_android.dart | 4 ---- .../google_maps_flutter_android/pubspec.yaml | 8 +------- 3 files changed, 2 insertions(+), 14 deletions(-) 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 c89ea86d11d6..ea97cea6d63d 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 @@ -39,5 +39,3 @@ flutter: dependency_overrides: google_maps_flutter_android: path: ../../../google_maps_flutter/google_maps_flutter_android - google_maps_flutter_platform_interface: - path: ../../../google_maps_flutter/google_maps_flutter_platform_interface 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 0c4fbc511c25..214acc41ed24 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 @@ -597,7 +597,6 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, - Set heatmaps = const {}, Set tileOverlays = const {}, Set clusterManagers = const {}, Set>? gestureRecognizers, @@ -615,7 +614,6 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { polylines: polylines, circles: circles, clusterManagers: clusterManagers, - heatmaps: heatmaps, tileOverlays: tileOverlays), mapOptions: mapOptions, ); @@ -630,7 +628,6 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, - Set heatmaps = const {}, Set tileOverlays = const {}, Set clusterManagers = const {}, Set>? gestureRecognizers, @@ -645,7 +642,6 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { polygons: polygons, polylines: polylines, circles: circles, - heatmaps: heatmaps, tileOverlays: tileOverlays, clusterManagers: clusterManagers, gestureRecognizers: gestureRecognizers, 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 e0022bcc2402..82068018bc7f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml @@ -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: @@ -33,12 +33,6 @@ dev_dependencies: pigeon: ^20.0.2 plugin_platform_interface: ^2.1.7 - -# FOR TESTING ONLY. DO NOT MERGE. -dependency_overrides: - google_maps_flutter_platform_interface: - path: ../../google_maps_flutter/google_maps_flutter_platform_interface - topics: - google-maps - google-maps-flutter From 4a99369c251c28ec2b7a5f5da4003fa7672ed89d Mon Sep 17 00:00:00 2001 From: Rexios Date: Mon, 5 Aug 2024 17:09:01 -0400 Subject: [PATCH 4/6] Revert platform interface changes --- .../src/method_channel/method_channel_google_maps_flutter.dart | 3 --- .../src/platform_interface/google_maps_flutter_platform.dart | 3 --- .../platform_interface/google_maps_flutter_platform_test.dart | 1 - 3 files changed, 7 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart index 53cea1a363d1..254a5444dc76 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart @@ -643,7 +643,6 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, - Set heatmaps = const {}, Set tileOverlays = const {}, Set clusterManagers = const {}, Set>? gestureRecognizers, @@ -675,7 +674,6 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, - Set heatmaps = const {}, Set tileOverlays = const {}, Set clusterManagers = const {}, Set>? gestureRecognizers, @@ -690,7 +688,6 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { polygons: polygons, polylines: polylines, circles: circles, - heatmaps: heatmaps, tileOverlays: tileOverlays, clusterManagers: clusterManagers, gestureRecognizers: gestureRecognizers, diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart index 3b9a8d24a046..a8f8e6d8b329 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart @@ -410,7 +410,6 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, - Set heatmaps = const {}, Set tileOverlays = const {}, Set>? gestureRecognizers = const >{}, @@ -441,7 +440,6 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, - Set heatmaps = const {}, Set tileOverlays = const {}, Map mapOptions = const {}, }) { @@ -453,7 +451,6 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { polygons: polygons, polylines: polylines, circles: circles, - heatmaps: heatmaps, tileOverlays: tileOverlays, gestureRecognizers: gestureRecognizers, mapOptions: mapOptions, diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart index 7cfc867aa73a..b0e48fd474bb 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart @@ -143,7 +143,6 @@ class BuildViewGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, - Set heatmaps = const {}, Set tileOverlays = const {}, Set clusterManagers = const {}, Set>? gestureRecognizers = From 0264ef03f0388e871d426a5f916627690eda3f21 Mon Sep 17 00:00:00 2001 From: Rexios Date: Mon, 5 Aug 2024 17:28:07 -0400 Subject: [PATCH 5/6] Remove dependency_overrides --- .../google_maps_flutter_android/example/pubspec.yaml | 6 ------ 1 file changed, 6 deletions(-) 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 ea97cea6d63d..9f3931140f67 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 @@ -33,9 +33,3 @@ flutter: uses-material-design: true assets: - assets/ - - -# FOR TESTING ONLY. DO NOT MERGE. -dependency_overrides: - google_maps_flutter_android: - path: ../../../google_maps_flutter/google_maps_flutter_android From 08231a156a6a85a99d4fc0e7d03d935e18882a11 Mon Sep 17 00:00:00 2001 From: Rexios Date: Mon, 5 Aug 2024 18:40:10 -0400 Subject: [PATCH 6/6] Fix tests --- .../io/flutter/plugins/googlemaps/HeatmapsControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 3e12d4b8681a..e087ba38683e 100644 --- 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 @@ -33,7 +33,7 @@ import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) -@Config(minSdk = Build.VERSION_CODES.LOLLIPOP) +@Config(sdk = Build.VERSION_CODES.P) public class HeatmapsControllerTest { private HeatmapsController controller; private GoogleMap googleMap;