diff --git a/include/mbgl/ios/MGLAnnotationImage.h b/include/mbgl/ios/MGLAnnotationImage.h index f9d9e70566c..fa2adb38309 100644 --- a/include/mbgl/ios/MGLAnnotationImage.h +++ b/include/mbgl/ios/MGLAnnotationImage.h @@ -21,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark Getting and Setting Attributes /** The image to be displayed for the annotation. */ -@property (nonatomic, strong) UIImage *image; +@property (nonatomic, strong, nullable) UIImage *image; /** The string that identifies that this annotation image is reusable. (read-only) diff --git a/ios/app/MBXViewController.mm b/ios/app/MBXViewController.mm index 8269b09f32a..15ec3360d04 100644 --- a/ios/app/MBXViewController.mm +++ b/ios/app/MBXViewController.mm @@ -169,6 +169,7 @@ - (void)showSettings @"Add Test Shapes", @"Start World Tour", @"Add Custom Callout Point", + @"Reset Annotation Images", @"Remove Annotations", (_isShowingCustomStyleLayer ? @"Hide Custom Style Layer" @@ -559,52 +560,70 @@ - (MGLAnnotationImage *)mapView:(MGLMapView * __nonnull)mapView imageForAnnotati if (!title.length) return nil; NSString *lastTwoCharacters = [title substringFromIndex:title.length - 2]; - UIColor *color; + MGLAnnotationImage *annotationImage = [mapView dequeueReusableAnnotationImageWithIdentifier:lastTwoCharacters]; - // make every tenth annotation blue - if ([lastTwoCharacters hasSuffix:@"0"]) { - color = [UIColor blueColor]; - } else { - color = [UIColor redColor]; - } - - MGLAnnotationImage *image = [mapView dequeueReusableAnnotationImageWithIdentifier:lastTwoCharacters]; - - if ( ! image) + if ( ! annotationImage) { - CGRect rect = CGRectMake(0, 0, 20, 15); - - UIGraphicsBeginImageContextWithOptions(rect.size, NO, [[UIScreen mainScreen] scale]); - - CGContextRef ctx = UIGraphicsGetCurrentContext(); - - CGContextSetFillColorWithColor(ctx, [[color colorWithAlphaComponent:0.75] CGColor]); - CGContextFillRect(ctx, rect); - - CGContextSetStrokeColorWithColor(ctx, [[UIColor blackColor] CGColor]); - CGContextStrokeRectWithWidth(ctx, rect, 2); - - NSAttributedString *drawString = [[NSAttributedString alloc] initWithString:lastTwoCharacters attributes:@{ - NSFontAttributeName: [UIFont fontWithName:@"Arial-BoldMT" size:12], - NSForegroundColorAttributeName: [UIColor whiteColor] }]; - CGSize stringSize = drawString.size; - CGRect stringRect = CGRectMake((rect.size.width - stringSize.width) / 2, - (rect.size.height - stringSize.height) / 2, - stringSize.width, - stringSize.height); - [drawString drawInRect:stringRect]; - - image = [MGLAnnotationImage annotationImageWithImage:UIGraphicsGetImageFromCurrentImageContext() reuseIdentifier:lastTwoCharacters]; + UIColor *color; + + // make every tenth annotation blue + if ([lastTwoCharacters hasSuffix:@"0"]) { + color = [UIColor blueColor]; + } else { + color = [UIColor redColor]; + } + + UIImage *image = [self imageWithText:lastTwoCharacters backgroundColor:color]; + annotationImage = [MGLAnnotationImage annotationImageWithImage:image reuseIdentifier:lastTwoCharacters]; // don't allow touches on blue annotations - if ([color isEqual:[UIColor blueColor]]) image.enabled = NO; - - UIGraphicsEndImageContext(); + if ([color isEqual:[UIColor blueColor]]) annotationImage.enabled = NO; } + return annotationImage; +} + +- (UIImage *)imageWithText:(NSString *)text backgroundColor:(UIColor *)color +{ + CGRect rect = CGRectMake(0, 0, 20, 15); + + UIGraphicsBeginImageContextWithOptions(rect.size, NO, [[UIScreen mainScreen] scale]); + + CGContextRef ctx = UIGraphicsGetCurrentContext(); + + CGContextSetFillColorWithColor(ctx, [[color colorWithAlphaComponent:0.75] CGColor]); + CGContextFillRect(ctx, rect); + + CGContextSetStrokeColorWithColor(ctx, [[UIColor blackColor] CGColor]); + CGContextStrokeRectWithWidth(ctx, rect, 2); + + NSAttributedString *drawString = [[NSAttributedString alloc] initWithString:text attributes:@{ + NSFontAttributeName: [UIFont fontWithName:@"Arial-BoldMT" size:12], + NSForegroundColorAttributeName: [UIColor whiteColor], + }]; + CGSize stringSize = drawString.size; + CGRect stringRect = CGRectMake((rect.size.width - stringSize.width) / 2, + (rect.size.height - stringSize.height) / 2, + stringSize.width, + stringSize.height); + [drawString drawInRect:stringRect]; + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); return image; } +- (void)mapView:(MGLMapView *)mapView didDeselectAnnotation:(id)annotation { + NSString *title = [(MGLPointAnnotation *)annotation title]; + if ( ! title.length) + { + return; + } + NSString *lastTwoCharacters = [title substringFromIndex:title.length - 2]; + MGLAnnotationImage *annotationImage = [mapView dequeueReusableAnnotationImageWithIdentifier:lastTwoCharacters]; + annotationImage.image = annotationImage.image ? nil : [self imageWithText:lastTwoCharacters backgroundColor:[UIColor grayColor]]; +} + - (BOOL)mapView:(__unused MGLMapView *)mapView annotationCanShowCallout:(__unused id )annotation { return YES; diff --git a/platform/ios/src/MGLAnnotationImage.m b/platform/ios/src/MGLAnnotationImage.m index 374ed162fb9..e1085be98da 100644 --- a/platform/ios/src/MGLAnnotationImage.m +++ b/platform/ios/src/MGLAnnotationImage.m @@ -3,6 +3,8 @@ @interface MGLAnnotationImage () @property (nonatomic, strong) NSString *reuseIdentifier; +@property (nonatomic, strong, nullable) NSString *styleIconIdentifier; + @property (nonatomic, weak) id delegate; @end diff --git a/platform/ios/src/MGLAnnotationImage_Private.h b/platform/ios/src/MGLAnnotationImage_Private.h index f22a9ac4e20..dcd8a49bf91 100644 --- a/platform/ios/src/MGLAnnotationImage_Private.h +++ b/platform/ios/src/MGLAnnotationImage_Private.h @@ -11,6 +11,9 @@ NS_ASSUME_NONNULL_BEGIN @interface MGLAnnotationImage (Private) +/// Unique identifier of the sprite image used by the style to represent the receiver’s `image`. +@property (nonatomic, strong, nullable) NSString *styleIconIdentifier; + @property (nonatomic, weak) id delegate; @end diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 126328b5fdf..88872c1737e 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -136,9 +136,8 @@ typedef NS_ENUM(NSUInteger, MGLUserTrackingState) { class MGLAnnotationContext { public: id annotation; - /// mbgl-given identifier for the annotation image used by this annotation. - /// Based on the annotation image’s reusable identifier. - NSString *symbolIdentifier; + /// The annotation’s image’s reuse identifier. + NSString *imageReuseIdentifier; }; #pragma mark - Private - @@ -2379,12 +2378,18 @@ - (void)addAnnotation:(id )annotation } - (void)addAnnotations:(NS_ARRAY_OF(id ) *)annotations +{ + [self addAnnotations:annotations withAnnotationImage:nil]; +} + +- (void)addAnnotations:(NS_ARRAY_OF(id ) *)annotations withAnnotationImage:(MGLAnnotationImage *)explicitAnnotationImage { if ( ! annotations) return; [self willChangeValueForKey:@"annotations"]; std::vector points; std::vector shapes; + NSMutableArray *annotationImages = [NSMutableArray arrayWithCapacity:annotations.count]; BOOL delegateImplementsImageForPoint = [self.delegate respondsToSelector:@selector(mapView:imageForAnnotation:)]; @@ -2398,31 +2403,32 @@ - (void)addAnnotations:(NS_ARRAY_OF(id ) *)annotations } else { - MGLAnnotationImage *annotationImage = delegateImplementsImageForPoint ? [self.delegate mapView:self imageForAnnotation:annotation] : nil; + MGLAnnotationImage *annotationImage = explicitAnnotationImage; + if ( ! annotationImage && delegateImplementsImageForPoint) + { + annotationImage = [self.delegate mapView:self imageForAnnotation:annotation]; + } if ( ! annotationImage) { annotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName]; } if ( ! annotationImage) { - // Create a default annotation image that depicts a round pin - // rising from the center, with a shadow slightly below center. - // The alignment rect therefore excludes the bottom half. - UIImage *defaultAnnotationImage = [MGLMapView resourceImageNamed:MGLDefaultStyleMarkerSymbolName]; - defaultAnnotationImage = [defaultAnnotationImage imageWithAlignmentRectInsets: - UIEdgeInsetsMake(0, 0, defaultAnnotationImage.size.height / 2, 0)]; - annotationImage = [MGLAnnotationImage annotationImageWithImage:defaultAnnotationImage - reuseIdentifier:MGLDefaultStyleMarkerSymbolName]; + annotationImage = self.defaultAnnotationImage; + } + + NSString *symbolName = annotationImage.styleIconIdentifier; + if ( ! symbolName) + { + symbolName = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier]; + annotationImage.styleIconIdentifier = symbolName; } if ( ! self.annotationImagesByIdentifier[annotationImage.reuseIdentifier]) { - self.annotationImagesByIdentifier[annotationImage.reuseIdentifier] = annotationImage; [self installAnnotationImage:annotationImage]; - annotationImage.delegate = self; } - - NSString *symbolName = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier]; + [annotationImages addObject:annotationImage]; points.emplace_back(MGLLatLngFromLocationCoordinate2D(annotation.coordinate), symbolName ? [symbolName UTF8String] : ""); } @@ -2434,9 +2440,12 @@ - (void)addAnnotations:(NS_ARRAY_OF(id ) *)annotations for (size_t i = 0; i < pointAnnotationTags.size(); ++i) { + MGLAnnotationImage *annotationImage = annotationImages[i]; + annotationImage.styleIconIdentifier = @(points[i].icon.c_str()); + MGLAnnotationContext context; context.annotation = annotations[i]; - context.symbolIdentifier = @(points[i].icon.c_str()); + context.imageReuseIdentifier = annotationImage.reuseIdentifier; _annotationContextsByAnnotationTag[pointAnnotationTags[i]] = context; } } @@ -2456,6 +2465,20 @@ - (void)addAnnotations:(NS_ARRAY_OF(id ) *)annotations [self didChangeValueForKey:@"annotations"]; } +/// Initialize and return a default annotation image that depicts a round pin +/// rising from the center, with a shadow slightly below center. The alignment +/// rect therefore excludes the bottom half. +- (MGLAnnotationImage *)defaultAnnotationImage +{ + UIImage *image = [MGLMapView resourceImageNamed:MGLDefaultStyleMarkerSymbolName]; + image = [image imageWithAlignmentRectInsets: + UIEdgeInsetsMake(0, 0, image.size.height / 2, 0)]; + MGLAnnotationImage *annotationImage = [MGLAnnotationImage annotationImageWithImage:image + reuseIdentifier:MGLDefaultStyleMarkerSymbolName]; + annotationImage.styleIconIdentifier = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier]; + return annotationImage; +} + - (double)alphaForShapeAnnotation:(MGLShape *)annotation { if (_delegateHasAlphasForShapeAnnotations) @@ -2492,6 +2515,10 @@ - (CGFloat)lineWidthForPolylineAnnotation:(MGLPolyline *)annotation - (void)installAnnotationImage:(MGLAnnotationImage *)annotationImage { + NSString *iconIdentifier = annotationImage.styleIconIdentifier; + self.annotationImagesByIdentifier[annotationImage.reuseIdentifier] = annotationImage; + annotationImage.delegate = self; + // retrieve pixels CGImageRef image = annotationImage.image.CGImage; size_t width = CGImageGetWidth(image); @@ -2512,8 +2539,7 @@ - (void)installAnnotationImage:(MGLAnnotationImage *)annotationImage float(annotationImage.image.scale)); // sprite upload - NSString *symbolName = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier]; - _mbglMap->addAnnotationIcon(symbolName.UTF8String, cSpriteImage); + _mbglMap->addAnnotationIcon(iconIdentifier.UTF8String, cSpriteImage); // Create a slop area with a “radius” equal in size to the annotation // image’s alignment rect, allowing the eventual tap to be on any point @@ -2601,12 +2627,6 @@ - (void)removeOverlays:(NS_ARRAY_OF(id ) *)overlays - (nullable MGLAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:(NSString *)identifier { - // This prefix is used to avoid collisions with style-defined sprites in - // mbgl, but reusable identifiers are never prefixed. - if ([identifier hasPrefix:MGLAnnotationSpritePrefix]) - { - identifier = [identifier substringFromIndex:MGLAnnotationSpritePrefix.length]; - } return self.annotationImagesByIdentifier[identifier]; } @@ -2641,6 +2661,9 @@ - (MGLAnnotationTag)annotationTagAtPoint:(CGPoint)point persistingResults:(BOOL) -MGLAnnotationImagePaddingForHitTest, -MGLAnnotationImagePaddingForHitTest); + MGLAnnotationImage *fallbackAnnotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName]; + UIImage *fallbackImage = fallbackAnnotationImage.image; + // Filter out any annotation whose image is unselectable or for which // hit testing fails. std::remove_if(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag annotationTag) @@ -2654,10 +2677,11 @@ - (MGLAnnotationTag)annotationTagAtPoint:(CGPoint)point persistingResults:(BOOL) return true; } + UIImage *image = annotationImage.image ? annotationImage.image : fallbackImage; + // Filter out the annotation if the fattened finger didn’t land // within the image’s alignment rect. - CGRect annotationRect = [self frameOfImage:annotationImage.image - centeredAtCoordinate:annotation.coordinate]; + CGRect annotationRect = [self frameOfImage:image centeredAtCoordinate:annotation.coordinate]; return !!!CGRectIntersectsRect(annotationRect, hitRect); }); } @@ -2895,6 +2919,10 @@ - (CGRect)positioningRectForCalloutForAnnotationWithTag:(MGLAnnotationTag)annota } UIImage *image = [self imageOfAnnotationWithTag:annotationTag].image; if ( ! image) + { + image = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName].image; + } + if ( ! image) { return CGRectZero; } @@ -2923,7 +2951,7 @@ - (MGLAnnotationImage *)imageOfAnnotationWithTag:(MGLAnnotationTag)annotationTag return nil; } - NSString *customSymbol = _annotationContextsByAnnotationTag.at(annotationTag).symbolIdentifier; + NSString *customSymbol = _annotationContextsByAnnotationTag.at(annotationTag).imageReuseIdentifier; NSString *symbolName = customSymbol.length ? customSymbol : MGLDefaultStyleMarkerSymbolName; return [self dequeueReusableAnnotationImageWithIdentifier:symbolName]; @@ -2988,10 +3016,60 @@ - (void)showAnnotations:(NS_ARRAY_OF(id ) *)annotations edgePaddi - (void)annotationImageNeedsRedisplay:(MGLAnnotationImage *)annotationImage { - // remove sprite - NSString *symbolName = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier]; - _mbglMap->removeAnnotationIcon(symbolName.UTF8String); - [self installAnnotationImage:annotationImage]; + NSString *reuseIdentifier = annotationImage.reuseIdentifier; + NSString *iconIdentifier = annotationImage.styleIconIdentifier; + NSString *fallbackReuseIdentifier = MGLDefaultStyleMarkerSymbolName; + NSString *fallbackIconIdentifier = [MGLAnnotationSpritePrefix stringByAppendingString:fallbackReuseIdentifier]; + + // Remove the old icon from the style. + if ( ! [iconIdentifier isEqualToString:fallbackIconIdentifier]) { + _mbglMap->removeAnnotationIcon(iconIdentifier.UTF8String); + } + + if (annotationImage.image) + { + // Add the new icon to the style. + annotationImage.styleIconIdentifier = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier]; + [self installAnnotationImage:annotationImage]; + + if ([iconIdentifier isEqualToString:fallbackIconIdentifier]) + { + // Remove any annotations associated with the annotation image. + NSMutableArray *annotationsToRecreate = [NSMutableArray array]; + for (auto &pair : _annotationContextsByAnnotationTag) + { + if ([pair.second.imageReuseIdentifier isEqualToString:reuseIdentifier]) + { + [annotationsToRecreate addObject:pair.second.annotation]; + } + } + [self removeAnnotations:annotationsToRecreate]; + + // Recreate the annotations with the new icon. + [self addAnnotations:annotationsToRecreate withAnnotationImage:annotationImage]; + } + } + else + { + // Remove any annotations associated with the annotation image. + NSMutableArray *annotationsToRecreate = [NSMutableArray array]; + for (auto &pair : _annotationContextsByAnnotationTag) + { + if ([pair.second.imageReuseIdentifier isEqualToString:reuseIdentifier]) + { + [annotationsToRecreate addObject:pair.second.annotation]; + } + } + [self removeAnnotations:annotationsToRecreate]; + + // Recreate the annotations, falling back to the default icon. + annotationImage.styleIconIdentifier = fallbackIconIdentifier; + if ( ! [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName]) + { + [self installAnnotationImage:self.defaultAnnotationImage]; + } + [self addAnnotations:annotationsToRecreate withAnnotationImage:annotationImage]; + } _mbglMap->update(mbgl::Update::Annotations); }