From 6875ceedf9cd40fabce2af1f5bbb929af5008dfd Mon Sep 17 00:00:00 2001 From: Rexios Date: Mon, 5 Aug 2024 11:45:39 -0400 Subject: [PATCH 1/5] 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 6b629ea037f982085c8e93f39c8a5740f64b4690 Mon Sep 17 00:00:00 2001 From: Rexios Date: Mon, 5 Aug 2024 11:51:11 -0400 Subject: [PATCH 2/5] Web changes to support heatmaps --- .../google_maps_flutter_web/CHANGELOG.md | 4 + .../google_maps_flutter_web/README.md | 13 ++ .../google_maps_controller_test.dart | 105 ++++++++++++++ .../google_maps_controller_test.mocks.dart | 89 ++++++++++++ .../google_maps_plugin_test.dart | 10 ++ .../google_maps_plugin_test.mocks.dart | 11 ++ .../example/integration_test/shape_test.dart | 49 +++++++ .../example/integration_test/shapes_test.dart | 133 ++++++++++++++++++ .../example/pubspec.yaml | 8 ++ .../example/web/index.html | 2 +- .../lib/google_maps_flutter_web.dart | 3 + .../lib/src/convert.dart | 32 +++++ .../lib/src/google_maps_controller.dart | 26 +++- .../lib/src/google_maps_flutter_web.dart | 9 ++ .../lib/src/heatmap.dart | 34 +++++ .../lib/src/heatmaps.dart | 57 ++++++++ .../google_maps_flutter_web/pubspec.yaml | 8 +- 17 files changed, 590 insertions(+), 3 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter_web/lib/src/heatmap.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter_web/lib/src/heatmaps.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md index 91d16d00e1a4..1fc48b49315b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.10 + +* Adds support for heatmap layers. + ## 0.5.9+2 * Restores support for Dart `^3.3.0` and Flutter `^3.19.0`. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/README.md b/packages/google_maps_flutter/google_maps_flutter_web/README.md index 1873645d7633..e53fe3506b07 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/README.md @@ -63,6 +63,9 @@ If you need marker clustering support, modify the tag to load the [js-mar ``` +## Heatmaps +To use heatmaps, add `&libraries=visualization` to the end of the URL. See [the documentation](https://developers.google.com/maps/documentation/javascript/libraries) for more information. + ## Limitations of the web version The following map options are not available in web, because the map doesn't rotate there: @@ -85,3 +88,13 @@ Indoor and building layers are still not available on the web. Traffic is. Only Android supports "[Lite Mode](https://developers.google.com/maps/documentation/android-sdk/lite)", so the `liteModeEnabled` constructor argument can't be set to `true` on web apps. Google Maps for web uses `HtmlElementView` to render maps. When a `GoogleMap` is stacked below other widgets, [`package:pointer_interceptor`](https://www.pub.dev/packages/pointer_interceptor) must be used to capture mouse events on the Flutter overlays. See issue [#73830](https://github.com/flutter/flutter/issues/73830). + +## Supported Heatmap Options + +| Field | Supported | +| ---------------------------- | :-------: | +| Heatmap.dissipating | ✓ | +| Heatmap.maxIntensity | ✓ | +| Heatmap.minimumZoomIntensity | x | +| Heatmap.maximumZoomIntensity | x | +| HeatmapGradient.colorMapSize | x | diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart index 7a735062c38d..be68cdf1c714 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart @@ -26,6 +26,9 @@ gmaps.Map mapShim() => throw UnimplementedError(); MockSpec( fallbackGenerators: {#googleMap: mapShim}, ), + MockSpec( + fallbackGenerators: {#googleMap: mapShim}, + ), MockSpec( fallbackGenerators: {#googleMap: mapShim}, ), @@ -161,6 +164,20 @@ void main() { }, throwsAssertionError); }); + testWidgets('cannot updateHeatmaps after dispose', + (WidgetTester tester) async { + controller.dispose(); + + expect(() { + controller.updateHeatmaps( + HeatmapUpdates.from( + const {}, + const {}, + ), + ); + }, throwsAssertionError); + }); + testWidgets('cannot updatePolygons after dispose', (WidgetTester tester) async { controller.dispose(); @@ -229,6 +246,7 @@ void main() { group('init', () { late MockCirclesController circles; + late MockHeatmapsController heatmaps; late MockMarkersController markers; late MockPolygonsController polygons; late MockPolylinesController polylines; @@ -237,6 +255,7 @@ void main() { setUp(() { circles = MockCirclesController(); + heatmaps = MockHeatmapsController(); markers = MockMarkersController(); polygons = MockPolygonsController(); polylines = MockPolylinesController(); @@ -249,6 +268,7 @@ void main() { ..debugSetOverrides( createMap: (_, __) => map, circles: circles, + heatmaps: heatmaps, markers: markers, polygons: polygons, polylines: polylines, @@ -287,6 +307,7 @@ void main() { ..debugSetOverrides( createMap: (_, __) => map, circles: circles, + heatmaps: heatmaps, markers: markers, polygons: polygons, polylines: polylines, @@ -295,6 +316,7 @@ void main() { ..init(); verify(circles.bindToMap(mapId, map)); + verify(heatmaps.bindToMap(mapId, map)); verify(markers.bindToMap(mapId, map)); verify(polygons.bindToMap(mapId, map)); verify(polylines.bindToMap(mapId, map)); @@ -307,6 +329,17 @@ void main() { circleId: CircleId('circle-1'), zIndex: 1234, ), + }, heatmaps: { + const Heatmap( + heatmapId: HeatmapId('heatmap-1'), + data: [ + WeightedLatLng(LatLng(43.355114, -5.851333)), + WeightedLatLng(LatLng(43.354797, -5.851860)), + WeightedLatLng(LatLng(43.354469, -5.851318)), + WeightedLatLng(LatLng(43.354762, -5.850824)), + ], + radius: HeatmapRadius.fromPixels(20), + ), }, markers: { const Marker( markerId: MarkerId('marker-1'), @@ -352,6 +385,7 @@ void main() { controller = createController(mapObjects: mapObjects) ..debugSetOverrides( circles: circles, + heatmaps: heatmaps, markers: markers, polygons: polygons, polylines: polylines, @@ -360,6 +394,7 @@ void main() { ..init(); verify(circles.addCircles(mapObjects.circles)); + verify(heatmaps.addHeatmaps(mapObjects.heatmaps)); verify(markers.addMarkers(mapObjects.markers)); verify(polygons.addPolygons(mapObjects.polygons)); verify(polylines.addPolylines(mapObjects.polylines)); @@ -670,6 +705,76 @@ void main() { })); }); + testWidgets('updateHeatmaps', (WidgetTester tester) async { + final MockHeatmapsController mock = MockHeatmapsController(); + controller.debugSetOverrides(heatmaps: mock); + + const List heatmapPoints = [ + WeightedLatLng(LatLng(37.782, -122.447)), + WeightedLatLng(LatLng(37.782, -122.445)), + WeightedLatLng(LatLng(37.782, -122.443)), + WeightedLatLng(LatLng(37.782, -122.441)), + WeightedLatLng(LatLng(37.782, -122.439)), + WeightedLatLng(LatLng(37.782, -122.437)), + WeightedLatLng(LatLng(37.782, -122.435)), + WeightedLatLng(LatLng(37.785, -122.447)), + WeightedLatLng(LatLng(37.785, -122.445)), + WeightedLatLng(LatLng(37.785, -122.443)), + WeightedLatLng(LatLng(37.785, -122.441)), + WeightedLatLng(LatLng(37.785, -122.439)), + WeightedLatLng(LatLng(37.785, -122.437)), + WeightedLatLng(LatLng(37.785, -122.435)) + ]; + + final Set previous = { + const Heatmap( + heatmapId: HeatmapId('to-be-updated'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + const Heatmap( + heatmapId: HeatmapId('to-be-removed'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + }; + + final Set current = { + const Heatmap( + heatmapId: HeatmapId('to-be-updated'), + data: heatmapPoints, + dissipating: false, + radius: HeatmapRadius.fromPixels(20), + ), + const Heatmap( + heatmapId: HeatmapId('to-be-added'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + }; + + controller.updateHeatmaps(HeatmapUpdates.from(previous, current)); + + verify(mock.removeHeatmaps({ + const HeatmapId('to-be-removed'), + })); + verify(mock.addHeatmaps({ + const Heatmap( + heatmapId: HeatmapId('to-be-added'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + })); + verify(mock.changeHeatmaps({ + const Heatmap( + heatmapId: HeatmapId('to-be-updated'), + data: heatmapPoints, + dissipating: false, + radius: HeatmapRadius.fromPixels(20), + ), + })); + }); + testWidgets('updateMarkers', (WidgetTester tester) async { final MockMarkersController mock = MockMarkersController(); controller = createController()..debugSetOverrides(markers: mock); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart index 832b799724e8..ca64a2a9d7f3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart @@ -114,6 +114,95 @@ class MockCirclesController extends _i1.Mock implements _i2.CirclesController { ); } +/// A class which mocks [HeatmapsController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHeatmapsController extends _i1.Mock + implements _i2.HeatmapsController { + @override + Map<_i3.HeatmapId, _i2.HeatmapController> get heatmaps => (super.noSuchMethod( + Invocation.getter(#heatmaps), + returnValue: <_i3.HeatmapId, _i2.HeatmapController>{}, + returnValueForMissingStub: <_i3.HeatmapId, _i2.HeatmapController>{}, + ) as Map<_i3.HeatmapId, _i2.HeatmapController>); + + @override + _i4.Map get googleMap => (super.noSuchMethod( + Invocation.getter(#googleMap), + returnValue: _i5.mapShim(), + returnValueForMissingStub: _i5.mapShim(), + ) as _i4.Map); + + @override + set googleMap(_i4.Map? _googleMap) => super.noSuchMethod( + Invocation.setter( + #googleMap, + _googleMap, + ), + returnValueForMissingStub: null, + ); + + @override + int get mapId => (super.noSuchMethod( + Invocation.getter(#mapId), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + + @override + set mapId(int? _mapId) => super.noSuchMethod( + Invocation.setter( + #mapId, + _mapId, + ), + returnValueForMissingStub: null, + ); + + @override + void addHeatmaps(Set<_i3.Heatmap>? heatmapsToAdd) => super.noSuchMethod( + Invocation.method( + #addHeatmaps, + [heatmapsToAdd], + ), + returnValueForMissingStub: null, + ); + + @override + void changeHeatmaps(Set<_i3.Heatmap>? heatmapsToChange) => super.noSuchMethod( + Invocation.method( + #changeHeatmaps, + [heatmapsToChange], + ), + returnValueForMissingStub: null, + ); + + @override + void removeHeatmaps(Set<_i3.HeatmapId>? heatmapIdsToRemove) => + super.noSuchMethod( + Invocation.method( + #removeHeatmaps, + [heatmapIdsToRemove], + ), + returnValueForMissingStub: null, + ); + + @override + void bindToMap( + int? mapId, + _i4.Map? googleMap, + ) => + super.noSuchMethod( + Invocation.method( + #bindToMap, + [ + mapId, + googleMap, + ], + ), + returnValueForMissingStub: null, + ); +} + /// A class which mocks [PolygonsController]. /// /// See the documentation for Mockito's code generation for more information. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart index de813b2efae6..1e02f576a089 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart @@ -263,6 +263,16 @@ void main() { verify(controller.updateCircles(expectedUpdates)); }); + testWidgets('updateHeatmaps', (WidgetTester tester) async { + final HeatmapUpdates expectedUpdates = HeatmapUpdates.from( + const {}, + const {}, + ); + + await plugin.updateHeatmaps(expectedUpdates, mapId: mapId); + + verify(controller.updateHeatmaps(expectedUpdates)); + }); // Tile Overlays testWidgets('updateTileOverlays', (WidgetTester tester) async { final Set expectedOverlays = { diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart index 2da0a46f13f2..cf5acfcb813c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart @@ -137,6 +137,7 @@ class MockGoogleMapController extends _i1.Mock _i4.DebugSetOptionsFunction? setOptions, _i4.MarkersController? markers, _i4.CirclesController? circles, + _i4.HeatmapsController? heatmaps, _i4.PolygonsController? polygons, _i4.PolylinesController? polylines, _i6.ClusterManagersController? clusterManagers, @@ -151,6 +152,7 @@ class MockGoogleMapController extends _i1.Mock #setOptions: setOptions, #markers: markers, #circles: circles, + #heatmaps: heatmaps, #polygons: polygons, #polylines: polylines, #clusterManagers: clusterManagers, @@ -289,6 +291,15 @@ class MockGoogleMapController extends _i1.Mock returnValueForMissingStub: null, ); + @override + void updateHeatmaps(_i2.HeatmapUpdates? updates) => super.noSuchMethod( + Invocation.method( + #updateHeatmaps, + [updates], + ), + returnValueForMissingStub: null, + ); + @override void updatePolygons(_i2.PolygonUpdates? updates) => super.noSuchMethod( Invocation.method( diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shape_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shape_test.dart index b8da855de840..0912fcbaec4d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shape_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shape_test.dart @@ -3,9 +3,11 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:js_interop'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps/google_maps.dart' as gmaps; +import 'package:google_maps/google_maps_visualization.dart' as visualization; import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; import 'package:integration_test/integration_test.dart'; @@ -216,4 +218,51 @@ void main() { }); }); }); + + group('HeatmapController', () { + late visualization.HeatmapLayer heatmap; + + setUp(() { + heatmap = visualization.HeatmapLayer(); + }); + + testWidgets('update', (WidgetTester tester) async { + final HeatmapController controller = HeatmapController(heatmap: heatmap); + final visualization.HeatmapLayerOptions options = + visualization.HeatmapLayerOptions() + ..data = [gmaps.LatLng(0, 0)].toJS; + + expect(heatmap.data, hasLength(0)); + + controller.update(options); + + expect(heatmap.data, hasLength(1)); + }); + + group('remove', () { + late HeatmapController controller; + + setUp(() { + controller = HeatmapController(heatmap: heatmap); + }); + + testWidgets('drops gmaps instance', (WidgetTester tester) async { + controller.remove(); + + expect(controller.heatmap, isNull); + }); + + testWidgets('cannot call update after remove', + (WidgetTester tester) async { + final visualization.HeatmapLayerOptions options = + visualization.HeatmapLayerOptions()..dissipating = true; + + controller.remove(); + + expect(() { + controller.update(options); + }, throwsAssertionError); + }); + }); + }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart index a6492a5b7bd6..6962b3b33e19 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart @@ -9,6 +9,7 @@ import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps/google_maps.dart' as gmaps; import 'package:google_maps/google_maps_geometry.dart' as geometry; +import 'package:google_maps/google_maps_visualization.dart' as visualization; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; // ignore: implementation_imports @@ -375,4 +376,136 @@ void main() { closeTo(0.5, _acceptableDelta)); }); }); + + group('HeatmapsController', () { + late HeatmapsController controller; + + const List heatmapPoints = [ + WeightedLatLng(LatLng(37.782, -122.447)), + WeightedLatLng(LatLng(37.782, -122.445)), + WeightedLatLng(LatLng(37.782, -122.443)), + WeightedLatLng(LatLng(37.782, -122.441)), + WeightedLatLng(LatLng(37.782, -122.439)), + WeightedLatLng(LatLng(37.782, -122.437)), + WeightedLatLng(LatLng(37.782, -122.435)), + WeightedLatLng(LatLng(37.785, -122.447)), + WeightedLatLng(LatLng(37.785, -122.445)), + WeightedLatLng(LatLng(37.785, -122.443)), + WeightedLatLng(LatLng(37.785, -122.441)), + WeightedLatLng(LatLng(37.785, -122.439)), + WeightedLatLng(LatLng(37.785, -122.437)), + WeightedLatLng(LatLng(37.785, -122.435)) + ]; + + setUp(() { + controller = HeatmapsController(); + controller.bindToMap(123, map); + }); + + testWidgets('addHeatmaps', (WidgetTester tester) async { + final Set heatmaps = { + const Heatmap( + heatmapId: HeatmapId('1'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + const Heatmap( + heatmapId: HeatmapId('2'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + }; + + controller.addHeatmaps(heatmaps); + + expect(controller.heatmaps.length, 2); + expect(controller.heatmaps, contains(const HeatmapId('1'))); + expect(controller.heatmaps, contains(const HeatmapId('2'))); + expect(controller.heatmaps, isNot(contains(const HeatmapId('66')))); + }); + + testWidgets('changeHeatmaps', (WidgetTester tester) async { + final Set heatmaps = { + const Heatmap( + heatmapId: HeatmapId('1'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + }; + controller.addHeatmaps(heatmaps); + + expect(controller.heatmaps[const HeatmapId('1')]?.heatmap?.data, + hasLength(0)); + + final Set updatedHeatmaps = { + const Heatmap( + heatmapId: HeatmapId('1'), + data: [WeightedLatLng(LatLng(0, 0))], + radius: HeatmapRadius.fromPixels(20), + ), + }; + controller.changeHeatmaps(updatedHeatmaps); + + expect(controller.heatmaps.length, 1); + expect(controller.heatmaps[const HeatmapId('1')]?.heatmap?.data, + hasLength(1)); + }); + + testWidgets('removeHeatmaps', (WidgetTester tester) async { + final Set heatmaps = { + const Heatmap( + heatmapId: HeatmapId('1'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + const Heatmap( + heatmapId: HeatmapId('2'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + const Heatmap( + heatmapId: HeatmapId('3'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + }; + + controller.addHeatmaps(heatmaps); + + expect(controller.heatmaps.length, 3); + + // Remove some polylines... + final Set heatmapIdsToRemove = { + const HeatmapId('1'), + const HeatmapId('3'), + }; + + controller.removeHeatmaps(heatmapIdsToRemove); + + expect(controller.heatmaps.length, 1); + expect(controller.heatmaps, isNot(contains(const HeatmapId('1')))); + expect(controller.heatmaps, contains(const HeatmapId('2'))); + expect(controller.heatmaps, isNot(contains(const HeatmapId('3')))); + }); + + testWidgets('Converts colors to CSS', (WidgetTester tester) async { + final Set heatmaps = { + const Heatmap( + heatmapId: HeatmapId('1'), + data: heatmapPoints, + gradient: HeatmapGradient( + [HeatmapGradientColor(Color(0xFFFABADA), 0)], + ), + radius: HeatmapRadius.fromPixels(20), + ), + }; + + controller.addHeatmaps(heatmaps); + + final visualization.HeatmapLayer heatmap = + controller.heatmaps.values.first.heatmap!; + + expect(heatmap.get('gradient'), ['rgba(250, 186, 218, 1)']); + }); + }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml index 2b96e1d5cab1..6dbee37d36a7 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml @@ -29,6 +29,14 @@ flutter: - assets/ dependency_overrides: + # FOR TESTING ONLY. DO NOT MERGE. + google_maps_flutter_android: + path: ../../../google_maps_flutter/google_maps_flutter_android + google_maps_flutter_ios: + path: ../../../google_maps_flutter/google_maps_flutter_ios + google_maps_flutter_platform_interface: + path: ../../../google_maps_flutter/google_maps_flutter_platform_interface + # Override the google_maps_flutter dependency on google_maps_flutter_web. # TODO(ditman): Unwind the circular dependency. This will create problems # if we need to make a breaking change to google_maps_flutter_web. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html b/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html index fc6ddfcfa7b4..51b7253b8e1f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html @@ -6,7 +6,7 @@ Browser Tests - + diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart index cda20cf9f526..f56e0b02bd1b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart @@ -16,6 +16,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:google_maps/google_maps.dart' as gmaps; +import 'package:google_maps/google_maps_visualization.dart' as visualization; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:sanitize_html/sanitize_html.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -34,6 +35,8 @@ part 'src/circles.dart'; part 'src/convert.dart'; part 'src/google_maps_controller.dart'; part 'src/google_maps_flutter_web.dart'; +part 'src/heatmap.dart'; +part 'src/heatmaps.dart'; part 'src/marker.dart'; part 'src/markers.dart'; part 'src/overlay.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart index dda30f23fa62..b8ec01d3c111 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart @@ -32,6 +32,11 @@ double _getCssOpacity(Color color) { return color.opacity; } +// Converts a [Color] into a valid CSS value rgba(R, G, B, A). +String _getCssColorWithAlpha(Color color) { + return 'rgba(${color.red}, ${color.green}, ${color.blue}, ${(color.alpha / 255).toStringAsFixed(2)})'; +} + // Converts options from the plugin into gmaps.MapOptions that can be used by the JS SDK. // The following options are not handled here, for various reasons: // The following are not available in web, because the map doesn't rotate there: @@ -475,6 +480,33 @@ gmaps.CircleOptions _circleOptionsFromCircle(Circle circle) { return circleOptions; } +visualization.HeatmapLayerOptions _heatmapOptionsFromHeatmap(Heatmap heatmap) { + final Iterable? gradientColors = + heatmap.gradient?.colors.map((HeatmapGradientColor e) => e.color); + final visualization.HeatmapLayerOptions heatmapOptions = + visualization.HeatmapLayerOptions() + ..data = heatmap.data + .map( + (WeightedLatLng e) => visualization.WeightedLocation() + ..location = gmaps.LatLng(e.point.latitude, e.point.longitude) + ..weight = e.weight, + ) + .toList() + .toJS + ..dissipating = heatmap.dissipating + ..gradient = gradientColors == null + ? null + : [ + // Web needs a first color with 0 alpha + gradientColors.first.withAlpha(0), + ...gradientColors, + ].map(_getCssColorWithAlpha).toList() + ..maxIntensity = heatmap.maxIntensity + ..opacity = heatmap.opacity + ..radius = heatmap.radius.radius; + return heatmapOptions; +} + gmaps.PolygonOptions _polygonOptionsFromPolygon( gmaps.Map googleMap, Polygon polygon) { // Convert all points to GmLatLng diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart index 3baeef8851ad..89f5e33e2387 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart @@ -30,9 +30,11 @@ class GoogleMapController { _polylines = mapObjects.polylines, _circles = mapObjects.circles, _clusterManagers = mapObjects.clusterManagers, + _heatmaps = mapObjects.heatmaps, _tileOverlays = mapObjects.tileOverlays, _lastMapConfiguration = mapConfiguration { _circlesController = CirclesController(stream: _streamController); + _heatmapsController = HeatmapsController(); _polygonsController = PolygonsController(stream: _streamController); _polylinesController = PolylinesController(stream: _streamController); _clusterManagersController = @@ -66,6 +68,7 @@ class GoogleMapController { final Set _polylines; final Set _circles; final Set _clusterManagers; + final Set _heatmaps; Set _tileOverlays; // The configuration passed by the user, before converting to gmaps. @@ -122,6 +125,7 @@ class GoogleMapController { // Geometry controllers, for different features of the map. CirclesController? _circlesController; + HeatmapsController? _heatmapsController; PolygonsController? _polygonsController; PolylinesController? _polylinesController; MarkersController? _markersController; @@ -146,6 +150,7 @@ class GoogleMapController { DebugSetOptionsFunction? setOptions, MarkersController? markers, CirclesController? circles, + HeatmapsController? heatmaps, PolygonsController? polygons, PolylinesController? polylines, ClusterManagersController? clusterManagers, @@ -155,6 +160,7 @@ class GoogleMapController { _overrideSetOptions = setOptions; _markersController = markers ?? _markersController; _circlesController = circles ?? _circlesController; + _heatmapsController = heatmaps ?? _heatmapsController; _polygonsController = polygons ?? _polygonsController; _polylinesController = polylines ?? _polylinesController; _clusterManagersController = clusterManagers ?? _clusterManagersController; @@ -263,6 +269,8 @@ class GoogleMapController { // null. assert(_circlesController != null, 'Cannot attach a map to a null CirclesController instance.'); + assert(_heatmapsController != null, + 'Cannot attach a map to a null HeatmapsController instance.'); assert(_polygonsController != null, 'Cannot attach a map to a null PolygonsController instance.'); assert(_polylinesController != null, @@ -275,6 +283,7 @@ class GoogleMapController { 'Cannot attach a map to a null TileOverlaysController instance.'); _circlesController!.bindToMap(_mapId, map); + _heatmapsController!.bindToMap(_mapId, map); _polygonsController!.bindToMap(_mapId, map); _polylinesController!.bindToMap(_mapId, map); _markersController!.bindToMap(_mapId, map); @@ -301,6 +310,7 @@ class GoogleMapController { _markersController!.addMarkers(_markers); _circlesController!.addCircles(_circles); + _heatmapsController!.addHeatmaps(_heatmaps); _polygonsController!.addPolygons(_polygons); _polylinesController!.addPolylines(_polylines); _tileOverlaysController!.addTileOverlays(_tileOverlays); @@ -439,12 +449,25 @@ class GoogleMapController { /// Applies [CircleUpdates] to the currently managed circles. void updateCircles(CircleUpdates updates) { assert( - _circlesController != null, 'Cannot update circles after dispose().'); + _circlesController != null, + 'Cannot update circles after dispose().', + ); _circlesController?.addCircles(updates.circlesToAdd); _circlesController?.changeCircles(updates.circlesToChange); _circlesController?.removeCircles(updates.circleIdsToRemove); } + /// Applies [HeatmapUpdates] to the currently managed heatmaps. + void updateHeatmaps(HeatmapUpdates updates) { + assert( + _heatmapsController != null, + 'Cannot update heatmaps after dispose().', + ); + _heatmapsController?.addHeatmaps(updates.heatmapsToAdd); + _heatmapsController?.changeHeatmaps(updates.heatmapsToChange); + _heatmapsController?.removeHeatmaps(updates.heatmapIdsToRemove); + } + /// Applies [PolygonUpdates] to the currently managed polygons. void updatePolygons(PolygonUpdates updates) { assert( @@ -531,6 +554,7 @@ class GoogleMapController { _widget = null; _googleMap = null; _circlesController = null; + _heatmapsController = null; _polygonsController = null; _polylinesController = null; _markersController = null; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart index 16618b59a6e8..c49b5ed67392 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart @@ -90,6 +90,15 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { _map(mapId).updateCircles(circleUpdates); } + /// Applies the passed in `heatmapUpdates` to the `mapId`. + @override + Future updateHeatmaps( + HeatmapUpdates heatmapUpdates, { + required int mapId, + }) async { + _map(mapId).updateHeatmaps(heatmapUpdates); + } + @override Future updateTileOverlays({ required Set newTileOverlays, diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/heatmap.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/heatmap.dart new file mode 100644 index 000000000000..1e792f44ca17 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/heatmap.dart @@ -0,0 +1,34 @@ +// 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. + +part of '../google_maps_flutter_web.dart'; + +/// The `HeatmapController` class wraps a [visualization.HeatmapLayer] and its `onTap` behavior. +class HeatmapController { + /// Creates a `HeatmapController`, which wraps a [visualization.HeatmapLayer] object and its `onTap` behavior. + HeatmapController({required visualization.HeatmapLayer heatmap}) + : _heatmap = heatmap; + + visualization.HeatmapLayer? _heatmap; + + /// Returns the wrapped [visualization.HeatmapLayer]. Only used for testing. + @visibleForTesting + visualization.HeatmapLayer? get heatmap => _heatmap; + + /// Updates the options of the wrapped [visualization.HeatmapLayer] object. + /// + /// This cannot be called after [remove]. + void update(visualization.HeatmapLayerOptions options) { + assert(_heatmap != null, 'Cannot `update` Heatmap after calling `remove`.'); + _heatmap!.options = options; + } + + /// Disposes of the currently wrapped [visualization.HeatmapLayer]. + void remove() { + if (_heatmap != null) { + _heatmap!.map = null; + _heatmap = null; + } + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/heatmaps.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/heatmaps.dart new file mode 100644 index 000000000000..2d235515a297 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/heatmaps.dart @@ -0,0 +1,57 @@ +// 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. + +part of '../google_maps_flutter_web.dart'; + +/// This class manages all the [HeatmapController]s associated to a [GoogleMapController]. +class HeatmapsController extends GeometryController { + /// Initialize the cache + HeatmapsController() + : _heatmapIdToController = {}; + + // A cache of [HeatmapController]s indexed by their [HeatmapId]. + final Map _heatmapIdToController; + + /// Returns the cache of [HeatmapController]s. Test only. + @visibleForTesting + Map get heatmaps => _heatmapIdToController; + + /// Adds a set of [Heatmap] objects to the cache. + /// + /// Wraps each [Heatmap] into its corresponding [HeatmapController]. + void addHeatmaps(Set heatmapsToAdd) { + heatmapsToAdd.forEach(_addHeatmap); + } + + void _addHeatmap(Heatmap heatmap) { + final visualization.HeatmapLayerOptions heatmapOptions = + _heatmapOptionsFromHeatmap(heatmap); + final visualization.HeatmapLayer gmHeatmap = + visualization.HeatmapLayer(heatmapOptions); + gmHeatmap.map = googleMap; + final HeatmapController controller = HeatmapController(heatmap: gmHeatmap); + _heatmapIdToController[heatmap.heatmapId] = controller; + } + + /// Updates a set of [Heatmap] objects with new options. + void changeHeatmaps(Set heatmapsToChange) { + heatmapsToChange.forEach(_changeHeatmap); + } + + void _changeHeatmap(Heatmap heatmap) { + final HeatmapController? heatmapController = + _heatmapIdToController[heatmap.heatmapId]; + heatmapController?.update(_heatmapOptionsFromHeatmap(heatmap)); + } + + /// Removes a set of [HeatmapId]s from the cache. + void removeHeatmaps(Set heatmapIdsToRemove) { + for (final HeatmapId heatmapId in heatmapIdsToRemove) { + final HeatmapController? heatmapController = + _heatmapIdToController[heatmapId]; + heatmapController?.remove(); + _heatmapIdToController.remove(heatmapId); + } + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index a07eab5ed6bd..4e346f7ef28e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 0.5.9+2 +version: 0.5.10 environment: sdk: ^3.3.0 @@ -40,3 +40,9 @@ topics: # The example deliberately includes limited-use secrets. false_secrets: - /example/web/index.html + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_platform_interface: + path: ../../google_maps_flutter/google_maps_flutter_platform_interface From e0768bbfa766355ed3cd13d13851f4010e0a4495 Mon Sep 17 00:00:00 2001 From: Rexios Date: Mon, 5 Aug 2024 17:02:14 -0400 Subject: [PATCH 3/5] Merge with google_maps_flutter PR --- .../example/integration_test/shapes_test.dart | 19 +++++++++++++------ .../example/pubspec.yaml | 4 +--- .../google_maps_flutter_web/pubspec.yaml | 8 +------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart index 6962b3b33e19..ec8198679d55 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart @@ -428,14 +428,16 @@ void main() { final Set heatmaps = { const Heatmap( heatmapId: HeatmapId('1'), - data: heatmapPoints, + data: [], radius: HeatmapRadius.fromPixels(20), ), }; controller.addHeatmaps(heatmaps); - expect(controller.heatmaps[const HeatmapId('1')]?.heatmap?.data, - hasLength(0)); + expect( + controller.heatmaps[const HeatmapId('1')]?.heatmap?.data, + hasLength(0), + ); final Set updatedHeatmaps = { const Heatmap( @@ -447,8 +449,10 @@ void main() { controller.changeHeatmaps(updatedHeatmaps); expect(controller.heatmaps.length, 1); - expect(controller.heatmaps[const HeatmapId('1')]?.heatmap?.data, - hasLength(1)); + expect( + controller.heatmaps[const HeatmapId('1')]?.heatmap?.data, + hasLength(1), + ); }); testWidgets('removeHeatmaps', (WidgetTester tester) async { @@ -505,7 +509,10 @@ void main() { final visualization.HeatmapLayer heatmap = controller.heatmaps.values.first.heatmap!; - expect(heatmap.get('gradient'), ['rgba(250, 186, 218, 1)']); + expect( + heatmap.get('gradient'), + ['rgba(250, 186, 218, 0.00)', 'rgba(250, 186, 218, 1.00)'], + ); }); }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml index 6dbee37d36a7..cbe41351af4f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml @@ -8,7 +8,7 @@ environment: dependencies: flutter: sdk: flutter - google_maps_flutter_platform_interface: ^2.7.0 + google_maps_flutter_platform_interface: ^2.9.0 google_maps_flutter_web: path: ../ web: ^1.0.0 @@ -34,8 +34,6 @@ dependency_overrides: path: ../../../google_maps_flutter/google_maps_flutter_android google_maps_flutter_ios: path: ../../../google_maps_flutter/google_maps_flutter_ios - google_maps_flutter_platform_interface: - path: ../../../google_maps_flutter/google_maps_flutter_platform_interface # Override the google_maps_flutter dependency on google_maps_flutter_web. # TODO(ditman): Unwind the circular dependency. This will create problems diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index 4e346f7ef28e..8240c1b632f3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: flutter_web_plugins: sdk: flutter google_maps: ^8.0.0 - google_maps_flutter_platform_interface: ^2.7.0 + google_maps_flutter_platform_interface: ^2.9.0 sanitize_html: ^2.0.0 stream_transform: ^2.0.0 web: ">=0.5.1 <2.0.0" @@ -40,9 +40,3 @@ topics: # The example deliberately includes limited-use secrets. false_secrets: - /example/web/index.html - - -# FOR TESTING ONLY. DO NOT MERGE. -dependency_overrides: - google_maps_flutter_platform_interface: - path: ../../google_maps_flutter/google_maps_flutter_platform_interface From ebdb9859c83fcbe1c9c32d4430a7fb70c512ec65 Mon Sep 17 00:00:00 2001 From: Rexios Date: Mon, 5 Aug 2024 17:09:46 -0400 Subject: [PATCH 4/5] 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 6f64d4c7c7d573c665ee707a9d8d46ffd6f102ea Mon Sep 17 00:00:00 2001 From: Rexios Date: Mon, 5 Aug 2024 17:28:51 -0400 Subject: [PATCH 5/5] Remove dependency overrides --- .../google_maps_flutter_web/example/pubspec.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml index cbe41351af4f..a883f5f75218 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml @@ -29,12 +29,6 @@ flutter: - assets/ dependency_overrides: - # FOR TESTING ONLY. DO NOT MERGE. - google_maps_flutter_android: - path: ../../../google_maps_flutter/google_maps_flutter_android - google_maps_flutter_ios: - path: ../../../google_maps_flutter/google_maps_flutter_ios - # Override the google_maps_flutter dependency on google_maps_flutter_web. # TODO(ditman): Unwind the circular dependency. This will create problems # if we need to make a breaking change to google_maps_flutter_web.