-
Notifications
You must be signed in to change notification settings - Fork 1.3k
[ios, macos] Add shapes annotations select/deselect delegates. #9755
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -152,6 +152,11 @@ typedef NS_ENUM(NSUInteger, MGLUserTrackingState) { | |
/// Mapping from an annotation object to an annotation tag. | ||
typedef std::map<id<MGLAnnotation>, MGLAnnotationTag> MGLAnnotationObjectTagMap; | ||
|
||
/// Mapping from a shape annotation object to shape layer id. | ||
typedef std::map<id<MGLAnnotation>, std::string> MGLShapeAnnotationObjectLayerIDMap; | ||
|
||
static NSString *const MGLLayerIDShapeAnnotation = @"com.mapbox.annotations.shape."; | ||
|
||
mbgl::util::UnitBezier MGLUnitBezierForMediaTimingFunction(CAMediaTimingFunction *function) | ||
{ | ||
if ( ! function) | ||
|
@@ -285,9 +290,11 @@ @implementation MGLMapView | |
|
||
MGLAnnotationTagContextMap _annotationContextsByAnnotationTag; | ||
MGLAnnotationObjectTagMap _annotationTagsByAnnotation; | ||
|
||
MGLShapeAnnotationObjectLayerIDMap _shapeAnnotationLayerIDsByAnnotation; | ||
|
||
/// Tag of the selected annotation. If the user location annotation is selected, this ivar is set to `MGLAnnotationTagNotFound`. | ||
MGLAnnotationTag _selectedAnnotationTag; | ||
MGLShape *_selectedShapeAnnotation; | ||
|
||
BOOL _userLocationAnnotationIsSelected; | ||
/// Size of the rectangle formed by unioning the maximum slop area around every annotation image and annotation image view. | ||
|
@@ -468,6 +475,7 @@ - (void)commonInit | |
_annotationImagesByIdentifier = [NSMutableDictionary dictionary]; | ||
_annotationContextsByAnnotationTag = {}; | ||
_annotationTagsByAnnotation = {}; | ||
_shapeAnnotationLayerIDsByAnnotation = {}; | ||
_annotationViewReuseQueueByIdentifier = [NSMutableDictionary dictionary]; | ||
_selectedAnnotationTag = MGLAnnotationTagNotFound; | ||
_annotationsNearbyLastTap = {}; | ||
|
@@ -1483,12 +1491,20 @@ - (void)handleSingleTapGesture:(UITapGestureRecognizer *)singleTap | |
id<MGLAnnotation>annotation = [self annotationForGestureRecognizer:singleTap persistingResults:YES]; | ||
if(annotation) | ||
{ | ||
[self deselectShapeAnnotation:_selectedShapeAnnotation]; | ||
[self selectAnnotation:annotation animated:YES]; | ||
} | ||
else | ||
{ | ||
[self deselectAnnotation:self.selectedAnnotation animated:YES]; | ||
MGLShape *shapeAnnotation = [self shapeAnnotationForGestureRecognizer:singleTap]; | ||
if (shapeAnnotation) { | ||
[self selectShapeAnnotation:shapeAnnotation]; | ||
} else { | ||
[self deselectShapeAnnotation:_selectedShapeAnnotation]; | ||
} | ||
} | ||
|
||
} | ||
|
||
/** | ||
|
@@ -1861,8 +1877,12 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer | |
if(!self.selectedAnnotation) | ||
{ | ||
id<MGLAnnotation>annotation = [self annotationForGestureRecognizer:(UITapGestureRecognizer*)gestureRecognizer persistingResults:NO]; | ||
if(!annotation) { | ||
return NO; | ||
if(!annotation && !_selectedShapeAnnotation) { | ||
MGLShape *shapeAnnotation = [self shapeAnnotationForGestureRecognizer:(UITapGestureRecognizer*)gestureRecognizer]; | ||
if (!shapeAnnotation) { | ||
return NO; | ||
} | ||
|
||
} | ||
} | ||
} | ||
|
@@ -3226,6 +3246,8 @@ - (void)addAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations | |
context.annotation = annotation; | ||
_annotationContextsByAnnotationTag[annotationTag] = context; | ||
_annotationTagsByAnnotation[annotation] = annotationTag; | ||
NSString *layerID = [NSString stringWithFormat:@"%@%u", MGLLayerIDShapeAnnotation, annotationTag]; | ||
_shapeAnnotationLayerIDsByAnnotation[annotation] = layerID.UTF8String; | ||
|
||
[(NSObject *)annotation addObserver:self forKeyPath:@"coordinates" options:0 context:(void *)(NSUInteger)annotationTag]; | ||
} | ||
|
@@ -3524,6 +3546,7 @@ - (void)removeAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations | |
|
||
_annotationContextsByAnnotationTag.erase(annotationTag); | ||
_annotationTagsByAnnotation.erase(annotation); | ||
_shapeAnnotationLayerIDsByAnnotation.erase(annotation); | ||
|
||
if ([annotation isKindOfClass:[NSObject class]] && ![annotation isKindOfClass:[MGLMultiPoint class]]) | ||
{ | ||
|
@@ -4150,6 +4173,99 @@ - (void)applyIconIdentifier:(NSString *)iconIdentifier toAnnotationsWithImageReu | |
} | ||
} | ||
|
||
#pragma mark - Shape Annotation | ||
|
||
- (void)selectShapeAnnotation:(MGLShape *)shapeAnnotation | ||
{ | ||
if (!shapeAnnotation) return; | ||
|
||
if (shapeAnnotation == _selectedShapeAnnotation) return; | ||
|
||
[self deselectShapeAnnotation:_selectedShapeAnnotation]; | ||
|
||
_selectedShapeAnnotation = shapeAnnotation; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The intent of #2082 was that we’d display a callout over the selected shape. Per #2082 (comment), we’d need to constrain the coordinate to the current viewport before calculating the centroid. So There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Our Android SDK gained essentially the exact same functionality that is proposed here in #9443. I don't think there is any mechanism in that SDK to assist with callout placement, yet. I think it would be ok to use the same approach and keep #2082 open and handle callouts separately. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess that would be an argument for adding a separate set of delegate methods, because it would be weird for It doesn’t feel great to potentially have a point annotation and shape annotation both be selected at the same time. It’ll lock us into a design that’s reminiscent of MapKit but quite different, even after we implement the callout views. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My principal deterrent to avoid using But selecting a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the broader question is whether shapes should have callouts. The original request in #2082 and much of the subsequent comments were about callouts, so I guess I’d consider that PR unfinished without them. If we land this feature without callouts and with an API that’s separate from normal annotation selection, does that make it more difficult for us to implement shape callouts later? Or are we counting on being able to clean up this API in v4.0.0? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How much of a lift would it be to support callouts for shape annotations? At a glance, it looks like most of the pieces are already in place (especially polylabel). |
||
if ([self.delegate respondsToSelector:@selector(mapView:didSelectShapeAnnotation:)]) | ||
{ | ||
[self.delegate mapView:self didSelectShapeAnnotation:shapeAnnotation]; | ||
} | ||
} | ||
|
||
- (void)deselectShapeAnnotation:(MGLShape *)shapeAnnotation | ||
{ | ||
if (!shapeAnnotation) return; | ||
|
||
if (_selectedShapeAnnotation == shapeAnnotation) | ||
{ | ||
if ([self.delegate respondsToSelector:@selector(mapView:didDeselectShapeAnnotation:)]) | ||
{ | ||
[self.delegate mapView:self didDeselectShapeAnnotation:shapeAnnotation]; | ||
} | ||
_selectedShapeAnnotation = nil; | ||
} | ||
|
||
} | ||
|
||
- (MGLShape*)shapeAnnotationForGestureRecognizer:(UITapGestureRecognizer*)singleTap | ||
{ | ||
CGPoint tapPoint = [singleTap locationInView:self]; | ||
|
||
MGLAnnotationTag hitAnnotationTag = [self shapeAnnotationTagAtPoint:tapPoint]; | ||
|
||
if (hitAnnotationTag != MGLAnnotationTagNotFound) { | ||
id <MGLAnnotation> annotation = [self annotationWithTag:hitAnnotationTag]; | ||
NSAssert(annotation, @"Cannot select nonexistent annotation with tag %u", hitAnnotationTag); | ||
if ([annotation isKindOfClass:[MGLShape class]]) { | ||
return (MGLShape *)annotation; | ||
} | ||
} | ||
|
||
return nil; | ||
} | ||
|
||
- (MGLAnnotationTag)shapeAnnotationTagAtPoint:(CGPoint)point | ||
{ | ||
CGRect queryRect = CGRectInset({ point, CGSizeZero }, | ||
-_unionedAnnotationRepresentationSize.width, | ||
-_unionedAnnotationRepresentationSize.height); | ||
queryRect = CGRectInset(queryRect, -MGLAnnotationImagePaddingForHitTest, | ||
-MGLAnnotationImagePaddingForHitTest); | ||
|
||
std::vector<MGLAnnotationTag> nearbyAnnotations = [self shapeAnnotationTagsInRect:queryRect]; | ||
|
||
MGLAnnotationTag hitAnnotationTag = MGLAnnotationTagNotFound; | ||
|
||
// Choose the first nearby annotation. | ||
if (nearbyAnnotations.size()) | ||
{ | ||
hitAnnotationTag = nearbyAnnotations.front(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The older -[MGLMapView annotationTagAtPoint:persistingResults:] method has logic to cycle through points that re overlapping / nearby each other. Do we need to worry about that for shapes? What is the expected behavior if a user touches in an area where several polygons overlap or several polylines intersect? cc @1ec5 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It’s currently possible to disable touch interaction for a given point annotation using either |
||
} | ||
return hitAnnotationTag; | ||
} | ||
|
||
- (std::vector<MGLAnnotationTag>)shapeAnnotationTagsInRect:(CGRect)rect | ||
{ | ||
mbgl::ScreenBox screenBox = { | ||
{ CGRectGetMinX(rect), CGRectGetMinY(rect) }, | ||
{ CGRectGetMaxX(rect), CGRectGetMaxY(rect) }, | ||
}; | ||
|
||
std::vector<MGLAnnotationTag> nearbyAnnotations; | ||
|
||
mbgl::optional<std::vector<std::string>> optionalLayerIDs; | ||
if (_shapeAnnotationLayerIDsByAnnotation.size()) { | ||
__block std::vector<std::string> layerIDs; | ||
layerIDs.reserve(_shapeAnnotationLayerIDsByAnnotation.size()); | ||
for (const auto &annotation : _shapeAnnotationLayerIDsByAnnotation) { | ||
layerIDs.push_back(annotation.second); | ||
} | ||
optionalLayerIDs = layerIDs; | ||
nearbyAnnotations = _mbglMap->queryShapeAnnotations(screenBox, { optionalLayerIDs }); | ||
} | ||
|
||
return nearbyAnnotations; | ||
} | ||
|
||
#pragma mark - User Location - | ||
|
||
- (void)validateLocationServices | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -381,6 +381,27 @@ NS_ASSUME_NONNULL_BEGIN | |
*/ | ||
- (void)mapView:(MGLMapView *)mapView didDeselectAnnotation:(id <MGLAnnotation>)annotation; | ||
|
||
/** | ||
Tells the delegate that one of its shape annotations was selected. | ||
|
||
You can use this method to track changes in the selection state of annotations. | ||
|
||
@param mapView The map view containing the annotation. | ||
@param shapeAnnotation The shape annotation that was selected. | ||
*/ | ||
- (void)mapView:(MGLMapView *)mapView didSelectShapeAnnotation:(MGLShape *)shapeAnnotation; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that the existing methods are called On the one hand, I understand that suddenly passing shape annotations into a method that has long received only point annotations could be a surprise to some developers. On the other hand, we’ve never documented that prior limitation – it’s essentially a bug. There is precedent for opening up delegate methods to new data, namely when we started passing MGLUserLocation into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In #9755 (comment) I mention an alternative There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason behind naming this as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These new APIs don't provide a shape "annotation". They do provide an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MGLShape conforms to MGLAnnotation. |
||
|
||
/** | ||
Tells the delegate that one of its shape annotations was deselected. | ||
|
||
You can use this method to track changes in the selection state of annotations. | ||
|
||
|
||
@param mapView The map view containing the annotation. | ||
@param shapeAnnotation The shape annotation that was deselected. | ||
*/ | ||
- (void)mapView:(MGLMapView *)mapView didDeselectShapeAnnotation:(MGLShape *)shapeAnnotation; | ||
|
||
/** | ||
Tells the delegate that one of its annotation views was selected. | ||
|
||
|
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.
Tapping a map with no annotations now crashes in
-[MGLMapView shapeAnnotationForGestureRecognizer:]
because thehitAnnotationTag
is 0 (instead ofMGLAnnotationTagNotFound
) and that gets a nil result from[self annotationWithTag:hitAnnotationTag]
which triggers an assert. I think the root cause is that-[MGLMapView shapeAnnotationTagsInRect:]
has no_shapeAnnotationLayerIDs
values to put in options so the_mbglMap->queryShapeAnnotations()
actually returns some values instead of an empty list.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.
Fixed. Thank you for catching this bug.