Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Commit

Permalink
[ios] Annotation image deletion
Browse files Browse the repository at this point in the history
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
  • Loading branch information
1ec5 committed Apr 19, 2016
1 parent 8703f1e commit bc9b52e
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 70 deletions.
1 change: 1 addition & 0 deletions platform/ios/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Mapbox welcomes participation and contributions from everyone. If you’d like
- Removed the `armv7s` slice from the SDK to reduce its size. iPhone 5 and iPhone 5c automatically use the `armv7` slice instead. ([#4641](https://github.com/mapbox/mapbox-gl-native/pull/4641))
- The user dot now moves smoothly between user location updates while user location tracking is disabled. ([#1582](https://github.com/mapbox/mapbox-gl-native/pull/1582))
- User location heading updates now resume properly when an app becomes active again. ([#4674](https://github.com/mapbox/mapbox-gl-native/pull/4674))
- Setting the `image` property of an MGLAnnotationImage to `nil` resets it to the default red pin image and reclaims resources that can be used to customize additional annotations. ([#3835](https://github.com/mapbox/mapbox-gl-native/pull/3835))
- Fixed an issue preventing KVO change notifications from being generated on MGLMapView’s `userTrackingMode` key path when `-setUserTrackingMode:animated:` is called. ([#4724](https://github.com/mapbox/mapbox-gl-native/pull/4724))
- Fixed a hang that could occur if the host application attempts to set user defaults on a background queue. ([#4745](https://github.com/mapbox/mapbox-gl-native/pull/4745))
- Added a `-reloadStyle:` action to MGLMapView to force a reload of the current style. ([#4728](https://github.com/mapbox/mapbox-gl-native/pull/4728))
Expand Down
92 changes: 55 additions & 37 deletions platform/ios/app/MBXViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -594,52 +594,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<MGLAnnotation>)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 <MGLAnnotation>)annotation
{
return YES;
Expand Down
2 changes: 1 addition & 1 deletion platform/ios/include/MGLAnnotationImage.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions platform/ios/src/MGLAnnotationImage.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
@interface MGLAnnotationImage ()

@property (nonatomic, strong) NSString *reuseIdentifier;
@property (nonatomic, strong, nullable) NSString *styleIconIdentifier;

@property (nonatomic, weak) id<MGLAnnotationImageDelegate> delegate;

@end
Expand Down
3 changes: 3 additions & 0 deletions platform/ios/src/MGLAnnotationImage_Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<MGLAnnotationImageDelegate> delegate;

@end
Expand Down
142 changes: 110 additions & 32 deletions platform/ios/src/MGLMapView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,8 @@ typedef NS_ENUM(NSUInteger, MGLUserTrackingState) {
class MGLAnnotationContext {
public:
id <MGLAnnotation> 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 -
Expand Down Expand Up @@ -2370,12 +2369,18 @@ - (void)addAnnotation:(id <MGLAnnotation>)annotation
}

- (void)addAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations
{
[self addAnnotations:annotations withAnnotationImage:nil];
}

- (void)addAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations withAnnotationImage:(MGLAnnotationImage *)explicitAnnotationImage
{
if ( ! annotations) return;
[self willChangeValueForKey:@"annotations"];

std::vector<mbgl::PointAnnotation> points;
std::vector<mbgl::ShapeAnnotation> shapes;
NSMutableArray *annotationImages = [NSMutableArray arrayWithCapacity:annotations.count];

BOOL delegateImplementsImageForPoint = [self.delegate respondsToSelector:@selector(mapView:imageForAnnotation:)];

Expand All @@ -2389,31 +2394,32 @@ - (void)addAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)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] : "");
}
Expand All @@ -2425,9 +2431,12 @@ - (void)addAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)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;
}
}
Expand All @@ -2447,6 +2456,20 @@ - (void)addAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)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)
Expand Down Expand Up @@ -2483,6 +2506,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);
Expand All @@ -2503,8 +2530,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
Expand Down Expand Up @@ -2596,12 +2622,6 @@ - (void)removeOverlays:(NS_ARRAY_OF(id <MGLOverlay>) *)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];
}

Expand Down Expand Up @@ -2636,6 +2656,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.
auto end = std::remove_if(nearbyAnnotations.begin(), nearbyAnnotations.end(),
Expand All @@ -2650,10 +2673,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);
});
nearbyAnnotations.resize(std::distance(nearbyAnnotations.begin(), end));
Expand Down Expand Up @@ -2904,6 +2928,10 @@ - (CGRect)positioningRectForCalloutForAnnotationWithTag:(MGLAnnotationTag)annota
}
UIImage *image = [self imageOfAnnotationWithTag:annotationTag].image;
if ( ! image)
{
image = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName].image;
}
if ( ! image)
{
return CGRectZero;
}
Expand Down Expand Up @@ -2932,7 +2960,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];
Expand Down Expand Up @@ -3026,10 +3054,60 @@ - (void)showAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)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);
}

Expand Down

0 comments on commit bc9b52e

Please sign in to comment.