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..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 @@ -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), }; 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..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 @@ -49,13 +49,13 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { throw UnimplementedError('init() has not been implemented.'); } - /// Updates configuration options of the map user interface - deprecated, use - /// updateMapConfiguration instead. + /// Updates configuration options of the map user interface. /// /// Change listeners are notified once the update has been made on the /// platform side. /// /// The returned [Future] completes after listeners have been notified. + @Deprecated('Use updateMapConfiguration instead.') Future updateMapOptions( Map optionsUpdate, { required int mapId, @@ -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 @@ -387,8 +400,8 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { return null; } - /// Returns a widget displaying the map view - deprecated, use - /// [buildViewWithConfiguration] instead. + /// Returns a widget displaying the map view. + @Deprecated('Use buildViewWithConfiguration instead.') Widget buildView( int creationId, PlatformViewCreatedCallback onPlatformViewCreated, { @@ -407,8 +420,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { throw UnimplementedError('buildView() has not been implemented.'); } - /// Returns a widget displaying the map view - deprecated, use - /// [buildViewWithConfiguration] instead. + /// Returns a widget displaying the map view. /// /// This method is similar to [buildView], but contains a parameter for /// platforms that require a text direction. @@ -417,6 +429,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// [buildView]. This is for backward compatibility with existing /// implementations. Platforms that use the text direction should override /// this as the primary implementation, and delegate to it from buildView. + @Deprecated('Use buildViewWithConfiguration instead.') Widget buildViewWithTextDirection( int creationId, PlatformViewCreatedCallback onPlatformViewCreated, { 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/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})', + ); + }); + }); +}