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 a21ab34c5416..548b8397ef33 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 +* Gracefully bypasses `HeatmapLayer` instantiation and operations when unsupported by the loaded Google Maps JavaScript API (version 3.65+), preventing runtime web crashes. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 0.6.2+1 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 c8f7f610d67c..28f8d65bbf03 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 @@ -8,8 +8,12 @@ import 'dart:js_interop'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps/google_maps.dart' as gmaps; import 'package:google_maps/google_maps_visualization.dart' as visualization; +import 'package:google_maps_flutter_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'; +import 'package:web/web.dart' as web; + +import 'shared_mocks.dart'; /// Test Shapes (Circle, Polygon, Polyline) void main() { @@ -203,47 +207,82 @@ void main() { }); group('HeatmapController', () { - late visualization.HeatmapLayer heatmap; + group('Heatmap on Google Maps JS SDK v3.64 (Supported)', () { + late visualization.HeatmapLayer heatmap; - setUp(() { - heatmap = visualization.HeatmapLayer(); - }); + setUp(() { + injectMockHeatmapLayer(); + mockVisualizationLibrary(); - testWidgets('update', (WidgetTester tester) async { - final controller = HeatmapController(heatmap: heatmap); - final options = visualization.HeatmapLayerOptions() - ..data = [gmaps.LatLng(0, 0)].toJS; + heatmap = visualization.HeatmapLayer(); + }); - expect(heatmap.data.array.toDart, hasLength(0)); + tearDown(() { + restoreVisualizationLibrary(); + }); - controller.update(options); + testWidgets('update', (WidgetTester tester) async { + final controller = HeatmapController(heatmap: heatmap); + final options = visualization.HeatmapLayerOptions() + ..data = [gmaps.LatLng(0, 0)].toJS; - expect(heatmap.data.array.toDart, hasLength(1)); - }); + expect(heatmap.data.array.toDart, hasLength(0)); - group('remove', () { - late HeatmapController controller; + controller.update(options); - setUp(() { - controller = HeatmapController(heatmap: heatmap); + expect(heatmap.data.array.toDart, hasLength(1)); }); - testWidgets('drops gmaps instance', (WidgetTester tester) async { - controller.remove(); + group('remove', () { + late HeatmapController controller; - expect(controller.heatmap, isNull); + 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 options = visualization.HeatmapLayerOptions() + ..dissipating = true; + + controller.remove(); + + expect(() { + controller.update(options); + }, throwsAssertionError); + }); }); + }); - testWidgets('cannot call update after remove', ( + group('Heatmap on Google Maps JS SDK v3.65 (Unsupported)', () { + testWidgets('gracefully bypasses and logs warning exactly once', ( WidgetTester tester, ) async { - final options = visualization.HeatmapLayerOptions()..dissipating = true; - - controller.remove(); - - expect(() { - controller.update(options); - }, throwsAssertionError); + final controller = HeatmapsController(); + final map = gmaps.Map( + web.document.createElement('div') as web.HTMLDivElement, + ); + controller.bindToMap(123, map); + + final heatmaps = { + const Heatmap( + heatmapId: HeatmapId('1'), + data: [], + radius: HeatmapRadius.fromPixels(20), + ), + }; + + controller.addHeatmaps(heatmaps); + + expect(controller.heatmaps.isEmpty, isTrue); + expect(controller.pendingHeatmaps.length, 1); }); }); }); 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 62630275bae8..750bb900c807 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 @@ -12,9 +12,10 @@ import 'package:google_maps/google_maps_geometry.dart' as geometry; import 'package:google_maps/google_maps_visualization.dart' as visualization; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; -// ignore: implementation_imports -import 'package:google_maps_flutter_web/src/utils.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:web/web.dart' as web; + +import 'shared_mocks.dart'; // This value is used when comparing the results of // converting from a byte value to a double between 0 and 1. @@ -32,7 +33,7 @@ void main() { late gmaps.Map map; setUp(() { - map = gmaps.Map(createDivElement()); + map = gmaps.Map(web.document.createElement('div') as web.HTMLDivElement); }); group('CirclesController', () { @@ -460,8 +461,6 @@ void main() { }); group('HeatmapsController', () { - late HeatmapsController controller; - const heatmapPoints = [ WeightedLatLng(LatLng(37.782, -122.447)), WeightedLatLng(LatLng(37.782, -122.445)), @@ -479,123 +478,200 @@ void main() { WeightedLatLng(LatLng(37.785, -122.435)), ]; - setUp(() { - controller = HeatmapsController(); - controller.bindToMap(123, map); - }); - - testWidgets('addHeatmaps', (WidgetTester tester) async { - final heatmaps = { - const Heatmap( - heatmapId: HeatmapId('1'), - data: heatmapPoints, - radius: HeatmapRadius.fromPixels(20), - ), - const Heatmap( - heatmapId: HeatmapId('2'), - data: heatmapPoints, - radius: HeatmapRadius.fromPixels(20), - ), - }; - - controller.addHeatmaps(heatmaps); - - expect(controller.heatmaps.length, 2); - expect(controller.heatmaps, contains(const HeatmapId('1'))); - expect(controller.heatmaps, contains(const HeatmapId('2'))); - expect(controller.heatmaps, isNot(contains(const HeatmapId('66')))); + group('Heatmaps on Google Maps JS SDK v3.64 (Supported)', () { + late HeatmapsController controller; + + setUp(() { + injectMockHeatmapLayer(); + mockVisualizationLibrary(); + + controller = HeatmapsController(); + controller.bindToMap(123, map); + }); + + tearDown(() { + restoreVisualizationLibrary(); + }); + + testWidgets('addHeatmaps', (WidgetTester tester) async { + final heatmaps = { + const Heatmap( + heatmapId: HeatmapId('1'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + const Heatmap( + heatmapId: HeatmapId('2'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + }; + + controller.addHeatmaps(heatmaps); + + expect(controller.heatmaps.length, 2); + expect(controller.heatmaps, contains(const HeatmapId('1'))); + expect(controller.heatmaps, contains(const HeatmapId('2'))); + expect(controller.heatmaps, isNot(contains(const HeatmapId('66')))); + }); + + testWidgets('changeHeatmaps', (WidgetTester tester) async { + final heatmaps = { + const Heatmap( + heatmapId: HeatmapId('1'), + data: [], + radius: HeatmapRadius.fromPixels(20), + ), + }; + controller.addHeatmaps(heatmaps); + + expect( + controller.heatmaps[const HeatmapId('1')]!.heatmap!.data.array.toDart, + hasLength(0), + ); + + final updatedHeatmaps = { + const Heatmap( + heatmapId: HeatmapId('1'), + data: [WeightedLatLng(LatLng(0, 0))], + radius: HeatmapRadius.fromPixels(20), + ), + }; + controller.changeHeatmaps(updatedHeatmaps); + + expect(controller.heatmaps.length, 1); + expect( + controller.heatmaps[const HeatmapId('1')]!.heatmap!.data.array.toDart, + hasLength(1), + ); + }); + + testWidgets('removeHeatmaps', (WidgetTester tester) async { + final heatmaps = { + const Heatmap( + heatmapId: HeatmapId('1'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + const Heatmap( + heatmapId: HeatmapId('2'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + const Heatmap( + heatmapId: HeatmapId('3'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + }; + + controller.addHeatmaps(heatmaps); + + expect(controller.heatmaps.length, 3); + + // Remove some heatmaps... + final 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 heatmaps = { + const Heatmap( + heatmapId: HeatmapId('1'), + data: heatmapPoints, + gradient: HeatmapGradient([ + HeatmapGradientColor(Color(0xFFFABADA), 0), + ]), + radius: HeatmapRadius.fromPixels(20), + ), + }; + + controller.addHeatmaps(heatmaps); + + final visualization.HeatmapLayer heatmap = + controller.heatmaps.values.first.heatmap!; + + expect( + (heatmap.get('gradient')! as JSArray).toDart.map( + (JSString? value) => value!.toDart, + ), + ['rgba(250, 186, 218, 0.00)', 'rgba(250, 186, 218, 1.00)'], + ); + }); }); - testWidgets('changeHeatmaps', (WidgetTester tester) async { - final heatmaps = { - const Heatmap( - heatmapId: HeatmapId('1'), - data: [], - radius: HeatmapRadius.fromPixels(20), - ), - }; - controller.addHeatmaps(heatmaps); - - expect( - controller.heatmaps[const HeatmapId('1')]!.heatmap!.data.array.toDart, - hasLength(0), + group('Heatmaps on Google Maps JS SDK v3.65 (Unsupported)', () { + late HeatmapsController controller; + + setUp(() { + controller = HeatmapsController(); + controller.bindToMap(123, map); + }); + + testWidgets( + 'addHeatmaps gracefully bypasses and queues heatmaps without crashing', + (WidgetTester tester) async { + final heatmaps = { + const Heatmap( + heatmapId: HeatmapId('1'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + }; + + controller.addHeatmaps(heatmaps); + + expect(controller.heatmaps.isEmpty, isTrue); + expect( + controller.pendingHeatmaps, + contains( + const Heatmap( + heatmapId: HeatmapId('1'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + ), + ); + }, ); - final updatedHeatmaps = { - const Heatmap( - heatmapId: HeatmapId('1'), - data: [WeightedLatLng(LatLng(0, 0))], - radius: HeatmapRadius.fromPixels(20), - ), - }; - controller.changeHeatmaps(updatedHeatmaps); - - expect(controller.heatmaps.length, 1); - expect( - controller.heatmaps[const HeatmapId('1')]!.heatmap!.data.array.toDart, - hasLength(1), - ); - }); - - testWidgets('removeHeatmaps', (WidgetTester tester) async { - final heatmaps = { - const Heatmap( - heatmapId: HeatmapId('1'), - data: heatmapPoints, - radius: HeatmapRadius.fromPixels(20), - ), - const Heatmap( - heatmapId: HeatmapId('2'), - data: heatmapPoints, - radius: HeatmapRadius.fromPixels(20), - ), - const Heatmap( - heatmapId: HeatmapId('3'), - data: heatmapPoints, - radius: HeatmapRadius.fromPixels(20), - ), - }; - - controller.addHeatmaps(heatmaps); - - expect(controller.heatmaps.length, 3); - - // Remove some polylines... - final 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 heatmaps = { - const Heatmap( - heatmapId: HeatmapId('1'), - data: heatmapPoints, - gradient: HeatmapGradient([ - HeatmapGradientColor(Color(0xFFFABADA), 0), - ]), - radius: HeatmapRadius.fromPixels(20), - ), - }; - - controller.addHeatmaps(heatmaps); - - final visualization.HeatmapLayer heatmap = - controller.heatmaps.values.first.heatmap!; - - expect( - (heatmap.get('gradient')! as JSArray).toDart.map( - (JSString? value) => value!.toDart, - ), - ['rgba(250, 186, 218, 0.00)', 'rgba(250, 186, 218, 1.00)'], + testWidgets( + 'recovers and flushes pending queue when support becomes active later', + (WidgetTester tester) async { + final heatmaps = { + const Heatmap( + heatmapId: HeatmapId('1'), + data: heatmapPoints, + radius: HeatmapRadius.fromPixels(20), + ), + }; + + controller.addHeatmaps(heatmaps); + + expect(controller.heatmaps.isEmpty, isTrue); + expect(controller.pendingHeatmaps.length, 1); + + injectMockHeatmapLayer(); + mockVisualizationLibrary(); + try { + controller.flushPendingHeatmaps(); + + expect(controller.heatmaps.length, 1); + expect(controller.heatmaps, contains(const HeatmapId('1'))); + expect(controller.pendingHeatmaps.isEmpty, isTrue); + } finally { + restoreVisualizationLibrary(); + } + }, ); }); }); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shared_mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shared_mocks.dart new file mode 100644 index 000000000000..8338b5cd2a31 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shared_mocks.dart @@ -0,0 +1,141 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:js_interop'; +import 'package:web/web.dart' as web; + +@JS('mockVisualizationLibrary') +external void mockVisualizationLibrary(); + +@JS('restoreVisualizationLibrary') +external void restoreVisualizationLibrary(); + +/// Extension giving [web.Window] a nullable getter to the custom `MockHeatmapLayer` constructor. +extension MockHeatmapLayerGetter on web.Window { + @JS('MockHeatmapLayer') + external JSFunction? get mockHeatmapLayer; +} + +/// Injects a robust prototype-based JavaScript mock constructor for HeatmapLayer +/// into the global window context, ensuring property-access tests execute cleanly. +void injectMockHeatmapLayer() { + if (web.window.mockHeatmapLayer != null) { + return; + } + + // Inject a robust mock prototype Javascript class supporting get(), set(), + // and getter properties (gradient, data, map, options) of MVCObject + final script = web.document.createElement('script') as web.HTMLScriptElement; + script.text = ''' + class MockMVCArray { + constructor() { + this._array = []; + } + getArray() { + return this._array; + } + get array() { + return this._array; + } + } + class MockHeatmapLayer { + constructor(options) { + this._gradient = []; + this._data = new MockMVCArray(); + this._map = null; + this._options = {}; + this.setOptions(options); + } + setOptions(options) { + if (!options) return; + this._options = options; + if (options.gradient) { + this._gradient = options.gradient; + } + if (options.data) { + this._data = new MockMVCArray(); + this._data._array = Array.from(options.data); + } + if (options.map) { + this._map = options.map; + } + } + get(key) { + if (key === 'gradient') return this._gradient; + if (key === 'data') return this._data; + if (key === 'map') return this._map; + return this._options[key]; + } + get data() { + return this._data; + } + getData() { + return this._data; + } + get map() { + return this._map; + } + set map(value) { + this._map = value; + } + setMap(value) { + this._map = value; + } + getMap() { + return this._map; + } + } + window.MockHeatmapLayer = MockHeatmapLayer; + window.mockVisualizationLibrary = function() { + console.log("mockVisualizationLibrary called! window.google:", window.google); + if (window.google && window.google.maps) { + console.log("window.google.maps exists!"); + window.originalMaps = window.google.maps; + var mapsProxy = new Proxy(window.originalMaps, { + get: function(target, prop) { + console.log("Proxy get called for prop:", prop); + if (prop === "visualization") { + return { + HeatmapLayer: window.MockHeatmapLayer + }; + } + var val = target[prop]; + if (typeof val === "function") { + return val.bind(target); + } + return val; + } + }); + try { + Object.defineProperty(window.google, "maps", { + value: mapsProxy, + configurable: true, + writable: true + }); + console.log("defineProperty succeeded!"); + } catch (e) { + console.error("defineProperty failed:", e); + window.google.maps = mapsProxy; + } + console.log("window.google.maps.visualization:", window.google.maps.visualization); + } else { + console.log("window.google or window.google.maps is missing!"); + } + }; + window.restoreVisualizationLibrary = function() { + if (window.google && window.originalMaps) { + try { + Object.defineProperty(window.google, "maps", { + value: window.originalMaps, + configurable: true, + writable: true + }); + } catch (e) { + window.google.maps = window.originalMaps; + } + } + }; + '''; + web.document.head!.appendChild(script); +} 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 0413faaf104d..c90824ce5f8e 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 @@ -30,6 +30,8 @@ import 'src/third_party/to_screen_location/to_screen_location.dart'; import 'src/types.dart'; import 'src/utils.dart'; +export 'src/dom_window_extension.dart'; + part 'src/circle.dart'; part 'src/circles.dart'; part 'src/convert.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/dom_window_extension.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/dom_window_extension.dart index 891e9cb6b62f..45d489e8ba4c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/dom_window_extension.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/dom_window_extension.dart @@ -9,6 +9,7 @@ library; import 'dart:js_interop'; +import 'package:flutter/foundation.dart'; import 'package:web/web.dart' as web; /// This extension gives [web.Window] a nullable getter to the `trustedTypes` @@ -44,3 +45,61 @@ extension CreateHTMLNoArgs on web.TrustedTypePolicy { @JS('createHTML') external web.TrustedHTML createHTMLNoArgs(String input); } + +/// This extension gives [web.Window] a nullable getter to the `google` +/// property, which is used to check if the Google Maps SDK is loaded. +@visibleForTesting +extension NullableGoogleGetter on web.Window { + /// (Nullable) Bindings to window.google. + @JS('google') + external JSObject? get nullableGoogle; +} + +/// Nullable bindings to get `maps` from a JSObject. +@visibleForTesting +extension NullableMapsGetter on JSObject { + /// (Nullable) Bindings to google.maps. + @JS('maps') + external JSObject? get nullableMaps; +} + +/// Nullable bindings to get `visualization` from a JSObject. +@visibleForTesting +extension NullableVisualizationGetter on JSObject { + /// (Nullable) Bindings to google.maps.visualization. + @JS('visualization') + external JSObject? get nullableVisualization; + + /// Bindings to set google.maps.visualization (for testing). + @JS('visualization') + external set nullableVisualization(JSObject? value); +} + +/// Nullable bindings to get `HeatmapLayer` from a JSObject. +@visibleForTesting +extension NullableHeatmapLayerGetter on JSObject { + /// (Nullable) Bindings to google.maps.visualization.HeatmapLayer. + @JS('HeatmapLayer') + external JSObject? get nullableHeatmapLayer; + + /// Bindings to set HeatmapLayer (for testing). + @JS('HeatmapLayer') + external set nullableHeatmapLayer(JSObject? value); +} + +/// Returns whether the Heatmap Layer is supported by the loaded Google Maps JS SDK. +bool isHeatmapSupported() { + final JSObject? google = web.window.nullableGoogle; + if (google == null) { + return false; + } + final JSObject? maps = google.nullableMaps; + if (maps == null) { + return false; + } + final JSObject? visualization = maps.nullableVisualization; + if (visualization == null) { + return false; + } + return visualization.nullableHeatmapLayer != null; +} 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 bb8a5c624340..da844e8705b1 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 @@ -582,6 +582,7 @@ class GoogleMapController { _heatmapsController != null, 'Cannot update heatmaps after dispose().', ); + _heatmapsController?.flushPendingHeatmaps(); _heatmapsController?.addHeatmaps(updates.heatmapsToAdd); _heatmapsController?.changeHeatmaps(updates.heatmapsToChange); _heatmapsController?.removeHeatmaps(updates.heatmapIdsToRemove); 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 index 105c2fb1a0c0..08c36351f9f7 100644 --- 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 @@ -13,28 +13,87 @@ class HeatmapsController extends GeometryController { // A cache of [HeatmapController]s indexed by their [HeatmapId]. final Map _heatmapIdToController; + // A cache of pending [Heatmap]s that are queued while the visualization library is loading. + final Set _pendingHeatmaps = {}; + + bool _warningLogged = false; + /// Returns the cache of [HeatmapController]s. Test only. @visibleForTesting Map get heatmaps => _heatmapIdToController; + /// Returns the cache of pending [Heatmap]s. Test only. + @visibleForTesting + Set get pendingHeatmaps => _pendingHeatmaps; + + /// Flushes all pending heatmaps that were bypassed due to the visualization + /// library not being loaded at the time of creation. + void flushPendingHeatmaps() { + if (_pendingHeatmaps.isEmpty) { + return; + } + if (isHeatmapSupported()) { + final pending = Set.from(_pendingHeatmaps); + _pendingHeatmaps.clear(); + pending.forEach(_addHeatmap); + } + } + /// Adds a set of [Heatmap] objects to the cache. /// /// Wraps each [Heatmap] into its corresponding [HeatmapController]. void addHeatmaps(Set heatmapsToAdd) { + if (heatmapsToAdd.isEmpty) { + return; + } + if (!isHeatmapSupported()) { + _pendingHeatmaps.addAll(heatmapsToAdd); + if (!_warningLogged) { + _warningLogged = true; + debugPrint( + 'The Heatmap Layer functionality is not supported by the loaded version of the Google Maps JavaScript API. Bypassing heatmap creation.', + ); + } + return; + } + flushPendingHeatmaps(); heatmapsToAdd.forEach(_addHeatmap); } void _addHeatmap(Heatmap heatmap) { final visualization.HeatmapLayerOptions heatmapOptions = _heatmapOptionsFromHeatmap(heatmap); - final gmHeatmap = visualization.HeatmapLayer(heatmapOptions); - gmHeatmap.map = googleMap; - final controller = HeatmapController(heatmap: gmHeatmap); - _heatmapIdToController[heatmap.heatmapId] = controller; + try { + final gmHeatmap = visualization.HeatmapLayer(heatmapOptions); + gmHeatmap.map = googleMap; + final controller = HeatmapController(heatmap: gmHeatmap); + _heatmapIdToController[heatmap.heatmapId] = controller; + } catch (e) { + _pendingHeatmaps.add(heatmap); + if (!_warningLogged) { + _warningLogged = true; + debugPrint( + 'The Heatmap Layer functionality is not supported by the loaded version of the Google Maps JavaScript API. Bypassing heatmap creation.', + ); + } + } } /// Updates a set of [Heatmap] objects with new options. void changeHeatmaps(Set heatmapsToChange) { + if (heatmapsToChange.isEmpty) { + return; + } + if (!isHeatmapSupported()) { + for (final heatmap in heatmapsToChange) { + _pendingHeatmaps.removeWhere( + (Heatmap h) => h.heatmapId == heatmap.heatmapId, + ); + _pendingHeatmaps.add(heatmap); + } + return; + } + flushPendingHeatmaps(); heatmapsToChange.forEach(_changeHeatmap); } @@ -46,6 +105,16 @@ class HeatmapsController extends GeometryController { /// Removes a set of [HeatmapId]s from the cache. void removeHeatmaps(Set heatmapIdsToRemove) { + if (heatmapIdsToRemove.isEmpty) { + return; + } + _pendingHeatmaps.removeWhere( + (Heatmap h) => heatmapIdsToRemove.contains(h.heatmapId), + ); + if (!isHeatmapSupported()) { + return; + } + flushPendingHeatmaps(); for (final heatmapId in heatmapIdsToRemove) { final HeatmapController? heatmapController = _heatmapIdToController[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 63cd0564b952..6ae05217458a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 0.6.2+1 +version: 0.6.2+2 environment: sdk: ^3.10.0