diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index bab8412142d9..94502c151372 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.3.0 +* Adds support for heatmap layers. * Updates minimum Flutter version to 3.0. ## 2.2.3 diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/google_maps_flutter/google_maps_flutter/example/android/gradle/wrapper/gradle-wrapper.properties index b8793d3c0d69..cc5527d781a7 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart index 38a02ea0d8f1..5e02bdf0d02a 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart @@ -1161,6 +1161,257 @@ void main() { expect(tileOverlayInfo1, isNull); }, ); + + /// 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) { + expect(gradient2, isNull); + 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: 1e-7), + ); + } + + expect(gradient1.colorMapSize, gradient2.colorMapSize); + } + + void expectHeatmapEquals(Heatmap heatmap1, Heatmap heatmap2) { + expectHeatmapDataMoreOrLessEquals(heatmap1.data, heatmap2.data); + expectHeatmapGradientMoreOrLessEquals(heatmap1.gradient, heatmap2.gradient); + if (Platform.isAndroid) { + expect(heatmap1.maxIntensity, heatmap2.maxIntensity); + } + expect(heatmap1.opacity, moreOrLessEquals(heatmap2.opacity, epsilon: 1e-8)); + expect(heatmap1.radius, heatmap2.radius); + if (Platform.isIOS) { + expect(heatmap1.minimumZoomIntensity, heatmap2.minimumZoomIntensity); + expect(heatmap1.maximumZoomIntensity, heatmap2.maximumZoomIntensity); + } + } + + final Heatmap heatmap1 = Heatmap( + heatmapId: const HeatmapId('heatmap_1'), + data: const [ + 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( + const [ + 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: 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!; + + 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: heatmap1.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)); + + 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)); + 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/Flutter/AppFrameworkInfo.plist b/packages/google_maps_flutter/google_maps_flutter/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f96d4..9b41e7d87980 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile b/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile index 8df8fef0a781..b690cc71379e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' 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 343e0504134c..d0e663e40439 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 @@ -3,14 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 4510D964F3B1259FEDD3ABA6 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */; }; - 4A097997B7B27CE82FFC3AB8 /* libPods-RunnerUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DC8ED0578E8D540BBDA17645 /* libPods-RunnerUITests.a */; }; 6851F3562835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6851F3552835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m */; }; 68E4726A2836FF0C00BDDDAC /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68E472692836FF0C00BDDDAC /* MapKit.framework */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; @@ -21,7 +20,6 @@ 982F2A6C27BADE17003C81F4 /* PartiallyMockedMapView.m in Sources */ = {isa = PBXBuildFile; fileRef = 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */; }; F7151F13265D7ED70028CB91 /* GoogleMapsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */; }; F7151F21265D7EE50028CB91 /* GoogleMapsUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F20265D7EE50028CB91 /* GoogleMapsUITests.m */; }; - FC8F35FC8CD533B128950487 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,8 +58,6 @@ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 6851F3552835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTGoogleMapJSONConversionsConversionTests.m; sourceTree = ""; }; 68E472692836FF0C00BDDDAC /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.0.sdk/System/iOSSupport/System/Library/Frameworks/MapKit.framework; sourceTree = DEVELOPER_DIR; }; - 6AC1E6095B09DE4B02ECF64E /* Pods-RunnerUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerUITests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerUITests/Pods-RunnerUITests.release.xcconfig"; sourceTree = ""; }; - 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; @@ -77,11 +73,7 @@ 982F2A6A27BADE17003C81F4 /* PartiallyMockedMapView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PartiallyMockedMapView.h; sourceTree = ""; }; 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PartiallyMockedMapView.m; sourceTree = ""; }; B7AFC65E3DD5AC60D834D83D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - DC8ED0578E8D540BBDA17645 /* libPods-RunnerUITests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerUITests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - DDDAC1342ABDF2F125577581 /* Pods-RunnerUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerUITests/Pods-RunnerUITests.debug.xcconfig"; sourceTree = ""; }; - E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; EA0E91726245EDC22B97E8B9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; F7151F10265D7ED70028CB91 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleMapsTests.m; sourceTree = ""; }; F7151F14265D7ED70028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -104,7 +96,6 @@ buildActionMask = 2147483647; files = ( 68E4726A2836FF0C00BDDDAC /* MapKit.framework in Frameworks */, - FC8F35FC8CD533B128950487 /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -112,7 +103,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4A097997B7B27CE82FFC3AB8 /* libPods-RunnerUITests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -124,8 +114,6 @@ children = ( 68E472692836FF0C00BDDDAC /* MapKit.framework */, 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */, - F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */, - DC8ED0578E8D540BBDA17645 /* libPods-RunnerUITests.a */, ); name = Frameworks; sourceTree = ""; @@ -193,10 +181,6 @@ children = ( B7AFC65E3DD5AC60D834D83D /* Pods-Runner.debug.xcconfig */, EA0E91726245EDC22B97E8B9 /* Pods-Runner.release.xcconfig */, - E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */, - 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */, - DDDAC1342ABDF2F125577581 /* Pods-RunnerUITests.debug.xcconfig */, - 6AC1E6095B09DE4B02ECF64E /* Pods-RunnerUITests.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -251,7 +235,6 @@ isa = PBXNativeTarget; buildConfigurationList = F7151F19265D7ED70028CB91 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - D067548A17DC238B80D2BD12 /* [CP] Check Pods Manifest.lock */, F7151F0C265D7ED70028CB91 /* Sources */, F7151F0D265D7ED70028CB91 /* Frameworks */, F7151F0E265D7ED70028CB91 /* Resources */, @@ -270,7 +253,6 @@ isa = PBXNativeTarget; buildConfigurationList = F7151F25265D7EE50028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; buildPhases = ( - BD39F60794E9A0264D5D3752 /* [CP] Check Pods Manifest.lock */, F7151F1A265D7EE50028CB91 /* Sources */, F7151F1B265D7EE50028CB91 /* Frameworks */, F7151F1C265D7EE50028CB91 /* Resources */, @@ -291,7 +273,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1320; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -360,6 +342,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -392,6 +375,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -422,50 +406,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; - BD39F60794E9A0264D5D3752 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerUITests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - D067548A17DC238B80D2BD12 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -581,7 +521,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -632,7 +572,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -652,7 +592,10 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -674,7 +617,10 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -686,13 +632,16 @@ }; F7151F17265D7ED70028CB91 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -702,13 +651,16 @@ }; F7151F18265D7ED70028CB91 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -718,11 +670,14 @@ }; F7151F26265D7EE50028CB91 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = DDDAC1342ABDF2F125577581 /* Pods-RunnerUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -732,11 +687,14 @@ }; F7151F27265D7EE50028CB91 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6AC1E6095B09DE4B02ECF64E /* Pods-RunnerUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c983bfc640ff..a60a46be23c1 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + 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..af26fad930b1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/heatmap.dart @@ -0,0 +1,163 @@ +// 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({Key? key}) : super(key: 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) { + 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: HeatmapGradient( + const [ + // Web needs a first color with 0 alpha + if (kIsWeb) + HeatmapGradientColor( + Color.fromARGB(0, 0, 255, 255), + 0, + ), + 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 behaves differently on web and Android/iOS. + radius: kIsWeb + ? 10 + : defaultTargetPlatform == TargetPlatform.android + ? 20 + : 40, + ) + }), + ), + ), + 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 60d4fdd95dcf..ca9891f7e744 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 @@ -7,6 +7,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'; @@ -39,6 +40,7 @@ final List _allPages = [ const SnapshotPage(), const LiteModePage(), const TileOverlayPage(), + const HeatmapPage(), ]; /// MapsDemo is the Main Application. diff --git a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml index 5813d42e617e..6c9368fb1472 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -18,8 +18,12 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - google_maps_flutter_android: ^2.1.10 - google_maps_flutter_platform_interface: ^2.2.1 + google_maps_flutter_android: + path: ../../google_maps_flutter_android + google_maps_flutter_platform_interface: + path: ../../google_maps_flutter_platform_interface + google_maps_flutter_web: + path: ../../google_maps_flutter_web dev_dependencies: build_runner: ^2.1.10 @@ -35,3 +39,12 @@ flutter: uses-material-design: true assets: - assets/ + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_android: + path: ../../google_maps_flutter_android + google_maps_flutter_ios: + path: ../../google_maps_flutter_ios + google_maps_flutter_platform_interface: + path: ../../google_maps_flutter_platform_interface diff --git a/packages/google_maps_flutter/google_maps_flutter/example/web/favicon.png b/packages/google_maps_flutter/google_maps_flutter/example/web/favicon.png new file mode 100644 index 000000000000..8aaa46ac1ae2 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter/example/web/favicon.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter/example/web/icons/Icon-192.png b/packages/google_maps_flutter/google_maps_flutter/example/web/icons/Icon-192.png new file mode 100644 index 000000000000..b749bfef0747 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter/example/web/icons/Icon-192.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter/example/web/icons/Icon-512.png b/packages/google_maps_flutter/google_maps_flutter/example/web/icons/Icon-512.png new file mode 100644 index 000000000000..88cfd48dff11 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter/example/web/icons/Icon-512.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter/example/web/icons/Icon-maskable-192.png b/packages/google_maps_flutter/google_maps_flutter/example/web/icons/Icon-maskable-192.png new file mode 100644 index 000000000000..eb9b4d76e525 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter/example/web/icons/Icon-maskable-192.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter/example/web/icons/Icon-maskable-512.png b/packages/google_maps_flutter/google_maps_flutter/example/web/icons/Icon-maskable-512.png new file mode 100644 index 000000000000..d69c56691fbd Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter/example/web/icons/Icon-maskable-512.png differ 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 new file mode 100644 index 000000000000..78e8d779cc77 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/web/index.html @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + google_maps_flutter_example + + + + + + + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter/example/web/manifest.json b/packages/google_maps_flutter/google_maps_flutter/example/web/manifest.json new file mode 100644 index 000000000000..8753cd239ed1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "google_maps_flutter_example", + "short_name": "google_maps_flutter_example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} 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 a4be120b2117..763fbf233a00 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 @@ -28,10 +28,15 @@ export 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf Cap, Circle, CircleId, + Heatmap, + HeatmapGradient, + HeatmapGradientColor, + HeatmapId, InfoWindow, JointType, LatLng, LatLngBounds, + WeightedLatLng, MapStyleException, MapType, Marker, 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 cd3d0781e471..ce41d55be9d5 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 @@ -141,6 +141,18 @@ 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) { + assert(heatmapUpdates != null); + 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 1f7871068cab..29b8bbbdd8a1 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 @@ -117,6 +117,7 @@ class GoogleMap extends StatefulWidget { this.polygons = const {}, this.polylines = const {}, this.circles = const {}, + this.heatmaps = const {}, this.onCameraMoveStarted, this.tileOverlays = const {}, this.onCameraMove, @@ -195,6 +196,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; @@ -298,6 +302,7 @@ class _GoogleMapState extends State { Map _polygons = {}; Map _polylines = {}; Map _circles = {}; + Map _heatmaps = {}; late MapConfiguration _mapConfiguration; @override @@ -317,6 +322,7 @@ class _GoogleMapState extends State { polygons: widget.polygons, polylines: widget.polylines, circles: widget.circles, + heatmaps: widget.heatmaps, ), mapConfiguration: _mapConfiguration, ); @@ -330,6 +336,7 @@ class _GoogleMapState extends State { _polygons = keyByPolygonId(widget.polygons); _polylines = keyByPolylineId(widget.polylines); _circles = keyByCircleId(widget.circles); + _heatmaps = keyByHeatmapId(widget.heatmaps); } @override @@ -351,6 +358,7 @@ class _GoogleMapState extends State { _updatePolygons(); _updatePolylines(); _updateCircles(); + _updateHeatmaps(); _updateTileOverlays(); } @@ -398,6 +406,15 @@ class _GoogleMapState extends State { _circles = keyByCircleId(widget.circles); } + Future _updateHeatmaps() async { + final GoogleMapController controller = await _controller.future; + // ignore: unawaited_futures + controller._updateHeatmaps( + HeatmapUpdates.from(_heatmaps.values.toSet(), widget.heatmaps), + ); + _heatmaps = keyByHeatmapId(widget.heatmaps); + } + Future _updateTileOverlays() async { final GoogleMapController controller = await _controller.future; // ignore: unawaited_futures diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/serialization.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/serialization.dart new file mode 100644 index 000000000000..4eb635cb6f8c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/serialization.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +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, + '${updates.objectName}sToAdd', + updates.objectsToAdd.map(serialize), + ); + _addIfNonNull( + json, + '${updates.objectName}sToChange', + updates.objectsToChange.map(serialize), + ); + _addIfNonNull( + json, + '${updates.objectName}IdsToRemove', + updates.objectIdsToRemove + .map((MapsObjectId m) => m.value) + .toList(), + ); + + return json; +} + +/// Serialize [Heatmap] +Object serializeHeatmap(Heatmap heatmap) { + final Map json = {}; + + _addIfNonNull(json, 'heatmapId', heatmap.heatmapId.value); + _addIfNonNull( + json, + 'data', + heatmap.data.map(serializeWeightedLatLng).toList(), + ); + _addIfNonNull(json, 'dissipating', heatmap.dissipating); + + final HeatmapGradient? gradient = heatmap.gradient; + if (gradient != null) { + _addIfNonNull(json, 'gradient', serializeHeatmapGradient(gradient)); + } + _addIfNonNull(json, 'maxIntensity', heatmap.maxIntensity); + _addIfNonNull(json, 'opacity', heatmap.opacity); + _addIfNonNull(json, 'radius', heatmap.radius); + _addIfNonNull(json, 'minimumZoomIntensity', heatmap.minimumZoomIntensity); + _addIfNonNull(json, 'maximumZoomIntensity', 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, + 'colors', + gradient.colors.map((HeatmapGradientColor e) => e.color.value).toList(), + ); + _addIfNonNull( + json, + 'startPoints', + gradient.colors.map((HeatmapGradientColor e) => e.startPoint).toList(), + ); + _addIfNonNull(json, 'colorMapSize', 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['colors']! as List) + .whereType() + .map((int e) => Color(e)) + .toList(); + final List startPoints = + (map['startPoints']! 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['colorMapSize'] as int? ?? 256, + ); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 0771314b9e44..e42de3e6428e 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/plugins/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.2.3 +version: 2.3.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -28,3 +28,13 @@ dev_dependencies: sdk: flutter plugin_platform_interface: ^2.0.0 stream_transform: ^2.0.0 + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_android: + path: ../google_maps_flutter_android + google_maps_flutter_ios: + path: ../google_maps_flutter_ios + google_maps_flutter_platform_interface: + path: ../google_maps_flutter_platform_interface diff --git a/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart b/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart index 2c6aba1bb0ba..9660eb8fbee2 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart @@ -7,6 +7,7 @@ import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter/src/serialization.dart'; class FakePlatformGoogleMap { FakePlatformGoogleMap(int id, Map params) @@ -21,6 +22,7 @@ class FakePlatformGoogleMap { updatePolygons(params); updatePolylines(params); updateCircles(params); + updateHeatmaps(params); updateTileOverlays(Map.castFrom(params)); } @@ -86,6 +88,12 @@ class FakePlatformGoogleMap { Set circlesToChange = {}; + Set heatmapIdsToRemove = {}; + + Set heatmapsToAdd = {}; + + Set heatmapsToChange = {}; + Set tileOverlayIdsToRemove = {}; Set tileOverlaysToAdd = {}; @@ -115,6 +123,9 @@ class FakePlatformGoogleMap { case 'circles#update': updateCircles(call.arguments as Map?); return Future.sync(() {}); + case 'heatmaps#update': + updateHeatmaps(call.arguments as Map?); + return Future.sync(() {}); default: return Future.sync(() {}); } @@ -292,6 +303,16 @@ class FakePlatformGoogleMap { circlesToChange = _deserializeCircles(circleUpdates['circlesToChange']); } + void updateHeatmaps(Map? heatmapUpdates) { + if (heatmapUpdates == null) { + return; + } + heatmapsToAdd = _deserializeHeatmaps(heatmapUpdates['heatmapsToAdd']); + heatmapIdsToRemove = _deserializeHeatmapIds( + heatmapUpdates['heatmapIdsToRemove'] as List?); + heatmapsToChange = _deserializeHeatmaps(heatmapUpdates['heatmapsToChange']); + } + void updateTileOverlays(Map updateTileOverlayUpdates) { if (updateTileOverlayUpdates == null) { return; @@ -350,6 +371,78 @@ class FakePlatformGoogleMap { return result; } + Set _deserializeHeatmapIds(List? heatmapIds) { + if (heatmapIds == null) { + return {}; + } + return heatmapIds + .map( + (dynamic heatmapId) => HeatmapId(heatmapId as String), + ) + .toSet(); + } + + Set _deserializeHeatmaps(dynamic heatmaps) { + if (heatmaps == null) { + return {}; + } + final List heatmapsData = heatmaps as List; + final Set result = {}; + for (final Map heatmapData + in heatmapsData.cast>()) { + final String heatmapId = heatmapData['heatmapId'] as String; + + final List dataData = heatmapData['data'] as List; + final List data = dataData + .map(deserializeWeightedLatLng) + .whereType() + .toList(); + + final bool dissipating = heatmapData['dissipating'] as bool; + + final Map? gradientData = + heatmapData['gradient'] as Map?; + final HeatmapGradient? gradient; + if (gradientData != null) { + final List colors = (gradientData['colors'] as List) + .cast() + .map((int e) => Color(e)) + .toList(); + final List startPoints = + (gradientData['startPoints'] as List) + .cast() + .toList(); + + final List gradientColors = + []; + for (int i = 0; i < colors.length; i++) { + gradientColors.add(HeatmapGradientColor(colors[i], startPoints[i])); + } + + gradient = HeatmapGradient(gradientColors, + colorMapSize: gradientData['colorMapSize'] as int); + } else { + gradient = null; + } + + final double? maxIntensity = heatmapData['maxIntensity'] as double?; + final double opacity = heatmapData['opacity'] as double; + final int radius = heatmapData['radius'] as int; + + result.add(Heatmap( + heatmapId: HeatmapId(heatmapId), + data: data, + dissipating: dissipating, + gradient: gradient, + maxIntensity: maxIntensity, + opacity: opacity, + radius: radius, + )); + } + + return result; + } + Set _deserializeTileOverlayIds(List? tileOverlayIds) { if (tileOverlayIds == null || tileOverlayIds.isEmpty) { return {}; 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..24205cadccb3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/test/heatmap_updates_test.dart @@ -0,0 +1,179 @@ +// 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/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'fake_maps_controllers.dart'; + +Widget _mapWithHeatmaps(Set heatmaps) { + return Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + heatmaps: heatmaps, + ), + ); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final FakePlatformViewsController fakePlatformViewsController = + FakePlatformViewsController(); + + setUpAll(() { + SystemChannels.platform_views.setMockMethodCallHandler( + fakePlatformViewsController.fakePlatformViewsMethodHandler); + }); + + setUp(() { + fakePlatformViewsController.reset(); + }); + + testWidgets('Initializing a heatmap', (WidgetTester tester) async { + const Heatmap c1 = Heatmap(heatmapId: HeatmapId('heatmap_1')); + await tester.pumpWidget(_mapWithHeatmaps({c1})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.heatmapsToAdd.length, 1); + + final Heatmap initializedHeatmap = platformGoogleMap.heatmapsToAdd.first; + expect(initializedHeatmap, equals(c1)); + expect(platformGoogleMap.heatmapIdsToRemove.isEmpty, true); + expect(platformGoogleMap.heatmapsToChange.isEmpty, true); + }); + + testWidgets('Adding a heatmap', (WidgetTester tester) async { + const Heatmap c1 = Heatmap(heatmapId: HeatmapId('heatmap_1')); + const Heatmap c2 = Heatmap(heatmapId: HeatmapId('heatmap_2')); + + await tester.pumpWidget(_mapWithHeatmaps({c1})); + await tester.pumpWidget(_mapWithHeatmaps({c1, c2})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.heatmapsToAdd.length, 1); + + final Heatmap addedHeatmap = platformGoogleMap.heatmapsToAdd.first; + expect(addedHeatmap, equals(c2)); + + expect(platformGoogleMap.heatmapIdsToRemove.isEmpty, true); + + expect(platformGoogleMap.heatmapsToChange.isEmpty, true); + }); + + testWidgets('Removing a heatmap', (WidgetTester tester) async { + const Heatmap c1 = Heatmap(heatmapId: HeatmapId('heatmap_1')); + + await tester.pumpWidget(_mapWithHeatmaps({c1})); + await tester.pumpWidget(_mapWithHeatmaps({})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.heatmapIdsToRemove.length, 1); + expect(platformGoogleMap.heatmapIdsToRemove.first, equals(c1.heatmapId)); + + expect(platformGoogleMap.heatmapsToChange.isEmpty, true); + expect(platformGoogleMap.heatmapsToAdd.isEmpty, true); + }); + + testWidgets('Updating a heatmap', (WidgetTester tester) async { + const Heatmap c1 = Heatmap(heatmapId: HeatmapId('heatmap_1')); + const Heatmap c2 = Heatmap(heatmapId: HeatmapId('heatmap_1'), radius: 10); + + await tester.pumpWidget(_mapWithHeatmaps({c1})); + await tester.pumpWidget(_mapWithHeatmaps({c2})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.heatmapsToChange.length, 1); + expect(platformGoogleMap.heatmapsToChange.first, equals(c2)); + + expect(platformGoogleMap.heatmapIdsToRemove.isEmpty, true); + expect(platformGoogleMap.heatmapsToAdd.isEmpty, true); + }); + + testWidgets('Updating a heatmap', (WidgetTester tester) async { + const Heatmap c1 = Heatmap(heatmapId: HeatmapId('heatmap_1')); + const Heatmap c2 = Heatmap(heatmapId: HeatmapId('heatmap_1'), radius: 10); + + await tester.pumpWidget(_mapWithHeatmaps({c1})); + await tester.pumpWidget(_mapWithHeatmaps({c2})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.heatmapsToChange.length, 1); + + final Heatmap update = platformGoogleMap.heatmapsToChange.first; + expect(update, equals(c2)); + expect(update.radius, 10); + }); + + testWidgets('Multi Update', (WidgetTester tester) async { + Heatmap c1 = const Heatmap(heatmapId: HeatmapId('heatmap_1')); + Heatmap c2 = const Heatmap(heatmapId: HeatmapId('heatmap_2')); + final Set prev = {c1, c2}; + c1 = const Heatmap(heatmapId: HeatmapId('heatmap_1'), dissipating: false); + c2 = const Heatmap(heatmapId: HeatmapId('heatmap_2'), radius: 10); + final Set cur = {c1, c2}; + + await tester.pumpWidget(_mapWithHeatmaps(prev)); + await tester.pumpWidget(_mapWithHeatmaps(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.heatmapsToChange, cur); + expect(platformGoogleMap.heatmapIdsToRemove.isEmpty, true); + expect(platformGoogleMap.heatmapsToAdd.isEmpty, true); + }); + + testWidgets('Multi Update', (WidgetTester tester) async { + Heatmap c2 = const Heatmap(heatmapId: HeatmapId('heatmap_2')); + const Heatmap c3 = Heatmap(heatmapId: HeatmapId('heatmap_3')); + final Set prev = {c2, c3}; + + // c1 is added, c2 is updated, c3 is removed. + const Heatmap c1 = Heatmap(heatmapId: HeatmapId('heatmap_1')); + c2 = const Heatmap(heatmapId: HeatmapId('heatmap_2'), radius: 10); + final Set cur = {c1, c2}; + + await tester.pumpWidget(_mapWithHeatmaps(prev)); + await tester.pumpWidget(_mapWithHeatmaps(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.heatmapsToChange.length, 1); + expect(platformGoogleMap.heatmapsToAdd.length, 1); + expect(platformGoogleMap.heatmapIdsToRemove.length, 1); + + expect(platformGoogleMap.heatmapsToChange.first, equals(c2)); + expect(platformGoogleMap.heatmapsToAdd.first, equals(c1)); + expect(platformGoogleMap.heatmapIdsToRemove.first, equals(c3.heatmapId)); + }); + + testWidgets('Partial Update', (WidgetTester tester) async { + const Heatmap c1 = Heatmap(heatmapId: HeatmapId('heatmap_1')); + const Heatmap c2 = Heatmap(heatmapId: HeatmapId('heatmap_2')); + Heatmap c3 = const Heatmap(heatmapId: HeatmapId('heatmap_3')); + final Set prev = {c1, c2, c3}; + c3 = const Heatmap(heatmapId: HeatmapId('heatmap_3'), radius: 10); + final Set cur = {c1, c2, c3}; + + await tester.pumpWidget(_mapWithHeatmaps(prev)); + await tester.pumpWidget(_mapWithHeatmaps(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.heatmapsToChange, {c3}); + expect(platformGoogleMap.heatmapIdsToRemove.isEmpty, true); + expect(platformGoogleMap.heatmapsToAdd.isEmpty, true); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart index 49b64b1b4b2a..670f9dc75125 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart @@ -121,6 +121,12 @@ class TestGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { required int mapId, }) async {} + @override + Future updateHeatmaps( + HeatmapUpdates heatmapUpdates, { + required int mapId, + }) async {} + @override Future updateTileOverlays({ required Set newTileOverlays, diff --git a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md index 68b9f677e2db..d025ba364015 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md @@ -1,3 +1,6 @@ +## 2.5.0 +* Adds support for heatmap layers. + ## 2.4.5 * Fixes Initial padding not working when map has not been created yet. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/README.md b/packages/google_maps_flutter/google_maps_flutter_android/README.md index e07b0bc8d406..608f4b427e88 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/README.md @@ -70,6 +70,16 @@ AndroidMapRenderer mapRenderer = AndroidMapRenderer.platformDefault; Available values are `AndroidMapRenderer.latest`, `AndroidMapRenderer.legacy`, `AndroidMapRenderer.platformDefault`. Note that getting the requested renderer as a response is not guaranteed. +## Supported Heatmap Options + +| Field | Supported | +| ---------------------------- | :-------: | +| Heatmap.dissipating | x | +| Heatmap.maxIntensity | ✓ | +| Heatmap.minimumZoomIntensity | x | +| Heatmap.maximumZoomIntensity | x | +| HeatmapGradient.colorMapSize | ✓ | + [1]: https://pub.dev/packages/google_maps_flutter [2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin [3]: https://docs.flutter.dev/development/platform-integration/android/platform-views diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle index 6f8d3060a9cf..d1ab8fab9526 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle @@ -35,6 +35,7 @@ android { dependencies { implementation "androidx.annotation:annotation:1.1.0" implementation 'com.google.android.gms:play-services-maps:18.1.0' + implementation 'com.google.maps.android:android-maps-utils:2.3.0' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:rules:1.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index 22c8f4d24be6..a30bfca03d7d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java @@ -25,12 +25,18 @@ import com.google.android.gms.maps.model.RoundCap; import com.google.android.gms.maps.model.SquareCap; import com.google.android.gms.maps.model.Tile; +import com.google.maps.android.heatmaps.Gradient; +import com.google.maps.android.heatmaps.HeatmapTileProvider; +import com.google.maps.android.heatmaps.WeightedLatLng; +import com.google.maps.android.projection.SphericalMercatorProjection; import io.flutter.view.FlutterMain; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; /** Conversions between JSON-like values and GoogleMaps data types. */ class Convert { @@ -226,6 +232,19 @@ static LatLng toLatLng(Object o) { return new LatLng(toDouble(data.get(0)), toDouble(data.get(1))); } + static WeightedLatLng toWeightedLatLng(Object o) { + final List data = toList(o); + return new WeightedLatLng(toLatLng(data.get(0)), toDouble(data.get(1))); + } + + private static final SphericalMercatorProjection sProjection = new SphericalMercatorProjection(1); + + static Object weightedLatLngToJson(WeightedLatLng weightedLatLng) { + return Arrays.asList( + latLngToJson(sProjection.toLatLng(weightedLatLng.getPoint())), + weightedLatLng.getIntensity()); + } + static Point toPoint(Object o) { Object x = toMap(o).get("x"); Object y = toMap(o).get("y"); @@ -593,8 +612,69 @@ static String interpretCircleOptions(Object o, CircleOptionsSink sink) { } } + static String interpretHeatmapOptions(Object o, HeatmapOptionsSink sink) { + final Map data = toMap(o); + final Object rawWeightedData = data.get("data"); + if (rawWeightedData != null) { + sink.setWeightedData(toWeightedData(rawWeightedData)); + } + final Object gradient = data.get("gradient"); + if (gradient != null) { + sink.setGradient(toGradient(gradient)); + } + final Object maxIntensity = data.get("maxIntensity"); + if (maxIntensity != null) { + sink.setMaxIntensity(toDouble(maxIntensity)); + } + final Object opacity = data.get("opacity"); + if (opacity != null) { + sink.setOpacity(toDouble(opacity)); + } + final Object radius = data.get("radius"); + if (radius != null) { + sink.setRadius(toInt(radius)); + } + final String heatmapId = (String) data.get("heatmapId"); + if (heatmapId == null) { + throw new IllegalArgumentException("heatmapId was null"); + } else { + return heatmapId; + } + } + + static Map heatmapToJson(HeatmapTileProvider heatmap) + throws NoSuchFieldException, IllegalAccessException { + final Field dataField = HeatmapTileProvider.class.getDeclaredField("mData"); + final Field gradientField = HeatmapTileProvider.class.getDeclaredField("mGradient"); + final Field maxIntensityField = + HeatmapTileProvider.class.getDeclaredField("mCustomMaxIntensity"); + final Field opacityField = HeatmapTileProvider.class.getDeclaredField("mOpacity"); + final Field radiusField = HeatmapTileProvider.class.getDeclaredField("mRadius"); + + dataField.setAccessible(true); + gradientField.setAccessible(true); + maxIntensityField.setAccessible(true); + opacityField.setAccessible(true); + radiusField.setAccessible(true); + + final List data = (List) dataField.get(heatmap); + final Gradient gradient = (Gradient) gradientField.get(heatmap); + final double maxIntensity = (double) maxIntensityField.get(heatmap); + final double opacity = (double) opacityField.get(heatmap); + final int radius = (int) radiusField.get(heatmap); + + Map heatmapInfo = new HashMap<>(); + heatmapInfo.put("data", Convert.weightedDataToJson(Objects.requireNonNull(data))); + heatmapInfo.put("gradient", gradientToJson(gradient)); + heatmapInfo.put("maxIntensity", maxIntensityField.get(heatmap)); + heatmapInfo.put("opacity", opacityField.get(heatmap)); + heatmapInfo.put("radius", radiusField.get(heatmap)); + + return heatmapInfo; + } + @VisibleForTesting - static List toPoints(Object o) { + private static List toPoints(Object o) { final List data = toList(o); final List points = new ArrayList<>(data.size()); @@ -605,6 +685,55 @@ static List toPoints(Object o) { return points; } + private static List toWeightedData(Object o) { + final List data = toList(o); + final List weightedData = new ArrayList<>(data.size()); + + for (Object rawWeightedPoint : data) { + weightedData.add(toWeightedLatLng(rawWeightedPoint)); + } + return weightedData; + } + + private static Object weightedDataToJson(List weightedData) { + final List data = new ArrayList<>(weightedData.size()); + + for (WeightedLatLng weightedLatLng : weightedData) { + data.add(weightedLatLngToJson(weightedLatLng)); + } + return data; + } + + private static Gradient toGradient(Object o) { + final Map data = toMap(o); + + final List colorData = toList(data.get("colors")); + assert colorData != null; + final int[] colors = new int[colorData.size()]; + for (int i = 0; i < colorData.size(); i++) { + colors[i] = toInt(colorData.get(i)); + } + + final List startPointData = toList(data.get("startPoints")); + assert startPointData != null; + final float[] startPoints = new float[startPointData.size()]; + for (int i = 0; i < startPointData.size(); i++) { + startPoints[i] = toFloat(startPointData.get(i)); + } + + final int colorMapSize = toInt(data.get("colorMapSize")); + + return new Gradient(colors, startPoints, colorMapSize); + } + + private static Object gradientToJson(Gradient gradient) { + final Map data = new HashMap<>(); + data.put("colors", gradient.mColors); + data.put("startPoints", gradient.mStartPoints); + data.put("colorMapSize", gradient.mColorMapSize); + return data; + } + private static List> toHoles(Object o) { final List data = toList(o); final List> holes = new ArrayList<>(data.size()); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java index ad5179a69a45..d3cc7c6c8f58 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java @@ -25,6 +25,7 @@ class GoogleMapBuilder implements GoogleMapOptionsSink { private Object initialPolygons; private Object initialPolylines; private Object initialCircles; + private Object initialHeatmaps; private List> initialTileOverlays; private Rect padding = new Rect(0, 0, 0, 0); @@ -46,6 +47,7 @@ GoogleMapController build( controller.setInitialPolygons(initialPolygons); controller.setInitialPolylines(initialPolylines); controller.setInitialCircles(initialCircles); + controller.setInitialHeatmaps(initialHeatmaps); controller.setPadding(padding.top, padding.left, padding.bottom, padding.right); controller.setInitialTileOverlays(initialTileOverlays); return controller; @@ -170,6 +172,11 @@ public void setInitialCircles(Object initialCircles) { this.initialCircles = initialCircles; } + @Override + public void setInitialHeatmaps(Object initialHeatmaps) { + this.initialHeatmaps = initialHeatmaps; + } + @Override public void setInitialTileOverlays(List> initialTileOverlays) { this.initialTileOverlays = initialTileOverlays; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index a57cd1a34c97..98c3b2a29dc6 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -78,11 +78,13 @@ final class GoogleMapController private final PolygonsController polygonsController; private final PolylinesController polylinesController; private final CirclesController circlesController; + private final HeatmapsController heatmapsController; private final TileOverlaysController tileOverlaysController; private List initialMarkers; private List initialPolygons; private List initialPolylines; private List initialCircles; + private List initialHeatmaps; private List> initialTileOverlays; @VisibleForTesting List initialPadding; @@ -105,6 +107,7 @@ final class GoogleMapController this.polygonsController = new PolygonsController(methodChannel, density); this.polylinesController = new PolylinesController(methodChannel, density); this.circlesController = new CirclesController(methodChannel, density); + this.heatmapsController = new HeatmapsController(); this.tileOverlaysController = new TileOverlaysController(methodChannel); } @@ -204,11 +207,13 @@ public void onMapReady(GoogleMap googleMap) { polygonsController.setGoogleMap(googleMap); polylinesController.setGoogleMap(googleMap); circlesController.setGoogleMap(googleMap); + heatmapsController.setGoogleMap(googleMap); tileOverlaysController.setGoogleMap(googleMap); updateInitialMarkers(); updateInitialPolygons(); updateInitialPolylines(); updateInitialCircles(); + updateInitialHeatmaps(); updateInitialTileOverlays(); if (initialPadding != null && initialPadding.size() == 4) { setPadding( @@ -376,6 +381,17 @@ public void onSnapshotReady(Bitmap bitmap) { result.success(null); break; } + case "heatmaps#update": + { + List heatmapsToAdd = call.argument("heatmapsToAdd"); + heatmapsController.addHeatmaps(heatmapsToAdd); + List heatmapsToChange = call.argument("heatmapsToChange"); + heatmapsController.changeHeatmaps(heatmapsToChange); + List heatmapIdsToRemove = call.argument("heatmapIdsToRemove"); + heatmapsController.removeHeatmaps(heatmapIdsToRemove); + result.success(null); + break; + } case "map#isCompassEnabled": { result.success(googleMap.getUiSettings().isCompassEnabled()); @@ -493,6 +509,12 @@ public void onSnapshotReady(Bitmap bitmap) { result.success(tileOverlaysController.getTileOverlayInfo(tileOverlayId)); break; } + case "map#getHeatmapInfo": + { + String heatmapId = call.argument("heatmapId"); + result.success(heatmapsController.getHeatmapInfo(heatmapId)); + break; + } default: result.notImplemented(); } @@ -608,17 +630,21 @@ private void setGoogleMapListener(@Nullable GoogleMapListener listener) { } // @Override - // The minimum supported version of Flutter doesn't have this method on the PlatformView interface, but the maximum + // The minimum supported version of Flutter doesn't have this method on the PlatformView + // interface, but the maximum // does. This will override it when available even with the annotation commented out. public void onInputConnectionLocked() { - // TODO(mklim): Remove this empty override once https://github.com/flutter/flutter/issues/40126 is fixed in stable. + // TODO(mklim): Remove this empty override once https://github.com/flutter/flutter/issues/40126 + // is fixed in stable. } // @Override - // The minimum supported version of Flutter doesn't have this method on the PlatformView interface, but the maximum + // The minimum supported version of Flutter doesn't have this method on the PlatformView + // interface, but the maximum // does. This will override it when available even with the annotation commented out. public void onInputConnectionUnlocked() { - // TODO(mklim): Remove this empty override once https://github.com/flutter/flutter/issues/40126 is fixed in stable. + // TODO(mklim): Remove this empty override once https://github.com/flutter/flutter/issues/40126 + // is fixed in stable. } // DefaultLifecycleObserver @@ -859,10 +885,23 @@ public void setInitialCircles(Object initialCircles) { } } + @Override + public void setInitialHeatmaps(Object initialHeatmaps) { + ArrayList heatmaps = (ArrayList) initialHeatmaps; + this.initialHeatmaps = heatmaps != null ? new ArrayList<>(heatmaps) : null; + if (googleMap != null) { + updateInitialHeatmaps(); + } + } + private void updateInitialCircles() { circlesController.addCircles(initialCircles); } + private void updateInitialHeatmaps() { + heatmapsController.addHeatmaps(initialHeatmaps); + } + @Override public void setInitialTileOverlays(List> initialTileOverlays) { this.initialTileOverlays = initialTileOverlays; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java index ffa2412f9c42..0b01d8c76ee2 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java @@ -51,6 +51,9 @@ public PlatformView create(Context context, int id, Object args) { if (params.containsKey("circlesToAdd")) { builder.setInitialCircles(params.get("circlesToAdd")); } + if (params.containsKey("heatmapsToAdd")) { + builder.setInitialHeatmaps(params.get("heatmapsToAdd")); + } if (params.containsKey("tileOverlaysToAdd")) { builder.setInitialTileOverlays((List>) params.get("tileOverlaysToAdd")); } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java index 17f0d970a4ef..5a1691afff55 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java @@ -54,5 +54,7 @@ interface GoogleMapOptionsSink { void setInitialCircles(Object initialCircles); + void setInitialHeatmaps(Object initialHeatmaps); + void setInitialTileOverlays(List> initialTileOverlays); } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapBuilder.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapBuilder.java new file mode 100644 index 000000000000..87e17dd91801 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapBuilder.java @@ -0,0 +1,47 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +import com.google.maps.android.heatmaps.Gradient; +import com.google.maps.android.heatmaps.HeatmapTileProvider; +import com.google.maps.android.heatmaps.WeightedLatLng; +import java.util.List; + +public class HeatmapBuilder implements HeatmapOptionsSink { + private final HeatmapTileProvider.Builder heatmapOptions; + + HeatmapBuilder() { + this.heatmapOptions = new HeatmapTileProvider.Builder(); + } + + HeatmapTileProvider build() { + return heatmapOptions.build(); + } + + @Override + public void setWeightedData(List weightedData) { + heatmapOptions.weightedData(weightedData); + } + + @Override + public void setGradient(Gradient gradient) { + heatmapOptions.gradient(gradient); + } + + @Override + public void setMaxIntensity(double maxIntensity) { + heatmapOptions.maxIntensity(maxIntensity); + } + + @Override + public void setOpacity(double opacity) { + heatmapOptions.opacity(opacity); + } + + @Override + public void setRadius(int radius) { + heatmapOptions.radius(radius); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapController.java new file mode 100644 index 000000000000..d3ab6167f862 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapController.java @@ -0,0 +1,63 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.TileOverlay; +import com.google.maps.android.heatmaps.Gradient; +import com.google.maps.android.heatmaps.HeatmapTileProvider; +import com.google.maps.android.heatmaps.WeightedLatLng; +import java.util.List; +import java.util.Map; + +public class HeatmapController implements HeatmapOptionsSink { + private final HeatmapTileProvider heatmap; + private final TileOverlay heatmapTileOverlay; + + HeatmapController(HeatmapTileProvider heatmap, TileOverlay heatmapTileOverlay) { + this.heatmap = heatmap; + this.heatmapTileOverlay = heatmapTileOverlay; + } + + void remove() { + heatmapTileOverlay.remove(); + } + + void clearTileCache() { + heatmapTileOverlay.clearTileCache(); + } + + Map getHeatmapInfo() { + try { + return Convert.heatmapToJson(heatmap); + } catch (Exception e) { + return null; + } + } + + @Override + public void setWeightedData(List weightedData) { + heatmap.setWeightedData(weightedData); + } + + @Override + public void setGradient(Gradient gradient) { + heatmap.setGradient(gradient); + } + + @Override + public void setMaxIntensity(double maxIntensity) { + heatmap.setMaxIntensity(maxIntensity); + } + + @Override + public void setOpacity(double opacity) { + heatmap.setOpacity(opacity); + } + + @Override + public void setRadius(int radius) { + heatmap.setRadius(radius); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapOptionsSink.java new file mode 100644 index 000000000000..54ba25a2f230 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapOptionsSink.java @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +import com.google.maps.android.heatmaps.Gradient; +import com.google.maps.android.heatmaps.WeightedLatLng; +import java.util.List; + +/** Receiver of Heatmap configuration options. */ +interface HeatmapOptionsSink { + void setWeightedData(List weightedData); + + void setGradient(Gradient gradient); + + void setMaxIntensity(double maxIntensity); + + void setOpacity(double opacity); + + void setRadius(int radius); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapsController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapsController.java new file mode 100644 index 000000000000..4d004145b831 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/HeatmapsController.java @@ -0,0 +1,112 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.TileOverlay; +import com.google.android.gms.maps.model.TileOverlayOptions; +import com.google.maps.android.heatmaps.HeatmapTileProvider; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class HeatmapsController { + private final Map heatmapIdToController; + private GoogleMap googleMap; + + HeatmapsController() { + this.heatmapIdToController = new HashMap<>(); + } + + void setGoogleMap(GoogleMap googleMap) { + this.googleMap = googleMap; + } + + void addHeatmaps(List heatmapsToAdd) { + if (heatmapsToAdd == null) { + return; + } + for (Object heatmapToAdd : heatmapsToAdd) { + addHeatmap(heatmapToAdd); + } + } + + void changeHeatmaps(List heatmapsToChange) { + if (heatmapsToChange == null) { + return; + } + for (Object heatmapToChange : heatmapsToChange) { + changeHeatmap(heatmapToChange); + } + } + + void removeHeatmaps(List heatmapIdsToRemove) { + if (heatmapIdsToRemove == null) { + return; + } + for (String heatmapId : heatmapIdsToRemove) { + if (heatmapId == null) { + continue; + } + removeHeatmap(heatmapId); + } + } + + Map getHeatmapInfo(String heatmapId) { + if (heatmapId == null) { + return null; + } + HeatmapController heatmapController = heatmapIdToController.get(heatmapId); + if (heatmapController == null) { + return null; + } + + try { + return heatmapController.getHeatmapInfo(); + } catch (Exception e) { + return null; + } + } + + private void addHeatmap(Object heatmapOptions) { + if (heatmapOptions == null) { + return; + } + HeatmapBuilder heatmapBuilder = new HeatmapBuilder(); + String heatmapId = Convert.interpretHeatmapOptions(heatmapOptions, heatmapBuilder); + + HeatmapTileProvider heatmap = heatmapBuilder.build(); + TileOverlay heatmapTileOverlay = + googleMap.addTileOverlay(new TileOverlayOptions().tileProvider(heatmap)); + HeatmapController heatmapController = new HeatmapController(heatmap, heatmapTileOverlay); + heatmapIdToController.put(heatmapId, heatmapController); + } + + private void changeHeatmap(Object heatmapOptions) { + if (heatmapOptions == null) { + return; + } + String heatmapId = getHeatmapId(heatmapOptions); + HeatmapController heatmapController = heatmapIdToController.get(heatmapId); + if (heatmapController != null) { + Convert.interpretHeatmapOptions(heatmapOptions, heatmapController); + heatmapController.clearTileCache(); + } + } + + private void removeHeatmap(String heatmapId) { + HeatmapController heatmapController = heatmapIdToController.get(heatmapId); + if (heatmapController != null) { + heatmapController.remove(); + heatmapIdToController.remove(heatmapId); + } + } + + @SuppressWarnings("unchecked") + private static String getHeatmapId(Object heatmap) { + Map heatmapMap = (Map) heatmap; + return (String) heatmapMap.get("heatmapId"); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java index 73530d1b5158..143cfc22c59e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java @@ -56,7 +56,8 @@ Tile getTile() { Convert.tileOverlayArgumentsToJson(tileOverlayId, x, y, zoom), this)); try { - // Because `methodChannel.invokeMethod` is async, we use a `countDownLatch` make it synchronized. + // Because `methodChannel.invokeMethod` is async, we use a `countDownLatch` make it + // synchronized. countDownLatch.await(); } catch (InterruptedException e) { Log.e( diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml index aa29fa99a97b..9bf31ca67abd 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml @@ -34,3 +34,8 @@ flutter: uses-material-design: true assets: - assets/ + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_platform_interface: + path: ../../google_maps_flutter_platform_interface diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart index 4e0cad78e869..afcc8ab46120 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart @@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'serialization.dart'; + /// An Android of implementation of [GoogleMapsInspectorPlatform]. @visibleForTesting class GoogleMapsInspectorAndroid extends GoogleMapsInspectorPlatform { @@ -81,6 +83,31 @@ class GoogleMapsInspectorAndroid extends GoogleMapsInspectorPlatform { ); } + @override + Future getHeatmapInfo(HeatmapId heatmapId, + {required int mapId}) async { + final Map? heatmapInfo = await _channelProvider(mapId)! + .invokeMapMethod( + 'map#getHeatmapInfo', { + 'heatmapId': heatmapId.value, + }); + if (heatmapInfo == null) { + return null; + } + + return Heatmap( + heatmapId: heatmapId, + data: (heatmapInfo['data']! as List) + .map(deserializeWeightedLatLng) + .whereType() + .toList(), + gradient: deserializeHeatmapGradient(heatmapInfo['gradient']), + maxIntensity: heatmapInfo['maxIntensity']! as double, + opacity: heatmapInfo['opacity']! as double, + radius: heatmapInfo['radius']! as int, + ); + } + @override Future isCompassEnabled({required int mapId}) async { return (await _channelProvider(mapId)! diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart index 0461b4cf71bc..4464ce464e89 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart @@ -16,6 +16,7 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf import 'package:stream_transform/stream_transform.dart'; import 'google_map_inspector_android.dart'; +import 'serialization.dart'; // TODO(stuartmorgan): Remove the dependency on platform interface toJson // methods. Channel serialization details should all be package-internal. @@ -364,6 +365,18 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { ); } + @override + Future updateHeatmaps( + HeatmapUpdates heatmapUpdates, { + required int mapId, + }) { + assert(heatmapUpdates != null); + return _channel(mapId).invokeMethod( + 'heatmaps#update', + serializeMapsObjectUpdates(heatmapUpdates, serializeHeatmap), + ); + } + @override Future updateTileOverlays({ required Set newTileOverlays, @@ -578,6 +591,7 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { 'polygonsToAdd': serializePolygonSet(mapObjects.polygons), 'polylinesToAdd': serializePolylineSet(mapObjects.polylines), 'circlesToAdd': serializeCircleSet(mapObjects.circles), + 'heatmapsToAdd': mapObjects.heatmaps.map(serializeHeatmap), 'tileOverlaysToAdd': serializeTileOverlaySet(mapObjects.tileOverlays), }; @@ -654,6 +668,7 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, + Set heatmaps = const {}, Set tileOverlays = const {}, Set>? gestureRecognizers, Map mapOptions = const {}, @@ -669,6 +684,7 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { polygons: polygons, polylines: polylines, circles: circles, + heatmaps: heatmaps, tileOverlays: tileOverlays), mapOptions: mapOptions, ); @@ -683,6 +699,7 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, + Set heatmaps = const {}, Set tileOverlays = const {}, Set>? gestureRecognizers, Map mapOptions = const {}, @@ -696,6 +713,7 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { polygons: polygons, polylines: polylines, circles: circles, + heatmaps: heatmaps, tileOverlays: tileOverlays, gestureRecognizers: gestureRecognizers, mapOptions: mapOptions, diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/serialization.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/serialization.dart new file mode 100644 index 000000000000..4eb635cb6f8c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/serialization.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +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, + '${updates.objectName}sToAdd', + updates.objectsToAdd.map(serialize), + ); + _addIfNonNull( + json, + '${updates.objectName}sToChange', + updates.objectsToChange.map(serialize), + ); + _addIfNonNull( + json, + '${updates.objectName}IdsToRemove', + updates.objectIdsToRemove + .map((MapsObjectId m) => m.value) + .toList(), + ); + + return json; +} + +/// Serialize [Heatmap] +Object serializeHeatmap(Heatmap heatmap) { + final Map json = {}; + + _addIfNonNull(json, 'heatmapId', heatmap.heatmapId.value); + _addIfNonNull( + json, + 'data', + heatmap.data.map(serializeWeightedLatLng).toList(), + ); + _addIfNonNull(json, 'dissipating', heatmap.dissipating); + + final HeatmapGradient? gradient = heatmap.gradient; + if (gradient != null) { + _addIfNonNull(json, 'gradient', serializeHeatmapGradient(gradient)); + } + _addIfNonNull(json, 'maxIntensity', heatmap.maxIntensity); + _addIfNonNull(json, 'opacity', heatmap.opacity); + _addIfNonNull(json, 'radius', heatmap.radius); + _addIfNonNull(json, 'minimumZoomIntensity', heatmap.minimumZoomIntensity); + _addIfNonNull(json, 'maximumZoomIntensity', 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, + 'colors', + gradient.colors.map((HeatmapGradientColor e) => e.color.value).toList(), + ); + _addIfNonNull( + json, + 'startPoints', + gradient.colors.map((HeatmapGradientColor e) => e.startPoint).toList(), + ); + _addIfNonNull(json, 'colorMapSize', 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['colors']! as List) + .whereType() + .map((int e) => Color(e)) + .toList(); + final List startPoints = + (map['startPoints']! 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['colorMapSize'] as int? ?? 256, + ); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml index cf8bc81e7e7c..32ee5489cafd 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_android description: Android implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.4.5 +version: 2.5.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -29,3 +29,9 @@ dev_dependencies: flutter_test: sdk: flutter plugin_platform_interface: ^2.0.0 + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_platform_interface: + path: ../google_maps_flutter_platform_interface diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md index a65523f426c1..d27d6b6fb5a7 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.2.0 +* Adds support for heatmap layers. * Updates minimum Flutter version to 3.0. ## 2.1.13 diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/README.md b/packages/google_maps_flutter/google_maps_flutter_ios/README.md index cd5d3f1b7e6e..3afca37cc029 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_ios/README.md @@ -8,5 +8,15 @@ This package is [endorsed][2], which means you can simply use `google_maps_flutter` normally. This package will be automatically included in your app when you do. +## Supported Heatmap Options + +| Field | Supported | +| ---------------------------- | :-------: | +| Heatmap.dissipating | x | +| Heatmap.maxIntensity | x | +| Heatmap.minimumZoomIntensity | ✓ | +| Heatmap.maximumZoomIntensity | ✓ | +| HeatmapGradient.colorMapSize | ✓ | + [1]: https://pub.dev/packages/google_maps_flutter [2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f96d4..9b41e7d87980 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Podfile b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Podfile index 29bfe631a3e7..39d60ec703f5 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Podfile +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/project.pbxproj index 343e0504134c..1eb52497f673 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -291,7 +291,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1320; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -581,7 +581,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -632,7 +632,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c983bfc640ff..a60a46be23c1 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/FLTGoogleMapJSONConversionsConversionTests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/FLTGoogleMapJSONConversionsConversionTests.m index bb9020d983c4..6bcf8ee2fbb4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/FLTGoogleMapJSONConversionsConversionTests.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/FLTGoogleMapJSONConversionsConversionTests.m @@ -287,4 +287,48 @@ - (void)testCameraUpdateFromChannelValueZoomTo { [classMockCameraUpdate stopMocking]; } +- (void)testWeightedLatLngFromArray { + NSArray *weightedLatLng = @[ @[ @1, @2 ], @3 ]; + + GMUWeightedLatLng *weightedLocation = + [FLTGoogleMapJSONConversions weightedLatLngFromArray:weightedLatLng]; + + // The location gets projected to different values + XCTAssertEqual([weightedLocation intensity], 3); + + weightedLatLng = @[]; + + XCTAssertThrows([FLTGoogleMapJSONConversions weightedLatLngFromArray:weightedLatLng]); +} + +- (void)testWeightedDataFromArray { + NSArray *data = @[ @[ @[ @1, @2 ], @3 ], @[ @[ @4, @5 ], @6 ] ]; + + NSArray *weightedData = + [FLTGoogleMapJSONConversions weightedDataFromArray:data]; + XCTAssertEqual([weightedData[0] intensity], 3); + XCTAssertEqual([weightedData[1] intensity], 6); +} + +- (void)testGradientFromDictionary { + NSDictionary *gradientData = @{ + @"colors" : @[ + // Color.fromARGB(255, 0, 255, 255) + @4278255615, + ], + @"startPoints" : @[ @0.6 ], + @"colorMapSize" : @200, + }; + + GMUGradient *gradient = [FLTGoogleMapJSONConversions gradientFromDictionary:gradientData]; + CGFloat red, green, blue, alpha; + [[gradient colors][0] getRed:&red green:&green blue:&blue alpha:&alpha]; + XCTAssertEqual(red, 0); + XCTAssertEqual(green, 1); + XCTAssertEqual(blue, 1); + XCTAssertEqual(alpha, 1); + XCTAssertEqualWithAccuracy([[gradient startPoints][0] doubleValue], 0.6, 0); + XCTAssertEqual([gradient mapSize], 200); +} + @end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml index ac27996fbc25..3848df6e255a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml @@ -32,3 +32,8 @@ flutter: uses-material-design: true assets: - assets/ + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_platform_interface: + path: ../../google_maps_flutter_platform_interface diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapHeatmapController.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapHeatmapController.h new file mode 100644 index 000000000000..1a1907f584d9 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapHeatmapController.h @@ -0,0 +1,29 @@ +// 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 +#import +@import GoogleMapsUtils; + +NS_ASSUME_NONNULL_BEGIN + +// Defines heatmap controllable by Flutter. +@interface FLTGoogleMapHeatmapController : NSObject +- (instancetype)initWithHeatmapTileLayer:(GMUHeatmapTileLayer *)heatmapTileLayer + mapView:(GMSMapView *)mapView + options:(NSDictionary *)options; +- (void)removeHeatmap; +- (void)clearTileCache; +@end + +@interface FLTHeatmapsController : NSObject +- (instancetype)init:(GMSMapView *)mapView; +- (void)addHeatmaps:(NSArray *)heatmapsToAdd; +- (void)changeHeatmaps:(NSArray *)heatmapsToChange; +- (void)removeHeatmapsWithIdentifiers:(NSArray *)identifiers; +- (bool)hasHeatmapWithIdentifier:(NSString *)identifier; +- (nullable NSDictionary *)heatmapInfoWithIdentifier:(NSString *)identifier; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapHeatmapController.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapHeatmapController.m new file mode 100644 index 000000000000..00f63b302b9e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapHeatmapController.m @@ -0,0 +1,176 @@ +// 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 "FLTGoogleMapHeatmapController.h" +#import "FLTGoogleMapJSONConversions.h" +@import GoogleMapsUtils; + +@interface FLTGoogleMapHeatmapController () + +@property(nonatomic, strong) GMUHeatmapTileLayer *heatmapTileLayer; +@property(nonatomic, weak) GMSMapView *mapView; + +@end + +@implementation FLTGoogleMapHeatmapController +- (instancetype)initWithHeatmapTileLayer:(GMUHeatmapTileLayer *)heatmapTileLayer + mapView:(GMSMapView *)mapView + options:(NSDictionary *)options { + self = [super init]; + if (self) { + _heatmapTileLayer = heatmapTileLayer; + _mapView = mapView; + [self interpretHeatmapOptions:options]; + } + return self; +} + +- (void)removeHeatmap { + _heatmapTileLayer.map = nil; +} + +- (void)clearTileCache { + [_heatmapTileLayer clearTileCache]; +} + +- (void)setWeightedData:(NSArray *)weightedData { + _heatmapTileLayer.weightedData = weightedData; +} + +- (void)setGradient:(GMUGradient *)gradient { + _heatmapTileLayer.gradient = gradient; +} + +- (void)setOpacity:(double)opacity { + _heatmapTileLayer.opacity = opacity; +} + +- (void)setRadius:(int)radius { + _heatmapTileLayer.radius = radius; +} + +- (void)setMinimumZoomIntensity:(int)intensity { + _heatmapTileLayer.minimumZoomIntensity = intensity; +} + +- (void)setMaximumZoomIntensity:(int)intensity { + _heatmapTileLayer.maximumZoomIntensity = intensity; +} + +- (void)setMap { + _heatmapTileLayer.map = _mapView; +} +- (void)interpretHeatmapOptions:(NSDictionary *)data { + NSArray *weightedData = data[@"data"]; + if (weightedData != nil && weightedData != (id)[NSNull null]) { + [self setWeightedData:[FLTGoogleMapJSONConversions weightedDataFromArray:weightedData]]; + } + + NSDictionary *gradient = data[@"gradient"]; + if (gradient != nil && gradient != (id)[NSNull null]) { + [self setGradient:[FLTGoogleMapJSONConversions gradientFromDictionary:gradient]]; + } + + NSNumber *opacity = data[@"opacity"]; + if (opacity != nil && opacity != (id)[NSNull null]) { + [self setOpacity:[opacity doubleValue]]; + } + + NSNumber *radius = data[@"radius"]; + if (radius != nil && radius != (id)[NSNull null]) { + [self setRadius:[radius intValue]]; + } + + NSNumber *minimumZoomIntensity = data[@"minimumZoomIntensity"]; + if (minimumZoomIntensity != nil && minimumZoomIntensity != (id)[NSNull null]) { + [self setMinimumZoomIntensity:[minimumZoomIntensity intValue]]; + } + + NSNumber *maximumZoomIntensity = data[@"maximumZoomIntensity"]; + if (maximumZoomIntensity != nil && maximumZoomIntensity != (id)[NSNull null]) { + [self setMaximumZoomIntensity:[maximumZoomIntensity intValue]]; + } + + // The map must be set each time for options to update + [self setMap]; +} +- (NSDictionary *)getHeatmapInfo { + NSMutableDictionary *options = [[NSMutableDictionary alloc] init]; + options[@"data"] = + [FLTGoogleMapJSONConversions arrayFromWeightedData:_heatmapTileLayer.weightedData]; + options[@"gradient"] = + [FLTGoogleMapJSONConversions dictionaryFromGradient:_heatmapTileLayer.gradient]; + options[@"opacity"] = @(_heatmapTileLayer.opacity); + options[@"radius"] = @(_heatmapTileLayer.radius); + options[@"minimumZoomIntensity"] = @(_heatmapTileLayer.minimumZoomIntensity); + options[@"maximumZoomIntensity"] = @(_heatmapTileLayer.maximumZoomIntensity); + return options; +} +@end + +@interface FLTHeatmapsController () + +@property(nonatomic, strong) NSMutableDictionary *heatmapIdToController; +@property(nonatomic, weak) GMSMapView *mapView; + +@end + +@implementation FLTHeatmapsController +- (instancetype)init:(GMSMapView *)mapView { + self = [super init]; + if (self) { + _mapView = mapView; + _heatmapIdToController = [[NSMutableDictionary alloc] init]; + } + return self; +} +- (void)addHeatmaps:(NSArray *)heatmapsToAdd { + for (NSDictionary *heatmap in heatmapsToAdd) { + NSString *heatmapId = [FLTHeatmapsController getHeatmapIdentifier:heatmap]; + GMUHeatmapTileLayer *heatmapTileLayer = [[GMUHeatmapTileLayer alloc] init]; + FLTGoogleMapHeatmapController *controller = + [[FLTGoogleMapHeatmapController alloc] initWithHeatmapTileLayer:heatmapTileLayer + mapView:_mapView + options:heatmap]; + _heatmapIdToController[heatmapId] = controller; + } +} +- (void)changeHeatmaps:(NSArray *)heatmapsToChange { + for (NSDictionary *heatmap in heatmapsToChange) { + NSString *heatmapId = [FLTHeatmapsController getHeatmapIdentifier:heatmap]; + FLTGoogleMapHeatmapController *controller = _heatmapIdToController[heatmapId]; + if (!controller) { + continue; + } + [controller interpretHeatmapOptions:heatmap]; + + [controller clearTileCache]; + } +} +- (void)removeHeatmapsWithIdentifiers:(NSArray *)identifiers { + for (NSString *heatmapId in identifiers) { + FLTGoogleMapHeatmapController *controller = _heatmapIdToController[heatmapId]; + if (!controller) { + continue; + } + [controller removeHeatmap]; + [_heatmapIdToController removeObjectForKey:heatmapId]; + } +} +- (bool)hasHeatmapWithIdentifier:(NSString *)identifier { + if (!identifier) { + return NO; + } + return _heatmapIdToController[identifier] != nil; +} +- (nullable NSDictionary *)heatmapInfoWithIdentifier:(NSString *)identifier { + if (self.heatmapIdToController[identifier] == nil) { + return nil; + } + return [self.heatmapIdToController[identifier] getHeatmapInfo]; +} ++ (NSString *)getHeatmapIdentifier:(NSDictionary *)heatmap { + return heatmap[@"heatmapId"]; +} +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h index cfccb7b0b5f9..b356cf3b385b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h @@ -3,7 +3,9 @@ // found in the LICENSE file. #import +#import #import +@import GoogleMapsUtils; NS_ASSUME_NONNULL_BEGIN @@ -13,6 +15,7 @@ NS_ASSUME_NONNULL_BEGIN + (CGPoint)pointFromArray:(NSArray *)array; + (NSArray *)arrayFromLocation:(CLLocationCoordinate2D)location; + (UIColor *)colorFromRGBA:(NSNumber *)data; ++ (NSNumber *)rgbaFromColor:(UIColor *)color; + (NSArray *)pointsFromLatLongs:(NSArray *)data; + (NSArray *> *)holesFromPointsArray:(NSArray *)data; + (nullable NSDictionary *)dictionaryFromPosition: @@ -24,6 +27,12 @@ NS_ASSUME_NONNULL_BEGIN + (GMSCoordinateBounds *)coordinateBoundsFromLatLongs:(NSArray *)latlongs; + (GMSMapViewType)mapViewTypeFromTypeValue:(NSNumber *)value; + (nullable GMSCameraUpdate *)cameraUpdateFromChannelValue:(NSArray *)channelValue; ++ (nullable GMUWeightedLatLng *)weightedLatLngFromArray:(NSArray *)data; ++ (NSArray *)arrayFromWeightedLatLng:(GMUWeightedLatLng *)weightedLatLng; ++ (NSArray *)weightedDataFromArray:(NSArray *)data; ++ (NSArray *)arrayFromWeightedData:(NSArray *)weightedData; ++ (GMUGradient *)gradientFromDictionary:(NSDictionary *)data; ++ (NSDictionary *)dictionaryFromGradient:(GMUGradient *)gradient; @end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.m index d554c501b1e2..9163cefea026 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.m @@ -26,6 +26,14 @@ + (UIColor *)colorFromRGBA:(NSNumber *)numberColor { alpha:((float)((value & 0xFF000000) >> 24)) / 255.0]; } ++ (NSNumber *)rgbaFromColor:(UIColor *)color { + CGFloat red, green, blue, alpha; + [color getRed:&red green:&green blue:&blue alpha:&alpha]; + unsigned long value = ((unsigned long)(alpha * 255) << 24) | ((unsigned long)(red * 255) << 16) | + ((unsigned long)(green * 255) << 8) | ((unsigned long)(blue * 255)); + return @(value); +} + + (NSArray *)pointsFromLatLongs:(NSArray *)data { NSMutableArray *points = [[NSMutableArray alloc] init]; for (unsigned i = 0; i < [data count]; i++) { @@ -141,4 +149,69 @@ + (nullable GMSCameraUpdate *)cameraUpdateFromChannelValue:(NSArray *)channelVal } return nil; } + ++ (GMUWeightedLatLng *)weightedLatLngFromArray:(NSArray *)data { + NSAssert(data.count == 2, @"WeightedLatLng data must have length of 2"); + if (data.count != 2) { + return nil; + } + return [[GMUWeightedLatLng alloc] + initWithCoordinate:[FLTGoogleMapJSONConversions locationFromLatLong:data[0]] + intensity:[data[1] doubleValue]]; +} + ++ (NSArray *)arrayFromWeightedLatLng:(GMUWeightedLatLng *)weightedLatLng { + GMSMapPoint point = {weightedLatLng.point.x, weightedLatLng.point.y}; + return @[ + [FLTGoogleMapJSONConversions arrayFromLocation:GMSUnproject(point)], @(weightedLatLng.intensity) + ]; +} + ++ (NSArray *)weightedDataFromArray:(NSArray *)data { + NSMutableArray *weightedData = [[NSMutableArray alloc] init]; + for (NSArray *latLng in data) { + GMUWeightedLatLng *weightedLatLng = + [FLTGoogleMapJSONConversions weightedLatLngFromArray:latLng]; + if (weightedLatLng == nil) continue; + [weightedData addObject:weightedLatLng]; + } + + return weightedData; +} + ++ (NSArray *)arrayFromWeightedData:(NSArray *)weightedData { + NSMutableArray *data = [[NSMutableArray alloc] init]; + for (GMUWeightedLatLng *weightedLatLng in weightedData) { + [data addObject:[FLTGoogleMapJSONConversions arrayFromWeightedLatLng:weightedLatLng]]; + } + + return data; +} + ++ (GMUGradient *)gradientFromDictionary:(NSDictionary *)data { + NSMutableArray *colors = [[NSMutableArray alloc] init]; + + NSArray *colorData = data[@"colors"]; + for (NSNumber *colorCode in colorData) { + [colors addObject:[FLTGoogleMapJSONConversions colorFromRGBA:colorCode]]; + } + + return [[GMUGradient alloc] initWithColors:colors + startPoints:data[@"startPoints"] + colorMapSize:[data[@"colorMapSize"] intValue]]; +} + ++ (NSDictionary *)dictionaryFromGradient:(GMUGradient *)gradient { + NSMutableArray *colorCodes = [[NSMutableArray alloc] init]; + for (UIColor *color in gradient.colors) { + [colorCodes addObject:[FLTGoogleMapJSONConversions rgbaFromColor:color]]; + } + + return @{ + @"colors" : colorCodes, + @"startPoints" : gradient.startPoints, + @"colorMapSize" : @(gradient.mapSize) + }; +} + @end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m index bd50c2d7a6de..a24de71ae2d9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m @@ -3,6 +3,7 @@ // found in the LICENSE file. #import "GoogleMapController.h" +#import "FLTGoogleMapHeatmapController.h" #import "FLTGoogleMapJSONConversions.h" #import "FLTGoogleMapTileOverlayController.h" @@ -66,6 +67,7 @@ @interface FLTGoogleMapController () @property(nonatomic, strong) FLTPolygonsController *polygonsController; @property(nonatomic, strong) FLTPolylinesController *polylinesController; @property(nonatomic, strong) FLTCirclesController *circlesController; +@property(nonatomic, strong) FLTHeatmapsController *heatmapsController; @property(nonatomic, strong) FLTTileOverlaysController *tileOverlaysController; @end @@ -118,6 +120,7 @@ - (instancetype)initWithMapView:(GMSMapView *_Nonnull)mapView _circlesController = [[FLTCirclesController alloc] init:_channel mapView:_mapView registrar:registrar]; + _heatmapsController = [[FLTHeatmapsController alloc] init:_mapView]; _tileOverlaysController = [[FLTTileOverlaysController alloc] init:_channel mapView:_mapView registrar:registrar]; @@ -137,6 +140,10 @@ - (instancetype)initWithMapView:(GMSMapView *_Nonnull)mapView if ([circlesToAdd isKindOfClass:[NSArray class]]) { [_circlesController addCircles:circlesToAdd]; } + id heatmapsToAdd = args[@"heatmapsToAdd"]; + if ([heatmapsToAdd isKindOfClass:[NSArray class]]) { + [_heatmapsController addHeatmaps:heatmapsToAdd]; + } id tileOverlaysToAdd = args[@"tileOverlaysToAdd"]; if ([tileOverlaysToAdd isKindOfClass:[NSArray class]]) { [_tileOverlaysController addTileOverlays:tileOverlaysToAdd]; @@ -329,6 +336,20 @@ - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { [self.circlesController removeCircleWithIdentifiers:circleIdsToRemove]; } result(nil); + } else if ([call.method isEqualToString:@"heatmaps#update"]) { + id heatmapsToAdd = call.arguments[@"heatmapsToAdd"]; + if ([heatmapsToAdd isKindOfClass:[NSArray class]]) { + [_heatmapsController addHeatmaps:heatmapsToAdd]; + } + id heatmapsToChange = call.arguments[@"heatmapsToChange"]; + if ([heatmapsToChange isKindOfClass:[NSArray class]]) { + [_heatmapsController changeHeatmaps:heatmapsToChange]; + } + id heatmapIdsToRemove = call.arguments[@"heatmapIdsToRemove"]; + if ([heatmapIdsToRemove isKindOfClass:[NSArray class]]) { + [_heatmapsController removeHeatmapsWithIdentifiers:heatmapIdsToRemove]; + } + result(nil); } else if ([call.method isEqualToString:@"tileOverlays#update"]) { id tileOverlaysToAdd = call.arguments[@"tileOverlaysToAdd"]; if ([tileOverlaysToAdd isKindOfClass:[NSArray class]]) { @@ -393,6 +414,9 @@ - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { } else if ([call.method isEqualToString:@"map#getTileOverlayInfo"]) { NSString *rawTileOverlayId = call.arguments[@"tileOverlayId"]; result([self.tileOverlaysController tileOverlayInfoWithIdentifier:rawTileOverlayId]); + } else if ([call.method isEqualToString:@"map#getHeatmapInfo"]) { + NSString *rawHeatmapId = call.arguments[@"heatmapId"]; + result([self.heatmapsController heatmapInfoWithIdentifier:rawHeatmapId]); } else { result(FlutterMethodNotImplemented); } diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios-umbrella.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios-umbrella.h index 791c3aaea6c3..b5e32b660655 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios-umbrella.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios-umbrella.h @@ -3,6 +3,7 @@ // found in the LICENSE file. #import +#import #import #import #import diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/google_maps_flutter_ios.podspec b/packages/google_maps_flutter/google_maps_flutter_ios/ios/google_maps_flutter_ios.podspec index 14be02f372e4..57e4ab17421d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/google_maps_flutter_ios.podspec +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/google_maps_flutter_ios.podspec @@ -19,8 +19,16 @@ Downloaded by pub (not CocoaPods). s.module_map = 'Classes/google_maps_flutter_ios.modulemap' s.dependency 'Flutter' s.dependency 'GoogleMaps' + s.dependency 'Google-Maps-iOS-Utils' s.static_framework = true s.platform = :ios, '9.0' + # "Google-Maps-iOS-Utils" is static and contains Swift classes. + # Find the Swift runtime when these plugins are built as libraries without `use_frameworks!` + s.swift_version = '5.0' + s.xcconfig = { + 'LIBRARY_SEARCH_PATHS' => '$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', + 'LD_RUNPATH_SEARCH_PATHS' => '$(inherited) /usr/lib/swift', + } # GoogleMaps does not support arm64 simulators. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart index 8fae1a35e316..d756a069e7dd 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart @@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'serialization.dart'; + /// An Android of implementation of [GoogleMapsInspectorPlatform]. @visibleForTesting class GoogleMapsInspectorIOS extends GoogleMapsInspectorPlatform { @@ -81,6 +83,32 @@ class GoogleMapsInspectorIOS extends GoogleMapsInspectorPlatform { ); } + @override + Future getHeatmapInfo(HeatmapId heatmapId, + {required int mapId}) async { + final Map? heatmapInfo = await _channelProvider(mapId)! + .invokeMapMethod( + 'map#getHeatmapInfo', { + 'heatmapId': heatmapId.value, + }); + if (heatmapInfo == null) { + return null; + } + + return Heatmap( + heatmapId: heatmapId, + data: (heatmapInfo['data']! as List) + .map(deserializeWeightedLatLng) + .whereType() + .toList(), + gradient: deserializeHeatmapGradient(heatmapInfo['gradient']), + opacity: heatmapInfo['opacity']! as double, + radius: heatmapInfo['radius']! as int, + minimumZoomIntensity: heatmapInfo['minimumZoomIntensity']! as int, + maximumZoomIntensity: heatmapInfo['maximumZoomIntensity']! as int, + ); + } + @override Future isCompassEnabled({required int mapId}) async { return (await _channelProvider(mapId)! diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart index a0b46f0a96d1..97e64544b24b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart @@ -15,6 +15,7 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf import 'package:stream_transform/stream_transform.dart'; import 'google_map_inspector_ios.dart'; +import 'serialization.dart'; // TODO(stuartmorgan): Remove the dependency on platform interface toJson // methods. Channel serialization details should all be package-internal. @@ -346,6 +347,18 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { ); } + @override + Future updateHeatmaps( + HeatmapUpdates heatmapUpdates, { + required int mapId, + }) { + assert(heatmapUpdates != null); + return _channel(mapId).invokeMethod( + 'heatmaps#update', + serializeMapsObjectUpdates(heatmapUpdates, serializeHeatmap), + ); + } + @override Future updateTileOverlays({ required Set newTileOverlays, @@ -505,6 +518,7 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { 'polygonsToAdd': serializePolygonSet(mapObjects.polygons), 'polylinesToAdd': serializePolylineSet(mapObjects.polylines), 'circlesToAdd': serializeCircleSet(mapObjects.circles), + 'heatmapsToAdd': mapObjects.heatmaps.map(serializeHeatmap), 'tileOverlaysToAdd': serializeTileOverlaySet(mapObjects.tileOverlays), }; @@ -544,6 +558,7 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, + Set heatmaps = const {}, Set tileOverlays = const {}, Set>? gestureRecognizers, Map mapOptions = const {}, @@ -559,6 +574,7 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { polygons: polygons, polylines: polylines, circles: circles, + heatmaps: heatmaps, tileOverlays: tileOverlays), mapOptions: mapOptions, ); @@ -573,6 +589,7 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, + Set heatmaps = const {}, Set tileOverlays = const {}, Set>? gestureRecognizers, Map mapOptions = const {}, @@ -586,6 +603,7 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { polygons: polygons, polylines: polylines, circles: circles, + heatmaps: heatmaps, tileOverlays: tileOverlays, gestureRecognizers: gestureRecognizers, mapOptions: mapOptions, diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/serialization.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/serialization.dart new file mode 100644 index 000000000000..4eb635cb6f8c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/serialization.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +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, + '${updates.objectName}sToAdd', + updates.objectsToAdd.map(serialize), + ); + _addIfNonNull( + json, + '${updates.objectName}sToChange', + updates.objectsToChange.map(serialize), + ); + _addIfNonNull( + json, + '${updates.objectName}IdsToRemove', + updates.objectIdsToRemove + .map((MapsObjectId m) => m.value) + .toList(), + ); + + return json; +} + +/// Serialize [Heatmap] +Object serializeHeatmap(Heatmap heatmap) { + final Map json = {}; + + _addIfNonNull(json, 'heatmapId', heatmap.heatmapId.value); + _addIfNonNull( + json, + 'data', + heatmap.data.map(serializeWeightedLatLng).toList(), + ); + _addIfNonNull(json, 'dissipating', heatmap.dissipating); + + final HeatmapGradient? gradient = heatmap.gradient; + if (gradient != null) { + _addIfNonNull(json, 'gradient', serializeHeatmapGradient(gradient)); + } + _addIfNonNull(json, 'maxIntensity', heatmap.maxIntensity); + _addIfNonNull(json, 'opacity', heatmap.opacity); + _addIfNonNull(json, 'radius', heatmap.radius); + _addIfNonNull(json, 'minimumZoomIntensity', heatmap.minimumZoomIntensity); + _addIfNonNull(json, 'maximumZoomIntensity', 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, + 'colors', + gradient.colors.map((HeatmapGradientColor e) => e.color.value).toList(), + ); + _addIfNonNull( + json, + 'startPoints', + gradient.colors.map((HeatmapGradientColor e) => e.startPoint).toList(), + ); + _addIfNonNull(json, 'colorMapSize', 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['colors']! as List) + .whereType() + .map((int e) => Color(e)) + .toList(); + final List startPoints = + (map['startPoints']! 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['colorMapSize'] as int? ?? 256, + ); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml index c4f8d23cb382..ecb7d4cac28c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_ios description: iOS implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.1.13 +version: 2.2.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -27,3 +27,9 @@ dev_dependencies: flutter_test: sdk: flutter plugin_platform_interface: ^2.0.0 + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_platform_interface: + path: ../google_maps_flutter_platform_interface 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 b3d6c5540e7a..eac24b5d5512 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,5 +1,6 @@ -## NEXT +## 2.3.0 +* Adds support for heatmap layers. * Updates minimum Flutter version to 3.0. ## 2.2.5 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 3fd860e126eb..f9cde60f4c5c 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 @@ -17,6 +17,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 { @@ -349,6 +350,18 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { ); } + @override + Future updateHeatmaps( + HeatmapUpdates heatmapUpdates, { + required int mapId, + }) { + assert(heatmapUpdates != null); + return channel(mapId).invokeMethod( + 'heatmaps#update', + serializeMapsObjectUpdates(heatmapUpdates, serializeHeatmap), + ); + } + @override Future updateTileOverlays({ required Set newTileOverlays, @@ -520,6 +533,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), }; @@ -608,6 +622,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, + Set heatmaps = const {}, Set tileOverlays = const {}, Set>? gestureRecognizers, Map mapOptions = const {}, @@ -637,6 +652,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, + Set heatmaps = const {}, Set tileOverlays = const {}, Set>? gestureRecognizers, Map mapOptions = const {}, @@ -650,6 +666,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { 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/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..80cb8856c8d7 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/serialization.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import '../../google_maps_flutter_platform_interface.dart'; + +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, + '${updates.objectName}sToAdd', + updates.objectsToAdd.map(serialize), + ); + _addIfNonNull( + json, + '${updates.objectName}sToChange', + updates.objectsToChange.map(serialize), + ); + _addIfNonNull( + json, + '${updates.objectName}IdsToRemove', + updates.objectIdsToRemove + .map((MapsObjectId m) => m.value) + .toList(), + ); + + return json; +} + +/// Serialize [Heatmap] +Object serializeHeatmap(Heatmap heatmap) { + final Map json = {}; + + _addIfNonNull(json, 'heatmapId', heatmap.heatmapId.value); + _addIfNonNull( + json, + 'data', + heatmap.data.map(serializeWeightedLatLng).toList(), + ); + _addIfNonNull(json, 'dissipating', heatmap.dissipating); + + final HeatmapGradient? gradient = heatmap.gradient; + if (gradient != null) { + _addIfNonNull(json, 'gradient', serializeHeatmapGradient(gradient)); + } + _addIfNonNull(json, 'maxIntensity', heatmap.maxIntensity); + _addIfNonNull(json, 'opacity', heatmap.opacity); + _addIfNonNull(json, 'radius', heatmap.radius); + _addIfNonNull(json, 'minimumZoomIntensity', heatmap.minimumZoomIntensity); + _addIfNonNull(json, 'maximumZoomIntensity', 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, + 'colors', + gradient.colors.map((HeatmapGradientColor e) => e.color.value).toList(), + ); + _addIfNonNull( + json, + 'startPoints', + gradient.colors.map((HeatmapGradientColor e) => e.startPoint).toList(), + ); + _addIfNonNull(json, 'colorMapSize', 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['colors']! as List) + .whereType() + .map((int e) => Color(e)) + .toList(); + final List startPoints = + (map['startPoints']! 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['colorMapSize'] 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 147d64f715b7..a5f893d87d9c 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 @@ -132,6 +132,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 @@ -375,6 +388,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, + Set heatmaps = const {}, Set tileOverlays = const {}, Set>? gestureRecognizers = const >{}, @@ -405,6 +419,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, + Set heatmaps = const {}, Set tileOverlays = const {}, Map mapOptions = const {}, }) { @@ -416,6 +431,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 1e07b97c300d..98dec373d5a5 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 @@ -115,4 +115,13 @@ abstract class GoogleMapsInspectorPlatform extends PlatformInterface { {required int mapId}) { throw UnimplementedError('getTileOverlayInfo() 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.'); + } } 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..375d064ebec6 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/heatmap.dart @@ -0,0 +1,313 @@ +// 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 listEquals, objectRuntimeType, immutable; +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(String value) : super(value); +} + +/// 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, + this.data = const [], + this.dissipating = true, + this.gradient, + this.maxIntensity, + this.opacity = 0.7, + this.radius = 20, + this.minimumZoomIntensity = 0, + this.maximumZoomIntensity = 21, + }) : assert(opacity >= 0 && opacity <= 1); + + /// Uniquely identifies a [Heatmap]. + final HeatmapId heatmapId; + + @override + HeatmapId get mapsId => heatmapId; + + /// The data points to display. + final List data; + + /// Specifies whether heatmaps 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 int 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, + int? 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); + 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, + }) : assert(colors.length > 0); + + /// The gradient colors. + /// + /// Distributed along [startPoint]s or uniformly depending on the platform. + 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..06a6d1e3fa67 --- /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( + Set previous, + Set current, + ) : super.from(previous, current, 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 56f80e8312dd..41ab7f45a166 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 {}, }); @@ -27,5 +28,6 @@ class MapObjects { final Set polygons; final Set polylines; final Set circles; + final Set heatmaps; final Set tileOverlays; } 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 0beb7d747ec8..7f8b492a2c40 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 @@ -9,6 +9,8 @@ export 'camera.dart'; export 'cap.dart'; export 'circle.dart'; export 'circle_updates.dart'; +export 'heatmap.dart'; +export 'heatmap_updates.dart'; export 'joint_type.dart'; export 'location.dart'; export 'map_configuration.dart'; @@ -30,6 +32,7 @@ export 'tile_provider.dart'; export 'ui.dart'; // Export the utils used by the Widget export 'utils/circle.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 6dfff89f8c4b..da7168533d4d 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/plugins/tree/main/packages/google_maps_fl 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.2.5 +version: 2.3.0 environment: sdk: '>=2.12.0 <3.0.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 d1dba2b75b55..221d1f8a1040 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 @@ -105,6 +105,7 @@ class BuildViewGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { Set polygons = const {}, Set polylines = const {}, Set circles = const {}, + Set heatmaps = const {}, Set tileOverlays = const {}, Set>? gestureRecognizers = const >{}, 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 42930348965f..28c63c689903 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.4.1 +* Adds support for heatmap layers. * Updates minimum Flutter version to 3.0. ## 0.4.0+5 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 692814731bec..304eaaf5f1d8 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/README.md @@ -26,6 +26,8 @@ Modify the `` tag of your `web/index.html` to load the Google Maps JavaScr Now you should be able to use the Google Maps plugin normally. +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: @@ -48,3 +50,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 | \ No newline at end of file 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 0226234ea97a..479d1b734b33 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 @@ -22,6 +22,7 @@ const double _acceptableDelta = 0.0000000001; @GenerateMocks([], customMocks: >[ MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), MockSpec(onMissingStub: OnMissingStub.returnDefault), MockSpec(onMissingStub: OnMissingStub.returnDefault), MockSpec(onMissingStub: OnMissingStub.returnDefault), @@ -148,6 +149,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(); @@ -209,6 +224,7 @@ void main() { group('init', () { late MockCirclesController circles; + late MockHeatmapsController heatmaps; late MockMarkersController markers; late MockPolygonsController polygons; late MockPolylinesController polylines; @@ -216,6 +232,7 @@ void main() { setUp(() { circles = MockCirclesController(); + heatmaps = MockHeatmapsController(); markers = MockMarkersController(); polygons = MockPolygonsController(); polylines = MockPolylinesController(); @@ -227,6 +244,7 @@ void main() { controller.debugSetOverrides( createMap: (_, __) => map, circles: circles, + heatmaps: heatmaps, markers: markers, polygons: polygons, polylines: polylines, @@ -266,6 +284,7 @@ void main() { controller.debugSetOverrides( createMap: (_, __) => map, circles: circles, + heatmaps: heatmaps, markers: markers, polygons: polygons, polylines: polylines, @@ -274,6 +293,7 @@ void main() { controller.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)); @@ -286,6 +306,16 @@ 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)), + ], + ), }, markers: { const Marker( markerId: MarkerId('marker-1'), @@ -328,6 +358,7 @@ void main() { controller.debugSetOverrides( circles: circles, + heatmaps: heatmaps, markers: markers, polygons: polygons, polylines: polylines, @@ -337,6 +368,9 @@ void main() { final Set capturedCircles = verify(circles.addCircles(captureAny)).captured[0] as Set; + final Set capturedHeatmaps = + verify(heatmaps.addHeatmaps(captureAny)).captured[0] + as Set; final Set capturedMarkers = verify(markers.addMarkers(captureAny)).captured[0] as Set; final Set capturedPolygons = @@ -348,6 +382,7 @@ void main() { expect(capturedCircles.first.circleId.value, 'circle-1'); expect(capturedCircles.first.zIndex, 1234); + expect(capturedHeatmaps.first.heatmapId.value, 'heatmap-1'); expect(capturedMarkers.first.markerId.value, 'marker-1'); expect(capturedMarkers.first.infoWindow.snippet, 'snippet for test'); expect(capturedMarkers.first.infoWindow.title, 'title for test'); @@ -602,6 +637,35 @@ void main() { })); }); + testWidgets('updateHeatmaps', (WidgetTester tester) async { + final MockHeatmapsController mock = MockHeatmapsController(); + controller.debugSetOverrides(heatmaps: mock); + + final Set previous = { + const Heatmap(heatmapId: HeatmapId('to-be-updated')), + const Heatmap(heatmapId: HeatmapId('to-be-removed')), + }; + + final Set current = { + const Heatmap( + heatmapId: HeatmapId('to-be-updated'), dissipating: false), + const Heatmap(heatmapId: HeatmapId('to-be-added')), + }; + + controller.updateHeatmaps(HeatmapUpdates.from(previous, current)); + + verify(mock.removeHeatmaps({ + const HeatmapId('to-be-removed'), + })); + verify(mock.addHeatmaps({ + const Heatmap(heatmapId: HeatmapId('to-be-added')), + })); + verify(mock.changeHeatmaps({ + const Heatmap( + heatmapId: HeatmapId('to-be-updated'), dissipating: false), + })); + }); + testWidgets('updateMarkers', (WidgetTester tester) async { final MockMarkersController mock = MockMarkersController(); controller.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 efde66459327..4f64afb7f818 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 @@ -116,6 +116,93 @@ class MockCirclesController extends _i1.Mock implements _i3.CirclesController { ); } +/// A class which mocks [HeatmapsController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHeatmapsController extends _i1.Mock + implements _i3.HeatmapsController { + @override + Map<_i4.HeatmapId, _i3.HeatmapController> get heatmaps => (super.noSuchMethod( + Invocation.getter(#heatmaps), + returnValue: <_i4.HeatmapId, _i3.HeatmapController>{}, + returnValueForMissingStub: <_i4.HeatmapId, _i3.HeatmapController>{}, + ) as Map<_i4.HeatmapId, _i3.HeatmapController>); + @override + _i2.GMap get googleMap => (super.noSuchMethod( + Invocation.getter(#googleMap), + returnValue: _FakeGMap_0( + this, + Invocation.getter(#googleMap), + ), + returnValueForMissingStub: _FakeGMap_0( + this, + Invocation.getter(#googleMap), + ), + ) as _i2.GMap); + @override + set googleMap(_i2.GMap? _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<_i4.Heatmap>? heatmapsToAdd) => super.noSuchMethod( + Invocation.method( + #addHeatmaps, + [heatmapsToAdd], + ), + returnValueForMissingStub: null, + ); + @override + void changeHeatmaps(Set<_i4.Heatmap>? heatmapsToChange) => super.noSuchMethod( + Invocation.method( + #changeHeatmaps, + [heatmapsToChange], + ), + returnValueForMissingStub: null, + ); + @override + void removeHeatmaps(Set<_i4.HeatmapId>? heatmapIdsToRemove) => + super.noSuchMethod( + Invocation.method( + #removeHeatmaps, + [heatmapIdsToRemove], + ), + returnValueForMissingStub: null, + ); + @override + void bindToMap( + int? mapId, + _i2.GMap? 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 9bd1a68c6207..c465c034690d 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 @@ -278,6 +278,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)); + }); // Camera testWidgets('animateCamera', (WidgetTester tester) async { final CameraUpdate expectedUpdates = CameraUpdate.newLatLng( 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 a85bce31e20f..22e524bc3fb7 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 @@ -99,6 +99,7 @@ class MockGoogleMapController extends _i1.Mock _i4.DebugCreateMapFunction? createMap, _i4.MarkersController? markers, _i4.CirclesController? circles, + _i4.HeatmapsController? heatmaps, _i4.PolygonsController? polygons, _i4.PolylinesController? polylines, }) => @@ -110,6 +111,7 @@ class MockGoogleMapController extends _i1.Mock #createMap: createMap, #markers: markers, #circles: circles, + #heatmaps: heatmaps, #polygons: polygons, #polylines: polylines, }, @@ -237,6 +239,14 @@ class MockGoogleMapController extends _i1.Mock returnValueForMissingStub: null, ); @override + void updateHeatmaps(_i3.HeatmapUpdates? updates) => super.noSuchMethod( + Invocation.method( + #updateHeatmaps, + [updates], + ), + returnValueForMissingStub: null, + ); + @override void updatePolygons(_i3.PolygonUpdates? updates) => super.noSuchMethod( Invocation.method( #updatePolygons, 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 11af181cffc2..07f1069b35f7 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 @@ -6,6 +6,7 @@ import 'dart:async'; 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'; @@ -204,4 +205,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)]; + + 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 b9bc2d371c9b..0a339011bb89 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'; import 'package:integration_test/integration_test.dart'; @@ -368,4 +369,94 @@ void main() { expect(line.get('strokeOpacity'), closeTo(0.5, _acceptableDelta)); }); }); + + group('HeatmapsController', () { + late HeatmapsController controller; + + setUp(() { + controller = HeatmapsController(); + controller.bindToMap(123, map); + }); + + testWidgets('addHeatmaps', (WidgetTester tester) async { + final Set heatmaps = { + const Heatmap(heatmapId: HeatmapId('1')), + const Heatmap(heatmapId: HeatmapId('2')), + }; + + 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')), + }; + 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))], + ), + }; + 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')), + const Heatmap(heatmapId: HeatmapId('2')), + const Heatmap(heatmapId: HeatmapId('3')), + }; + + 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 = { + Heatmap( + heatmapId: const HeatmapId('1'), + gradient: HeatmapGradient( + const [ + HeatmapGradientColor(Color(0xFFFABADA), 0) + ], + ), + ), + }; + + 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 43f67946464a..0a351c88a838 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 @@ -26,3 +26,12 @@ dev_dependencies: integration_test: sdk: flutter mockito: ^5.3.2 + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_android: + path: ../../google_maps_flutter_android + google_maps_flutter_ios: + path: ../../google_maps_flutter_ios + google_maps_flutter_platform_interface: + path: ../../google_maps_flutter_platform_interface 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 3121d189b913..af13b9b747a4 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 0650184a14d0..06d34b30f833 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 @@ -15,6 +15,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'; @@ -28,6 +29,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/polygon.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 25cba849475b..dbbc0a302355 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 @@ -29,6 +29,14 @@ double _getCssOpacity(Color color) { return color.opacity; } +// Converts a [Color] into a valid CSS value rgba(R, G, B, A). +String _getCssColorWithAlpha(Color color) { + if (color == null) { + return _defaultCssColor; + } + return 'rgba(${color.red}, ${color.green}, ${color.blue}, ${color.alpha / 255})'; +} + // 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: @@ -325,6 +333,28 @@ gmaps.CircleOptions _circleOptionsFromCircle(Circle circle) { return circleOptions; } +visualization.HeatmapLayerOptions _heatmapOptionsFromHeatmap( + Heatmap heatmap, +) { + 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() + ..dissipating = heatmap.dissipating + ..gradient = heatmap.gradient?.colors + .map((HeatmapGradientColor e) => _getCssColorWithAlpha(e.color)) + .toList() + ..maxIntensity = heatmap.maxIntensity + ..opacity = heatmap.opacity + ..radius = heatmap.radius; + return heatmapOptions; +} + gmaps.PolygonOptions _polygonOptionsFromPolygon( gmaps.GMap 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 a659fb218803..99dc32da7966 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 @@ -25,8 +25,10 @@ class GoogleMapController { _polygons = mapObjects.polygons, _polylines = mapObjects.polylines, _circles = mapObjects.circles, + _heatmaps = mapObjects.heatmaps, _lastMapConfiguration = mapConfiguration { _circlesController = CirclesController(stream: _streamController); + _heatmapsController = HeatmapsController(); _polygonsController = PolygonsController(stream: _streamController); _polylinesController = PolylinesController(stream: _streamController); _markersController = MarkersController(stream: _streamController); @@ -53,6 +55,7 @@ class GoogleMapController { final Set _polygons; final Set _polylines; final Set _circles; + final Set _heatmaps; // The configuraiton passed by the user, before converting to gmaps. // Caching this allows us to re-create the map faithfully when needed. MapConfiguration _lastMapConfiguration = const MapConfiguration(); @@ -97,6 +100,7 @@ class GoogleMapController { // Geometry controllers, for different features of the map. CirclesController? _circlesController; + HeatmapsController? _heatmapsController; PolygonsController? _polygonsController; PolylinesController? _polylinesController; MarkersController? _markersController; @@ -112,12 +116,14 @@ class GoogleMapController { DebugCreateMapFunction? createMap, MarkersController? markers, CirclesController? circles, + HeatmapsController? heatmaps, PolygonsController? polygons, PolylinesController? polylines, }) { _overrideCreateMap = createMap; _markersController = markers ?? _markersController; _circlesController = circles ?? _circlesController; + _heatmapsController = heatmaps ?? _heatmapsController; _polygonsController = polygons ?? _polygonsController; _polylinesController = polylines ?? _polylinesController; } @@ -172,6 +178,7 @@ class GoogleMapController { _renderInitialGeometry( markers: _markers, circles: _circles, + heatmaps: _heatmaps, polygons: _polygons, polylines: _polylines, ); @@ -222,6 +229,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, @@ -230,6 +239,7 @@ class GoogleMapController { 'Cannot attach a map to a null MarkersController instance.'); _circlesController!.bindToMap(_mapId, map); + _heatmapsController!.bindToMap(_mapId, map); _polygonsController!.bindToMap(_mapId, map); _polylinesController!.bindToMap(_mapId, map); _markersController!.bindToMap(_mapId, map); @@ -241,6 +251,7 @@ class GoogleMapController { void _renderInitialGeometry({ Set markers = const {}, Set circles = const {}, + Set heatmaps = const {}, Set polygons = const {}, Set polylines = const {}, }) { @@ -255,6 +266,7 @@ class GoogleMapController { _markersController!.addMarkers(markers); _circlesController!.addCircles(circles); + _heatmapsController!.addHeatmaps(heatmaps); _polygonsController!.addPolygons(polygons); _polylinesController!.addPolylines(polylines); } @@ -361,12 +373,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( @@ -423,6 +448,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 c2085a2bddfc..9d4e3104658a 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..4dc79a8b0bde --- /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; + +/// 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..46bf64d9b3c2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/heatmaps.dart @@ -0,0 +1,61 @@ +// 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; + +/// 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) { + if (heatmap == null) { + return; + } + + 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 072d584b133f..d2d859e0d39e 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/plugins/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.4.0+5 +version: 0.4.1 environment: sdk: ">=2.12.0 <3.0.0" @@ -33,3 +33,9 @@ dev_dependencies: # 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_platform_interface