-
Notifications
You must be signed in to change notification settings - Fork 1.3k
[ios, macos] Add selection support to MGLMultiPoint annotations. #9984
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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!
platform/ios/src/MGLMapView.mm
Outdated
@@ -285,6 +289,7 @@ @implementation MGLMapView | |||
|
|||
MGLAnnotationTagContextMap _annotationContextsByAnnotationTag; | |||
MGLAnnotationObjectTagMap _annotationTagsByAnnotation; | |||
MGLShapeAnnotationObjectLayerIDMap _shapeAnnotationLayerIDsByAnnotation; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_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.
platform/ios/src/MGLMapView.mm
Outdated
@@ -152,6 +152,10 @@ typedef NS_ENUM(NSUInteger, MGLUserTrackingState) { | |||
/// Mapping from an annotation object to an annotation tag. | |||
typedef std::map<id<MGLAnnotation>, MGLAnnotationTag> MGLAnnotationObjectTagMap; | |||
|
|||
typedef std::map<id<MGLAnnotation>, std::string> MGLShapeAnnotationObjectLayerIDMap; | |||
|
|||
static NSString *const MGLLayerIDShapeAnnotation = @"com.mapbox.annotations.shape."; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let’s call this constant MGLShapeAnnotationLayerIdentifierPrefix
, by analogy with MGLAnnotationSpritePrefix
.
platform/ios/src/MGLMapView.mm
Outdated
@@ -3646,6 +3667,11 @@ - (MGLAnnotationTag)annotationTagAtPoint:(CGPoint)point persistingResults:(BOOL) | |||
CGRect annotationRect; | |||
|
|||
MGLAnnotationView *annotationView = annotationContext.annotationView; | |||
|
|||
if (queryingShapeAnnotations) { | |||
return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
platform/ios/src/MGLMapView.mm
Outdated
{ CGRectGetMaxX(rect), CGRectGetMaxY(rect) }, | ||
}; | ||
|
||
std::vector<MGLAnnotationTag> nearbyAnnotations; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This appears to be a list of all the annotation tags, not just the nearby ones.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -3960,6 +4010,13 @@ - (CGRect)positioningRectForCalloutForAnnotationWithTag:(MGLAnnotationTag)annota | |||
{ | |||
return CGRectZero; | |||
} | |||
|
|||
if ([annotation isKindOfClass:[MGLMultiPoint class]]) { | |||
CLLocationCoordinate2D origin = annotation.coordinate; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have an API for clipping?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to cut a ticket for this. I will add a new API for clipping.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
platform/ios/src/MGLMapView.mm
Outdated
{ CGRectGetMaxX(rect), CGRectGetMaxY(rect) }, | ||
}; | ||
|
||
std::vector<MGLAnnotationTag> nearbyAnnotations; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
platform/ios/src/MGLMapView.mm
Outdated
@@ -152,6 +152,8 @@ typedef NS_ENUM(NSUInteger, MGLUserTrackingState) { | |||
/// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This constant should have a documentation comment.
platform/ios/src/MGLMapView.mm
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -3960,6 +4010,13 @@ - (CGRect)positioningRectForCalloutForAnnotationWithTag:(MGLAnnotationTag)annota | |||
{ | |||
return CGRectZero; | |||
} | |||
|
|||
if ([annotation isKindOfClass:[MGLMultiPoint class]]) { | |||
CLLocationCoordinate2D origin = annotation.coordinate; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
platform/ios/src/MGLMapView.mm
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
platform/ios/src/MGLMapView.mm
Outdated
@@ -3246,6 +3246,11 @@ - (void)removeStyleClass:(NSString *)styleClass | |||
} | |||
|
|||
std::vector<MGLAnnotationTag> annotationTags = [self annotationTagsInRect:rect]; | |||
|
|||
if (!annotationTags.size()) { | |||
annotationTags = [self shapeAnnotationTagsInRect:rect]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
platform/ios/src/MGLMapView.mm
Outdated
NSAssert(annotation, @"Unknown annotation found nearby tap"); | ||
if ( ! annotation) | ||
{ | ||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: funky indentation.
src/mbgl/renderer/renderer.cpp
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
platform/ios/src/MGLMapView.mm
Outdated
std::vector<MGLAnnotationTag> shapeAnnotationTags = [self shapeAnnotationTagsInRect:rect]; | ||
|
||
if (!shapeAnnotationTags.size()) { | ||
annotationTags.insert(annotationTags.end(), shapeAnnotationTags.begin(), shapeAnnotationTags.end()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@1ec5 You're right I made the corresponding changes.
src/mbgl/renderer/renderer_impl.cpp
Outdated
std::vector<Feature> Renderer::Impl::queryShapeAnnotations(const ScreenLineString& geometry) const { | ||
std::vector<const RenderLayer*> layers; | ||
RenderedQueryOptions options; | ||
options.layerIDs = {{ AnnotationManager::ShapeLayerID }}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
src/mbgl/renderer/renderer_impl.cpp
Outdated
@@ -655,6 +655,52 @@ std::vector<Feature> Renderer::Impl::queryRenderedFeatures(const ScreenLineStrin | |||
|
|||
return result; | |||
} | |||
|
|||
std::vector<Feature> Renderer::Impl::queryShapeAnnotations(const ScreenLineString& geometry) const { | |||
std::vector<const RenderLayer*> layers; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit var name: shapeAnnotationLayers
src/mbgl/renderer/renderer.cpp
Outdated
@@ -52,6 +52,17 @@ std::vector<Feature> Renderer::queryRenderedFeatures(const ScreenBox& box, const | |||
options | |||
); | |||
} | |||
|
|||
std::vector<Feature> Renderer::queryRenderedSourceFeatures(const ScreenBox& box) const { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method could be removed and it's logic encapsulated in Renderer::queryShapeAnnotations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
src/mbgl/renderer/renderer_impl.cpp
Outdated
layers.emplace_back(layer); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
src/mbgl/renderer/renderer_impl.cpp
Outdated
std::unordered_set<std::string> sourceIDs; | ||
for (const RenderLayer* layer : layers) { | ||
sourceIDs.emplace(layer->baseImpl->source); | ||
} | ||
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Tabs on empty lines
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think queryRenderedSourceFeatures
could be renamed to queryShapeAnnotationFeatures
or better yet collapsed into queryShapeAnnotations
src/mbgl/renderer/renderer.cpp
Outdated
@@ -52,6 +52,17 @@ std::vector<Feature> Renderer::queryRenderedFeatures(const ScreenBox& box, const | |||
options | |||
); | |||
} | |||
|
|||
std::vector<Feature> Renderer::queryRenderedSourceFeatures(const ScreenBox& box) const { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
@@ -3762,61 +3768,69 @@ - (MGLAnnotationTag)annotationTagAtPoint:(CGPoint)point persistingResults:(BOOL) | |||
queryRect = CGRectInset(queryRect, -MGLAnnotationImagePaddingForHitTest, | |||
-MGLAnnotationImagePaddingForHitTest); | |||
std::vector<MGLAnnotationTag> nearbyAnnotations = [self annotationTagsInRect:queryRect]; | |||
BOOL queryingShapeAnnotations = NO; | |||
|
|||
if (!nearbyAnnotations.size()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
MGLMultiPoint
support.MGLMultiPoint
callout.MGLPointAnnotations
.This is how it looks.