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

Commit

Permalink
[ios, macos] Convert NSColor/UIColor expressions to CSS color string …
Browse files Browse the repository at this point in the history
…values

When converting a constant NSExpression to an mbgl::Value, turn an NSColor or UIColor into an std::string containing a CSS color string. This allows developers to set an attribute of an MGLFeature to an NSColor or UIColor (rather than a CSS color string, which would be foreign), then use it in an MGLStyleFunction with identity interpolation.
  • Loading branch information
1ec5 committed Feb 11, 2017
1 parent e6c15d0 commit 03cdf6a
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 175 deletions.
30 changes: 24 additions & 6 deletions platform/darwin/src/MGLFeature.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 use attributes in a slightly different 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
Expand All @@ -115,6 +124,15 @@ NS_ASSUME_NONNULL_BEGIN
and
<a href="https://www.mapbox.com/vector-tiles/mapbox-terrain/">Mapbox Terrain</a>
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
<a href="https://www.mapbox.com/mapbox-gl-js/style-spec/#types-color">CSS string representation</a>
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
Expand Down
10 changes: 10 additions & 0 deletions platform/darwin/src/NSExpression+MGLAdditions.mm
Original file line number Diff line number Diff line change
@@ -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<mbgl::Value>)mgl_aggregateMBGLValue {
Expand Down Expand Up @@ -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];
Expand Down
247 changes: 78 additions & 169 deletions platform/darwin/test/MGLExpressionTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,29 @@

#import <string>

#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
Expand All @@ -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<std::string>());
XCTAssertEqualObjects(@(convertedValue.get<std::string>().c_str()), @"bar");
}
#pragma mark - Valuation tests

- (void)testExpressionConversionStringWithUnicode
{
NSComparisonPredicate *predicate = [self equalityComparisonPredicateWithRightConstantValue:@"🆔🆗🇦🇶"];
mbgl::Value convertedValue = predicate.rightExpression.mgl_constantMBGLValue;
XCTAssertTrue(convertedValue.is<std::string>());
XCTAssertEqual(convertedValue.get<std::string>(), "🆔🆗🇦🇶");
- (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<bool>());
XCTAssertEqual(convertedValue.get<bool>(), 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<bool>());
XCTAssertEqual(convertedValue.get<bool>(), 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<double>());
XCTAssertEqual(convertedValue.get<double>(), DBL_MIN);

predicate = [self equalityComparisonPredicateWithRightConstantValue:[NSNumber numberWithDouble:DBL_MAX]];
convertedValue = predicate.rightExpression.mgl_constantMBGLValue;
XCTAssertTrue(convertedValue.is<double>());
XCTAssertEqual(convertedValue.get<double>(), 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<double>());
XCTAssertEqual(convertedValue.get<double>(), -1);

predicate = [self equalityComparisonPredicateWithRightConstantValue:[NSNumber numberWithFloat:1]];
convertedValue = predicate.rightExpression.mgl_constantMBGLValue;
XCTAssertTrue(convertedValue.is<double>());
XCTAssertEqual(convertedValue.get<double>(), 1);

predicate = [self equalityComparisonPredicateWithRightConstantValue:[NSNumber numberWithFloat:-23.232342]];
convertedValue = predicate.rightExpression.mgl_constantMBGLValue;
XCTAssertTrue(convertedValue.is<double>());
XCTAssertEqualWithAccuracy(convertedValue.get<double>(), -23.232342, 0.000001);

predicate = [self equalityComparisonPredicateWithRightConstantValue:[NSNumber numberWithFloat:23.232342]];
convertedValue = predicate.rightExpression.mgl_constantMBGLValue;
XCTAssertTrue(convertedValue.is<double>());
XCTAssertEqualWithAccuracy(convertedValue.get<double>(), 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<double>(-FLT_MAX), 0.000001, @"Float NSNumber should convert to double.");
MGLAssertConstantEqualsValueWithAccuracy(@FLT_MAX, static_cast<double>(FLT_MAX), 0.000001, @"Float NSNumber should convert to double.");
}

#pragma mark - Integer Tests

