From a1ae3bc0f6edbc47dda6a259138ba5c6d4d7ffc5 Mon Sep 17 00:00:00 2001 From: lume-code <63221039+lume-code@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:19:25 +0400 Subject: [PATCH] [google_maps_flutter_web] Add editable polyline/polygon support Implements editable polylines and polygons on web using the Google Maps JavaScript API `editable` flag: - Wires `editable`/`onEdited` through to the JS `Polyline`/`Polygon` controllers. - Listens to MVCArray path-change events and emits `PolylineEditEvent` / `PolygonEditEvent` (including polygon holes). - Recreates the controller on change so edit listeners rewire when `editable` toggles. Bumps to 0.7.0. Second of three PRs splitting flutter/packages#11492; depends on google_maps_flutter_platform_interface 2.16.0. --- .../google_maps_flutter_web/CHANGELOG.md | 4 + .../latest/integration_test/shape_test.dart | 197 ++++++++++++++++ .../latest/integration_test/shapes_test.dart | 215 ++++++++++++++++++ .../lib/src/convert.dart | 6 +- .../lib/src/google_maps_flutter_web.dart | 10 + .../lib/src/polygon.dart | 52 ++++- .../lib/src/polygons.dart | 28 ++- .../lib/src/polyline.dart | 42 +++- .../lib/src/polylines.dart | 18 +- .../google_maps_flutter_web/pubspec.yaml | 4 +- 10 files changed, 561 insertions(+), 15 deletions(-) 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 c8657c54f8d8..d1a936a8b8d4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.7.0 + +* Adds support for editable polylines and polygons via the native Google Maps JavaScript API `editable` feature. + ## 0.6.2+3 * Updates README to include setup information. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/shape_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/shape_test.dart index 3f71d790dcd4..22beefe5b4b9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/shape_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/shape_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:js_interop'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps/google_maps.dart' as gmaps; @@ -135,6 +136,119 @@ void main() { }, throwsAssertionError); }); }); + + group('onEdited', () { + late gmaps.Polygon editablePolygon; + + setUp(() { + editablePolygon = gmaps.Polygon( + gmaps.PolygonOptions() + ..editable = true + ..paths = >[ + [gmaps.LatLng(0, 0), gmaps.LatLng(1, 0), gmaps.LatLng(1, 1)].toJS, + [ + gmaps.LatLng(0.1, 0.1), + gmaps.LatLng(0.2, 0.1), + gmaps.LatLng(0.2, 0.2), + ].toJS, + ].toJS, + ); + }); + + testWidgets('fires onEdited on setAt with outer path and holes', (WidgetTester tester) async { + final completer = Completer<({List outer, List> holes})>(); + PolygonController( + polygon: editablePolygon, + onEdited: (List outer, List> holes) { + if (!completer.isCompleted) { + completer.complete((outer: outer, holes: holes)); + } + }, + ); + + editablePolygon.paths.getAt(0).setAt(0, gmaps.LatLng(5, 5)); + + final ({List outer, List> holes}) result = + await completer.future; + expect(result.outer, hasLength(3)); + expect(result.outer.first.lat, 5); + expect(result.outer.first.lng, 5); + expect(result.holes, hasLength(1)); + expect(result.holes.first, hasLength(3)); + }); + + testWidgets('fires onEdited when a hole path is mutated', (WidgetTester tester) async { + final completer = Completer<({List outer, List> holes})>(); + PolygonController( + polygon: editablePolygon, + onEdited: (List outer, List> holes) { + if (!completer.isCompleted) { + completer.complete((outer: outer, holes: holes)); + } + }, + ); + + editablePolygon.paths.getAt(1).setAt(0, gmaps.LatLng(0.9, 0.9)); + + final ({List outer, List> holes}) result = + await completer.future; + expect(result.outer, hasLength(3)); + expect(result.holes, hasLength(1)); + expect(result.holes.first.first.lat, 0.9); + expect(result.holes.first.first.lng, 0.9); + }); + + testWidgets('fires onEdited on insertAt', (WidgetTester tester) async { + final completer = Completer>(); + PolygonController( + polygon: editablePolygon, + onEdited: (List outer, List> holes) { + if (!completer.isCompleted) { + completer.complete(outer); + } + }, + ); + + editablePolygon.paths.getAt(0).insertAt(0, gmaps.LatLng(9, 9)); + + final List result = await completer.future; + expect(result, hasLength(4)); + expect(result.first.lat, 9); + }); + + testWidgets('fires onEdited on removeAt', (WidgetTester tester) async { + final completer = Completer>(); + PolygonController( + polygon: editablePolygon, + onEdited: (List outer, List> holes) { + if (!completer.isCompleted) { + completer.complete(outer); + } + }, + ); + + editablePolygon.paths.getAt(0).removeAt(0); + + final List result = await completer.future; + expect(result, hasLength(2)); + }); + + testWidgets('remove cancels onEdited subscriptions', (WidgetTester tester) async { + var callCount = 0; + final controller = PolygonController( + polygon: editablePolygon, + onEdited: (List outer, List> holes) { + callCount++; + }, + ); + + controller.remove(); + editablePolygon.paths.getAt(0).setAt(0, gmaps.LatLng(7, 7)); + await Future.delayed(const Duration(milliseconds: 50)); + + expect(callCount, 0); + }); + }); }); group('PolylineController', () { @@ -188,5 +302,88 @@ void main() { }, throwsAssertionError); }); }); + + group('onEdited', () { + late gmaps.Polyline editablePolyline; + + setUp(() { + editablePolyline = gmaps.Polyline( + gmaps.PolylineOptions() + ..editable = true + ..path = [gmaps.LatLng(0, 0), gmaps.LatLng(1, 1)].toJS, + ); + }); + + testWidgets('fires onEdited on setAt with updated path', (WidgetTester tester) async { + final completer = Completer>(); + PolylineController( + polyline: editablePolyline, + onEdited: (List path) { + if (!completer.isCompleted) { + completer.complete(path); + } + }, + ); + + editablePolyline.path.setAt(0, gmaps.LatLng(2, 2)); + + final List result = await completer.future; + expect(result, hasLength(2)); + expect(result.first.lat, 2); + expect(result.first.lng, 2); + }); + + testWidgets('fires onEdited on insertAt with updated path', (WidgetTester tester) async { + final completer = Completer>(); + PolylineController( + polyline: editablePolyline, + onEdited: (List path) { + if (!completer.isCompleted) { + completer.complete(path); + } + }, + ); + + editablePolyline.path.insertAt(0, gmaps.LatLng(9, 9)); + + final List result = await completer.future; + expect(result, hasLength(3)); + expect(result.first.lat, 9); + }); + + testWidgets('fires onEdited on removeAt with updated path', (WidgetTester tester) async { + final completer = Completer>(); + PolylineController( + polyline: editablePolyline, + onEdited: (List path) { + if (!completer.isCompleted) { + completer.complete(path); + } + }, + ); + + editablePolyline.path.removeAt(0); + + final List result = await completer.future; + expect(result, hasLength(1)); + expect(result.first.lat, 1); + }); + + testWidgets('remove cancels onEdited subscriptions', (WidgetTester tester) async { + var callCount = 0; + final controller = PolylineController( + polyline: editablePolyline, + onEdited: (List path) { + callCount++; + }, + ); + + controller.remove(); + editablePolyline.path.setAt(0, gmaps.LatLng(3, 3)); + await Future.delayed(const Duration(milliseconds: 50)); + + expect(callCount, 0); + }); + }); }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/shapes_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/shapes_test.dart index 23c85c058095..3e027c5bd195 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/shapes_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/shapes_test.dart @@ -307,6 +307,130 @@ void main() { expect(polygon1Clickable, true); expect(polygon2Clickable, false); }); + + testWidgets('addPolygons forwards editable to the gmaps Polygon', (WidgetTester tester) async { + final polygons = { + const Polygon( + polygonId: PolygonId('editable'), + editable: true, + points: [LatLng(0, 0), LatLng(1, 0), LatLng(1, 1)], + ), + const Polygon( + polygonId: PolygonId('not-editable'), + points: [LatLng(0, 0), LatLng(1, 0), LatLng(1, 1)], + ), + }; + + controller.addPolygons(polygons); + + final gmaps.Polygon editable = controller.polygons[const PolygonId('editable')]!.polygon!; + final gmaps.Polygon notEditable = + controller.polygons[const PolygonId('not-editable')]!.polygon!; + + expect((editable.get('editable')! as JSBoolean).toDart, isTrue); + expect((notEditable.get('editable')! as JSBoolean).toDart, isFalse); + }); + + testWidgets('emits PolygonEditEvent with points and holes on edit', ( + WidgetTester tester, + ) async { + controller.addPolygons({ + const Polygon( + polygonId: PolygonId('p'), + editable: true, + points: [LatLng(0, 0), LatLng(1, 0), LatLng(1, 1)], + holes: >[ + [LatLng(0.1, 0.1), LatLng(0.2, 0.1), LatLng(0.2, 0.2)], + ], + ), + }); + + final Future received = events.stream + .where((MapEvent e) => e is PolygonEditEvent) + .cast() + .first; + final gmaps.Polygon gmPolygon = controller.polygons[const PolygonId('p')]!.polygon!; + gmPolygon.paths.getAt(0).setAt(0, gmaps.LatLng(5, 5)); + + final PolygonEditEvent event = await received; + expect(event.value, const PolygonId('p')); + expect(event.points, hasLength(3)); + expect(event.points.first, const LatLng(5, 5)); + expect(event.holes, hasLength(1)); + expect(event.holes.first, hasLength(3)); + }); + + testWidgets('emits PolygonEditEvent reflecting hole mutations', (WidgetTester tester) async { + controller.addPolygons({ + const Polygon( + polygonId: PolygonId('p'), + editable: true, + points: [LatLng(0, 0), LatLng(1, 0), LatLng(1, 1)], + holes: >[ + [LatLng(0.1, 0.1), LatLng(0.2, 0.1), LatLng(0.2, 0.2)], + ], + ), + }); + + final Future received = events.stream + .where((MapEvent e) => e is PolygonEditEvent) + .cast() + .first; + final gmaps.Polygon gmPolygon = controller.polygons[const PolygonId('p')]!.polygon!; + gmPolygon.paths.getAt(1).setAt(0, gmaps.LatLng(0.9, 0.9)); + + final PolygonEditEvent event = await received; + expect(event.holes.first.first, const LatLng(0.9, 0.9)); + }); + + testWidgets('does not emit PolygonEditEvent when not editable', (WidgetTester tester) async { + controller.addPolygons({ + const Polygon( + polygonId: PolygonId('p'), + points: [LatLng(0, 0), LatLng(1, 0), LatLng(1, 1)], + ), + }); + PolygonEditEvent? captured; + final StreamSubscription sub = events.stream + .where((MapEvent e) => e is PolygonEditEvent) + .cast() + .listen((PolygonEditEvent e) => captured = e); + + final gmaps.Polygon gmPolygon = controller.polygons[const PolygonId('p')]!.polygon!; + gmPolygon.paths.getAt(0).setAt(0, gmaps.LatLng(5, 5)); + await Future.delayed(const Duration(milliseconds: 50)); + + expect(captured, isNull); + await sub.cancel(); + }); + + testWidgets('changePolygons rewires listeners when editable is toggled on', ( + WidgetTester tester, + ) async { + controller.addPolygons({ + const Polygon( + polygonId: PolygonId('t'), + points: [LatLng(0, 0), LatLng(1, 0), LatLng(1, 1)], + ), + }); + controller.changePolygons({ + const Polygon( + polygonId: PolygonId('t'), + editable: true, + points: [LatLng(0, 0), LatLng(1, 0), LatLng(1, 1)], + ), + }); + + final Future received = events.stream + .where((MapEvent e) => e is PolygonEditEvent) + .cast() + .first; + final gmaps.Polygon gmPolygon = controller.polygons[const PolygonId('t')]!.polygon!; + gmPolygon.paths.getAt(0).setAt(0, gmaps.LatLng(7, 7)); + + final PolygonEditEvent event = await received; + expect(event.points.first, const LatLng(7, 7)); + }); }); group('PolylinesController', () { @@ -402,5 +526,96 @@ void main() { expect(polyline1Clickable, true); expect(polyline2Clickable, false); }); + + testWidgets('addPolylines forwards editable to the gmaps Polyline', ( + WidgetTester tester, + ) async { + final polylines = { + const Polyline( + polylineId: PolylineId('editable'), + editable: true, + points: [LatLng(0, 0), LatLng(1, 1)], + ), + const Polyline( + polylineId: PolylineId('not-editable'), + points: [LatLng(0, 0), LatLng(1, 1)], + ), + }; + + controller.addPolylines(polylines); + + final gmaps.Polyline editable = controller.lines[const PolylineId('editable')]!.line!; + final gmaps.Polyline notEditable = controller.lines[const PolylineId('not-editable')]!.line!; + + expect((editable.get('editable')! as JSBoolean).toDart, isTrue); + expect((notEditable.get('editable')! as JSBoolean).toDart, isFalse); + }); + + testWidgets('emits PolylineEditEvent on path mutation when editable', ( + WidgetTester tester, + ) async { + controller.addPolylines({ + const Polyline( + polylineId: PolylineId('e'), + editable: true, + points: [LatLng(0, 0), LatLng(1, 1)], + ), + }); + + final Future received = events.stream + .where((MapEvent e) => e is PolylineEditEvent) + .cast() + .first; + final gmaps.Polyline line = controller.lines[const PolylineId('e')]!.line!; + line.path.setAt(0, gmaps.LatLng(5, 5)); + + final PolylineEditEvent event = await received; + expect(event.value, const PolylineId('e')); + expect(event.points, hasLength(2)); + expect(event.points.first, const LatLng(5, 5)); + }); + + testWidgets('does not emit PolylineEditEvent when not editable', (WidgetTester tester) async { + controller.addPolylines({ + const Polyline(polylineId: PolylineId('n'), points: [LatLng(0, 0), LatLng(1, 1)]), + }); + PolylineEditEvent? captured; + final StreamSubscription sub = events.stream + .where((MapEvent e) => e is PolylineEditEvent) + .cast() + .listen((PolylineEditEvent e) => captured = e); + + final gmaps.Polyline line = controller.lines[const PolylineId('n')]!.line!; + line.path.setAt(0, gmaps.LatLng(5, 5)); + await Future.delayed(const Duration(milliseconds: 50)); + + expect(captured, isNull); + await sub.cancel(); + }); + + testWidgets('changePolylines rewires listeners when editable is toggled on', ( + WidgetTester tester, + ) async { + controller.addPolylines({ + const Polyline(polylineId: PolylineId('t'), points: [LatLng(0, 0), LatLng(1, 1)]), + }); + controller.changePolylines({ + const Polyline( + polylineId: PolylineId('t'), + editable: true, + points: [LatLng(0, 0), LatLng(1, 1)], + ), + }); + + final Future received = events.stream + .where((MapEvent e) => e is PolylineEditEvent) + .cast() + .first; + final gmaps.Polyline line = controller.lines[const PolylineId('t')]!.line!; + line.path.setAt(0, gmaps.LatLng(9, 9)); + + final PolylineEditEvent event = await received; + expect(event.points.first, const LatLng(9, 9)); + }); }); } 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 99d7986fe308..9eebe1c31e53 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 @@ -808,7 +808,8 @@ gmaps.PolygonOptions _polygonOptionsFromPolygon(gmaps.Map googleMap, Polygon pol ..visible = polygon.visible ..zIndex = polygon.zIndex ..geodesic = polygon.geodesic - ..clickable = polygon.consumeTapEvents; + ..clickable = polygon.consumeTapEvents + ..editable = polygon.editable; } List _ensureHoleHasReverseWinding( @@ -867,7 +868,8 @@ gmaps.PolylineOptions _polylineOptionsFromPolyline(gmaps.Map googleMap, Polyline ..visible = polyline.visible ..zIndex = polyline.zIndex ..geodesic = polyline.geodesic - ..clickable = polyline.consumeTapEvents; + ..clickable = polyline.consumeTapEvents + ..editable = polyline.editable; // this.endCap = Cap.buttCap, // this.jointType = JointType.mitered, // this.patterns = const [], 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 9e1ce02285d9..b96e175ea285 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 @@ -234,11 +234,21 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { return _events(mapId).whereType(); } + @override + Stream onPolylineEdited({required int mapId}) { + return _events(mapId).whereType(); + } + @override Stream onPolygonTap({required int mapId}) { return _events(mapId).whereType(); } + @override + Stream onPolygonEdited({required int mapId}) { + return _events(mapId).whereType(); + } + @override Stream onCircleTap({required int mapId}) { return _events(mapId).whereType(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygon.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygon.dart index 6283f84f4594..747db68fe3be 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygon.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygon.dart @@ -11,12 +11,18 @@ class PolygonController { required gmaps.Polygon polygon, bool consumeTapEvents = false, VoidCallback? onTap, + void Function(List points, List> holes)? onEdited, }) : _polygon = polygon, _consumeTapEvents = consumeTapEvents { if (onTap != null) { - polygon.onClick.listen((gmaps.PolyMouseEvent event) { - onTap.call(); - }); + _subscriptions.add( + polygon.onClick.listen((gmaps.PolyMouseEvent event) { + onTap.call(); + }), + ); + } + if (onEdited != null) { + _listenToPathEdits(polygon, onEdited); } } @@ -24,6 +30,8 @@ class PolygonController { final bool _consumeTapEvents; + final List> _subscriptions = >[]; + /// Returns the wrapped [gmaps.Polygon]. Only used for testing. @visibleForTesting gmaps.Polygon? get polygon => _polygon; @@ -31,6 +39,40 @@ class PolygonController { /// Returns `true` if this Controller will use its own `onTap` handler to consume events. bool get consumeTapEvents => _consumeTapEvents; + List _readMvcPath(gmaps.MVCArray mvcPath) { + final points = []; + for (var i = 0; i < mvcPath.length.toInt(); i++) { + points.add(mvcPath.getAt(i)); + } + return points; + } + + void _listenToPathEdits( + gmaps.Polygon polygon, + void Function(List points, List> holes) onEdited, + ) { + void emitCurrentPaths() { + final gmaps.MVCArray> allPaths = polygon.paths; + final List outerPath = allPaths.length.toInt() > 0 + ? _readMvcPath(allPaths.getAt(0)) + : []; + final holes = >[]; + for (var i = 1; i < allPaths.length.toInt(); i++) { + holes.add(_readMvcPath(allPaths.getAt(i))); + } + onEdited(outerPath, holes); + } + + // Listen on all paths (outer boundary + holes). + final gmaps.MVCArray> allPaths = polygon.paths; + for (var i = 0; i < allPaths.length.toInt(); i++) { + final gmaps.MVCArray path = allPaths.getAt(i); + _subscriptions.add(path.onSetAt.listen((_) => emitCurrentPaths())); + _subscriptions.add(path.onInsertAt.listen((_) => emitCurrentPaths())); + _subscriptions.add(path.onRemoveAt.listen((_) => emitCurrentPaths())); + } + } + /// Updates the options of the wrapped [gmaps.Polygon] object. /// /// This cannot be called after [remove]. @@ -45,6 +87,10 @@ class PolygonController { _polygon!.visible = false; _polygon!.map = null; _polygon = null; + for (final StreamSubscription sub in _subscriptions) { + sub.cancel(); + } + _subscriptions.clear(); } } } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart index 86bfbf943c29..122d382683e2 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart @@ -37,6 +37,11 @@ class PolygonsController extends GeometryController { onTap: () { _onPolygonTap(polygon.polygonId); }, + onEdited: polygon.editable + ? (List path, List> holes) { + _onPolygonEdited(polygon.polygonId, path, holes); + } + : null, ); _polygonIdToController[polygon.polygonId] = controller; } @@ -47,8 +52,10 @@ class PolygonsController extends GeometryController { } void _changePolygon(Polygon polygon) { - final PolygonController? polygonController = _polygonIdToController[polygon.polygonId]; - polygonController?.update(_polygonOptionsFromPolygon(googleMap, polygon)); + // Remove and recreate the controller to ensure edit listeners are + // properly set up when the editable property changes. + _removePolygon(polygon.polygonId); + _addPolygon(polygon); } /// Removes a set of [PolygonId]s from the cache. @@ -70,4 +77,21 @@ class PolygonsController extends GeometryController { _streamController.add(PolygonTapEvent(mapId, polygonId)); return _polygonIdToController[polygonId]?.consumeTapEvents ?? false; } + + void _onPolygonEdited( + PolygonId polygonId, + List path, + List> holes, + ) { + final List points = path + .map((gmaps.LatLng p) => LatLng(p.lat.toDouble(), p.lng.toDouble())) + .toList(); + final List> convertedHoles = holes + .map( + (List hole) => + hole.map((gmaps.LatLng p) => LatLng(p.lat.toDouble(), p.lng.toDouble())).toList(), + ) + .toList(); + _streamController.add(PolygonEditEvent(mapId, polygonId, points, convertedHoles)); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polyline.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polyline.dart index ef3430371e26..208b44291b14 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polyline.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polyline.dart @@ -4,19 +4,25 @@ part of '../google_maps_flutter_web.dart'; -/// The `PolygonController` class wraps a [gmaps.Polyline] and its `onTap` behavior. +/// The `PolylineController` class wraps a [gmaps.Polyline] and its `onTap` behavior. class PolylineController { /// Creates a `PolylineController` that wraps a [gmaps.Polyline] object and its `onTap` behavior. PolylineController({ required gmaps.Polyline polyline, bool consumeTapEvents = false, VoidCallback? onTap, + void Function(List path)? onEdited, }) : _polyline = polyline, _consumeTapEvents = consumeTapEvents { if (onTap != null) { - polyline.onClick.listen((gmaps.PolyMouseEvent event) { - onTap.call(); - }); + _subscriptions.add( + polyline.onClick.listen((gmaps.PolyMouseEvent event) { + onTap.call(); + }), + ); + } + if (onEdited != null) { + _listenToPathEdits(polyline, onEdited); } } @@ -24,6 +30,8 @@ class PolylineController { final bool _consumeTapEvents; + final List> _subscriptions = >[]; + /// Returns the wrapped [gmaps.Polyline]. Only used for testing. @visibleForTesting gmaps.Polyline? get line => _polyline; @@ -31,6 +39,28 @@ class PolylineController { /// Returns `true` if this Controller will use its own `onTap` handler to consume events. bool get consumeTapEvents => _consumeTapEvents; + List _readPath(gmaps.Polyline polyline) { + final gmaps.MVCArray path = polyline.path; + final points = []; + for (var i = 0; i < path.length.toInt(); i++) { + points.add(path.getAt(i)); + } + return points; + } + + void _listenToPathEdits( + gmaps.Polyline polyline, + void Function(List path) onEdited, + ) { + void emitCurrentPath() { + onEdited(_readPath(polyline)); + } + + _subscriptions.add(polyline.path.onSetAt.listen((_) => emitCurrentPath())); + _subscriptions.add(polyline.path.onInsertAt.listen((_) => emitCurrentPath())); + _subscriptions.add(polyline.path.onRemoveAt.listen((_) => emitCurrentPath())); + } + /// Updates the options of the wrapped [gmaps.Polyline] object. /// /// This cannot be called after [remove]. @@ -45,6 +75,10 @@ class PolylineController { _polyline!.visible = false; _polyline!.map = null; _polyline = null; + for (final StreamSubscription sub in _subscriptions) { + sub.cancel(); + } + _subscriptions.clear(); } } } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart index 1afd553b3c54..e97cee359323 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart @@ -37,6 +37,11 @@ class PolylinesController extends GeometryController { onTap: () { _onPolylineTap(polyline.polylineId); }, + onEdited: polyline.editable + ? (List path) { + _onPolylineEdited(polyline.polylineId, path); + } + : null, ); _polylineIdToController[polyline.polylineId] = controller; } @@ -47,8 +52,10 @@ class PolylinesController extends GeometryController { } void _changePolyline(Polyline polyline) { - final PolylineController? polylineController = _polylineIdToController[polyline.polylineId]; - polylineController?.update(_polylineOptionsFromPolyline(googleMap, polyline)); + // Remove and recreate the controller to ensure edit listeners are + // properly set up when the editable property changes. + _removePolyline(polyline.polylineId); + _addPolyline(polyline); } /// Removes a set of [PolylineId]s from the cache. @@ -71,4 +78,11 @@ class PolylinesController extends GeometryController { _streamController.add(PolylineTapEvent(mapId, polylineId)); return _polylineIdToController[polylineId]?.consumeTapEvents ?? false; } + + void _onPolylineEdited(PolylineId polylineId, List path) { + final List points = path + .map((gmaps.LatLng p) => LatLng(p.lat.toDouble(), p.lng.toDouble())) + .toList(); + _streamController.add(PolylineEditEvent(mapId, polylineId, points)); + } } 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 9a19bffc4e88..1167e3454e38 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+3 +version: 0.7.0 environment: sdk: ^3.10.0 @@ -23,7 +23,7 @@ dependencies: flutter_web_plugins: sdk: flutter google_maps: ^8.1.0 - google_maps_flutter_platform_interface: ^2.15.0 + google_maps_flutter_platform_interface: ^2.16.0 sanitize_html: ^2.0.0 stream_transform: ^2.0.0 web: ^1.0.0