[ios, macos] Add selection support to MGLMultiPoint annotations.#9984
Conversation
1ec5
left a comment
There was a problem hiding this comment.
If you don’t mind, please make the same changes to the macOS SDK’s implementation of MGLMapView so the two implementations don’t diverge any further. Thanks!
|
|
||
| MGLAnnotationTagContextMap _annotationContextsByAnnotationTag; | ||
| MGLAnnotationObjectTagMap _annotationTagsByAnnotation; | ||
| MGLShapeAnnotationObjectLayerIDMap _shapeAnnotationLayerIDsByAnnotation; |
There was a problem hiding this comment.
_annotationTagsByAnnotation already associates each shape annotation with an annotation tag. Do we need a separate ivar to track the layer IDs used by mbgl? Seems like we can prefix the annotation tag on the fly as needed.
|
|
||
| typedef std::map<id<MGLAnnotation>, std::string> MGLShapeAnnotationObjectLayerIDMap; | ||
|
|
||
| static NSString *const MGLLayerIDShapeAnnotation = @"com.mapbox.annotations.shape."; |
There was a problem hiding this comment.
Let’s call this constant MGLShapeAnnotationLayerIdentifierPrefix, by analogy with MGLAnnotationSpritePrefix.
| MGLAnnotationView *annotationView = annotationContext.annotationView; | ||
|
|
||
| if (queryingShapeAnnotations) { | ||
| return false; |
There was a problem hiding this comment.
If queryingShapeAnnotations is true, then we still end up running this lambda once for each entry in nearbyAnnotations, which can be expensive. Bring this queryingShapeAnnotations check outside the lambda so that the lambda doesn’t run at all.
| { CGRectGetMaxX(rect), CGRectGetMaxY(rect) }, | ||
| }; | ||
|
|
||
| std::vector<MGLAnnotationTag> nearbyAnnotations; |
There was a problem hiding this comment.
This appears to be a list of all the annotation tags, not just the nearby ones.
There was a problem hiding this comment.
It contains nearby ones. You can look at line 3799:
return nearbyAnnotations = _mbglMap->queryShapeAnnotations(screenBox, { optionalLayerIDs });
Otherwise returns an empty vector.
There was a problem hiding this comment.
What I meant was that this vector contains all the annotations within the specified rect. It just happens that this method is called with a rect that represents the area near the tap gesture, but that isn’t required by this method. Perhaps annotationsInRect would be more self-explanatory.
| } | ||
|
|
||
| if ([annotation isKindOfClass:[MGLMultiPoint class]]) { | ||
| CLLocationCoordinate2D origin = annotation.coordinate; |
There was a problem hiding this comment.
If the coordinate lies outside the current viewport, we should move it somewhere within the current viewport. This might require us to clip the geometry to the viewport (in a temporary new shape object) before calculating its centroid.
There was a problem hiding this comment.
Do we have an API for clipping?
There was a problem hiding this comment.
turf-difference returns the difference between two polygons (in this case, the shape and the visible coordinate rect). But we don’t have a Swift port at the ready. mbgl may be using some clipping utilities that we can take advantage of.
There was a problem hiding this comment.
I'm going to cut a ticket for this. I will add a new API for clipping.
There was a problem hiding this comment.
The reason I brought this up is that the callout might end up being completely invisible, making the user think the map did nothing in response to the tap. But it does make sense to treat the clipping behavior as tail work.
There was a problem hiding this comment.
It just occurred to me that the best fix for this issue would be to automatically pan to the selected shape annotation’s centroid, to show as much of the shape as possible, before showing the callout. We could fix #3249 at the same time.
There was a problem hiding this comment.
Also @frederoni pointed that it may be useful to set the callout anchor to where the user tapped in case the centroid is off screen. What do you think?
There was a problem hiding this comment.
Anchoring the callout at the tap location would also be reasonable. If I’m not mistaken, that’s how polygon selection works in Leaflet, so there’s precedent for it. #3249 would still be desirable, but we can do that separately.
174da96 to
7152903
Compare
e71320c to
7408d21
Compare
| { CGRectGetMaxX(rect), CGRectGetMaxY(rect) }, | ||
| }; | ||
|
|
||
| std::vector<MGLAnnotationTag> nearbyAnnotations; |
There was a problem hiding this comment.
What I meant was that this vector contains all the annotations within the specified rect. It just happens that this method is called with a rect that represents the area near the tap gesture, but that isn’t required by this method. Perhaps annotationsInRect would be more self-explanatory.
| /// Mapping from an annotation object to an annotation tag. | ||
| typedef std::map<id<MGLAnnotation>, MGLAnnotationTag> MGLAnnotationObjectTagMap; | ||
|
|
||
| static NSString *const MGLShapeAnnotationLayerIdentifierPrefix = @"com.mapbox.annotations.shape."; |
There was a problem hiding this comment.
This constant should have a documentation comment.
| auto end = std::remove_if(nearbyAnnotations.begin(), nearbyAnnotations.end(), | ||
| [&](const MGLAnnotationTag annotationTag) | ||
| { | ||
| id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag]; |
There was a problem hiding this comment.
The indentation is a bit extreme here. I realize Xcode did this automatically, but would you mind indenting this lambda by only one tab beyond the auto instead of aligning with the opening parenthesis of the remove_if() call? (In this situation, I’d probably use the ⇧⌥⌘V shortcut to preserve indentation then indent manually.) Alternatively, consider breaking the lambda out into a separate local variable, which will make this code easier to read.
| } | ||
|
|
||
| if ([annotation isKindOfClass:[MGLMultiPoint class]]) { | ||
| CLLocationCoordinate2D origin = annotation.coordinate; |
There was a problem hiding this comment.
The reason I brought this up is that the callout might end up being completely invisible, making the user think the map did nothing in response to the tap. But it does make sense to treat the clipping behavior as tail work.
| NSString *layerID; | ||
| for (const auto &annotation : _annotationTagsByAnnotation) { | ||
| layerID = [NSString stringWithFormat:@"%@%u", MGLShapeAnnotationLayerIdentifierPrefix, annotation.second]; | ||
| layerIDs.push_back(layerID.UTF8String); |
There was a problem hiding this comment.
The dedicated queryShapeAnnotations() method should only ever return features in the shape layers; it shouldn’t be necessary for callers to build their own lists of layer IDs. Push this logic down to map.cpp, and pull this string out into mbgl::AnnotationManager so the code in map.cpp can reuse it.
7408d21 to
27b2993
Compare
8d81d5e to
a69461d
Compare
| std::vector<MGLAnnotationTag> annotationTags = [self annotationTagsInRect:rect]; | ||
|
|
||
| if (!annotationTags.size()) { | ||
| annotationTags = [self shapeAnnotationTagsInRect:rect]; |
There was a problem hiding this comment.
If a point annotation and a shape annotation are both visible, this method should return both. That’s what I’d expect after reading this public method’s documentation, anyways.
| NSAssert(annotation, @"Unknown annotation found nearby tap"); | ||
| if ( ! annotation) | ||
| { | ||
| return true; |
| if (layerIDs.size()) { | ||
| shapeLayerIDs.reserve(layerIDs.size()); | ||
| for (const auto &layerID : layerIDs) { | ||
| shapeLayerIDs.push_back(AnnotationManager::ShapeLayerID + layerID); |
There was a problem hiding this comment.
mbgl::Renderer::Impl should have a method queryShapeAnnotations() that figures out all the shape layer IDs on its own. mbgl::Renderer::Impl has a layerImpls member that lists all the style layers; iterate over that vector to get each layer that has AnnotationManager::ShapeLayerID in its ID. In the end, -[MGLMapView shapeAnnotationTagsInRect:] shouldn’t have to build a layerIDs vector at all.
f9580a7 to
6d667e4
Compare
1ec5
left a comment
There was a problem hiding this comment.
The iOS/macOS side of this PR is looking better, although I still have concerns about the behavior where -visibleAnnotationsInRect: (a public method that can be used for more than tap selection) only returns shape annotations if no point annotations are nearby.
As for the mbgl side, I’d appreciate a second pair of eyes from @asheemmamoowala.
There was a problem hiding this comment.
My point in #9984 (comment) stands, but moreover, I think this code won’t do anything: if shapeAnnotationTags is empty, it adds the contents of shapeAnnotationTags to annotationTags.
There was a problem hiding this comment.
@1ec5 You're right I made the corresponding changes.
There was a problem hiding this comment.
There isn’t ever a layer with this ID, is there? Does mbgl use options.layerIDs for anything past this point?
6d667e4 to
fe5bc8d
Compare
| } | ||
|
|
||
| std::vector<Feature> Renderer::Impl::queryShapeAnnotations(const ScreenLineString& geometry) const { | ||
| std::vector<const RenderLayer*> layers; |
There was a problem hiding this comment.
nit var name: shapeAnnotationLayers
| ); | ||
| } | ||
|
|
||
| std::vector<Feature> Renderer::queryRenderedSourceFeatures(const ScreenBox& box) const { |
There was a problem hiding this comment.
This method could be removed and it's logic encapsulated in Renderer::queryShapeAnnotations
There was a problem hiding this comment.
Technically both methods should return different annotation types with different parameters. What I did is refactor the logic that was common to both queryPointAnnotations and queryShapeAnnotations.
There was a problem hiding this comment.
I don't follow, queryPointAnnotations calls queryRenderedFeatures.
Additionally, this method only calls queryShapeAnnotations, but has a name that implies it is a generic query for a source.
| layers.emplace_back(layer); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The rest of this method is identical to Renderer::Impl::queryRenderedFeatures. Can you refactor the two methods to share the querying?
fe5bc8d to
8d39252
Compare
| sourceIDs.emplace(layer->baseImpl->source); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
nit: Tabs on empty lines
There was a problem hiding this comment.
Renderer::queryPointAnnotations calls queryRenderedFeatures specifying the layer it should query. In this case point annotations are rendered in a single layer. You can pass all the layer ID's for the shape annotations; it was already originally designed this way as you can read in this comment we decided to change that to let the rendered figures out all the shape layer IDs on its own.
Which name do you think it would be more explicit than queryShapeAnnotations?
There was a problem hiding this comment.
I think queryRenderedSourceFeatures could be renamed to queryShapeAnnotationFeatures or better yet collapsed into queryShapeAnnotations
| ); | ||
| } | ||
|
|
||
| std::vector<Feature> Renderer::queryRenderedSourceFeatures(const ScreenBox& box) const { |
There was a problem hiding this comment.
I don't follow, queryPointAnnotations calls queryRenderedFeatures.
Additionally, this method only calls queryShapeAnnotations, but has a name that implies it is a generic query for a source.
93f1785 to
447664f
Compare
cbef83c to
4c6e4d8
Compare
| std::vector<MGLAnnotationTag> nearbyAnnotations = [self annotationTagsInRect:queryRect]; | ||
| BOOL queryingShapeAnnotations = NO; | ||
|
|
||
| if (!nearbyAnnotations.size()) { |
There was a problem hiding this comment.
If there is both a point annotation and a shape annotation beneath the passed-in point, this method should return the point annotation the first time this method is called and the shape annotation the second time, as long as persist is set to YES. That way the shape annotation remains accessible even when there is an overlapping point annotation.
/ref #9984 (comment)
There was a problem hiding this comment.
I’m OK with this detail being tail work, by the way. Overall, this PR is looking good, especially after the last few changes in core.
|
is there any example on how to use this to tap on a polyline for objective-C? I took a look at the pull request but couldn't figure out how to use it |
|
Hi, @lawrencelm. The behavior is similar to Tap the line. |
|
thanks! does this also work for MGLPolylineFeature? |
|
I tried adding a .title and then tapping on the polyline and it didn't work. I also tried changing from MGLPolylineFeature to MGLPolyline, and adding it in different ways ([self.mapView addOverlay] and [self.mapView addAnnotation]). This is my code: |
|
Try In the code you posted I don't see where you add Also please open a new issue if shape selection is not working. |
|
Works now! I was making a silly mistake where I was deleting annotations. Thanks a ton! |
No, if you use the runtime styling API (as in a source and a layer), you’d have to implement interactivity manually and also manage the current selection. #6515 would alleviate a little of that. |
WIP fixes #2082
I modified the initial implementation from #9755 to support callouts and reuse our current API.
Next steps:
MGLMultiPointsupport.MGLMultiPointcallout.MGLPointAnnotations.This is how it looks.
