summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJordan Kiley <jmkiley@users.noreply.github.com>2019-10-11 13:46:41 -0700
committerGitHub <noreply@github.com>2019-10-11 13:46:41 -0700
commite4e2a78033f25fd966fa9cbbd2babb7e7b499e18 (patch)
tree6de20c867cab7e548d9cb5bfc46a0e74e63dc2bb
parenta1f124c39442d76af77a5aa63bba5afa4f4d6d26 (diff)
downloadqtlocation-mapboxgl-e4e2a78033f25fd966fa9cbbd2babb7e7b499e18.tar.gz
Add iOS bindings for cluster properties (#15515)
-rw-r--r--platform/darwin/src/MGLShapeSource.h32
-rw-r--r--platform/darwin/src/MGLShapeSource.mm53
-rw-r--r--platform/darwin/src/MGLStyleValue.mm13
-rw-r--r--platform/darwin/src/MGLStyleValue_Private.h5
-rw-r--r--platform/darwin/src/NSExpression+MGLAdditions.h7
-rw-r--r--platform/darwin/src/NSExpression+MGLAdditions.mm43
-rw-r--r--platform/darwin/test/MGLDocumentationExampleTests.swift34
-rw-r--r--platform/darwin/test/MGLExpressionTests.mm55
-rw-r--r--platform/darwin/test/MGLShapeSourceTests.mm5
-rw-r--r--platform/ios/CHANGELOG.md4
-rw-r--r--src/mbgl/style/sources/geojson_source_impl.cpp1
11 files changed, 244 insertions, 8 deletions
diff --git a/platform/darwin/src/MGLShapeSource.h b/platform/darwin/src/MGLShapeSource.h
index a57b963c63..675c219300 100644
--- a/platform/darwin/src/MGLShapeSource.h
+++ b/platform/darwin/src/MGLShapeSource.h
@@ -42,6 +42,38 @@ FOUNDATION_EXTERN MGL_EXPORT const MGLShapeSourceOption MGLShapeSourceOptionClus
FOUNDATION_EXTERN MGL_EXPORT const MGLShapeSourceOption MGLShapeSourceOptionClusterRadius;
/**
+ An `NSDictionary` object where the key is an `NSString`. The dictionary key will
+ be the feature attribute key. The resulting attribute value is
+ aggregated from the clustered points. The dictionary value is an `NSArray`
+ consisting of two `NSExpression` objects.
+
+ The first object determines how the attribute values are accumulated from the
+ cluster points. It is an `NSExpression` with an expression function that accepts
+ two or more arguments, such as `sum` or `max`. The arguments should be
+ `featureAccumulated` and the previously defined feature attribute key. The
+ resulting value is assigned to the specified attribute key.
+
+ The second `NSExpression` in the array determines which
+ attribute values are accessed from individual features within a cluster.
+
+ ```swift
+ let firstExpression = NSExpression(format: "sum:({$featureAccumulated, sumValue})")
+ let secondExpression = NSExpression(forKeyPath: "magnitude")
+ let clusterPropertiesDictionary = ["sumValue" : [firstExpression, secondExpression]]
+
+ let options : [MGLShapeSourceOption : Any] = [.clustered : true,
+ .clusterProperties: clusterPropertiesDictionary]
+ ```
+
+ This option corresponds to the
+ <a href="https://www.mapbox.com/mapbox-gl-style-spec/#sources-geojson-clusterProperties"><code>clusterProperties</code></a>
+ source property in the Mapbox Style Specification.
+
+ This option only affects point features within an `MGLShapeSource` object; it
+ is ignored when creating an `MGLComputedShapeSource` object.
+ */
+FOUNDATION_EXTERN MGL_EXPORT const MGLShapeSourceOption MGLShapeSourceOptionClusterProperties;
+/**
An `NSNumber` object containing an integer; specifies the maximum zoom level at
which to cluster points if clustering is enabled. Defaults to one zoom level
less than the value of `MGLShapeSourceOptionMaximumZoomLevel` so that, at the
diff --git a/platform/darwin/src/MGLShapeSource.mm b/platform/darwin/src/MGLShapeSource.mm
index 3628a0eb74..3820fe9d60 100644
--- a/platform/darwin/src/MGLShapeSource.mm
+++ b/platform/darwin/src/MGLShapeSource.mm
@@ -3,6 +3,7 @@
#import "MGLLoggingConfiguration_Private.h"
#import "MGLStyle_Private.h"
+#import "MGLStyleValue_Private.h"
#import "MGLMapView_Private.h"
#import "MGLSource_Private.h"
#import "MGLFeature_Private.h"
@@ -19,6 +20,7 @@
const MGLShapeSourceOption MGLShapeSourceOptionBuffer = @"MGLShapeSourceOptionBuffer";
const MGLShapeSourceOption MGLShapeSourceOptionClusterRadius = @"MGLShapeSourceOptionClusterRadius";
const MGLShapeSourceOption MGLShapeSourceOptionClustered = @"MGLShapeSourceOptionClustered";
+const MGLShapeSourceOption MGLShapeSourceOptionClusterProperties = @"MGLShapeSourceOptionClusterProperties";
const MGLShapeSourceOption MGLShapeSourceOptionMaximumZoomLevel = @"MGLShapeSourceOptionMaximumZoomLevel";
const MGLShapeSourceOption MGLShapeSourceOptionMaximumZoomLevelForClustering = @"MGLShapeSourceOptionMaximumZoomLevelForClustering";
const MGLShapeSourceOption MGLShapeSourceOptionMinimumZoomLevel = @"MGLShapeSourceOptionMinimumZoomLevel";
@@ -84,6 +86,57 @@ mbgl::style::GeoJSONOptions MGLGeoJSONOptionsFromDictionary(NSDictionary<MGLShap
geoJSONOptions.cluster = value.boolValue;
}
+ if (NSDictionary *value = options[MGLShapeSourceOptionClusterProperties]) {
+ if (![value isKindOfClass:[NSDictionary<NSString *, NSArray *> class]]) {
+ [NSException raise:NSInvalidArgumentException
+ format:@"MGLShapeSourceOptionClusterProperties must be an NSDictionary with an NSString as a key and an array containing two NSExpression objects as a value."];
+ }
+
+ NSEnumerator *stringEnumerator = [value keyEnumerator];
+ NSString *key;
+
+ while (key = [stringEnumerator nextObject]) {
+ NSArray *expressionsArray = value[key];
+ if (![expressionsArray isKindOfClass:[NSArray class]]) {
+ [NSException raise:NSInvalidArgumentException
+ format:@"MGLShapeSourceOptionClusterProperties dictionary member value must be an array containing two objects."];
+ }
+ // Check that the array has 2 values. One should be a the reduce expression and one should be the map expression.
+ if ([expressionsArray count] != 2) {
+ [NSException raise:NSInvalidArgumentException
+ format:@"MGLShapeSourceOptionClusterProperties member value requires array of two objects."];
+ }
+
+ // reduceExpression should be a valid NSExpression
+ NSExpression *reduceExpression = expressionsArray[0];
+ if (![reduceExpression isKindOfClass:[NSExpression class]]) {
+ [NSException raise:NSInvalidArgumentException
+ format:@"MGLShapeSourceOptionClusterProperties array value requires two expression objects."];
+ }
+ auto reduce = MGLClusterPropertyFromNSExpression(reduceExpression);
+ if (!reduce) {
+ [NSException raise:NSInvalidArgumentException
+ format:@"Failed to convert MGLShapeSourceOptionClusterProperties reduce expression."];
+ }
+
+ // mapExpression should be a valid NSExpression
+ NSExpression *mapExpression = expressionsArray[1];
+ if (![mapExpression isKindOfClass:[NSExpression class]]) {
+ [NSException raise:NSInvalidArgumentException
+ format:@"MGLShapeSourceOptionClusterProperties member value must contain a valid NSExpression."];
+ }
+ auto map = MGLClusterPropertyFromNSExpression(mapExpression);
+ if (!map) {
+ [NSException raise:NSInvalidArgumentException
+ format:@"Failed to convert MGLShapeSourceOptionClusterProperties map expression."];
+ }
+
+ std::string keyString = std::string([key UTF8String]);
+
+ geoJSONOptions.clusterProperties.emplace(keyString, std::make_pair(std::move(map), std::move(reduce)));
+ }
+ }
+
if (NSNumber *value = options[MGLShapeSourceOptionLineDistanceMetrics]) {
if (![value isKindOfClass:[NSNumber class]]) {
[NSException raise:NSInvalidArgumentException
diff --git a/platform/darwin/src/MGLStyleValue.mm b/platform/darwin/src/MGLStyleValue.mm
index 5103b5f5cf..01ad108d7f 100644
--- a/platform/darwin/src/MGLStyleValue.mm
+++ b/platform/darwin/src/MGLStyleValue.mm
@@ -44,3 +44,16 @@ id MGLJSONObjectFromMBGLValue(const mbgl::Value &value) {
id MGLJSONObjectFromMBGLExpression(const mbgl::style::expression::Expression &mbglExpression) {
return MGLJSONObjectFromMBGLValue(mbglExpression.serialize());
}
+
+
+std::unique_ptr<mbgl::style::expression::Expression> MGLClusterPropertyFromNSExpression(NSExpression *expression) {
+ if (!expression) {
+ return nullptr;
+ }
+
+ NSArray *jsonExpression = expression.mgl_jsonExpressionObject;
+
+ auto expr = mbgl::style::expression::dsl::createExpression(mbgl::style::conversion::makeConvertible(jsonExpression));
+
+ return expr;
+}
diff --git a/platform/darwin/src/MGLStyleValue_Private.h b/platform/darwin/src/MGLStyleValue_Private.h
index 376bf5e73b..82ce232c6b 100644
--- a/platform/darwin/src/MGLStyleValue_Private.h
+++ b/platform/darwin/src/MGLStyleValue_Private.h
@@ -12,12 +12,15 @@
#include <mbgl/style/conversion/color_ramp_property_value.hpp>
#include <mbgl/style/conversion/property_value.hpp>
#include <mbgl/style/conversion/position.hpp>
+#include <mbgl/style/expression/dsl.hpp>
#import <mbgl/style/transition_options.hpp>
#import <mbgl/style/types.hpp>
#import <mbgl/util/enum.hpp>
#include <mbgl/util/interpolate.hpp>
+#include <memory>
+
#if TARGET_OS_IPHONE
#import "UIColor+MGLAdditions.h"
#else
@@ -45,6 +48,8 @@ NS_INLINE mbgl::style::TransitionOptions MGLOptionsFromTransition(MGLTransition
return options;
}
+std::unique_ptr<mbgl::style::expression::Expression> MGLClusterPropertyFromNSExpression(NSExpression *expression);
+
id MGLJSONObjectFromMBGLExpression(const mbgl::style::expression::Expression &mbglExpression);
template <typename MBGLType, typename ObjCType, typename MBGLElement = MBGLType, typename ObjCEnum = ObjCType>
diff --git a/platform/darwin/src/NSExpression+MGLAdditions.h b/platform/darwin/src/NSExpression+MGLAdditions.h
index 2a33367e9c..2109310e69 100644
--- a/platform/darwin/src/NSExpression+MGLAdditions.h
+++ b/platform/darwin/src/NSExpression+MGLAdditions.h
@@ -86,6 +86,13 @@ FOUNDATION_EXTERN MGL_EXPORT const MGLExpressionInterpolationMode MGLExpressionI
/**
`NSExpression` variable that corresponds to the
+ <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/#accumulated"><code>id</code></a>
+ expression operator in the Mapbox Style Specification.
+ */
+@property (class, nonatomic, readonly) NSExpression *featureAccumulatedVariableExpression;
+
+/**
+ `NSExpression` variable that corresponds to the
<a href="https://www.mapbox.com/mapbox-gl-js/style-spec/#expressions-properties"><code>properties</code></a>
expression operator in the Mapbox Style Specification.
*/
diff --git a/platform/darwin/src/NSExpression+MGLAdditions.mm b/platform/darwin/src/NSExpression+MGLAdditions.mm
index 2ca4e0ed88..f139b86a88 100644
--- a/platform/darwin/src/NSExpression+MGLAdditions.mm
+++ b/platform/darwin/src/NSExpression+MGLAdditions.mm
@@ -553,6 +553,10 @@ const MGLExpressionInterpolationMode MGLExpressionInterpolationModeCubicBezier =
return [NSExpression expressionForVariable:@"lineProgress"];
}
++ (NSExpression *)featureAccumulatedVariableExpression {
+ return [NSExpression expressionForVariable:@"featureAccumulated"];
+}
+
+ (NSExpression *)geometryTypeVariableExpression {
return [NSExpression expressionForVariable:@"geometryType"];
}
@@ -648,7 +652,6 @@ NSArray *MGLSubexpressionsWithJSONObjects(NSArray *objects) {
@"let": @"MGL_LET",
};
});
-
if (!object || object == [NSNull null]) {
return [NSExpression expressionForConstantValue:nil];
}
@@ -667,11 +670,10 @@ NSArray *MGLSubexpressionsWithJSONObjects(NSArray *objects) {
}];
return [NSExpression expressionForConstantValue:dictionary];
}
-
if ([object isKindOfClass:[NSArray class]]) {
NSArray *array = (NSArray *)object;
NSString *op = array.firstObject;
-
+
if (![op isKindOfClass:[NSString class]]) {
NSArray *subexpressions = MGLSubexpressionsWithJSONObjects(array);
return [NSExpression expressionForFunction:@"MGL_FUNCTION" arguments:subexpressions];
@@ -839,6 +841,8 @@ NSArray *MGLSubexpressionsWithJSONObjects(NSArray *objects) {
return NSExpression.heatmapDensityVariableExpression;
} else if ([op isEqualToString:@"line-progress"]) {
return NSExpression.lineProgressVariableExpression;
+ } else if ([op isEqualToString:@"accumulated"]) {
+ return NSExpression.featureAccumulatedVariableExpression;
} else if ([op isEqualToString:@"geometry-type"]) {
return NSExpression.geometryTypeVariableExpression;
} else if ([op isEqualToString:@"id"]) {
@@ -961,6 +965,9 @@ NSArray *MGLSubexpressionsWithJSONObjects(NSArray *objects) {
if ([self.variable isEqualToString:@"zoomLevel"]) {
return @[@"zoom"];
}
+ if ([self.variable isEqualToString:@"featureAccumulated"]) {
+ return @[@"accumulated"];
+ }
if ([self.variable isEqualToString:@"geometryType"]) {
return @[@"geometry-type"];
}
@@ -1046,6 +1053,8 @@ NSArray *MGLSubexpressionsWithJSONObjects(NSArray *objects) {
case NSFunctionExpressionType: {
NSString *function = self.function;
+
+ BOOL hasCollectionProperty = !( ! [self.arguments.firstObject isKindOfClass: [NSExpression class]] || self.arguments.firstObject.expressionType != NSAggregateExpressionType || self.arguments.firstObject.expressionType == NSSubqueryExpressionType);
NSString *op = MGLExpressionOperatorsByFunctionNames[function];
if (op) {
NSArray *arguments = self.arguments.mgl_jsonExpressionObject;
@@ -1057,16 +1066,31 @@ NSArray *MGLSubexpressionsWithJSONObjects(NSArray *objects) {
NSExpression *count = [NSExpression expressionForFunction:@"count:" arguments:self.arguments];
return [NSExpression expressionForFunction:@"divide:by:" arguments:@[sum, count]].mgl_jsonExpressionObject;
} else if ([function isEqualToString:@"sum:"]) {
- NSArray *arguments = [self.arguments.firstObject.collection valueForKeyPath:@"mgl_jsonExpressionObject"];
+ NSArray *arguments;
+ if (hasCollectionProperty) {
+ arguments = [self.arguments.firstObject.collection valueForKeyPath:@"mgl_jsonExpressionObject"];
+ } else {
+ arguments = [self.arguments valueForKeyPath:@"mgl_jsonExpressionObject"];
+ }
return [@[@"+"] arrayByAddingObjectsFromArray:arguments];
} else if ([function isEqualToString:@"count:"]) {
NSArray *arguments = self.arguments.firstObject.mgl_jsonExpressionObject;
return @[@"length", arguments];
} else if ([function isEqualToString:@"min:"]) {
- NSArray *arguments = [self.arguments.firstObject.collection valueForKeyPath:@"mgl_jsonExpressionObject"];
+ NSArray *arguments;
+ if (!hasCollectionProperty) {
+ arguments = [self.arguments valueForKeyPath:@"mgl_jsonExpressionObject"];
+ } else {
+ arguments = [self.arguments.firstObject.collection valueForKeyPath:@"mgl_jsonExpressionObject"];
+ }
return [@[@"min"] arrayByAddingObjectsFromArray:arguments];
} else if ([function isEqualToString:@"max:"]) {
- NSArray *arguments = [self.arguments.firstObject.collection valueForKeyPath:@"mgl_jsonExpressionObject"];
+ NSArray *arguments;
+ if (!hasCollectionProperty) {
+ arguments = [self.arguments valueForKeyPath:@"mgl_jsonExpressionObject"];
+ } else {
+ arguments = [self.arguments.firstObject.collection valueForKeyPath:@"mgl_jsonExpressionObject"];
+ }
return [@[@"max"] arrayByAddingObjectsFromArray:arguments];
} else if ([function isEqualToString:@"exp:"]) {
return [NSExpression expressionForFunction:@"raise:toPower:" arguments:@[@(M_E), self.arguments.firstObject]].mgl_jsonExpressionObject;
@@ -1074,7 +1098,12 @@ NSArray *MGLSubexpressionsWithJSONObjects(NSArray *objects) {
return [NSExpression expressionWithFormat:@"%@ - modulus:by:(%@, 1)",
self.arguments.firstObject, self.arguments.firstObject].mgl_jsonExpressionObject;
} else if ([function isEqualToString:@"mgl_join:"]) {
- NSArray *arguments = [self.arguments.firstObject.collection valueForKeyPath:@"mgl_jsonExpressionObject"];
+ NSArray *arguments;
+ if (!hasCollectionProperty) {
+ arguments = [self.arguments valueForKeyPath:@"mgl_jsonExpressionObject"];
+ } else {
+ arguments = [self.arguments.firstObject.collection valueForKeyPath:@"mgl_jsonExpressionObject"];
+ }
return [@[@"concat"] arrayByAddingObjectsFromArray:arguments];
} else if ([function isEqualToString:@"stringByAppendingString:"]) {
NSArray *arguments = self.arguments.mgl_jsonExpressionObject;
diff --git a/platform/darwin/test/MGLDocumentationExampleTests.swift b/platform/darwin/test/MGLDocumentationExampleTests.swift
index 9fbb0cc329..7d6bdbed54 100644
--- a/platform/darwin/test/MGLDocumentationExampleTests.swift
+++ b/platform/darwin/test/MGLDocumentationExampleTests.swift
@@ -554,7 +554,39 @@ class MGLDocumentationExampleTests: XCTestCase, MGLMapViewDelegate {
XCTAssertNotNil(attributedExpression)
}
-
+
+ func testMGLShapeSourceOptionClusterProperties() {
+ //#-example-code
+ let firstExpression = NSExpression(format: "sum:({$featureAccumulated, sumValue})")
+ let secondExpression = NSExpression(forKeyPath: "magnitude")
+ let clusterPropertiesDictionary = ["sumValue" : [firstExpression, secondExpression]]
+
+ let options : [MGLShapeSourceOption : Any] = [.clustered : true,
+ .clusterProperties: clusterPropertiesDictionary]
+ //#-end-example-code
+ let geoJSON: [String: Any] = [
+ "type" : "Feature",
+ "geometry" : [
+ "coordinates" : [
+ -77.00896639534831,
+ 38.87031006108791,
+ 0.0
+ ],
+ "type" : "Point"
+ ],
+ "properties" : [
+ "cluster" : true,
+ "cluster_id" : 123,
+ "point_count" : 4567,
+ ]
+ ]
+
+ let clusterShapeData = try! JSONSerialization.data(withJSONObject: geoJSON, options: [])
+ let shape = try! MGLShape(data: clusterShapeData, encoding: String.Encoding.utf8.rawValue)
+ let source = MGLShapeSource(identifier: "source", shape: shape, options: options)
+ mapView.style?.addSource(source)
+
+ }
// For testMGLMapView().
func myCustomFunction() {}
}
diff --git a/platform/darwin/test/MGLExpressionTests.mm b/platform/darwin/test/MGLExpressionTests.mm
index f1fe3ea878..4ccd7adb6e 100644
--- a/platform/darwin/test/MGLExpressionTests.mm
+++ b/platform/darwin/test/MGLExpressionTests.mm
@@ -180,6 +180,13 @@ using namespace std::string_literals;
XCTAssertEqualObjects([expression expressionValueWithObject:nil context:context], @1);
}
{
+ NSExpression *expression = [NSExpression expressionForVariable:@"featureAccumulated"];
+ XCTAssertEqualObjects(expression.mgl_jsonExpressionObject, @[@"accumulated"]);
+ XCTAssertEqualObjects([NSExpression expressionWithFormat:@"$featureAccumulated"].mgl_jsonExpressionObject, @[@"accumulated"]);
+ XCTAssertEqualObjects([NSExpression expressionWithMGLJSONObject:@[@"accumulated"]], expression);
+ }
+
+ {
NSExpression *expression = [NSExpression expressionForVariable:@"geometryType"];
XCTAssertEqualObjects(expression.mgl_jsonExpressionObject, @[@"geometry-type"]);
XCTAssertEqualObjects([NSExpression expressionWithFormat:@"$geometryType"].mgl_jsonExpressionObject, @[@"geometry-type"]);
@@ -381,6 +388,26 @@ using namespace std::string_literals;
XCTAssertEqualObjects([NSExpression expressionWithMGLJSONObject:jsonExpression], expression);
}
{
+ NSExpression *testExpression = [NSExpression expressionWithFormat:@"sum:({1, 1, 2})"];
+ NSExpression *expression = [NSExpression expressionForFunction:@"sum:" arguments:@[[NSExpression expressionForAggregate:@[MGLConstantExpression(@1), MGLConstantExpression(@1), MGLConstantExpression(@2)]]]];
+
+ NSArray *jsonExpression = @[@"+", @1, @1, @2];
+
+ XCTAssertEqualObjects(testExpression.mgl_jsonExpressionObject, jsonExpression);
+ XCTAssertEqualObjects(expression.mgl_jsonExpressionObject, jsonExpression);
+ XCTAssertEqualObjects(expression, testExpression);
+ }
+ {
+ NSExpression *expression = [NSExpression expressionForFunction:@"sum:" arguments:@[MGLConstantExpression(@1), MGLConstantExpression(@1), MGLConstantExpression(@2)]];
+ NSArray *jsonExpression = @[@"+", @1, @1, @2];
+ XCTAssertEqualObjects(expression.mgl_jsonExpressionObject, jsonExpression);
+
+ // - [NSExpression expressionWithMGLJSONObject:] creates an expression with an aggregate expression as an argument. This is not equal to an expression with an array of expressions as an argument. For testing purposes, we will compare their operands and arrays of expressions.
+ NSExpression *aggregateExpression = [NSExpression expressionWithMGLJSONObject:jsonExpression];
+ XCTAssertEqualObjects(aggregateExpression.operand, expression.operand);
+ XCTAssertEqualObjects(aggregateExpression.arguments.firstObject.collection, expression.arguments);
+ }
+ {
NSArray *threeArguments = @[MGLConstantExpression(@1), MGLConstantExpression(@1), MGLConstantExpression(@1)];
NSExpression *expression = [NSExpression expressionForFunction:@"add:to:" arguments:threeArguments];
NSArray *jsonExpression = @[@"+", @1, @1, @1];
@@ -418,6 +445,24 @@ using namespace std::string_literals;
XCTAssertEqualObjects([NSExpression expressionWithMGLJSONObject:jsonExpression], expression);
}
{
+ NSExpression *expression = [NSExpression expressionForFunction:@"max:" arguments:arguments];
+ NSArray *jsonExpression = @[@"max", @1, @1];
+ XCTAssertEqualObjects(expression.mgl_jsonExpressionObject, jsonExpression);
+
+ NSExpression *aggregateExpression = [NSExpression expressionWithMGLJSONObject:jsonExpression];
+ XCTAssertEqualObjects(aggregateExpression.operand, expression.operand);
+ XCTAssertEqualObjects(aggregateExpression.arguments.firstObject.collection, expression.arguments);
+ }
+ {
+ NSExpression *expression = [NSExpression expressionForFunction:@"min:" arguments:arguments];
+ NSArray *jsonExpression = @[@"min", @1, @1];
+ XCTAssertEqualObjects(expression.mgl_jsonExpressionObject, jsonExpression);
+
+ NSExpression *aggregateExpression = [NSExpression expressionWithMGLJSONObject:jsonExpression];
+ XCTAssertEqualObjects(aggregateExpression.operand, expression.operand);
+ XCTAssertEqualObjects(aggregateExpression.arguments.firstObject.collection, expression.arguments);
+ }
+ {
NSExpression *expression = [NSExpression expressionForFunction:@"ceiling:" arguments:@[MGLConstantExpression(@1.5)]];
NSArray *jsonExpression = @[@"ceil", @1.5];
XCTAssertEqualObjects(expression.mgl_jsonExpressionObject, jsonExpression);
@@ -622,6 +667,16 @@ using namespace std::string_literals;
XCTAssertEqualObjects([NSExpression expressionWithMGLJSONObject:jsonExpression], aftermarketExpression);
}
{
+ NSExpression *expression = [NSExpression expressionForFunction:@"mgl_join:" arguments:@[@"Old", @"MacDonald"]];
+ NSExpression *aftermarketExpression = [NSExpression expressionWithFormat:@"mgl_join({'Old', 'MacDonald'})"];
+ NSArray *jsonExpression = @[@"concat", @"Old", @"MacDonald"];
+ XCTAssertEqualObjects(expression.mgl_jsonExpressionObject, jsonExpression);
+
+ XCTAssertEqualObjects(aftermarketExpression.mgl_jsonExpressionObject, expression.mgl_jsonExpressionObject);
+ NSExpression *aggregateExpression = [NSExpression expressionWithMGLJSONObject:jsonExpression];
+ XCTAssertEqualObjects(aggregateExpression.operand, expression.operand);
+ }
+ {
NSExpression *expression = [NSExpression expressionForFunction:@"uppercase:" arguments:arguments];
NSArray *jsonExpression = @[@"upcase", @"MacDonald"];
XCTAssertEqualObjects(expression.mgl_jsonExpressionObject, jsonExpression);
diff --git a/platform/darwin/test/MGLShapeSourceTests.mm b/platform/darwin/test/MGLShapeSourceTests.mm
index 3459fb1733..3bf3ef04bd 100644
--- a/platform/darwin/test/MGLShapeSourceTests.mm
+++ b/platform/darwin/test/MGLShapeSourceTests.mm
@@ -13,8 +13,12 @@
@implementation MGLShapeSourceTests
- (void)testGeoJSONOptionsFromDictionary {
+ NSExpression *reduceExpression = [NSExpression expressionForFunction:@"sum:" arguments:@[[NSExpression expressionForKeyPath:@"featureAccumulated"], [NSExpression expressionForKeyPath:@"sumValue"]]];
+ NSExpression *mapExpression = [NSExpression expressionForKeyPath:@"mag"];
+ NSArray *clusterPropertyArray = @[reduceExpression, mapExpression];
NSDictionary *options = @{MGLShapeSourceOptionClustered: @YES,
MGLShapeSourceOptionClusterRadius: @42,
+ MGLShapeSourceOptionClusterProperties: @{@"sumValue": clusterPropertyArray},
MGLShapeSourceOptionMaximumZoomLevelForClustering: @98,
MGLShapeSourceOptionMaximumZoomLevel: @99,
MGLShapeSourceOptionBuffer: @1976,
@@ -29,6 +33,7 @@
XCTAssertEqual(mbglOptions.buffer, 1976);
XCTAssertEqual(mbglOptions.tolerance, 0.42);
XCTAssertTrue(mbglOptions.lineMetrics);
+ XCTAssertTrue(!mbglOptions.clusterProperties.empty());
options = @{MGLShapeSourceOptionClustered: @"number 1"};
XCTAssertThrows(MGLGeoJSONOptionsFromDictionary(options));
diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md
index 5a7235b25a..03ed8cdc46 100644
--- a/platform/ios/CHANGELOG.md
+++ b/platform/ios/CHANGELOG.md
@@ -14,6 +14,10 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT
* Improved rendering performance for the styles with multiple sources ([#15756](https://github.com/mapbox/mapbox-gl-native/pull/15756))
+### Styles and rendering
+
+* Added an `MGLShapeSourceOptionClusterProperties` option that allows styling individual clusters based on aggregated feature data. ([#15515](https://github.com/mapbox/mapbox-gl-native/pull/15515))
+
### Other changes
* Added `-[MGLMapSnapshotOverlay coordinateForPoint:]` and `-[MGLMapSnapshotOverlay pointForCoordinate:]` to convert between context and map coordinates, mirroring those of `MGLMapSnapshot`. ([#15746](https://github.com/mapbox/mapbox-gl-native/pull/15746))
diff --git a/src/mbgl/style/sources/geojson_source_impl.cpp b/src/mbgl/style/sources/geojson_source_impl.cpp
index c3cb942709..8067b1ab1d 100644
--- a/src/mbgl/style/sources/geojson_source_impl.cpp
+++ b/src/mbgl/style/sources/geojson_source_impl.cpp
@@ -99,6 +99,7 @@ GeoJSONSource::Impl::Impl(const Impl& other, const GeoJSON& geoJSON)
Feature feature;
clusterOptions.map = [&](const PropertyMap& properties) -> PropertyMap {
PropertyMap ret{};
+ if (properties.empty()) return ret;
for (const auto& p : options.clusterProperties) {
feature.properties = properties;
ret[p.first] = evaluateFeature<Value>(feature, p.second.first);