- (void)testExpressionNegativeIntegers
{
NSComparisonPredicate *predicate;
mbgl::Value convertedValue;

NSArray<NSNumber *> *minValues = @[
[NSNumber numberWithShort: SHRT_MIN],
[NSNumber numberWithInt: INT_MIN],
[NSNumber numberWithLong: LONG_MIN],
[NSNumber numberWithLongLong: LLONG_MIN],
[NSNumber numberWithInteger: NSIntegerMin]
];

NSArray<NSNumber *> *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<int64_t>());
XCTAssertEqual(convertedValue.get<int64_t>(), min.longLongValue);
}
MGLAssertConstantEqualsValue(@SHRT_MIN, static_cast<int64_t>(SHRT_MIN), @"Negative short NSNumber should convert to int64_t.");
MGLAssertConstantEqualsValue(@INT_MIN, static_cast<int64_t>(INT_MIN), @"Negative int NSNumber should convert to int64_t.");
MGLAssertConstantEqualsValue(@LONG_MIN, static_cast<int64_t>(LONG_MIN), @"Negative long NSNumber should convert to int64_t.");
MGLAssertConstantEqualsValue(@LLONG_MIN, static_cast<int64_t>(LLONG_MIN), @"Negative long long NSNumber should convert to int64_t.");
MGLAssertConstantEqualsValue(@NSIntegerMin, static_cast<int64_t>(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<uint64_t>());
XCTAssertEqual(convertedValue.get<uint64_t>(), max.unsignedLongLongValue);
}

MGLAssertConstantEqualsValue(@SHRT_MAX, static_cast<uint64_t>(SHRT_MAX), @"Positive short NSNumber should convert to uint64_t.");
MGLAssertConstantEqualsValue(@INT_MAX, static_cast<uint64_t>(INT_MAX), @"Positive int NSNumber should convert to uint64_t.");
MGLAssertConstantEqualsValue(@LONG_MAX, static_cast<uint64_t>(LONG_MAX), @"Positive long NSNumber should convert to uint64_t.");
MGLAssertConstantEqualsValue(@LLONG_MAX, static_cast<uint64_t>(LLONG_MAX), @"Positive long long NSNumber should convert to uint64_t.");
MGLAssertConstantEqualsValue(@NSIntegerMax, static_cast<uint64_t>(NSIntegerMax), @"Positive NSInteger NSNumber should convert to uint64_t.");
}

- (void)testExpressionPositiveAndZeroIntegers
{
NSComparisonPredicate *predicate;
mbgl::Value convertedValue;

NSArray<NSNumber *> *minValues = @[
[NSNumber numberWithUnsignedShort: 0],
[NSNumber numberWithUnsignedInt: 0],
[NSNumber numberWithUnsignedLong: 0],
[NSNumber numberWithUnsignedLongLong: 0],
[NSNumber numberWithUnsignedInteger: 0]
];

NSArray<NSNumber *> *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<uint64_t>());
XCTAssertEqual(convertedValue.get<uint64_t>(), min.unsignedLongLongValue);
}
MGLAssertConstantEqualsValue(@(static_cast<unsigned short>(0)), static_cast<uint64_t>(0), @"Unsigned short NSNumber should convert to uint64_t.");
MGLAssertConstantEqualsValue(@0u, static_cast<uint64_t>(0), @"Unsigned int NSNumber should convert to uint64_t.");
MGLAssertConstantEqualsValue(@0UL, static_cast<uint64_t>(0), @"Unsigned long NSNumber should convert to uint64_t.");
MGLAssertConstantEqualsValue(@0ULL, static_cast<uint64_t>(0), @"Unsigned long long NSNumber should convert to uint64_t.");
MGLAssertConstantEqualsValue(@(static_cast<NSUInteger>(0)), static_cast<uint64_t>(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<uint64_t>());
XCTAssertEqual(convertedValue.get<uint64_t>(), max.unsignedLongLongValue);
}
MGLAssertConstantEqualsValue(@USHRT_MAX, static_cast<uint64_t>(USHRT_MAX), @"Unsigned short NSNumber should convert to uint64_t.");
MGLAssertConstantEqualsValue(@UINT_MAX, static_cast<uint64_t>(UINT_MAX), @"Unsigned int NSNumber should convert to uint64_t.");
MGLAssertConstantEqualsValue(@ULONG_MAX, static_cast<uint64_t>(ULONG_MAX), @"Unsigned long NSNumber should convert to uint64_t.");
MGLAssertConstantEqualsValue(@ULLONG_MAX, static_cast<uint64_t>(ULLONG_MAX), @"Unsigned long long NSNumber should convert to uint64_t.");
MGLAssertConstantEqualsValue(@NSUIntegerMax, static_cast<uint64_t>(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<mbgl::NullValue>());
- (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
Loading

0 comments on commit 03cdf6a

Please sign in to comment.