diff --git a/platform/darwin/src/MGLShapeSource.h b/platform/darwin/src/MGLShapeSource.h index a57b963c630..675c219300e 100644 --- a/platform/darwin/src/MGLShapeSource.h +++ b/platform/darwin/src/MGLShapeSource.h @@ -41,6 +41,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 + clusterProperties + 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 diff --git a/platform/darwin/src/MGLShapeSource.mm b/platform/darwin/src/MGLShapeSource.mm index 3628a0eb74e..3820fe9d605 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 @@ geoJSONOptions.cluster = value.boolValue; } + if (NSDictionary *value = options[MGLShapeSourceOptionClusterProperties]) { + if (![value isKindOfClass:[NSDictionary 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 5103b5f5cfe..01ad108d7fd 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 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 376bf5e73b6..82ce232c6bf 100644 --- a/platform/darwin/src/MGLStyleValue_Private.h +++ b/platform/darwin/src/MGLStyleValue_Private.h @@ -12,12 +12,15 @@ #include #include #include +#include #import #import #import #include +#include + #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 MGLClusterPropertyFromNSExpression(NSExpression *expression); + id MGLJSONObjectFromMBGLExpression(const mbgl::style::expression::Expression &mbglExpression); template diff --git a/platform/darwin/src/NSExpression+MGLAdditions.h b/platform/darwin/src/NSExpression+MGLAdditions.h index 2a33367e9c2..2109310e690 100644 --- a/platform/darwin/src/NSExpression+MGLAdditions.h +++ b/platform/darwin/src/NSExpression+MGLAdditions.h @@ -84,6 +84,13 @@ FOUNDATION_EXTERN MGL_EXPORT const MGLExpressionInterpolationMode MGLExpressionI */ @property (class, nonatomic, readonly) NSExpression *featureIdentifierVariableExpression; +/** + `NSExpression` variable that corresponds to the + id + expression operator in the Mapbox Style Specification. + */ +@property (class, nonatomic, readonly) NSExpression *featureAccumulatedVariableExpression; + /** `NSExpression` variable that corresponds to the properties diff --git a/platform/darwin/src/NSExpression+MGLAdditions.mm b/platform/darwin/src/NSExpression+MGLAdditions.mm index 2ca4e0ed880..f139b86a887 100644 --- a/platform/darwin/src/NSExpression+MGLAdditions.mm +++ b/platform/darwin/src/NSExpression+MGLAdditions.mm @@ -553,6 +553,10 @@ + (NSExpression *)lineProgressVariableExpression { return [NSExpression expressionForVariable:@"lineProgress"]; } ++ (NSExpression *)featureAccumulatedVariableExpression { + return [NSExpression expressionForVariable:@"featureAccumulated"]; +} + + (NSExpression *)geometryTypeVariableExpression { return [NSExpression expressionForVariable:@"geometryType"]; } @@ -648,7 +652,6 @@ + (instancetype)expressionWithMGLJSONObject:(id)object { @"let": @"MGL_LET", }; }); - if (!object || object == [NSNull null]) { return [NSExpression expressionForConstantValue:nil]; } @@ -667,11 +670,10 @@ + (instancetype)expressionWithMGLJSONObject:(id)object { }]; 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 @@ + (instancetype)expressionWithMGLJSONObject:(id)object { 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 @@ - (id)mgl_jsonExpressionObject { 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 @@ - (id)mgl_jsonExpressionObject { 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 @@ - (id)mgl_jsonExpressionObject { 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 @@ - (id)mgl_jsonExpressionObject { 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 9fbb0cc329d..7d6bdbed54b 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 f1fe3ea8787..4ccd7adb6ed 100644 --- a/platform/darwin/test/MGLExpressionTests.mm +++ b/platform/darwin/test/MGLExpressionTests.mm @@ -179,6 +179,13 @@ - (void)testVariableExpressionObject { NSMutableDictionary *context = [@{@"lineProgress": @1} mutableCopy]; 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"]); @@ -380,6 +387,26 @@ - (void)testArithmeticExpressionObject { XCTAssertEqualObjects([NSExpression expressionWithFormat:@"1 + 1"].mgl_jsonExpressionObject, jsonExpression); 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]; @@ -417,6 +444,24 @@ - (void)testArithmeticExpressionObject { // NSExpression lacks a shorthand operator for modulus. 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]; @@ -621,6 +666,16 @@ - (void)testStringFormattingExpressionObject { XCTAssertEqualObjects([aftermarketExpression expressionValueWithObject:nil context:nil], @"OldMacDonald"); 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"]; diff --git a/platform/darwin/test/MGLShapeSourceTests.mm b/platform/darwin/test/MGLShapeSourceTests.mm index 3459fb1733e..3bf3ef04bd0 100644 --- a/platform/darwin/test/MGLShapeSourceTests.mm +++ b/platform/darwin/test/MGLShapeSourceTests.mm @@ -13,8 +13,12 @@ @interface MGLShapeSourceTests : XCTestCase @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 @@ - (void)testGeoJSONOptionsFromDictionary { 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 5a7235b25a0..03ed8cdc46d 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 c3cb9427099..8067b1ab1d6 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(feature, p.second.first);