From 4900e75d147d52c150389c945fd516b69e5cf6b7 Mon Sep 17 00:00:00 2001 From: Matthew Lloyd Date: Wed, 29 May 2019 21:49:54 -0500 Subject: [PATCH] Adds support for polygon overlays to the Google Maps plugin (#1551) This is relatively trivial, requiring only some additional logic to disambiguate click events between the various possible overlays. Also adds a page to the example app demonstrating polygons, which I tested on iOS and Android. --- packages/google_maps_flutter/CHANGELOG.md | 4 + .../flutter/plugins/googlemaps/Convert.java | 51 ++++ .../plugins/googlemaps/GoogleMapBuilder.java | 7 + .../googlemaps/GoogleMapController.java | 36 +++ .../plugins/googlemaps/GoogleMapFactory.java | 3 + .../googlemaps/GoogleMapOptionsSink.java | 2 + .../plugins/googlemaps/PolygonBuilder.java | 63 +++++ .../plugins/googlemaps/PolygonController.java | 71 ++++++ .../googlemaps/PolygonOptionsSink.java | 24 ++ .../googlemaps/PolygonsController.java | 112 +++++++++ .../google_maps_flutter/example/lib/main.dart | 2 + .../example/lib/place_polygon.dart | 235 ++++++++++++++++++ .../ios/Classes/GoogleMapController.h | 1 + .../ios/Classes/GoogleMapController.m | 24 ++ .../ios/Classes/GoogleMapPolygonController.h | 37 +++ .../ios/Classes/GoogleMapPolygonController.m | 189 ++++++++++++++ .../ios/Classes/GoogleMapsPlugin.h | 1 + .../lib/google_maps_flutter.dart | 2 + .../lib/src/controller.dart | 20 ++ .../lib/src/google_map.dart | 21 ++ .../google_maps_flutter/lib/src/polygon.dart | 179 +++++++++++++ .../lib/src/polygon_updates.dart | 90 +++++++ packages/google_maps_flutter/pubspec.yaml | 2 +- .../test/fake_maps_controllers.dart | 57 +++++ .../test/polygon_updates_test.dart | 200 +++++++++++++++ 25 files changed, 1432 insertions(+), 1 deletion(-) create mode 100644 packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java create mode 100644 packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java create mode 100644 packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java create mode 100644 packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java create mode 100644 packages/google_maps_flutter/example/lib/place_polygon.dart create mode 100644 packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h create mode 100644 packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m create mode 100644 packages/google_maps_flutter/lib/src/polygon.dart create mode 100644 packages/google_maps_flutter/lib/src/polygon_updates.dart create mode 100644 packages/google_maps_flutter/test/polygon_updates_test.dart diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index 9ecdd881e751..d7e623d963a5 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.15 + +* Add support for Polygons. + ## 0.5.14+1 * Example app update(comment out usage of the ImageStreamListener API which has a breaking change diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index 46ae935cbeb6..b8775b97cf55 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java @@ -170,6 +170,15 @@ static Object markerIdToJson(String markerId) { return data; } + static Object polygonIdToJson(String polygonId) { + if (polygonId == null) { + return null; + } + final Map data = new HashMap<>(1); + data.put("polygonId", polygonId); + return data; + } + static Object polylineIdToJson(String polylineId) { if (polylineId == null) { return null; @@ -364,6 +373,48 @@ private static void interpretInfoWindowOptions( } } + static String interpretPolygonOptions(Object o, PolygonOptionsSink sink) { + final Map data = toMap(o); + final Object consumeTapEvents = data.get("consumeTapEvents"); + if (consumeTapEvents != null) { + sink.setConsumeTapEvents(toBoolean(consumeTapEvents)); + } + final Object geodesic = data.get("geodesic"); + if (geodesic != null) { + sink.setGeodesic(toBoolean(geodesic)); + } + final Object visible = data.get("visible"); + if (visible != null) { + sink.setVisible(toBoolean(visible)); + } + final Object fillColor = data.get("fillColor"); + if (fillColor != null) { + sink.setFillColor(toInt(fillColor)); + } + final Object strokeColor = data.get("strokeColor"); + if (strokeColor != null) { + sink.setStrokeColor(toInt(strokeColor)); + } + final Object strokeWidth = data.get("strokeWidth"); + if (strokeWidth != null) { + sink.setStrokeWidth(toInt(strokeWidth)); + } + final Object zIndex = data.get("zIndex"); + if (zIndex != null) { + sink.setZIndex(toFloat(zIndex)); + } + final Object points = data.get("points"); + if (points != null) { + sink.setPoints(toPoints(points)); + } + final String polygonId = (String) data.get("polygonId"); + if (polygonId == null) { + throw new IllegalArgumentException("polygonId was null"); + } else { + return polygonId; + } + } + static String interpretPolylineOptions(Object o, PolylineOptionsSink sink) { final Map data = toMap(o); final Object consumeTapEvents = data.get("consumeTapEvents"); diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java index bb3d750a870f..49bad465ae83 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java @@ -17,6 +17,7 @@ class GoogleMapBuilder implements GoogleMapOptionsSink { private boolean myLocationEnabled = false; private boolean myLocationButtonEnabled = false; private Object initialMarkers; + private Object initialPolygons; private Object initialPolylines; private Object initialCircles; @@ -29,6 +30,7 @@ GoogleMapController build( controller.setMyLocationButtonEnabled(myLocationButtonEnabled); controller.setTrackCameraPosition(trackCameraPosition); controller.setInitialMarkers(initialMarkers); + controller.setInitialPolygons(initialPolygons); controller.setInitialPolylines(initialPolylines); controller.setInitialCircles(initialCircles); return controller; @@ -103,6 +105,11 @@ public void setInitialMarkers(Object initialMarkers) { this.initialMarkers = initialMarkers; } + @Override + public void setInitialPolygons(Object initialPolygons) { + this.initialPolygons = initialPolygons; + } + @Override public void setInitialPolylines(Object initialPolylines) { this.initialPolylines = initialPolylines; diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index 338f33aaebfd..31f532a1d18b 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -30,6 +30,7 @@ import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.Polygon; import com.google.android.gms.maps.model.Polyline; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; @@ -50,6 +51,7 @@ final class GoogleMapController GoogleMap.OnCameraMoveStartedListener, GoogleMap.OnInfoWindowClickListener, GoogleMap.OnMarkerClickListener, + GoogleMap.OnPolygonClickListener, GoogleMap.OnPolylineClickListener, GoogleMap.OnCircleClickListener, GoogleMapOptionsSink, @@ -75,9 +77,11 @@ final class GoogleMapController private final int registrarActivityHashCode; private final Context context; private final MarkersController markersController; + private final PolygonsController polygonsController; private final PolylinesController polylinesController; private final CirclesController circlesController; private List initialMarkers; + private List initialPolygons; private List initialPolylines; private List initialCircles; @@ -98,6 +102,7 @@ final class GoogleMapController methodChannel.setMethodCallHandler(this); this.registrarActivityHashCode = registrar.activity().hashCode(); this.markersController = new MarkersController(methodChannel); + this.polygonsController = new PolygonsController(methodChannel); this.polylinesController = new PolylinesController(methodChannel); this.circlesController = new CirclesController(methodChannel); } @@ -169,15 +174,18 @@ public void onMapReady(GoogleMap googleMap) { googleMap.setOnCameraMoveListener(this); googleMap.setOnCameraIdleListener(this); googleMap.setOnMarkerClickListener(this); + googleMap.setOnPolygonClickListener(this); googleMap.setOnPolylineClickListener(this); googleMap.setOnCircleClickListener(this); googleMap.setOnMapClickListener(this); googleMap.setOnMapLongClickListener(this); updateMyLocationSettings(); markersController.setGoogleMap(googleMap); + polygonsController.setGoogleMap(googleMap); polylinesController.setGoogleMap(googleMap); circlesController.setGoogleMap(googleMap); updateInitialMarkers(); + updateInitialPolygons(); updateInitialPolylines(); updateInitialCircles(); } @@ -238,6 +246,17 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { result.success(null); break; } + case "polygons#update": + { + Object polygonsToAdd = call.argument("polygonsToAdd"); + polygonsController.addPolygons((List) polygonsToAdd); + Object polygonsToChange = call.argument("polygonsToChange"); + polygonsController.changePolygons((List) polygonsToChange); + Object polygonIdsToRemove = call.argument("polygonIdsToRemove"); + polygonsController.removePolygons((List) polygonIdsToRemove); + result.success(null); + break; + } case "polylines#update": { Object polylinesToAdd = call.argument("polylinesToAdd"); @@ -350,6 +369,11 @@ public boolean onMarkerClick(Marker marker) { return markersController.onMarkerTap(marker.getId()); } + @Override + public void onPolygonClick(Polygon polygon) { + polygonsController.onPolygonTap(polygon.getId()); + } + @Override public void onPolylineClick(Polyline polyline) { polylinesController.onPolylineTap(polyline.getId()); @@ -514,6 +538,18 @@ private void updateInitialMarkers() { markersController.addMarkers(initialMarkers); } + @Override + public void setInitialPolygons(Object initialPolygons) { + this.initialPolygons = (List) initialPolygons; + if (googleMap != null) { + updateInitialPolygons(); + } + } + + private void updateInitialPolygons() { + polygonsController.addPolygons(initialPolygons); + } + @Override public void setInitialPolylines(Object initialPolylines) { this.initialPolylines = (List) initialPolylines; diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java index 1e1082a67460..bc19fa56572a 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java @@ -35,6 +35,9 @@ public PlatformView create(Context context, int id, Object args) { if (params.containsKey("markersToAdd")) { builder.setInitialMarkers(params.get("markersToAdd")); } + if (params.containsKey("polygonsToAdd")) { + builder.setInitialPolygons(params.get("polygonsToAdd")); + } if (params.containsKey("polylinesToAdd")) { builder.setInitialPolylines(params.get("polylinesToAdd")); } diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java index 347b85d428da..5e11eb21e2ac 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java @@ -32,6 +32,8 @@ interface GoogleMapOptionsSink { void setInitialMarkers(Object initialMarkers); + void setInitialPolygons(Object initialPolygons); + void setInitialPolylines(Object initialPolylines); void setInitialCircles(Object initialCircles); diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java new file mode 100644 index 000000000000..68c35864b08c --- /dev/null +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java @@ -0,0 +1,63 @@ +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.PolygonOptions; +import java.util.List; + +class PolygonBuilder implements PolygonOptionsSink { + private final PolygonOptions polygonOptions; + private boolean consumeTapEvents; + + PolygonBuilder() { + this.polygonOptions = new PolygonOptions(); + } + + PolygonOptions build() { + return polygonOptions; + } + + boolean consumeTapEvents() { + return consumeTapEvents; + } + + @Override + public void setFillColor(int color) { + polygonOptions.fillColor(color); + } + + @Override + public void setStrokeColor(int color) { + polygonOptions.strokeColor(color); + } + + @Override + public void setPoints(List points) { + polygonOptions.addAll(points); + } + + @Override + public void setConsumeTapEvents(boolean consumeTapEvents) { + this.consumeTapEvents = consumeTapEvents; + polygonOptions.clickable(consumeTapEvents); + } + + @Override + public void setGeodesic(boolean geodisc) { + polygonOptions.geodesic(geodisc); + } + + @Override + public void setVisible(boolean visible) { + polygonOptions.visible(visible); + } + + @Override + public void setStrokeWidth(float width) { + polygonOptions.strokeWidth(width); + } + + @Override + public void setZIndex(float zIndex) { + polygonOptions.zIndex(zIndex); + } +} diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java new file mode 100644 index 000000000000..d77c86922fd5 --- /dev/null +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java @@ -0,0 +1,71 @@ +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.Polygon; +import java.util.List; + +/** Controller of a single Polygon on the map. */ +class PolygonController implements PolygonOptionsSink { + private final Polygon polygon; + private final String googleMapsPolygonId; + private boolean consumeTapEvents; + + PolygonController(Polygon polygon, boolean consumeTapEvents) { + this.polygon = polygon; + this.consumeTapEvents = consumeTapEvents; + this.googleMapsPolygonId = polygon.getId(); + } + + void remove() { + polygon.remove(); + } + + @Override + public void setConsumeTapEvents(boolean consumeTapEvents) { + this.consumeTapEvents = consumeTapEvents; + polygon.setClickable(consumeTapEvents); + } + + @Override + public void setFillColor(int color) { + polygon.setFillColor(color); + } + + @Override + public void setStrokeColor(int color) { + polygon.setStrokeColor(color); + } + + @Override + public void setGeodesic(boolean geodesic) { + polygon.setGeodesic(geodesic); + } + + @Override + public void setPoints(List points) { + polygon.setPoints(points); + } + + @Override + public void setVisible(boolean visible) { + polygon.setVisible(visible); + } + + @Override + public void setStrokeWidth(float width) { + polygon.setStrokeWidth(width); + } + + @Override + public void setZIndex(float zIndex) { + polygon.setZIndex(zIndex); + } + + String getGoogleMapsPolygonId() { + return googleMapsPolygonId; + } + + boolean consumeTapEvents() { + return consumeTapEvents; + } +} diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java new file mode 100644 index 000000000000..7abbcfaa634e --- /dev/null +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java @@ -0,0 +1,24 @@ +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.LatLng; +import java.util.List; + +/** Receiver of Polygon configuration options. */ +interface PolygonOptionsSink { + + void setConsumeTapEvents(boolean consumetapEvents); + + void setFillColor(int color); + + void setStrokeColor(int color); + + void setGeodesic(boolean geodesic); + + void setPoints(List points); + + void setVisible(boolean visible); + + void setStrokeWidth(float width); + + void setZIndex(float zIndex); +} diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java new file mode 100644 index 000000000000..992e8669e403 --- /dev/null +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java @@ -0,0 +1,112 @@ +// Copyright 2019 The Chromium 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.Polygon; +import com.google.android.gms.maps.model.PolygonOptions; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class PolygonsController { + + private final Map polygonIdToController; + private final Map googleMapsPolygonIdToDartPolygonId; + private final MethodChannel methodChannel; + private GoogleMap googleMap; + + PolygonsController(MethodChannel methodChannel) { + this.polygonIdToController = new HashMap<>(); + this.googleMapsPolygonIdToDartPolygonId = new HashMap<>(); + this.methodChannel = methodChannel; + } + + void setGoogleMap(GoogleMap googleMap) { + this.googleMap = googleMap; + } + + void addPolygons(List polygonsToAdd) { + if (polygonsToAdd != null) { + for (Object polygonToAdd : polygonsToAdd) { + addPolygon(polygonToAdd); + } + } + } + + void changePolygons(List polygonsToChange) { + if (polygonsToChange != null) { + for (Object polygonToChange : polygonsToChange) { + changePolygon(polygonToChange); + } + } + } + + void removePolygons(List polygonIdsToRemove) { + if (polygonIdsToRemove == null) { + return; + } + for (Object rawPolygonId : polygonIdsToRemove) { + if (rawPolygonId == null) { + continue; + } + String polygonId = (String) rawPolygonId; + final PolygonController polygonController = polygonIdToController.remove(polygonId); + if (polygonController != null) { + polygonController.remove(); + googleMapsPolygonIdToDartPolygonId.remove(polygonController.getGoogleMapsPolygonId()); + } + } + } + + boolean onPolygonTap(String googlePolygonId) { + String polygonId = googleMapsPolygonIdToDartPolygonId.get(googlePolygonId); + if (polygonId == null) { + return false; + } + methodChannel.invokeMethod("polygon#onTap", Convert.polygonIdToJson(polygonId)); + PolygonController polygonController = polygonIdToController.get(polygonId); + if (polygonController != null) { + return polygonController.consumeTapEvents(); + } + return false; + } + + private void addPolygon(Object polygon) { + if (polygon == null) { + return; + } + PolygonBuilder polygonBuilder = new PolygonBuilder(); + String polygonId = Convert.interpretPolygonOptions(polygon, polygonBuilder); + PolygonOptions options = polygonBuilder.build(); + addPolygon(polygonId, options, polygonBuilder.consumeTapEvents()); + } + + private void addPolygon( + String polygonId, PolygonOptions polygonOptions, boolean consumeTapEvents) { + final Polygon polygon = googleMap.addPolygon(polygonOptions); + PolygonController controller = new PolygonController(polygon, consumeTapEvents); + polygonIdToController.put(polygonId, controller); + googleMapsPolygonIdToDartPolygonId.put(polygon.getId(), polygonId); + } + + private void changePolygon(Object polygon) { + if (polygon == null) { + return; + } + String polygonId = getPolygonId(polygon); + PolygonController polygonController = polygonIdToController.get(polygonId); + if (polygonController != null) { + Convert.interpretPolygonOptions(polygon, polygonController); + } + } + + @SuppressWarnings("unchecked") + private static String getPolygonId(Object polygon) { + Map polygonMap = (Map) polygon; + return (String) polygonMap.get("polygonId"); + } +} diff --git a/packages/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/example/lib/main.dart index 5a8d28c5c2d6..1d17f139ead4 100644 --- a/packages/google_maps_flutter/example/lib/main.dart +++ b/packages/google_maps_flutter/example/lib/main.dart @@ -12,6 +12,7 @@ import 'move_camera.dart'; import 'page.dart'; import 'place_circle.dart'; import 'place_marker.dart'; +import 'place_polygon.dart'; import 'place_polyline.dart'; import 'scrolling_map.dart'; @@ -25,6 +26,7 @@ final List _allPages = [ MarkerIconsPage(), ScrollingMapPage(), PlacePolylinePage(), + PlacePolygonPage(), PlaceCirclePage(), ]; diff --git a/packages/google_maps_flutter/example/lib/place_polygon.dart b/packages/google_maps_flutter/example/lib/place_polygon.dart new file mode 100644 index 000000000000..1e48dfe789f1 --- /dev/null +++ b/packages/google_maps_flutter/example/lib/place_polygon.dart @@ -0,0 +1,235 @@ +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'page.dart'; + +class PlacePolygonPage extends Page { + PlacePolygonPage() : super(const Icon(Icons.linear_scale), 'Place polygon'); + + @override + Widget build(BuildContext context) { + return const PlacePolygonBody(); + } +} + +class PlacePolygonBody extends StatefulWidget { + const PlacePolygonBody(); + + @override + State createState() => PlacePolygonBodyState(); +} + +class PlacePolygonBodyState extends State { + PlacePolygonBodyState(); + + GoogleMapController controller; + Map polygons = {}; + int _polygonIdCounter = 1; + PolygonId selectedPolygon; + + // Values when toggling polygon color + int strokeColorsIndex = 0; + int fillColorsIndex = 0; + List colors = [ + Colors.purple, + Colors.red, + Colors.green, + Colors.pink, + ]; + + // Values when toggling polygon width + int widthsIndex = 0; + List widths = [10, 20, 5]; + + void _onMapCreated(GoogleMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + void _onPolygonTapped(PolygonId polygonId) { + setState(() { + selectedPolygon = polygonId; + }); + } + + void _remove() { + setState(() { + if (polygons.containsKey(selectedPolygon)) { + polygons.remove(selectedPolygon); + } + selectedPolygon = null; + }); + } + + void _add() { + final int polygonCount = polygons.length; + + if (polygonCount == 12) { + return; + } + + final String polygonIdVal = 'polygon_id_$_polygonIdCounter'; + _polygonIdCounter++; + final PolygonId polygonId = PolygonId(polygonIdVal); + + final Polygon polygon = Polygon( + polygonId: polygonId, + consumeTapEvents: true, + strokeColor: Colors.orange, + strokeWidth: 5, + fillColor: Colors.green, + points: _createPoints(), + onTap: () { + _onPolygonTapped(polygonId); + }, + ); + + setState(() { + polygons[polygonId] = polygon; + }); + } + + void _toggleGeodesic() { + final Polygon polygon = polygons[selectedPolygon]; + setState(() { + polygons[selectedPolygon] = polygon.copyWith( + geodesicParam: !polygon.geodesic, + ); + }); + } + + void _toggleVisible() { + final Polygon polygon = polygons[selectedPolygon]; + setState(() { + polygons[selectedPolygon] = polygon.copyWith( + visibleParam: !polygon.visible, + ); + }); + } + + void _changeStrokeColor() { + final Polygon polygon = polygons[selectedPolygon]; + setState(() { + polygons[selectedPolygon] = polygon.copyWith( + strokeColorParam: colors[++strokeColorsIndex % colors.length], + ); + }); + } + + void _changeFillColor() { + final Polygon polygon = polygons[selectedPolygon]; + setState(() { + polygons[selectedPolygon] = polygon.copyWith( + fillColorParam: colors[++fillColorsIndex % colors.length], + ); + }); + } + + void _changeWidth() { + final Polygon polygon = polygons[selectedPolygon]; + setState(() { + polygons[selectedPolygon] = polygon.copyWith( + strokeWidthParam: widths[++widthsIndex % widths.length], + ); + }); + } + + @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: LatLng(52.4478, -3.5402), + zoom: 7.0, + ), + polygons: Set.of(polygons.values), + onMapCreated: _onMapCreated, + ), + ), + ), + Expanded( + child: SingleChildScrollView( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + Column( + children: [ + FlatButton( + child: const Text('add'), + onPressed: _add, + ), + FlatButton( + child: const Text('remove'), + onPressed: (selectedPolygon == null) ? null : _remove, + ), + FlatButton( + child: const Text('toggle visible'), + onPressed: + (selectedPolygon == null) ? null : _toggleVisible, + ), + FlatButton( + child: const Text('toggle geodesic'), + onPressed: (selectedPolygon == null) + ? null + : _toggleGeodesic, + ), + ], + ), + Column( + children: [ + FlatButton( + child: const Text('change stroke width'), + onPressed: + (selectedPolygon == null) ? null : _changeWidth, + ), + FlatButton( + child: const Text('change stroke color'), + onPressed: (selectedPolygon == null) + ? null + : _changeStrokeColor, + ), + FlatButton( + child: const Text('change fill color'), + onPressed: (selectedPolygon == null) + ? null + : _changeFillColor, + ), + ], + ) + ], + ) + ], + ), + ), + ), + ], + ); + } + + List _createPoints() { + final List points = []; + final double offset = _polygonIdCounter.ceilToDouble(); + points.add(_createLatLng(51.2395 + offset, -3.4314)); + points.add(_createLatLng(53.5234 + offset, -3.5314)); + points.add(_createLatLng(52.4351 + offset, -4.5235)); + points.add(_createLatLng(52.1231 + offset, -5.0829)); + return points; + } + + LatLng _createLatLng(double lat, double lng) { + return LatLng(lat, lng); + } +} diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h index 60e1d4260a78..8c39537f896e 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h @@ -6,6 +6,7 @@ #import #import "GoogleMapCircleController.h" #import "GoogleMapMarkerController.h" +#import "GoogleMapPolygonController.h" #import "GoogleMapPolylineController.h" // Defines map UI options writable from Flutter. diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m index 5dd1d250ef10..fa496387bc2c 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -54,6 +54,7 @@ @implementation FLTGoogleMapController { // https://github.com/flutter/flutter/issues/27550 BOOL _cameraDidInitialSetup; FLTMarkersController* _markersController; + FLTPolygonsController* _polygonsController; FLTPolylinesController* _polylinesController; FLTCirclesController* _circlesController; } @@ -86,6 +87,9 @@ - (instancetype)initWithFrame:(CGRect)frame _markersController = [[FLTMarkersController alloc] init:_channel mapView:_mapView registrar:registrar]; + _polygonsController = [[FLTPolygonsController alloc] init:_channel + mapView:_mapView + registrar:registrar]; _polylinesController = [[FLTPolylinesController alloc] init:_channel mapView:_mapView registrar:registrar]; @@ -96,6 +100,10 @@ - (instancetype)initWithFrame:(CGRect)frame if ([markersToAdd isKindOfClass:[NSArray class]]) { [_markersController addMarkers:markersToAdd]; } + id polygonsToAdd = args[@"polygonToAdd"]; + if ([polygonsToAdd isKindOfClass:[NSArray class]]) { + [_polygonsController addPolygons:polygonsToAdd]; + } id polylinesToAdd = args[@"polylinesToAdd"]; if ([polylinesToAdd isKindOfClass:[NSArray class]]) { [_polylinesController addPolylines:polylinesToAdd]; @@ -155,6 +163,20 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [_markersController removeMarkerIds:markerIdsToRemove]; } result(nil); + } else if ([call.method isEqualToString:@"polygons#update"]) { + id polygonsToAdd = call.arguments[@"polygonsToAdd"]; + if ([polygonsToAdd isKindOfClass:[NSArray class]]) { + [_polygonsController addPolygons:polygonsToAdd]; + } + id polygonsToChange = call.arguments[@"polygonsToChange"]; + if ([polygonsToChange isKindOfClass:[NSArray class]]) { + [_polygonsController changePolygons:polygonsToChange]; + } + id polygonIdsToRemove = call.arguments[@"polygonIdsToRemove"]; + if ([polygonIdsToRemove isKindOfClass:[NSArray class]]) { + [_polygonsController removePolygonIds:polygonIdsToRemove]; + } + result(nil); } else if ([call.method isEqualToString:@"polylines#update"]) { id polylinesToAdd = call.arguments[@"polylinesToAdd"]; if ([polylinesToAdd isKindOfClass:[NSArray class]]) { @@ -325,6 +347,8 @@ - (void)mapView:(GMSMapView*)mapView didTapOverlay:(GMSOverlay*)overlay { NSString* overlayId = overlay.userData[0]; if ([_polylinesController hasPolylineWithId:overlayId]) { [_polylinesController onPolylineTap:overlayId]; + } else if ([_polygonsController hasPolygonWithId:overlayId]) { + [_polygonsController onPolygonTap:overlayId]; } else if ([_circlesController hasCircleWithId:overlayId]) { [_circlesController onCircleTap:overlayId]; } diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h b/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h new file mode 100644 index 000000000000..c7613fde5f93 --- /dev/null +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h @@ -0,0 +1,37 @@ +// Copyright 2018 The Chromium 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 + +// Defines polygon UI options writable from Flutter. +@protocol FLTGoogleMapPolygonOptionsSink +- (void)setConsumeTapEvents:(BOOL)consume; +- (void)setVisible:(BOOL)visible; +- (void)setFillColor:(UIColor*)color; +- (void)setStrokeColor:(UIColor*)color; +- (void)setStrokeWidth:(CGFloat)width; +- (void)setPoints:(NSArray*)points; +- (void)setZIndex:(int)zIndex; +@end + +// Defines polygon controllable by Flutter. +@interface FLTGoogleMapPolygonController : NSObject +@property(atomic, readonly) NSString* polygonId; +- (instancetype)initPolygonWithPath:(GMSMutablePath*)path + polygonId:(NSString*)polygonId + mapView:(GMSMapView*)mapView; +- (void)removePolygon; +@end + +@interface FLTPolygonsController : NSObject +- (instancetype)init:(FlutterMethodChannel*)methodChannel + mapView:(GMSMapView*)mapView + registrar:(NSObject*)registrar; +- (void)addPolygons:(NSArray*)polygonsToAdd; +- (void)changePolygons:(NSArray*)polygonsToChange; +- (void)removePolygonIds:(NSArray*)polygonIdsToRemove; +- (void)onPolygonTap:(NSString*)polygonId; +- (bool)hasPolygonWithId:(NSString*)polygonId; +@end diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m new file mode 100644 index 000000000000..4bc4f1780338 --- /dev/null +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m @@ -0,0 +1,189 @@ +// Copyright 2018 The Chromium 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 "GoogleMapPolygonController.h" +#import "JsonConversions.h" + +@implementation FLTGoogleMapPolygonController { + GMSPolygon* _polygon; + GMSMapView* _mapView; +} +- (instancetype)initPolygonWithPath:(GMSMutablePath*)path + polygonId:(NSString*)polygonId + mapView:(GMSMapView*)mapView { + self = [super init]; + if (self) { + _polygon = [GMSPolygon polygonWithPath:path]; + _mapView = mapView; + _polygonId = polygonId; + _polygon.userData = @[ polygonId ]; + } + return self; +} + +- (void)removePolygon { + _polygon.map = nil; +} + +#pragma mark - FLTGoogleMapPolygonOptionsSink methods + +- (void)setConsumeTapEvents:(BOOL)consumes { + _polygon.tappable = consumes; +} +- (void)setVisible:(BOOL)visible { + _polygon.map = visible ? _mapView : nil; +} +- (void)setZIndex:(int)zIndex { + _polygon.zIndex = zIndex; +} +- (void)setPoints:(NSArray*)points { + GMSMutablePath* path = [GMSMutablePath path]; + + for (CLLocation* location in points) { + [path addCoordinate:location.coordinate]; + } + _polygon.path = path; +} + +- (void)setFillColor:(UIColor*)color { + _polygon.fillColor = color; +} +- (void)setStrokeColor:(UIColor*)color { + _polygon.strokeColor = color; +} +- (void)setStrokeWidth:(CGFloat)width { + _polygon.strokeWidth = width; +} +@end + +static int ToInt(NSNumber* data) { return [FLTGoogleMapJsonConversions toInt:data]; } + +static BOOL ToBool(NSNumber* data) { return [FLTGoogleMapJsonConversions toBool:data]; } + +static NSArray* ToPoints(NSArray* data) { + return [FLTGoogleMapJsonConversions toPoints:data]; +} + +static UIColor* ToColor(NSNumber* data) { return [FLTGoogleMapJsonConversions toColor:data]; } + +static void InterpretPolygonOptions(NSDictionary* data, id sink, + NSObject* registrar) { + NSNumber* consumeTapEvents = data[@"consumeTapEvents"]; + if (consumeTapEvents) { + [sink setConsumeTapEvents:ToBool(consumeTapEvents)]; + } + + NSNumber* visible = data[@"visible"]; + if (visible) { + [sink setVisible:ToBool(visible)]; + } + + NSNumber* zIndex = data[@"zIndex"]; + if (zIndex) { + [sink setZIndex:ToInt(zIndex)]; + } + + NSArray* points = data[@"points"]; + if (points) { + [sink setPoints:ToPoints(points)]; + } + + NSNumber* fillColor = data[@"fillColor"]; + if (fillColor) { + [sink setFillColor:ToColor(fillColor)]; + } + + NSNumber* strokeColor = data[@"strokeColor"]; + if (strokeColor) { + [sink setStrokeColor:ToColor(strokeColor)]; + } + + NSNumber* strokeWidth = data[@"strokeWidth"]; + if (strokeWidth) { + [sink setStrokeWidth:ToInt(strokeWidth)]; + } +} + +@implementation FLTPolygonsController { + NSMutableDictionary* _polygonIdToController; + FlutterMethodChannel* _methodChannel; + NSObject* _registrar; + GMSMapView* _mapView; +} +- (instancetype)init:(FlutterMethodChannel*)methodChannel + mapView:(GMSMapView*)mapView + registrar:(NSObject*)registrar { + self = [super init]; + if (self) { + _methodChannel = methodChannel; + _mapView = mapView; + _polygonIdToController = [NSMutableDictionary dictionaryWithCapacity:1]; + _registrar = registrar; + } + return self; +} +- (void)addPolygons:(NSArray*)polygonsToAdd { + for (NSDictionary* polygon in polygonsToAdd) { + GMSMutablePath* path = [FLTPolygonsController getPath:polygon]; + NSString* polygonId = [FLTPolygonsController getPolygonId:polygon]; + FLTGoogleMapPolygonController* controller = + [[FLTGoogleMapPolygonController alloc] initPolygonWithPath:path + polygonId:polygonId + mapView:_mapView]; + InterpretPolygonOptions(polygon, controller, _registrar); + _polygonIdToController[polygonId] = controller; + } +} +- (void)changePolygons:(NSArray*)polygonsToChange { + for (NSDictionary* polygon in polygonsToChange) { + NSString* polygonId = [FLTPolygonsController getPolygonId:polygon]; + FLTGoogleMapPolygonController* controller = _polygonIdToController[polygonId]; + if (!controller) { + continue; + } + InterpretPolygonOptions(polygon, controller, _registrar); + } +} +- (void)removePolygonIds:(NSArray*)polygonIdsToRemove { + for (NSString* polygonId in polygonIdsToRemove) { + if (!polygonId) { + continue; + } + FLTGoogleMapPolygonController* controller = _polygonIdToController[polygonId]; + if (!controller) { + continue; + } + [controller removePolygon]; + [_polygonIdToController removeObjectForKey:polygonId]; + } +} +- (void)onPolygonTap:(NSString*)polygonId { + if (!polygonId) { + return; + } + FLTGoogleMapPolygonController* controller = _polygonIdToController[polygonId]; + if (!controller) { + return; + } + [_methodChannel invokeMethod:@"polygon#onTap" arguments:@{@"polygonId" : polygonId}]; +} +- (bool)hasPolygonWithId:(NSString*)polygonId { + if (!polygonId) { + return false; + } + return _polygonIdToController[polygonId] != nil; +} ++ (GMSMutablePath*)getPath:(NSDictionary*)polygon { + NSArray* pointArray = polygon[@"points"]; + NSArray* points = ToPoints(pointArray); + GMSMutablePath* path = [GMSMutablePath path]; + for (CLLocation* location in points) { + [path addCoordinate:location.coordinate]; + } + return path; +} ++ (NSString*)getPolygonId:(NSDictionary*)polygon { + return polygon[@"polygonId"]; +} +@end diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h b/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h index 5b8942968e4c..645ace34f9ed 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h @@ -7,6 +7,7 @@ #import "GoogleMapCircleController.h" #import "GoogleMapController.h" #import "GoogleMapMarkerController.h" +#import "GoogleMapPolygonController.h" #import "GoogleMapPolylineController.h" @interface FLTGoogleMapsPlugin : NSObject diff --git a/packages/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/lib/google_maps_flutter.dart index 14adf45578b0..91f037192255 100644 --- a/packages/google_maps_flutter/lib/google_maps_flutter.dart +++ b/packages/google_maps_flutter/lib/google_maps_flutter.dart @@ -24,6 +24,8 @@ part 'src/marker.dart'; part 'src/marker_updates.dart'; part 'src/location.dart'; part 'src/pattern_item.dart'; +part 'src/polygon.dart'; +part 'src/polygon_updates.dart'; part 'src/polyline.dart'; part 'src/polyline_updates.dart'; part 'src/circle.dart'; diff --git a/packages/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/lib/src/controller.dart index 5cc406528e7a..b3f3990416f5 100644 --- a/packages/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/lib/src/controller.dart @@ -66,6 +66,9 @@ class GoogleMapController { case 'polyline#onTap': _googleMapState.onPolylineTap(call.arguments['polylineId']); break; + case 'polygon#onTap': + _googleMapState.onPolygonTap(call.arguments['polygonId']); + break; case 'circle#onTap': _googleMapState.onCircleTap(call.arguments['circleId']); break; @@ -117,6 +120,23 @@ class GoogleMapController { ); } + /// Updates polygon 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 _updatePolygons(_PolygonUpdates polygonUpdates) async { + assert(polygonUpdates != null); + // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. + // https://github.com/flutter/flutter/issues/26431 + // ignore: strong_mode_implicit_dynamic_method + await channel.invokeMethod( + 'polygons#update', + polygonUpdates._toMap(), + ); + } + /// Updates polyline configuration. /// /// Change listeners are notified once the update has been made on the diff --git a/packages/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/lib/src/google_map.dart index 2a2562fbb351..775b94a329ba 100644 --- a/packages/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/lib/src/google_map.dart @@ -31,6 +31,7 @@ class GoogleMap extends StatefulWidget { this.myLocationEnabled = false, this.myLocationButtonEnabled = true, this.markers, + this.polygons, this.polylines, this.circles, this.onCameraMoveStarted, @@ -75,6 +76,9 @@ class GoogleMap extends StatefulWidget { /// Markers to be placed on the map. final Set markers; + /// Polygons to be placed on the map. + final Set polygons; + /// Polylines to be placed on the map. final Set polylines; @@ -166,6 +170,7 @@ class _GoogleMapState extends State { Completer(); Map _markers = {}; + Map _polygons = {}; Map _polylines = {}; Map _circles = {}; _GoogleMapOptions _googleMapOptions; @@ -176,6 +181,7 @@ class _GoogleMapState extends State { 'initialCameraPosition': widget.initialCameraPosition?._toMap(), 'options': _googleMapOptions.toMap(), 'markersToAdd': _serializeMarkerSet(widget.markers), + 'polygonsToAdd': _serializePolygonSet(widget.polygons), 'polylinesToAdd': _serializePolylineSet(widget.polylines), 'circlesToAdd': _serializeCircleSet(widget.circles), }; @@ -206,6 +212,7 @@ class _GoogleMapState extends State { super.initState(); _googleMapOptions = _GoogleMapOptions.fromWidget(widget); _markers = _keyByMarkerId(widget.markers); + _polygons = _keyByPolygonId(widget.polygons); _polylines = _keyByPolylineId(widget.polylines); _circles = _keyByCircleId(widget.circles); } @@ -215,6 +222,7 @@ class _GoogleMapState extends State { super.didUpdateWidget(oldWidget); _updateOptions(); _updateMarkers(); + _updatePolygons(); _updatePolylines(); _updateCircles(); } @@ -238,6 +246,13 @@ class _GoogleMapState extends State { _markers = _keyByMarkerId(widget.markers); } + void _updatePolygons() async { + final GoogleMapController controller = await _controller.future; + controller._updatePolygons( + _PolygonUpdates.from(_polygons.values.toSet(), widget.polygons)); + _polygons = _keyByPolygonId(widget.polygons); + } + void _updatePolylines() async { final GoogleMapController controller = await _controller.future; controller._updatePolylines( @@ -272,6 +287,12 @@ class _GoogleMapState extends State { } } + void onPolygonTap(String polygonIdParam) { + assert(polygonIdParam != null); + final PolygonId polygonId = PolygonId(polygonIdParam); + _polygons[polygonId].onTap(); + } + void onPolylineTap(String polylineIdParam) { assert(polylineIdParam != null); final PolylineId polylineId = PolylineId(polylineIdParam); diff --git a/packages/google_maps_flutter/lib/src/polygon.dart b/packages/google_maps_flutter/lib/src/polygon.dart new file mode 100644 index 000000000000..439a5f5403b0 --- /dev/null +++ b/packages/google_maps_flutter/lib/src/polygon.dart @@ -0,0 +1,179 @@ +part of google_maps_flutter; + +/// Uniquely identifies a [Polygon] among [GoogleMap] polygons. +/// +/// This does not have to be globally unique, only unique among the list. +@immutable +class PolygonId { + PolygonId(this.value) : assert(value != null); + + /// value of the [PolygonId]. + final String value; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final PolygonId typedOther = other; + return value == typedOther.value; + } + + @override + int get hashCode => value.hashCode; + + @override + String toString() { + return 'PolygonId{value: $value}'; + } +} + +/// Draws a polygon through geographical locations on the map. +@immutable +class Polygon { + const Polygon({ + @required this.polygonId, + this.consumeTapEvents = false, + this.fillColor = Colors.black, + this.geodesic = false, + this.points = const [], + this.strokeColor = Colors.black, + this.strokeWidth = 10, + this.visible = true, + this.zIndex = 0, + this.onTap, + }); + + /// Uniquely identifies a [Polygon]. + final PolygonId polygonId; + + /// True if the [Polygon] consumes tap events. + /// + /// If this is false, [onTap] callback will not be triggered. + final bool consumeTapEvents; + + /// Fill color in ARGB format, the same format used by Color. The default value is black (0xff000000). + final Color fillColor; + + /// Indicates whether the segments of the polygon should be drawn as geodesics, as opposed to straight lines + /// on the Mercator projection. + /// + /// A geodesic is the shortest path between two points on the Earth's surface. + /// The geodesic curve is constructed assuming the Earth is a sphere + final bool geodesic; + + /// The vertices of the polygon to be drawn. + /// + /// Line segments are drawn between consecutive points. A polygon is not closed by + /// default; to form a closed polygon, the start and end points must be the same. + final List points; + + /// True if the marker is visible. + final bool visible; + + /// Line color in ARGB format, the same format used by Color. The default value is black (0xff000000). + final Color strokeColor; + + /// Width of the polygon, used to define the width of the line to be drawn. + /// + /// The width is constant and independent of the camera's zoom level. + /// The default value is 10. + final int strokeWidth; + + /// The z-index of the polygon, used to determine relative drawing order of + /// map overlays. + /// + /// Overlays are drawn in order of z-index, so that lower values means drawn + /// earlier, and thus appearing to be closer to the surface of the Earth. + final int zIndex; + + /// Callbacks to receive tap events for polygon placed on this map. + final VoidCallback onTap; + + /// Creates a new [Polygon] object whose values are the same as this instance, + /// unless overwritten by the specified parameters. + Polygon copyWith({ + bool consumeTapEventsParam, + Color fillColorParam, + bool geodesicParam, + List pointsParam, + Color strokeColorParam, + int strokeWidthParam, + bool visibleParam, + int zIndexParam, + VoidCallback onTapParam, + }) { + return Polygon( + polygonId: polygonId, + consumeTapEvents: consumeTapEventsParam ?? consumeTapEvents, + fillColor: fillColorParam ?? fillColor, + geodesic: geodesicParam ?? geodesic, + points: pointsParam ?? points, + strokeColor: strokeColorParam ?? strokeColor, + strokeWidth: strokeWidthParam ?? strokeWidth, + visible: visibleParam ?? visible, + onTap: onTapParam ?? onTap, + zIndex: zIndexParam ?? zIndex, + ); + } + + dynamic _toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('polygonId', polygonId.value); + addIfPresent('consumeTapEvents', consumeTapEvents); + addIfPresent('fillColor', fillColor.value); + addIfPresent('geodesic', geodesic); + addIfPresent('strokeColor', strokeColor.value); + addIfPresent('strokeWidth', strokeWidth); + addIfPresent('visible', visible); + addIfPresent('zIndex', zIndex); + + if (points != null) { + json['points'] = _pointsToJson(); + } + + return json; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final Polygon typedOther = other; + return polygonId == typedOther.polygonId; + } + + @override + int get hashCode => polygonId.hashCode; + + dynamic _pointsToJson() { + final List result = []; + for (final LatLng point in points) { + result.add(point._toJson()); + } + return result; + } +} + +Map _keyByPolygonId(Iterable polygons) { + if (polygons == null) { + return {}; + } + return Map.fromEntries(polygons.map((Polygon polygon) => + MapEntry(polygon.polygonId, polygon))); +} + +List> _serializePolygonSet(Set polygons) { + if (polygons == null) { + return null; + } + return polygons + .map>((Polygon p) => p._toJson()) + .toList(); +} diff --git a/packages/google_maps_flutter/lib/src/polygon_updates.dart b/packages/google_maps_flutter/lib/src/polygon_updates.dart new file mode 100644 index 000000000000..c7a04f426074 --- /dev/null +++ b/packages/google_maps_flutter/lib/src/polygon_updates.dart @@ -0,0 +1,90 @@ +// Copyright 2018 The Chromium 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; + +/// [Polygon] update events to be applied to the [GoogleMap]. +/// +/// Used in [GoogleMapController] when the map is updated. +class _PolygonUpdates { + /// Computes [_PolygonUpdates] given previous and current [Polygon]s. + _PolygonUpdates.from(Set previous, Set current) { + if (previous == null) { + previous = Set.identity(); + } + + if (current == null) { + current = Set.identity(); + } + + final Map previousPolygons = _keyByPolygonId(previous); + final Map currentPolygons = _keyByPolygonId(current); + + final Set prevPolygonIds = previousPolygons.keys.toSet(); + final Set currentPolygonIds = currentPolygons.keys.toSet(); + + Polygon idToCurrentPolygon(PolygonId id) { + return currentPolygons[id]; + } + + final Set _polygonIdsToRemove = + prevPolygonIds.difference(currentPolygonIds); + + final Set _polygonsToAdd = currentPolygonIds + .difference(prevPolygonIds) + .map(idToCurrentPolygon) + .toSet(); + + final Set _polygonsToChange = currentPolygonIds + .intersection(prevPolygonIds) + .map(idToCurrentPolygon) + .toSet(); + + polygonsToAdd = _polygonsToAdd; + polygonIdsToRemove = _polygonIdsToRemove; + polygonsToChange = _polygonsToChange; + } + + Set polygonsToAdd; + Set polygonIdsToRemove; + Set polygonsToChange; + + Map _toMap() { + final Map updateMap = {}; + + void addIfNonNull(String fieldName, dynamic value) { + if (value != null) { + updateMap[fieldName] = value; + } + } + + addIfNonNull('polygonsToAdd', _serializePolygonSet(polygonsToAdd)); + addIfNonNull('polygonsToChange', _serializePolygonSet(polygonsToChange)); + addIfNonNull('polygonIdsToRemove', + polygonIdsToRemove.map((PolygonId m) => m.value).toList()); + + return updateMap; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final _PolygonUpdates typedOther = other; + return setEquals(polygonsToAdd, typedOther.polygonsToAdd) && + setEquals(polygonIdsToRemove, typedOther.polygonIdsToRemove) && + setEquals(polygonsToChange, typedOther.polygonsToChange); + } + + @override + int get hashCode => + hashValues(polygonsToAdd, polygonIdsToRemove, polygonsToChange); + + @override + String toString() { + return '_PolygonUpdates{polygonsToAdd: $polygonsToAdd, ' + 'polygonIdsToRemove: $polygonIdsToRemove, ' + 'polygonsToChange: $polygonsToChange}'; + } +} diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index ecf94672126d..9af1da63c62d 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/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. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter -version: 0.5.14+1 +version: 0.5.15 dependencies: flutter: diff --git a/packages/google_maps_flutter/test/fake_maps_controllers.dart b/packages/google_maps_flutter/test/fake_maps_controllers.dart index 805fa7a2648b..f9dd2c71b861 100644 --- a/packages/google_maps_flutter/test/fake_maps_controllers.dart +++ b/packages/google_maps_flutter/test/fake_maps_controllers.dart @@ -16,6 +16,7 @@ class FakePlatformGoogleMap { channel.setMockMethodCallHandler(onMethodCall); updateOptions(params['options']); updateMarkers(params); + updatePolygons(params); updatePolylines(params); updateCircles(params); } @@ -52,6 +53,12 @@ class FakePlatformGoogleMap { Set markersToChange; + Set polygonIdsToRemove; + + Set polygonsToAdd; + + Set polygonsToChange; + Set polylineIdsToRemove; Set polylinesToAdd; @@ -72,6 +79,9 @@ class FakePlatformGoogleMap { case 'markers#update': updateMarkers(call.arguments); return Future.sync(() {}); + case 'polygons#update': + updatePolygons(call.arguments); + return Future.sync(() {}); case 'polylines#update': updatePolylines(call.arguments); return Future.sync(() {}); @@ -141,6 +151,53 @@ class FakePlatformGoogleMap { return result; } + void updatePolygons(Map polygonUpdates) { + if (polygonUpdates == null) { + return; + } + polygonsToAdd = _deserializePolygons(polygonUpdates['polygonsToAdd']); + polygonIdsToRemove = + _deserializePolygonIds(polygonUpdates['polygonIdsToRemove']); + polygonsToChange = _deserializePolygons(polygonUpdates['polygonsToChange']); + } + + Set _deserializePolygonIds(List polygonIds) { + if (polygonIds == null) { + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // https://github.com/flutter/flutter/issues/28312 + // ignore: prefer_collection_literals + return Set(); + } + return polygonIds.map((dynamic polygonId) => PolygonId(polygonId)).toSet(); + } + + Set _deserializePolygons(dynamic polygons) { + if (polygons == null) { + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // https://github.com/flutter/flutter/issues/28312 + // ignore: prefer_collection_literals + return Set(); + } + final List polygonsData = polygons; + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // https://github.com/flutter/flutter/issues/28312 + // ignore: prefer_collection_literals + final Set result = Set(); + for (Map polygonData in polygonsData) { + final String polygonId = polygonData['polygonId']; + final bool visible = polygonData['visible']; + final bool geodesic = polygonData['geodesic']; + + result.add(Polygon( + polygonId: PolygonId(polygonId), + visible: visible, + geodesic: geodesic, + )); + } + + return result; + } + void updatePolylines(Map polylineUpdates) { if (polylineUpdates == null) { return; diff --git a/packages/google_maps_flutter/test/polygon_updates_test.dart b/packages/google_maps_flutter/test/polygon_updates_test.dart new file mode 100644 index 000000000000..f7b2c1cb8286 --- /dev/null +++ b/packages/google_maps_flutter/test/polygon_updates_test.dart @@ -0,0 +1,200 @@ +// Copyright 2018 The Chromium 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'; + +Set _toSet({Polygon p1, Polygon p2, Polygon p3}) { + final Set res = Set.identity(); + if (p1 != null) { + res.add(p1); + } + if (p2 != null) { + res.add(p2); + } + if (p3 != null) { + res.add(p3); + } + return res; +} + +Widget _mapWithPolygons(Set polygons) { + return Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + polygons: polygons, + ), + ); +} + +void main() { + final FakePlatformViewsController fakePlatformViewsController = + FakePlatformViewsController(); + + setUpAll(() { + SystemChannels.platform_views.setMockMethodCallHandler( + fakePlatformViewsController.fakePlatformViewsMethodHandler); + }); + + setUp(() { + fakePlatformViewsController.reset(); + }); + + testWidgets('Initializing a polygon', (WidgetTester tester) async { + final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + expect(platformGoogleMap.polygonsToAdd.length, 1); + + final Polygon initializedPolygon = platformGoogleMap.polygonsToAdd.first; + expect(initializedPolygon, equals(p1)); + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polygonsToChange.isEmpty, true); + }); + + testWidgets("Adding a polygon", (WidgetTester tester) async { + final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + final Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); + + await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1, p2: p2))); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + expect(platformGoogleMap.polygonsToAdd.length, 1); + + final Polygon addedPolygon = platformGoogleMap.polygonsToAdd.first; + expect(addedPolygon, equals(p2)); + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + + expect(platformGoogleMap.polygonsToChange.length, 1); + expect(platformGoogleMap.polygonsToChange.first, equals(p1)); + }); + + testWidgets("Removing a polygon", (WidgetTester tester) async { + final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + + await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolygons(null)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + expect(platformGoogleMap.polygonIdsToRemove.length, 1); + expect(platformGoogleMap.polygonIdsToRemove.first, equals(p1.polygonId)); + + expect(platformGoogleMap.polygonsToChange.isEmpty, true); + expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + }); + + testWidgets("Updating a polygon", (WidgetTester tester) async { + final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + final Polygon p2 = + Polygon(polygonId: PolygonId("polygon_1"), geodesic: true); + + await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p2))); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + expect(platformGoogleMap.polygonsToChange.length, 1); + expect(platformGoogleMap.polygonsToChange.first, equals(p2)); + + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + }); + + testWidgets("Updating a polygon", (WidgetTester tester) async { + final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + final Polygon p2 = + Polygon(polygonId: PolygonId("polygon_1"), geodesic: true); + + await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p2))); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + expect(platformGoogleMap.polygonsToChange.length, 1); + + final Polygon update = platformGoogleMap.polygonsToChange.first; + expect(update, equals(p2)); + expect(update.geodesic, true); + }); + + testWidgets("Multi Update", (WidgetTester tester) async { + Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); + final Set prev = _toSet(p1: p1, p2: p2); + p1 = Polygon(polygonId: PolygonId("polygon_1"), visible: false); + p2 = Polygon(polygonId: PolygonId("polygon_2"), geodesic: true); + final Set cur = _toSet(p1: p1, p2: p2); + + await tester.pumpWidget(_mapWithPolygons(prev)); + await tester.pumpWidget(_mapWithPolygons(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.polygonsToChange, cur); + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + }); + + testWidgets("Multi Update", (WidgetTester tester) async { + Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); + final Polygon p3 = Polygon(polygonId: PolygonId("polygon_3")); + final Set prev = _toSet(p2: p2, p3: p3); + + // p1 is added, p2 is updated, p3 is removed. + final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + p2 = Polygon(polygonId: PolygonId("polygon_2"), geodesic: true); + final Set cur = _toSet(p1: p1, p2: p2); + + await tester.pumpWidget(_mapWithPolygons(prev)); + await tester.pumpWidget(_mapWithPolygons(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.polygonsToChange.length, 1); + expect(platformGoogleMap.polygonsToAdd.length, 1); + expect(platformGoogleMap.polygonIdsToRemove.length, 1); + + expect(platformGoogleMap.polygonsToChange.first, equals(p2)); + expect(platformGoogleMap.polygonsToAdd.first, equals(p1)); + expect(platformGoogleMap.polygonIdsToRemove.first, equals(p3.polygonId)); + }); + + testWidgets( + "Partial Update", + (WidgetTester tester) async { + final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); + final Set prev = _toSet(p1: p1, p2: p2); + p2 = Polygon(polygonId: PolygonId("polygon_2"), geodesic: true); + final Set cur = _toSet(p1: p1, p2: p2); + + await tester.pumpWidget(_mapWithPolygons(prev)); + await tester.pumpWidget(_mapWithPolygons(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.polygonsToChange, _toSet(p2: p2)); + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + }, + // The test is currently broken due to a bug (we're updating all polygons + // instead of just the ones that were changed): + // https://github.com/flutter/flutter/issues/30764 + // TODO(amirh): enable this test when the issue is fixed. + skip: true, + ); +}