diff --git a/gyp/platform-ios.gypi b/gyp/platform-ios.gypi index 52a66ea45f4..7baba06e5cc 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/annotation/sprite_image.hpp b/include/mbgl/annotation/sprite_image.hpp new file mode 100644 index 00000000000..7d8ea0501c8 --- /dev/null +++ b/include/mbgl/annotation/sprite_image.hpp @@ -0,0 +1,40 @@ +#ifndef MBGL_ANNOTATIONS_SPRITE_IMAGE +#define MBGL_ANNOTATIONS_SPRITE_IMAGE + +#include +#include + +#include +#include +#include + +namespace mbgl { + +class SpriteImage : private util::noncopyable { +public: + SpriteImage( + uint16_t width, uint16_t height, float pixelRatio, std::string&& data, bool sdf = false); + + // Logical dimensions of the sprite image. + const uint16_t width; + const uint16_t height; + + // Pixel ratio of the sprite image. + const float pixelRatio; + + // Physical dimensions of the sprite image. + const uint16_t pixelWidth; + const uint16_t pixelHeight; + + // A string of an RGBA8 representation of the sprite. It must have exactly + // (width * ratio) * (height * ratio) * 4 (RGBA) bytes. The scan lines may + // not have gaps between them (i.e. stride == 0). + const std::string data; + + // Whether this image should be interpreted as a signed distance field icon. + const bool sdf; +}; + +} + +#endif diff --git a/include/mbgl/ios/MGLAnnotationImage.h b/include/mbgl/ios/MGLAnnotationImage.h new file mode 100644 index 00000000000..9707fca8f86 --- /dev/null +++ b/include/mbgl/ios/MGLAnnotationImage.h @@ -0,0 +1,16 @@ +#import + +#import "MGLTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MGLAnnotationImage : NSObject + +@property (nonatomic, readonly) UIImage *image; +@property (nonatomic, readonly) NSString *reuseIdentifier; + ++ (instancetype)annotationImageWithImage:(UIImage *)image reuseIdentifier:(NSString *)reuseIdentifier; + +@end + +NS_ASSUME_NONNULL_END diff --git a/include/mbgl/ios/MGLMapView.h b/include/mbgl/ios/MGLMapView.h index d0704a41c6a..900728034fd 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. */ +- (nullable 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 bcdc5359bf5..401a62e82e1 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/include/mbgl/map/map.hpp b/include/mbgl/map/map.hpp index e9d25bfe891..8178fab49cd 100644 --- a/include/mbgl/map/map.hpp +++ b/include/mbgl/map/map.hpp @@ -21,6 +21,7 @@ class View; class MapData; class MapContext; class StillImage; +class SpriteImage; class Transform; class PointAnnotation; class ShapeAnnotation; @@ -141,6 +142,10 @@ class Map : private util::noncopyable { AnnotationIDs getAnnotationsInBounds(const LatLngBounds&, const AnnotationType& = AnnotationType::Any); LatLngBounds getBoundsForAnnotations(const AnnotationIDs&); + // Sprites + void setSprite(const std::string&, std::shared_ptr); + void removeSprite(const std::string&); + // Memory void setSourceTileCacheSize(size_t); void onLowMemory(); diff --git a/include/mbgl/platform/default/glfw_view.hpp b/include/mbgl/platform/default/glfw_view.hpp index cb40db12481..f65565c0307 100644 --- a/include/mbgl/platform/default/glfw_view.hpp +++ b/include/mbgl/platform/default/glfw_view.hpp @@ -46,14 +46,18 @@ class GLFWView : public mbgl::View { private: mbgl::LatLng makeRandomPoint() const; + static std::shared_ptr + makeSpriteImage(int width, int height, float pixelRatio); void addRandomPointAnnotations(int count); void addRandomShapeAnnotations(int count); + void addRandomCustomPointAnnotations(int count); void clearAnnotations(); void popAnnotation(); mbgl::AnnotationIDs annotationIDs; + std::vector spriteIDs; private: bool fullscreen = false; diff --git a/include/mbgl/util/exception.hpp b/include/mbgl/util/exception.hpp index da61aa482ad..31fa693f8d9 100644 --- a/include/mbgl/util/exception.hpp +++ b/include/mbgl/util/exception.hpp @@ -11,6 +11,11 @@ struct Exception : std::runtime_error { inline Exception(const std::string &msg) : std::runtime_error(msg) {} }; +struct SpriteImageException : Exception { + inline SpriteImageException(const char *msg) : Exception(msg) {} + inline SpriteImageException(const std::string &msg) : Exception(msg) {} +}; + struct GlyphRangeLoadingException : Exception { inline GlyphRangeLoadingException(const char *msg) : Exception(msg) {} inline GlyphRangeLoadingException(const std::string &msg) : Exception(msg) {} diff --git a/include/mbgl/util/image.hpp b/include/mbgl/util/image.hpp index b2f70e14421..f58c2c09897 100644 --- a/include/mbgl/util/image.hpp +++ b/include/mbgl/util/image.hpp @@ -7,12 +7,12 @@ namespace mbgl { namespace util { -std::string compress_png(int width, int height, void *rgba); +std::string compress_png(int width, int height, const void *rgba); class Image { public: - Image(const std::string &img); + explicit Image(const std::string &img); inline const char *getData() const { return img.get(); } inline uint32_t getWidth() const { return width; } diff --git a/ios/app/MBXViewController.mm b/ios/app/MBXViewController.mm index 00245828ed3..e11f7eea5cc 100644 --- a/ios/app/MBXViewController.mm +++ b/ios/app/MBXViewController.mm @@ -340,6 +340,45 @@ - (void)dealloc #pragma mark - MGLMapViewDelegate +- (MGLAnnotationImage *)mapView:(MGLMapView * __nonnull)mapView imageForAnnotation:(id __nonnull)annotation +{ + NSString *title = [(MGLPointAnnotation *)annotation title]; + NSString *lastTwoCharacters = [title substringFromIndex:title.length - 2]; + + MGLAnnotationImage *image = [mapView dequeueReusableAnnotationImageWithIdentifier:lastTwoCharacters]; + + if ( ! image) + { + CGRect rect = CGRectMake(0, 0, 20, 15); + + UIGraphicsBeginImageContextWithOptions(rect.size, NO, [[UIScreen mainScreen] scale]); + + CGContextRef ctx = UIGraphicsGetCurrentContext(); + + CGContextSetFillColorWithColor(ctx, [[[UIColor redColor] 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]; + + UIGraphicsEndImageContext(); + } + + return image; +} + - (BOOL)mapView:(__unused MGLMapView *)mapView annotationCanShowCallout:(__unused id )annotation { return YES; diff --git a/platform/darwin/image.mm b/platform/darwin/image.mm index f6a8a6783c0..31373e9d151 100644 --- a/platform/darwin/image.mm +++ b/platform/darwin/image.mm @@ -11,7 +11,7 @@ namespace mbgl { namespace util { -std::string compress_png(int width, int height, void *rgba) { +std::string compress_png(int width, int height, const void *rgba) { CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, rgba, width * height * 4, NULL); if (!provider) { return ""; diff --git a/platform/default/glfw_view.cpp b/platform/default/glfw_view.cpp index 9ccf194f621..fb3bb1097ae 100644 --- a/platform/default/glfw_view.cpp +++ b/platform/default/glfw_view.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -120,6 +121,9 @@ void GLFWView::onKey(GLFWwindow *window, int key, int /*scancode*/, int action, case GLFW_KEY_Q: view->clearAnnotations(); break; + case GLFW_KEY_P: { + view->addRandomCustomPointAnnotations(1); + } break; } } @@ -150,6 +154,49 @@ mbgl::LatLng GLFWView::makeRandomPoint() const { return { lat, lon }; } +std::shared_ptr +GLFWView::makeSpriteImage(int width, int height, float pixelRatio) { + 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); + } + } + } + + return std::make_shared(width, height, pixelRatio, std::move(pixels)); +} + +void GLFWView::addRandomCustomPointAnnotations(int count) { + std::vector points; + + for (int i = 0; i < count; i++) { + static int spriteID = 1; + const auto name = std::string{ "marker-" } + std::to_string(spriteID++); + map->setSprite(name, makeSpriteImage(22, 22, 1)); + spriteIDs.push_back(name); + points.emplace_back(makeRandomPoint(), name); + } + + auto newIDs = map->addPointAnnotations(points); + annotationIDs.insert(annotationIDs.end(), newIDs.begin(), newIDs.end()); +} void GLFWView::addRandomPointAnnotations(int count) { std::vector points; diff --git a/platform/default/image.cpp b/platform/default/image.cpp index 12aea898c82..d7dcfc38b55 100644 --- a/platform/default/image.cpp +++ b/platform/default/image.cpp @@ -27,7 +27,7 @@ const static bool png_version_check = []() { namespace mbgl { namespace util { -std::string compress_png(int width, int height, void *rgba) { +std::string compress_png(int width, int height, const void *rgba) { png_voidp error_ptr = 0; png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, error_ptr, NULL, NULL); if (!png_ptr) { diff --git a/platform/ios/MGLAnnotationImage.m b/platform/ios/MGLAnnotationImage.m new file mode 100644 index 00000000000..e5052bc03ab --- /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 9542d63d07f..6f6a6d9ae1d 100644 --- a/platform/ios/MGLMapView.mm +++ b/platform/ios/MGLMapView.mm @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,7 @@ #include #include #include +#include #import "MapboxGL.h" @@ -33,6 +35,7 @@ #import "SMCalloutView.h" #import +#import class MBGLView; @@ -46,6 +49,7 @@ const CGFloat MGLMinimumZoom = 3; NSString *const MGLAnnotationIDKey = @"MGLAnnotationIDKey"; +NSString *const MGLAnnotationSymbolKey = @"MGLAnnotationSymbolKey"; static NSURL *MGLURLForBundledStyleNamed(NSString *styleName) { @@ -81,6 +85,7 @@ @interface MGLMapView () annotationsNearbyLastTap; @property (nonatomic, weak) id selectedAnnotation; @property (nonatomic) SMCalloutView *selectedAnnotationCalloutView; @@ -275,6 +280,9 @@ - (void)commonInit // setup annotations // _annotationIDsByAnnotation = [NSMapTable mapTableWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory]; + + _annotationImages = [NSMutableDictionary new]; + std::string defaultSymbolName([MGLDefaultStyleMarkerSymbolName UTF8String]); _mbglMap->setDefaultPointAnnotationSymbol(defaultSymbolName); @@ -1772,12 +1780,14 @@ - (void)addAnnotations:(NS_ARRAY_OF(id ) *)annotations 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 +1866,49 @@ - (void)addAnnotations:(NS_ARRAY_OF(id ) *)annotations } 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]; + + // retrieve pixels + CGImageRef image = annotationImage.image.CGImage; + size_t width = CGImageGetWidth(image); + size_t height = CGImageGetHeight(image); + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + std::string pixels(width * height * 4, '\0'); + size_t bytesPerPixel = 4; + size_t bytesPerRow = bytesPerPixel * width; + size_t bitsPerComponent = 8; + char *pixelData = const_cast(pixels.data()); + CGContextRef context = CGBitmapContextCreate(pixelData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast); + CGContextDrawImage(context, CGRectMake(0, 0, width, height), image); + CGContextRelease(context); + CGColorSpaceRelease(colorSpace); + + // add sprite + auto cSpriteImage = std::make_shared( + uint16_t(annotationImage.image.size.width), + uint16_t(annotationImage.image.size.height), + float(annotationImage.image.scale), + std::move(pixels)); + + // sprite upload + _mbglMap->setSprite(cSymbolName, cSpriteImage); + } + } } points.emplace_back(MGLLatLngFromLocationCoordinate2D(annotation.coordinate), symbolName ? [symbolName UTF8String] : ""); @@ -1873,8 +1921,10 @@ - (void)addAnnotations:(NS_ARRAY_OF(id ) *)annotations 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]]; } } @@ -1912,8 +1962,9 @@ - (void)removeAnnotations:(NS_ARRAY_OF(id ) *)annotations { assert([annotation conformsToProtocol:@protocol(MGLAnnotation)]); - annotationIDsToRemove.push_back([[[self.annotationIDsByAnnotation objectForKey:annotation] - objectForKey:MGLAnnotationIDKey] unsignedIntValue]); + NSDictionary *infoDictionary = [self.annotationIDsByAnnotation objectForKey:annotation]; + annotationIDsToRemove.push_back([[infoDictionary objectForKey:MGLAnnotationIDKey] unsignedIntValue]); + [self.annotationIDsByAnnotation removeObjectForKey:annotation]; if (annotation == self.selectedAnnotation) @@ -1925,6 +1976,11 @@ - (void)removeAnnotations:(NS_ARRAY_OF(id ) *)annotations _mbglMap->removeAnnotations(annotationIDsToRemove); } +- (MGLAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:(NSString *)identifier +{ + return [self.annotationImages objectForKey:identifier]; +} + - (NS_ARRAY_OF(id ) *)selectedAnnotations { return (self.selectedAnnotation ? @[ self.selectedAnnotation ] : @[]); @@ -1977,16 +2033,13 @@ - (void)selectAnnotation:(id )annotation animated:(BOOL)animated 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); } diff --git a/src/mbgl/annotation/sprite_image.cpp b/src/mbgl/annotation/sprite_image.cpp new file mode 100644 index 00000000000..e482d8f13fc --- /dev/null +++ b/src/mbgl/annotation/sprite_image.cpp @@ -0,0 +1,29 @@ +#include + +#include + +#include + +namespace mbgl { + +SpriteImage::SpriteImage(const uint16_t width_, + const uint16_t height_, + const float pixelRatio_, + std::string&& data_, + bool sdf_) + : width(width_), + height(height_), + pixelRatio(pixelRatio_), + pixelWidth(std::ceil(width * pixelRatio)), + pixelHeight(std::ceil(height * pixelRatio)), + data(std::move(data_)), + sdf(sdf_) { + const size_t size = pixelWidth * pixelHeight * 4; + if (size == 0) { + throw util::SpriteImageException("Sprite image dimensions may not be zero"); + } else if (size != data.size()) { + throw util::SpriteImageException("Sprite image pixel count mismatch"); + } +} + +} // namespace mbgl diff --git a/src/mbgl/annotation/sprite_parser.cpp b/src/mbgl/annotation/sprite_parser.cpp new file mode 100644 index 00000000000..0f9ba97c981 --- /dev/null +++ b/src/mbgl/annotation/sprite_parser.cpp @@ -0,0 +1,148 @@ +#include +#include + +#include + +#include + +#include + +#include +#include + +namespace mbgl { + +SpriteImagePtr createSpriteImage(const util::Image& image, + const uint16_t srcX, + const uint16_t srcY, + const uint16_t srcWidth, + const uint16_t srcHeight, + const double ratio, + const bool sdf) { + // Disallow invalid parameter configurations. + if (srcWidth == 0 || srcHeight == 0 || ratio <= 0 || ratio > 10 || srcWidth > 1024 || + srcHeight > 1024) { + Log::Warning(Event::Sprite, "Can't create sprite with invalid metrics"); + return nullptr; + } + + const uint16_t width = std::ceil(double(srcWidth) / ratio); + const uint16_t dstWidth = std::ceil(width * ratio); + assert(dstWidth >= srcWidth); + const uint16_t height = std::ceil(double(srcHeight) / ratio); + const uint16_t dstHeight = std::ceil(height * ratio); + assert(dstHeight >= srcHeight); + + std::string data(dstWidth * dstHeight * 4, '\0'); + + auto srcData = reinterpret_cast(image.getData()); + auto dstData = reinterpret_cast(const_cast(data.data())); + + const int32_t maxX = std::min(image.getWidth(), uint32_t(srcWidth + srcX)) - srcX; + assert(maxX <= int32_t(image.getWidth())); + const int32_t maxY = std::min(image.getHeight(), uint32_t(srcHeight + srcY)) - srcY; + assert(maxY <= int32_t(image.getHeight())); + + // Copy from the source image into our individual sprite image + for (uint16_t y = 0; y < maxY; ++y) { + const auto dstRow = y * dstWidth; + const auto srcRow = (y + srcY) * image.getWidth() + srcX; + for (uint16_t x = 0; x < maxX; ++x) { + dstData[dstRow + x] = srcData[srcRow + x]; + } + } + + return std::make_unique(width, height, ratio, std::move(data), sdf); +} + +namespace { + +inline uint16_t getUInt16(const rapidjson::Value& value, const char* name, const uint16_t def = 0) { + if (value.HasMember(name)) { + auto& v = value[name]; + if (v.IsUint() && v.GetUint() <= std::numeric_limits::max()) { + return v.GetUint(); + } else { + Log::Warning(Event::Sprite, "Value of '%s' must be an integer between 0 and 65535", + name); + } + } + + return def; +} + +inline double getDouble(const rapidjson::Value& value, const char* name, const double def = 0) { + if (value.HasMember(name)) { + auto& v = value[name]; + if (v.IsNumber()) { + return v.GetDouble(); + } else { + Log::Warning(Event::Sprite, "Value of '%s' must be a number", name); + } + } + + return def; +} + +inline bool getBoolean(const rapidjson::Value& value, const char* name, const bool def = false) { + if (value.HasMember(name)) { + auto& v = value[name]; + if (v.IsBool()) { + return v.GetBool(); + } else { + Log::Warning(Event::Sprite, "Value of '%s' must be a boolean", name); + } + } + + return def; +} + +} // namespace + +Sprites parseSprite(const std::string& image, const std::string& json) { + using namespace rapidjson; + + Sprites sprites; + + // Parse the sprite image. + const util::Image raster(image); + if (!raster) { + Log::Warning(Event::Sprite, "Could not parse sprite image"); + return sprites; + } + + Document doc; + doc.Parse<0>(json.c_str()); + + if (doc.HasParseError()) { + Log::Warning(Event::Sprite, std::string{ "Failed to parse JSON: " } + doc.GetParseError() + + " at offset " + std::to_string(doc.GetErrorOffset())); + return sprites; + } else if (!doc.IsObject()) { + Log::Warning(Event::Sprite, "Sprite JSON root must be an object"); + return sprites; + } else { + for (Value::ConstMemberIterator itr = doc.MemberBegin(); itr != doc.MemberEnd(); ++itr) { + const std::string name = { itr->name.GetString(), itr->name.GetStringLength() }; + const Value& value = itr->value; + + if (value.IsObject()) { + const uint16_t x = getUInt16(value, "x", 0); + const uint16_t y = getUInt16(value, "y", 0); + const uint16_t width = getUInt16(value, "width", 0); + const uint16_t height = getUInt16(value, "height", 0); + const double pixelRatio = getDouble(value, "pixelRatio", 1); + const bool sdf = getBoolean(value, "sdf", false); + + auto sprite = createSpriteImage(raster, x, y, width, height, pixelRatio, sdf); + if (sprite) { + sprites.emplace(name, sprite); + } + } + } + } + + return sprites; +} + +} // namespace mbgl diff --git a/src/mbgl/annotation/sprite_parser.hpp b/src/mbgl/annotation/sprite_parser.hpp new file mode 100644 index 00000000000..71228aacf67 --- /dev/null +++ b/src/mbgl/annotation/sprite_parser.hpp @@ -0,0 +1,39 @@ +#ifndef MBGL_ANNOTATIONS_SPRITE_PARSER +#define MBGL_ANNOTATIONS_SPRITE_PARSER + +#include +#include + +#include +#include +#include + +namespace mbgl { + +namespace util { + +class Image; + +} // namespace util + +class SpriteImage; + +using SpriteImagePtr = std::shared_ptr; + +// Extracts an individual image from a spritesheet from the given location. +SpriteImagePtr createSpriteImage(const util::Image& image, + uint16_t srcX, + uint16_t srcY, + uint16_t srcWidth, + uint16_t srcHeight, + double ratio, + bool sdf); + +using Sprites = std::map; + +// Parses an image and an associated JSON file and returns the sprite objects. +Sprites parseSprite(const std::string& image, const std::string& json); + +} // namespace mbgl + +#endif diff --git a/src/mbgl/annotation/sprite_store.cpp b/src/mbgl/annotation/sprite_store.cpp new file mode 100644 index 00000000000..1c6511d8e89 --- /dev/null +++ b/src/mbgl/annotation/sprite_store.cpp @@ -0,0 +1,69 @@ +#include + +#include + +namespace mbgl { + +void SpriteStore::setSprite(const std::string& name, std::shared_ptr sprite) { + std::lock_guard lock(mutex); + _setSprite(name, sprite); +} + +void SpriteStore::_setSprite(const std::string& name, + const std::shared_ptr& sprite) { + if (sprite) { + auto it = sprites.find(name); + if (it != sprites.end()) { + // There is already a sprite with that name in our store. + if ((it->second->width != sprite->width || it->second->height != sprite->height)) { + Log::Warning(Event::Sprite, "Can't change sprite dimensions for '%s'", name.c_str()); + return; + } + it->second = sprite; + } else { + sprites.emplace(name, sprite); + } + + // Always add/replace the value in the dirty list. + auto dirty_it = dirty.find(name); + if (dirty_it != dirty.end()) { + dirty_it->second = sprite; + } else { + dirty.emplace(name, sprite); + } + } else if (sprites.erase(name) > 0) { + dirty.emplace(name, nullptr); + } +} + +void SpriteStore::setSprites(const Sprites& newSprites) { + std::lock_guard lock(mutex); + for (const auto& pair : newSprites) { + _setSprite(pair.first, pair.second); + } +} + +void SpriteStore::removeSprite(const std::string& name) { + std::lock_guard lock(mutex); + _setSprite(name); +} + +std::shared_ptr SpriteStore::getSprite(const std::string& name) { + std::lock_guard lock(mutex); + const auto it = sprites.find(name); + if (it != sprites.end()) { + return it->second; + } else { + Log::Info(Event::Sprite, "Can't find sprite named '%s'", name.c_str()); + return nullptr; + } +} + +SpriteStore::Sprites SpriteStore::getDirty() { + Sprites result; + std::lock_guard lock(mutex); + dirty.swap(result); + return result; +} + +} // namespace mbgl diff --git a/src/mbgl/annotation/sprite_store.hpp b/src/mbgl/annotation/sprite_store.hpp new file mode 100644 index 00000000000..78e02ac6955 --- /dev/null +++ b/src/mbgl/annotation/sprite_store.hpp @@ -0,0 +1,53 @@ +#ifndef MBGL_ANNOTATION_SPRITE_STORE +#define MBGL_ANNOTATION_SPRITE_STORE + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace mbgl { + +// The SpriteStore object holds Sprite images. +class SpriteStore : private util::noncopyable { + using Sprites = std::map>; + +public: + // Adds/replaces a Sprite image. + void setSprite(const std::string&, std::shared_ptr = nullptr); + + // Adds/replaces mutliple Sprite images. + void setSprites(const Sprites& sprites); + + // Removes a Sprite. + void removeSprite(const std::string&); + + // Obtains a Sprite image. + std::shared_ptr getSprite(const std::string&); + + // Returns Sprite images that changed since the last invocation of this function. + Sprites getDirty(); + +private: + void _setSprite(const std::string&, const std::shared_ptr& = nullptr); + + // Lock for sprites and dirty maps. + std::mutex mutex; + + // Stores all current sprites. + Sprites sprites; + + // Stores all Sprite IDs that changed since the last invocation. + Sprites dirty; +}; + +} // namespace mbgl + +#endif diff --git a/src/mbgl/geometry/glyph_atlas.cpp b/src/mbgl/geometry/glyph_atlas.cpp index 067c5a40629..c2fc23bf9ce 100644 --- a/src/mbgl/geometry/glyph_atlas.cpp +++ b/src/mbgl/geometry/glyph_atlas.cpp @@ -176,7 +176,7 @@ void GlyphAtlas::upload() { dirty = false; #if defined(DEBUG) - // platform::showDebugImage("Glyph Atlas", data.get(), width, height); + // platform::showDebugImage("Glyph Atlas", reinterpret_cast(data.get()), width, height); #endif } } diff --git a/src/mbgl/geometry/sprite_atlas.cpp b/src/mbgl/geometry/sprite_atlas.cpp index 799f43b161a..743848d5f27 100644 --- a/src/mbgl/geometry/sprite_atlas.cpp +++ b/src/mbgl/geometry/sprite_atlas.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -18,50 +19,17 @@ using namespace mbgl; -SpriteAtlas::SpriteAtlas(dimension width_, dimension height_) +SpriteAtlas::SpriteAtlas(dimension width_, dimension height_, float pixelRatio_, SpriteStore& store_) : width(width_), height(height_), + pixelWidth(std::ceil(width * pixelRatio_)), + pixelHeight(std::ceil(height * pixelRatio_)), + pixelRatio(pixelRatio_), + store(store_), bin(width_, height_), + data(std::make_unique(pixelWidth * pixelHeight)), dirty(true) { -} - -bool SpriteAtlas::resize(const float newRatio) { - if (pixelRatio == newRatio) return false; - - std::lock_guard lock(mtx); - - const float oldRatio = pixelRatio; - pixelRatio = newRatio; - - if (data) { - const auto oldData = std::move(data); - allocate(); - - const int old_w = width * oldRatio; - const int old_h = height * oldRatio; - const int new_w = width * newRatio; - const int new_h = height * newRatio; - - // Basic image scaling. TODO: Replace this with better image scaling. - for (int y = 0; y < new_h; y++) { - const int old_yoffset = ((y * old_h) / new_h) * old_w; - const int new_yoffset = y * new_w; - for (int x = 0; x < new_w; x++) { - const int old_x = (x * old_w) / new_w; - data[new_yoffset + x] = oldData[old_yoffset + old_x]; - } - } - - dirty = true; - fullUploadRequired = true; - - // Mark all sprite images as in need of update - for (const auto &pair : images) { - uninitialized.emplace(pair.first); - } - } - - return dirty; + std::fill(data.get(), data.get() + pixelWidth * pixelHeight, 0); } Rect SpriteAtlas::allocateImage(const size_t pixel_width, const size_t pixel_height) { @@ -84,48 +52,48 @@ Rect SpriteAtlas::allocateImage(const size_t pixel_width return rect; } -Rect SpriteAtlas::getImage(const std::string& name, const bool wrap) { +SpriteAtlasElement SpriteAtlas::getImage(const std::string& name, const bool wrap) { std::lock_guard lock(mtx); - auto rect_it = images.find(name); + auto rect_it = images.find({ name, wrap }); if (rect_it != images.end()) { - return rect_it->second; + return { rect_it->second.pos, rect_it->second.texture }; } - const SpritePosition &pos = sprite->getSpritePosition(name); - if (!pos.width || !pos.height) { - return Rect { 0, 0, 0, 0 }; + auto sprite = store.getSprite(name); + if (!sprite) { + return { Rect { 0, 0, 0, 0 }, nullptr }; } - Rect rect = allocateImage(pos.width / pos.pixelRatio, pos.height / pos.pixelRatio); + Rect rect = allocateImage(sprite->width, sprite->height); if (rect.w == 0) { if (debug::spriteWarnings) { Log::Warning(Event::Sprite, "sprite atlas bitmap overflow"); } - return rect; + return { Rect { 0, 0, 0, 0 }, nullptr }; } - images.emplace(name, rect); + const Holder& holder = images.emplace(Key{ name, wrap }, Holder{ sprite, rect }).first->second; + copy(holder, wrap); - copy(rect, pos, wrap); - - return rect; + return { rect, sprite }; } SpriteAtlasPosition SpriteAtlas::getPosition(const std::string& name, bool repeating) { std::lock_guard lock(mtx); - Rect rect = getImage(name, repeating); + auto rect = getImage(name, repeating).pos; if (repeating) { // When the image is repeating, get the correct position of the image, rather than the // one rounded up to 4 pixels. - const SpritePosition &pos = sprite->getSpritePosition(name); - if (!pos.width || !pos.height) { + // TODO: Can't we just use originalW/originalH? + auto sprite = store.getSprite(name); + if (!sprite) { return SpriteAtlasPosition {}; } - rect.w = pos.width / pos.pixelRatio; - rect.h = pos.height / pos.pixelRatio; + rect.w = sprite->width; + rect.h = sprite->height; } const float padding = 1; @@ -137,33 +105,21 @@ SpriteAtlasPosition SpriteAtlas::getPosition(const std::string& name, bool repea }; } -void SpriteAtlas::allocate() { - if (!data) { - dimension w = static_cast(width * pixelRatio); - dimension h = static_cast(height * pixelRatio); - data = std::make_unique(w * h); - std::fill(data.get(), data.get() + w * h, 0); - } -} - -void SpriteAtlas::copy(const Rect& dst, const SpritePosition& src, const bool wrap) { - if (!sprite->raster) return; - - const uint32_t *srcData = reinterpret_cast(sprite->raster->getData()); +void SpriteAtlas::copy(const Holder& holder, const bool wrap) { + const uint32_t *srcData = reinterpret_cast(holder.texture->data.data()); if (!srcData) return; - const vec2 srcSize { sprite->raster->getWidth(), sprite->raster->getHeight() }; - const Rect srcPos { src.x, src.y, src.width, src.height }; + const vec2 srcSize { holder.texture->pixelWidth, holder.texture->pixelHeight }; + const Rect srcPos { 0, 0, srcSize.x, srcSize.y }; + const auto& dst = holder.pos; const int offset = 1; - allocate(); uint32_t *const dstData = data.get(); - const vec2 dstSize { static_cast(width * pixelRatio), - static_cast(height * pixelRatio) }; - const Rect dstPos { static_cast((offset + dst.x) * pixelRatio), - static_cast((offset + dst.y) * pixelRatio), - static_cast(dst.originalW * pixelRatio), - static_cast(dst.originalH * pixelRatio) }; + const vec2 dstSize{ pixelWidth, pixelHeight }; + const Rect dstPos{ static_cast((offset + dst.x) * pixelRatio), + static_cast((offset + dst.y) * pixelRatio), + static_cast(dst.originalW * pixelRatio), + static_cast(dst.originalH * pixelRatio) }; util::bilinearScale(srcData, srcSize, srcPos, dstData, dstSize, dstPos, wrap); @@ -201,38 +157,37 @@ void SpriteAtlas::copy(const Rect& dst, const SpritePosition& src, co dirty = true; } -void SpriteAtlas::setSprite(util::ptr sprite_) { - std::lock_guard lock(mtx); - - sprite = sprite_; +void SpriteAtlas::upload() { + if (dirty) { + bind(); + } +} - if (!sprite->isLoaded()) return; +void SpriteAtlas::updateDirty() { + auto dirtySprites = store.getDirty(); + if (dirtySprites.empty()) { + return; + } - util::erase_if(uninitialized, [this](const std::string &name) { - Rect dst = getImage(name, false); - const SpritePosition& src = sprite->getSpritePosition(name); - if (!src) { - if (debug::spriteWarnings) { - Log::Warning(Event::Sprite, "sprite doesn't have image with name '%s'", name.c_str()); - } - return true; - } + std::lock_guard lock(mtx); - if (src.width == dst.w * pixelRatio && src.height == dst.h * pixelRatio && src.pixelRatio == pixelRatio) { - copy(dst, src, false); - return true; + auto imageIterator = images.begin(); + auto spriteIterator = dirtySprites.begin(); + while (imageIterator != images.end() && spriteIterator != dirtySprites.end()) { + if (imageIterator->first.first < spriteIterator->first) { + ++imageIterator; + } else if (spriteIterator->first < imageIterator->first.first) { + ++spriteIterator; } else { - if (debug::spriteWarnings) { - Log::Warning(Event::Sprite, "sprite icon dimension mismatch"); - } - return false; + // The two names match; + Holder& holder = imageIterator->second; + holder.texture = spriteIterator->second; + copy(holder, imageIterator->first.second); + + ++imageIterator; + // Don't advance the spriteIterator because there might be another sprite with the same + // name, but a different wrap value. } - }); -} - -void SpriteAtlas::upload() { - if (dirty) { - bind(); } } @@ -261,15 +216,14 @@ void SpriteAtlas::bind(bool linear) { if (dirty) { std::lock_guard lock(mtx); - allocate(); if (fullUploadRequired) { MBGL_CHECK_ERROR(glTexImage2D( GL_TEXTURE_2D, // GLenum target 0, // GLint level GL_RGBA, // GLint internalformat - width * pixelRatio, // GLsizei width - height * pixelRatio, // GLsizei height + pixelWidth, // GLsizei width + pixelHeight, // GLsizei height 0, // GLint border GL_RGBA, // GLenum format GL_UNSIGNED_BYTE, // GLenum type @@ -282,8 +236,8 @@ void SpriteAtlas::bind(bool linear) { 0, // GLint level 0, // GLint xoffset 0, // GLint yoffset - width * pixelRatio, // GLsizei width - height * pixelRatio, // GLsizei height + pixelWidth, // GLsizei width + pixelHeight, // GLsizei height GL_RGBA, // GLenum format GL_UNSIGNED_BYTE, // GLenum type data.get() // const GLvoid *pixels @@ -293,7 +247,8 @@ void SpriteAtlas::bind(bool linear) { dirty = false; #ifndef GL_ES_VERSION_2_0 - // platform::showColorDebugImage("Sprite Atlas", reinterpret_cast(data), width * pixelRatio, height * pixelRatio, width * pixelRatio, height * pixelRatio); + // platform::showColorDebugImage("Sprite Atlas", reinterpret_cast(data.get()), + // pixelWidth, pixelHeight, pixelWidth, pixelHeight); #endif } }; @@ -305,3 +260,11 @@ SpriteAtlas::~SpriteAtlas() { texture = 0; } } + +SpriteAtlas::Holder::Holder(const std::shared_ptr& texture_, + const Rect& pos_) + : texture(texture_), pos(pos_) { +} + +SpriteAtlas::Holder::Holder(Holder&& h) : texture(std::move(h.texture)), pos(h.pos) { +} diff --git a/src/mbgl/geometry/sprite_atlas.hpp b/src/mbgl/geometry/sprite_atlas.hpp index d34013cd6cf..bfe4f843dae 100644 --- a/src/mbgl/geometry/sprite_atlas.hpp +++ b/src/mbgl/geometry/sprite_atlas.hpp @@ -15,7 +15,8 @@ namespace mbgl { -class Sprite; +class SpriteStore; +class SpriteImage; class SpritePosition; struct SpriteAtlasPosition { @@ -28,56 +29,68 @@ struct SpriteAtlasPosition { std::array br; }; +struct SpriteAtlasElement { + const Rect pos; + const std::shared_ptr texture; +}; + class SpriteAtlas : public util::noncopyable { public: typedef uint16_t dimension; - // Add way to construct this from another SpriteAtlas (e.g. with another pixelRatio) - SpriteAtlas(dimension width, dimension height); + SpriteAtlas(dimension width, dimension height, float pixelRatio, SpriteStore& store); ~SpriteAtlas(); - // Changes the pixel ratio. - bool resize(float newRatio); - - // Changes the source sprite. - void setSprite(util::ptr sprite); - // Returns the coordinates of an image that is sourced from the sprite image. // This getter attempts to read the image from the sprite if it is already loaded. // In that case, it copies it into the sprite atlas and returns the dimensions. // Otherwise, it returns a 0/0/0/0 rect. - Rect getImage(const std::string& name, const bool wrap); + // This function is used during bucket creation. + SpriteAtlasElement getImage(const std::string& name, const bool wrap); + // This function is used for getting the position during render time. SpriteAtlasPosition getPosition(const std::string& name, bool repeating = false); // Binds the atlas texture to the GPU, and uploads data if it is out of date. void bind(bool linear = false); + // Updates sprites in the atlas texture that may have changed in the source SpriteStore object. + void updateDirty(); + // Uploads the texture to the GPU to be available when we need it. This is a lazy operation; // the texture is only bound when the data is out of date (=dirty). void upload(); - inline float getWidth() const { return width; } - inline float getHeight() const { return height; } - inline float getTextureWidth() const { return width * pixelRatio; } - inline float getTextureHeight() const { return height * pixelRatio; } + inline dimension getWidth() const { return width; } + inline dimension getHeight() const { return height; } + inline dimension getTextureWidth() const { return pixelWidth; } + inline dimension getTextureHeight() const { return pixelHeight; } inline float getPixelRatio() const { return pixelRatio; } - - const dimension width = 0; - const dimension height = 0; + inline const uint32_t* getData() const { return data.get(); } private: - void allocate(); + const dimension width, height; + const dimension pixelWidth, pixelHeight; + const float pixelRatio; + + struct Holder : private util::noncopyable { + inline Holder(const std::shared_ptr&, const Rect&); + inline Holder(Holder&&); + std::shared_ptr texture; + const Rect pos; + }; + + using Key = std::pair; + Rect allocateImage(size_t width, size_t height); - void copy(const Rect& dst, const SpritePosition& src, const bool wrap); + void copy(const Holder& holder, const bool wrap); std::recursive_mutex mtx; - float pixelRatio = 1.0f; + SpriteStore& store; BinPack bin; - util::ptr sprite; - std::map> images; + std::map images; std::set uninitialized; - std::unique_ptr data; + const std::unique_ptr data; std::atomic dirty; bool fullUploadRequired = true; uint32_t texture = 0; diff --git a/src/mbgl/map/map.cpp b/src/mbgl/map/map.cpp index ca8303ec6e6..37cd3be335f 100644 --- a/src/mbgl/map/map.cpp +++ b/src/mbgl/map/map.cpp @@ -346,6 +346,17 @@ LatLngBounds Map::getBoundsForAnnotations(const std::vector& annotatio } +#pragma mark - Sprites + +void Map::setSprite(const std::string& name, std::shared_ptr sprite) { + context->invoke(&MapContext::setSprite, name, sprite); +} + +void Map::removeSprite(const std::string& name) { + setSprite(name, nullptr); +} + + #pragma mark - Toggles void Map::setDebug(bool value) { diff --git a/src/mbgl/map/map_context.cpp b/src/mbgl/map/map_context.cpp index 455539436fa..5b1825bff23 100644 --- a/src/mbgl/map/map_context.cpp +++ b/src/mbgl/map/map_context.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -12,6 +13,8 @@ #include #include +#include + #include #include #include @@ -96,6 +99,8 @@ void MapContext::setStyleURL(const std::string& url) { styleURL = url; styleJSON.clear(); + style = std::make_unique