diff --git a/AUTHORS b/AUTHORS index 0ab7d0298832..f497b6c6ad5d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -27,6 +27,8 @@ Sarthak Verma Mike Diarmid Invertase Elliot Hesp +Aawaz Gyawali +EUI Limited Katarina Sheremet Thomas Stockx Sarbagya Dhaubanjar diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index 1ef4a154364e..fe9a33b97b65 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.6 + +* Add support for Polylines on GoogleMap. + ## 0.5.5 * Enable iOS accessibility. 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 5ced8142beb6..dc400fb602e3 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 @@ -11,10 +11,20 @@ import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.model.BitmapDescriptor; import com.google.android.gms.maps.model.BitmapDescriptorFactory; +import com.google.android.gms.maps.model.ButtCap; import com.google.android.gms.maps.model.CameraPosition; +import com.google.android.gms.maps.model.Cap; +import com.google.android.gms.maps.model.CustomCap; +import com.google.android.gms.maps.model.Dash; +import com.google.android.gms.maps.model.Dot; +import com.google.android.gms.maps.model.Gap; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; +import com.google.android.gms.maps.model.PatternItem; +import com.google.android.gms.maps.model.RoundCap; +import com.google.android.gms.maps.model.SquareCap; import io.flutter.view.FlutterMain; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -124,19 +134,19 @@ private static int toInt(Object o) { return ((Number) o).intValue(); } - static Object toJson(CameraPosition position) { + static Object latlngBoundsToJson(CameraPosition position) { if (position == null) { return null; } final Map data = new HashMap<>(); data.put("bearing", position.bearing); - data.put("target", toJson(position.target)); + data.put("target", latLngToJson(position.target)); data.put("tilt", position.tilt); data.put("zoom", position.zoom); return data; } - static Object toJson(String markerId) { + static Object markerIdToJson(String markerId) { if (markerId == null) { return null; } @@ -145,14 +155,23 @@ static Object toJson(String markerId) { return data; } - static Object toJson(LatLng latLng) { + static Object polylineIdToJson(String polylineId) { + if (polylineId == null) { + return null; + } + final Map data = new HashMap<>(1); + data.put("polylineId", polylineId); + return data; + } + + static Object latLngToJson(LatLng latLng) { return Arrays.asList(latLng.latitude, latLng.longitude); } - public static Object toJson(LatLngBounds latLngBounds) { + public static Object latlngBoundsToJson(LatLngBounds latLngBounds) { final Map arguments = new HashMap<>(2); - arguments.put("southwest", toJson(latLngBounds.southwest)); - arguments.put("northeast", toJson(latLngBounds.northeast)); + arguments.put("southwest", latLngToJson(latLngBounds.southwest)); + arguments.put("northeast", latLngToJson(latLngBounds.northeast)); return arguments; } @@ -323,4 +342,118 @@ private static void interpretInfoWindowOptions( sink.setInfoWindowAnchor(toFloat(anchorData.get(0)), toFloat(anchorData.get(1))); } } + + static String interpretPolylineOptions(Object o, PolylineOptionsSink sink) { + final Map data = toMap(o); + final Object consumeTapEvents = data.get("consumeTapEvents"); + if (consumeTapEvents != null) { + sink.setConsumeTapEvents(toBoolean(consumeTapEvents)); + } + final Object color = data.get("color"); + if (color != null) { + sink.setColor(toInt(color)); + } + final Object endCap = data.get("endCap"); + if (endCap != null) { + sink.setEndCap(toCap(endCap)); + } + final Object geodesic = data.get("geodesic"); + if (geodesic != null) { + sink.setGeodesic(toBoolean(geodesic)); + } + final Object jointType = data.get("jointType"); + if (jointType != null) { + sink.setJointType(toInt(jointType)); + } + final Object startCap = data.get("startCap"); + if (startCap != null) { + sink.setStartCap(toCap(startCap)); + } + final Object visible = data.get("visible"); + if (visible != null) { + sink.setVisible(toBoolean(visible)); + } + final Object width = data.get("width"); + if (width != null) { + sink.setWidth(toInt(width)); + } + 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 Object pattern = data.get("pattern"); + if (pattern != null) { + sink.setPattern(toPattern(pattern)); + } + final String polylineId = (String) data.get("polylineId"); + if (polylineId == null) { + throw new IllegalArgumentException("polylineId was null"); + } else { + return polylineId; + } + } + + private static List toPoints(Object o) { + final List data = toList(o); + final List points = new ArrayList<>(data.size()); + + for (Object ob : data) { + final List point = toList(ob); + points.add(new LatLng(toFloat(point.get(0)), toFloat(point.get(1)))); + } + return points; + } + + private static List toPattern(Object o) { + final List data = toList(o); + + if (data.isEmpty()) { + return null; + } + + final List pattern = new ArrayList<>(data.size()); + + for (Object ob : data) { + final List patternItem = toList(ob); + switch (toString(patternItem.get(0))) { + case "dot": + pattern.add(new Dot()); + break; + case "dash": + pattern.add(new Dash(toFloat(patternItem.get(1)))); + break; + case "gap": + pattern.add(new Gap(toFloat(patternItem.get(1)))); + break; + default: + throw new IllegalArgumentException("Cannot interpret " + pattern + " as PatternItem"); + } + } + + return pattern; + } + + private static Cap toCap(Object o) { + final List data = toList(o); + switch (toString(data.get(0))) { + case "buttCap": + return new ButtCap(); + case "roundCap": + return new RoundCap(); + case "squareCap": + return new SquareCap(); + case "customCap": + if (data.size() == 2) { + return new CustomCap(toBitmapDescriptor(data.get(1))); + } else { + return new CustomCap(toBitmapDescriptor(data.get(1)), toFloat(data.get(2))); + } + default: + throw new IllegalArgumentException("Cannot interpret " + o + " as Cap"); + } + } } 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 8e8200369486..e8929740347b 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 @@ -16,6 +16,7 @@ class GoogleMapBuilder implements GoogleMapOptionsSink { private boolean trackCameraPosition = false; private boolean myLocationEnabled = false; private Object initialMarkers; + private Object initialPolylines; GoogleMapController build( int id, Context context, AtomicInteger state, PluginRegistry.Registrar registrar) { @@ -25,6 +26,7 @@ GoogleMapController build( controller.setMyLocationEnabled(myLocationEnabled); controller.setTrackCameraPosition(trackCameraPosition); controller.setInitialMarkers(initialMarkers); + controller.setInitialPolylines(initialPolylines); return controller; } @@ -91,4 +93,9 @@ public void setMyLocationEnabled(boolean myLocationEnabled) { public void setInitialMarkers(Object initialMarkers) { this.initialMarkers = initialMarkers; } + + @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 e7a92c6773c1..a8a005387aec 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 @@ -29,6 +29,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.Polyline; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.PluginRegistry; @@ -48,6 +49,7 @@ final class GoogleMapController GoogleMap.OnCameraMoveStartedListener, GoogleMap.OnInfoWindowClickListener, GoogleMap.OnMarkerClickListener, + GoogleMap.OnPolylineClickListener, GoogleMapOptionsSink, MethodChannel.MethodCallHandler, OnMapReadyCallback, @@ -60,6 +62,7 @@ final class GoogleMapController private final MethodChannel methodChannel; private final PluginRegistry.Registrar registrar; private final MapView mapView; + private final Map polylines; private GoogleMap googleMap; private boolean trackCameraPosition = false; private boolean myLocationEnabled = false; @@ -69,7 +72,9 @@ final class GoogleMapController private final int registrarActivityHashCode; private final Context context; private final MarkersController markersController; + private final PolylinesController polylinesController; private List initialMarkers; + private List initialPolylines; GoogleMapController( int id, @@ -82,12 +87,14 @@ final class GoogleMapController this.activityState = activityState; this.registrar = registrar; this.mapView = new MapView(context, options); + this.polylines = new HashMap<>(); this.density = context.getResources().getDisplayMetrics().density; methodChannel = new MethodChannel(registrar.messenger(), "plugins.flutter.io/google_maps_" + id); methodChannel.setMethodCallHandler(this); this.registrarActivityHashCode = registrar.activity().hashCode(); this.markersController = new MarkersController(methodChannel); + this.polylinesController = new PolylinesController(methodChannel); } @Override @@ -157,10 +164,13 @@ public void onMapReady(GoogleMap googleMap) { googleMap.setOnCameraMoveListener(this); googleMap.setOnCameraIdleListener(this); googleMap.setOnMarkerClickListener(this); + googleMap.setOnPolylineClickListener(this); googleMap.setOnMapClickListener(this); updateMyLocationEnabled(); markersController.setGoogleMap(googleMap); + polylinesController.setGoogleMap(googleMap); updateInitialMarkers(); + updateInitialPolylines(); } @Override @@ -176,14 +186,14 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { case "map#update": { Convert.interpretGoogleMapOptions(call.argument("options"), this); - result.success(Convert.toJson(getCameraPosition())); + result.success(Convert.latlngBoundsToJson(getCameraPosition())); break; } case "map#getVisibleRegion": { if (googleMap != null) { LatLngBounds latLngBounds = googleMap.getProjection().getVisibleRegion().latLngBounds; - result.success(Convert.toJson(latLngBounds)); + result.success(Convert.latlngBoundsToJson(latLngBounds)); } else { result.error( "GoogleMap uninitialized", @@ -219,6 +229,17 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { result.success(null); break; } + case "polylines#update": + { + Object polylinesToAdd = call.argument("polylinesToAdd"); + polylinesController.addPolylines((List) polylinesToAdd); + Object polylinesToChange = call.argument("polylinesToChange"); + polylinesController.changePolylines((List) polylinesToChange); + Object polylineIdsToRemove = call.argument("polylineIdsToRemove"); + polylinesController.removePolylines((List) polylineIdsToRemove); + result.success(null); + break; + } case "map#isCompassEnabled": { result.success(googleMap.getUiSettings().isCompassEnabled()); @@ -260,7 +281,7 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { @Override public void onMapClick(LatLng latLng) { final Map arguments = new HashMap<>(2); - arguments.put("position", Convert.toJson(latLng)); + arguments.put("position", Convert.latLngToJson(latLng)); methodChannel.invokeMethod("map#onTap", arguments); } @@ -283,7 +304,7 @@ public void onCameraMove() { return; } final Map arguments = new HashMap<>(2); - arguments.put("position", Convert.toJson(googleMap.getCameraPosition())); + arguments.put("position", Convert.latlngBoundsToJson(googleMap.getCameraPosition())); methodChannel.invokeMethod("camera#onMove", arguments); } @@ -297,6 +318,11 @@ public boolean onMarkerClick(Marker marker) { return markersController.onMarkerTap(marker.getId()); } + @Override + public void onPolylineClick(Polyline polyline) { + polylinesController.onPolylineTap(polyline.getId()); + } + @Override public void dispose() { if (disposed) { @@ -440,6 +466,18 @@ private void updateInitialMarkers() { markersController.addMarkers(initialMarkers); } + @Override + public void setInitialPolylines(Object initialPolylines) { + this.initialPolylines = (List) initialPolylines; + if (googleMap != null) { + updateInitialPolylines(); + } + } + + private void updateInitialPolylines() { + polylinesController.addPolylines(initialPolylines); + } + @SuppressLint("MissingPermission") private void updateMyLocationEnabled() { if (hasLocationPermission()) { 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 c66dc3e42f54..239561691535 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("polylinesToAdd")) { + builder.setInitialPolylines(params.get("polylinesToAdd")); + } return builder.build(id, context, mActivityState, mPluginRegistrar); } } 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 bf6cc2e2202e..6fec3bc71b15 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 @@ -29,4 +29,6 @@ interface GoogleMapOptionsSink { void setMyLocationEnabled(boolean myLocationEnabled); void setInitialMarkers(Object initialMarkers); + + void setInitialPolylines(Object initialPolylines); } diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java index 43f5fe6ff08f..6923a23fd586 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java @@ -67,7 +67,7 @@ boolean onMarkerTap(String googleMarkerId) { if (markerId == null) { return false; } - methodChannel.invokeMethod("marker#onTap", Convert.toJson(markerId)); + methodChannel.invokeMethod("marker#onTap", Convert.markerIdToJson(markerId)); MarkerController markerController = markerIdToController.get(markerId); if (markerController != null) { return markerController.consumeTapEvents(); @@ -80,7 +80,7 @@ void onInfoWindowTap(String googleMarkerId) { if (markerId == null) { return; } - methodChannel.invokeMethod("infoWindow#onTap", Convert.toJson(markerId)); + methodChannel.invokeMethod("infoWindow#onTap", Convert.markerIdToJson(markerId)); } private void addMarker(Object marker) { diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineBuilder.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineBuilder.java new file mode 100644 index 000000000000..034659768844 --- /dev/null +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineBuilder.java @@ -0,0 +1,80 @@ +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.Cap; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.PatternItem; +import com.google.android.gms.maps.model.PolylineOptions; +import java.util.List; + +class PolylineBuilder implements PolylineOptionsSink { + private final PolylineOptions polylineOptions; + private boolean consumeTapEvents; + + PolylineBuilder() { + this.polylineOptions = new PolylineOptions(); + } + + PolylineOptions build() { + return polylineOptions; + } + + boolean consumeTapEvents() { + return consumeTapEvents; + } + + @Override + public void setColor(int color) { + polylineOptions.color(color); + } + + @Override + public void setEndCap(Cap endCap) { + polylineOptions.endCap(endCap); + } + + @Override + public void setJointType(int jointType) { + polylineOptions.jointType(jointType); + } + + @Override + public void setPattern(List pattern) { + polylineOptions.pattern(pattern); + } + + @Override + public void setPoints(List points) { + polylineOptions.addAll(points); + } + + @Override + public void setConsumeTapEvents(boolean consumeTapEvents) { + this.consumeTapEvents = consumeTapEvents; + polylineOptions.clickable(consumeTapEvents); + } + + @Override + public void setGeodesic(boolean geodisc) { + polylineOptions.geodesic(geodisc); + } + + @Override + public void setStartCap(Cap startCap) { + polylineOptions.startCap(startCap); + } + + @Override + public void setVisible(boolean visible) { + polylineOptions.visible(visible); + } + + @Override + public void setWidth(float width) { + polylineOptions.width(width); + } + + @Override + public void setZIndex(float zIndex) { + polylineOptions.zIndex(zIndex); + } +} diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineController.java new file mode 100644 index 000000000000..303945a97d95 --- /dev/null +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineController.java @@ -0,0 +1,88 @@ +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.Cap; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.PatternItem; +import com.google.android.gms.maps.model.Polyline; +import java.util.List; + +/** Controller of a single Polyline on the map. */ +class PolylineController implements PolylineOptionsSink { + private final Polyline polyline; + private final String googleMapsPolylineId; + private boolean consumeTapEvents; + + PolylineController(Polyline polyline, boolean consumeTapEvents) { + this.polyline = polyline; + this.consumeTapEvents = consumeTapEvents; + this.googleMapsPolylineId = polyline.getId(); + } + + void remove() { + polyline.remove(); + } + + @Override + public void setConsumeTapEvents(boolean consumeTapEvents) { + this.consumeTapEvents = consumeTapEvents; + polyline.setClickable(consumeTapEvents); + } + + @Override + public void setColor(int color) { + polyline.setColor(color); + } + + @Override + public void setEndCap(Cap endCap) { + polyline.setEndCap(endCap); + } + + @Override + public void setGeodesic(boolean geodesic) { + polyline.setGeodesic(geodesic); + } + + @Override + public void setJointType(int jointType) { + polyline.setJointType(jointType); + } + + @Override + public void setPattern(List pattern) { + polyline.setPattern(pattern); + } + + @Override + public void setPoints(List points) { + polyline.setPoints(points); + } + + @Override + public void setStartCap(Cap startCap) { + polyline.setStartCap(startCap); + } + + @Override + public void setVisible(boolean visible) { + polyline.setVisible(visible); + } + + @Override + public void setWidth(float width) { + polyline.setWidth(width); + } + + @Override + public void setZIndex(float zIndex) { + polyline.setZIndex(zIndex); + } + + String getGoogleMapsPolylineId() { + return googleMapsPolylineId; + } + + boolean consumeTapEvents() { + return consumeTapEvents; + } +} diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineOptionsSink.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineOptionsSink.java new file mode 100644 index 000000000000..d682d1c53078 --- /dev/null +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineOptionsSink.java @@ -0,0 +1,32 @@ +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.Cap; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.PatternItem; +import java.util.List; + +/** Receiver of Polyline configuration options. */ +interface PolylineOptionsSink { + + void setConsumeTapEvents(boolean consumetapEvents); + + void setColor(int color); + + void setEndCap(Cap endCap); + + void setGeodesic(boolean geodesic); + + void setJointType(int jointType); + + void setPattern(List pattern); + + void setPoints(List points); + + void setStartCap(Cap startCap); + + void setVisible(boolean visible); + + void setWidth(float width); + + void setZIndex(float zIndex); +} diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylinesController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylinesController.java new file mode 100644 index 000000000000..8647589da03d --- /dev/null +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylinesController.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.Polyline; +import com.google.android.gms.maps.model.PolylineOptions; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class PolylinesController { + + private final Map polylineIdToController; + private final Map googleMapsPolylineIdToDartPolylineId; + private final MethodChannel methodChannel; + private GoogleMap googleMap; + + PolylinesController(MethodChannel methodChannel) { + this.polylineIdToController = new HashMap<>(); + this.googleMapsPolylineIdToDartPolylineId = new HashMap<>(); + this.methodChannel = methodChannel; + } + + void setGoogleMap(GoogleMap googleMap) { + this.googleMap = googleMap; + } + + void addPolylines(List polylinesToAdd) { + if (polylinesToAdd != null) { + for (Object polylineToAdd : polylinesToAdd) { + addPolyline(polylineToAdd); + } + } + } + + void changePolylines(List polylinesToChange) { + if (polylinesToChange != null) { + for (Object polylineToChange : polylinesToChange) { + changePolyline(polylineToChange); + } + } + } + + void removePolylines(List polylineIdsToRemove) { + if (polylineIdsToRemove == null) { + return; + } + for (Object rawPolylineId : polylineIdsToRemove) { + if (rawPolylineId == null) { + continue; + } + String polylineId = (String) rawPolylineId; + final PolylineController polylineController = polylineIdToController.remove(polylineId); + if (polylineController != null) { + polylineController.remove(); + googleMapsPolylineIdToDartPolylineId.remove(polylineController.getGoogleMapsPolylineId()); + } + } + } + + boolean onPolylineTap(String googlePolylineId) { + String polylineId = googleMapsPolylineIdToDartPolylineId.get(googlePolylineId); + if (polylineId == null) { + return false; + } + methodChannel.invokeMethod("polyline#onTap", Convert.polylineIdToJson(polylineId)); + PolylineController polylineController = polylineIdToController.get(polylineId); + if (polylineController != null) { + return polylineController.consumeTapEvents(); + } + return false; + } + + private void addPolyline(Object polyline) { + if (polyline == null) { + return; + } + PolylineBuilder polylineBuilder = new PolylineBuilder(); + String polylineId = Convert.interpretPolylineOptions(polyline, polylineBuilder); + PolylineOptions options = polylineBuilder.build(); + addPolyline(polylineId, options, polylineBuilder.consumeTapEvents()); + } + + private void addPolyline( + String polylineId, PolylineOptions polylineOptions, boolean consumeTapEvents) { + final Polyline polyline = googleMap.addPolyline(polylineOptions); + PolylineController controller = new PolylineController(polyline, consumeTapEvents); + polylineIdToController.put(polylineId, controller); + googleMapsPolylineIdToDartPolylineId.put(polyline.getId(), polylineId); + } + + private void changePolyline(Object polyline) { + if (polyline == null) { + return; + } + String polylineId = getPolylineId(polyline); + PolylineController polylineController = polylineIdToController.get(polylineId); + if (polylineController != null) { + Convert.interpretPolylineOptions(polyline, polylineController); + } + } + + @SuppressWarnings("unchecked") + private static String getPolylineId(Object polyline) { + Map polylineMap = (Map) polyline; + return (String) polylineMap.get("polylineId"); + } +} diff --git a/packages/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/example/lib/main.dart index dc86ab40e0b5..9b99b4f9ea56 100644 --- a/packages/google_maps_flutter/example/lib/main.dart +++ b/packages/google_maps_flutter/example/lib/main.dart @@ -9,6 +9,7 @@ import 'map_ui.dart'; import 'move_camera.dart'; import 'page.dart'; import 'place_marker.dart'; +import 'place_polyline.dart'; import 'scrolling_map.dart'; final List _allPages = [ @@ -18,6 +19,7 @@ final List _allPages = [ MoveCameraPage(), PlaceMarkerPage(), ScrollingMapPage(), + PlacePolylinePage(), ]; class MapsDemo extends StatelessWidget { diff --git a/packages/google_maps_flutter/example/lib/place_polyline.dart b/packages/google_maps_flutter/example/lib/place_polyline.dart new file mode 100644 index 000000000000..c5f0d94f0ac2 --- /dev/null +++ b/packages/google_maps_flutter/example/lib/place_polyline.dart @@ -0,0 +1,304 @@ +import 'dart:io' show Platform; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'page.dart'; + +class PlacePolylinePage extends Page { + PlacePolylinePage() : super(const Icon(Icons.linear_scale), 'Place polyline'); + + @override + Widget build(BuildContext context) { + return const PlacePolylineBody(); + } +} + +class PlacePolylineBody extends StatefulWidget { + const PlacePolylineBody(); + + @override + State createState() => PlacePolylineBodyState(); +} + +class PlacePolylineBodyState extends State { + PlacePolylineBodyState(); + + GoogleMapController controller; + Map polylines = {}; + int _polylineIdCounter = 1; + PolylineId selectedPolyline; + + // Values when toggling polyline color + int colorsIndex = 0; + List colors = [ + Colors.purple, + Colors.red, + Colors.green, + Colors.pink, + ]; + + // Values when toggling polyline width + int widthsIndex = 0; + List widths = [10, 20, 5]; + + int jointTypesIndex = 0; + List jointTypes = [ + JointType.mitered, + JointType.bevel, + JointType.round + ]; + + // Values when toggling polyline end cap type + int endCapsIndex = 0; + List endCaps = [Cap.buttCap, Cap.squareCap, Cap.roundCap]; + + // Values when toggling polyline start cap type + int startCapsIndex = 0; + List startCaps = [Cap.buttCap, Cap.squareCap, Cap.roundCap]; + + // Values when toggling polyline pattern + int patternsIndex = 0; + List> patterns = >[ + [], + [ + PatternItem.dash(30.0), + PatternItem.gap(20.0), + PatternItem.dot, + PatternItem.gap(20.0) + ], + [PatternItem.dash(30.0), PatternItem.gap(20.0)], + [PatternItem.dot, PatternItem.gap(10.0)], + ]; + + void _onMapCreated(GoogleMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + void _onPolylineTapped(PolylineId polylineId) { + setState(() { + selectedPolyline = polylineId; + }); + } + + void _remove() { + setState(() { + if (polylines.containsKey(selectedPolyline)) { + polylines.remove(selectedPolyline); + } + selectedPolyline = null; + }); + } + + void _add() { + final int polylineCount = polylines.length; + + if (polylineCount == 12) { + return; + } + + final String polylineIdVal = 'polyline_id_$_polylineIdCounter'; + _polylineIdCounter++; + final PolylineId polylineId = PolylineId(polylineIdVal); + + final Polyline polyline = Polyline( + polylineId: polylineId, + consumeTapEvents: true, + color: Colors.orange, + width: 5, + points: _createPoints(), + onTap: () { + _onPolylineTapped(polylineId); + }, + ); + + setState(() { + polylines[polylineId] = polyline; + }); + } + + void _toggleGeodesic() { + final Polyline polyline = polylines[selectedPolyline]; + setState(() { + polylines[selectedPolyline] = polyline.copyWith( + geodesicParam: !polyline.geodesic, + ); + }); + } + + void _toggleVisible() { + final Polyline polyline = polylines[selectedPolyline]; + setState(() { + polylines[selectedPolyline] = polyline.copyWith( + visibleParam: !polyline.visible, + ); + }); + } + + void _changeColor() { + final Polyline polyline = polylines[selectedPolyline]; + setState(() { + polylines[selectedPolyline] = polyline.copyWith( + colorParam: colors[++colorsIndex % colors.length], + ); + }); + } + + void _changeWidth() { + final Polyline polyline = polylines[selectedPolyline]; + setState(() { + polylines[selectedPolyline] = polyline.copyWith( + widthParam: widths[++widthsIndex % widths.length], + ); + }); + } + + void _changeJointType() { + final Polyline polyline = polylines[selectedPolyline]; + setState(() { + polylines[selectedPolyline] = polyline.copyWith( + jointTypeParam: jointTypes[++jointTypesIndex % jointTypes.length], + ); + }); + } + + void _changeEndCap() { + final Polyline polyline = polylines[selectedPolyline]; + setState(() { + polylines[selectedPolyline] = polyline.copyWith( + endCapParam: endCaps[++endCapsIndex % endCaps.length], + ); + }); + } + + void _changeStartCap() { + final Polyline polyline = polylines[selectedPolyline]; + setState(() { + polylines[selectedPolyline] = polyline.copyWith( + startCapParam: startCaps[++startCapsIndex % startCaps.length], + ); + }); + } + + void _changePattern() { + final Polyline polyline = polylines[selectedPolyline]; + setState(() { + polylines[selectedPolyline] = polyline.copyWith( + patternsParam: patterns[++patternsIndex % patterns.length], + ); + }); + } + + @override + Widget build(BuildContext context) { + final bool iOSorNotSelected = Platform.isIOS || (selectedPolyline == null); + + 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, + ), + polylines: Set.of(polylines.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: + (selectedPolyline == null) ? null : _remove, + ), + FlatButton( + child: const Text('toggle visible'), + onPressed: (selectedPolyline == null) + ? null + : _toggleVisible, + ), + FlatButton( + child: const Text('toggle geodesic'), + onPressed: (selectedPolyline == null) + ? null + : _toggleGeodesic, + ), + ], + ), + Column( + children: [ + FlatButton( + child: const Text('change width'), + onPressed: + (selectedPolyline == null) ? null : _changeWidth, + ), + FlatButton( + child: const Text('change color'), + onPressed: + (selectedPolyline == null) ? null : _changeColor, + ), + FlatButton( + child: const Text('change start cap [Android only]'), + onPressed: iOSorNotSelected ? null : _changeStartCap, + ), + FlatButton( + child: const Text('change end cap [Android only]'), + onPressed: iOSorNotSelected ? null : _changeEndCap, + ), + FlatButton( + child: const Text('change joint type [Android only]'), + onPressed: iOSorNotSelected ? null : _changeJointType, + ), + FlatButton( + child: const Text('change pattern [Android only]'), + onPressed: iOSorNotSelected ? null : _changePattern, + ), + ], + ) + ], + ) + ], + ), + ), + ), + ], + ); + } + + List _createPoints() { + final List points = []; + final double offset = _polylineIdCounter.ceilToDouble(); + points.add(_createLatLng(51.4816 + offset, -3.1791)); + points.add(_createLatLng(53.0430 + offset, -2.9925)); + points.add(_createLatLng(53.1396 + offset, -4.2739)); + points.add(_createLatLng(52.4153 + offset, -4.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 5de2fdd08fb1..01063f6a2e80 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h @@ -5,10 +5,11 @@ #import #import #import "GoogleMapMarkerController.h" +#import "GoogleMapPolylineController.h" // Defines map UI options writable from Flutter. @protocol FLTGoogleMapOptionsSink -- (void)setCameraTargetBounds:(GMSCoordinateBounds*)bounds; +- (void)setCameraTargetBounds:(GMSCoordinateBounds *)bounds; - (void)setCompassEnabled:(BOOL)enabled; - (void)setMapType:(GMSMapViewType)type; - (void)setMinZoom:(float)minZoom maxZoom:(float)maxZoom; @@ -26,15 +27,15 @@ - (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args - registrar:(NSObject*)registrar; + registrar:(NSObject *)registrar; - (void)showAtX:(CGFloat)x Y:(CGFloat)y; - (void)hide; -- (void)animateWithCameraUpdate:(GMSCameraUpdate*)cameraUpdate; -- (void)moveWithCameraUpdate:(GMSCameraUpdate*)cameraUpdate; -- (GMSCameraPosition*)cameraPosition; +- (void)animateWithCameraUpdate:(GMSCameraUpdate *)cameraUpdate; +- (void)moveWithCameraUpdate:(GMSCameraUpdate *)cameraUpdate; +- (GMSCameraPosition *)cameraPosition; @end // Allows the engine to create new Google Map instances. @interface FLTGoogleMapFactory : NSObject -- (instancetype)initWithRegistrar:(NSObject*)registrar; +- (instancetype)initWithRegistrar:(NSObject *)registrar; @end diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m index ee95a645001f..b15ab70331f3 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; + FLTPolylinesController* _polylinesController; } - (instancetype)initWithFrame:(CGRect)frame @@ -84,10 +85,17 @@ - (instancetype)initWithFrame:(CGRect)frame _markersController = [[FLTMarkersController alloc] init:_channel mapView:_mapView registrar:registrar]; + _polylinesController = [[FLTPolylinesController alloc] init:_channel + mapView:_mapView + registrar:registrar]; id markersToAdd = args[@"markersToAdd"]; if ([markersToAdd isKindOfClass:[NSArray class]]) { [_markersController addMarkers:markersToAdd]; } + id polylinesToAdd = args[@"polylinesToAdd"]; + if ([polylinesToAdd isKindOfClass:[NSArray class]]) { + [_polylinesController addPolylines:polylinesToAdd]; + } } return self; } @@ -139,6 +147,20 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [_markersController removeMarkerIds:markerIdsToRemove]; } result(nil); + } else if ([call.method isEqualToString:@"polylines#update"]) { + id polylinesToAdd = call.arguments[@"polylinesToAdd"]; + if ([polylinesToAdd isKindOfClass:[NSArray class]]) { + [_polylinesController addPolylines:polylinesToAdd]; + } + id polylinesToChange = call.arguments[@"polylinesToChange"]; + if ([polylinesToChange isKindOfClass:[NSArray class]]) { + [_polylinesController changePolylines:polylinesToChange]; + } + id polylineIdsToRemove = call.arguments[@"polylineIdsToRemove"]; + if ([polylineIdsToRemove isKindOfClass:[NSArray class]]) { + [_polylinesController removePolylineIds:polylineIdsToRemove]; + } + result(nil); } else if ([call.method isEqualToString:@"map#isCompassEnabled"]) { NSNumber* isCompassEnabled = @(_mapView.settings.compassButton); result(isCompassEnabled); @@ -270,6 +292,10 @@ - (void)mapView:(GMSMapView*)mapView didTapInfoWindowOfMarker:(GMSMarker*)marker NSString* markerId = marker.userData[0]; [_markersController onInfoWindowTap:markerId]; } +- (void)mapView:(GMSMapView*)mapView didTapOverlay:(GMSOverlay*)overlay { + NSString* polylineId = overlay.userData[0]; + [_polylinesController onPolylineTap:polylineId]; +} - (void)mapView:(GMSMapView*)mapView didTapAtCoordinate:(CLLocationCoordinate2D)coordinate { [_channel invokeMethod:@"map#onTap" arguments:@{@"position" : LocationToJson(coordinate)}]; diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m index 569531ff69f7..96af64da477e 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m @@ -88,6 +88,10 @@ static CLLocationCoordinate2D ToLocation(NSArray* data) { static CGPoint ToPoint(NSArray* data) { return [FLTGoogleMapJsonConversions toPoint:data]; } +static NSArray* PositionToJson(CLLocationCoordinate2D data) { + return [FLTGoogleMapJsonConversions positionToJson:data]; +} + static void InterpretMarkerOptions(NSDictionary* data, id sink, NSObject* registrar) { NSNumber* alpha = data[@"alpha"]; diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapPolylineController.h b/packages/google_maps_flutter/ios/Classes/GoogleMapPolylineController.h new file mode 100644 index 000000000000..462803060af0 --- /dev/null +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapPolylineController.h @@ -0,0 +1,35 @@ +// 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 polyline UI options writable from Flutter. +@protocol FLTGoogleMapPolylineOptionsSink +- (void)setConsumeTapEvents:(BOOL)consume; +- (void)setVisible:(BOOL)visible; +- (void)setColor:(UIColor*)color; +- (void)setStrokeWidth:(CGFloat)width; +- (void)setPoints:(NSArray*)points; +- (void)setZIndex:(int)zIndex; +@end + +// Defines polyline controllable by Flutter. +@interface FLTGoogleMapPolylineController : NSObject +@property(atomic, readonly) NSString* polylineId; +- (instancetype)initPolylineWithPath:(GMSMutablePath*)path + polylineId:(NSString*)polylineId + mapView:(GMSMapView*)mapView; +- (void)removePolyline; +@end + +@interface FLTPolylinesController : NSObject +- (instancetype)init:(FlutterMethodChannel*)methodChannel + mapView:(GMSMapView*)mapView + registrar:(NSObject*)registrar; +- (void)addPolylines:(NSArray*)polylinesToAdd; +- (void)changePolylines:(NSArray*)polylinesToChange; +- (void)removePolylineIds:(NSArray*)polylineIdsToRemove; +- (void)onPolylineTap:(NSString*)polylineId; +@end diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapPolylineController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapPolylineController.m new file mode 100644 index 000000000000..170c9870fdd7 --- /dev/null +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapPolylineController.m @@ -0,0 +1,175 @@ +// 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 "GoogleMapPolylineController.h" +#import "JsonConversions.h" + +@implementation FLTGoogleMapPolylineController { + GMSPolyline* _polyline; + GMSMapView* _mapView; +} +- (instancetype)initPolylineWithPath:(GMSMutablePath*)path + polylineId:(NSString*)polylineId + mapView:(GMSMapView*)mapView { + self = [super init]; + if (self) { + _polyline = [GMSPolyline polylineWithPath:path]; + _mapView = mapView; + _polylineId = polylineId; + _polyline.userData = @[ polylineId ]; + } + return self; +} + +- (void)removePolyline { + _polyline.map = nil; +} + +#pragma mark - FLTGoogleMapPolylineOptionsSink methods + +- (void)setConsumeTapEvents:(BOOL)consumes { + _polyline.tappable = consumes; +} +- (void)setVisible:(BOOL)visible { + _polyline.map = visible ? _mapView : nil; +} +- (void)setZIndex:(int)zIndex { + _polyline.zIndex = zIndex; +} +- (void)setPoints:(NSArray*)points { + GMSMutablePath* path = [GMSMutablePath path]; + + for (CLLocation* location in points) { + [path addCoordinate:location.coordinate]; + } + _polyline.path = path; +} + +- (void)setColor:(UIColor*)color { + _polyline.strokeColor = color; +} +- (void)setStrokeWidth:(CGFloat)width { + _polyline.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 InterpretPolylineOptions(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* strokeColor = data[@"color"]; + if (strokeColor) { + [sink setColor:ToColor(strokeColor)]; + } + + NSNumber* strokeWidth = data[@"width"]; + if (strokeWidth) { + [sink setStrokeWidth:ToInt(strokeWidth)]; + } +} + +@implementation FLTPolylinesController { + NSMutableDictionary* _polylineIdToController; + 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; + _polylineIdToController = [NSMutableDictionary dictionaryWithCapacity:1]; + _registrar = registrar; + } + return self; +} +- (void)addPolylines:(NSArray*)polylinesToAdd { + for (NSDictionary* polyline in polylinesToAdd) { + GMSMutablePath* path = [FLTPolylinesController getPath:polyline]; + NSString* polylineId = [FLTPolylinesController getPolylineId:polyline]; + FLTGoogleMapPolylineController* controller = + [[FLTGoogleMapPolylineController alloc] initPolylineWithPath:path + polylineId:polylineId + mapView:_mapView]; + InterpretPolylineOptions(polyline, controller, _registrar); + _polylineIdToController[polylineId] = controller; + } +} +- (void)changePolylines:(NSArray*)polylinesToChange { + for (NSDictionary* polyline in polylinesToChange) { + NSString* polylineId = [FLTPolylinesController getPolylineId:polyline]; + FLTGoogleMapPolylineController* controller = _polylineIdToController[polylineId]; + if (!controller) { + continue; + } + InterpretPolylineOptions(polyline, controller, _registrar); + } +} +- (void)removePolylineIds:(NSArray*)polylineIdsToRemove { + for (NSString* polylineId in polylineIdsToRemove) { + if (!polylineId) { + continue; + } + FLTGoogleMapPolylineController* controller = _polylineIdToController[polylineId]; + if (!controller) { + continue; + } + [controller removePolyline]; + [_polylineIdToController removeObjectForKey:polylineId]; + } +} +- (void)onPolylineTap:(NSString*)polylineId { + if (!polylineId) { + return; + } + FLTGoogleMapPolylineController* controller = _polylineIdToController[polylineId]; + if (!controller) { + return; + } + [_methodChannel invokeMethod:@"polyline#onTap" arguments:@{@"polylineId" : polylineId}]; +} ++ (GMSMutablePath*)getPath:(NSDictionary*)polyline { + NSArray* pointArray = polyline[@"points"]; + NSArray* points = ToPoints(pointArray); + GMSMutablePath* path = [GMSMutablePath path]; + for (CLLocation* location in points) { + [path addCoordinate:location.coordinate]; + } + return path; +} ++ (NSString*)getPolylineId:(NSDictionary*)polyline { + return polyline[@"polylineId"]; +} +@end diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h b/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h index 817f2d2a857f..dfdce8ab0350 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h @@ -6,6 +6,7 @@ #import #import "GoogleMapController.h" #import "GoogleMapMarkerController.h" +#import "GoogleMapPolylineController.h" @interface FLTGoogleMapsPlugin : NSObject @end diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.m b/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.m index 8d54082fb20a..7606d1593bcc 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.m @@ -3,8 +3,6 @@ // found in the LICENSE file. #import "GoogleMapsPlugin.h" -#import "GoogleMapController.h" -#import "GoogleMapMarkerController.h" #pragma mark - GoogleMaps plugin implementation diff --git a/packages/google_maps_flutter/ios/Classes/JsonConversions.h b/packages/google_maps_flutter/ios/Classes/JsonConversions.h index 625b2e396f6a..c54b2ad8cc8a 100644 --- a/packages/google_maps_flutter/ios/Classes/JsonConversions.h +++ b/packages/google_maps_flutter/ios/Classes/JsonConversions.h @@ -12,4 +12,7 @@ + (float)toFloat:(NSNumber*)data; + (CLLocationCoordinate2D)toLocation:(NSArray*)data; + (CGPoint)toPoint:(NSArray*)data; ++ (NSArray*)positionToJson:(CLLocationCoordinate2D)position; ++ (UIColor*)toColor:(NSNumber*)data; ++ (NSArray*)toPoints:(NSArray*)data; @end diff --git a/packages/google_maps_flutter/ios/Classes/JsonConversions.m b/packages/google_maps_flutter/ios/Classes/JsonConversions.m index 7353c333b8dd..ddec920211df 100644 --- a/packages/google_maps_flutter/ios/Classes/JsonConversions.m +++ b/packages/google_maps_flutter/ios/Classes/JsonConversions.m @@ -32,4 +32,30 @@ + (CGPoint)toPoint:(NSArray*)data { [FLTGoogleMapJsonConversions toDouble:data[1]]); } -@end \ No newline at end of file ++ (NSArray*)positionToJson:(CLLocationCoordinate2D)position { + return @[ @(position.latitude), @(position.longitude) ]; +} + ++ (UIColor*)toColor:(NSNumber*)numberColor { + long value = [numberColor longValue]; + return [UIColor colorWithRed:((float)((value & 0xFF0000) >> 16)) / 255.0 + green:((float)((value & 0xFF00) >> 8)) / 255.0 + blue:((float)(value & 0xFF)) / 255.0 + alpha:1.0]; +} + ++ (NSArray*)toPoints:(NSArray*)data { + NSMutableArray* points = [[NSMutableArray alloc] init]; + for (unsigned i = 0; i < [data count]; i++) { + NSNumber* latitude = data[i][0]; + NSNumber* longitude = data[i][1]; + CLLocation* point = + [[CLLocation alloc] initWithLatitude:[FLTGoogleMapJsonConversions toDouble:latitude] + longitude:[FLTGoogleMapJsonConversions toDouble:longitude]]; + [points addObject:point]; + } + + return points; +} + +@end diff --git a/packages/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/lib/google_maps_flutter.dart index 1ee7cb898210..dad1e59bea9b 100644 --- a/packages/google_maps_flutter/lib/google_maps_flutter.dart +++ b/packages/google_maps_flutter/lib/google_maps_flutter.dart @@ -16,9 +16,14 @@ import 'package:flutter/services.dart'; part 'src/bitmap.dart'; part 'src/callbacks.dart'; part 'src/camera.dart'; +part 'src/cap.dart'; part 'src/controller.dart'; part 'src/google_map.dart'; +part 'src/joint_type.dart'; part 'src/marker.dart'; part 'src/marker_updates.dart'; part 'src/location.dart'; +part 'src/pattern_item.dart'; +part 'src/polyline.dart'; +part 'src/polyline_updates.dart'; part 'src/ui.dart'; diff --git a/packages/google_maps_flutter/lib/src/cap.dart b/packages/google_maps_flutter/lib/src/cap.dart new file mode 100644 index 000000000000..03676c0561fa --- /dev/null +++ b/packages/google_maps_flutter/lib/src/cap.dart @@ -0,0 +1,48 @@ +part of google_maps_flutter; + +/// Cap that can be applied at the start or end vertex of a [Polyline]. +@immutable +class Cap { + const Cap._(this._json); + + /// Cap that is squared off exactly at the start or end vertex of a [Polyline] + /// with solid stroke pattern, equivalent to having no additional cap beyond + /// the start or end vertex. + /// + /// This is the default cap type at start and end vertices of Polylines with + /// solid stroke pattern. + static const Cap buttCap = Cap._(['buttCap']); + + /// Cap that is a semicircle with radius equal to half the stroke width, + /// centered at the start or end vertex of a [Polyline] with solid stroke + /// pattern. + static const Cap roundCap = Cap._(['roundCap']); + + /// Cap that is squared off after extending half the stroke width beyond the + /// start or end vertex of a [Polyline] with solid stroke pattern. + static const Cap squareCap = Cap._(['squareCap']); + + /// Constructs a new CustomCap with a bitmap overlay centered at the start or + /// end vertex of a [Polyline], orientated according to the direction of the line's + /// first or last edge and scaled with respect to the line's stroke width. + /// + /// CustomCap can be applied to [Polyline] with any stroke pattern. + /// + /// [bitmapDescriptor] must not be null. + /// + /// [refWidth] is the reference stroke width (in pixels) - the stroke width for which + /// the cap bitmap at its native dimension is designed. Must be positive. Default value + /// is 10 pixels. + static Cap customCapFromBitmap( + BitmapDescriptor bitmapDescriptor, { + double refWidth = 10, + }) { + assert(bitmapDescriptor != null); + assert(refWidth > 0.0); + return Cap._(['customCap', bitmapDescriptor._toJson(), refWidth]); + } + + final dynamic _json; + + dynamic _toJson() => _json; +} diff --git a/packages/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/lib/src/controller.dart index 93304a64f21c..4df64292e732 100644 --- a/packages/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/lib/src/controller.dart @@ -63,6 +63,9 @@ class GoogleMapController { case 'infoWindow#onTap': _googleMapState.onInfoWindowTap(call.arguments['markerId']); break; + case 'polyline#onTap': + _googleMapState.onPolylineTap(call.arguments['polylineId']); + break; case 'map#onTap': _googleMapState.onTap(LatLng._fromJson(call.arguments['position'])); break; @@ -107,6 +110,23 @@ class GoogleMapController { ); } + /// Updates polyline 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 _updatePolylines(_PolylineUpdates polylineUpdates) async { + assert(polylineUpdates != 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( + 'polylines#update', + polylineUpdates._toMap(), + ); + } + /// Starts an animated change of the map camera position. /// /// The returned [Future] completes after the change has been started 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 a75424b03640..a3b394613e46 100644 --- a/packages/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/lib/src/google_map.dart @@ -30,6 +30,7 @@ class GoogleMap extends StatefulWidget { this.tiltGesturesEnabled = true, this.myLocationEnabled = false, this.markers, + this.polylines, this.onCameraMoveStarted, this.onCameraMove, this.onCameraIdle, @@ -71,6 +72,9 @@ class GoogleMap extends StatefulWidget { /// Markers to be placed on the map. final Set markers; + /// Polylines to be placed on the map. + final Set polylines; + /// Called when the camera starts moving. /// /// This can be initiated by the following: @@ -140,6 +144,7 @@ class _GoogleMapState extends State { Completer(); Map _markers = {}; + Map _polylines = {}; _GoogleMapOptions _googleMapOptions; @override @@ -148,6 +153,7 @@ class _GoogleMapState extends State { 'initialCameraPosition': widget.initialCameraPosition?._toMap(), 'options': _googleMapOptions.toMap(), 'markersToAdd': _serializeMarkerSet(widget.markers), + 'polylinesToAdd': _serializePolylineSet(widget.polylines), }; if (defaultTargetPlatform == TargetPlatform.android) { return AndroidView( @@ -176,6 +182,7 @@ class _GoogleMapState extends State { super.initState(); _googleMapOptions = _GoogleMapOptions.fromWidget(widget); _markers = _keyByMarkerId(widget.markers); + _polylines = _keyByPolylineId(widget.polylines); } @override @@ -183,6 +190,7 @@ class _GoogleMapState extends State { super.didUpdateWidget(oldWidget); _updateOptions(); _updateMarkers(); + _updatePolylines(); } void _updateOptions() async { @@ -204,6 +212,13 @@ class _GoogleMapState extends State { _markers = _keyByMarkerId(widget.markers); } + void _updatePolylines() async { + final GoogleMapController controller = await _controller.future; + controller._updatePolylines( + _PolylineUpdates.from(_polylines.values.toSet(), widget.polylines)); + _polylines = _keyByPolylineId(widget.polylines); + } + Future onPlatformViewCreated(int id) async { final GoogleMapController controller = await GoogleMapController.init( id, @@ -222,6 +237,12 @@ class _GoogleMapState extends State { _markers[markerId].onTap(); } + void onPolylineTap(String polylineIdParam) { + assert(polylineIdParam != null); + final PolylineId polylineId = PolylineId(polylineIdParam); + _polylines[polylineId].onTap(); + } + void onInfoWindowTap(String markerIdParam) { assert(markerIdParam != null); final MarkerId markerId = MarkerId(markerIdParam); diff --git a/packages/google_maps_flutter/lib/src/joint_type.dart b/packages/google_maps_flutter/lib/src/joint_type.dart new file mode 100644 index 000000000000..26eee4e3ef63 --- /dev/null +++ b/packages/google_maps_flutter/lib/src/joint_type.dart @@ -0,0 +1,25 @@ +part of google_maps_flutter; + +/// Joint types for [Polyline]. +@immutable +class JointType { + const JointType._(this.value); + + /// The value representing the [JointType] on the sdk. + final int value; + + /// Mitered joint, with fixed pointed extrusion equal to half the stroke width on the outside of the joint. + /// + /// Constant Value: 0 + static const JointType mitered = JointType._(0); + + /// Flat bevel on the outside of the joint. + /// + /// Constant Value: 1 + static const JointType bevel = JointType._(1); + + /// Rounded on the outside of the joint by an arc of radius equal to half the stroke width, centered at the vertex. + /// + /// Constant Value: 2 + static const JointType round = JointType._(2); +} diff --git a/packages/google_maps_flutter/lib/src/pattern_item.dart b/packages/google_maps_flutter/lib/src/pattern_item.dart new file mode 100644 index 000000000000..620af0ebd9d4 --- /dev/null +++ b/packages/google_maps_flutter/lib/src/pattern_item.dart @@ -0,0 +1,29 @@ +part of google_maps_flutter; + +/// Item used in the stroke pattern for a Polyline. +@immutable +class PatternItem { + const PatternItem._(this._json); + + static const PatternItem dot = PatternItem._(['dot']); + + /// A dash used in the stroke pattern for a [Polyline]. + /// + /// [length] has to be non-negative. + static PatternItem dash(double length) { + assert(length >= 0.0); + return PatternItem._(['dash', length]); + } + + /// A gap used in the stroke pattern for a [Polyline]. + /// + /// [length] has to be non-negative. + static PatternItem gap(double length) { + assert(length >= 0.0); + return PatternItem._(['gap', length]); + } + + final dynamic _json; + + dynamic _toJson() => _json; +} diff --git a/packages/google_maps_flutter/lib/src/polyline.dart b/packages/google_maps_flutter/lib/src/polyline.dart new file mode 100644 index 000000000000..563b5ec0148e --- /dev/null +++ b/packages/google_maps_flutter/lib/src/polyline.dart @@ -0,0 +1,232 @@ +part of google_maps_flutter; + +/// Uniquely identifies a [Polyline] among [GoogleMap] polylines. +/// +/// This does not have to be globally unique, only unique among the list. +@immutable +class PolylineId { + PolylineId(this.value) : assert(value != null); + + /// value of the [PolylineId]. + final String value; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final PolylineId typedOther = other; + return value == typedOther.value; + } + + @override + int get hashCode => value.hashCode; + + @override + String toString() { + return 'PolylineId{value: $value}'; + } +} + +/// Draws a line through geographical locations on the map. +@immutable +class Polyline { + const Polyline({ + @required this.polylineId, + this.consumeTapEvents = false, + this.color = Colors.black, + this.endCap = Cap.buttCap, + this.geodesic = false, + this.jointType = JointType.mitered, + this.points = const [], + this.patterns = const [], + this.startCap = Cap.buttCap, + this.visible = true, + this.width = 10, + this.zIndex = 0, + this.onTap, + }); + + /// Uniquely identifies a [Polyline]. + final PolylineId polylineId; + + /// True if the [Polyline] consumes tap events. + /// + /// If this is false, [onTap] callback will not be triggered. + final bool consumeTapEvents; + + /// Line segment color in ARGB format, the same format used by Color. The default value is black (0xff000000). + final Color color; + + /// Indicates whether the segments of the polyline 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; + + /// Joint type of the polyline line segments. + /// + /// The joint type defines the shape to be used when joining adjacent line segments at all vertices of the + /// polyline except the start and end vertices. See [JointType] for supported joint types. The default value is + /// mitered. + /// + /// Supported on Android only. + final JointType jointType; + + /// The stroke pattern for the polyline. + /// + /// Solid or a sequence of PatternItem objects to be repeated along the line. + /// Available PatternItem types: Gap (defined by gap length in pixels), Dash (defined by line width and dash + /// length in pixels) and Dot (circular, centered on the line, diameter defined by line width in pixels). + final List patterns; + + /// The vertices of the polyline to be drawn. + /// + /// Line segments are drawn between consecutive points. A polyline is not closed by + /// default; to form a closed polyline, the start and end points must be the same. + final List points; + + /// The cap at the start vertex of the polyline. + /// + /// The default start cap is ButtCap. + /// + /// Supported on Android only. + final Cap startCap; + + /// The cap at the end vertex of the polyline. + /// + /// The default end cap is ButtCap. + /// + /// Supported on Android only. + final Cap endCap; + + /// True if the marker is visible. + final bool visible; + + /// Width of the polyline, used to define the width of the line segment to be drawn. + /// + /// The width is constant and independent of the camera's zoom level. + /// The default value is 10. + final int width; + + /// The z-index of the polyline, 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 polyline placed on this map. + final VoidCallback onTap; + + /// Creates a new [Polyline] object whose values are the same as this instance, + /// unless overwritten by the specified parameters. + Polyline copyWith({ + Color colorParam, + bool consumeTapEventsParam, + Cap endCapParam, + bool geodesicParam, + JointType jointTypeParam, + List patternsParam, + List pointsParam, + Cap startCapParam, + bool visibleParam, + int widthParam, + int zIndexParam, + VoidCallback onTapParam, + }) { + return Polyline( + polylineId: polylineId, + color: colorParam ?? color, + consumeTapEvents: consumeTapEventsParam ?? consumeTapEvents, + endCap: endCapParam ?? endCap, + geodesic: geodesicParam ?? geodesic, + jointType: jointTypeParam ?? jointType, + patterns: patternsParam ?? patterns, + points: pointsParam ?? points, + startCap: startCapParam ?? startCap, + visible: visibleParam ?? visible, + width: widthParam ?? width, + onTap: onTapParam ?? onTap, + zIndex: zIndexParam ?? zIndex, + ); + } + + dynamic _toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('polylineId', polylineId.value); + addIfPresent('consumeTapEvents', consumeTapEvents); + addIfPresent('color', color.value); + addIfPresent('endCap', endCap?._toJson()); + addIfPresent('geodesic', geodesic); + addIfPresent('jointType', jointType?.value); + addIfPresent('startCap', startCap?._toJson()); + addIfPresent('visible', visible); + addIfPresent('width', width); + addIfPresent('zIndex', zIndex); + + if (points != null) { + json['points'] = _pointsToJson(); + } + + if (patterns != null) { + json['pattern'] = _patternToJson(); + } + + return json; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final Polyline typedOther = other; + return polylineId == typedOther.polylineId; + } + + @override + int get hashCode => polylineId.hashCode; + + dynamic _pointsToJson() { + final List result = []; + for (final LatLng point in points) { + result.add(point._toJson()); + } + return result; + } + + dynamic _patternToJson() { + final List result = []; + for (final PatternItem patternItem in patterns) { + if (patternItem != null) { + result.add(patternItem._toJson()); + } + } + return result; + } +} + +Map _keyByPolylineId(Iterable polylines) { + if (polylines == null) { + return {}; + } + return Map.fromEntries(polylines.map( + (Polyline polyline) => + MapEntry(polyline.polylineId, polyline))); +} + +List> _serializePolylineSet(Set polylines) { + if (polylines == null) { + return null; + } + return polylines + .map>((Polyline p) => p._toJson()) + .toList(); +} diff --git a/packages/google_maps_flutter/lib/src/polyline_updates.dart b/packages/google_maps_flutter/lib/src/polyline_updates.dart new file mode 100644 index 000000000000..5d82597fd212 --- /dev/null +++ b/packages/google_maps_flutter/lib/src/polyline_updates.dart @@ -0,0 +1,92 @@ +// 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; + +/// [Polyline] update events to be applied to the [GoogleMap]. +/// +/// Used in [GoogleMapController] when the map is updated. +class _PolylineUpdates { + /// Computes [_PolylineUpdates] given previous and current [Polyline]s. + _PolylineUpdates.from(Set previous, Set current) { + if (previous == null) { + previous = Set.identity(); + } + + if (current == null) { + current = Set.identity(); + } + + final Map previousPolylines = + _keyByPolylineId(previous); + final Map currentPolylines = + _keyByPolylineId(current); + + final Set prevPolylineIds = previousPolylines.keys.toSet(); + final Set currentPolylineIds = currentPolylines.keys.toSet(); + + Polyline idToCurrentPolyline(PolylineId id) { + return currentPolylines[id]; + } + + final Set _polylineIdsToRemove = + prevPolylineIds.difference(currentPolylineIds); + + final Set _polylinesToAdd = currentPolylineIds + .difference(prevPolylineIds) + .map(idToCurrentPolyline) + .toSet(); + + final Set _polylinesToChange = currentPolylineIds + .intersection(prevPolylineIds) + .map(idToCurrentPolyline) + .toSet(); + + polylinesToAdd = _polylinesToAdd; + polylineIdsToRemove = _polylineIdsToRemove; + polylinesToChange = _polylinesToChange; + } + + Set polylinesToAdd; + Set polylineIdsToRemove; + Set polylinesToChange; + + Map _toMap() { + final Map updateMap = {}; + + void addIfNonNull(String fieldName, dynamic value) { + if (value != null) { + updateMap[fieldName] = value; + } + } + + addIfNonNull('polylinesToAdd', _serializePolylineSet(polylinesToAdd)); + addIfNonNull('polylinesToChange', _serializePolylineSet(polylinesToChange)); + addIfNonNull('polylineIdsToRemove', + polylineIdsToRemove.map((PolylineId m) => m.value).toList()); + + return updateMap; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final _PolylineUpdates typedOther = other; + return setEquals(polylinesToAdd, typedOther.polylinesToAdd) && + setEquals(polylineIdsToRemove, typedOther.polylineIdsToRemove) && + setEquals(polylinesToChange, typedOther.polylinesToChange); + } + + @override + int get hashCode => + hashValues(polylinesToAdd, polylineIdsToRemove, polylinesToChange); + + @override + String toString() { + return '_PolylineUpdates{polylinesToAdd: $polylinesToAdd, ' + 'polylineIdsToRemove: $polylineIdsToRemove, ' + 'polylinesToChange: $polylinesToChange}'; + } +} diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index 63421952e540..fd6241caed11 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.5 +version: 0.5.6 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 fb7308c01e82..8b793eb5112b 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); + updatePolylines(params); } MethodChannel channel; @@ -48,6 +49,12 @@ class FakePlatformGoogleMap { Set markersToChange; + Set polylineIdsToRemove; + + Set polylinesToAdd; + + Set polylinesToChange; + Future onMethodCall(MethodCall call) { switch (call.method) { case 'map#update': @@ -56,6 +63,9 @@ class FakePlatformGoogleMap { case 'markers#update': updateMarkers(call.arguments); return Future.sync(() {}); + case 'polylines#update': + updatePolylines(call.arguments); + return Future.sync(() {}); default: return Future.sync(() {}); } @@ -119,6 +129,56 @@ class FakePlatformGoogleMap { return result; } + void updatePolylines(Map polylineUpdates) { + if (polylineUpdates == null) { + return; + } + polylinesToAdd = _deserializePolylines(polylineUpdates['polylinesToAdd']); + polylineIdsToRemove = + _deserializePolylineIds(polylineUpdates['polylineIdsToRemove']); + polylinesToChange = + _deserializePolylines(polylineUpdates['polylinesToChange']); + } + + Set _deserializePolylineIds(List polylineIds) { + if (polylineIds == 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 polylineIds + .map((dynamic polylineId) => PolylineId(polylineId)) + .toSet(); + } + + Set _deserializePolylines(dynamic polylines) { + if (polylines == 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 polylinesData = polylines; + // 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 polylineData in polylinesData) { + final String polylineId = polylineData['polylineId']; + final bool visible = polylineData['visible']; + final bool geodesic = polylineData['geodesic']; + + result.add(Polyline( + polylineId: PolylineId(polylineId), + visible: visible, + geodesic: geodesic, + )); + } + + return result; + } + void updateOptions(Map options) { if (options.containsKey('compassEnabled')) { compassEnabled = options['compassEnabled']; diff --git a/packages/google_maps_flutter/test/polyline_updates_test.dart b/packages/google_maps_flutter/test/polyline_updates_test.dart new file mode 100644 index 000000000000..1d04aef485fb --- /dev/null +++ b/packages/google_maps_flutter/test/polyline_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({Polyline p1, Polyline p2, Polyline 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 _mapWithPolylines(Set polylines) { + return Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + polylines: polylines, + ), + ); +} + +void main() { + final FakePlatformViewsController fakePlatformViewsController = + FakePlatformViewsController(); + + setUpAll(() { + SystemChannels.platform_views.setMockMethodCallHandler( + fakePlatformViewsController.fakePlatformViewsMethodHandler); + }); + + setUp(() { + fakePlatformViewsController.reset(); + }); + + testWidgets('Initializing a polyline', (WidgetTester tester) async { + final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); + await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + expect(platformGoogleMap.polylinesToAdd.length, 1); + + final Polyline initializedPolyline = platformGoogleMap.polylinesToAdd.first; + expect(initializedPolyline, equals(p1)); + expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polylinesToChange.isEmpty, true); + }); + + testWidgets("Adding a polyline", (WidgetTester tester) async { + final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); + final Polyline p2 = Polyline(polylineId: PolylineId("polyline_2")); + + await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1, p2: p2))); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + expect(platformGoogleMap.polylinesToAdd.length, 1); + + final Polyline addedPolyline = platformGoogleMap.polylinesToAdd.first; + expect(addedPolyline, equals(p2)); + expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); + + expect(platformGoogleMap.polylinesToChange.length, 1); + expect(platformGoogleMap.polylinesToChange.first, equals(p1)); + }); + + testWidgets("Removing a polyline", (WidgetTester tester) async { + final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); + + await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolylines(null)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + expect(platformGoogleMap.polylineIdsToRemove.length, 1); + expect(platformGoogleMap.polylineIdsToRemove.first, equals(p1.polylineId)); + + expect(platformGoogleMap.polylinesToChange.isEmpty, true); + expect(platformGoogleMap.polylinesToAdd.isEmpty, true); + }); + + testWidgets("Updating a polyline", (WidgetTester tester) async { + final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); + final Polyline p2 = + Polyline(polylineId: PolylineId("polyline_1"), geodesic: true); + + await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p2))); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + expect(platformGoogleMap.polylinesToChange.length, 1); + expect(platformGoogleMap.polylinesToChange.first, equals(p2)); + + expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polylinesToAdd.isEmpty, true); + }); + + testWidgets("Updating a polyline", (WidgetTester tester) async { + final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); + final Polyline p2 = + Polyline(polylineId: PolylineId("polyline_1"), geodesic: true); + + await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p2))); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + expect(platformGoogleMap.polylinesToChange.length, 1); + + final Polyline update = platformGoogleMap.polylinesToChange.first; + expect(update, equals(p2)); + expect(update.geodesic, true); + }); + + testWidgets("Multi Update", (WidgetTester tester) async { + Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); + Polyline p2 = Polyline(polylineId: PolylineId("polyline_2")); + final Set prev = _toSet(p1: p1, p2: p2); + p1 = Polyline(polylineId: PolylineId("polyline_1"), visible: false); + p2 = Polyline(polylineId: PolylineId("polyline_2"), geodesic: true); + final Set cur = _toSet(p1: p1, p2: p2); + + await tester.pumpWidget(_mapWithPolylines(prev)); + await tester.pumpWidget(_mapWithPolylines(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.polylinesToChange, cur); + expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polylinesToAdd.isEmpty, true); + }); + + testWidgets("Multi Update", (WidgetTester tester) async { + Polyline p2 = Polyline(polylineId: PolylineId("polyline_2")); + final Polyline p3 = Polyline(polylineId: PolylineId("polyline_3")); + final Set prev = _toSet(p2: p2, p3: p3); + + // p1 is added, p2 is updated, p3 is removed. + final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); + p2 = Polyline(polylineId: PolylineId("polyline_2"), geodesic: true); + final Set cur = _toSet(p1: p1, p2: p2); + + await tester.pumpWidget(_mapWithPolylines(prev)); + await tester.pumpWidget(_mapWithPolylines(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.polylinesToChange.length, 1); + expect(platformGoogleMap.polylinesToAdd.length, 1); + expect(platformGoogleMap.polylineIdsToRemove.length, 1); + + expect(platformGoogleMap.polylinesToChange.first, equals(p2)); + expect(platformGoogleMap.polylinesToAdd.first, equals(p1)); + expect(platformGoogleMap.polylineIdsToRemove.first, equals(p3.polylineId)); + }); + + testWidgets( + "Partial Update", + (WidgetTester tester) async { + final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); + Polyline p2 = Polyline(polylineId: PolylineId("polyline_2")); + final Set prev = _toSet(p1: p1, p2: p2); + p2 = Polyline(polylineId: PolylineId("polyline_2"), geodesic: true); + final Set cur = _toSet(p1: p1, p2: p2); + + await tester.pumpWidget(_mapWithPolylines(prev)); + await tester.pumpWidget(_mapWithPolylines(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.polylinesToChange, _toSet(p2: p2)); + expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polylinesToAdd.isEmpty, true); + }, + // The test is currently broken due to a bug (we're updating all polylines + // 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, + ); +}