maplibre / maplibre-plugins-android

MapLibre Native for Android Plugins
https://maplibre.org/
BSD 2-Clause "Simplified" License
24 stars 25 forks source link

Annotation dragging does not respect the layer order #44

Closed JonasVautherin closed 1 year ago

JonasVautherin commented 1 year ago

If I create two circles in two different managers, the second one appear above the first one, but the first one gets priority when dragging.

I can solve that by setting the belowLayer of the second manager to the layerId or the first manager. So the drawing order seems fine, but the dragging order is wrong (it should follow the drawing order).

Here is the code I am testing:

val circleAnnotationManager = CircleManager(map, mapboxMap, style)
val circleOptions = CircleOptions()
    .withCircleColor("Blue")
    .withCircleRadius(30.0F)
    .withLatLng(LatLng(46.0, 6.0))
    .withDraggable(true)
circleAnnotationManager.create(circleOptions)

val circleAnnotationManager2 = CircleManager(map, mapboxMap, style)
val circleOptions2 = CircleOptions()
    .withCircleColor("Yellow")
    .withCircleRadius(30.0F)
    .withLatLng(LatLng(46.0, 6.0))
    .withDraggable(true)
circleAnnotationManager2.create(circleOptions2)

And here is a video showing the result:

https://github.com/maplibre/maplibre-plugins-android/assets/2606672/5ada7025-4096-4952-a4c2-8af10d09cc0d

@louwers @fynngodau: any idea where this could be happening? If you point me roughly where you think it could be in the code, I'm happy to try to debug it :blush:.

louwers commented 1 year ago

Thanks for the clear bug report.

So just to clarify, when you don't set withDraggable(true) the order is correct?

Have you looked at the implementation? Where is the concept of something being draggable defined? The Plugin, the Android SDK or the C++ layer? I guess that's the best place to start debugging.

JonasVautherin commented 1 year ago

So just to clarify, when you don't set withDraggable(true) the order is correct?

The drawing order is always correct. The dragging order is not. So if I don't set withDraggable(true), then it is not draggable, and I don't see the dragging problem :grin:.

Where is the concept of something being draggable defined?

The Annotation type itself is set to be draggable. But then I am not completely sure where the logic happens that goes from a touch event on the map to actually deciding that this annotation should capture the event.

JonasVautherin commented 1 year ago

Actually I think the query for the annotations is happening here: https://github.com/maplibre/maplibre-plugins-android/blob/main/plugin-annotation/src/main/java/com/mapbox/mapboxsdk/plugins/annotation/DraggableAnnotationController.java#L109

        for (AnnotationManager annotationManager : annotationManagers) {
            if (detector.getPointersCount() == 1) {
                Annotation annotation = annotationManager.queryMapForFeatures(detector.getFocalPoint());
                if (annotation != null && startDragging(annotation, annotationManager)) {
                    return true;
                }
            }
        }

Which calls

    T queryMapForFeatures(@NonNull PointF point) {
        List<Feature> features = mapboxMap.queryRenderedFeatures(point, coreElementProvider.getLayerId());
        if (!features.isEmpty()) {
            long id = features.get(0).getProperty(getAnnotationIdKey()).getAsLong();
            return annotations.get(id);
        }
        return null;
    }

Which seems... like it is taking the first feature (features.get(0)), right? That looks suspicious.

JonasVautherin commented 1 year ago

I think I got it, it's happening in onMoveBegin:

    boolean onMoveBegin(MoveGestureDetector detector) {
        for (AnnotationManager annotationManager : annotationManagers) {
            if (detector.getPointersCount() == 1) {
                Annotation annotation = annotationManager.queryMapForFeatures(detector.getFocalPoint());
                if (annotation != null && startDragging(annotation, annotationManager)) {
                    return true;
                }
            }
        }
        return false;
    }

It goes through all the annotationManagers, and check if that particular annotationManager has an annotation. If it does (and it is draggable), then it returns true (which starts the dragging).

But the problem is that it returns true for the first annotationManager for which it finds an annotation, regardless its zIndex. So instead, I think onMoveBegin should keep a list of all the annotationManager instances that have an annotation at this point, and then assign the drag to the top one.

Not sure how to do it yet, though :sweat_smile:, as the AnnotationManager elements don't have directly a zIndex, so I need to find a way to compare them ("is this one above or below this other one?").

JonasVautherin commented 1 year ago

@louwers: I think we may need something from the NativeMap :thinking:. Right now when adding a layer, it ends up calling something like style.addLayerBelow(), and Style.java calls nativeMap.addLayerBelow(layer, below);.

But the layer itself does not know it's position in the "vertical stack", and has no way to get it as far as I can see.

Would it make sense to have a native function that would help compare two different layers? Something like this:

int compareLayersZIndex(Layer left, Layer right);

Which would call NativeMap and return something like -1 (left is below), 0 (they have the same zIndex) or 1 (right is below)? That way the onMoveBegin() function above could get all the candidate layers, compare them to extract the top one, and drag only the top one.

@fynngodau: I'd like to have your opinion on this, too!

louwers commented 1 year ago

I agree that the expected behavior is that dragging puts something at the top. We should probably incorporate idea in the new Annotations API.

In the meantime, as a workaround, I think you may be able to use circle-sort-key even though it is not exposed with CircleOptions with CircleOptions.withData. You'd have to update the matching annotation in a callback when the dragging starts.

JonasVautherin commented 1 year ago

I am not sure I understand. So you mean that somewhere in the AnnotationManager logic, I should inject custom data (e.g. the zIndex) and then use that to sort the layers in the onMoveBegin code above?

The thing is that circle-sort-key is not enough, because it's not only between circles: it's between layers...

I guess one thing I could do is expose belowLayerId and aboveLayerId in the AnnotationManager, and then from the addAnnotationManager and removeAnnotationManager of DraggableAnnotationController I could reconstruct the vertical stack, and use it to sort the layers in onMoveBegin.

But since the native map below has to know about that vertical stack, I thought maybe it would be better to expose that (with a function like compareLayersZIndex) rather than re-implement the vertical stack logic in the plugin. Or the native map could return the layers in vertical order (instead of adding a compareLayersZIndex function), but that would make it implicit, which is maybe not better).

Now if it is not possible to expose from the native map, then I guess the workaround is to duplicate that state in the plugin :thinking:.

louwers commented 1 year ago

Took a while, but now I understand the problem. The problem is not the rendering order, but that the one that is rendered on top should be the one that is dragged. Instead of introducing this new API, should we not just make sure queryRenderedFeatures returns features in the right order?

By the way I also expect something that is dragged to be put on the top, but that is a separate issue then I suppose.

JonasVautherin commented 1 year ago

Instead of introducing this new API, should we not just make sure queryRenderedFeatures returns features in the right order?

Yes exactly! That's what I meant with "Or the native map could return the layers in vertical order (instead of adding a compareLayersZIndex function), but that would make it implicit".

But that is going into the C++ native code, right? Are you familiar with that part of the code? I tracked it to here in the render_orchestrator.cpp, but I don't really understand enough to see where the layer vertical order should intervene.

louwers commented 1 year ago

I'll have a look but I can't make any promises.

JonasVautherin commented 1 year ago

I had another look, and I am starting to think that the C++ part is really per layer. A layer queries the rendered feature for itself.

And the bug is between layers, so that would be on the Android side. I am guessing that somewhere, the move gesture is detected and passed to each layer one at a time. The layer then has an opportunity to "capture" the event (if it has a feature that should use it) or not. And obviously if the layers are not called in the right order, then the dragging may be captured by the wrong layer.

So yeah, I'll have a closer look on the Android side over the weekend!