diff --git a/platform/darwin/src/MGLFeature.h b/platform/darwin/src/MGLFeature.h index 8ba56874800..2380a817e35 100644 --- a/platform/darwin/src/MGLFeature.h +++ b/platform/darwin/src/MGLFeature.h @@ -83,12 +83,21 @@ NS_ASSUME_NONNULL_BEGIN `MGLVectorStyleLayer.predicate` to an `NSPredicate` with the format `importance > 50`. - You can also configure some attributes of an `MGLSymbolStyleLayer` object to - include the value of an attribute in this dictionary whenever it renders this - feature. For example, to label features in an `MGLShapeSource` object by their - names, you can assign a `name` attribute to each of the source’s features, then - set `MGLSymbolStyleLayer.textField` to an `MGLStyleValue` object containing the - string `{name}`. + You can also configure many layout and paint attributes of an `MGLStyleLayer` + object to match the value of an attribute in this dictionary whenever it + renders this feature. For example, if you display features in an + `MGLShapeSource` using an `MGLCircleStyleLayer`, you can assign a `halfway` + attribute to each of the source’s features, then set + `MGLCircleStyleLayer.circleRadius` to an `MGLStyleValue` object with an + interpolation mode of `MGLInterpolationModeIdentity` and an attribute name of + `halfway`. + + The `MGLSymbolStyleLayer.textField` and `MGLSymbolStyleLayer.iconImageName` + properties allow you to use attributes yet another way. For example, to label + features in an `MGLShapeSource` object by their names, you can assign a `name` + attribute to each of the source’s features, then set + `MGLSymbolStyleLayer.textField` to an `MGLStyleValue` object containing the + raw string value `{name}`. In vector tiles loaded by `MGLVectorSource` objects, the keys and values of each feature’s attribute dictionary are determined by the source. Each @@ -115,6 +124,15 @@ NS_ASSUME_NONNULL_BEGIN and Mapbox Terrain layer references. + + When adding a feature to an `MGLShapeSource`, use the same Foundation types + listed above for each attribute value. In addition to the Foundation types, you + may also set an attribute to an `NSColor` (macOS) or `UIColor` (iOS), which + will be converted into its + CSS string representation + when the feature is added to an `MGLShapeSource`. This can be convenient when + using the attribute to supply a value for a color-typed layout or paint + attribute via the `MGLInterpolationModeIdentity` interpolation mode. Note that while it is possible to change this value on feature instances obtained from `-[MGLMapView visibleFeaturesAtPoint:]` and related diff --git a/platform/darwin/src/NSExpression+MGLAdditions.mm b/platform/darwin/src/NSExpression+MGLAdditions.mm index c54102b8c97..a7759cda9dd 100644 --- a/platform/darwin/src/NSExpression+MGLAdditions.mm +++ b/platform/darwin/src/NSExpression+MGLAdditions.mm @@ -1,5 +1,12 @@ #import "NSExpression+MGLAdditions.h" +#import "MGLTypes.h" +#if TARGET_OS_IPHONE + #import "UIColor+MGLAdditions.h" +#else + #import "NSColor+MGLAdditions.h" +#endif + @implementation NSExpression (MGLAdditions) - (std::vector)mgl_aggregateMBGLValue { @@ -56,6 +63,9 @@ @implementation NSExpression (MGLAdditions) // We use long long here to avoid any truncation. return { (int64_t)number.longLongValue }; } + } else if ([value isKindOfClass:[MGLColor class]]) { + auto hexString = [(MGLColor *)value mgl_color].stringify(); + return { hexString }; } else if (value && value != [NSNull null]) { [NSException raise:NSInvalidArgumentException format:@"Can’t convert %s:%@ to mbgl::Value", [value objCType], value]; diff --git a/platform/darwin/test/MGLExpressionTests.mm b/platform/darwin/test/MGLExpressionTests.mm index 47315b97d6f..ad0833a0682 100644 --- a/platform/darwin/test/MGLExpressionTests.mm +++ b/platform/darwin/test/MGLExpressionTests.mm @@ -2,8 +2,29 @@ #import +#import "MGLTypes.h" #import "NSExpression+MGLAdditions.h" +#define MGLAssertEqualValues(actual, expected, ...) \ + XCTAssertTrue(actual.is<__typeof__(expected)>()); \ + if (actual.is<__typeof__(expected)>()) { \ + XCTAssertEqual(actual.get<__typeof__(expected)>(), expected, __VA_ARGS__); \ + } + +#define MGLAssertEqualValuesWithAccuracy(actual, expected, accuracy, ...) \ + XCTAssertTrue(actual.is<__typeof__(expected)>()); \ + if (actual.is<__typeof__(expected)>()) { \ + XCTAssertEqualWithAccuracy(actual.get<__typeof__(expected)>(), expected, accuracy, __VA_ARGS__); \ + } + +#define MGLAssertConstantEqualsValue(constant, value, ...) \ + MGLAssertEqualValues([NSExpression expressionForConstantValue:constant].mgl_constantMBGLValue, value, __VA_ARGS__); + +#define MGLAssertConstantEqualsValueWithAccuracy(constant, value, accuracy, ...) \ + MGLAssertEqualValuesWithAccuracy([NSExpression expressionForConstantValue:constant].mgl_constantMBGLValue, value, accuracy, __VA_ARGS__); + +using namespace std::string_literals; + @interface MGLExpressionTests : XCTestCase @end @@ -23,212 +44,100 @@ - (NSComparisonPredicate *)equalityComparisonPredicateWithRightConstantValue:(id return predicate; } -#pragma mark - String Tests - -- (void)testExpressionConversionString -{ - NSComparisonPredicate *predicate = [self equalityComparisonPredicateWithRightConstantValue:@"bar"]; - mbgl::Value convertedValue = predicate.rightExpression.mgl_constantMBGLValue; - XCTAssertTrue(convertedValue.is()); - XCTAssertEqualObjects(@(convertedValue.get().c_str()), @"bar"); -} +#pragma mark - Valuation tests -- (void)testExpressionConversionStringWithUnicode -{ - NSComparisonPredicate *predicate = [self equalityComparisonPredicateWithRightConstantValue:@"🆔🆗🇦🇶"]; - mbgl::Value convertedValue = predicate.rightExpression.mgl_constantMBGLValue; - XCTAssertTrue(convertedValue.is()); - XCTAssertEqual(convertedValue.get(), "🆔🆗🇦🇶"); +- (void)testStringValuation { + MGLAssertConstantEqualsValue(@"bar", "bar"s, @"NSString should convert to std::string."); + MGLAssertConstantEqualsValue(@"🆔🆗🇦🇶", "🆔🆗🇦🇶"s, @"NSString with non-ASCII characters should convert losslessly to std::string."); } -#pragma mark - Boolean Tests - -- (void)testExpressionConversionBooleanTrue -{ - NSComparisonPredicate *predicate = [self equalityComparisonPredicateWithRightConstantValue:@YES]; - mbgl::Value convertedValue = predicate.rightExpression.mgl_constantMBGLValue; - XCTAssertTrue(convertedValue.is()); - XCTAssertEqual(convertedValue.get(), true); +- (void)testColorValuation { + MGLAssertConstantEqualsValue([MGLColor redColor], "rgba(255,0,0,1)"s, @"MGLColor should convert to std::string containing CSS color string."); } -- (void)testExpressionConversionBooleanFalse -{ - NSComparisonPredicate *predicate = [self equalityComparisonPredicateWithRightConstantValue:@NO]; - mbgl::Value convertedValue = predicate.rightExpression.mgl_constantMBGLValue; - XCTAssertTrue(convertedValue.is()); - XCTAssertEqual(convertedValue.get(), false); +- (void)testBooleanValuation { + MGLAssertConstantEqualsValue(@NO, false, @"Boolean NSNumber should convert to bool."); + MGLAssertConstantEqualsValue(@YES, true, @"Boolean NSNumber should convert to bool."); } -#pragma mark - Floating Point Tests - -- (void)testExpressionConversionDouble +- (void)testDoubleValuation { - NSComparisonPredicate *predicate; - mbgl::Value convertedValue; - - predicate = [self equalityComparisonPredicateWithRightConstantValue:[NSNumber numberWithDouble:DBL_MIN]]; - convertedValue = predicate.rightExpression.mgl_constantMBGLValue; - XCTAssertTrue(convertedValue.is()); - XCTAssertEqual(convertedValue.get(), DBL_MIN); - - predicate = [self equalityComparisonPredicateWithRightConstantValue:[NSNumber numberWithDouble:DBL_MAX]]; - convertedValue = predicate.rightExpression.mgl_constantMBGLValue; - XCTAssertTrue(convertedValue.is()); - XCTAssertEqual(convertedValue.get(), DBL_MAX); + MGLAssertConstantEqualsValue(@DBL_MIN, DBL_MIN, @"Double NSNumber should convert to double."); + MGLAssertConstantEqualsValue(@DBL_MAX, DBL_MAX, @"Double NSNumber should convert to double."); } -- (void)testExpressionConversionFloat -{ +- (void)testFloatValuation { // Because we can't guarantee precision when using float, and because // we warn the user to this effect in -[NSExpression mgl_constantMBGLValue], // we just check that things are in the ballpark here with integer values // and some lower-precision checks. - - NSComparisonPredicate *predicate; - mbgl::Value convertedValue; - - predicate = [self equalityComparisonPredicateWithRightConstantValue:[NSNumber numberWithFloat:-1]]; - convertedValue = predicate.rightExpression.mgl_constantMBGLValue; - XCTAssertTrue(convertedValue.is()); - XCTAssertEqual(convertedValue.get(), -1); - - predicate = [self equalityComparisonPredicateWithRightConstantValue:[NSNumber numberWithFloat:1]]; - convertedValue = predicate.rightExpression.mgl_constantMBGLValue; - XCTAssertTrue(convertedValue.is()); - XCTAssertEqual(convertedValue.get(), 1); - - predicate = [self equalityComparisonPredicateWithRightConstantValue:[NSNumber numberWithFloat:-23.232342]]; - convertedValue = predicate.rightExpression.mgl_constantMBGLValue; - XCTAssertTrue(convertedValue.is()); - XCTAssertEqualWithAccuracy(convertedValue.get(), -23.232342, 0.000001); - - predicate = [self equalityComparisonPredicateWithRightConstantValue:[NSNumber numberWithFloat:23.232342]]; - convertedValue = predicate.rightExpression.mgl_constantMBGLValue; - XCTAssertTrue(convertedValue.is()); - XCTAssertEqualWithAccuracy(convertedValue.get(), 23.232342, 0.000001); + + MGLAssertConstantEqualsValue(@-1.0f, -1.0, @"Float NSNumber should convert to double."); + MGLAssertConstantEqualsValue(@1.0f, 1.0, @"Float NSNumber should convert to double."); + MGLAssertConstantEqualsValueWithAccuracy(@-23.232342f, -23.232342, 0.000001, @"Float NSNumber should convert to double."); + MGLAssertConstantEqualsValueWithAccuracy(@23.232342f, 23.232342, 0.000001, @"Float NSNumber should convert to double."); + MGLAssertConstantEqualsValueWithAccuracy(@-FLT_MAX, static_cast(-FLT_MAX), 0.000001, @"Float NSNumber should convert to double."); + MGLAssertConstantEqualsValueWithAccuracy(@FLT_MAX, static_cast(FLT_MAX), 0.000001, @"Float NSNumber should convert to double."); } -#pragma mark - Integer Tests - -- (void)testExpressionNegativeIntegers -{ - NSComparisonPredicate *predicate; - mbgl::Value convertedValue; - - NSArray *minValues = @[ - [NSNumber numberWithShort: SHRT_MIN], - [NSNumber numberWithInt: INT_MIN], - [NSNumber numberWithLong: LONG_MIN], - [NSNumber numberWithLongLong: LLONG_MIN], - [NSNumber numberWithInteger: NSIntegerMin] - ]; - - NSArray *maxValues = @[ - [NSNumber numberWithShort: SHRT_MAX], - [NSNumber numberWithInt: INT_MAX], - [NSNumber numberWithLong: LONG_MAX], - [NSNumber numberWithLongLong: LLONG_MAX], - [NSNumber numberWithInteger: NSIntegerMax] - ]; - +- (void)testIntegerValuation { // Negative integers should always come back as int64_t per mbgl::Value definition. - // We use the long long value because it can store the highest number on both 32- - // and 64-bit and won't overflow. - - for (NSNumber *min in minValues) - { - predicate = [self equalityComparisonPredicateWithRightConstantValue:min]; - convertedValue = predicate.rightExpression.mgl_constantMBGLValue; - XCTAssertTrue(convertedValue.is()); - XCTAssertEqual(convertedValue.get(), min.longLongValue); - } + MGLAssertConstantEqualsValue(@SHRT_MIN, static_cast(SHRT_MIN), @"Negative short NSNumber should convert to int64_t."); + MGLAssertConstantEqualsValue(@INT_MIN, static_cast(INT_MIN), @"Negative int NSNumber should convert to int64_t."); + MGLAssertConstantEqualsValue(@LONG_MIN, static_cast(LONG_MIN), @"Negative long NSNumber should convert to int64_t."); + MGLAssertConstantEqualsValue(@LLONG_MIN, static_cast(LLONG_MIN), @"Negative long long NSNumber should convert to int64_t."); + MGLAssertConstantEqualsValue(@NSIntegerMin, static_cast(NSIntegerMin), @"Negative NSInteger NSNumber should convert to int64_t."); // Positive integers should always come back as uint64_t per mbgl::Value definition. - // We use the unsigned long long value because it can store the highest number on - // both 32- and 64-bit and won't overflow. - - for (NSNumber *max in maxValues) - { - predicate = [self equalityComparisonPredicateWithRightConstantValue:max]; - convertedValue = predicate.rightExpression.mgl_constantMBGLValue; - XCTAssertTrue(convertedValue.is()); - XCTAssertEqual(convertedValue.get(), max.unsignedLongLongValue); - } - + MGLAssertConstantEqualsValue(@SHRT_MAX, static_cast(SHRT_MAX), @"Positive short NSNumber should convert to uint64_t."); + MGLAssertConstantEqualsValue(@INT_MAX, static_cast(INT_MAX), @"Positive int NSNumber should convert to uint64_t."); + MGLAssertConstantEqualsValue(@LONG_MAX, static_cast(LONG_MAX), @"Positive long NSNumber should convert to uint64_t."); + MGLAssertConstantEqualsValue(@LLONG_MAX, static_cast(LLONG_MAX), @"Positive long long NSNumber should convert to uint64_t."); + MGLAssertConstantEqualsValue(@NSIntegerMax, static_cast(NSIntegerMax), @"Positive NSInteger NSNumber should convert to uint64_t."); } -- (void)testExpressionPositiveAndZeroIntegers -{ - NSComparisonPredicate *predicate; - mbgl::Value convertedValue; - - NSArray *minValues = @[ - [NSNumber numberWithUnsignedShort: 0], - [NSNumber numberWithUnsignedInt: 0], - [NSNumber numberWithUnsignedLong: 0], - [NSNumber numberWithUnsignedLongLong: 0], - [NSNumber numberWithUnsignedInteger: 0] - ]; - - NSArray *maxValues = @[ - [NSNumber numberWithUnsignedShort: USHRT_MAX], - [NSNumber numberWithUnsignedInt: UINT_MAX], - [NSNumber numberWithUnsignedLong: ULONG_MAX], - [NSNumber numberWithUnsignedLongLong: ULLONG_MAX], - [NSNumber numberWithUnsignedInteger: NSUIntegerMax] - ]; - +- (void)testUnsignedIntegerValuation { // Zero-value integers should always come back as uint64_t per mbgl::Value definition // (using the interpretation that zero is not negative). We use the unsigned long long // value just for parity with the positive integer test. - - for (NSNumber *min in minValues) - { - predicate = [self equalityComparisonPredicateWithRightConstantValue:min]; - convertedValue = predicate.rightExpression.mgl_constantMBGLValue; - XCTAssertTrue(convertedValue.is()); - XCTAssertEqual(convertedValue.get(), min.unsignedLongLongValue); - } + MGLAssertConstantEqualsValue(@(static_cast(0)), static_cast(0), @"Unsigned short NSNumber should convert to uint64_t."); + MGLAssertConstantEqualsValue(@0u, static_cast(0), @"Unsigned int NSNumber should convert to uint64_t."); + MGLAssertConstantEqualsValue(@0UL, static_cast(0), @"Unsigned long NSNumber should convert to uint64_t."); + MGLAssertConstantEqualsValue(@0ULL, static_cast(0), @"Unsigned long long NSNumber should convert to uint64_t."); + MGLAssertConstantEqualsValue(@(static_cast(0)), static_cast(0), @"Unsigned NSUInteger NSNumber should convert to uint64_t."); // Positive integers should always come back as uint64_t per mbgl::Value definition. // We use the unsigned long long value because it can store the highest number on // both 32- and 64-bit and won't overflow. - - for (NSNumber *max in maxValues) - { - predicate = [self equalityComparisonPredicateWithRightConstantValue:max]; - convertedValue = predicate.rightExpression.mgl_constantMBGLValue; - XCTAssertTrue(convertedValue.is()); - XCTAssertEqual(convertedValue.get(), max.unsignedLongLongValue); - } + MGLAssertConstantEqualsValue(@USHRT_MAX, static_cast(USHRT_MAX), @"Unsigned short NSNumber should convert to uint64_t."); + MGLAssertConstantEqualsValue(@UINT_MAX, static_cast(UINT_MAX), @"Unsigned int NSNumber should convert to uint64_t."); + MGLAssertConstantEqualsValue(@ULONG_MAX, static_cast(ULONG_MAX), @"Unsigned long NSNumber should convert to uint64_t."); + MGLAssertConstantEqualsValue(@ULLONG_MAX, static_cast(ULLONG_MAX), @"Unsigned long long NSNumber should convert to uint64_t."); + MGLAssertConstantEqualsValue(@NSUIntegerMax, static_cast(NSUIntegerMax), @"Unsigned NSUInteger NSNumber should convert to uint64_t."); } -#pragma mark - Null Tests - -- (void)testExpressionConversionNull -{ - NSComparisonPredicate *predicate = [self equalityComparisonPredicateWithRightConstantValue:[NSNull null]]; - mbgl::Value convertedValue = predicate.rightExpression.mgl_constantMBGLValue; - XCTAssertTrue(convertedValue.is()); +- (void)testNullValuation { + mbgl::NullValue nullValue; + MGLAssertConstantEqualsValue([NSNull null], nullValue, @"NSNull should convert to mbgl::NullValue."); } #pragma mark - Feature type tests - (void)testFeatureType { - XCTAssertEqual([NSExpression expressionWithFormat:@"'Point'"].mgl_featureType, mbgl::FeatureType::Point); - XCTAssertEqual([NSExpression expressionWithFormat:@"'LineString'"].mgl_featureType, mbgl::FeatureType::LineString); - XCTAssertEqual([NSExpression expressionWithFormat:@"'Polygon'"].mgl_featureType, mbgl::FeatureType::Polygon); - XCTAssertEqual([NSExpression expressionWithFormat:@"'Unknown'"].mgl_featureType, mbgl::FeatureType::Unknown); - XCTAssertEqual([NSExpression expressionWithFormat:@"''"].mgl_featureType, mbgl::FeatureType::Unknown); + XCTAssertEqual([NSExpression expressionForConstantValue:@"Point"].mgl_featureType, mbgl::FeatureType::Point); + XCTAssertEqual([NSExpression expressionForConstantValue:@"LineString"].mgl_featureType, mbgl::FeatureType::LineString); + XCTAssertEqual([NSExpression expressionForConstantValue:@"Polygon"].mgl_featureType, mbgl::FeatureType::Polygon); + XCTAssertEqual([NSExpression expressionForConstantValue:@"Unknown"].mgl_featureType, mbgl::FeatureType::Unknown); + XCTAssertEqual([NSExpression expressionForConstantValue:@""].mgl_featureType, mbgl::FeatureType::Unknown); - XCTAssertEqual([NSExpression expressionWithFormat:@"1"].mgl_featureType, mbgl::FeatureType::Point); - XCTAssertEqual([NSExpression expressionWithFormat:@"2"].mgl_featureType, mbgl::FeatureType::LineString); - XCTAssertEqual([NSExpression expressionWithFormat:@"3"].mgl_featureType, mbgl::FeatureType::Polygon); - XCTAssertEqual([NSExpression expressionWithFormat:@"0"].mgl_featureType, mbgl::FeatureType::Unknown); - XCTAssertEqual([NSExpression expressionWithFormat:@"-1"].mgl_featureType, mbgl::FeatureType::Unknown); - XCTAssertEqual([NSExpression expressionWithFormat:@"4"].mgl_featureType, mbgl::FeatureType::Unknown); + XCTAssertEqual([NSExpression expressionForConstantValue:@1].mgl_featureType, mbgl::FeatureType::Point); + XCTAssertEqual([NSExpression expressionForConstantValue:@2].mgl_featureType, mbgl::FeatureType::LineString); + XCTAssertEqual([NSExpression expressionForConstantValue:@3].mgl_featureType, mbgl::FeatureType::Polygon); + XCTAssertEqual([NSExpression expressionForConstantValue:@0].mgl_featureType, mbgl::FeatureType::Unknown); + XCTAssertEqual([NSExpression expressionForConstantValue:@-1].mgl_featureType, mbgl::FeatureType::Unknown); + XCTAssertEqual([NSExpression expressionForConstantValue:@4].mgl_featureType, mbgl::FeatureType::Unknown); - XCTAssertEqual([NSExpression expressionWithFormat:@"nil"].mgl_featureType, mbgl::FeatureType::Unknown); + XCTAssertEqual([NSExpression expressionForConstantValue:nil].mgl_featureType, mbgl::FeatureType::Unknown); } @end diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index a0281b3f02f..cd7b44165c4 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -26,6 +26,7 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT * Fixed artifacts when drawing particularly acute line joins. ([#7786](https://github.com/mapbox/mapbox-gl-native/pull/7786)) * Fixed an issue in which a vector style layer predicate involving the `$id` key path would exclude all features from the layer. ([#7989](https://github.com/mapbox/mapbox-gl-native/pull/7989), [#7971](https://github.com/mapbox/mapbox-gl-native/pull/7971)) * Fixed an issue causing vector style layer predicates to be evaluated as if each feature had a `$type` attribute of 1, 2, or 3. The `$type` key path can now be compared to `Point`, `LineString`, or `Polygon`, as described in the documentation. ([#7971](https://github.com/mapbox/mapbox-gl-native/pull/7971)) +* When setting an `MGLShapeSource`’s shape to an `MGLFeature` instance, any `UIColor` attribute value is now converted to the equivalent CSS string representation for use with `MGLInterpolationModeIdentity` in style functions. ([#8025](https://github.com/mapbox/mapbox-gl-native/pull/8025)) ### User interaction diff --git a/platform/macos/CHANGELOG.md b/platform/macos/CHANGELOG.md index 4738cc3b780..b00b0cee41c 100644 --- a/platform/macos/CHANGELOG.md +++ b/platform/macos/CHANGELOG.md @@ -25,6 +25,7 @@ * Fixed artifacts when drawing particularly acute line joins. ([#7786](https://github.com/mapbox/mapbox-gl-native/pull/7786)) * Fixed an issue in which a vector style layer predicate involving the `$id` key path would exclude all features from the layer. ([#7989](https://github.com/mapbox/mapbox-gl-native/pull/7989), [#7971](https://github.com/mapbox/mapbox-gl-native/pull/7971)) * Fixed an issue causing vector style layer predicates to be evaluated as if each feature had a `$type` attribute of 1, 2, or 3. The `$type` key path can now be compared to `Point`, `LineString`, or `Polygon`, as described in the documentation. ([#7971](https://github.com/mapbox/mapbox-gl-native/pull/7971)) +* When setting an `MGLShapeSource`’s shape to an `MGLFeature` instance, any `NSColor` attribute value is now converted to the equivalent CSS string representation for use with `MGLInterpolationModeIdentity` in style functions. ([#8025](https://github.com/mapbox/mapbox-gl-native/pull/8025)) ### User interaction