diff options
author | Minh Nguyễn <mxn@1ec5.org> | 2018-03-26 14:27:14 -0700 |
---|---|---|
committer | Fabian Guerra <fabian.guerra@mapbox.com> | 2018-03-28 19:38:25 -0400 |
commit | e467df8b24cebaab02d462347609a6402f7ba116 (patch) | |
tree | 55e7558bb10e006a38fd25f29065bb0dbbd0e0df | |
parent | 1dcc93d9213a7aee2f049ec3cb52b04d7a244373 (diff) | |
download | qtlocation-mapboxgl-e467df8b24cebaab02d462347609a6402f7ba116.tar.gz |
[ios, macos] Refactored aftermarket expression functions
Refactored the installation of aftermarket expression functions to use macros. It is no longer necessary to handwrite the type encoding of a function. Also added aftermarket functions for interpolating and stepping.
-rw-r--r-- | platform/darwin/docs/guides/Predicates and Expressions.md | 38 | ||||
-rw-r--r-- | platform/darwin/src/NSExpression+MGLAdditions.mm | 194 | ||||
-rw-r--r-- | platform/darwin/test/MGLExpressionTests.mm | 12 |
3 files changed, 168 insertions, 76 deletions
diff --git a/platform/darwin/docs/guides/Predicates and Expressions.md b/platform/darwin/docs/guides/Predicates and Expressions.md index 5e6af06dc4..fd0f82c0eb 100644 --- a/platform/darwin/docs/guides/Predicates and Expressions.md +++ b/platform/darwin/docs/guides/Predicates and Expressions.md @@ -190,10 +190,12 @@ functions just like the predefined functions above, using either the `+[NSExpression expressionForFunction:arguments:]` method or a convenient format string syntax: -Initializer parameter | Format string syntax | Arguments | Returns -----------------------|----------------------|-----------|-------- -`mgl_join:` | `mgl_join({'Old', 'MacDonald'})` | An aggregate expression or `NSArray` constant value expression containing one or more `NSExpression`s, each evaluating to a string. | An `NSString` object (the result of concatenating together all the elements of an array in order). -`MGL_LET` | `MGL_LET('age', uppercase('old'), 'name', uppercase('MacDonald'), mgl_join({$age, $name}))` | Any number of variable names interspersed with their assigned `NSExpression` values, followed by an `NSExpression` that may contain references to those variables. +Initializer parameter | Format string syntax | Description +----------------------|----------------------|------------ +`mgl_interpolate:withCurveType:parameters:stops:` | `mgl_interpolate:withCurveType:parameters:stops:($zoom, 'linear', x, nil, %@)` | Produces continuous, smooth results by interpolating between pairs of input and output values (“stops”). Compared to the `mgl_interpolateWithCurveType:parameters:stops:` custom function, the input expression (that function’s target) is instead passed in as the first argument to this function. +`mgl_step:from:stops:` | `mgl_step:from:stops:(x, 11, %@)` |Produces discrete, stepped results by evaluating a piecewise-constant function defined by pairs of input and output values ("stops"). Compared to the `mgl_stepWithMinimum:stops:` custom function, the input expression (that function’s target) is instead passed in as the first argument to this function. +`mgl_join:` | `mgl_join({'Old', 'MacDonald'})` | Returns the result of concatenating together all the elements of an array in order. Compared to the `stringByAppendingString:` custom function, this function takes only one argument, which is an aggregate expression containing the strings to concatenate. +`MGL_LET` | `MGL_LET('age', uppercase('old'), 'name', uppercase('MacDonald'), mgl_join({$age, $name}))` | Any number of variable names interspersed with their assigned `NSExpression` values, followed by an `NSExpression` that may contain references to those variables. Compared to the `mgl_expressionWithContext:` custom function, this function takes the variable names and values inline before the expression that contains references to those variables. The following custom functions are also available with the `+[NSExpression expressionForFunction:selectorName:arguments:]` method or the @@ -216,6 +218,22 @@ The following custom functions are also available with the </td> </tr> <tr> + <td><code>mgl_expressionWithContext:</code></td> + <td> + An `NSExpression` that may contain references to the variables defined in + the context dictionary. + </td> + <td> + An `NSDictionary` with `NSString`s as keys and `NSExpression`s as values. + Each key is a variable name and each value is the variable’s value within + the target expression. + </td> + <td> + The target expression with variable subexpressions replaced with the + values defined in the context dictionary. + </td> +</tr> +<tr> <td><code>mgl_interpolateWithCurveType:parameters:stops:</code></td> <td> An `NSExpression` that evaluates to a number and contains a variable or @@ -305,6 +323,18 @@ The following custom functions are also available with the </td> </tr> <tr> + <td><code>stringByAppendingString:</code></td> + <td> + An `NSExpression` that evaluates to a string. + </td> + <td> + One or more `NSExpression`s, each evaluating to a string. + </td> + <td> + The target string with each of the argument strings appended in order. + </td> +</tr> +<tr> <td><code>stringValue</code></td> <td> An `NSExpression` that evaluates to a Boolean value, number, or string. diff --git a/platform/darwin/src/NSExpression+MGLAdditions.mm b/platform/darwin/src/NSExpression+MGLAdditions.mm index 16c1656fb8..4ee0371e9d 100644 --- a/platform/darwin/src/NSExpression+MGLAdditions.mm +++ b/platform/darwin/src/NSExpression+MGLAdditions.mm @@ -15,28 +15,24 @@ #import <mbgl/style/expression/expression.hpp> -/** - Joins the given components into a single string by concatenating each component - in order. - */ -NSString *MGLJoinComponents(Class self, SEL _cmd, NSArray<NSString *> *components) { - return [components componentsJoinedByString:@""]; -} +@interface MGLAftermarketExpressionInstaller: NSObject +@end -/** - A placeholder for a method that evaluates an expression based on an arbitrary - number of variable names and assigned expressions. - */ -id MGLEvaluateWithContext(Class self, SEL _cmd, NSString *firstVariableName, ...) { - [NSException raise:NSInvalidArgumentException - format:@"Assignment expressions lack underlying Objective-C implementations."]; - return nil; -}; +@implementation MGLAftermarketExpressionInstaller + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self installFunctions]; + }); +} /** Adds to NSExpression’s built-in repertoire of functions. */ -void MGLInstallAftermarketExpressionFunctions() { ++ (void)installFunctions { + Class MGLAftermarketExpressionInstaller = [self class]; + // NSExpression’s built-in functions are backed by class methods on a // private class, so use a function expression to get at the class. // http://funwithobjc.tumblr.com/post/2922267976/using-custom-functions-with-nsexpression @@ -47,21 +43,67 @@ void MGLInstallAftermarketExpressionFunctions() { Class NSPredicateUtilities = objc_getMetaClass(className.UTF8String); #pragma clang push #pragma clang diagnostic ignored "-Wundeclared-selector" - class_addMethod(NSPredicateUtilities, @selector(mgl_join:), (IMP)MGLJoinComponents, "@@:@"); + #define INSTALL_METHOD(sel) \ + { \ + Method method = class_getInstanceMethod(MGLAftermarketExpressionInstaller, @selector(sel)); \ + class_addMethod(NSPredicateUtilities, @selector(sel), method_getImplementation(method), method_getTypeEncoding(method)); \ + } + #define INSTALL_CONTROL_STRUCTURE(sel) \ + { \ + Method method = class_getInstanceMethod(MGLAftermarketExpressionInstaller, @selector(sel:)); \ + class_addMethod(NSPredicateUtilities, @selector(sel), method_getImplementation(method), method_getTypeEncoding(method)); \ + class_addMethod(NSPredicateUtilities, @selector(sel:), method_getImplementation(method), method_getTypeEncoding(method)); \ + } - // Vararg aftermarket expressions need to be declared with an explicit and implicit first argument. - class_addMethod(NSPredicateUtilities, @selector(MGL_LET), (IMP)MGLEvaluateWithContext, "@@:@"); - class_addMethod(NSPredicateUtilities, @selector(MGL_LET:), (IMP)MGLEvaluateWithContext, "@@:@"); + // Install method-like functions, taking the number of arguments implied by + // the selector name. + INSTALL_METHOD(mgl_join:); + INSTALL_METHOD(mgl_interpolate:withCurveType:parameters:stops:); + INSTALL_METHOD(mgl_step:from:stops:); + + // Install functions that resemble control structures, taking arbitrary + // numbers of arguments. Vararg aftermarket functions need to be declared + // with an explicit and implicit first argument. + INSTALL_CONTROL_STRUCTURE(MGL_LET); + + #undef INSTALL_AFTERMARKET_FN #pragma clang pop } -@interface MGLAftermarketExpressionInstaller: NSObject -@end +/** + Joins the given components into a single string by concatenating each component + in order. + */ +- (NSString *)mgl_join:(NSArray<NSString *> *)components { + return [components componentsJoinedByString:@""]; +} -@implementation MGLAftermarketExpressionInstaller +/** + A placeholder for a method that evaluates an interpolation expression. + */ +- (id)mgl_interpolate:(id)inputExpression withCurveType:(NSString *)curveType parameters:(NSDictionary *)params stops:(NSDictionary *)stops { + [NSException raise:NSInvalidArgumentException + format:@"Interpolation expressions lack underlying Objective-C implementations."]; + return nil; +} -+ (void)load { - MGLInstallAftermarketExpressionFunctions(); +/** + A placeholder for a method that evaluates a step expression. + */ +- (id)mgl_step:(id)inputExpression from:(id)minimumExpression stops:(NSDictionary *)stops { + [NSException raise:NSInvalidArgumentException + format:@"Step expressions lack underlying Objective-C implementations."]; + return nil; +} + +/** + A placeholder for a method that evaluates an expression based on an arbitrary + number of variable names and assigned expressions. + */ +- (id)MGL_LET:(NSString *)firstVariableName, ... { + [NSException raise:NSInvalidArgumentException + format:@"Assignment expressions lack underlying Objective-C implementations."]; + return nil; } @end @@ -501,7 +543,7 @@ NSArray *MGLSubexpressionsWithJSONObjects(NSArray *objects) { } NSExpression *curveParameterExpression = [NSExpression mgl_expressionWithJSONObject:curveParameters]; argumentObjects = [argumentObjects subarrayWithRange:NSMakeRange(1, argumentObjects.count - 1)]; - NSExpression *operand = [NSExpression mgl_expressionWithJSONObject:argumentObjects.firstObject]; + NSExpression *inputExpression = [NSExpression mgl_expressionWithJSONObject:argumentObjects.firstObject]; NSArray *stopExpressions = [argumentObjects subarrayWithRange:NSMakeRange(1, argumentObjects.count - 1)]; NSMutableDictionary *stops = [NSMutableDictionary dictionaryWithCapacity:stopExpressions.count / 2]; NSEnumerator *stopEnumerator = stopExpressions.objectEnumerator; @@ -510,11 +552,10 @@ NSArray *MGLSubexpressionsWithJSONObjects(NSArray *objects) { stops[key] = [NSExpression mgl_expressionWithJSONObject:valueExpression]; } NSExpression *stopExpression = [NSExpression expressionForConstantValue:stops]; - return [NSExpression expressionForFunction:operand - selectorName:@"mgl_interpolateWithCurveType:parameters:stops:" - arguments:@[curveTypeExpression, curveParameterExpression, stopExpression]]; + return [NSExpression expressionForFunction:@"mgl_interpolate:withCurveType:parameters:stops:" + arguments:@[inputExpression, curveTypeExpression, curveParameterExpression, stopExpression]]; } else if ([op isEqualToString:@"step"]) { - NSExpression *operand = [NSExpression mgl_expressionWithJSONObject:argumentObjects[0]]; + NSExpression *inputExpression = [NSExpression mgl_expressionWithJSONObject:argumentObjects[0]]; NSArray *stopExpressions = [argumentObjects subarrayWithRange:NSMakeRange(1, argumentObjects.count - 1)]; NSExpression *minimum; if (stopExpressions.count % 2) { @@ -532,9 +573,8 @@ NSArray *MGLSubexpressionsWithJSONObjects(NSArray *objects) { } } NSExpression *stopExpression = [NSExpression expressionForConstantValue:stops]; - return [NSExpression expressionForFunction:operand - selectorName:@"mgl_stepWithMinimum:stops:" - arguments:@[minimum, stopExpression]]; + return [NSExpression expressionForFunction:@"mgl_step:from:stops:" + arguments:@[inputExpression, minimum, stopExpression]]; } else if ([op isEqualToString:@"zoom"]) { return [NSExpression expressionForVariable:@"zoomLevel"]; } else if ([op isEqualToString:@"heatmap-density"]) { @@ -753,40 +793,12 @@ NSArray *MGLSubexpressionsWithJSONObjects(NSArray *objects) { [expressionObject addObject:self.operand.mgl_jsonExpressionObject]; } return expressionObject; - } else if ([function isEqualToString:@"mgl_interpolateWithCurveType:parameters:stops:"]) { - if (self.arguments.count < 3) { - [NSException raise:NSInvalidArgumentException format: - @"Too few arguments to ‘mgl_interpolateWithCurveType:parameters:stops:’ function; expected 3 arguments."]; - } else if (self.arguments.count > 3) { - [NSException raise:NSInvalidArgumentException format: - @"%lu unexpected arguments to ‘mgl_interpolateWithCurveType:parameters:stops:’ function; expected 3 arguments.", - self.arguments.count - 3]; - } - NSString *curveType = self.arguments.firstObject.constantValue; - NSMutableArray *interpolationArray = [NSMutableArray arrayWithObject:curveType]; - if ([curveType isEqualToString:@"exponential"]) { - id base = [self.arguments[1] mgl_jsonExpressionObject]; - [interpolationArray addObject:base]; - } else if ([curveType isEqualToString:@"cubic-bezier"]) { - NSArray *controlPoints = [self.arguments[1].collection mgl_jsonExpressionObject]; - [interpolationArray addObjectsFromArray:controlPoints]; - } - NSMutableArray *expressionObject = [NSMutableArray arrayWithObjects:@"interpolate", interpolationArray, self.operand.mgl_jsonExpressionObject, nil]; - NSDictionary<NSNumber *, NSExpression *> *stops = self.arguments[2].constantValue; - for (NSNumber *key in [stops.allKeys sortedArrayUsingSelector:@selector(compare:)]) { - [expressionObject addObject:key]; - [expressionObject addObject:[stops[key] mgl_jsonExpressionObject]]; - } - return expressionObject; - } else if ([function isEqualToString:@"mgl_stepWithMinimum:stops:"]) { - id minimum = self.arguments.firstObject.mgl_jsonExpressionObject; - NSMutableArray *expressionObject = [NSMutableArray arrayWithObjects:@"step", self.operand.mgl_jsonExpressionObject, minimum, nil]; - NSDictionary<NSNumber *, NSExpression *> *stops = self.arguments[1].constantValue; - for (NSNumber *key in [stops.allKeys sortedArrayUsingSelector:@selector(compare:)]) { - [expressionObject addObject:key]; - [expressionObject addObject:[stops[key] mgl_jsonExpressionObject]]; - } - return expressionObject; + } else if ([function isEqualToString:@"mgl_interpolate:withCurveType:parameters:stops:"] + || [function isEqualToString:@"mgl_interpolateWithCurveType:parameters:stops:"]) { + return self.mgl_jsonInterpolationExpressionObject; + } else if ([function isEqualToString:@"mgl_step:from:stops:"] + || [function isEqualToString:@"mgl_stepWithMinimum:stops:"]) { + return self.mgl_jsonStepExpressionObject; } else if ([function isEqualToString:@"mgl_expressionWithContext:"]) { id context = self.arguments.firstObject; if ([context isKindOfClass:[NSExpression class]]) { @@ -918,4 +930,50 @@ NSArray *MGLSubexpressionsWithJSONObjects(NSArray *objects) { return nil; } +- (id)mgl_jsonInterpolationExpressionObject { + NSUInteger expectedArgumentCount = [self.function componentsSeparatedByString:@":"].count - 1; + if (self.arguments.count < expectedArgumentCount) { + [NSException raise:NSInvalidArgumentException format: + @"Too few arguments to ‘%@’ function; expected %lu arguments.", + self.function, expectedArgumentCount]; + } else if (self.arguments.count > expectedArgumentCount) { + [NSException raise:NSInvalidArgumentException format: + @"%lu unexpected arguments to ‘%@’ function; expected %lu arguments.", + self.arguments.count - expectedArgumentCount, self.function, expectedArgumentCount]; + } + + BOOL isAftermarketFunction = [self.function isEqualToString:@"mgl_interpolate:withCurveType:parameters:stops:"]; + NSUInteger curveTypeIndex = isAftermarketFunction ? 1 : 0; + NSString *curveType = self.arguments[curveTypeIndex].constantValue; + NSMutableArray *interpolationArray = [NSMutableArray arrayWithObject:curveType]; + if ([curveType isEqualToString:@"exponential"]) { + id base = [self.arguments[curveTypeIndex + 1] mgl_jsonExpressionObject]; + [interpolationArray addObject:base]; + } else if ([curveType isEqualToString:@"cubic-bezier"]) { + NSArray *controlPoints = [self.arguments[curveTypeIndex + 1].collection mgl_jsonExpressionObject]; + [interpolationArray addObjectsFromArray:controlPoints]; + } + NSMutableArray *expressionObject = [NSMutableArray arrayWithObjects:@"interpolate", interpolationArray, nil]; + [expressionObject addObject:(isAftermarketFunction ? self.arguments.firstObject : self.operand).mgl_jsonExpressionObject]; + NSDictionary<NSNumber *, NSExpression *> *stops = self.arguments[curveTypeIndex + 2].constantValue; + for (NSNumber *key in [stops.allKeys sortedArrayUsingSelector:@selector(compare:)]) { + [expressionObject addObject:key]; + [expressionObject addObject:[stops[key] mgl_jsonExpressionObject]]; + } + return expressionObject; +} + +- (id)mgl_jsonStepExpressionObject { + BOOL isAftermarketFunction = [self.function isEqualToString:@"mgl_step:from:stops:"]; + NSUInteger minimumIndex = isAftermarketFunction ? 1 : 0; + id minimum = self.arguments[minimumIndex].mgl_jsonExpressionObject; + NSMutableArray *expressionObject = [NSMutableArray arrayWithObjects:@"step", (isAftermarketFunction ? self.arguments.firstObject : self.operand).mgl_jsonExpressionObject, minimum, nil]; + NSDictionary<NSNumber *, NSExpression *> *stops = self.arguments[minimumIndex + 1].constantValue; + for (NSNumber *key in [stops.allKeys sortedArrayUsingSelector:@selector(compare:)]) { + [expressionObject addObject:key]; + [expressionObject addObject:[stops[key] mgl_jsonExpressionObject]]; + } + return expressionObject; +} + @end diff --git a/platform/darwin/test/MGLExpressionTests.mm b/platform/darwin/test/MGLExpressionTests.mm index 94c9410892..b37c880783 100644 --- a/platform/darwin/test/MGLExpressionTests.mm +++ b/platform/darwin/test/MGLExpressionTests.mm @@ -594,30 +594,34 @@ using namespace std::string_literals; - (void)testInterpolationExpressionObject { { NSDictionary *stops = @{@0: MGLConstantExpression(@100), @10: MGLConstantExpression(@200)}; - NSExpression *expression = [NSExpression expressionWithFormat:@"FUNCTION(x, 'mgl_interpolateWithCurveType:parameters:stops:', 'linear', nil, %@)", stops]; + NSExpression *expression = [NSExpression expressionWithFormat:@"mgl_interpolate:withCurveType:parameters:stops:(x, 'linear', nil, %@)", stops]; + NSExpression *compatibilityExpression = [NSExpression expressionWithFormat:@"FUNCTION(x, 'mgl_interpolateWithCurveType:parameters:stops:', 'linear', nil, %@)", stops]; NSArray *jsonExpression = @[@"interpolate", @[@"linear"], @[@"get", @"x"], @0, @100, @10, @200]; XCTAssertEqualObjects(expression.mgl_jsonExpressionObject, jsonExpression); + XCTAssertEqualObjects(compatibilityExpression.mgl_jsonExpressionObject, jsonExpression); XCTAssertEqualObjects([NSExpression mgl_expressionWithJSONObject:jsonExpression], expression); } { NSDictionary *stops = @{@1: MGLConstantExpression(@2), @3: MGLConstantExpression(@6)}; - NSExpression *expression = [NSExpression expressionWithFormat:@"FUNCTION(x, 'mgl_interpolateWithCurveType:parameters:stops:', 'exponential', 2, %@)", stops]; + NSExpression *expression = [NSExpression expressionWithFormat:@"mgl_interpolate:withCurveType:parameters:stops:(x, 'exponential', 2, %@)", stops]; NSArray *jsonExpression = @[@"interpolate", @[@"exponential", @2], @[@"get", @"x"], @1, @2, @3, @6]; XCTAssertEqualObjects(expression.mgl_jsonExpressionObject, jsonExpression); XCTAssertEqualObjects([NSExpression mgl_expressionWithJSONObject:jsonExpression], expression); } { NSDictionary *stops = @{@0: MGLConstantExpression(@0), @100: MGLConstantExpression(@100)}; - NSExpression *expression = [NSExpression expressionWithFormat:@"FUNCTION(x, 'mgl_interpolateWithCurveType:parameters:stops:', 'cubic-bezier', { 0.42, 0, 0.58, 1 }, %@)", stops]; + NSExpression *expression = [NSExpression expressionWithFormat:@"mgl_interpolate:withCurveType:parameters:stops:(x, 'cubic-bezier', { 0.42, 0, 0.58, 1 }, %@)", stops]; NSArray *jsonExpression = @[@"interpolate", @[@"cubic-bezier", @0.42, @0, @0.58, @1], @[@"get", @"x"], @0, @0, @100, @100]; XCTAssertEqualObjects(expression.mgl_jsonExpressionObject, jsonExpression); XCTAssertEqualObjects([NSExpression mgl_expressionWithJSONObject:jsonExpression], expression); } { NSDictionary *stops = @{@0: MGLConstantExpression(@111), @1: MGLConstantExpression(@1111)}; - NSExpression *expression = [NSExpression expressionWithFormat:@"FUNCTION(x, 'mgl_stepWithMinimum:stops:', 11, %@)", stops]; + NSExpression *expression = [NSExpression expressionWithFormat:@"mgl_step:from:stops:(x, 11, %@)", stops]; + NSExpression *compatibilityExpression = [NSExpression expressionWithFormat:@"FUNCTION(x, 'mgl_stepWithMinimum:stops:', 11, %@)", stops]; NSArray *jsonExpression = @[@"step", @[@"get", @"x"], @11, @0, @111, @1, @1111]; XCTAssertEqualObjects(expression.mgl_jsonExpressionObject, jsonExpression); + XCTAssertEqualObjects(compatibilityExpression.mgl_jsonExpressionObject, jsonExpression); XCTAssertEqualObjects([NSExpression mgl_expressionWithJSONObject:jsonExpression], expression); } } |