diff --git a/CHANGELOG.md b/CHANGELOG.md index 789c44ad..eb12a467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ +## 0.12.0, Oct 5, 2021 +* Update to Maplibre-Android-SDK 9.4.2 +* Update to MapLibre-iOS-SDK 5.12.0 + +### Changes cherry-picked/ported from tobrun/flutter-mapbox-gl:0.12.0 +* Batch creation/removal for circles, fills and lines [#576](https://github.com/tobrun/flutter-mapbox-gl/pull/576) +* Dependencies: updated image package [#598](https://github.com/tobrun/flutter-mapbox-gl/pull/598) +* Improve description to enable location features [#596](https://github.com/tobrun/flutter-mapbox-gl/pull/596) +* Fix feature manager on release build [#593](https://github.com/tobrun/flutter-mapbox-gl/pull/593) +* Emit onTap only for the feature above the others [#589](https://github.com/tobrun/flutter-mapbox-gl/pull/589) +* Add annotationOrder to web [#588](https://github.com/tobrun/flutter-mapbox-gl/pull/588) + ## Below is the original changelog of the tobrun/flutter-mapbox-gl project, before the fork. +## 0.11.0, March 30, 2020 +* Fixed issues caused by new android API [#544](https://github.com/tobrun/flutter-mapbox-gl/pull/544) +* Add option to set maximum offline tile count [#549](https://github.com/tobrun/flutter-mapbox-gl/pull/549) +* Fixed web build failure due to http package upgrade [#550](https://github.com/tobrun/flutter-mapbox-gl/pull/550) +* Update OfflineRegion/OfflineRegionDefinition interfaces, synchronize with iOS and Android [#545](https://github.com/tobrun/flutter-mapbox-gl/pull/545) +* Fix Mapbox GL JS CSS embedding on web [#551](https://github.com/tobrun/flutter-mapbox-gl/pull/551) +* Update Podfile to fix iOS CI [#565](https://github.com/tobrun/flutter-mapbox-gl/pull/565) +* Update deprecated patterns to fix CI static analysis [#568](https://github.com/tobrun/flutter-mapbox-gl/pull/568) +* Add setOffline method on Android [#537](https://github.com/tobrun/flutter-mapbox-gl/pull/537) +* Add batch mode of screen locations [#554](https://github.com/tobrun/flutter-mapbox-gl/pull/554) +* Define which annotations consume the tap events [#575](https://github.com/tobrun/flutter-mapbox-gl/pull/575) +* Remove failed offline region downloads [#583](https://github.com/tobrun/flutter-mapbox-gl/pull/583) + ## 0.10.0, February 12, 2020 * Merge offline regions [#532](https://github.com/tobrun/flutter-mapbox-gl/pull/532) * Update offline region metadata [#530](https://github.com/tobrun/flutter-mapbox-gl/pull/530) diff --git a/README.md b/README.md index ad765f2f..9fc99980 100644 --- a/README.md +++ b/README.md @@ -65,17 +65,25 @@ Map styles can be supplied by setting the `styleString` in the `MapOptions`. The ## Location features -#### To enable location features in an **Android** application: +### Android +Add the `ACCESS_COARSE_LOCATION` or `ACCESS_FINE_LOCATION` permission in the application manifest `android/app/src/main/AndroidManifest.xml` to enable location features in an **Android** application: +``` + + +``` -You need to declare the `ACCESS_COARSE_LOCATION` or `ACCESS_FINE_LOCATION` permission in the AndroidManifest.xml and starting from Android API level 23 also request it at runtime. The plugin does not handle this for you. The example app uses the flutter ['location' plugin](https://pub.dev/packages/location) for this. +Starting from Android API level 23 you also need to request it at runtime. This plugin does not handle this for you. The example app uses the flutter ['location' plugin](https://pub.dev/packages/location) for this. -#### To enable location features in an **iOS** application: +### iOS +To enable location features in an **iOS** application: -If you access your users' location, you should also add the following key to your Info.plist to explain why you need access to their location data: +If you access your users' location, you should also add the following key to `ios/Runner/Info.plist` to explain why you need access to their location data: -```xml -NSLocationWhenInUseUsageDescription -[Your explanation here] +``` +xml ... + NSLocationWhenInUseUsageDescription + [Your explanation here] ``` A possible explanation could be: "Shows your location on the map". diff --git a/android/build.gradle b/android/build.gradle index a3cba094..d68302c7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -17,9 +17,6 @@ rootProject.allprojects { google() jcenter() mavenCentral() - maven { - url = "https://dl.bintray.com/maplibre/maplibre-gl-native" - } } } @@ -40,17 +37,10 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } dependencies { - implementation 'org.maplibre.gl:android-sdk:9.2.1' - //implementation "com.mapbox.mapboxsdk:mapbox-android-sdk:9.2.0" - implementation ("com.mapbox.mapboxsdk:mapbox-android-plugin-annotation-v9:0.9.0") { - exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-sdk' - } - implementation ("com.mapbox.mapboxsdk:mapbox-android-plugin-localization-v9:0.12.0") { - exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-sdk' - } - implementation ("com.mapbox.mapboxsdk:mapbox-android-plugin-offline-v9:0.7.0") { - exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-sdk' - } + implementation 'org.maplibre.gl:android-sdk:9.4.2' + implementation 'org.maplibre.gl:android-plugin-annotation-v9:1.0.0' + implementation 'org.maplibre.gl:android-plugin-localization-v9:1.0.0' + implementation 'org.maplibre.gl:android-plugin-offline-v9:1.0.0' } compileOptions { sourceCompatibility 1.8 diff --git a/android/src/main/java/com/mapbox/mapboxgl/CircleBuilder.java b/android/src/main/java/com/mapbox/mapboxgl/CircleBuilder.java index 9d902d50..db7accf5 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/CircleBuilder.java +++ b/android/src/main/java/com/mapbox/mapboxgl/CircleBuilder.java @@ -21,6 +21,10 @@ class CircleBuilder implements CircleOptionsSink { this.circleOptions = new CircleOptions(); } + public CircleOptions getCircleOptions(){ + return this.circleOptions; + } + Circle build() { return circleManager.create(circleOptions); } diff --git a/android/src/main/java/com/mapbox/mapboxgl/CircleController.java b/android/src/main/java/com/mapbox/mapboxgl/CircleController.java index 404d37b8..76860ea6 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/CircleController.java +++ b/android/src/main/java/com/mapbox/mapboxgl/CircleController.java @@ -24,6 +24,10 @@ class CircleController implements CircleOptionsSink { this.onTappedListener = onTappedListener; } + public Circle getCircle(){ + return this.circle; + } + boolean onTap() { if (onTappedListener != null) { onTappedListener.onCircleTapped(circle); diff --git a/android/src/main/java/com/mapbox/mapboxgl/Convert.java b/android/src/main/java/com/mapbox/mapboxgl/Convert.java index 9ac55862..ca22c4b5 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/Convert.java +++ b/android/src/main/java/com/mapbox/mapboxgl/Convert.java @@ -72,6 +72,10 @@ static List toAnnotationOrder(Object o) { return annotations; } + static List toAnnotationConsumeTapEvents(Object o) { + return toAnnotationOrder(o); + } + static boolean isScrollByCameraUpdate(Object o) { return toString(toList(o).get(0)).equals("scrollBy"); } diff --git a/android/src/main/java/com/mapbox/mapboxgl/FillBuilder.java b/android/src/main/java/com/mapbox/mapboxgl/FillBuilder.java index 09adcd90..b70589cb 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/FillBuilder.java +++ b/android/src/main/java/com/mapbox/mapboxgl/FillBuilder.java @@ -22,6 +22,10 @@ class FillBuilder implements FillOptionsSink { this.fillOptions = new FillOptions(); } + public FillOptions getFillOptions(){ + return this.fillOptions; + } + Fill build() { return fillManager.create(fillOptions); } diff --git a/android/src/main/java/com/mapbox/mapboxgl/FillController.java b/android/src/main/java/com/mapbox/mapboxgl/FillController.java index 200b13f7..86bc0113 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/FillController.java +++ b/android/src/main/java/com/mapbox/mapboxgl/FillController.java @@ -27,6 +27,10 @@ class FillController implements FillOptionsSink { this.onTappedListener = onTappedListener; } + public Fill getFill(){ + return this.fill; + } + boolean onTap() { if (onTappedListener != null) { onTappedListener.onFillTapped(fill); diff --git a/android/src/main/java/com/mapbox/mapboxgl/GlobalMethodHandler.java b/android/src/main/java/com/mapbox/mapboxgl/GlobalMethodHandler.java index 4906e64f..1b95f47f 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/GlobalMethodHandler.java +++ b/android/src/main/java/com/mapbox/mapboxgl/GlobalMethodHandler.java @@ -5,7 +5,7 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; -import android.content.Context; +import com.mapbox.mapboxsdk.net.ConnectivityReceiver; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -64,14 +64,17 @@ public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { installOfflineMapTiles(tilesDb); result.success(null); break; + case "setOffline": + boolean offline = methodCall.argument("offline"); + ConnectivityReceiver.instance(context).setConnected(offline ? false : null); + result.success(null); + break; case "mergeOfflineRegions": OfflineManagerUtils.mergeRegions(result, context, methodCall.argument("path")); break; - case "setOfflineTileCountLimit": OfflineManagerUtils.setOfflineTileCountLimit(result, context, methodCall.argument("limit").longValue()); break; - case "downloadOfflineRegion": // Get args from caller Map definitionMap = (Map) methodCall.argument("definition"); diff --git a/android/src/main/java/com/mapbox/mapboxgl/LineBuilder.java b/android/src/main/java/com/mapbox/mapboxgl/LineBuilder.java index a164ac60..5d727614 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/LineBuilder.java +++ b/android/src/main/java/com/mapbox/mapboxgl/LineBuilder.java @@ -22,6 +22,10 @@ class LineBuilder implements LineOptionsSink { this.lineOptions = new LineOptions(); } + public LineOptions getLineOptions(){ + return this.lineOptions; + } + Line build() { return lineManager.create(lineOptions); } diff --git a/android/src/main/java/com/mapbox/mapboxgl/LineController.java b/android/src/main/java/com/mapbox/mapboxgl/LineController.java index 815401a5..d7715909 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/LineController.java +++ b/android/src/main/java/com/mapbox/mapboxgl/LineController.java @@ -32,6 +32,10 @@ class LineController implements LineOptionsSink { this.onTappedListener = onTappedListener; } + public Line getLine(){ + return this.line; + } + boolean onTap() { if (onTappedListener != null) { onTappedListener.onLineTapped(line); diff --git a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapBuilder.java b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapBuilder.java index 7cafa105..045bb76f 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapBuilder.java +++ b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapBuilder.java @@ -32,11 +32,13 @@ class MapboxMapBuilder implements MapboxMapOptionsSink { private int myLocationRenderMode = 0; private String styleString = Style.MAPBOX_STREETS; private List annotationOrder = new ArrayList(); + private List annotationConsumeTapEvents = new ArrayList(); + MapboxMapController build( int id, Context context, BinaryMessenger messenger, MapboxMapsPlugin.LifecycleProvider lifecycleProvider, String accessToken) { final MapboxMapController controller = - new MapboxMapController(id, context, messenger, lifecycleProvider, options, accessToken, styleString, annotationOrder); + new MapboxMapController(id, context, messenger, lifecycleProvider, options, accessToken, styleString, annotationOrder, annotationConsumeTapEvents); controller.init(); controller.setMyLocationEnabled(myLocationEnabled); controller.setMyLocationTrackingMode(myLocationTrackingMode); @@ -178,4 +180,9 @@ public void setAttributionButtonMargins(int x, int y) { public void setAnnotationOrder(List annotations) { this.annotationOrder = annotations; } + + public void setAnnotationConsumeTapEvents(List annotations) { + this.annotationConsumeTapEvents = annotations; + } + } diff --git a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java index ba5cfb0d..7ae9c3ee 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java +++ b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java @@ -15,7 +15,6 @@ import android.graphics.RectF; import android.location.Location; import android.os.Build; -import android.os.Bundle; import android.util.DisplayMetrics; import android.util.Log; import android.view.Gravity; @@ -29,11 +28,6 @@ import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; -import com.mapbox.android.core.location.LocationEngine; -import com.mapbox.android.core.location.LocationEngineCallback; -import com.mapbox.android.core.location.LocationEngineProvider; -import com.mapbox.android.core.location.LocationEngineResult; -import com.mapbox.android.telemetry.TelemetryEnabler; import com.mapbox.geojson.Feature; import com.mapbox.mapboxsdk.Mapbox; import com.mapbox.mapboxsdk.camera.CameraPosition; @@ -45,6 +39,10 @@ import com.mapbox.mapboxsdk.location.LocationComponent; import com.mapbox.mapboxsdk.location.LocationComponentOptions; import com.mapbox.mapboxsdk.location.OnCameraTrackingChangedListener; +import com.mapbox.mapboxsdk.location.engine.LocationEngine; +import com.mapbox.mapboxsdk.location.engine.LocationEngineCallback; +import com.mapbox.mapboxsdk.location.engine.LocationEngineProvider; +import com.mapbox.mapboxsdk.location.engine.LocationEngineResult; import com.mapbox.mapboxsdk.location.modes.CameraMode; import com.mapbox.mapboxsdk.location.modes.RenderMode; import com.mapbox.mapboxsdk.maps.MapView; @@ -56,10 +54,13 @@ import com.mapbox.mapboxsdk.plugins.annotation.Annotation; import com.mapbox.mapboxsdk.plugins.annotation.Circle; import com.mapbox.mapboxsdk.plugins.annotation.CircleManager; +import com.mapbox.mapboxsdk.plugins.annotation.CircleOptions; import com.mapbox.mapboxsdk.plugins.annotation.Fill; import com.mapbox.mapboxsdk.plugins.annotation.FillManager; +import com.mapbox.mapboxsdk.plugins.annotation.FillOptions; import com.mapbox.mapboxsdk.plugins.annotation.Line; import com.mapbox.mapboxsdk.plugins.annotation.LineManager; +import com.mapbox.mapboxsdk.plugins.annotation.LineOptions; import com.mapbox.mapboxsdk.plugins.annotation.OnAnnotationClickListener; import com.mapbox.mapboxsdk.plugins.annotation.Symbol; import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager; @@ -71,10 +72,11 @@ import java.io.IOException; import java.io.InputStream; -import java.util.*; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.ArrayList; +import java.util.Map; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; @@ -130,7 +132,8 @@ final class MapboxMapController private LocationEngineCallback locationEngineCallback = null; private LocalizationPlugin localizationPlugin; private Style style; - private List annotationOrder = new ArrayList(); + private List annotationOrder; + private List annotationConsumeTapEvents; MapboxMapController( int id, @@ -140,7 +143,8 @@ final class MapboxMapController MapboxMapOptions options, String accessToken, String styleStringInitial, - List annotationOrder) { + List annotationOrder, + List annotationConsumeTapEvents) { MapBoxUtils.getMapbox(context, accessToken); this.id = id; this.context = context; @@ -155,6 +159,7 @@ final class MapboxMapController methodChannel = new MethodChannel(messenger, "plugins.flutter.io/mapbox_maps_" + id); methodChannel.setMethodCallHandler(this); this.annotationOrder = annotationOrder; + this.annotationConsumeTapEvents = annotationConsumeTapEvents; } @Override @@ -380,8 +385,6 @@ private void enableSymbolManager(@NonNull Style style) { } } - - private void enableLineManager(@NonNull Style style) { if (lineManager == null) { lineManager = new LineManager(mapView, mapboxMap, style); @@ -461,6 +464,19 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { result.success(reply); break; } + case "map#toScreenLocationBatch": { + double[] param = (double[])call.argument("coordinates"); + double[] reply = new double[param.length]; + + for (int i = 0; i < param.length; i += 2) { + PointF pointf = mapboxMap.getProjection().toScreenLocation(new LatLng(param[i], param[i + 1])); + reply[i] = pointf.x; + reply[i + 1] = pointf.y; + } + + result.success(reply); + break; + } case "map#toLatLng": { Map reply = new HashMap<>(); LatLng latlng = mapboxMap.getProjection().fromScreenLocation(new PointF( ((Double) call.argument("x")).floatValue(), ((Double) call.argument("y")).floatValue())); @@ -563,14 +579,11 @@ public void onCancel() { break; } case "map#setTelemetryEnabled": { - final boolean enabled = call.argument("enabled"); - Mapbox.getTelemetry().setUserTelemetryRequestState(enabled); result.success(null); break; } case "map#getTelemetryEnabled": { - final TelemetryEnabler.State telemetryState = TelemetryEnabler.retrieveTelemetryStateFromPreferences(); - result.success(telemetryState == TelemetryEnabler.State.ENABLED); + result.success(false); break; } case "map#invalidateAmbientCache": { @@ -606,7 +619,7 @@ public void onError(@NonNull String message) { for (Symbol symbol : newSymbols) { symbolId = String.valueOf(symbol.getId()); newSymbolIds.add(symbolId); - symbols.put(symbolId, new SymbolController(symbol, true, this)); + symbols.put(symbolId, new SymbolController(symbol, annotationConsumeTapEvents.contains("AnnotationType.symbol"), this)); } } } @@ -614,7 +627,7 @@ public void onError(@NonNull String message) { break; } case "symbols#removeAll": { - final ArrayList symbolIds = call.argument("symbols"); + final ArrayList symbolIds = call.argument("ids"); SymbolController symbolController; List symbolList = new ArrayList(); @@ -676,7 +689,7 @@ public void onError(@NonNull String message) { Convert.interpretLineOptions(call.argument("options"), lineBuilder); final Line line = lineBuilder.build(); final String lineId = String.valueOf(line.getId()); - lines.put(lineId, new LineController(line, true, this)); + lines.put(lineId, new LineController(line, annotationConsumeTapEvents.contains("AnnotationType.line"), this)); result.success(lineId); break; } @@ -686,6 +699,47 @@ public void onError(@NonNull String message) { result.success(null); break; } + case "line#addAll": { + List newIds = new ArrayList(); + final List options = call.argument("options"); + List optionList = new ArrayList(); + if (options != null) { + LineBuilder builder; + for (Object o : options) { + builder = newLineBuilder(); + Convert.interpretLineOptions(o, builder); + optionList.add(builder.getLineOptions()); + } + if (!optionList.isEmpty()) { + List newLines = lineManager.create(optionList); + String id; + for (Line line : newLines) { + id = String.valueOf(line.getId()); + newIds.add(id); + lines.put(id, new LineController(line, true, this)); + } + } + } + result.success(newIds); + break; + } + case "line#removeAll": { + final ArrayList ids = call.argument("ids"); + LineController lineController; + + List toBeRemoved = new ArrayList(); + for(String id : ids){ + lineController = lines.remove(id); + if (lineController != null) { + toBeRemoved.add(lineController.getLine()); + } + } + if(!toBeRemoved.isEmpty()) { + lineManager.delete(toBeRemoved); + } + result.success(null); + break; + } case "line#update": { final String lineId = call.argument("line"); final LineController line = line(lineId); @@ -713,10 +767,51 @@ public void onError(@NonNull String message) { Convert.interpretCircleOptions(call.argument("options"), circleBuilder); final Circle circle = circleBuilder.build(); final String circleId = String.valueOf(circle.getId()); - circles.put(circleId, new CircleController(circle, true, this)); + circles.put(circleId, new CircleController(circle, annotationConsumeTapEvents.contains("AnnotationType.circle"), this)); result.success(circleId); break; } + case "circle#addAll": { + List newIds = new ArrayList(); + final List options = call.argument("options"); + List optionList = new ArrayList(); + if (options != null) { + CircleBuilder builder; + for (Object o : options) { + builder = newCircleBuilder(); + Convert.interpretCircleOptions(o, builder); + optionList.add(builder.getCircleOptions()); + } + if (!optionList.isEmpty()) { + List newCircles = circleManager.create(optionList); + String id; + for (Circle circle : newCircles) { + id = String.valueOf(circle.getId()); + newIds.add(id); + circles.put(id, new CircleController(circle, true, this)); + } + } + } + result.success(newIds); + break; + } + case "circle#removeAll": { + final ArrayList ids = call.argument("ids"); + CircleController circleController; + + List toBeRemoved = new ArrayList(); + for(String id : ids){ + circleController = circles.remove(id); + if (circleController != null) { + toBeRemoved.add(circleController.getCircle()); + } + } + if(!toBeRemoved.isEmpty()) { + circleManager.delete(toBeRemoved); + } + result.success(null); + break; + } case "circle#remove": { final String circleId = call.argument("circle"); removeCircle(circleId); @@ -747,10 +842,52 @@ public void onError(@NonNull String message) { Convert.interpretFillOptions(call.argument("options"), fillBuilder); final Fill fill = fillBuilder.build(); final String fillId = String.valueOf(fill.getId()); - fills.put(fillId, new FillController(fill, true, this)); + fills.put(fillId, new FillController(fill, annotationConsumeTapEvents.contains("AnnotationType.fill"), this)); result.success(fillId); break; } + + case "fill#addAll": { + List newIds = new ArrayList(); + final List options = call.argument("options"); + List optionList = new ArrayList(); + if (options != null) { + FillBuilder builder; + for (Object o : options) { + builder = newFillBuilder(); + Convert.interpretFillOptions(o, builder); + optionList.add(builder.getFillOptions()); + } + if (!optionList.isEmpty()) { + List newFills = fillManager.create(optionList); + String id; + for (Fill fill : newFills) { + id = String.valueOf(fill.getId()); + newIds.add(id); + fills.put(id, new FillController(fill, true, this)); + } + } + } + result.success(newIds); + break; + } + case "fill#removeAll": { + final ArrayList ids = call.argument("ids"); + FillController fillController; + + List toBeRemoved = new ArrayList(); + for(String id : ids){ + fillController = fills.remove(id); + if (fillController != null) { + toBeRemoved.add(fillController.getFill()); + } + } + if(!toBeRemoved.isEmpty()) { + fillManager.delete(toBeRemoved); + } + result.success(null); + break; + } case "fill#remove": { final String fillId = call.argument("fill"); removeFill(fillId); @@ -891,31 +1028,27 @@ public boolean onAnnotationClick(Annotation annotation) { if (annotation instanceof Symbol) { final SymbolController symbolController = symbols.get(String.valueOf(annotation.getId())); if (symbolController != null) { - symbolController.onTap(); - return true; + return symbolController.onTap(); } } if (annotation instanceof Line) { final LineController lineController = lines.get(String.valueOf(annotation.getId())); if (lineController != null) { - lineController.onTap(); - return true; + return lineController.onTap(); } } if (annotation instanceof Circle) { final CircleController circleController = circles.get(String.valueOf(annotation.getId())); if (circleController != null) { - circleController.onTap(); - return true; + return circleController.onTap(); } } if (annotation instanceof Fill) { final FillController fillController = fills.get(String.valueOf(annotation.getId())); if (fillController != null) { - fillController.onTap(); - return true; + return fillController.onTap(); } } return false; diff --git a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapFactory.java b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapFactory.java index 4c2d378c..5a1b6c60 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapFactory.java +++ b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapFactory.java @@ -39,6 +39,10 @@ public PlatformView create(Context context, int id, Object args) { List annotations = Convert.toAnnotationOrder(params.get("annotationOrder")); builder.setAnnotationOrder(annotations); } + if (params.containsKey("annotationConsumeTapEvents")) { + List annotations = Convert.toAnnotationConsumeTapEvents(params.get("annotationConsumeTapEvents")); + builder.setAnnotationConsumeTapEvents(annotations); + } return builder.build(id, context, messenger, lifecycleProvider, (String) params.get("accessToken")); } } diff --git a/example/lib/animate_camera.dart b/example/lib/animate_camera.dart index 69781794..452cc007 100644 --- a/example/lib/animate_camera.dart +++ b/example/lib/animate_camera.dart @@ -56,26 +56,32 @@ class AnimateCameraState extends State { children: [ TextButton( onPressed: () { - mapController.animateCamera( - CameraUpdate.newCameraPosition( - const CameraPosition( - bearing: 270.0, - target: LatLng(51.5160895, -0.1294527), - tilt: 30.0, - zoom: 17.0, - ), - ), - ).then((result)=>print("mapController.animateCamera() returned $result")); + mapController + .animateCamera( + CameraUpdate.newCameraPosition( + const CameraPosition( + bearing: 270.0, + target: LatLng(51.5160895, -0.1294527), + tilt: 30.0, + zoom: 17.0, + ), + ), + ) + .then((result) => print( + "mapController.animateCamera() returned $result")); }, child: const Text('newCameraPosition'), ), TextButton( onPressed: () { - mapController.animateCamera( - CameraUpdate.newLatLng( - const LatLng(56.1725505, 10.1850512), - ), - ).then((result)=>print("mapController.animateCamera() returned $result")); + mapController + .animateCamera( + CameraUpdate.newLatLng( + const LatLng(56.1725505, 10.1850512), + ), + ) + .then((result) => print( + "mapController.animateCamera() returned $result")); }, child: const Text('newLatLng'), ), diff --git a/example/lib/custom_marker.dart b/example/lib/custom_marker.dart new file mode 100644 index 00000000..80ff2f77 --- /dev/null +++ b/example/lib/custom_marker.dart @@ -0,0 +1,255 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:maplibre_gl/mapbox_gl.dart'; + +import 'main.dart'; +import 'page.dart'; + +const randomMarkerNum = 100; + +class CustomMarkerPage extends ExamplePage { + CustomMarkerPage() : super(const Icon(Icons.place), 'Custom marker'); + + @override + Widget build(BuildContext context) { + return CustomMarker(); + } +} + +class CustomMarker extends StatefulWidget { + const CustomMarker(); + + @override + State createState() => CustomMarkerState(); +} + +class CustomMarkerState extends State { + final Random _rnd = new Random(); + + MaplibreMapController _mapController; + List _markers = []; + List<_MarkerState> _markerStates = []; + + void _addMarkerStates(_MarkerState markerState) { + _markerStates.add(markerState); + } + + void _onMapCreated(MaplibreMapController controller) { + _mapController = controller; + controller.addListener(() { + if (controller.isCameraMoving) { + _updateMarkerPosition(); + } + }); + } + + void _onStyleLoadedCallback() { + print('onStyleLoadedCallback'); + } + + void _onMapLongClickCallback(Point point, LatLng coordinates) { + _addMarker(point, coordinates); + } + + void _onCameraIdleCallback() { + _updateMarkerPosition(); + } + + void _updateMarkerPosition() { + final coordinates = []; + + for (final markerState in _markerStates) { + coordinates.add(markerState.getCoordinate()); + } + + _mapController.toScreenLocationBatch(coordinates).then((points){ + _markerStates.asMap().forEach((i, value){ + _markerStates[i].updatePosition(points[i]); + }); + }); + } + + void _addMarker(Point point, LatLng coordinates) { + setState(() { + _markers.add(Marker(_rnd.nextInt(100000).toString(), coordinates, point, _addMarkerStates)); + }); + } + + @override + Widget build(BuildContext context) { + return new Scaffold( + body: Stack( + children: [ + MaplibreMap( + accessToken: MapsDemo.ACCESS_TOKEN, + trackCameraPosition: true, + onMapCreated: _onMapCreated, + onMapLongClick: _onMapLongClickCallback, + onCameraIdle: _onCameraIdleCallback, + onStyleLoadedCallback: _onStyleLoadedCallback, + initialCameraPosition: const CameraPosition(target: LatLng(35.0, 135.0), zoom: 5), + ), + IgnorePointer( + ignoring: true, + child: + Stack( + children: _markers, + ) + ) + ] + ), + floatingActionButton: FloatingActionButton( + onPressed: (){ + //_measurePerformance(); + + // Generate random markers + var param = []; + for (var i = 0; i < randomMarkerNum; i++) { + final lat = _rnd.nextDouble() * 20 + 30; + final lng = _rnd.nextDouble() * 20 + 125; + param.add(LatLng(lat, lng)); + } + + _mapController.toScreenLocationBatch(param).then((value) { + for (var i = 0; i < randomMarkerNum; i++) { + var point = Point(value[i].x, value[i].y); + _addMarker(point, param[i]); + } + }); + }, + child: Icon(Icons.add), + ), + ); + } + + // ignore: unused_element + void _measurePerformance() { + final trial = 10; + final batches = [500, 1000, 1500, 2000, 2500, 3000]; + var results = Map>(); + for (final batch in batches) { + results[batch] = [0.0, 0.0]; + } + + _mapController.toScreenLocation(LatLng(0, 0)); + Stopwatch sw = Stopwatch(); + + for (final batch in batches) { + // + // primitive + // + for (var i = 0; i < trial; i++) { + sw.start(); + var list = >>[]; + for (var j = 0; j < batch; j++) { + var p = _mapController.toScreenLocation(LatLng(j.toDouble() % 80, j.toDouble() % 300)); + list.add(p); + } + Future.wait(list); + sw.stop(); + results[batch][0] += sw.elapsedMilliseconds; + sw.reset(); + } + + // + // batch + // + for (var i = 0; i < trial; i++) { + sw.start(); + var param = []; + for (var j = 0; j < batch; j++) { + param.add(LatLng(j.toDouble() % 80, j.toDouble() % 300)); + } + Future.wait([_mapController.toScreenLocationBatch(param)]); + sw.stop(); + results[batch][1] += sw.elapsedMilliseconds; + sw.reset(); + } + + print('batch=$batch,primitive=${results[batch][0] / trial}ms, batch=${results[batch][1] / trial}ms'); + } + + } +} + +class Marker extends StatefulWidget { + final Point _initialPosition; + final LatLng _coordinate; + final void Function(_MarkerState) _addMarkerState; + + Marker(String key, this._coordinate, this._initialPosition, this._addMarkerState) : super(key: Key(key)); + + @override + State createState() { + final state = _MarkerState(_initialPosition); + _addMarkerState(state); + return state; + } +} + +class _MarkerState extends State with TickerProviderStateMixin { + final _iconSize = 20.0; + + Point _position; + + AnimationController _controller; + Animation _animation; + + _MarkerState(this._position); + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + )..repeat(reverse: true); + _animation = CurvedAnimation( + parent: _controller, + curve: Curves.elasticOut, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + var ratio = 1.0; + + //web does not support Platform._operatingSystem + if (!kIsWeb) { + // iOS returns logical pixel while Android returns screen pixel + ratio = Platform.isIOS ? 1.0 : MediaQuery.of(context).devicePixelRatio; + } + + return + Positioned( + left: _position.x / ratio - _iconSize / 2, + top: _position.y / ratio - _iconSize / 2, + child: + RotationTransition( + turns: _animation, + child: + Image.asset('assets/symbols/2.0x/custom-icon.png', height: _iconSize)) + ); + } + + void updatePosition(Point point) { + setState(() { + _position = point; + }); + } + + LatLng getCoordinate() { + return (widget as Marker)._coordinate; + } +} + diff --git a/example/lib/generated_plugin_registrant.dart b/example/lib/generated_plugin_registrant.dart index caf5915d..0292fea8 100644 --- a/example/lib/generated_plugin_registrant.dart +++ b/example/lib/generated_plugin_registrant.dart @@ -2,6 +2,7 @@ // Generated file. Do not edit. // +// ignore_for_file: directives_ordering // ignore_for_file: lines_longer_than_80_chars import 'package:maplibre_gl_web/mapbox_gl_web.dart'; diff --git a/example/lib/line.dart b/example/lib/line.dart index 8ba2a8a3..fdfb477b 100644 --- a/example/lib/line.dart +++ b/example/lib/line.dart @@ -71,17 +71,16 @@ class LineBodyState extends State { void _add() { controller.addLine( LineOptions( - geometry: [ - LatLng(-33.86711, 151.1947171), - LatLng(-33.86711, 151.1947171), - LatLng(-32.86711, 151.1947171), - LatLng(-33.86711, 152.1947171), - ], - lineColor: "#ff0000", - lineWidth: 14.0, - lineOpacity: 0.5, - draggable: true - ), + geometry: [ + LatLng(-33.86711, 151.1947171), + LatLng(-33.86711, 151.1947171), + LatLng(-32.86711, 151.1947171), + LatLng(-33.86711, 152.1947171), + ], + lineColor: "#ff0000", + lineWidth: 14.0, + lineOpacity: 0.5, + draggable: true), ); setState(() { _lineCount += 1; @@ -96,7 +95,6 @@ class LineBodyState extends State { }); } - Future _changeAlpha() async { double current = _selectedLine.options.lineOpacity; if (current == null) { @@ -123,10 +121,7 @@ class LineBodyState extends State { void onStyleLoadedCallback() { controller.addLine( LineOptions( - geometry: [ - LatLng(37.4220, -122.0841), - LatLng(37.4240, -122.0941) - ], + geometry: [LatLng(37.4220, -122.0841), LatLng(37.4240, -122.0941)], lineColor: "#ff0000", lineWidth: 14.0, lineOpacity: 0.5, @@ -188,13 +183,15 @@ class LineBodyState extends State { ), TextButton( child: const Text('print current LatLng'), - onPressed: - (_selectedLine == null) ? null : () async{ - var latLngs = await controller.getLineLatLngs(_selectedLine); - for (var latLng in latLngs) { - print(latLng.toString()); - } - }, + onPressed: (_selectedLine == null) + ? null + : () async { + var latLngs = await controller + .getLineLatLngs(_selectedLine); + for (var latLng in latLngs) { + print(latLng.toString()); + } + }, ), ], ), diff --git a/example/lib/main.dart b/example/lib/main.dart index 87241248..3057b7cf 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -20,6 +20,8 @@ import 'place_symbol.dart'; import 'place_fill.dart'; import 'scrolling_map.dart'; import 'offline_regions.dart'; +import 'custom_marker.dart'; +import 'place_batch.dart'; final List _allPages = [ MapUiPage(), @@ -35,6 +37,8 @@ final List _allPages = [ ScrollingMapPage(), OfflineRegionsPage(), AnnotationOrderPage(), + CustomMarkerPage(), + BatchAddPage(), ]; class MapsDemo extends StatelessWidget { diff --git a/example/lib/map_ui.dart b/example/lib/map_ui.dart index 4c95fd22..4ac03acf 100644 --- a/example/lib/map_ui.dart +++ b/example/lib/map_ui.dart @@ -48,9 +48,17 @@ class MapUiBodyState extends State { MinMaxZoomPreference _minMaxZoomPreference = MinMaxZoomPreference.unbounded; int _styleStringIndex = 0; // Style string can a reference to a local or remote resources. - // On Android the raw JSON can also be passed via a styleString, on iOS this is not supported. - List _styleStrings = [MapboxStyles.MAPBOX_STREETS, MapboxStyles.SATELLITE, "assets/style.json"]; - List _styleStringLabels = ["MAPBOX_STREETS", "SATELLITE", "LOCAL_ASSET"]; + // On Android the raw JSON can also be passed via a styleString, on iOS this is not supported. + List _styleStrings = [ + MapboxStyles.MAPBOX_STREETS, + MapboxStyles.SATELLITE, + "assets/style.json" + ]; + List _styleStringLabels = [ + "MAPBOX_STREETS", + "SATELLITE", + "LOCAL_ASSET" + ]; bool _rotateGesturesEnabled = true; bool _scrollGesturesEnabled = true; bool _tiltGesturesEnabled = true; @@ -84,8 +92,9 @@ class MapUiBodyState extends State { } Widget _myLocationTrackingModeCycler() { - final MyLocationTrackingMode nextType = - MyLocationTrackingMode.values[(_myLocationTrackingMode.index + 1) % MyLocationTrackingMode.values.length]; + final MyLocationTrackingMode nextType = MyLocationTrackingMode.values[ + (_myLocationTrackingMode.index + 1) % + MyLocationTrackingMode.values.length]; return TextButton( child: Text('change to $nextType'), onPressed: () { @@ -98,11 +107,16 @@ class MapUiBodyState extends State { Widget _queryFilterToggler() { return TextButton( - child: Text('filter zoo on click ${ _featureQueryFilter == null ? 'disabled' : 'enabled'}'), + child: Text( + 'filter zoo on click ${_featureQueryFilter == null ? 'disabled' : 'enabled'}'), onPressed: () { setState(() { if (_featureQueryFilter == null) { - _featureQueryFilter = ["==", ["get", "type"] , "zoo"]; + _featureQueryFilter = [ + "==", + ["get", "type"], + "zoo" + ]; } else { _featureQueryFilter = null; } @@ -156,7 +170,8 @@ class MapUiBodyState extends State { Widget _setStyleToSatellite() { return TextButton( - child: Text('change map style to ${_styleStringLabels[(_styleStringIndex + 1) % _styleStringLabels.length]}'), + child: Text( + 'change map style to ${_styleStringLabels[(_styleStringIndex + 1) % _styleStringLabels.length]}'), onPressed: () { setState(() { _styleStringIndex = (_styleStringIndex + 1) % _styleStrings.length; @@ -232,12 +247,15 @@ class MapUiBodyState extends State { ); } - Widget _visibleRegionGetter(){ + Widget _visibleRegionGetter() { return TextButton( child: Text('get currently visible region'), - onPressed: () async{ + onPressed: () async { var result = await mapController.getVisibleRegion(); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("SW: ${result.southwest.toString()} NE: ${result.northeast.toString()}"),)); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + "SW: ${result.southwest.toString()} NE: ${result.northeast.toString()}"), + )); }, ); } @@ -255,19 +273,17 @@ class MapUiBodyState extends State { Map feature = jsonDecode(features[0]); if (feature['geometry']['type'] == 'Polygon') { var coordinates = feature['geometry']['coordinates']; - List> geometry = coordinates.map( - (ll) => ll.map( - (l) => LatLng(l[1],l[0]) - ).toList().cast() - ).toList().cast>(); - Fill fill = await mapController.addFill( - FillOptions( - geometry: geometry, - fillColor: "#FF0000", - fillOutlineColor: "#FF0000", - fillOpacity: 0.6, - ) - ); + List> geometry = coordinates + .map( + (ll) => ll.map((l) => LatLng(l[1], l[0])).toList().cast()) + .toList() + .cast>(); + Fill fill = await mapController.addFill(FillOptions( + geometry: geometry, + fillColor: "#FF0000", + fillOutlineColor: "#FF0000", + fillOpacity: 0.6, + )); setState(() { _selectedFill = fill; }); @@ -293,29 +309,36 @@ class MapUiBodyState extends State { myLocationTrackingMode: _myLocationTrackingMode, myLocationRenderMode: MyLocationRenderMode.GPS, onMapClick: (point, latLng) async { - print("Map click: ${point.x},${point.y} ${latLng.latitude}/${latLng.longitude}"); + print( + "Map click: ${point.x},${point.y} ${latLng.latitude}/${latLng.longitude}"); print("Filter $_featureQueryFilter"); - List features = await mapController.queryRenderedFeatures(point, [], _featureQueryFilter); + List features = await mapController.queryRenderedFeatures( + point, [], _featureQueryFilter); print('# features: ${features.length}'); _clearFill(); if (features.length == 0 && _featureQueryFilter != null) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('QueryRenderedFeatures: No features found!'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('QueryRenderedFeatures: No features found!'))); } else { _drawFill(features); - } + } }, onMapLongClick: (point, latLng) async { - print("Map long press: ${point.x},${point.y} ${latLng.latitude}/${latLng.longitude}"); + print( + "Map long press: ${point.x},${point.y} ${latLng.latitude}/${latLng.longitude}"); Point convertedPoint = await mapController.toScreenLocation(latLng); LatLng convertedLatLng = await mapController.toLatLng(point); - print("Map long press converted: ${convertedPoint.x},${convertedPoint.y} ${convertedLatLng.latitude}/${convertedLatLng.longitude}"); - double metersPerPixel = await mapController.getMetersPerPixelAtLatitude(latLng.latitude); - - print ("Map long press The distance measured in meters at latitude ${latLng.latitude} is $metersPerPixel m"); + print( + "Map long press converted: ${convertedPoint.x},${convertedPoint.y} ${convertedLatLng.latitude}/${convertedLatLng.longitude}"); + double metersPerPixel = + await mapController.getMetersPerPixelAtLatitude(latLng.latitude); + print( + "Map long press The distance measured in meters at latitude ${latLng.latitude} is $metersPerPixel m"); - List features = await mapController.queryRenderedFeatures(point, [], null); - if (features.length>0) { + List features = + await mapController.queryRenderedFeatures(point, [], null); + if (features.length > 0) { print(features[0]); } }, @@ -324,8 +347,9 @@ class MapUiBodyState extends State { _myLocationTrackingMode = MyLocationTrackingMode.None; }); }, - onUserLocationUpdated:(location){ - print("new location: ${location.position}, alt.: ${location.altitude}, bearing: ${location.bearing}, speed: ${location.speed}, horiz. accuracy: ${location.horizontalAccuracy}, vert. accuracy: ${location.verticalAccuracy}"); + onUserLocationUpdated: (location) { + print( + "new location: ${location.position}, alt.: ${location.altitude}, bearing: ${location.bearing}, speed: ${location.speed}, horiz. accuracy: ${location.horizontalAccuracy}, vert. accuracy: ${location.verticalAccuracy}"); }, ); @@ -384,8 +408,7 @@ class MapUiBodyState extends State { mapController.addListener(_onMapChanged); _extractMapInfo(); - mapController.getTelemetryEnabled().then((isEnabled) => - setState(() { + mapController.getTelemetryEnabled().then((isEnabled) => setState(() { _telemetryEnabled = isEnabled; })); } diff --git a/example/lib/move_camera.dart b/example/lib/move_camera.dart index 9b46caef..684748a4 100644 --- a/example/lib/move_camera.dart +++ b/example/lib/move_camera.dart @@ -43,7 +43,7 @@ class MoveCameraState extends State { child: MaplibreMap( accessToken: MapsDemo.ACCESS_TOKEN, onMapCreated: _onMapCreated, - onCameraIdle: ()=>print("onCameraIdle"), + onCameraIdle: () => print("onCameraIdle"), initialCameraPosition: const CameraPosition(target: LatLng(0.0, 0.0)), ), diff --git a/example/lib/place_batch.dart b/example/lib/place_batch.dart new file mode 100644 index 00000000..b36101e8 --- /dev/null +++ b/example/lib/place_batch.dart @@ -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 'package:flutter/material.dart'; +import 'package:maplibre_gl/mapbox_gl.dart'; +import 'package:maplibre_gl_example/main.dart'; + +import 'page.dart'; + +const fillOptions = [ + FillOptions( + geometry: [ + [ + LatLng(-33.719, 151.150), + LatLng(-33.858, 151.150), + LatLng(-33.866, 151.401), + LatLng(-33.747, 151.328), + LatLng(-33.719, 151.150), + ], + [ + LatLng(-33.762, 151.250), + LatLng(-33.827, 151.250), + LatLng(-33.833, 151.347), + LatLng(-33.762, 151.250), + ] + ], + fillColor: "#FF0000", + ), + FillOptions(geometry: [ + [ + LatLng(-33.719, 151.550), + LatLng(-33.858, 151.550), + LatLng(-33.866, 151.801), + LatLng(-33.747, 151.728), + LatLng(-33.719, 151.550), + ], + [ + LatLng(-33.762, 151.650), + LatLng(-33.827, 151.650), + LatLng(-33.833, 151.747), + LatLng(-33.762, 151.650), + ] + ], fillColor: "#FF0000"), +]; + +class BatchAddPage extends ExamplePage { + BatchAddPage() : super(const Icon(Icons.check_circle), 'Batch add/remove'); + + @override + Widget build(BuildContext context) { + return const BatchAddBody(); + } +} + +class BatchAddBody extends StatefulWidget { + const BatchAddBody(); + + @override + State createState() => BatchAddBodyState(); +} + +class BatchAddBodyState extends State { + BatchAddBodyState(); + List _fills = []; + List _circles = []; + List _lines = []; + List _symbols = []; + + static final LatLng center = const LatLng(-33.86711, 151.1947171); + + MaplibreMapController controller; + + void _onMapCreated(MaplibreMapController controller) { + this.controller = controller; + } + + List makeLinesOptionsForFillOptions( + Iterable options) { + final listOptions = []; + for (final option in options) { + for (final geom in option.geometry) { + listOptions.add(LineOptions(geometry: geom, lineColor: "#00FF00")); + } + } + return listOptions; + } + + List makeCircleOptionsForFillOptions( + Iterable options) { + final circleOptions = []; + for (final option in options) { + // put circles only on the outside + for (final latLng in option.geometry.first) { + circleOptions + .add(CircleOptions(geometry: latLng, circleColor: "#00FF00")); + } + } + return circleOptions; + } + + List makeSymbolOptionsForFillOptions( + Iterable options) { + final symbolOptions = []; + for (final option in options) { + // put symbols only on the inner most ring if it exists + if (option.geometry.length > 1) + for (final latLng in option.geometry.last) { + symbolOptions + .add(SymbolOptions(iconImage: 'hospital-11', geometry: latLng)); + } + } + return symbolOptions; + } + + void _add() async { + if (_fills.isEmpty) { + _fills = await controller.addFills(fillOptions); + _lines = await controller + .addLines(makeLinesOptionsForFillOptions(fillOptions)); + _circles = await controller + .addCircles(makeCircleOptionsForFillOptions(fillOptions)); + _symbols = await controller + .addSymbols(makeSymbolOptionsForFillOptions(fillOptions)); + } + } + + void _remove() { + controller.removeFills(_fills); + controller.removeLines(_lines); + controller.removeCircles(_circles); + controller.removeSymbols(_symbols); + _fills.clear(); + _lines.clear(); + _circles.clear(); + _symbols.clear(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + height: 200.0, + child: MaplibreMap( + accessToken: MapsDemo.ACCESS_TOKEN, + onMapCreated: _onMapCreated, + initialCameraPosition: const CameraPosition( + target: LatLng(-33.8, 151.511), + zoom: 8.2, + ), + annotationOrder: const [ + AnnotationType.fill, + AnnotationType.line, + AnnotationType.circle, + AnnotationType.symbol, + ], + ), + ), + ), + Expanded( + child: SingleChildScrollView( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + Column( + children: [ + TextButton( + child: const Text('batch add'), onPressed: _add), + TextButton( + child: const Text('batch remove'), + onPressed: _remove), + ], + ), + ], + ) + ], + ), + ), + ), + ], + ); + } +} diff --git a/example/lib/place_circle.dart b/example/lib/place_circle.dart index 247d4f83..15131d18 100644 --- a/example/lib/place_circle.dart +++ b/example/lib/place_circle.dart @@ -145,7 +145,8 @@ class PlaceCircleBodyState extends State { // default value current = 0; } - _updateSelectedCircle(CircleOptions(circleStrokeWidth: current == 0 ? 5.0 : 0)); + _updateSelectedCircle( + CircleOptions(circleStrokeWidth: current == 0 ? 5.0 : 0)); } Future _changeCircleStrokeColor() async { @@ -156,7 +157,8 @@ class PlaceCircleBodyState extends State { } _updateSelectedCircle( - CircleOptions(circleStrokeColor: current == "#FFFFFF" ? "#FF0000" : "#FFFFFF"), + CircleOptions( + circleStrokeColor: current == "#FFFFFF" ? "#FF0000" : "#FFFFFF"), ); } @@ -191,8 +193,7 @@ class PlaceCircleBodyState extends State { } _updateSelectedCircle( - CircleOptions( - circleColor: "#FFFF00"), + CircleOptions(circleColor: "#FFFF00"), ); } @@ -250,8 +251,9 @@ class PlaceCircleBodyState extends State { children: [ TextButton( child: const Text('change circle-opacity'), - onPressed: - (_selectedCircle == null) ? null : _changeCircleOpacity, + onPressed: (_selectedCircle == null) + ? null + : _changeCircleOpacity, ), TextButton( child: const Text('change circle-radius'), @@ -261,18 +263,21 @@ class PlaceCircleBodyState extends State { ), TextButton( child: const Text('change circle-color'), - onPressed: - (_selectedCircle == null) ? null : _changeCircleColor, + onPressed: (_selectedCircle == null) + ? null + : _changeCircleColor, ), TextButton( child: const Text('change circle-blur'), - onPressed: - (_selectedCircle == null) ? null : _changeCircleBlur, + onPressed: (_selectedCircle == null) + ? null + : _changeCircleBlur, ), TextButton( child: const Text('change circle-stroke-width'), - onPressed: - (_selectedCircle == null) ? null : _changeCircleStrokeWidth, + onPressed: (_selectedCircle == null) + ? null + : _changeCircleStrokeWidth, ), TextButton( child: const Text('change circle-stroke-color'), @@ -300,9 +305,8 @@ class PlaceCircleBodyState extends State { ), TextButton( child: const Text('get current LatLng'), - onPressed: (_selectedCircle == null) - ? null - : _getLatLng, + onPressed: + (_selectedCircle == null) ? null : _getLatLng, ), ], ), @@ -315,4 +319,4 @@ class PlaceCircleBodyState extends State { ], ); } -} \ No newline at end of file +} diff --git a/example/lib/place_source.dart b/example/lib/place_source.dart index f155a703..aea32521 100644 --- a/example/lib/place_source.dart +++ b/example/lib/place_source.dart @@ -30,7 +30,7 @@ class PlaceSymbolBody extends StatefulWidget { class PlaceSymbolBodyState extends State { PlaceSymbolBodyState(); - + static const SOURCE_ID = 'sydney_source'; static const LAYER_ID = 'sydney_layer'; @@ -47,7 +47,8 @@ class PlaceSymbolBodyState extends State { } /// Adds an asset image as a source to the currently displayed style - Future addImageSourceFromAsset(String imageSourceId, String assetName) async { + Future addImageSourceFromAsset( + String imageSourceId, String assetName) async { final ByteData bytes = await rootBundle.load(assetName); final Uint8List list = bytes.buffer.asUint8List(); return controller.addImageSource( @@ -70,7 +71,8 @@ class PlaceSymbolBodyState extends State { return controller.addLayer(imageLayerId, imageSourceId); } - Future addLayerBelow(String imageLayerId, String imageSourceId, String belowLayerId) { + Future addLayerBelow( + String imageLayerId, String imageSourceId, String belowLayerId) { return controller.addLayerBelow(imageLayerId, imageSourceId, belowLayerId); } @@ -112,7 +114,9 @@ class PlaceSymbolBodyState extends State { onPressed: sourceAdded ? null : () { - addImageSourceFromAsset(SOURCE_ID, 'assets/sydney.png').then((value) { + addImageSourceFromAsset( + SOURCE_ID, 'assets/sydney.png') + .then((value) { setState(() => sourceAdded = true); }); }, @@ -130,15 +134,21 @@ class PlaceSymbolBodyState extends State { ), TextButton( child: const Text('Show layer'), - onPressed: sourceAdded ? () => addLayer(LAYER_ID, SOURCE_ID) : null, + onPressed: sourceAdded + ? () => addLayer(LAYER_ID, SOURCE_ID) + : null, ), TextButton( child: const Text('Show layer below water'), - onPressed: sourceAdded ? () => addLayerBelow(LAYER_ID, SOURCE_ID, 'water') : null, + onPressed: sourceAdded + ? () => + addLayerBelow(LAYER_ID, SOURCE_ID, 'water') + : null, ), TextButton( child: const Text('Hide layer'), - onPressed: sourceAdded ? () => removeLayer(LAYER_ID) : null, + onPressed: + sourceAdded ? () => removeLayer(LAYER_ID) : null, ), ], ), diff --git a/example/lib/place_symbol.dart b/example/lib/place_symbol.dart index 766d011b..255dd084 100644 --- a/example/lib/place_symbol.dart +++ b/example/lib/place_symbol.dart @@ -48,7 +48,8 @@ class PlaceSymbolBodyState extends State { void _onStyleLoaded() { addImageFromAsset("assetImage", "assets/symbols/custom-icon.png"); - addImageFromUrl("networkImage", Uri.parse("https://via.placeholder.com/50")); + addImageFromUrl( + "networkImage", Uri.parse("https://via.placeholder.com/50")); } @override @@ -93,20 +94,17 @@ class PlaceSymbolBodyState extends State { void _add(String iconImage) { List availableNumbers = Iterable.generate(12).toList(); controller.symbols.forEach( - (s) => availableNumbers.removeWhere((i) => i == s.data['count']) - ); + (s) => availableNumbers.removeWhere((i) => i == s.data['count'])); if (availableNumbers.isNotEmpty) { - controller.addSymbol( - _getSymbolOptions(iconImage, availableNumbers.first), - {'count': availableNumbers.first} - ); + controller.addSymbol(_getSymbolOptions(iconImage, availableNumbers.first), + {'count': availableNumbers.first}); setState(() { _symbolCount += 1; }); } } - SymbolOptions _getSymbolOptions(String iconImage, int symbolCount){ + SymbolOptions _getSymbolOptions(String iconImage, int symbolCount) { LatLng geometry = LatLng( center.latitude + sin(symbolCount * pi / 6.0) / 20.0, center.longitude + cos(symbolCount * pi / 6.0) / 20.0, @@ -134,18 +132,15 @@ class PlaceSymbolBodyState extends State { Future _addAll(String iconImage) async { List symbolsToAddNumbers = Iterable.generate(12).toList(); controller.symbols.forEach( - (s) => symbolsToAddNumbers.removeWhere((i) => i == s.data['count']) - ); - + (s) => symbolsToAddNumbers.removeWhere((i) => i == s.data['count'])); + if (symbolsToAddNumbers.isNotEmpty) { - final List symbolOptionsList = symbolsToAddNumbers.map( - (i) => _getSymbolOptions(iconImage, i) - ).toList(); - controller.addSymbols( - symbolOptionsList, - symbolsToAddNumbers.map((i) => {'count': i}).toList() - ); - + final List symbolOptionsList = symbolsToAddNumbers + .map((i) => _getSymbolOptions(iconImage, i)) + .toList(); + controller.addSymbols(symbolOptionsList, + symbolsToAddNumbers.map((i) => {'count': i}).toList()); + setState(() { _symbolCount += symbolOptionsList.length; }); @@ -264,7 +259,7 @@ class PlaceSymbolBodyState extends State { ); } - void _getLatLng() async { + void _getLatLng() async { LatLng latLng = await controller.getSymbolLatLng(_selectedSymbol); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -317,8 +312,9 @@ class PlaceSymbolBodyState extends State { ), TextButton( child: const Text('add all'), - onPressed: () => - (_symbolCount == 12) ? null : _addAll("airport-15"), + onPressed: () => (_symbolCount == 12) + ? null + : _addAll("airport-15"), ), TextButton( child: const Text('add (custom icon)'), @@ -331,7 +327,8 @@ class PlaceSymbolBodyState extends State { onPressed: (_selectedSymbol == null) ? null : _remove, ), TextButton( - child: Text('${_iconAllowOverlap ? 'disable' : 'enable'} icon overlap'), + child: Text( + '${_iconAllowOverlap ? 'disable' : 'enable'} icon overlap'), onPressed: _changeIconOverlap, ), TextButton( @@ -347,12 +344,15 @@ class PlaceSymbolBodyState extends State { ), TextButton( child: const Text('add (network image)'), - onPressed: () => - (_symbolCount == 12) ? null : _add("networkImage"), //networkImage added to the style in _onStyleLoaded + onPressed: () => (_symbolCount == 12) + ? null + : _add( + "networkImage"), //networkImage added to the style in _onStyleLoaded ), TextButton( child: const Text('add (custom font)'), - onPressed: () => (_symbolCount == 12) ? null : _add("customFont"), + onPressed: () => + (_symbolCount == 12) ? null : _add("customFont"), ) ], ), @@ -405,9 +405,8 @@ class PlaceSymbolBodyState extends State { ), TextButton( child: const Text('get current LatLng'), - onPressed: (_selectedSymbol == null) - ? null - : _getLatLng, + onPressed: + (_selectedSymbol == null) ? null : _getLatLng, ), ], ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 96a78292..09293ff1 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ">=2.0.0-dev.68.0 <3.0.0" + sdk: ">=2.1.0 <3.0.0" dependencies: flutter: diff --git a/ios/Classes/MapboxMapController.swift b/ios/Classes/MapboxMapController.swift index 1444bac1..27f2e6c4 100644 --- a/ios/Classes/MapboxMapController.swift +++ b/ios/Classes/MapboxMapController.swift @@ -23,6 +23,7 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma private var fillAnnotationController: MGLPolygonAnnotationController? private var annotationOrder = [String]() + private var annotationConsumeTapEvents = [String]() func view() -> UIView { return mapView @@ -63,9 +64,17 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma if let annotationOrderArg = args["annotationOrder"] as? [String] { annotationOrder = annotationOrderArg } + if let annotationConsumeTapEventsArg = args["annotationConsumeTapEvents"] as? [String] { + annotationConsumeTapEvents = annotationConsumeTapEventsArg + } } } - + func removeAllForController(controller: MGLAnnotationController, ids: [String]){ + let idSet = Set(ids) + let annotations = controller.styleAnnotations() + controller.removeStyleAnnotations(annotations.filter { idSet.contains($0.identifier) }) + } + func onMethodCall(methodCall: FlutterMethodCall, result: @escaping FlutterResult) { switch(methodCall.method) { case "map#waitForMap": @@ -176,6 +185,25 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma reply["x"] = returnVal.x as NSObject reply["y"] = returnVal.y as NSObject result(reply) + case "map#toScreenLocationBatch": + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let data = arguments["coordinates"] as? FlutterStandardTypedData else { return } + let latLngs = data.data.withUnsafeBytes { + Array( + UnsafeBufferPointer( + start: $0.baseAddress!.assumingMemoryBound(to: Double.self), + count:Int(data.elementCount)) + ) + } + var reply: [Double] = Array(repeating: 0.0, count: latLngs.count) + for i in stride(from: 0, to: latLngs.count, by: 2) { + let coordinate = CLLocationCoordinate2DMake(latLngs[i], latLngs[i + 1]) + let returnVal = mapView.convert(coordinate, toPointTo: mapView) + reply[i] = Double(returnVal.x) + reply[i + 1] = Double(returnVal.y) + } + result(FlutterStandardTypedData( + float64: Data(bytes: &reply, count: reply.count * 8) )) case "map#getMetersPerPixelAtLatitude": guard let arguments = methodCall.arguments as? [String: Any] else { return } var reply = [String: NSObject]() @@ -225,6 +253,7 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma } if !symbols.isEmpty { symbolAnnotationController.addStyleAnnotations(symbols) + symbolAnnotationController.annotationsInteractionEnabled = annotationConsumeTapEvents.contains("AnnotationType.symbol") } result(symbols.map { $0.identifier }) @@ -252,16 +281,11 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma case "symbols#removeAll": guard let symbolAnnotationController = symbolAnnotationController else { return } guard let arguments = methodCall.arguments as? [String: Any] else { return } - guard let symbolIds = arguments["symbols"] as? [String] else { return } - var symbols: [MGLSymbolStyleAnnotation] = []; + guard let symbolIds = arguments["ids"] as? [String] else { return } - for symbol in symbolAnnotationController.styleAnnotations(){ - if symbolIds.contains(symbol.identifier) { - symbols.append(symbol as! MGLSymbolStyleAnnotation) - } - } - symbolAnnotationController.removeStyleAnnotations(symbols) + removeAllForController(controller:symbolAnnotationController, ids:symbolIds) result(nil) + case "symbol#getGeometry": guard let symbolAnnotationController = symbolAnnotationController else { return } guard let arguments = methodCall.arguments as? [String: Any] else { return } @@ -312,10 +336,39 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma let circle = MGLCircleStyleAnnotation(center: coordinate) Convert.interpretCircleOptions(options: arguments["options"], delegate: circle) circleAnnotationController.addStyleAnnotation(circle) + circleAnnotationController.annotationsInteractionEnabled = annotationConsumeTapEvents.contains("AnnotationType.circle") result(circle.identifier) } else { result(nil) } + + case "circle#addAll": + guard let circleAnnotationController = circleAnnotationController else { return } + guard let arguments = methodCall.arguments as? [String: Any] else { return } + // Parse geometry + var identifier: String? = nil + if let allOptions = arguments["options"] as? [[String: Any]]{ + var circles: [MGLCircleStyleAnnotation] = []; + + for options in allOptions { + if let geometry = options["geometry"] as? [Double] { + guard geometry.count > 0 else { break } + + let coordinate = CLLocationCoordinate2DMake(geometry[0], geometry[1]) + let circle = MGLCircleStyleAnnotation(center: coordinate) + Convert.interpretCircleOptions(options: options, delegate: circle) + circles.append(circle) + } + } + if !circles.isEmpty { + circleAnnotationController.addStyleAnnotations(circles) + } + result(circles.map { $0.identifier }) + } + else { + result(nil) + } + case "circle#update": guard let circleAnnotationController = circleAnnotationController else { return } guard let arguments = methodCall.arguments as? [String: Any] else { return } @@ -341,6 +394,16 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma } } result(nil) + + case "circle#removeAll": + guard let circleAnnotationController = circleAnnotationController else { return } + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let ids = arguments["ids"] as? [String] else { return } + + removeAllForController(controller:circleAnnotationController, ids:ids) + result(nil) + + case "line#add": guard let lineAnnotationController = lineAnnotationController else { return } guard let arguments = methodCall.arguments as? [String: Any] else { return } @@ -355,10 +418,43 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma let line = MGLLineStyleAnnotation(coordinates: lineCoordinates, count: UInt(lineCoordinates.count)) Convert.interpretLineOptions(options: arguments["options"], delegate: line) lineAnnotationController.addStyleAnnotation(line) + lineAnnotationController.annotationsInteractionEnabled = annotationConsumeTapEvents.contains("AnnotationType.line") result(line.identifier) } else { result(nil) } + + case "line#addAll": + guard let lineAnnotationController = lineAnnotationController else { return } + guard let arguments = methodCall.arguments as? [String: Any] else { return } + // Parse geometry + var identifier: String? = nil + if let allOptions = arguments["options"] as? [[String: Any]]{ + var lines: [MGLLineStyleAnnotation] = []; + + for options in allOptions { + if let geometry = options["geometry"] as? [[Double]] { + guard geometry.count > 0 else { break } + // Convert geometry to coordinate and create a line. + var lineCoordinates: [CLLocationCoordinate2D] = [] + for coordinate in geometry { + lineCoordinates.append(CLLocationCoordinate2DMake(coordinate[0], coordinate[1])) + } + let line = MGLLineStyleAnnotation(coordinates: lineCoordinates, count: UInt(lineCoordinates.count)) + Convert.interpretLineOptions(options: options, delegate: line) + lines.append(line) + } + } + if !lines.isEmpty { + lineAnnotationController.addStyleAnnotations(lines) + } + result(lines.map { $0.identifier }) + } + else { + result(nil) + } + + case "line#update": guard let lineAnnotationController = lineAnnotationController else { return } guard let arguments = methodCall.arguments as? [String: Any] else { return } @@ -384,6 +480,15 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma } } result(nil) + + case "line#removeAll": + guard let lineAnnotationController = lineAnnotationController else { return } + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let ids = arguments["ids"] as? [String] else { return } + + removeAllForController(controller:lineAnnotationController, ids:ids) + result(nil) + case "line#getGeometry": guard let lineAnnotationController = lineAnnotationController else { return } guard let arguments = methodCall.arguments as? [String: Any] else { return } @@ -417,9 +522,43 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma let fill = MGLPolygonStyleAnnotation(coordinates: fillCoordinates, count: UInt(fillCoordinates.count), interiorPolygons: polygons) Convert.interpretFillOptions(options: arguments["options"], delegate: fill) fillAnnotationController.addStyleAnnotation(fill) + fillAnnotationController.annotationsInteractionEnabled = annotationConsumeTapEvents.contains("AnnotationType.fill") identifier = fill.identifier } + result(identifier) + + case "fill#addAll": + guard let fillAnnotationController = fillAnnotationController else { return } + guard let arguments = methodCall.arguments as? [String: Any] else { return } + // Parse geometry + var identifier: String? = nil + if let allOptions = arguments["options"] as? [[String: Any]]{ + var fills: [MGLPolygonStyleAnnotation] = []; + + for options in allOptions{ + if let geometry = options["geometry"] as? [[[Double]]] { + guard geometry.count > 0 else { break } + // Convert geometry to coordinate and interior polygonc. + var fillCoordinates: [CLLocationCoordinate2D] = [] + for coordinate in geometry[0] { + fillCoordinates.append(CLLocationCoordinate2DMake(coordinate[0], coordinate[1])) + } + let polygons = Convert.toPolygons(geometry: geometry.tail) + let fill = MGLPolygonStyleAnnotation(coordinates: fillCoordinates, count: UInt(fillCoordinates.count), interiorPolygons: polygons) + Convert.interpretFillOptions(options: options, delegate: fill) + fills.append(fill) + } + } + if !fills.isEmpty { + fillAnnotationController.addStyleAnnotations(fills) + } + result(fills.map { $0.identifier }) + } + else { + result(nil) + } + case "fill#update": guard let fillAnnotationController = fillAnnotationController else { return } guard let arguments = methodCall.arguments as? [String: Any] else { return } @@ -432,6 +571,7 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma break; } } + result(nil) case "fill#remove": guard let fillAnnotationController = fillAnnotationController else { return } @@ -445,6 +585,15 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma } } result(nil) + + case "fill#removeAll": + guard let fillAnnotationController = fillAnnotationController else { return } + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let ids = arguments["ids"] as? [String] else { return } + + removeAllForController(controller:fillAnnotationController, ids:ids) + result(nil) + case "style#addImage": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let name = arguments["name"] as? String else { return } @@ -459,6 +608,8 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma self.mapView.style?.setImage(image, forName: name) } result(nil) + + case "style#addImageSource": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let imageSourceId = arguments["imageSourceId"] as? String else { return } diff --git a/ios/Classes/OfflinePackDownloadManager.swift b/ios/Classes/OfflinePackDownloadManager.swift index bec1420d..e1802ff9 100644 --- a/ios/Classes/OfflinePackDownloadManager.swift +++ b/ios/Classes/OfflinePackDownloadManager.swift @@ -119,7 +119,7 @@ class OfflinePackDownloader { guard let pack = notification.object as? MGLOfflinePack, verifyPack(pack: pack) else { return } let error = notification.userInfo?[MGLOfflinePackUserInfoKey.error] as? NSError - print("Pack download error: \(error?.localizedDescription)") + print("Pack download error: \(String(describing: error?.localizedDescription))") // set download state to inactive isCompleted = true channelHandler.onError( @@ -133,6 +133,7 @@ class OfflinePackDownloader { details: nil )) if let region = OfflineRegion.fromOfflinePack(pack) { + OfflineManagerUtils.deleteRegion(result: result, id: region.id) OfflineManagerUtils.releaseDownloader(id:region.id) } } diff --git a/lib/src/controller.dart b/lib/src/controller.dart index 905692f1..1325c130 100644 --- a/lib/src/controller.dart +++ b/lib/src/controller.dart @@ -100,7 +100,7 @@ class MaplibreMapController extends ChangeNotifier { .add((cameraPosition) { _isCameraMoving = false; if (cameraPosition != null) { - _cameraPosition = cameraPosition; + _cameraPosition = cameraPosition; } if (onCameraIdle != null) { onCameraIdle(); @@ -362,6 +362,14 @@ class MaplibreMapController extends ChangeNotifier { return result.first; } + /// Adds multiple symbols to the map, configured using the specified custom + /// [options]. + /// + /// Change listeners are notified once the symbol has been added on the + /// platform side. + /// + /// The returned [Future] completes with the added symbol once listeners have + /// been notified. Future> addSymbols(List options, [List data]) async { final List effectiveOptions = @@ -416,13 +424,18 @@ class MaplibreMapController extends ChangeNotifier { notifyListeners(); } + /// Removes the specified [symbols] from the map. The symbols must be current + /// members of the [symbols] set. + /// + /// Change listeners are notified once the symbol has been removed on the + /// platform side. + /// + /// The returned [Future] completes once listeners have been notified. Future removeSymbols(Iterable symbols) async { - assert(symbols.length > 0); - symbols.forEach((s) { - assert(_symbols[s.id] == s); - }); + final ids = symbols.where((s) => _symbols[s.id] == s).map((s) => s.id); + assert(symbols.length == ids.length); - await _removeSymbols(symbols.map((s) => s.id)); + await _removeSymbols(ids); notifyListeners(); } @@ -434,8 +447,8 @@ class MaplibreMapController extends ChangeNotifier { /// The returned [Future] completes once listeners have been notified. Future clearSymbols() async { assert(_symbols != null); - final List symbolIds = List.from(_symbols.keys); - _removeSymbols(symbolIds); + await MapLibreGlPlatform.getInstance(_id).removeLines(_symbols.keys); + _symbols.clear(); notifyListeners(); } @@ -466,8 +479,24 @@ class MaplibreMapController extends ChangeNotifier { return line; } + /// Adds multiple lines to the map, configured using the specified custom [options]. + /// + /// Change listeners are notified once the lines have been added on the + /// platform side. + /// + /// The returned [Future] completes with the added line once listeners have + /// been notified. + Future> addLines(List options, + [List data]) async { + final lines = + await MapLibreGlPlatform.getInstance(_id).addLines(options, data); + lines.forEach((l) => _lines[l.id] = l); + notifyListeners(); + return lines; + } + /// Updates the specified [line] with the given [changes]. The line must - /// be a current member of the [lines] set. + /// be a current member of the [lines] set.‚ /// /// Change listeners are notified once the line has been updated on the /// platform side. @@ -504,7 +533,25 @@ class MaplibreMapController extends ChangeNotifier { Future removeLine(Line line) async { assert(line != null); assert(_lines[line.id] == line); - await _removeLine(line.id); + + await MapLibreGlPlatform.getInstance(_id).removeLine(line.id); + _lines.remove(line.id); + notifyListeners(); + } + + /// Removes the specified [lines] from the map. The lines must be current + /// members of the [lines] set. + /// + /// Change listeners are notified once the lines have been removed on the + /// platform side. + /// + /// The returned [Future] completes once listeners have been notified. + Future removeLines(Iterable lines) async { + final ids = lines.where((l) => _lines[l.id] == l).map((l) => l.id); + assert(lines.length == ids.length); + + await MapLibreGlPlatform.getInstance(_id).removeLines(ids); + ids.forEach((id) => _lines.remove(id)); notifyListeners(); } @@ -517,22 +564,11 @@ class MaplibreMapController extends ChangeNotifier { Future clearLines() async { assert(_lines != null); final List lineIds = List.from(_lines.keys); - for (String id in lineIds) { - await _removeLine(id); - } + await MapLibreGlPlatform.getInstance(_id).removeLines(lineIds); + _lines.clear(); notifyListeners(); } - /// Helper method to remove a single line from the map. Consumed by - /// [removeLine] and [clearLines]. - /// - /// The returned [Future] completes once the line has been removed from - /// [_lines]. - Future _removeLine(String id) async { - await MapLibreGlPlatform.getInstance(_id).removeLine(id); - _lines.remove(id); - } - /// Adds a circle to the map, configured using the specified custom [options]. /// /// Change listeners are notified once the circle has been added on the @@ -550,6 +586,23 @@ class MaplibreMapController extends ChangeNotifier { return circle; } + /// Adds multiple circles to the map, configured using the specified custom + /// [options]. + /// + /// Change listeners are notified once the circles have been added on the + /// platform side. + /// + /// The returned [Future] completes with the added circle once listeners have + /// been notified. + Future> addCircles(List options, + [List data]) async { + final circles = + await MapLibreGlPlatform.getInstance(_id).addCircles(options, data); + circles.forEach((c) => _circles[c.id] = c); + notifyListeners(); + return circles; + } + /// Updates the specified [circle] with the given [changes]. The circle must /// be a current member of the [circles] set. /// @@ -588,34 +641,42 @@ class MaplibreMapController extends ChangeNotifier { Future removeCircle(Circle circle) async { assert(circle != null); assert(_circles[circle.id] == circle); - await _removeCircle(circle.id); + + await MapLibreGlPlatform.getInstance(_id).removeCircle(circle.id); + _circles.remove(circle.id); + notifyListeners(); } - /// Removes all [circles] from the map. + /// Removes the specified [circles] from the map. The circles must be current + /// members of the [circles] set. /// - /// Change listeners are notified once all circles have been removed on the + /// Change listeners are notified once the circles have been removed on the /// platform side. /// /// The returned [Future] completes once listeners have been notified. - Future clearCircles() async { - assert(_circles != null); - final List circleIds = List.from(_circles.keys); - for (String id in circleIds) { - await _removeCircle(id); - } + Future removeCircles(Iterable circles) async { + assert(circles != null); + final ids = circles.where((c) => _circles[c.id] == c).map((c) => c.id); + assert(circles.length == ids.length); + + await MapLibreGlPlatform.getInstance(_id).removeCircles(ids); + ids.forEach((id) => _circles.remove(id)); notifyListeners(); } - /// Helper method to remove a single circle from the map. Consumed by - /// [removeCircle] and [clearCircles]. + /// Removes all [circles] from the map. /// - /// The returned [Future] completes once the circle has been removed from - /// [_circles]. - Future _removeCircle(String id) async { - await MapLibreGlPlatform.getInstance(_id).removeCircle(id); + /// Change listeners are notified once all circles have been removed on the + /// platform side. + /// + /// The returned [Future] completes once listeners have been notified. + Future clearCircles() async { + assert(_circles != null); + await MapLibreGlPlatform.getInstance(_id).removeCircles(_circles.keys); + _circles.clear(); - _circles.remove(id); + notifyListeners(); } /// Adds a fill to the map, configured using the specified custom [options]. @@ -635,6 +696,23 @@ class MaplibreMapController extends ChangeNotifier { return fill; } + /// Adds multiple fills to the map, configured using the specified custom + /// [options]. + /// + /// Change listeners are notified once the fills has been added on the + /// platform side. + /// + /// The returned [Future] completes with the added fills once listeners have + /// been notified. + Future> addFills(List options, + [List data]) async { + final circles = + await MapLibreGlPlatform.getInstance(_id).addFills(options, data); + circles.forEach((f) => _fills[f.id] = f); + notifyListeners(); + return circles; + } + /// Updates the specified [fill] with the given [changes]. The fill must /// be a current member of the [fills] set. /// @@ -659,10 +737,9 @@ class MaplibreMapController extends ChangeNotifier { /// The returned [Future] completes once listeners have been notified. Future clearFills() async { assert(_fills != null); - final List fillIds = List.from(_fills.keys); - for (String id in fillIds) { - await _removeFill(id); - } + await MapLibreGlPlatform.getInstance(_id).removeFills(_fills.keys); + _fills.clear(); + notifyListeners(); } @@ -676,19 +753,26 @@ class MaplibreMapController extends ChangeNotifier { Future removeFill(Fill fill) async { assert(fill != null); assert(_fills[fill.id] == fill); - await _removeFill(fill.id); + await MapLibreGlPlatform.getInstance(_id).removeFill(fill.id); + _fills.remove(fill.id); + notifyListeners(); } - /// Helper method to remove a single fill from the map. Consumed by - /// [removeFill] and [clearFills]. + /// Removes the specified [fills] from the map. The fills must be current + /// members of the [fills] set. + /// + /// Change listeners are notified once the fills have been removed on the + /// platform side. /// - /// The returned [Future] completes once the fill has been removed from - /// [_fills]. - Future _removeFill(String id) async { - await MapLibreGlPlatform.getInstance(_id).removeFill(id); + /// The returned [Future] completes once listeners have been notified. + Future removeFills(Iterable fills) async { + final ids = fills.where((f) => _fills[f.id] == f).map((f) => f.id); + assert(fills.length == ids.length); - _fills.remove(id); + await MapLibreGlPlatform.getInstance(_id).removeFills(ids); + ids.forEach((id) => _fills.remove(id)); + notifyListeners(); } Future queryRenderedFeatures( @@ -820,6 +904,10 @@ class MaplibreMapController extends ChangeNotifier { return MapLibreGlPlatform.getInstance(_id).toScreenLocation(latLng); } + Future> toScreenLocationBatch(Iterable latLngs) async { + return MapLibreGlPlatform.getInstance(_id).toScreenLocationBatch(latLngs); + } + /// Returns the geographic location (as [LatLng]) that corresponds to a point on the screen. The screen location is specified in screen pixels (not display pixels) relative to the top left of the map (not the top left of the whole screen). Future toLatLng(Point screenLocation) async { return MapLibreGlPlatform.getInstance(_id).toLatLng(screenLocation); diff --git a/lib/src/global.dart b/lib/src/global.dart index b2d518bb..48098d8a 100644 --- a/lib/src/global.dart +++ b/lib/src/global.dart @@ -18,6 +18,18 @@ Future installOfflineMapTiles(String tilesDb) async { ); } +Future setOffline( + bool offline, { + String accessToken, +}) => + _globalChannel.invokeMethod( + 'setOffline', + { + 'offline': offline, + 'accessToken': accessToken, + }, + ); + Future> mergeOfflineRegions( String path, { String accessToken, diff --git a/lib/src/mapbox_map.dart b/lib/src/mapbox_map.dart index 421d896f..f8c7ae7a 100644 --- a/lib/src/mapbox_map.dart +++ b/lib/src/mapbox_map.dart @@ -45,15 +45,27 @@ class MaplibreMap extends StatefulWidget { AnnotationType.circle, AnnotationType.fill, ], + this.annotationConsumeTapEvents = const [ + AnnotationType.symbol, + AnnotationType.fill, + AnnotationType.line, + AnnotationType.circle, + ], }) : assert(initialCameraPosition != null), assert(annotationOrder != null), assert(annotationOrder.length == 4), + assert(annotationConsumeTapEvents != null), + assert(annotationConsumeTapEvents.length > 0), super(key: key); /// Defined the layer order of annotations displayed on map /// (must contain all annotation types, 4 items) final List annotationOrder; + /// Defined the layer order of click annotations + /// (must contain at least 1 annotation type, 4 items max) + final List annotationConsumeTapEvents; + /// If you want to use Mapbox hosted styles and map tiles, you need to provide a Mapbox access token. /// Obtain a free access token on [your Mapbox account page](https://www.mapbox.com/account/access-tokens/). /// The reccommended way is to use this parameter to set your access token, an alternative way to add your access tokens through external files is described in the plugin's wiki on Github. @@ -197,13 +209,17 @@ class _MaplibreMapState extends State { @override Widget build(BuildContext context) { - final List annotationOrder = widget.annotationOrder.map((e) => e.toString()).toList(); + final List annotationOrder = + widget.annotationOrder.map((e) => e.toString()).toList(); + final List annotationConsumeTapEvents = + widget.annotationConsumeTapEvents.map((e) => e.toString()).toList(); final Map creationParams = { 'initialCameraPosition': widget.initialCameraPosition?.toMap(), 'options': _MapboxMapOptions.fromWidget(widget).toMap(), 'accessToken': widget.accessToken, 'annotationOrder': annotationOrder, + 'annotationConsumeTapEvents': annotationConsumeTapEvents, }; return _mapboxGlPlatform.buildView( creationParams, onPlatformViewCreated, widget.gestureRecognizers); @@ -236,12 +252,10 @@ class _MaplibreMapState extends State { Future onPlatformViewCreated(int id) async { MapLibreGlPlatform.addInstance(id, _mapboxGlPlatform); final MaplibreMapController controller = MaplibreMapController.init( - id, - widget.initialCameraPosition, - onStyleLoadedCallback: () { - if (_controller.isCompleted) { - widget.onStyleLoadedCallback(); - } else { + id, widget.initialCameraPosition, onStyleLoadedCallback: () { + if (_controller.isCompleted) { + widget.onStyleLoadedCallback(); + } else { _controller.future.then((_) => widget.onStyleLoadedCallback()); } }, @@ -251,8 +265,7 @@ class _MaplibreMapState extends State { onCameraTrackingDismissed: widget.onCameraTrackingDismissed, onCameraTrackingChanged: widget.onCameraTrackingChanged, onCameraIdle: widget.onCameraIdle, - onMapIdle: widget.onMapIdle - ); + onMapIdle: widget.onMapIdle); await MaplibreMapController.initPlatform(id); _controller.complete(controller); if (widget.onMapCreated != null) { diff --git a/maplibre_gl_platform_interface/CHANGELOG.md b/maplibre_gl_platform_interface/CHANGELOG.md index 2ca1199d..769d5719 100644 --- a/maplibre_gl_platform_interface/CHANGELOG.md +++ b/maplibre_gl_platform_interface/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.12.0, Oct 5, 2021 +### Changes cherry-picked/ported from tobrun/flutter-mapbox-gl:0.12.0 +* Batch creation/removal for circles, fills and lines [#576](https://github.com/tobrun/flutter-mapbox-gl/pull/576) + +## Below is the original changelog of the tobrun/flutter-mapbox-gl project, before the fork. + +## 0.11.0, March 30, 2020 +* Add batch mode of screen locations [#554](https://github.com/tobrun/flutter-mapbox-gl/pull/554) + ## 0.10.0, February 12, 2020 * Added web support for fills [#501](https://github.com/tobrun/flutter-mapbox-gl/pull/501) * Add heading to UserLocation and expose UserLocation type [#522](https://github.com/tobrun/flutter-mapbox-gl/pull/522) diff --git a/maplibre_gl_platform_interface/lib/src/mapbox_gl_platform_interface.dart b/maplibre_gl_platform_interface/lib/src/mapbox_gl_platform_interface.dart index e5d7edb9..60f68327 100644 --- a/maplibre_gl_platform_interface/lib/src/mapbox_gl_platform_interface.dart +++ b/maplibre_gl_platform_interface/lib/src/mapbox_gl_platform_interface.dart @@ -63,8 +63,9 @@ abstract class MapLibreGlPlatform { ArgumentCallbacks(); final ArgumentCallbacks onMapIdlePlatform = ArgumentCallbacks(); - - final ArgumentCallbacks onUserLocationUpdatedPlatform = ArgumentCallbacks(); + + final ArgumentCallbacks onUserLocationUpdatedPlatform = + ArgumentCallbacks(); Future initPlatform(int id) async { throw UnimplementedError('initPlatform() has not been implemented.'); @@ -116,8 +117,9 @@ abstract class MapLibreGlPlatform { Future getTelemetryEnabled() async { throw UnimplementedError('getTelemetryEnabled() has not been implemented.'); } - - Future> addSymbols(List options, [List data]) async { + + Future> addSymbols(List options, + [List data]) async { throw UnimplementedError('addSymbols() has not been implemented.'); } @@ -133,6 +135,11 @@ abstract class MapLibreGlPlatform { throw UnimplementedError('addLine() has not been implemented.'); } + Future> addLines(List options, + [List data]) async { + throw UnimplementedError('addLines() has not been implemented.'); + } + Future updateLine(Line line, LineOptions changes) async { throw UnimplementedError('updateLine() has not been implemented.'); } @@ -141,10 +148,19 @@ abstract class MapLibreGlPlatform { throw UnimplementedError('removeLine() has not been implemented.'); } + Future removeLines(Iterable ids) async { + throw UnimplementedError('removeLines() has not been implemented.'); + } + Future addCircle(CircleOptions options, [Map data]) async { throw UnimplementedError('addCircle() has not been implemented.'); } + Future> addCircles(List options, + [List data]) async { + throw UnimplementedError('addCircles() has not been implemented.'); + } + Future updateCircle(Circle circle, CircleOptions changes) async { throw UnimplementedError('updateCircle() has not been implemented.'); } @@ -165,11 +181,20 @@ abstract class MapLibreGlPlatform { throw UnimplementedError('removeCircle() has not been implemented.'); } + Future removeCircles(Iterable ids) async { + throw UnimplementedError('removeCircles() has not been implemented.'); + } + Future addFill(FillOptions options, [Map data]) async { throw UnimplementedError('addFill() has not been implemented.'); } - FutureupdateFill(Fill fill, FillOptions changes) async { + Future> addFills(List options, + [List data]) async { + throw UnimplementedError('addFills() has not been implemented.'); + } + + Future updateFill(Fill fill, FillOptions changes) async { throw UnimplementedError('updateFill() has not been implemented.'); } @@ -177,6 +202,10 @@ abstract class MapLibreGlPlatform { throw UnimplementedError('removeFill() has not been implemented.'); } + Future removeFills(Iterable fillIds) async { + throw UnimplementedError('removeFills() has not been implemented.'); + } + Future queryRenderedFeatures( Point point, List layerIds, List filter) async { throw UnimplementedError( @@ -228,8 +257,8 @@ abstract class MapLibreGlPlatform { 'setSymbolTextIgnorePlacement() has not been implemented.'); } - Future addImageSource(String imageSourceId, Uint8List bytes, - LatLngQuad coordinates) async { + Future addImageSource( + String imageSourceId, Uint8List bytes, LatLngQuad coordinates) async { throw UnimplementedError('addImageSource() has not been implemented.'); } @@ -241,7 +270,8 @@ abstract class MapLibreGlPlatform { throw UnimplementedError('addLayer() has not been implemented.'); } - Future addLayerBelow(String imageLayerId, String imageSourceId, String belowLayerId) async { + Future addLayerBelow( + String imageLayerId, String imageSourceId, String belowLayerId) async { throw UnimplementedError('addLayerBelow() has not been implemented.'); } @@ -249,9 +279,13 @@ abstract class MapLibreGlPlatform { throw UnimplementedError('removeLayer() has not been implemented.'); } - Future toScreenLocation(LatLng latLng) async{ + Future toScreenLocation(LatLng latLng) async { + throw UnimplementedError('toScreenLocation() has not been implemented.'); + } + + Future> toScreenLocationBatch(Iterable latLngs) async{ throw UnimplementedError( - 'toScreenLocation() has not been implemented.'); + 'toScreenLocationList() has not been implemented.'); } Future toLatLng(Point screenLocation) async{ @@ -259,7 +293,7 @@ abstract class MapLibreGlPlatform { 'toLatLng() has not been implemented.'); } - Future getMetersPerPixelAtLatitude(double latitude) async{ + Future getMetersPerPixelAtLatitude(double latitude) async { throw UnimplementedError( 'getMetersPerPixelAtLatitude() has not been implemented.'); } diff --git a/maplibre_gl_platform_interface/lib/src/method_channel_mapbox_gl.dart b/maplibre_gl_platform_interface/lib/src/method_channel_mapbox_gl.dart index d147c9de..177d9ae4 100644 --- a/maplibre_gl_platform_interface/lib/src/method_channel_mapbox_gl.dart +++ b/maplibre_gl_platform_interface/lib/src/method_channel_mapbox_gl.dart @@ -83,7 +83,8 @@ class MethodChannelMaplibreGl extends MapLibreGlPlatform { final dynamic heading = call.arguments['heading']; if (onUserLocationUpdatedPlatform != null) { onUserLocationUpdatedPlatform(UserLocation( - position: LatLng(userLocation['position'][0], userLocation['position'][1]), + position: LatLng( + userLocation['position'][0], userLocation['position'][1]), altitude: userLocation['altitude'], bearing: userLocation['bearing'], speed: userLocation['speed'], @@ -259,7 +260,7 @@ class MethodChannelMaplibreGl extends MapLibreGlPlatform { @override Future removeSymbols(Iterable ids) async { await _channel.invokeMethod('symbols#removeAll', { - 'symbols': ids.toList(), + 'ids': ids.toList(), }); } @@ -274,6 +275,27 @@ class MethodChannelMaplibreGl extends MapLibreGlPlatform { return Line(lineId, options, data); } + @override + Future> addLines(List options, + [List data]) async { + final List ids = await _channel.invokeMethod( + 'line#addAll', + { + 'options': options.map((o) => o.toJson()).toList(), + }, + ); + final List lines = ids + .asMap() + .map((i, id) => MapEntry( + i, + Line(id, options.elementAt(i), + data != null && data.length > i ? data.elementAt(i) : null))) + .values + .toList(); + + return lines; + } + @override Future updateLine(Line line, LineOptions changes) async { await _channel.invokeMethod('line#update', { @@ -302,6 +324,13 @@ class MethodChannelMaplibreGl extends MapLibreGlPlatform { }); } + @override + Future removeLines(Iterable ids) async { + await _channel.invokeMethod('line#removeAll', { + 'ids': ids.toList(), + }); + } + @override Future addCircle(CircleOptions options, [Map data]) async { final String circleId = await _channel.invokeMethod( @@ -313,6 +342,25 @@ class MethodChannelMaplibreGl extends MapLibreGlPlatform { return Circle(circleId, options, data); } + @override + Future> addCircles(List options, + [List data]) async { + final List ids = await _channel.invokeMethod( + 'circle#addAll', + { + 'options': options.map((o) => o.toJson()).toList(), + }, + ); + return ids + .asMap() + .map((i, id) => MapEntry( + i, + Circle(id, options.elementAt(i), + data != null && data.length > i ? data.elementAt(i) : null))) + .values + .toList(); + } + @override Future updateCircle(Circle circle, CircleOptions changes) async { await _channel.invokeMethod('circle#update', { @@ -337,6 +385,13 @@ class MethodChannelMaplibreGl extends MapLibreGlPlatform { }); } + @override + Future removeCircles(Iterable ids) async { + await _channel.invokeMethod('circle#removeAll', { + 'ids': ids.toList(), + }); + } + @override Future addFill(FillOptions options, [Map data]) async { final String fillId = await _channel.invokeMethod( @@ -348,6 +403,27 @@ class MethodChannelMaplibreGl extends MapLibreGlPlatform { return Fill(fillId, options, data); } + @override + Future> addFills(List options, + [List data]) async { + final List ids = await _channel.invokeMethod( + 'fill#addAll', + { + 'options': options.map((o) => o.toJson()).toList(), + }, + ); + final List fills = ids + .asMap() + .map((i, id) => MapEntry( + i, + Fill(id, options.elementAt(i), + data != null && data.length > i ? data.elementAt(i) : null))) + .values + .toList(); + + return fills; + } + @override Future updateFill(Fill fill, FillOptions changes) async { await _channel.invokeMethod('fill#update', { @@ -363,6 +439,13 @@ class MethodChannelMaplibreGl extends MapLibreGlPlatform { }); } + @override + Future removeFills(Iterable ids) async { + await _channel.invokeMethod('fill#removeAll', { + 'ids': ids.toList(), + }); + } + @override Future queryRenderedFeatures( Point point, List layerIds, List filter) async { @@ -515,10 +598,11 @@ class MethodChannelMaplibreGl extends MapLibreGlPlatform { } @override - Future addImageSource(String imageSourceId, Uint8List bytes, - LatLngQuad coordinates) async { + Future addImageSource( + String imageSourceId, Uint8List bytes, LatLngQuad coordinates) async { try { - return await _channel.invokeMethod('style#addImageSource', { + return await _channel + .invokeMethod('style#addImageSource', { 'imageSourceId': imageSourceId, 'bytes': bytes, 'length': bytes.length, @@ -532,10 +616,10 @@ class MethodChannelMaplibreGl extends MapLibreGlPlatform { @override Future toScreenLocation(LatLng latLng) async { try { - var screenPosMap = await _channel - .invokeMethod('map#toScreenLocation', { + var screenPosMap = + await _channel.invokeMethod('map#toScreenLocation', { 'latitude': latLng.latitude, - 'longitude':latLng.longitude, + 'longitude': latLng.longitude, }); return Point(screenPosMap['x'], screenPosMap['y']); } on PlatformException catch (e) { @@ -543,17 +627,35 @@ class MethodChannelMaplibreGl extends MapLibreGlPlatform { } } + @override + Future> toScreenLocationBatch(Iterable latLngs) async { + try { + var coordinates = Float64List.fromList( + latLngs.map((e) => [e.latitude, e.longitude]).expand((e) => e).toList()); + Float64List result = await _channel + .invokeMethod('map#toScreenLocationBatch', {"coordinates": coordinates}); + + var points = []; + for (int i = 0; i < result.length; i += 2) { + points.add(Point(result[i], result[i + 1])); + } + + return points; + } on PlatformException catch (e) { + return new Future.error(e); + } + } + @override Future removeImageSource(String imageSourceId) async { try { - return await _channel.invokeMethod('style#removeImageSource', { - 'imageSourceId': imageSourceId - }); + return await _channel.invokeMethod('style#removeImageSource', + {'imageSourceId': imageSourceId}); } on PlatformException catch (e) { return new Future.error(e); } } - + @override Future addLayer(String imageLayerId, String imageSourceId) async { try { @@ -567,9 +669,11 @@ class MethodChannelMaplibreGl extends MapLibreGlPlatform { } @override - Future addLayerBelow(String imageLayerId, String imageSourceId, String belowLayerId) async { + Future addLayerBelow( + String imageLayerId, String imageSourceId, String belowLayerId) async { try { - return await _channel.invokeMethod('style#addLayerBelow', { + return await _channel + .invokeMethod('style#addLayerBelow', { 'imageLayerId': imageLayerId, 'imageSourceId': imageSourceId, 'belowLayerId': belowLayerId @@ -578,25 +682,24 @@ class MethodChannelMaplibreGl extends MapLibreGlPlatform { return new Future.error(e); } } - + @override Future removeLayer(String imageLayerId) async { try { - return await _channel.invokeMethod('style#removeLayer', { - 'imageLayerId': imageLayerId - }); + return await _channel.invokeMethod( + 'style#removeLayer', {'imageLayerId': imageLayerId}); } on PlatformException catch (e) { return new Future.error(e); } } - + @override Future toLatLng(Point screenLocation) async { try { - var latLngMap = await _channel - .invokeMethod('map#toLatLng', { + var latLngMap = + await _channel.invokeMethod('map#toLatLng', { 'x': screenLocation.x, - 'y':screenLocation.y, + 'y': screenLocation.y, }); return LatLng(latLngMap['latitude'], latLngMap['longitude']); } on PlatformException catch (e) { @@ -605,7 +708,7 @@ class MethodChannelMaplibreGl extends MapLibreGlPlatform { } @override - Future getMetersPerPixelAtLatitude(double latitude) async{ + Future getMetersPerPixelAtLatitude(double latitude) async { try { var latLngMap = await _channel .invokeMethod('map#getMetersPerPixelAtLatitude', { @@ -616,5 +719,4 @@ class MethodChannelMaplibreGl extends MapLibreGlPlatform { return new Future.error(e); } } - } diff --git a/maplibre_gl_web/CHANGELOG.md b/maplibre_gl_web/CHANGELOG.md index cef00ad2..910fd068 100644 --- a/maplibre_gl_web/CHANGELOG.md +++ b/maplibre_gl_web/CHANGELOG.md @@ -1,3 +1,16 @@ +## 0.12.0, Oct 5, 2021 +### Changes cherry-picked/ported from tobrun/flutter-mapbox-gl:0.12.0 +* Dependencies: updated image package [#598](https://github.com/tobrun/flutter-mapbox-gl/pull/598) +* Fix feature manager on release build [#593](https://github.com/tobrun/flutter-mapbox-gl/pull/593) +* Emit onTap only for the feature above the others [#589](https://github.com/tobrun/flutter-mapbox-gl/pull/589) +* Add annotationOrder to web [#588](https://github.com/tobrun/flutter-mapbox-gl/pull/588) + +## Below is the original changelog of the tobrun/flutter-mapbox-gl project, before the fork. + +## 0.11.0, March 30, 2020 +* Fix Mapbox GL JS CSS embedding on web [#551](https://github.com/tobrun/flutter-mapbox-gl/pull/551) +* Add batch mode of screen locations [#554](https://github.com/tobrun/flutter-mapbox-gl/pull/554) + ## 0.10.0, February 12, 2020 * Added web support for fills [#501](https://github.com/tobrun/flutter-mapbox-gl/pull/501) * Add heading to UserLocation and expose UserLocation type [#522](https://github.com/tobrun/flutter-mapbox-gl/pull/522) diff --git a/maplibre_gl_web/lib/src/feature_manager/feature_manager.dart b/maplibre_gl_web/lib/src/feature_manager/feature_manager.dart index 6e68b798..76621bf6 100644 --- a/maplibre_gl_web/lib/src/feature_manager/feature_manager.dart +++ b/maplibre_gl_web/lib/src/feature_manager/feature_manager.dart @@ -64,9 +64,14 @@ abstract class FeatureManager { } void _initClickHandler() { - map.on('click', layerId, (e) { - if (onTap != null) { - onTap('${e.features[0].id}'); + map.on('click', (e) { + if (e is Event) { + final features = map.queryRenderedFeatures([e.point.x, e.point.y]); + if (features.length > 0 && features[0].source == sourceId) { + if (onTap != null) { + onTap('${features[0].id}'); + } + } } }); diff --git a/maplibre_gl_web/lib/src/mapbox_map_controller.dart b/maplibre_gl_web/lib/src/mapbox_map_controller.dart index 1997c493..1cadd5d7 100644 --- a/maplibre_gl_web/lib/src/mapbox_map_controller.dart +++ b/maplibre_gl_web/lib/src/mapbox_map_controller.dart @@ -1,15 +1,16 @@ part of maplibre_gl_web; +//TODO Url taken from the Maptiler tutorial; use official and stable release once available +final _maplibreGlCssUrl = 'https://cdn.maptiler.com/maplibre-gl-js/v1.13.0-rc.4/mapbox-gl.css'; + class MaplibreMapController extends MapLibreGlPlatform implements MapboxMapOptionsSink { DivElement _mapElement; - //TODO Url taken from the Maptiler tutorial; use official and stable release once available - final _maplibreGlCssUrl = 'https://cdn.maptiler.com/maplibre-gl-js/v1.13.0-rc.4/mapbox-gl.css'; - Map _creationParams; MapboxMap _map; + List annotationOrder = []; SymbolManager symbolManager; LineManager lineManager; CircleManager circleManager; @@ -64,6 +65,10 @@ class MaplibreMapController extends MapLibreGlPlatform _map.on('load', _onStyleLoaded); } Convert.interpretMapboxMapOptions(_creationParams['options'], this); + + if (_creationParams.containsKey('annotationOrder')) { + annotationOrder = _creationParams['annotationOrder']; + } } Future _addStylesheetToShadowRoot(HtmlElement e) async { @@ -75,8 +80,7 @@ class MaplibreMapController extends MapLibreGlPlatform await link.onLoad.first; } - - @override + @override Future updateMapOptions( Map optionsUpdate) async { // FIX: why is called indefinitely? (map_ui page) @@ -344,10 +348,28 @@ class MaplibreMapController extends MapLibreGlPlatform } void _onStyleLoaded(_) { - symbolManager = SymbolManager(map: _map, onTap: onSymbolTappedPlatform); - lineManager = LineManager(map: _map, onTap: onLineTappedPlatform); - circleManager = CircleManager(map: _map, onTap: onCircleTappedPlatform); - fillManager = FillManager(map: _map, onTap: onFillTappedPlatform); + for (final annotationType in annotationOrder) { + switch (annotationType) { + case 'AnnotationType.symbol': + symbolManager = + SymbolManager(map: _map, onTap: onSymbolTappedPlatform); + break; + case 'AnnotationType.line': + lineManager = LineManager(map: _map, onTap: onLineTappedPlatform); + break; + case 'AnnotationType.circle': + circleManager = + CircleManager(map: _map, onTap: onCircleTappedPlatform); + break; + case 'AnnotationType.fill': + fillManager = FillManager(map: _map, onTap: onFillTappedPlatform); + break; + default: + print( + "Unknown annotation type: \(annotationType), must be either 'fill', 'line', 'circle' or 'symbol'"); + } + } + onMapStyleLoadedPlatform(null); _map.on('click', _onMapClick); // long click not available in web, so it is mapped to double click @@ -434,7 +456,15 @@ class MaplibreMapController extends MapLibreGlPlatform ); _geolocateControl.on('geolocate', (e) { _myLastLocation = LatLng(e.coords.latitude, e.coords.longitude); - onUserLocationUpdatedPlatform(UserLocation(position: LatLng(e.coords.latitude, e.coords.longitude), altitude: e.coords.altitude, bearing: e.coords.heading, speed: e.coords.speed, horizontalAccuracy: e.coords.accuracy, verticalAccuracy: e.coords.altitudeAccuracy, heading: null, timestamp: DateTime.fromMillisecondsSinceEpoch(e.timestamp))); + onUserLocationUpdatedPlatform(UserLocation( + position: LatLng(e.coords.latitude, e.coords.longitude), + altitude: e.coords.altitude, + bearing: e.coords.heading, + speed: e.coords.speed, + horizontalAccuracy: e.coords.accuracy, + verticalAccuracy: e.coords.altitudeAccuracy, + heading: null, + timestamp: DateTime.fromMillisecondsSinceEpoch(e.timestamp))); }); _geolocateControl.on('trackuserlocationstart', (_) { _onCameraTrackingChanged(true); @@ -658,6 +688,15 @@ class MaplibreMapController extends MapLibreGlPlatform return Point(screenPosition.x.round(), screenPosition.y.round()); } + @override + Future> toScreenLocationBatch(Iterable latLngs) async { + return latLngs.map((latLng) { + var screenPosition = + _map.project(LngLat(latLng.longitude, latLng.latitude)); + return Point(screenPosition.x.round(), screenPosition.y.round()); + }).toList(growable: false); + } + @override Future toLatLng(Point screenLocation) async { var lngLat = diff --git a/maplibre_gl_web/pubspec.yaml b/maplibre_gl_web/pubspec.yaml index 608b388b..5ba1f27a 100644 --- a/maplibre_gl_web/pubspec.yaml +++ b/maplibre_gl_web/pubspec.yaml @@ -1,6 +1,6 @@ name: maplibre_gl_web description: Web platform implementation of maplibre_gl -version: 0.10.0 +version: 0.12.0 homepage: https://github.com/m0nac0/flutter-maplibre-gl flutter: @@ -22,7 +22,7 @@ dependencies: path: maplibre_gl_platform_interface ref: main mapbox_gl_dart: ^0.1.5 - image: ^3.0.1 + image: ^3.0.2 dependency_overrides: maplibre_gl_platform_interface: diff --git a/pubspec.lock b/pubspec.lock index f3f5230d..f59c48c7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -73,14 +73,14 @@ packages: path: maplibre_gl_web relative: true source: path - version: "0.10.0" + version: "0.12.0" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" path: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ab99c82b..75757f8e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: maplibre_gl description: A Flutter plugin for integrating Maplibre Maps inside a Flutter application on Android, iOS and web platforms. -version: 0.10.0 +version: 0.12.0 homepage: https://github.com/m0nac0/flutter-maplibre-gl dependencies: