From cef101c9b3b61d73975dddfd82134addcbc75a21 Mon Sep 17 00:00:00 2001 From: Mikhail Pozdnyakov Date: Tue, 26 Mar 2019 15:02:13 +0200 Subject: [darwin] Support for variable text placement API --- platform/darwin/scripts/generate-style-code.js | 18 +++- platform/darwin/src/MGLStyleValue_Private.h | 24 +---- platform/darwin/src/MGLSymbolStyleLayer.h | 60 +++++++++++- platform/darwin/src/MGLSymbolStyleLayer.mm | 37 ++++++++ platform/darwin/test/MGLSymbolStyleLayerTests.mm | 116 +++++++++++++++++++++++ 5 files changed, 228 insertions(+), 27 deletions(-) diff --git a/platform/darwin/scripts/generate-style-code.js b/platform/darwin/scripts/generate-style-code.js index a8bdec7865..1e7b2a3973 100755 --- a/platform/darwin/scripts/generate-style-code.js +++ b/platform/darwin/scripts/generate-style-code.js @@ -137,6 +137,8 @@ global.objCTestValue = function (property, layerType, arraysAsStructs, indent) { } return '@"{1, 1}"'; } + case 'anchor': + return `@"{'top','bottom'}"`; default: throw new Error(`unknown array type for ${property.name}`); } @@ -185,6 +187,8 @@ global.mbglTestValue = function (property, layerType) { case 'offset': case 'translate': return '{ 1, 1 }'; + case 'anchor': + return '{ mbgl::style::SymbolAnchorType::Top, mbgl::style::SymbolAnchorType::Bottom }'; default: throw new Error(`unknown array type for ${property.name}`); } @@ -200,6 +204,13 @@ global.mbglExpressionTestValue = function (property, layerType) { return `"${_.last(_.keys(property.values))}"`; case 'color': return 'mbgl::Color(1, 0, 0, 1)'; + case 'array': + switch (arrayType(property)) { + case 'anchor': + return `{"top", "bottom"}`; + default: + break; + } default: return global.mbglTestValue(property, layerType); } @@ -539,11 +550,12 @@ global.propertyType = function (property) { case 'font': return 'NSArray *'; case 'padding': - return 'NSValue *'; case 'position': case 'offset': case 'translate': return 'NSValue *'; + case 'anchor': + return 'NSArray *'; default: throw new Error(`unknown array type for ${property.name}`); } @@ -588,6 +600,8 @@ global.valueTransformerArguments = function (property) { case 'offset': case 'translate': return ['std::array', objCType]; + case 'anchor': + return ['std::vector', objCType, 'mbgl::style::SymbolAnchorType', 'MGLTextAnchor']; default: throw new Error(`unknown array type for ${property.name}`); } @@ -637,6 +651,8 @@ global.mbglType = function(property) { return 'std::array'; case 'position': return 'mbgl::style::Position'; + case 'anchor': + return 'std::vector'; default: throw new Error(`unknown array type for ${property.name}`); } diff --git a/platform/darwin/src/MGLStyleValue_Private.h b/platform/darwin/src/MGLStyleValue_Private.h index 84d7ccccec..fee34b4b71 100644 --- a/platform/darwin/src/MGLStyleValue_Private.h +++ b/platform/darwin/src/MGLStyleValue_Private.h @@ -307,10 +307,8 @@ private: // Private utilities for converting from mbgl to mgl values // Enumerations template - static NSValue *toMGLRawStyleValue(const MBGLEnum &value) { - auto str = mbgl::Enum::toString(value); - MGLEnum mglType = *mbgl::Enum::toEnum(str); - return [NSValue value:&mglType withObjCType:@encode(MGLEnum)]; + static NSString *toMGLRawStyleValue(const MBGLEnum &value) { + return @(mbgl::Enum::toString(value)); } /// Converts all types of mbgl property values into an equivalent NSExpression. @@ -320,15 +318,6 @@ private: // Private utilities for converting from mbgl to mgl values return nil; } - /** - As hack to allow converting enum => string values, we accept a second, dummy parameter in - the toRawStyleSpecValue() methods for converting 'atomic' (non-style-function) values. - This allows us to use `std::enable_if` to test (at compile time) whether or not MBGLType is an Enum. - */ - template ::value>::type, - typename MGLEnum = ObjCEnum, - class = typename std::enable_if::value>::type> NSExpression *operator()(const MBGLType &value) const { id constantValue = toMGLRawStyleValue(value); if ([constantValue isKindOfClass:[NSArray class]]) { @@ -337,15 +326,6 @@ private: // Private utilities for converting from mbgl to mgl values return [NSExpression expressionForConstantValue:constantValue]; } - template ::value>::type, - typename MGLEnum = ObjCEnum, - class = typename std::enable_if::value>::type> - NSExpression *operator()(const MBGLEnum &value) const { - NSString *constantValue = @(mbgl::Enum::toString(value)); - return [NSExpression expressionForConstantValue:constantValue]; - } - NSExpression *operator()(const mbgl::style::PropertyExpression &mbglValue) const { return [NSExpression expressionWithMGLJSONObject:MGLJSONObjectFromMBGLExpression(mbglValue.getExpression())]; } diff --git a/platform/darwin/src/MGLSymbolStyleLayer.h b/platform/darwin/src/MGLSymbolStyleLayer.h index ee8afb1fb2..ad792880e7 100644 --- a/platform/darwin/src/MGLSymbolStyleLayer.h +++ b/platform/darwin/src/MGLSymbolStyleLayer.h @@ -222,6 +222,10 @@ typedef NS_ENUM(NSUInteger, MGLTextAnchor) { property. */ typedef NS_ENUM(NSUInteger, MGLTextJustification) { + /** + The text is aligned towards the anchor position. + */ + MGLTextJustificationAuto, /** The text is aligned to the left. */ @@ -1279,6 +1283,7 @@ MGL_EXPORT * Constant `MGLTextJustification` values * Any of the following constant string values: + * `auto`: The text is aligned towards the anchor position. * `left`: The text is aligned to the left. * `center`: The text is centered. * `right`: The text is aligned to the right. @@ -1349,8 +1354,8 @@ MGL_EXPORT `NSValue` object containing a `CGVector` struct set to 0 ems rightward and 0 ems downward. Set this property to `nil` to reset it to the default value. - This property is only applied to the style if `text` is non-`nil`. Otherwise, - it is ignored. + This property is only applied to the style if `text` is non-`nil`, and + `textRadialOffset` is set to `nil`. Otherwise, it is ignored. You can set this property to an expression containing any of the following: @@ -1372,8 +1377,8 @@ MGL_EXPORT `NSValue` object containing a `CGVector` struct set to 0 ems rightward and 0 ems upward. Set this property to `nil` to reset it to the default value. - This property is only applied to the style if `text` is non-`nil`. Otherwise, - it is ignored. + This property is only applied to the style if `text` is non-`nil`, and + `textRadialOffset` is set to `nil`. Otherwise, it is ignored. You can set this property to an expression containing any of the following: @@ -1463,6 +1468,27 @@ MGL_EXPORT */ @property (nonatomic, null_resettable) NSExpression *textPitchAlignment; +/** + Radial offset of text, in the direction of the symbol's anchor. Useful in + combination with `textVariableAnchor`, which doesn't support the + two-dimensional `textOffset`. + + This property is measured in ems. + + This property is only applied to the style if `textOffset` is set to `nil`. + Otherwise, it is ignored. + + You can set this property to an expression containing any of the following: + + * Constant numeric values + * Predefined functions, including mathematical and string operators + * Conditional expressions + * Variable assignments and references to assigned variables + * Interpolation and step functions applied to the `$zoomLevel` variable and/or + feature attributes + */ +@property (nonatomic, null_resettable) NSExpression *textRadialOffset; + /** Rotates the text clockwise. @@ -1549,6 +1575,32 @@ MGL_EXPORT */ @property (nonatomic, null_resettable) NSExpression *textTransform; +/** + To increase the chance of placing high-priority labels on the map, you can + provide an array of `textAnchor` locations: the render will attempt to place + the label at each location, in order, before moving onto the next label. Use + `textJustify: auto` to choose justification based on anchor position. To apply + an offset, use the `textRadialOffset` instead of the two-dimensional + `textOffset`. + + This property is only applied to the style if `textAnchor` is set to `nil`, and + `textOffset` is set to `nil`, and `symbolPlacement` is set to an expression + that evaluates to or `MGLSymbolPlacementPoint`. Otherwise, it is ignored. + + You can set this property to an expression containing any of the following: + + * Constant array values + * Predefined functions, including mathematical and string operators + * Conditional expressions + * Variable assignments and references to assigned variables + * Step functions applied to the `$zoomLevel` variable + + This property does not support applying interpolation functions to the + `$zoomLevel` variable or applying interpolation or step functions to feature + attributes. + */ +@property (nonatomic, null_resettable) NSExpression *textVariableAnchor; + #pragma mark - Accessing the Paint Attributes #if TARGET_OS_IPHONE diff --git a/platform/darwin/src/MGLSymbolStyleLayer.mm b/platform/darwin/src/MGLSymbolStyleLayer.mm index 60fc4d6881..6d91bbe87f 100644 --- a/platform/darwin/src/MGLSymbolStyleLayer.mm +++ b/platform/darwin/src/MGLSymbolStyleLayer.mm @@ -71,6 +71,7 @@ namespace mbgl { }); MBGL_DEFINE_ENUM(MGLTextJustification, { + { MGLTextJustificationAuto, "auto" }, { MGLTextJustificationLeft, "left" }, { MGLTextJustificationCenter, "center" }, { MGLTextJustificationRight, "right" }, @@ -906,6 +907,24 @@ namespace mbgl { return MGLStyleValueTransformer().toExpression(propertyValue); } +- (void)setTextRadialOffset:(NSExpression *)textRadialOffset { + MGLAssertStyleLayerIsValid(); + MGLLogDebug(@"Setting textRadialOffset: %@", textRadialOffset); + + auto mbglValue = MGLStyleValueTransformer().toPropertyValue>(textRadialOffset, true); + self.rawLayer->setTextRadialOffset(mbglValue); +} + +- (NSExpression *)textRadialOffset { + MGLAssertStyleLayerIsValid(); + + auto propertyValue = self.rawLayer->getTextRadialOffset(); + if (propertyValue.isUndefined()) { + propertyValue = self.rawLayer->getDefaultTextRadialOffset(); + } + return MGLStyleValueTransformer().toExpression(propertyValue); +} + - (void)setTextRotation:(NSExpression *)textRotation { MGLAssertStyleLayerIsValid(); MGLLogDebug(@"Setting textRotation: %@", textRotation); @@ -967,6 +986,24 @@ namespace mbgl { return MGLStyleValueTransformer().toExpression(propertyValue); } +- (void)setTextVariableAnchor:(NSExpression *)textVariableAnchor { + MGLAssertStyleLayerIsValid(); + MGLLogDebug(@"Setting textVariableAnchor: %@", textVariableAnchor); + + auto mbglValue = MGLStyleValueTransformer, NSArray *, mbgl::style::SymbolAnchorType, MGLTextAnchor>().toPropertyValue>>(textVariableAnchor, false); + self.rawLayer->setTextVariableAnchor(mbglValue); +} + +- (NSExpression *)textVariableAnchor { + MGLAssertStyleLayerIsValid(); + + auto propertyValue = self.rawLayer->getTextVariableAnchor(); + if (propertyValue.isUndefined()) { + propertyValue = self.rawLayer->getDefaultTextVariableAnchor(); + } + return MGLStyleValueTransformer, NSArray *, mbgl::style::SymbolAnchorType, MGLTextAnchor>().toExpression(propertyValue); +} + #pragma mark - Accessing the Paint Attributes - (void)setIconColor:(NSExpression *)iconColor { diff --git a/platform/darwin/test/MGLSymbolStyleLayerTests.mm b/platform/darwin/test/MGLSymbolStyleLayerTests.mm index 2f4206a96b..083b12bcc3 100644 --- a/platform/darwin/test/MGLSymbolStyleLayerTests.mm +++ b/platform/darwin/test/MGLSymbolStyleLayerTests.mm @@ -1741,6 +1741,75 @@ XCTAssertThrowsSpecificNamed(layer.textPitchAlignment = functionExpression, NSException, NSInvalidArgumentException, @"MGLSymbolLayer should raise an exception if a camera-data expression is applied to a property that does not support key paths to feature attributes."); } + // text-radial-offset + { + XCTAssertTrue(rawLayer->getTextRadialOffset().isUndefined(), + @"text-radial-offset should be unset initially."); + NSExpression *defaultExpression = layer.textRadialOffset; + + NSExpression *constantExpression = [NSExpression expressionWithFormat:@"1"]; + layer.textRadialOffset = constantExpression; + mbgl::style::PropertyValue propertyValue = { 1.0 }; + XCTAssertEqual(rawLayer->getTextRadialOffset(), propertyValue, + @"Setting textRadialOffset to a constant value expression should update text-radial-offset."); + XCTAssertEqualObjects(layer.textRadialOffset, constantExpression, + @"textRadialOffset should round-trip constant value expressions."); + + constantExpression = [NSExpression expressionWithFormat:@"1"]; + NSExpression *functionExpression = [NSExpression expressionWithFormat:@"mgl_step:from:stops:($zoomLevel, %@, %@)", constantExpression, @{@18: constantExpression}]; + layer.textRadialOffset = functionExpression; + + { + using namespace mbgl::style::expression::dsl; + propertyValue = mbgl::style::PropertyExpression( + step(zoom(), literal(1.0), 18.0, literal(1.0)) + ); + } + + XCTAssertEqual(rawLayer->getTextRadialOffset(), propertyValue, + @"Setting textRadialOffset to a camera expression should update text-radial-offset."); + XCTAssertEqualObjects(layer.textRadialOffset, functionExpression, + @"textRadialOffset should round-trip camera expressions."); + + functionExpression = [NSExpression expressionWithFormat:@"mgl_interpolate:withCurveType:parameters:stops:(keyName, 'linear', nil, %@)", @{@18: constantExpression}]; + layer.textRadialOffset = functionExpression; + + { + using namespace mbgl::style::expression::dsl; + propertyValue = mbgl::style::PropertyExpression( + interpolate(linear(), number(get("keyName")), 18.0, literal(1.0)) + ); + } + + XCTAssertEqual(rawLayer->getTextRadialOffset(), propertyValue, + @"Setting textRadialOffset to a data expression should update text-radial-offset."); + NSExpression *pedanticFunctionExpression = [NSExpression expressionWithFormat:@"mgl_interpolate:withCurveType:parameters:stops:(CAST(keyName, 'NSNumber'), 'linear', nil, %@)", @{@18: constantExpression}]; + XCTAssertEqualObjects(layer.textRadialOffset, pedanticFunctionExpression, + @"textRadialOffset should round-trip data expressions."); + + functionExpression = [NSExpression expressionWithFormat:@"mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", @{@10: functionExpression}]; + layer.textRadialOffset = functionExpression; + + { + using namespace mbgl::style::expression::dsl; + propertyValue = mbgl::style::PropertyExpression( + interpolate(linear(), zoom(), 10.0, interpolate(linear(), number(get("keyName")), 18.0, literal(1.0))) + ); + } + + XCTAssertEqual(rawLayer->getTextRadialOffset(), propertyValue, + @"Setting textRadialOffset to a camera-data expression should update text-radial-offset."); + pedanticFunctionExpression = [NSExpression expressionWithFormat:@"mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", @{@10: pedanticFunctionExpression}]; + XCTAssertEqualObjects(layer.textRadialOffset, pedanticFunctionExpression, + @"textRadialOffset should round-trip camera-data expressions."); + + layer.textRadialOffset = nil; + XCTAssertTrue(rawLayer->getTextRadialOffset().isUndefined(), + @"Unsetting textRadialOffset should return text-radial-offset to the default value."); + XCTAssertEqualObjects(layer.textRadialOffset, defaultExpression, + @"textRadialOffset should return the default value after being unset."); + } + // text-rotate { XCTAssertTrue(rawLayer->getTextRotate().isUndefined(), @@ -1892,6 +1961,50 @@ @"textTransform should return the default value after being unset."); } + // text-variable-anchor + { + XCTAssertTrue(rawLayer->getTextVariableAnchor().isUndefined(), + @"text-variable-anchor should be unset initially."); + NSExpression *defaultExpression = layer.textVariableAnchor; + + NSExpression *constantExpression = [NSExpression expressionWithFormat:@"{'top','bottom'}"]; + layer.textVariableAnchor = constantExpression; + mbgl::style::PropertyValue> propertyValue = { { mbgl::style::SymbolAnchorType::Top, mbgl::style::SymbolAnchorType::Bottom } }; + XCTAssertEqual(rawLayer->getTextVariableAnchor(), propertyValue, + @"Setting textVariableAnchor to a constant value expression should update text-variable-anchor."); + XCTAssertEqualObjects(layer.textVariableAnchor, constantExpression, + @"textVariableAnchor should round-trip constant value expressions."); + + constantExpression = [NSExpression expressionWithFormat:@"{'top','bottom'}"]; + NSExpression *functionExpression = [NSExpression expressionWithFormat:@"mgl_step:from:stops:($zoomLevel, %@, %@)", constantExpression, @{@18: constantExpression}]; + layer.textVariableAnchor = functionExpression; + + { + using namespace mbgl::style::expression::dsl; + propertyValue = mbgl::style::PropertyExpression>( + step(zoom(), literal({"top", "bottom"}), 18.0, literal({"top", "bottom"})) + ); + } + + XCTAssertEqual(rawLayer->getTextVariableAnchor(), propertyValue, + @"Setting textVariableAnchor to a camera expression should update text-variable-anchor."); + XCTAssertEqualObjects(layer.textVariableAnchor, functionExpression, + @"textVariableAnchor should round-trip camera expressions."); + + + layer.textVariableAnchor = nil; + XCTAssertTrue(rawLayer->getTextVariableAnchor().isUndefined(), + @"Unsetting textVariableAnchor should return text-variable-anchor to the default value."); + XCTAssertEqualObjects(layer.textVariableAnchor, defaultExpression, + @"textVariableAnchor should return the default value after being unset."); + + functionExpression = [NSExpression expressionForKeyPath:@"bogus"]; + XCTAssertThrowsSpecificNamed(layer.textVariableAnchor = functionExpression, NSException, NSInvalidArgumentException, @"MGLSymbolLayer should raise an exception if a camera-data expression is applied to a property that does not support key paths to feature attributes."); + functionExpression = [NSExpression expressionWithFormat:@"mgl_step:from:stops:(bogus, %@, %@)", constantExpression, @{@18: constantExpression}]; + functionExpression = [NSExpression expressionWithFormat:@"mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", @{@10: functionExpression}]; + XCTAssertThrowsSpecificNamed(layer.textVariableAnchor = functionExpression, NSException, NSInvalidArgumentException, @"MGLSymbolLayer should raise an exception if a camera-data expression is applied to a property that does not support key paths to feature attributes."); + } + // icon-color { XCTAssertTrue(rawLayer->getIconColor().isUndefined(), @@ -2896,9 +3009,11 @@ [self testPropertyName:@"is-text-optional" isBoolean:YES]; [self testPropertyName:@"text-padding" isBoolean:NO]; [self testPropertyName:@"text-pitch-alignment" isBoolean:NO]; + [self testPropertyName:@"text-radial-offset" isBoolean:NO]; [self testPropertyName:@"text-rotation" isBoolean:NO]; [self testPropertyName:@"text-rotation-alignment" isBoolean:NO]; [self testPropertyName:@"text-transform" isBoolean:NO]; + [self testPropertyName:@"text-variable-anchor" isBoolean:NO]; [self testPropertyName:@"icon-color" isBoolean:NO]; [self testPropertyName:@"icon-halo-blur" isBoolean:NO]; [self testPropertyName:@"icon-halo-color" isBoolean:NO]; @@ -2949,6 +3064,7 @@ XCTAssertEqual([NSValue valueWithMGLTextAnchor:MGLTextAnchorTopRight].MGLTextAnchorValue, MGLTextAnchorTopRight); XCTAssertEqual([NSValue valueWithMGLTextAnchor:MGLTextAnchorBottomLeft].MGLTextAnchorValue, MGLTextAnchorBottomLeft); XCTAssertEqual([NSValue valueWithMGLTextAnchor:MGLTextAnchorBottomRight].MGLTextAnchorValue, MGLTextAnchorBottomRight); + XCTAssertEqual([NSValue valueWithMGLTextJustification:MGLTextJustificationAuto].MGLTextJustificationValue, MGLTextJustificationAuto); XCTAssertEqual([NSValue valueWithMGLTextJustification:MGLTextJustificationLeft].MGLTextJustificationValue, MGLTextJustificationLeft); XCTAssertEqual([NSValue valueWithMGLTextJustification:MGLTextJustificationCenter].MGLTextJustificationValue, MGLTextJustificationCenter); XCTAssertEqual([NSValue valueWithMGLTextJustification:MGLTextJustificationRight].MGLTextJustificationValue, MGLTextJustificationRight); -- cgit v1.2.1