diff --git a/platform/darwin/scripts/style-spec-overrides-v8.json b/platform/darwin/scripts/style-spec-overrides-v8.json index e478ccaf00..d3ecdae549 100644 --- a/platform/darwin/scripts/style-spec-overrides-v8.json +++ b/platform/darwin/scripts/style-spec-overrides-v8.json @@ -57,6 +57,9 @@ } } }, + "icon-text-fit": { + "doc": "The directions in which the icon stretches to fit around the text. If the icon image is a resizable image, the resizable areas may be stretched, while the cap insets are always drawn at the original scale." + }, "icon-text-fit-padding": { "doc": "Size of the additional area added to dimensions determined by `icon-text-fit`." }, diff --git a/platform/darwin/src/MGLSymbolStyleLayer.h b/platform/darwin/src/MGLSymbolStyleLayer.h index 9ca6628871..712ed55b84 100644 --- a/platform/darwin/src/MGLSymbolStyleLayer.h +++ b/platform/darwin/src/MGLSymbolStyleLayer.h @@ -102,7 +102,9 @@ typedef NS_ENUM(NSUInteger, MGLIconRotationAlignment) { }; /** - Scales the icon to fit around the associated text. + The directions in which the icon stretches to fit around the text. If the icon + image is a resizable image, the resizable areas may be stretched, while the cap + insets are always drawn at the original scale. Values of this type are used in the `MGLSymbolStyleLayer.iconTextFit` property. @@ -776,7 +778,9 @@ MGL_EXPORT @property (nonatomic, null_resettable) NSExpression *iconSize __attribute__((unavailable("Use iconScale instead."))); /** - Scales the icon to fit around the associated text. + The directions in which the icon stretches to fit around the text. If the icon + image is a resizable image, the resizable areas may be stretched, while the cap + insets are always drawn at the original scale. The default value of this property is an expression that evaluates to `none`. Set this property to `nil` to reset it to the default value. diff --git a/platform/darwin/test/MGLImageTests.mm b/platform/darwin/test/MGLImageTests.mm new file mode 100644 index 0000000000..6f3e1d1ebd --- /dev/null +++ b/platform/darwin/test/MGLImageTests.mm @@ -0,0 +1,133 @@ +#import +#import + +#if TARGET_OS_IPHONE + #import "UIImage+MGLAdditions.h" + #define MGLImageResizingModeTile UIImageResizingModeTile + #define MGLImageResizingModeStretch UIImageResizingModeStretch +#else + #import "NSImage+MGLAdditions.h" + #define MGLImageResizingModeTile NSImageResizingModeTile + #define MGLImageResizingModeStretch NSImageResizingModeStretch +#endif + +@interface MGLImageTests : XCTestCase + +@end + +@implementation MGLImageTests + +- (void)testStretching { +#if TARGET_OS_IPHONE + CGRect rect = CGRectMake(0, 0, 24, 24); + UIGraphicsBeginImageContextWithOptions(rect.size, NO, UIScreen.mainScreen.scale); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetStrokeColorWithColor(context, UIColor.blackColor.CGColor); + CGContextStrokeRectWithWidth(context, rect, 2); + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); +#else + NSImage *image = [NSImage imageWithSize:NSMakeSize(24, 24) flipped:NO drawingHandler:^BOOL(NSRect dstRect) { + // A little more fanciful than the iOS version, but we aren’t testing the actual contents of the image anyways. + NSRectEdge allSides[] = {NSMinYEdge, NSMaxXEdge, NSMaxYEdge, NSMinXEdge, NSMinYEdge, NSMaxXEdge}; + CGFloat grays[] = {NSBlack, NSBlack, NSWhite, NSWhite, NSDarkGray, NSDarkGray}; + dstRect = NSDrawTiledRects(dstRect, dstRect, allSides, grays, sizeof(grays) / sizeof(grays[0])); + [NSColor.grayColor set]; + NSRectFill(dstRect); + return YES; + }]; + image.resizingMode = NSImageResizingModeTile; +#endif + + { + auto styleImage = [image mgl_styleImageWithIdentifier:@"box"]; + XCTAssert(styleImage); + if (styleImage) { + XCTAssert(!styleImage->getContent()); + XCTAssertFalse(styleImage->isSdf()); + XCTAssertTrue(styleImage->getStretchX().empty()); + XCTAssertTrue(styleImage->getStretchY().empty()); + + MGLImage *imageAfter = [[MGLImage alloc] initWithMGLStyleImage:*styleImage]; + XCTAssertEqual(imageAfter.resizingMode, MGLImageResizingModeTile); + XCTAssertEqual(imageAfter.capInsets.top, 0); + XCTAssertEqual(imageAfter.capInsets.left, 0); + XCTAssertEqual(imageAfter.capInsets.bottom, 0); + XCTAssertEqual(imageAfter.capInsets.right, 0); + } + } + +#if TARGET_OS_IPHONE + image = [image resizableImageWithCapInsets:UIEdgeInsetsZero resizingMode:UIImageResizingModeStretch]; +#else + image.resizingMode = NSImageResizingModeStretch; + image.capInsets = NSEdgeInsetsZero; +#endif + { + auto styleImage = [image mgl_styleImageWithIdentifier:@"box"]; + XCTAssert(styleImage); + if (styleImage) { + auto scale = styleImage->getPixelRatio(); + XCTAssert(!styleImage->getContent()); + + auto stretchX = styleImage->getStretchX(); + XCTAssertEqual(stretchX.size(), 1UL); + if (!stretchX.empty()) { + XCTAssertEqual(stretchX.front(), mbgl::style::ImageStretch(0, 24 * scale)); + } + auto stretchY = styleImage->getStretchY(); + XCTAssertEqual(stretchY.size(), 1UL); + if (!stretchY.empty()) { + XCTAssertEqual(stretchY.front(), mbgl::style::ImageStretch(0, 24 * scale)); + } + + MGLImage *imageAfter = [[MGLImage alloc] initWithMGLStyleImage:*styleImage]; + XCTAssertEqual(imageAfter.resizingMode, MGLImageResizingModeStretch); + XCTAssertEqual(imageAfter.capInsets.top, 0); + XCTAssertEqual(imageAfter.capInsets.left, 0); + XCTAssertEqual(imageAfter.capInsets.bottom, 0); + XCTAssertEqual(imageAfter.capInsets.right, 0); + } + } + +#if TARGET_OS_IPHONE + image = [image resizableImageWithCapInsets:UIEdgeInsetsMake(1, 2, 3, 4)]; +#else + image.capInsets = NSEdgeInsetsMake(1, 2, 3, 4); +#endif + { + auto styleImage = [image mgl_styleImageWithIdentifier:@"box"]; + XCTAssert(styleImage); + if (styleImage) { + auto scale = styleImage->getPixelRatio(); + auto content = styleImage->getContent(); + XCTAssert(content); + if (content) { + XCTAssertEqual(content->top, 1 * scale); + XCTAssertEqual(content->left, 2 * scale); + XCTAssertEqual(content->bottom, 21 * scale); + XCTAssertEqual(content->right, 20 * scale); + } + + auto stretchX = styleImage->getStretchX(); + XCTAssertEqual(stretchX.size(), 1UL); + if (!stretchX.empty()) { + XCTAssertEqual(stretchX.front(), mbgl::style::ImageStretch(2 * scale, 20 * scale)); + } + auto stretchY = styleImage->getStretchY(); + XCTAssertEqual(stretchY.size(), 1UL); + if (!stretchY.empty()) { + XCTAssertEqual(stretchY.front(), mbgl::style::ImageStretch(1 * scale, 21 * scale)); + } + + MGLImage *imageAfter = [[MGLImage alloc] initWithMGLStyleImage:*styleImage]; + XCTAssertEqual(imageAfter.resizingMode, MGLImageResizingModeStretch); + XCTAssertEqual(imageAfter.capInsets.top, 1); + XCTAssertEqual(imageAfter.capInsets.left, 2); + XCTAssertEqual(imageAfter.capInsets.bottom, 3); + XCTAssertEqual(imageAfter.capInsets.right, 4); + } + } +} + +@end diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index 516a682199..88b42e2ade 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -9,6 +9,7 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT * Added the `in` expression function for testing whether a value is included in an array expression or whether a string is a substring of another string. Use this function in expressions in style JSON or with the `MGL_FUNCTION()` syntax in an `NSExpression` format string. ([#16162](https://github.com/mapbox/mapbox-gl-native/pull/16162)) * Added the `within` expression function for testing whether the evaluated feature lies within the given GeoJSON object. Use this function in expressions in style JSON or with the `MGL_FUNCTION()` syntax in an `NSExpression` format string. ([#16157](https://github.com/mapbox/mapbox-gl-native/pull/16157)) * Added the `MGLLineStyleLayer.lineSortKey` and `MGLFillStyleLayer.fillSortKey` properties. ([#179](https://github.com/mapbox/mapbox-gl-native-ios/pull/179), [#16194](https://github.com/mapbox/mapbox-gl-native/pull/16194), [#16220](https://github.com/mapbox/mapbox-gl-native/pull/16220)) +* The `MGLSymbolStyleLayer.iconTextFit` property now respects the cap insets of any [nine-part stretchable image](https://developer.apple.com/documentation/uikit/uiimage#1658362) passed into the `-[MGLStyle setImage:forName:]` method. You can define the stretchable area in Xcode’s asset catalog or by calling the `-[UIImage resizableImageWithCapInsets:]` method. ([#182](https://github.com/mapbox/mapbox-gl-native-ios/pull/182)) * The `-[MGLStyle localizeLabelsIntoLocale:]` and `-[NSExpression mgl_expressionLocalizedIntoLocale:]` methods can now localize text into Traditional Chinese and Vietnamese. ([#173](https://github.com/mapbox/mapbox-gl-native-ios/pull/173)) * Fixed an issue where an `MGLSymbolStyleLayer.lineDashPattern` value of `{1, 0}` resulted in hairline gaps. ([#16202](https://github.com/mapbox/mapbox-gl-native/pull/16202)) * Improved the performance of loading a style that has many style images. ([#16187](https://github.com/mapbox/mapbox-gl-native/pull/16187)) diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj index 50a1441aa2..7c7381fb7b 100644 --- a/platform/ios/ios.xcodeproj/project.pbxproj +++ b/platform/ios/ios.xcodeproj/project.pbxproj @@ -439,6 +439,7 @@ DA0BDD202407C12600DAA576 /* libmbgl-core.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F78399D235AA1E600D4D606 /* libmbgl-core.a */; }; DA0BDD212407C13000DAA576 /* libmbgl-core.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F78399D235AA1E600D4D606 /* libmbgl-core.a */; }; DA0CD5901CF56F6A00A5F5A5 /* MGLFeatureTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA0CD58F1CF56F6A00A5F5A5 /* MGLFeatureTests.mm */; }; + DA0E9F3F2411AC9B007C75D4 /* MGLImageTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA0E9F3E2411AC9B007C75D4 /* MGLImageTests.mm */; }; DA17BE301CC4BAC300402C41 /* MGLMapView_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = DA17BE2F1CC4BAC300402C41 /* MGLMapView_Private.h */; }; DA17BE311CC4BDAA00402C41 /* MGLMapView_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = DA17BE2F1CC4BAC300402C41 /* MGLMapView_Private.h */; }; DA1DC96A1CB6C6B7006E619F /* MBXCustomCalloutView.m in Sources */ = {isa = PBXBuildFile; fileRef = DA1DC9671CB6C6B7006E619F /* MBXCustomCalloutView.m */; }; @@ -1075,6 +1076,7 @@ DA00FC8C1D5EEB0D009AABC8 /* MGLAttributionInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLAttributionInfo.h; sourceTree = ""; }; DA00FC8D1D5EEB0D009AABC8 /* MGLAttributionInfo.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLAttributionInfo.mm; sourceTree = ""; }; DA0CD58F1CF56F6A00A5F5A5 /* MGLFeatureTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = MGLFeatureTests.mm; path = ../../darwin/test/MGLFeatureTests.mm; sourceTree = ""; }; + DA0E9F3E2411AC9B007C75D4 /* MGLImageTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = MGLImageTests.mm; path = ../../darwin/test/MGLImageTests.mm; sourceTree = ""; }; DA17BE2F1CC4BAC300402C41 /* MGLMapView_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapView_Private.h; sourceTree = ""; }; DA1AC01B1E5B8774006DF1D6 /* lt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Localizable.strings; sourceTree = ""; }; DA1AC0201E5B8917006DF1D6 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Foundation.stringsdict; sourceTree = ""; }; @@ -1858,6 +1860,7 @@ DD58A4C51D822BD000E1F038 /* MGLExpressionTests.mm */, DA0CD58F1CF56F6A00A5F5A5 /* MGLFeatureTests.mm */, DA2E885C1CC0382C00F24E7B /* MGLGeometryTests.mm */, + DA0E9F3E2411AC9B007C75D4 /* MGLImageTests.mm */, DA5DB1291FABF1EE001C2326 /* MGLMapAccessibilityElementTests.m */, DA695425215B1E75002041A4 /* MGLMapCameraTests.m */, 96E6145522CC135200109F14 /* MGLMapViewCompassViewTests.mm */, @@ -3014,6 +3017,7 @@ 357579801D501E09000B822E /* MGLFillStyleLayerTests.mm in Sources */, 35D9DDE21DA25EEC00DAAD69 /* MGLCodingTests.mm in Sources */, DA1F8F3D1EBD287B00367E42 /* MGLDocumentationGuideTests.swift in Sources */, + DA0E9F3F2411AC9B007C75D4 /* MGLImageTests.mm in Sources */, 076171C32139C70900668A35 /* MGLMapViewTests.m in Sources */, 3598544D1E1D38AA00B29F84 /* MGLDistanceFormatterTests.m in Sources */, 071BBB071EE77631001FB02A /* MGLImageSourceTests.m in Sources */, diff --git a/platform/ios/src/UIImage+MGLAdditions.mm b/platform/ios/src/UIImage+MGLAdditions.mm index e2f8ea09dc..19c000c6a5 100644 --- a/platform/ios/src/UIImage+MGLAdditions.mm +++ b/platform/ios/src/UIImage+MGLAdditions.mm @@ -5,6 +5,10 @@ const MGLExceptionName MGLResourceNotFoundException = @"MGLResourceNotFoundException"; +BOOL MGLEdgeInsetsIsZero(UIEdgeInsets edgeInsets) { + return edgeInsets.left == 0 && edgeInsets.top == 0 && edgeInsets.right == 0 && edgeInsets.bottom == 0; +} + @implementation UIImage (MGLAdditions) - (nullable instancetype)initWithMGLStyleImage:(const mbgl::style::Image &)styleImage @@ -14,12 +18,31 @@ - (nullable instancetype)initWithMGLStyleImage:(const mbgl::style::Image &)style return nil; } - if (self = [self initWithCGImage:image scale:styleImage.getPixelRatio() orientation:UIImageOrientationUp]) + CGFloat scale = styleImage.getPixelRatio(); + if (self = [self initWithCGImage:image scale:scale orientation:UIImageOrientationUp]) { if (styleImage.isSdf()) { self = [self imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; } + + if (auto content = styleImage.getContent()) + { + UIEdgeInsets capInsets = UIEdgeInsetsMake(content->top / scale, + content->left / scale, + self.size.height - content->bottom / scale, + self.size.width - content->right / scale); + UIImageResizingMode resizingMode = UIImageResizingModeTile; + if (!styleImage.getStretchX().empty() || !styleImage.getStretchY().empty()) + { + resizingMode = UIImageResizingModeStretch; + } + self = [self resizableImageWithCapInsets:capInsets resizingMode:resizingMode]; + } + if (!styleImage.getStretchX().empty() || !styleImage.getStretchY().empty()) + { + self = [self resizableImageWithCapInsets:self.capInsets resizingMode:UIImageResizingModeStretch]; + } } CGImageRelease(image); return self; @@ -39,10 +62,35 @@ - (nullable instancetype)initWithMGLPremultipliedImage:(const mbgl::Premultiplie } - (std::unique_ptr)mgl_styleImageWithIdentifier:(NSString *)identifier { + mbgl::style::ImageStretches stretchX, stretchY; + if (self.resizingMode == UIImageResizingModeStretch) + { + stretchX.push_back({ + self.capInsets.left * self.scale, (self.size.width - self.capInsets.right) * self.scale, + }); + stretchY.push_back({ + self.capInsets.top * self.scale, (self.size.height - self.capInsets.bottom) * self.scale, + }); + } + + mbgl::optional imageContent; + if (!MGLEdgeInsetsIsZero(self.capInsets)) + { + imageContent = (mbgl::style::ImageContent){ + .left = static_cast(self.capInsets.left * self.scale), + .top = static_cast(self.capInsets.top * self.scale), + .right = static_cast((self.size.width - self.capInsets.right) * self.scale), + .bottom = static_cast((self.size.height - self.capInsets.bottom) * self.scale), + }; + } + BOOL isTemplate = self.renderingMode == UIImageRenderingModeAlwaysTemplate; return std::make_unique([identifier UTF8String], self.mgl_premultipliedImage, - float(self.scale), isTemplate); + static_cast(self.scale), + isTemplate, + stretchX, stretchY, + imageContent); } - (mbgl::PremultipliedImage)mgl_premultipliedImage { diff --git a/platform/macos/CHANGELOG.md b/platform/macos/CHANGELOG.md index ee41770aa1..74be7d930c 100644 --- a/platform/macos/CHANGELOG.md +++ b/platform/macos/CHANGELOG.md @@ -10,7 +10,8 @@ * Added the `within` expression function for testing whether the evaluated feature lies within the given GeoJSON object. Use this function in expressions in style JSON or with the `MGL_FUNCTION()` syntax in an `NSExpression` format string. ([#16157](https://github.com/mapbox/mapbox-gl-native/pull/16157), [#16194](https://github.com/mapbox/mapbox-gl-native/pull/16194), [#16220](https://github.com/mapbox/mapbox-gl-native/pull/16220)) * Added the `MGLSymbolStyleLayer.textWritingModes` layout property. This property can be set to `MGLTextWritingModeHorizontal` or `MGLTextWritingModeVertical`. ([#14932](https://github.com/mapbox/mapbox-gl-native/pull/14932)) * Added the `MGLLineStyleLayer.lineSortKey` and `MGLFillStyleLayer.fillSortKey` properties. ([#179](https://github.com/mapbox/mapbox-gl-native-ios/pull/179)) -* The `MGLIdeographicFontFamilyName` Info.plist key now also accepts an array of font family names, to customize font fallback behavior. It can also be set to a Boolean value of `NO` to force the SDK to typeset CJK characters in a remote font specified by `MGLSymbolStyleLayer.textFontNames`. ([#14862](https://github.com/mapbox/mapbox-gl-native/pull/14862)) +* The `MGLIdeographicFontFamilyName` Info.plist key now also accepts an array of font family names, to customize font fallback behavior. It can also be set to a Boolean value of `NO` to force the SDK to typeset CJK characters in a remote font specified by `MGLSymbolStyleLayer.textFontNames`. ([#14862](https://github.com/mapbox/mapbox-gl-native/pull/14862)) +* The `MGLSymbolStyleLayer.iconTextFit` property now respects the cap insets of any nine-part stretchable image passed into the `-[MGLStyle setImage:forName:]` method. You can define the stretchable area in Xcode’s asset catalog or by setting the `NSImage.capInsets` property. ([#182](https://github.com/mapbox/mapbox-gl-native-ios/pull/182)) * The `-[MGLStyle localizeLabelsIntoLocale:]` and `-[NSExpression mgl_expressionLocalizedIntoLocale:]` methods can now localize text into Traditional Chinese and Vietnamese. ([#173](https://github.com/mapbox/mapbox-gl-native-ios/pull/173)) * Fixed crashes triggered when `MGLSource` and `MGLStyleLayer` objects are accessed after having been invalidated after a style change. ([#15539](https://github.com/mapbox/mapbox-gl-native/pull/15539)) * Fixed an issue where fill extrusion layers would be incorrectly rendered above other layers. ([#15065](https://github.com/mapbox/mapbox-gl-native/pull/15065)) diff --git a/platform/macos/app/Assets.xcassets/ohio.imageset/Contents.json b/platform/macos/app/Assets.xcassets/ohio.imageset/Contents.json new file mode 100644 index 0000000000..aa28df8d12 --- /dev/null +++ b/platform/macos/app/Assets.xcassets/ohio.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ohio.pdf", + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "stretch", + "width" : 15, + "height" : 18 + }, + "cap-insets" : { + "bottom" : 3, + "top" : 3, + "right" : 4, + "left" : 5 + } + } + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/platform/macos/app/Assets.xcassets/ohio.imageset/ohio.pdf b/platform/macos/app/Assets.xcassets/ohio.imageset/ohio.pdf new file mode 100644 index 0000000000..a09b2e0b46 Binary files /dev/null and b/platform/macos/app/Assets.xcassets/ohio.imageset/ohio.pdf differ diff --git a/platform/macos/app/MapDocument.m b/platform/macos/app/MapDocument.m index 1ab8b690b9..3431f4e258 100644 --- a/platform/macos/app/MapDocument.m +++ b/platform/macos/app/MapDocument.m @@ -1012,9 +1012,9 @@ - (IBAction)manipulateStyle:(id)sender { MGLSource *streetsSource = [self.mapView.style sourceWithIdentifier:@"composite"]; if (streetsSource) { - NSImage *image = [NSImage imageNamed:NSImageNameIChatTheaterTemplate]; - [self.mapView.style setImage:image forName:NSImageNameIChatTheaterTemplate]; - + NSImage *image = [NSImage imageNamed:NSImageNameIChatTheaterTemplate]; + [self.mapView.style setImage:image forName:NSImageNameIChatTheaterTemplate]; + MGLSymbolStyleLayer *theaterLayer = [[MGLSymbolStyleLayer alloc] initWithIdentifier:@"theaters" source:streetsSource]; theaterLayer.sourceLayerIdentifier = @"poi_label"; theaterLayer.predicate = [NSPredicate predicateWithFormat:@"maki == 'theatre'"]; @@ -1026,6 +1026,23 @@ - (IBAction)manipulateStyle:(id)sender { @20.0: [NSColor blackColor], }]; [self.mapView.style addLayer:theaterLayer]; + + NSImage *ohio = [NSImage imageNamed:@"ohio"]; + [self.mapView.style setImage:ohio forName:@"ohio"]; + + MGLSymbolStyleLayer *ohioLayer = [[MGLSymbolStyleLayer alloc] initWithIdentifier:@"ohio" source:streetsSource]; + ohioLayer.sourceLayerIdentifier = @"road"; + ohioLayer.predicate = [NSPredicate predicateWithFormat:@"shield = 'circle-white' and iso_3166_2 = 'US-OH'"]; + ohioLayer.symbolPlacement = [NSExpression expressionForConstantValue:@"line"]; + ohioLayer.text = [NSExpression expressionForKeyPath:@"ref"]; + ohioLayer.textFontNames = [NSExpression expressionWithFormat:@"{'DIN Offc Pro Bold', 'Arial Unicode MS Bold'}"]; + ohioLayer.textFontSize = [NSExpression expressionForConstantValue:@10]; + ohioLayer.textRotationAlignment = [NSExpression expressionForConstantValue:@"viewport"]; + ohioLayer.iconImageName = [NSExpression expressionForConstantValue:@"ohio"]; + ohioLayer.iconTextFit = [NSExpression expressionForConstantValue:@"both"]; + ohioLayer.iconTextFitPadding = [NSExpression expressionForConstantValue:[NSValue valueWithEdgeInsets:NSEdgeInsetsMake(1, 2, 1, 3)]]; + ohioLayer.iconRotationAlignment = [NSExpression expressionForConstantValue:@"viewport"]; + [self.mapView.style addLayer:ohioLayer]; } NSURL *imageURL = [NSURL URLWithString:@"https://www.mapbox.com/mapbox-gl-js/assets/radar.gif"]; diff --git a/platform/macos/macos.xcodeproj/project.pbxproj b/platform/macos/macos.xcodeproj/project.pbxproj index dd3cafecbb..03e18743ea 100644 --- a/platform/macos/macos.xcodeproj/project.pbxproj +++ b/platform/macos/macos.xcodeproj/project.pbxproj @@ -134,6 +134,7 @@ DA00FC8A1D5EEAC3009AABC8 /* MGLAttributionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = DA00FC881D5EEAC3009AABC8 /* MGLAttributionInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; DA00FC8B1D5EEAC3009AABC8 /* MGLAttributionInfo.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA00FC891D5EEAC3009AABC8 /* MGLAttributionInfo.mm */; }; DA0CD58E1CF56F5800A5F5A5 /* MGLFeatureTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA0CD58D1CF56F5800A5F5A5 /* MGLFeatureTests.mm */; }; + DA0E9F3B24119F6B007C75D4 /* MGLImageTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA0E9F3A24119F6B007C75D4 /* MGLImageTests.mm */; }; DA2784FE1DF03060001D5B8D /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DA2784FD1DF03060001D5B8D /* Media.xcassets */; }; DA29875A1E1A4290002299F5 /* MGLDocumentationExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2987591E1A4290002299F5 /* MGLDocumentationExampleTests.swift */; }; DA35A2A41CC9EB1A00E826B2 /* MGLCoordinateFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = DA35A2A31CC9EB1A00E826B2 /* MGLCoordinateFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -480,6 +481,7 @@ DA00FC881D5EEAC3009AABC8 /* MGLAttributionInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLAttributionInfo.h; sourceTree = ""; }; DA00FC891D5EEAC3009AABC8 /* MGLAttributionInfo.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLAttributionInfo.mm; sourceTree = ""; }; DA0CD58D1CF56F5800A5F5A5 /* MGLFeatureTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = MGLFeatureTests.mm; path = ../../darwin/test/MGLFeatureTests.mm; sourceTree = ""; }; + DA0E9F3A24119F6B007C75D4 /* MGLImageTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = MGLImageTests.mm; path = ../../darwin/test/MGLImageTests.mm; sourceTree = ""; }; DA1AC01E1E5B8826006DF1D6 /* lt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Localizable.strings; sourceTree = ""; }; DA1AC01F1E5B8904006DF1D6 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Foundation.stringsdict; sourceTree = ""; }; DA2207BA1DC076930002F84D /* test-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "test-Bridging-Header.h"; sourceTree = ""; }; @@ -1206,6 +1208,7 @@ DD58A4C71D822C6200E1F038 /* MGLExpressionTests.mm */, DA0CD58D1CF56F5800A5F5A5 /* MGLFeatureTests.mm */, DAE6C3C81CC34BD800DB3429 /* MGLGeometryTests.mm */, + DA0E9F3A24119F6B007C75D4 /* MGLImageTests.mm */, DA695423215B1E6C002041A4 /* MGLMapCameraTests.m */, 076171C4213A0DC200668A35 /* MGLMapViewTests.m */, 1F95931A1E6DE2B600D5B294 /* MGLNSDateAdditionsTests.mm */, @@ -1768,6 +1771,7 @@ DA695424215B1E6C002041A4 /* MGLMapCameraTests.m in Sources */, 920A3E591E6F859D00C16EFC /* MGLSourceQueryTests.m in Sources */, DA35A2B61CCA14D700E826B2 /* MGLCompassDirectionFormatterTests.m in Sources */, + DA0E9F3B24119F6B007C75D4 /* MGLImageTests.mm in Sources */, 35C6DF871E214C1800ACA483 /* MGLDistanceFormatterTests.m in Sources */, CAD9D0AC22A88A32001B25EE /* MGLResourceTests.mm in Sources */, DAE6C3D21CC34C9900DB3429 /* MGLGeometryTests.mm in Sources */, diff --git a/platform/macos/src/NSImage+MGLAdditions.mm b/platform/macos/src/NSImage+MGLAdditions.mm index a1671a276d..178cd17c74 100644 --- a/platform/macos/src/NSImage+MGLAdditions.mm +++ b/platform/macos/src/NSImage+MGLAdditions.mm @@ -2,6 +2,10 @@ #include +BOOL MGLEdgeInsetsIsZero(NSEdgeInsets edgeInsets) { + return edgeInsets.left == 0 && edgeInsets.top == 0 && edgeInsets.right == 0 && edgeInsets.bottom == 0; +} + @implementation NSImage (MGLAdditions) - (nullable instancetype)initWithMGLPremultipliedImage:(mbgl::PremultipliedImage&&)src { @@ -23,11 +27,23 @@ - (nullable instancetype)initWithMGLStyleImage:(const mbgl::style::Image &)style NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithCGImage:image]; CGImageRelease(image); - CGFloat w = styleImage.getImage().size.width / styleImage.getPixelRatio(); - CGFloat h = styleImage.getImage().size.height / styleImage.getPixelRatio(); - if (self = [self initWithSize:NSMakeSize(w, h)]) { + CGFloat scale = styleImage.getPixelRatio(); + NSSize size = NSMakeSize(styleImage.getImage().size.width / scale, + styleImage.getImage().size.height / scale); + if (self = [self initWithSize:size]) { [self addRepresentation:rep]; [self setTemplate:styleImage.isSdf()]; + if (!styleImage.getStretchX().empty() || !styleImage.getStretchY().empty()) { + self.resizingMode = NSImageResizingModeStretch; + } else { + self.resizingMode = NSImageResizingModeTile; + } + if (auto content = styleImage.getContent()) { + self.capInsets = NSEdgeInsetsMake(content->top / scale, + content->left / scale, + size.height - content->bottom / scale, + size.width - content->right / scale); + } } return self; } @@ -35,10 +51,34 @@ - (nullable instancetype)initWithMGLStyleImage:(const mbgl::style::Image &)style - (std::unique_ptr)mgl_styleImageWithIdentifier:(NSString *)identifier { mbgl::PremultipliedImage cPremultipliedImage = self.mgl_premultipliedImage; auto imageWidth = cPremultipliedImage.size.width; + + float scale = static_cast(imageWidth) / self.size.width; + mbgl::style::ImageStretches stretchX, stretchY; + if (self.resizingMode == NSImageResizingModeStretch) { + stretchX.push_back({ + self.capInsets.left * scale, (self.size.width - self.capInsets.right) * scale, + }); + stretchY.push_back({ + self.capInsets.top * scale, (self.size.height - self.capInsets.bottom) * scale, + }); + } + + mbgl::optional imageContent; + if (!MGLEdgeInsetsIsZero(self.capInsets)) { + imageContent = (mbgl::style::ImageContent){ + .left = static_cast(self.capInsets.left * scale), + .top = static_cast(self.capInsets.top * scale), + .right = static_cast((self.size.width - self.capInsets.right) * scale), + .bottom = static_cast((self.size.height - self.capInsets.bottom) * scale), + }; + } + return std::make_unique([identifier UTF8String], std::move(cPremultipliedImage), - (float)(imageWidth / self.size.width), - [self isTemplate]); + scale, + [self isTemplate], + stretchX, stretchY, + imageContent); } - (mbgl::PremultipliedImage)mgl_premultipliedImage {