From 8246a842d845de89008d285e5d97649878eb39d4 Mon Sep 17 00:00:00 2001 From: Louis <47295080+LouisRaverdy@users.noreply.github.com> Date: Mon, 2 Dec 2024 23:14:43 +0100 Subject: [PATCH] [FEATURE] Returns layer Id on Feature Tap & update NDK version to fix crash on Android 15 (#475) Hello MapLibre Team ! I needed to retrieve the layer Id of the feature tapped, so I've implemented it. This is much cleaner than using the given Id to have a unique identifier and the layer id. Best regards, Louis --------- Co-authored-by: Joscha <34318751+josxha@users.noreply.github.com> --- example/lib/layer.dart | 5 +- maplibre_gl/android/build.gradle | 8 +- .../maplibregl/MapLibreMapController.java | 20 ++-- .../maplibre_gl/MapLibreMapController.swift | 91 ++++++++++--------- maplibre_gl/lib/src/annotation_manager.dart | 3 +- maplibre_gl/lib/src/controller.dart | 5 +- .../lib/src/method_channel_maplibre_gl.dart | 4 +- 7 files changed, 73 insertions(+), 63 deletions(-) diff --git a/example/lib/layer.dart b/example/lib/layer.dart index 7a03fb47..c2cbac78 100644 --- a/example/lib/layer.dart +++ b/example/lib/layer.dart @@ -145,10 +145,11 @@ class LayerState extends State { controller.onFeatureTapped.add(onFeatureTap); } - void onFeatureTap(dynamic featureId, Point point, LatLng latLng) { + void onFeatureTap( + dynamic featureId, Point point, LatLng latLng, String layerId) { final snackBar = SnackBar( content: Text( - 'Tapped feature with id $featureId', + 'Tapped feature with id $featureId on layer $layerId', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), backgroundColor: Theme.of(context).primaryColor, diff --git a/maplibre_gl/android/build.gradle b/maplibre_gl/android/build.gradle index c10fbb9b..c6e8cb05 100644 --- a/maplibre_gl/android/build.gradle +++ b/maplibre_gl/android/build.gradle @@ -30,7 +30,7 @@ android { } compileSdkVersion 34 - ndkVersion "26.1.10909125" + ndkVersion "27.0.12077973" defaultConfig { minSdkVersion 21 @@ -48,9 +48,9 @@ android { jvmTarget = JavaVersion.VERSION_1_8 } dependencies { - implementation 'org.maplibre.gl:android-sdk:11.0.0' - implementation 'org.maplibre.gl:android-plugin-annotation-v9:3.0.0' - implementation 'org.maplibre.gl:android-plugin-offline-v9:3.0.0' + implementation 'org.maplibre.gl:android-sdk:11.6.1' + implementation 'org.maplibre.gl:android-plugin-annotation-v9:3.0.2' + implementation 'org.maplibre.gl:android-plugin-offline-v9:3.0.2' implementation 'com.squareup.okhttp3:okhttp:4.12.0' } } diff --git a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapController.java b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapController.java index 33568ebb..c856ed80 100644 --- a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapController.java +++ b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapController.java @@ -22,6 +22,7 @@ import android.view.TextureView; import android.view.View; import android.widget.FrameLayout; +import android.util.Pair; import androidx.annotation.NonNull; import androidx.lifecycle.DefaultLifecycleObserver; @@ -31,8 +32,8 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonParser; -import com.mapbox.android.gestures.AndroidGesturesManager; -import com.mapbox.android.gestures.MoveGestureDetector; +import org.maplibre.android.gestures.AndroidGesturesManager; +import org.maplibre.android.gestures.MoveGestureDetector; import org.maplibre.geojson.Feature; import org.maplibre.geojson.FeatureCollection; import org.maplibre.android.camera.CameraPosition; @@ -652,7 +653,7 @@ private void addHeatmapLayer( } } - private Feature firstFeatureOnLayers(RectF in) { + private Pair firstFeatureOnLayers(RectF in) { if (style != null) { final List layers = style.getLayers(); final List layersInOrder = new ArrayList(); @@ -665,7 +666,7 @@ private Feature firstFeatureOnLayers(RectF in) { for (String id : layersInOrder) { List features = mapLibreMap.queryRenderedFeatures(in, id); if (!features.isEmpty()) { - return features.get(0); + return new Pair(features.get(0), id); } } } @@ -1677,14 +1678,15 @@ public void onDidBecomeIdle() { public boolean onMapClick(@NonNull LatLng point) { PointF pointf = mapLibreMap.getProjection().toScreenLocation(point); RectF rectF = new RectF(pointf.x - 10, pointf.y - 10, pointf.x + 10, pointf.y + 10); - Feature feature = firstFeatureOnLayers(rectF); + Pair featureLayerPair = firstFeatureOnLayers(rectF); final Map arguments = new HashMap<>(); arguments.put("x", pointf.x); arguments.put("y", pointf.y); arguments.put("lng", point.getLongitude()); arguments.put("lat", point.getLatitude()); - if (feature != null) { - arguments.put("id", feature.id()); + if (featureLayerPair != null && featureLayerPair.first != null) { + arguments.put("layerId", featureLayerPair.second); + arguments.put("id", featureLayerPair.first.id()); methodChannel.invokeMethod("feature#onTap", arguments); } else { methodChannel.invokeMethod("map#onMapClick", arguments); @@ -2155,8 +2157,8 @@ boolean onMoveBegin(MoveGestureDetector detector) { PointF pointf = detector.getFocalPoint(); LatLng origin = mapLibreMap.getProjection().fromScreenLocation(pointf); RectF rectF = new RectF(pointf.x - 10, pointf.y - 10, pointf.x + 10, pointf.y + 10); - Feature feature = firstFeatureOnLayers(rectF); - if (feature != null && startDragging(feature, origin)) { + Pair featureLayerPair = firstFeatureOnLayers(rectF); + if (featureLayerPair != null && featureLayerPair.first != null && startDragging(featureLayerPair.first, origin)) { invokeFeatureDrag(pointf, "start"); return true; } diff --git a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift index 04718664..6bfcf1fc 100644 --- a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift +++ b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift @@ -71,9 +71,9 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, longPress.require(toFail: recognizer) } var longPressRecognizerAdded = false - + if let args = args as? [String: Any] { - + Convert.interpretMapLibreMapOptions(options: args["options"], delegate: self) if let initialCameraPosition = args["initialCameraPosition"] as? [String: Any], let camera = MLNMapCamera.fromDict(initialCameraPosition, mapView: mapView), @@ -179,7 +179,7 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, if let langStr = Locale.current.languageCode { setMapLanguage(language: langStr) } - + result(nil) case "map#updateContentInsets": guard let arguments = methodCall.arguments as? [String: Any] else { return } @@ -325,7 +325,7 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, case "camera#move": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let cameraUpdate = arguments["cameraUpdate"] as? [Any] else { return } - + if let camera = Convert.parseCameraUpdate(cameraUpdate: cameraUpdate, mapView: mapView) { mapView.setCamera(camera, animated: false) } @@ -334,12 +334,12 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let cameraUpdate = arguments["cameraUpdate"] as? [Any] else { return } guard let camera = Convert.parseCameraUpdate(cameraUpdate: cameraUpdate, mapView: mapView) else { return } - - + + let completion = { result(nil) } - + if let duration = arguments["duration"] as? TimeInterval { if let padding = Convert.parseLatLngBoundsPadding(cameraUpdate) { mapView.fly(to: camera, edgePadding: padding, withDuration: duration / 1000, completionHandler: completion) @@ -546,7 +546,7 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, properties: properties ) result(nil) - + case "heatmapLayer#add": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let sourceId = arguments["sourceId"] as? String else { return } @@ -849,11 +849,11 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, } layer.isVisible = visible result(nil) - + case "map#querySourceFeatures": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let sourceId = arguments["sourceId"] as? String else { return } - + var sourceLayerId = Set() if let layerId = arguments["sourceLayerId"] as? String { sourceLayerId.insert(layerId) @@ -862,10 +862,10 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, if let filter = arguments["filter"] as? [Any] { filterExpression = NSPredicate(mglJSONObject: filter) } - + var reply = [String: NSObject]() var features: [MLNFeature] = [] - + guard let style = mapView.style else { return } if let source = style.source(withIdentifier: sourceId) { if let vectorSource = source as? MLNVectorTileSource { @@ -874,7 +874,7 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, features = shapeSource.features(matching: filterExpression) } } - + var featuresJson = [String]() for feature in features { let dictionary = feature.geoJSONDictionary() @@ -892,11 +892,11 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, case "style#getLayerIds": var layerIds = [String]() - + guard let style = mapView.style else { return } - + style.layers.forEach { layer in layerIds.append(layer.identifier) } - + var reply = [String: NSObject]() reply["layers"] = layerIds as NSObject result(reply) @@ -911,18 +911,18 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, var reply = [String: NSObject]() reply["sources"] = sourceIds as NSObject result(reply) - + case "style#getFilter": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let layerId = arguments["layerId"] as? String else { return } - + guard let style = mapView.style else { return } guard let layer = style.layer(withIdentifier: layerId) else { return } - + var currentLayerFilter : String = "" if let vectorLayer = layer as? MLNVectorStyleLayer { if let layerFilter = vectorLayer.predicate { - + let jsonExpression = layerFilter.mgl_jsonExpressionObject if let data = try? JSONSerialization.data(withJSONObject: jsonExpression, options: []) { currentLayerFilter = String(data: data, encoding: String.Encoding.utf8) ?? "" @@ -934,11 +934,11 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, ).flutterError) return; } - + var reply = [String: NSObject]() reply["filter"] = currentLayerFilter as NSObject result(reply) - + default: result(FlutterMethodNotImplemented) } @@ -976,7 +976,7 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, private func getCamera() -> MLNMapCamera? { return trackCameraPosition ? mapView.camera : nil } - + private func setMapLanguage(language: String) { self.mapView.setMapLanguage(language) } @@ -984,8 +984,8 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, /* * Scan layers from top to bottom and return the first matching feature */ - private func firstFeatureOnLayers(at: CGPoint) -> MLNFeature? { - guard let style = mapView.style else { return nil } + private func firstFeatureOnLayers(at: CGPoint) -> (feature: MLNFeature?, layerId: String?) { + guard let style = mapView.style else { return (nil, nil) } // get layers in order (interactiveFeatureLayerIds is unordered) let clickableLayers = style.layers.filter { layer in @@ -998,10 +998,10 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, styleLayerIdentifiers: [layer.identifier] ) if let feature = features.first { - return feature + return (feature, layer.identifier) } } - return nil + return (nil, nil) } /* @@ -1013,13 +1013,15 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, let point = sender.location(in: mapView) let coordinate = mapView.convert(point, toCoordinateFrom: mapView) - if let feature = firstFeatureOnLayers(at: point) { + let result = firstFeatureOnLayers(at: point) + if let feature = result.feature { channel?.invokeMethod("feature#onTap", arguments: [ "id": feature.identifier, "x": point.x, "y": point.y, "lng": coordinate.longitude, "lat": coordinate.latitude, + "layerId": result.layerId, ]) } else { channel?.invokeMethod("map#onMapClick", arguments: [ @@ -1062,22 +1064,23 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, let point = sender.location(in: mapView) let coordinate = mapView.convert(point, toCoordinateFrom: mapView) - if dragFeature == nil, began, sender.numberOfTouches == 1, - let feature = firstFeatureOnLayers(at: point), - let draggable = feature.attribute(forKey: "draggable") as? Bool, - draggable - { - sender.state = UIGestureRecognizer.State.began - dragFeature = feature - originDragCoordinate = coordinate - previousDragCoordinate = coordinate - mapView.allowsScrolling = false - let eventType = "start" - invokeFeatureDrag(point, coordinate, eventType) - for gestureRecognizer in mapView.gestureRecognizers! { - if let _ = gestureRecognizer as? UIPanGestureRecognizer { - gestureRecognizer.addTarget(self, action: #selector(handleMapPan)) - break + if dragFeature == nil, began, sender.numberOfTouches == 1 { + let result = firstFeatureOnLayers(at: point) + if let feature = result.feature, + let draggable = feature.attribute(forKey: "draggable") as? Bool, + draggable { + sender.state = UIGestureRecognizer.State.began + dragFeature = feature + originDragCoordinate = coordinate + previousDragCoordinate = coordinate + mapView.allowsScrolling = false + let eventType = "start" + invokeFeatureDrag(point, coordinate, eventType) + for gestureRecognizer in mapView.gestureRecognizers! { + if let _ = gestureRecognizer as? UIPanGestureRecognizer { + gestureRecognizer.addTarget(self, action: #selector(handleMapPan)) + break + } } } } diff --git a/maplibre_gl/lib/src/annotation_manager.dart b/maplibre_gl/lib/src/annotation_manager.dart index 4c8ac4d9..f4c980e4 100644 --- a/maplibre_gl/lib/src/annotation_manager.dart +++ b/maplibre_gl/lib/src/annotation_manager.dart @@ -55,7 +55,8 @@ abstract class AnnotationManager { } } - _onFeatureTapped(dynamic id, Point point, LatLng coordinates) { + _onFeatureTapped( + dynamic id, Point point, LatLng coordinates, String layerId) { final annotation = _idToAnnotation[id]; if (annotation != null) { onTap!(annotation); diff --git a/maplibre_gl/lib/src/controller.dart b/maplibre_gl/lib/src/controller.dart index 35ce31d5..192a8cff 100644 --- a/maplibre_gl/lib/src/controller.dart +++ b/maplibre_gl/lib/src/controller.dart @@ -8,7 +8,7 @@ typedef OnMapClickCallback = void Function( Point point, LatLng coordinates); typedef OnFeatureInteractionCallback = void Function( - dynamic id, Point point, LatLng coordinates); + dynamic id, Point point, LatLng coordinates, String layerId); typedef OnFeatureDragnCallback = void Function(dynamic id, {required Point point, @@ -93,7 +93,8 @@ class MapLibreMapController extends ChangeNotifier { _maplibrePlatform.onFeatureTappedPlatform.add((payload) { for (final fun in List.from(onFeatureTapped)) { - fun(payload["id"], payload["point"], payload["latLng"]); + fun(payload["id"], payload["point"], payload["latLng"], + payload["layerId"]); } }); diff --git a/maplibre_gl_platform_interface/lib/src/method_channel_maplibre_gl.dart b/maplibre_gl_platform_interface/lib/src/method_channel_maplibre_gl.dart index 78d86412..3272a250 100644 --- a/maplibre_gl_platform_interface/lib/src/method_channel_maplibre_gl.dart +++ b/maplibre_gl_platform_interface/lib/src/method_channel_maplibre_gl.dart @@ -18,10 +18,12 @@ class MapLibreMethodChannel extends MapLibrePlatform { final double y = call.arguments['y']; final double lng = call.arguments['lng']; final double lat = call.arguments['lat']; + final String layerId = call.arguments['layerId']; onFeatureTappedPlatform({ 'id': id, 'point': Point(x, y), - 'latLng': LatLng(lat, lng) + 'latLng': LatLng(lat, lng), + 'layerId': layerId }); case 'feature#onDrag': final id = call.arguments['id'];