Skip to content

Commit

Permalink
[FEATURE] Returns layer Id on Feature Tap & update NDK version to fix…
Browse files Browse the repository at this point in the history
… 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 <[email protected]>
  • Loading branch information
LouisRaverdy and josxha authored Dec 2, 2024
1 parent 3c18004 commit 8246a84
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 63 deletions.
5 changes: 3 additions & 2 deletions example/lib/layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,11 @@ class LayerState extends State {
controller.onFeatureTapped.add(onFeatureTap);
}

void onFeatureTap(dynamic featureId, Point<double> point, LatLng latLng) {
void onFeatureTap(
dynamic featureId, Point<double> 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,
Expand Down
8 changes: 4 additions & 4 deletions maplibre_gl/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ android {
}

compileSdkVersion 34
ndkVersion "26.1.10909125"
ndkVersion "27.0.12077973"

defaultConfig {
minSdkVersion 21
Expand All @@ -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'
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -652,7 +653,7 @@ private void addHeatmapLayer(
}
}

private Feature firstFeatureOnLayers(RectF in) {
private Pair<Feature, String> firstFeatureOnLayers(RectF in) {
if (style != null) {
final List<Layer> layers = style.getLayers();
final List<String> layersInOrder = new ArrayList<String>();
Expand All @@ -665,7 +666,7 @@ private Feature firstFeatureOnLayers(RectF in) {
for (String id : layersInOrder) {
List<Feature> features = mapLibreMap.queryRenderedFeatures(in, id);
if (!features.isEmpty()) {
return features.get(0);
return new Pair<Feature, String>(features.get(0), id);
}
}
}
Expand Down Expand Up @@ -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<Feature, String> featureLayerPair = firstFeatureOnLayers(rectF);
final Map<String, Object> 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);
Expand Down Expand Up @@ -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<Feature, String> featureLayerPair = firstFeatureOnLayers(rectF);
if (featureLayerPair != null && featureLayerPair.first != null && startDragging(featureLayerPair.first, origin)) {
invokeFeatureDrag(pointf, "start");
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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<String>()
if let layerId = arguments["sourceLayerId"] as? String {
sourceLayerId.insert(layerId)
Expand All @@ -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 {
Expand All @@ -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()
Expand All @@ -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)
Expand All @@ -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) ?? ""
Expand All @@ -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)
}
Expand Down Expand Up @@ -976,16 +976,16 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate,
private func getCamera() -> MLNMapCamera? {
return trackCameraPosition ? mapView.camera : nil
}

private func setMapLanguage(language: String) {
self.mapView.setMapLanguage(language)
}

/*
* 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
Expand All @@ -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)
}

/*
Expand All @@ -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: [
Expand Down Expand Up @@ -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
}
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion maplibre_gl/lib/src/annotation_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ abstract class AnnotationManager<T extends Annotation> {
}
}

_onFeatureTapped(dynamic id, Point<double> point, LatLng coordinates) {
_onFeatureTapped(
dynamic id, Point<double> point, LatLng coordinates, String layerId) {
final annotation = _idToAnnotation[id];
if (annotation != null) {
onTap!(annotation);
Expand Down
5 changes: 3 additions & 2 deletions maplibre_gl/lib/src/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ typedef OnMapClickCallback = void Function(
Point<double> point, LatLng coordinates);

typedef OnFeatureInteractionCallback = void Function(
dynamic id, Point<double> point, LatLng coordinates);
dynamic id, Point<double> point, LatLng coordinates, String layerId);

typedef OnFeatureDragnCallback = void Function(dynamic id,
{required Point<double> point,
Expand Down Expand Up @@ -93,7 +93,8 @@ class MapLibreMapController extends ChangeNotifier {
_maplibrePlatform.onFeatureTappedPlatform.add((payload) {
for (final fun
in List<OnFeatureInteractionCallback>.from(onFeatureTapped)) {
fun(payload["id"], payload["point"], payload["latLng"]);
fun(payload["id"], payload["point"], payload["latLng"],
payload["layerId"]);
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>(x, y),
'latLng': LatLng(lat, lng)
'latLng': LatLng(lat, lng),
'layerId': layerId
});
case 'feature#onDrag':
final id = call.arguments['id'];
Expand Down

0 comments on commit 8246a84

Please sign in to comment.