From e0199a7c49d7e3ddb90b287bf4cf952fdf91dae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 5 Feb 2016 08:04:07 -0800 Subject: [PATCH] Annotation image deletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added an API for deleting unused annotation images’ images. When you nil out the image of an MGLAnnotationImage, MGLMapView deletes the sprite from the style and recreates any annotation associated with the MGLAnnotationImage instance; the MGLAnnotationImage’s falls back to SDK’s default annotation image. In iosapp, deselecting an annotation resets its image to the default; deselecting it again restores the image. ref #3185 --- include/mbgl/ios/MGLAnnotationImage.h | 2 +- ios/app/MBXViewController.mm | 93 +++++++----- platform/ios/src/MGLAnnotationImage.m | 2 + platform/ios/src/MGLAnnotationImage_Private.h | 3 + platform/ios/src/MGLMapView.mm | 142 ++++++++++++++---- 5 files changed, 172 insertions(+), 70 deletions(-) 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); }