diff --git a/platform/darwin/src/MLNMapSnapshotter.h b/platform/darwin/src/MLNMapSnapshotter.h index 3092bdd83..ba405f350 100644 --- a/platform/darwin/src/MLNMapSnapshotter.h +++ b/platform/darwin/src/MLNMapSnapshotter.h @@ -6,7 +6,13 @@ NS_ASSUME_NONNULL_BEGIN +@class MLNAnnotationImage; +@class MLNShape; +@class MLNPolyline; +@class MLNPolygon; + @protocol MLNMapSnapshotterDelegate; +@protocol MLNAnnotation; /** An overlay that is placed within a ``MLNMapSnapshot``. @@ -249,6 +255,35 @@ MLN_EXPORT */ - (instancetype)initWithOptions:(MLNMapSnapshotOptions *)options NS_DESIGNATED_INITIALIZER; +/** + Adds an annotation to the snapshotter. + + > Note: ``MLNMultiPolyline``, ``MLNMultiPolyline``, ``MLNMultiPolyline``, and + ``MLNPointCollection`` objects cannot be added to the snapshotter at this time. + Any multipoint, multipolyline, multipolygon, shape or point collection + object that is specified is silently ignored. + + @param annotation The annotation object to add to the receiver. This object + must conform to the ``MLNAnnotation`` protocol. The snapshotter retains the + annotation object. + */ +- (void)addAnnotation:(id)annotation; + +/** + Adds an array of annotations to the snapshotter. + + > Note: ``MLNMultiPolyline``, ``MLNMultiPolyline``, and ``MLNMultiPolyline`` objects + cannot be added to the snapshotter at this time. Nor can ``MLNMultiPoint`` + objects that are not instances of ``MLNPolyline`` or ``MLNPolyline``. Any + multipoint, multipolyline, multipolygon, or shape collection objects that + are specified are silently ignored. + + @param annotations An array of annotation objects. Each object in the array + must conform to the ``MLNAnnotation`` protocol. The snapshotter retains each + individual annotation object. + */ +- (void)addAnnotations:(NSArray> *)annotations; + /** Starts the snapshot creation and executes the specified block with the result. @@ -367,6 +402,89 @@ MLN_EXPORT - (void)mapSnapshotter:(MLNMapSnapshotter *)snapshotter didFailLoadingImageNamed:(NSString *)name; +/** + Returns an annotation image object to mark the given point annotation object on + the map. + + Implement this method to mark a point annotation with a static image. + + Static annotation images use less memory and draw more quickly than annotation + views. On the other hand, annotation views are compatible with UIKit, Core + Animation, and other Cocoa Touch frameworks. + + @param snapshotter The snapshotter that has just loaded a style. + @param annotation The object representing the annotation that is about to be + displayed. + @return The annotation image object to display for the given annotation or + `nil` if you want to display the default marker image or an annotation view. +*/ +- (nullable MLNAnnotationImage *)mapSnapshotter:(MLNMapSnapshotter *)snapshotter + imageForAnnotation:(id)annotation; + +/** + Returns the alpha value to use when rendering a shape annotation. + + A value of `0.0` results in a completely transparent shape. A value of `1.0`, + the default, results in a completely opaque shape. + + This method sets the opacity of an entire shape, inclusive of its stroke and + fill. To independently set the values for stroke or fill, specify an alpha + component in the color returned by `-mapSnapshotter:strokeColorForShapeAnnotation:` or + `-mapSnapshotter:fillColorForPolygonAnnotation:`. + + @param snapshotter The snapshotter that has just loaded a style. + @param annotation The annotation being rendered. + @return An alpha value between `0` and `1.0`. + */ +- (CGFloat)mapSnapshotter:(MLNMapSnapshotter *)snapshotter + alphaForShapeAnnotation:(MLNShape *)annotation; + +/** + Returns the color to use when rendering the outline of a shape annotation. + + The default stroke color is MLNColor.whiteColor. + + Opacity may be set by specifying an alpha component. The default alpha value is + `1.0` and results in a completely opaque stroke. + + @param snapshotter The snapshotter that has just loaded a style. + @param annotation The annotation being rendered. + @return A color to use for the shape outline. + + */ +- (MLNColor *)mapSnapshotter:(MLNMapSnapshotter *)snapshotter + strokeColorForShapeAnnotation:(MLNShape *)annotation; + +/** + Returns the color to use when rendering the fill of a polygon annotation. + + The default fill color is the MLNColor.whiteColor. + + Opacity may be set by specifying an alpha component. The default alpha value is + `1.0` and results in a completely opaque shape. + + @param snapshotter The snapshotter that has just loaded a style. + @param annotation The annotation being rendered. + @return The polygon’s interior fill color. + + */ +- (MLNColor *)mapSnapshotter:(MLNMapSnapshotter *)snapshotter + fillColorForPolygonAnnotation:(MLNPolygon *)annotation; + +/** + Returns the line width in points to use when rendering the outline of a + polyline annotation. + + By default, the polyline is outlined with a line `3.0` points wide. + + @param snapshotter The snapshotter that has just loaded a style. + @param annotation The annotation being rendered. + @return A line width for the polyline, measured in points. + + */ +- (CGFloat)mapSnapshotter:(MLNMapSnapshotter *)snapshotter + lineWidthForPolylineAnnotation:(MLNPolyline *)annotation; + @end NS_ASSUME_NONNULL_END diff --git a/platform/darwin/src/MLNMapSnapshotter.mm b/platform/darwin/src/MLNMapSnapshotter.mm index 6f632f369..b6fc746e2 100644 --- a/platform/darwin/src/MLNMapSnapshotter.mm +++ b/platform/darwin/src/MLNMapSnapshotter.mm @@ -18,11 +18,20 @@ #import "MLNRendererConfiguration.h" #import "MLNMapSnapshotter_Private.h" #import "MLNSettings_Private.h" +#import "MLNShape.h" +#import "MLNPolyline.h" +#import "MLNPolygon.h" +#import "MLNMultiPoint_Private.h" +#import "MLNShapeCollection.h" +#import "MLNPointCollection.h" +#import "MLNAnnotationImage_Private.h" #if TARGET_OS_IPHONE #import "UIImage+MLNAdditions.h" +#import "UIColor+MLNAdditions.h" #else #import "NSImage+MLNAdditions.h" +#import "NSColor+MLNAdditions.h" #import #import #import @@ -33,6 +42,7 @@ const CGPoint MLNLogoImagePosition = CGPointMake(8, 8); const CGFloat MLNSnapshotterMinimumPixelSize = 64; +NSString * const MLNSnapshotterAnnotationSpritePrefix = @"org.maplibre.sprites."; MLNImage *MLNAttributedSnapshot(mbgl::MapSnapshotter::Attributions attributions, MLNImage *mglImage, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn, MLNMapSnapshotOptions *options, MLNMapSnapshotOverlayHandler overlayHandler); MLNMapSnapshot *MLNSnapshotWithDecoratedImage(MLNImage *mglImage, MLNMapSnapshotOptions *options, mbgl::MapSnapshotter::Attributions attributions, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn, MLNMapSnapshotOverlayHandler overlayHandler, NSError * _Nullable *outError); @@ -224,7 +234,7 @@ private: @end -@interface MLNMapSnapshotter() +@interface MLNMapSnapshotter() @property (nonatomic) BOOL loading; @property (nonatomic) BOOL terminated; @end @@ -232,6 +242,8 @@ private: @implementation MLNMapSnapshotter { std::unique_ptr _mbglMapSnapshotter; std::unique_ptr _delegateHost; + NSMutableArray> *_annotationsToInstall; + NSMutableSet *_installedImages; } - (void)dealloc { @@ -251,6 +263,7 @@ private: self = [super init]; if (self) { self.options = options; + _installedImages = [NSMutableSet set]; #if TARGET_OS_IPHONE [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; #else @@ -266,6 +279,79 @@ private: self.terminated = YES; } +- (void)addAnnotation:(id)annotation +{ + MLNLogDebug(@"Adding annotation: %@", annotation); + if ( ! annotation) return; + return [self addAnnotations:@[annotation]]; +} + +- (void)addAnnotations:(NSArray> *)annotations { + MLNLogDebug(@"Adding: %lu annotations", annotations.count); + + if (!_annotationsToInstall) { + _annotationsToInstall = [NSMutableArray array]; + } + + [_annotationsToInstall addObjectsFromArray:annotations]; +} + +- (void)installAnnotations:(NSArray> *)annotations +{ + MLNLogDebug(@"install: %lu annotations", annotations.count); + if ( ! annotations) return; + + for (id annotation in annotations) + { + MLNAssert([annotation conformsToProtocol:@protocol(MLNAnnotation)], @"annotation should conform to MLNAnnotation"); + + if ([annotation isKindOfClass:[MLNMultiPoint class]]) + { + MLNMultiPoint *multiPoint = (MLNMultiPoint *)annotation; + if (!multiPoint.pointCount) { + continue; + } + _mbglMapSnapshotter->addAnnotation([multiPoint annotationObjectWithDelegate:self]); + } else if (! [annotation isKindOfClass:[MLNMultiPolyline class]] + && ![annotation isKindOfClass:[MLNMultiPolygon class]] + && ![annotation isKindOfClass:[MLNShapeCollection class]] + && ![annotation isKindOfClass:[MLNPointCollection class]]) { + NSString *symbolName; + NSValue *annotationValue = [NSValue valueWithNonretainedObject:annotation]; + MLNAnnotationImage *annotationImage; + + if ([self.delegate respondsToSelector:@selector(mapSnapshotter:imageForAnnotation:)]) { + annotationImage = [self.delegate mapSnapshotter:self imageForAnnotation:annotation]; + } + if (!annotationImage) { + // doesn't support no icon point for snapshotter + continue; + } + + symbolName = annotationImage.styleIconIdentifier; + if ( ! symbolName) + { + symbolName = [MLNSnapshotterAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier]; + annotationImage.styleIconIdentifier = symbolName; + } + + [self installAnnotationImage:annotationImage]; + + _mbglMapSnapshotter->addAnnotation(mbgl::SymbolAnnotation { + MLNPointFromLocationCoordinate2D(annotation.coordinate), + symbolName.UTF8String + }); + } + } +} + +- (void)installAnnotationImage:(MLNAnnotationImage *)annotationImage { + NSString *iconIdentifier = annotationImage.styleIconIdentifier; + if (![_installedImages containsObject:iconIdentifier]) { + _mbglMapSnapshotter->addAnnotationImage([annotationImage.image mgl_styleImageWithIdentifier:iconIdentifier]); + } +} + - (void)startWithCompletionHandler:(MLNMapSnapshotCompletionHandler)completion { MLNLogDebug(@"Starting withCompletionHandler: %@", completion); @@ -749,6 +835,11 @@ NSArray *MLNAttributionInfosFromAttributions(mbgl::MapSnap if (!MLNCoordinateBoundsIsEmpty(options.coordinateBounds)) { _mbglMapSnapshotter->setRegion(MLNLatLngBoundsFromCoordinateBounds(options.coordinateBounds)); } + + // Annotations + if (_annotationsToInstall) { + [self installAnnotations:_annotationsToInstall]; + } } - (MLNStyle *)style { @@ -758,4 +849,40 @@ NSArray *MLNAttributionInfosFromAttributions(mbgl::MapSnap return [[MLNStyle alloc] initWithRawStyle:&_mbglMapSnapshotter->getStyle() stylable:self]; } +- (double)alphaForShapeAnnotation:(MLNShape *)annotation +{ + if ([self.delegate respondsToSelector:@selector(mapSnapshotter:alphaForShapeAnnotation:)]) { + return [self.delegate mapSnapshotter:self alphaForShapeAnnotation:annotation]; + } + return 1.0; +} + +- (mbgl::Color)strokeColorForShapeAnnotation:(MLNShape *)annotation +{ + if ([self.delegate respondsToSelector:@selector(mapSnapshotter:strokeColorForShapeAnnotation:)]) { + MLNColor *color = [self.delegate mapSnapshotter:self strokeColorForShapeAnnotation:annotation]; + return color.mgl_color; + } + + return mbgl::Color::white(); +} + +- (mbgl::Color)fillColorForPolygonAnnotation:(MLNPolygon *)annotation +{ + if ([self.delegate respondsToSelector:@selector(mapSnapshotter:fillColorForPolygonAnnotation:)]){ + MLNColor *color = [self.delegate mapSnapshotter:self fillColorForPolygonAnnotation:annotation]; + return color.mgl_color; + } + return mbgl::Color::white(); +} + +- (CGFloat)lineWidthForPolylineAnnotation:(MLNPolyline *)annotation +{ + if ([self.delegate respondsToSelector:@selector(mapSnapshotter:lineWidthForPolylineAnnotation:)]) { + return [self.delegate mapSnapshotter:self lineWidthForPolylineAnnotation:annotation]; + } + + return 3.0; +} + @end diff --git a/platform/default/include/mbgl/map/map_snapshotter.hpp b/platform/default/include/mbgl/map/map_snapshotter.hpp index 5bcbf98ed..bb75a4170 100644 --- a/platform/default/include/mbgl/map/map_snapshotter.hpp +++ b/platform/default/include/mbgl/map/map_snapshotter.hpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include @@ -63,6 +65,9 @@ public: void setPadding(const mbgl::EdgeInsets&); mbgl::EdgeInsets getPadding() const; + void addAnnotationImage(std::unique_ptr); + void addAnnotation(const Annotation&); + style::Style& getStyle(); const style::Style& getStyle() const; diff --git a/platform/default/src/mbgl/map/map_snapshotter.cpp b/platform/default/src/mbgl/map/map_snapshotter.cpp index eb81c132f..a87c60080 100644 --- a/platform/default/src/mbgl/map/map_snapshotter.cpp +++ b/platform/default/src/mbgl/map/map_snapshotter.cpp @@ -210,6 +210,10 @@ public: mbgl::EdgeInsets getPadding() const { return regionInsets; } + void addAnnotationImage(std::unique_ptr image) { map.addAnnotationImage(std::move(image)); } + + void addAnnotation(const Annotation& annotation) { map.addAnnotation(annotation); } + void snapshot(MapSnapshotter::Callback callback) { if (!callback) { Log::Error(Event::General, "MapSnapshotter::Callback is not set"); @@ -366,6 +370,14 @@ mbgl::EdgeInsets MapSnapshotter::getPadding() const { return impl->getPadding(); } +void MapSnapshotter::addAnnotationImage(std::unique_ptr image) { + impl->addAnnotationImage(std::move(image)); +} + +void MapSnapshotter::addAnnotation(const Annotation& annotation) { + impl->addAnnotation(annotation); +} + style::Style& MapSnapshotter::getStyle() { return impl->getMap().getStyle(); } diff --git a/test/fixtures/map/map_snapshotter_annotation/expected.png b/test/fixtures/map/map_snapshotter_annotation/expected.png new file mode 100644 index 000000000..1d6e91958 Binary files /dev/null and b/test/fixtures/map/map_snapshotter_annotation/expected.png differ diff --git a/test/map/map_snapshotter.test.cpp b/test/map/map_snapshotter.test.cpp index 0ea31c5bb..907bd4a57 100644 --- a/test/map/map_snapshotter.test.cpp +++ b/test/map/map_snapshotter.test.cpp @@ -7,9 +7,18 @@ #include #include #include +#include using namespace mbgl; +static PremultipliedImage namedImage(const std::string& name) { + return decodeImage(util::read_file("test/fixtures/sprites/" + name + ".png")); +} + +static std::unique_ptr namedMarker(const std::string& name) { + return std::make_unique(name, namedImage(name), 1.0f); +} + class SnapshotterObserver final : public MapSnapshotterObserver { public: ~SnapshotterObserver() = default; @@ -137,3 +146,31 @@ TEST(MapSnapshotter, TEST_REQUIRES_SERVER(runtimeStyling)) { runLoop.run(); } + +TEST(MapSnapshotter, TEST_REQUIRES_SERVER(annotation)) { + util::RunLoop runLoop; + MapSnapshotter snapshotter(Size{256, 128}, 1.0f, ResourceOptions(), ClientOptions()); + snapshotter.setStyleURL("http://127.0.0.1:3000/online/style.json"); + snapshotter.addAnnotationImage(namedMarker("default_marker")); + snapshotter.addAnnotation(SymbolAnnotation{Point{0, 0}, "default_marker"}); + FillAnnotation fillAnnotation{Polygon{{{{0, 0}, {0, 45}, {45, 45}, {45, 0}}}}}; + fillAnnotation.color = Color::red(); + snapshotter.addAnnotation(fillAnnotation); + LineAnnotation lineAnnotation{LineString{{{0, 0}, {45, 45}, {30, 0}}}}; + lineAnnotation.color = Color::blue(); + lineAnnotation.width = {5}; + snapshotter.addAnnotation(lineAnnotation); + snapshotter.snapshot([&runLoop](std::exception_ptr ptr, + mbgl::PremultipliedImage image, + mbgl::MapSnapshotter::Attributions, + mbgl::MapSnapshotter::PointForFn, + mbgl::MapSnapshotter::LatLngForFn) { + EXPECT_EQ(nullptr, ptr); + EXPECT_EQ(256, image.size.width); + EXPECT_EQ(128, image.size.height); + test::checkImage("test/fixtures/map/map_snapshotter_annotation", image, 0.005, 0.1); + runLoop.stop(); + }); + + runLoop.run(); +}