diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 78e2e457191e..4fa7a9b37b98 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.8.0 + +* Adds support for heatmap layers. + ## 2.7.1 * Updates the example app to use TLHC mode, per current package guidance. diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/tiles_inspector.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/tiles_inspector.dart index 9c9914d8935d..d115257c07b4 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/tiles_inspector.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/tiles_inspector.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; @@ -21,6 +22,8 @@ void main() { } void runTests() { + const double floatTolerance = 1e-8; + GoogleMapsFlutterPlatform.instance.enableDebugInspection(); final GoogleMapsInspectorPlatform inspector = @@ -204,6 +207,271 @@ void runTests() { }, ); }, skip: isWeb /* Tiles not supported on the web */); + + /// Check that two lists of [WeightedLatLng] are more or less equal. + void expectHeatmapDataMoreOrLessEquals( + List data1, + List data2, + ) { + expect(data1.length, data2.length); + for (int i = 0; i < data1.length; i++) { + final WeightedLatLng wll1 = data1[i]; + final WeightedLatLng wll2 = data2[i]; + expect(wll1.weight, wll2.weight); + expect(wll1.point.latitude, moreOrLessEquals(wll2.point.latitude)); + expect(wll1.point.longitude, moreOrLessEquals(wll2.point.longitude)); + } + } + + /// Check that two [HeatmapGradient]s are more or less equal. + void expectHeatmapGradientMoreOrLessEquals( + HeatmapGradient? gradient1, + HeatmapGradient? gradient2, + ) { + if (gradient1 == null || gradient2 == null) { + expect(gradient1, gradient2); + return; + } + expect(gradient2, isNotNull); + + expect(gradient1.colors.length, gradient2.colors.length); + for (int i = 0; i < gradient1.colors.length; i++) { + final HeatmapGradientColor color1 = gradient1.colors[i]; + final HeatmapGradientColor color2 = gradient2.colors[i]; + expect(color1.color, color2.color); + expect( + color1.startPoint, + moreOrLessEquals(color2.startPoint, epsilon: floatTolerance), + ); + } + + expect(gradient1.colorMapSize, gradient2.colorMapSize); + } + + void expectHeatmapEquals(Heatmap heatmap1, Heatmap heatmap2) { + expectHeatmapDataMoreOrLessEquals(heatmap1.data, heatmap2.data); + expectHeatmapGradientMoreOrLessEquals(heatmap1.gradient, heatmap2.gradient); + + // Only Android supports `maxIntensity` + // so the platform value is undefined on others. + bool canHandleMaxIntensity() { + return Platform.isAndroid; + } + + // Only iOS supports `minimumZoomIntensity` and `maximumZoomIntensity` + // so the platform value is undefined on others. + bool canHandleZoomIntensity() { + return Platform.isIOS; + } + + if (canHandleMaxIntensity()) { + expect(heatmap1.maxIntensity, heatmap2.maxIntensity); + } + expect( + heatmap1.opacity, + moreOrLessEquals(heatmap2.opacity, epsilon: floatTolerance), + ); + expect(heatmap1.radius, heatmap2.radius); + if (canHandleZoomIntensity()) { + expect(heatmap1.minimumZoomIntensity, heatmap2.minimumZoomIntensity); + expect(heatmap1.maximumZoomIntensity, heatmap2.maximumZoomIntensity); + } + } + + const Heatmap heatmap1 = Heatmap( + heatmapId: HeatmapId('heatmap_1'), + data: [ + 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), weight: 2) + ], + dissipating: false, + gradient: HeatmapGradient( + [ + HeatmapGradientColor( + Color.fromARGB(255, 0, 255, 255), + 0.2, + ), + HeatmapGradientColor( + Color.fromARGB(255, 0, 63, 255), + 0.4, + ), + HeatmapGradientColor( + Color.fromARGB(255, 0, 0, 191), + 0.6, + ), + HeatmapGradientColor( + Color.fromARGB(255, 63, 0, 91), + 0.8, + ), + HeatmapGradientColor( + Color.fromARGB(255, 255, 0, 0), + 1, + ), + ], + ), + maxIntensity: 1, + opacity: 0.5, + radius: HeatmapRadius.fromPixels(40), + minimumZoomIntensity: 1, + maximumZoomIntensity: 20, + ); + + testWidgets('set heatmap correctly', (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final Heatmap heatmap2 = Heatmap( + heatmapId: const HeatmapId('heatmap_2'), + data: heatmap1.data, + dissipating: heatmap1.dissipating, + gradient: heatmap1.gradient, + maxIntensity: heatmap1.maxIntensity, + opacity: heatmap1.opacity - 0.1, + radius: heatmap1.radius, + minimumZoomIntensity: heatmap1.minimumZoomIntensity, + maximumZoomIntensity: heatmap1.maximumZoomIntensity, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: kInitialCameraPosition, + heatmaps: {heatmap1, heatmap2}, + onMapCreated: (GoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + if (inspector.supportsGettingHeatmapInfo()) { + final Heatmap heatmapInfo1 = + (await inspector.getHeatmapInfo(heatmap1.mapsId, mapId: mapId))!; + final Heatmap heatmapInfo2 = + (await inspector.getHeatmapInfo(heatmap2.mapsId, mapId: mapId))!; + + expectHeatmapEquals(heatmap1, heatmapInfo1); + expectHeatmapEquals(heatmap2, heatmapInfo2); + } + }); + + testWidgets('update heatmaps correctly', (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final Key key = GlobalKey(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: kInitialCameraPosition, + heatmaps: {heatmap1}, + onMapCreated: (GoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + ), + ); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + final Heatmap heatmap1New = heatmap1.copyWith( + dataParam: heatmap1.data.sublist(5), + dissipatingParam: !heatmap1.dissipating, + gradientParam: heatmap1.gradient, + maxIntensityParam: heatmap1.maxIntensity! + 1, + opacityParam: heatmap1.opacity - 0.1, + radiusParam: HeatmapRadius.fromPixels(heatmap1.radius.radius + 1), + minimumZoomIntensityParam: heatmap1.minimumZoomIntensity + 1, + maximumZoomIntensityParam: heatmap1.maximumZoomIntensity + 1, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: kInitialCameraPosition, + heatmaps: {heatmap1New}, + onMapCreated: (GoogleMapController controller) { + fail('update: OnMapCreated should get called only once.'); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + if (inspector.supportsGettingHeatmapInfo()) { + final Heatmap heatmapInfo1 = + (await inspector.getHeatmapInfo(heatmap1.mapsId, mapId: mapId))!; + + expectHeatmapEquals(heatmap1New, heatmapInfo1); + } + }); + + testWidgets('remove heatmaps correctly', (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final Key key = GlobalKey(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: kInitialCameraPosition, + heatmaps: {heatmap1}, + onMapCreated: (GoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + ), + ); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + if (inspector.supportsGettingHeatmapInfo()) { + final Heatmap? heatmapInfo1 = + await inspector.getHeatmapInfo(heatmap1.mapsId, mapId: mapId); + + expect(heatmapInfo1, isNull); + } + }); } class _DebugTileProvider implements TileProvider { diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 3e067e9f7632..b25dee2203e3 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -263,12 +263,12 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", - "${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.framework/Resources/GoogleMaps.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleMaps/GoogleMapsResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/google_maps_flutter_ios/google_maps_flutter_ios_privacy.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMaps.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMapsResources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/google_maps_flutter_ios_privacy.bundle", ); runOnlyForDeploymentPostprocessing = 0; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/heatmap.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/heatmap.dart new file mode 100644 index 000000000000..956a0a49e1b3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/heatmap.dart @@ -0,0 +1,166 @@ +// 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. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'page.dart'; + +class HeatmapPage extends GoogleMapExampleAppPage { + const HeatmapPage({Key? key}) + : super(const Icon(Icons.map), 'Heatmaps', key: key); + + @override + Widget build(BuildContext context) { + return const HeatmapBody(); + } +} + +class HeatmapBody extends StatefulWidget { + const HeatmapBody({super.key}); + + @override + State createState() => HeatmapBodyState(); +} + +class HeatmapBodyState extends State { + static const LatLng sanFrancisco = LatLng(37.774546, -122.433523); + + List enabledPoints = [ + const WeightedLatLng(LatLng(37.782, -122.447)), + const WeightedLatLng(LatLng(37.782, -122.445)), + const WeightedLatLng(LatLng(37.782, -122.443)), + const WeightedLatLng(LatLng(37.782, -122.441)), + const WeightedLatLng(LatLng(37.782, -122.439)), + const WeightedLatLng(LatLng(37.782, -122.437)), + const WeightedLatLng(LatLng(37.782, -122.435)), + const WeightedLatLng(LatLng(37.785, -122.447)), + const WeightedLatLng(LatLng(37.785, -122.445)), + const WeightedLatLng(LatLng(37.785, -122.443)), + const WeightedLatLng(LatLng(37.785, -122.441)), + const WeightedLatLng(LatLng(37.785, -122.439)), + const WeightedLatLng(LatLng(37.785, -122.437)), + const WeightedLatLng(LatLng(37.785, -122.435)) + ]; + + List disabledPoints = []; + + void _addPoint() { + if (disabledPoints.isEmpty) { + return; + } + + final WeightedLatLng point = disabledPoints.first; + disabledPoints.removeAt(0); + + setState(() => enabledPoints.add(point)); + } + + void _removePoint() { + if (enabledPoints.isEmpty) { + return; + } + + final WeightedLatLng point = enabledPoints.first; + enabledPoints.removeAt(0); + + setState(() => disabledPoints.add(point)); + } + + @override + Widget build(BuildContext context) { + // The Google Maps SDK handles radius differently across platforms. + // It is the developer's responsibility to ensure proper behavior on + // each platform. + final int radius; + if (kIsWeb) { + radius = 10; + } else { + radius = switch (defaultTargetPlatform) { + TargetPlatform.android => 20, + TargetPlatform.iOS => 40, + _ => 20, + }; + } + + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: GoogleMap( + initialCameraPosition: const CameraPosition( + target: sanFrancisco, + zoom: 13, + ), + heatmaps: { + Heatmap( + heatmapId: const HeatmapId('test'), + data: enabledPoints, + gradient: const HeatmapGradient( + [ + HeatmapGradientColor( + Color.fromARGB(255, 0, 255, 255), + 0.2, + ), + HeatmapGradientColor( + Color.fromARGB(255, 0, 63, 255), + 0.4, + ), + HeatmapGradientColor( + Color.fromARGB(255, 0, 0, 191), + 0.6, + ), + HeatmapGradientColor( + Color.fromARGB(255, 63, 0, 91), + 0.8, + ), + HeatmapGradientColor( + Color.fromARGB(255, 255, 0, 0), + 1, + ), + ], + ), + maxIntensity: 1, + radius: HeatmapRadius.fromPixels(radius), + ) + }), + ), + ), + Expanded( + child: SingleChildScrollView( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + Column( + children: [ + TextButton( + onPressed: + disabledPoints.isNotEmpty ? _addPoint : null, + child: const Text('Add point'), + ), + TextButton( + onPressed: + enabledPoints.isNotEmpty ? _removePoint : null, + child: const Text('Remove point'), + ), + ], + ) + ], + ) + ], + ), + ), + ), + ], + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart index a7bd320b5bf2..e000e22d2e81 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart @@ -9,6 +9,7 @@ import 'package:google_maps_flutter_android/google_maps_flutter_android.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'animate_camera.dart'; +import 'heatmap.dart'; import 'lite_mode.dart'; import 'map_click.dart'; import 'map_coordinates.dart'; @@ -43,6 +44,7 @@ final List _allPages = [ const LiteModePage(), const TileOverlayPage(), const MapIdPage(), + const HeatmapPage(), ]; /// MapsDemo is the Main Application. diff --git a/packages/google_maps_flutter/google_maps_flutter/example/web/index.html b/packages/google_maps_flutter/google_maps_flutter/example/web/index.html index 26de2ffd7cb0..62869e8931fc 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/web/index.html +++ b/packages/google_maps_flutter/google_maps_flutter/example/web/index.html @@ -37,7 +37,7 @@ - + diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart index 7eb4af947318..a92e6ba6a0ff 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart @@ -27,6 +27,11 @@ export 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf Cap, Circle, CircleId, + Heatmap, + HeatmapGradient, + HeatmapGradientColor, + HeatmapId, + HeatmapRadius, InfoWindow, JointType, LatLng, @@ -47,7 +52,8 @@ export 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf TileOverlay, TileOverlayId, TileProvider, - WebGestureHandling; + WebGestureHandling, + WeightedLatLng; part 'src/controller.dart'; part 'src/google_map.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart index 393288a6165f..9cea738dbc3d 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart @@ -136,6 +136,17 @@ class GoogleMapController { .updateCircles(circleUpdates, mapId: mapId); } + /// 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) { + return GoogleMapsFlutterPlatform.instance + .updateHeatmaps(heatmapUpdates, mapId: mapId); + } + /// Updates tile overlays configuration. /// /// Change listeners are notified once the update has been made on the diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart index bbd3a425248d..bd1b0086358a 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart @@ -120,6 +120,7 @@ class GoogleMap extends StatefulWidget { this.polygons = const {}, this.polylines = const {}, this.circles = const {}, + this.heatmaps = const {}, this.onCameraMoveStarted, this.tileOverlays = const {}, this.onCameraMove, @@ -214,6 +215,9 @@ class GoogleMap extends StatefulWidget { /// Circles to be placed on the map. final Set circles; + /// Heatmaps to show on the map. + final Set heatmaps; + /// Tile overlays to be placed on the map. final Set tileOverlays; @@ -328,6 +332,7 @@ class _GoogleMapState extends State { Map _polygons = {}; Map _polylines = {}; Map _circles = {}; + Map _heatmaps = {}; late MapConfiguration _mapConfiguration; @override @@ -347,6 +352,7 @@ class _GoogleMapState extends State { polygons: widget.polygons, polylines: widget.polylines, circles: widget.circles, + heatmaps: widget.heatmaps, ), mapConfiguration: _mapConfiguration, ); @@ -360,6 +366,7 @@ class _GoogleMapState extends State { _polygons = keyByPolygonId(widget.polygons); _polylines = keyByPolylineId(widget.polylines); _circles = keyByCircleId(widget.circles); + _heatmaps = keyByHeatmapId(widget.heatmaps); } @override @@ -381,6 +388,7 @@ class _GoogleMapState extends State { _updatePolygons(); _updatePolylines(); _updateCircles(); + _updateHeatmaps(); _updateTileOverlays(); } @@ -423,6 +431,16 @@ class _GoogleMapState extends State { _circles = keyByCircleId(widget.circles); } + Future _updateHeatmaps() async { + final GoogleMapController controller = await _controller.future; + unawaited( + controller._updateHeatmaps( + HeatmapUpdates.from(_heatmaps.values.toSet(), widget.heatmaps), + ), + ); + _heatmaps = keyByHeatmapId(widget.heatmaps); + } + Future _updateTileOverlays() async { final GoogleMapController controller = await _controller.future; unawaited(controller._updateTileOverlays(widget.tileOverlays)); diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index c38f7e9c5839..413ad8421595 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.7.1 +version: 2.8.0 environment: sdk: ^3.4.0 @@ -21,10 +21,10 @@ flutter: dependencies: flutter: sdk: flutter - google_maps_flutter_android: ^2.9.0 - google_maps_flutter_ios: ^2.7.0 - google_maps_flutter_platform_interface: ^2.7.0 - google_maps_flutter_web: ^0.5.8 + google_maps_flutter_android: ^2.13.0 + google_maps_flutter_ios: ^2.11.0 + google_maps_flutter_platform_interface: ^2.9.0 + google_maps_flutter_web: ^0.5.10 dev_dependencies: flutter_test: diff --git a/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart index 22447ba5ecad..422f6026b9f1 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart @@ -85,6 +85,15 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { await _fakeDelay(); } + @override + Future updateHeatmaps( + HeatmapUpdates heatmapUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.heatmapUpdates.add(heatmapUpdates); + await _fakeDelay(); + } + @override Future updateTileOverlays({ required Set newTileOverlays, @@ -288,6 +297,8 @@ class PlatformMapStateRecorder { polylineUpdates .add(PolylineUpdates.from(const {}, mapObjects.polylines)); circleUpdates.add(CircleUpdates.from(const {}, mapObjects.circles)); + heatmapUpdates + .add(HeatmapUpdates.from(const {}, mapObjects.heatmaps)); tileOverlaySets.add(mapObjects.tileOverlays); } @@ -299,5 +310,6 @@ class PlatformMapStateRecorder { final List polygonUpdates = []; final List polylineUpdates = []; final List circleUpdates = []; + final List heatmapUpdates = []; final List> tileOverlaySets = >[]; } diff --git a/packages/google_maps_flutter/google_maps_flutter/test/heatmap_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/heatmap_updates_test.dart new file mode 100644 index 000000000000..b9cb250c8564 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/test/heatmap_updates_test.dart @@ -0,0 +1,265 @@ +// 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/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'fake_google_maps_flutter_platform.dart'; + +Widget _mapWithHeatmaps(Set heatmaps) { + return Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + heatmaps: heatmaps, + ), + ); +} + +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)) +]; + +void main() { + late FakeGoogleMapsFlutterPlatform platform; + + setUp(() { + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; + }); + + testWidgets('Initializing a heatmap', (WidgetTester tester) async { + const Heatmap h1 = Heatmap( + heatmapId: HeatmapId('heatmap_1'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ); + await tester.pumpWidget(_mapWithHeatmaps({h1})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.heatmapUpdates.last.heatmapsToAdd.length, 1); + + final Heatmap initializedHeatmap = + map.heatmapUpdates.last.heatmapsToAdd.first; + expect(initializedHeatmap, equals(h1)); + expect(map.heatmapUpdates.last.heatmapIdsToRemove.isEmpty, true); + expect(map.heatmapUpdates.last.heatmapsToChange.isEmpty, true); + }); + + testWidgets('Adding a heatmap', (WidgetTester tester) async { + const Heatmap h1 = Heatmap( + heatmapId: HeatmapId('heatmap_1'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ); + const Heatmap h2 = Heatmap( + heatmapId: HeatmapId('heatmap_2'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ); + + await tester.pumpWidget(_mapWithHeatmaps({h1})); + await tester.pumpWidget(_mapWithHeatmaps({h1, h2})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.heatmapUpdates.last.heatmapsToAdd.length, 1); + + final Heatmap addedHeatmap = map.heatmapUpdates.last.heatmapsToAdd.first; + expect(addedHeatmap, equals(h2)); + + expect(map.heatmapUpdates.last.heatmapIdsToRemove.isEmpty, true); + + expect(map.heatmapUpdates.last.heatmapsToChange.isEmpty, true); + }); + + testWidgets('Removing a heatmap', (WidgetTester tester) async { + const Heatmap h1 = Heatmap( + heatmapId: HeatmapId('heatmap_1'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ); + + await tester.pumpWidget(_mapWithHeatmaps({h1})); + await tester.pumpWidget(_mapWithHeatmaps({})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.heatmapUpdates.last.heatmapIdsToRemove.length, 1); + expect( + map.heatmapUpdates.last.heatmapIdsToRemove.first, equals(h1.heatmapId)); + + expect(map.heatmapUpdates.last.heatmapsToChange.isEmpty, true); + expect(map.heatmapUpdates.last.heatmapsToAdd.isEmpty, true); + }); + + testWidgets('Updating a heatmap', (WidgetTester tester) async { + const Heatmap h1 = Heatmap( + heatmapId: HeatmapId('heatmap_1'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ); + const Heatmap h2 = Heatmap( + heatmapId: HeatmapId('heatmap_1'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(10), + ); + + await tester.pumpWidget(_mapWithHeatmaps({h1})); + await tester.pumpWidget(_mapWithHeatmaps({h2})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.heatmapUpdates.last.heatmapsToChange.length, 1); + expect(map.heatmapUpdates.last.heatmapsToChange.first, equals(h2)); + + expect(map.heatmapUpdates.last.heatmapIdsToRemove.isEmpty, true); + expect(map.heatmapUpdates.last.heatmapsToAdd.isEmpty, true); + }); + + testWidgets('Updating a heatmap', (WidgetTester tester) async { + const Heatmap h1 = Heatmap( + heatmapId: HeatmapId('heatmap_1'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ); + const Heatmap h2 = Heatmap( + heatmapId: HeatmapId('heatmap_1'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(10), + ); + + await tester.pumpWidget(_mapWithHeatmaps({h1})); + await tester.pumpWidget(_mapWithHeatmaps({h2})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.heatmapUpdates.last.heatmapsToChange.length, 1); + + final Heatmap update = map.heatmapUpdates.last.heatmapsToChange.first; + expect(update, equals(h2)); + expect(update.radius.radius, 10); + }); + + testWidgets('Multi Update', (WidgetTester tester) async { + Heatmap h1 = const Heatmap( + heatmapId: HeatmapId('heatmap_1'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ); + Heatmap h2 = const Heatmap( + heatmapId: HeatmapId('heatmap_2'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ); + final Set prev = {h1, h2}; + h1 = const Heatmap( + heatmapId: HeatmapId('heatmap_1'), + data: _heatmapPoints, + dissipating: false, + radius: HeatmapRadius.fromPixels(20), + ); + h2 = const Heatmap( + heatmapId: HeatmapId('heatmap_2'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(10), + ); + final Set cur = {h1, h2}; + + await tester.pumpWidget(_mapWithHeatmaps(prev)); + await tester.pumpWidget(_mapWithHeatmaps(cur)); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.heatmapUpdates.last.heatmapsToChange, cur); + expect(map.heatmapUpdates.last.heatmapIdsToRemove.isEmpty, true); + expect(map.heatmapUpdates.last.heatmapsToAdd.isEmpty, true); + }); + + testWidgets('Multi Update', (WidgetTester tester) async { + Heatmap h2 = const Heatmap( + heatmapId: HeatmapId('heatmap_2'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ); + const Heatmap h3 = Heatmap( + heatmapId: HeatmapId('heatmap_3'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ); + final Set prev = {h2, h3}; + + // h1 is added, h2 is updated, h3 is removed. + const Heatmap h1 = Heatmap( + heatmapId: HeatmapId('heatmap_1'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ); + h2 = const Heatmap( + heatmapId: HeatmapId('heatmap_2'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(10), + ); + final Set cur = {h1, h2}; + + await tester.pumpWidget(_mapWithHeatmaps(prev)); + await tester.pumpWidget(_mapWithHeatmaps(cur)); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.heatmapUpdates.last.heatmapsToChange.length, 1); + expect(map.heatmapUpdates.last.heatmapsToAdd.length, 1); + expect(map.heatmapUpdates.last.heatmapIdsToRemove.length, 1); + + expect(map.heatmapUpdates.last.heatmapsToChange.first, equals(h2)); + expect(map.heatmapUpdates.last.heatmapsToAdd.first, equals(h1)); + expect( + map.heatmapUpdates.last.heatmapIdsToRemove.first, equals(h3.heatmapId)); + }); + + testWidgets('Partial Update', (WidgetTester tester) async { + const Heatmap h1 = Heatmap( + heatmapId: HeatmapId('heatmap_1'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ); + const Heatmap h2 = Heatmap( + heatmapId: HeatmapId('heatmap_2'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ); + Heatmap h3 = const Heatmap( + heatmapId: HeatmapId('heatmap_3'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ); + final Set prev = {h1, h2, h3}; + h3 = const Heatmap( + heatmapId: HeatmapId('heatmap_3'), + data: _heatmapPoints, + radius: HeatmapRadius.fromPixels(10), + ); + final Set cur = {h1, h2, h3}; + + await tester.pumpWidget(_mapWithHeatmaps(prev)); + await tester.pumpWidget(_mapWithHeatmaps(cur)); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.heatmapUpdates.last.heatmapsToChange, {h3}); + expect(map.heatmapUpdates.last.heatmapIdsToRemove.isEmpty, true); + expect(map.heatmapUpdates.last.heatmapsToAdd.isEmpty, true); + }); +}