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

Nine-part resizable images #182

Merged
merged 4 commits into from
Mar 11, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions platform/darwin/scripts/style-spec-overrides-v8.json
Original file line number Diff line number Diff line change
Expand Up @@ -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`."
},
Expand Down
8 changes: 6 additions & 2 deletions platform/darwin/src/MGLSymbolStyleLayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
99 changes: 99 additions & 0 deletions platform/darwin/test/MGLImageTests.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#import <XCTest/XCTest.h>
#import <Mapbox/Mapbox.h>

#if TARGET_OS_IPHONE
#import "UIImage+MGLAdditions.h"
#else
#import "NSImage+MGLAdditions.h"
#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;
}];
#endif

{
auto styleImage = [image mgl_styleImageWithIdentifier:@"box"];
XCTAssert(styleImage);
if (styleImage) {
XCTAssert(!styleImage->getContent());
XCTAssertFalse(styleImage->isSdf());

MGLImage *imageAfter = [[MGLImage alloc] initWithMGLStyleImage:*styleImage];
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];
#else
image.capInsets = NSEdgeInsetsZero;
#endif
{
auto styleImage = [image mgl_styleImageWithIdentifier:@"box"];
XCTAssert(styleImage);
if (styleImage) {
XCTAssert(!styleImage->getContent());

MGLImage *imageAfter = [[MGLImage alloc] initWithMGLStyleImage:*styleImage];
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);
}

MGLImage *imageAfter = [[MGLImage alloc] initWithMGLStyleImage:*styleImage];
XCTAssertEqual(imageAfter.capInsets.top, 1);
XCTAssertEqual(imageAfter.capInsets.left, 2);
XCTAssertEqual(imageAfter.capInsets.bottom, 3);
XCTAssertEqual(imageAfter.capInsets.right, 4);
}
}
}

@end
1 change: 1 addition & 0 deletions platform/ios/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
4 changes: 4 additions & 0 deletions platform/ios/ios.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -1075,6 +1076,7 @@
DA00FC8C1D5EEB0D009AABC8 /* MGLAttributionInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLAttributionInfo.h; sourceTree = "<group>"; };
DA00FC8D1D5EEB0D009AABC8 /* MGLAttributionInfo.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLAttributionInfo.mm; sourceTree = "<group>"; };
DA0CD58F1CF56F6A00A5F5A5 /* MGLFeatureTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = MGLFeatureTests.mm; path = ../../darwin/test/MGLFeatureTests.mm; sourceTree = "<group>"; };
DA0E9F3E2411AC9B007C75D4 /* MGLImageTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = MGLImageTests.mm; path = ../../darwin/test/MGLImageTests.mm; sourceTree = "<group>"; };
DA17BE2F1CC4BAC300402C41 /* MGLMapView_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapView_Private.h; sourceTree = "<group>"; };
DA1AC01B1E5B8774006DF1D6 /* lt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Localizable.strings; sourceTree = "<group>"; };
DA1AC0201E5B8917006DF1D6 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Foundation.stringsdict; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
38 changes: 36 additions & 2 deletions platform/ios/src/UIImage+MGLAdditions.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,12 +18,22 @@ - (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);
self = [self resizableImageWithCapInsets:capInsets resizingMode:UIImageResizingModeStretch];
}
}
CGImageRelease(image);
return self;
Expand All @@ -39,10 +53,30 @@ - (nullable instancetype)initWithMGLPremultipliedImage:(const mbgl::Premultiplie
}

- (std::unique_ptr<mbgl::style::Image>)mgl_styleImageWithIdentifier:(NSString *)identifier {
mbgl::style::ImageStretches stretchX = {{
self.capInsets.left / self.scale, (self.size.width - self.capInsets.right) / self.scale,
}};
mbgl::style::ImageStretches stretchY = {{
self.capInsets.top / self.scale, (self.size.height - self.capInsets.bottom) / self.scale,
}};

mbgl::optional<mbgl::style::ImageContent> imageContent;
if (!MGLEdgeInsetsIsZero(self.capInsets)) {
imageContent = (mbgl::style::ImageContent){
.left = static_cast<float>(self.capInsets.left * self.scale),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by the * self.scale here, compared with the / self.scale above. What's going on here? (The macOS version below has * in both)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-initWithMGLStyleImage: divides by the scale and -mgl_styleImageWithIdentifier: multiplies by the scale on both platforms.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see what you mean – you’re referring to the stretchX and stretchY variables. Yes, I think the division above is incorrect. The tests aren’t catching the mistake because they only assert on the image’s content rect, not its stretchable areas per se.

.top = static_cast<float>(self.capInsets.top * self.scale),
.right = static_cast<float>((self.size.width - self.capInsets.right) * self.scale),
.bottom = static_cast<float>((self.size.height - self.capInsets.bottom) * self.scale),
};
}

BOOL isTemplate = self.renderingMode == UIImageRenderingModeAlwaysTemplate;
return std::make_unique<mbgl::style::Image>([identifier UTF8String],
self.mgl_premultipliedImage,
float(self.scale), isTemplate);
static_cast<float>(self.scale),
isTemplate,
stretchX, stretchY,
imageContent);
}

- (mbgl::PremultipliedImage)mgl_premultipliedImage {
Expand Down
3 changes: 2 additions & 1 deletion platform/macos/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
26 changes: 26 additions & 0 deletions platform/macos/app/Assets.xcassets/ohio.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Binary file not shown.
23 changes: 20 additions & 3 deletions platform/macos/app/MapDocument.m
Original file line number Diff line number Diff line change
Expand Up @@ -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'"];
Expand All @@ -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"];
Expand Down
Loading