From 5ab58deb44de2361bf7ed943c561cb4f68fac317 Mon Sep 17 00:00:00 2001 From: "Justin R. Miller" Date: Mon, 6 Jul 2015 18:08:16 -0700 Subject: [PATCH] Cocoa custom marker imagery API --- gyp/platform-ios.gypi | 2 + include/mbgl/ios/MGLAnnotationImage.h | 10 +++ include/mbgl/ios/MGLMapView.h | 21 +++++- include/mbgl/ios/MapboxGL.h | 1 + ios/app/MBXViewController.mm | 33 ++++++++++ platform/ios/MGLAnnotationImage.m | 30 +++++++++ platform/ios/MGLMapView.mm | 95 +++++++++++++++++++++++---- 7 files changed, 179 insertions(+), 13 deletions(-) create mode 100644 include/mbgl/ios/MGLAnnotationImage.h create mode 100644 platform/ios/MGLAnnotationImage.m diff --git a/gyp/platform-ios.gypi b/gyp/platform-ios.gypi index 52a66ea45..7baba06e5 100644 --- a/gyp/platform-ios.gypi +++ b/gyp/platform-ios.gypi @@ -50,6 +50,8 @@ '../platform/ios/MGLPolygon.m', '../include/mbgl/ios/MGLShape.h', '../platform/ios/MGLShape.m', + '../include/mbgl/ios/MGLAnnotationImage.h', + '../platform/ios/MGLAnnotationImage.m', '../platform/ios/NSBundle+MGLAdditions.h', '../platform/ios/NSBundle+MGLAdditions.m', '../platform/ios/NSException+MGLAdditions.h', diff --git a/include/mbgl/ios/MGLAnnotationImage.h b/include/mbgl/ios/MGLAnnotationImage.h new file mode 100644 index 000000000..142f794a0 --- /dev/null +++ b/include/mbgl/ios/MGLAnnotationImage.h @@ -0,0 +1,10 @@ +#import + +@interface MGLAnnotationImage : NSObject + +@property (nonatomic, readonly) UIImage *image; +@property (nonatomic, readonly) NSString *reuseIdentifier; + ++ (instancetype)annotationImageWithImage:(UIImage *)image reuseIdentifier:(NSString *)reuseIdentifier; + +@end diff --git a/include/mbgl/ios/MGLMapView.h b/include/mbgl/ios/MGLMapView.h index d0704a41c..db006f810 100644 --- a/include/mbgl/ios/MGLMapView.h +++ b/include/mbgl/ios/MGLMapView.h @@ -5,6 +5,7 @@ NS_ASSUME_NONNULL_BEGIN +@class MGLAnnotationImage; @class MGLUserLocation; @class MGLPolyline; @class MGLPolygon; @@ -258,6 +259,18 @@ IB_DESIGNABLE * @param annotations The array of annotations to remove. Objects in the array must conform to the MGLAnnotation protocol. */ - (void)removeAnnotations:(NS_ARRAY_OF(id ) *)annotations; +/** Returns a reusable annotation image object located by its identifier. +* +* For performance reasons, you should generally reuse MGLAnnotationImage objects for annotations in your map views. Dequeueing saves time and memory during performance-critical operations such as scrolling. +* +* @param identifier A string identifying the annotation image to be reused. This string is the same one you specify when initially returning the annotation image object using the mapView:imageForAnnotation: method. +* @return An annotation image object with the specified identifier, or `nil` if no such object exists in the reuse queue. */ +- (MGLAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:(NSString *)identifier; + +#pragma mark - Managing Annotation Selections + +/** @name Managing Annotation Selections */ + /** The annotations that are currently selected. * * Assigning a new array to this property selects only the first annotation in the array. */ @@ -336,7 +349,13 @@ IB_DESIGNABLE * @param mapView The map view that requested the annotation symbol name. * @param annotation The object representing the annotation that is about to be displayed. * @return The marker symbol to display for the specified annotation or `nil` if you want to display the default symbol. */ -- (nullable NSString *)mapView:(MGLMapView *)mapView symbolNameForAnnotation:(id )annotation; +- (nullable NSString *)mapView:(MGLMapView *)mapView symbolNameForAnnotation:(id )annotation __attribute__((unavailable("Use -mapView:imageForAnnotation:."))); + +/** Returns an image object to use for the marker for the specified point annotation object. +* @param mapView The map view that requested the annotation image. +* @param annotation The object representing the annotation that is about to be displayed. +* @return The image object to display for the specified annotation or `nil` if you want to display the default marker image. */ +- (nullable MGLAnnotationImage *)mapView:(MGLMapView *)mapView imageForAnnotation:(id )annotation; /** Returns the alpha value to use when rendering a shape annotation. Defaults to `1.0`. * @param mapView The map view rendering the shape annotation. diff --git a/include/mbgl/ios/MapboxGL.h b/include/mbgl/ios/MapboxGL.h index bcdc5359b..401a62e82 100644 --- a/include/mbgl/ios/MapboxGL.h +++ b/include/mbgl/ios/MapboxGL.h @@ -1,5 +1,6 @@ #import "MGLAccountManager.h" #import "MGLAnnotation.h" +#import "MGLAnnotationImage.h" #import "MGLGeometry.h" #import "MGLMapView.h" #import "MGLMultiPoint.h" diff --git a/ios/app/MBXViewController.mm b/ios/app/MBXViewController.mm index 00245828e..995ae9d44 100644 --- a/ios/app/MBXViewController.mm +++ b/ios/app/MBXViewController.mm @@ -340,6 +340,39 @@ mbgl::Settings_NSUserDefaults *settings = nullptr; #pragma mark - MGLMapViewDelegate +- (MGLAnnotationImage *)mapView:(MGLMapView * __nonnull)mapView imageForAnnotation:(id __nonnull)annotation +{ + if ([[((MGLPointAnnotation *)annotation) title] hasSuffix:@"7"]) + { + NSLog(@"iOS: %p annotation title ends in '7'; using default sprite", annotation); + + return nil; + } + + static NSString *identifier = @"red_square"; + + MGLAnnotationImage *image = [mapView dequeueReusableAnnotationImageWithIdentifier:identifier]; + + if ( ! image) + { + NSLog(@"iOS: creating sprite for %p / %@", annotation, identifier); + + CGRect rect = CGRectMake(0, 0, 20, 20); + UIGraphicsBeginImageContextWithOptions(rect.size, NO, [[UIScreen mainScreen] scale]); + CGContextRef ctx = UIGraphicsGetCurrentContext(); + CGContextSetFillColorWithColor(ctx, [[[UIColor redColor] colorWithAlphaComponent:0.25] CGColor]); + CGContextFillEllipseInRect(ctx, rect); + image = [MGLAnnotationImage annotationImageWithImage:UIGraphicsGetImageFromCurrentImageContext() reuseIdentifier:identifier]; + UIGraphicsEndImageContext(); + } + else + { + NSLog(@"iOS: reusing sprite for %p / %@", annotation, identifier); + } + + return image; +} + - (BOOL)mapView:(__unused MGLMapView *)mapView annotationCanShowCallout:(__unused id )annotation { return YES; diff --git a/platform/ios/MGLAnnotationImage.m b/platform/ios/MGLAnnotationImage.m new file mode 100644 index 000000000..e5052bc03 --- /dev/null +++ b/platform/ios/MGLAnnotationImage.m @@ -0,0 +1,30 @@ +#import "MGLAnnotationImage.h" + +@interface MGLAnnotationImage () + +@property (nonatomic) UIImage *image; +@property (nonatomic) NSString *reuseIdentifier; + +@end + +@implementation MGLAnnotationImage + ++ (instancetype)annotationImageWithImage:(UIImage *)image reuseIdentifier:(NSString *)reuseIdentifier +{ + return [[self alloc] initWithImage:image reuseIdentifier:reuseIdentifier]; +} + +- (instancetype)initWithImage:(UIImage *)image reuseIdentifier:(NSString *)reuseIdentifier +{ + self = [super init]; + + if (self) + { + _image = image; + _reuseIdentifier = [reuseIdentifier copy]; + } + + return self; +} + +@end diff --git a/platform/ios/MGLMapView.mm b/platform/ios/MGLMapView.mm index 9542d63d0..fb0705ef0 100644 --- a/platform/ios/MGLMapView.mm +++ b/platform/ios/MGLMapView.mm @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,7 @@ #import "SMCalloutView.h" #import +#import class MBGLView; @@ -46,6 +48,7 @@ const CGSize MGLAnnotationUpdateViewportOutset = {150, 150}; const CGFloat MGLMinimumZoom = 3; NSString *const MGLAnnotationIDKey = @"MGLAnnotationIDKey"; +NSString *const MGLAnnotationSymbolKey = @"MGLAnnotationSymbolKey"; static NSURL *MGLURLForBundledStyleNamed(NSString *styleName) { @@ -81,6 +84,7 @@ CLLocationDegrees MGLDegreesFromRadians(CGFloat radians) @property (nonatomic) UIRotationGestureRecognizer *rotate; @property (nonatomic) UILongPressGestureRecognizer *quickZoom; @property (nonatomic) NSMapTable *annotationIDsByAnnotation; +@property (nonatomic) NS_MUTABLE_DICTIONARY_OF(NSString *, MGLAnnotationImage *) *annotationImages; @property (nonatomic) std::vector annotationsNearbyLastTap; @property (nonatomic, weak) id selectedAnnotation; @property (nonatomic) SMCalloutView *selectedAnnotationCalloutView; @@ -275,6 +279,9 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) // setup annotations // _annotationIDsByAnnotation = [NSMapTable mapTableWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory]; + + _annotationImages = [NSMutableDictionary new]; + std::string defaultSymbolName([MGLDefaultStyleMarkerSymbolName UTF8String]); _mbglMap->setDefaultPointAnnotationSymbol(defaultSymbolName); @@ -1772,12 +1779,14 @@ CLLocationCoordinate2D MGLLocationCoordinate2DFromLatLng(mbgl::LatLng latLng) std::vector points; std::vector shapes; - BOOL delegateImplementsSymbolLookup = [self.delegate respondsToSelector:@selector(mapView:symbolNameForAnnotation:)]; + BOOL delegateImplementsImageForPoint = [self.delegate respondsToSelector:@selector(mapView:imageForAnnotation:)]; BOOL delegateImplementsAlphaForShape = [self.delegate respondsToSelector:@selector(mapView:alphaForShapeAnnotation:)]; BOOL delegateImplementsStrokeColorForShape = [self.delegate respondsToSelector:@selector(mapView:strokeColorForShapeAnnotation:)]; BOOL delegateImplementsFillColorForPolygon = [self.delegate respondsToSelector:@selector(mapView:fillColorForPolygonAnnotation:)]; BOOL delegateImplementsLineWidthForPolyline = [self.delegate respondsToSelector:@selector(mapView:lineWidthForPolylineAnnotation:)]; + const std::string spritePrefix = "com.mapbox.sprites."; + for (id annotation in annotations) { assert([annotation conformsToProtocol:@protocol(MGLAnnotation)]); @@ -1856,11 +1865,69 @@ CLLocationCoordinate2D MGLLocationCoordinate2DFromLatLng(mbgl::LatLng latLng) } else { + MGLAnnotationImage *annotationImage = nil; NSString *symbolName = nil; - if (delegateImplementsSymbolLookup) + if (delegateImplementsImageForPoint) { - symbolName = [self.delegate mapView:self symbolNameForAnnotation:annotation]; + annotationImage = [self.delegate mapView:self imageForAnnotation:annotation]; + + if (annotationImage) + { + const std::string cSymbolName = spritePrefix + std::string(annotationImage.reuseIdentifier.UTF8String); + symbolName = [NSString stringWithUTF8String:cSymbolName.c_str()]; + + if ( ! [self.annotationImages objectForKey:annotationImage.reuseIdentifier]) + { + // store image & symbol name + [self.annotationImages setObject:annotationImage forKey:annotationImage.reuseIdentifier]; + + // manually draw random-colored sprite for now + float pixelRatio = [[UIScreen mainScreen] scale]; + float width = 20; + float height = 20; + + const int r = 255 * (double(std::rand()) / RAND_MAX); + const int g = 255 * (double(std::rand()) / RAND_MAX); + const int b = 255 * (double(std::rand()) / RAND_MAX); + + const int w = std::ceil(pixelRatio * width); + const int h = std::ceil(pixelRatio * height); + + std::string pixels(w * h * 4, '\x00'); + auto data = reinterpret_cast(const_cast(pixels.data())); + const int dist = (w / 2) * (w / 2); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + const int dx = x - w / 2; + const int dy = y - h / 2; + const int diff = dist - (dx * dx + dy * dy); + if (diff > 0) { + const int a = std::min(0xFF, diff) * 0xFF / dist; + // Premultiply the rgb values with alpha + data[w * y + x] = + (a << 24) | ((a * r / 0xFF) << 16) | ((a * g / 0xFF) << 8) | (a * b / 0xFF); + } + } + } + + // add sprite + auto cSpriteImage = std::make_shared( + width, + height, + pixelRatio, + std::move(pixels)); + + // sprite upload + _mbglMap->setSprite(cSymbolName, cSpriteImage); + + NSLog(@"GL: uploaded sprite with name '%@'", [NSString stringWithUTF8String:cSymbolName.c_str()]); + } + } + else + { + NSLog(@"GL: using default symbol"); + } } points.emplace_back(MGLLatLngFromLocationCoordinate2D(annotation.coordinate), symbolName ? [symbolName UTF8String] : ""); @@ -1873,8 +1940,10 @@ CLLocationCoordinate2D MGLLocationCoordinate2DFromLatLng(mbgl::LatLng latLng) for (size_t i = 0; i < pointAnnotationIDs.size(); ++i) { - [self.annotationIDsByAnnotation setObject:@{ MGLAnnotationIDKey : @(pointAnnotationIDs[i]) } - forKey:annotations[i]]; + [self.annotationIDsByAnnotation setObject:@{ + MGLAnnotationIDKey : @(pointAnnotationIDs[i]), + MGLAnnotationSymbolKey : [NSString stringWithUTF8String:points[i].icon.c_str()] + } forKey:annotations[i]]; } } @@ -1925,6 +1994,11 @@ CLLocationCoordinate2D MGLLocationCoordinate2DFromLatLng(mbgl::LatLng latLng) _mbglMap->removeAnnotations(annotationIDsToRemove); } +- (MGLAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:(NSString *)identifier +{ + return [self.annotationImages objectForKey:identifier]; +} + - (NS_ARRAY_OF(id ) *)selectedAnnotations { return (self.selectedAnnotation ? @[ self.selectedAnnotation ] : @[]); @@ -1977,16 +2051,13 @@ CLLocationCoordinate2D MGLLocationCoordinate2DFromLatLng(mbgl::LatLng latLng) else { // determine symbol in use for point - NSString *symbol = MGLDefaultStyleMarkerSymbolName; - if ([self.delegate respondsToSelector:@selector(mapView:symbolNameForAnnotation:)]) - { - symbol = [self.delegate mapView:self symbolNameForAnnotation:annotation]; - } - std::string symbolName([symbol UTF8String]); + NSString *customSymbol = [[self.annotationIDsByAnnotation objectForKey:annotation] objectForKey:MGLAnnotationSymbolKey]; + NSString *symbolName = [customSymbol length] ? customSymbol : MGLDefaultStyleMarkerSymbolName; + std::string cSymbolName([symbolName UTF8String]); // determine anchor point based on symbol CGPoint calloutAnchorPoint = [self convertCoordinate:annotation.coordinate toPointToView:self]; - double y = _mbglMap->getTopOffsetPixelsForAnnotationSymbol(symbolName); + double y = _mbglMap->getTopOffsetPixelsForAnnotationSymbol(cSymbolName); calloutBounds = CGRectMake(calloutAnchorPoint.x - 1, calloutAnchorPoint.y + y, 0, 0); }