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

Commit

Permalink
Add iOS bindings for cluster properties (#15515)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmkiley authored Oct 11, 2019
1 parent a1f124c commit e4e2a78
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 8 deletions.
32 changes: 32 additions & 0 deletions platform/darwin/src/MGLShapeSource.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
<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
Expand Down
53 changes: 53 additions & 0 deletions platform/darwin/src/MGLShapeSource.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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";
Expand Down Expand Up @@ -84,6 +86,57 @@
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
Expand Down
13 changes: 13 additions & 0 deletions platform/darwin/src/MGLStyleValue.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
5 changes: 5 additions & 0 deletions platform/darwin/src/MGLStyleValue_Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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>
Expand Down
7 changes: 7 additions & 0 deletions platform/darwin/src/NSExpression+MGLAdditions.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ FOUNDATION_EXTERN MGL_EXPORT const MGLExpressionInterpolationMode MGLExpressionI
*/
@property (class, nonatomic, readonly) NSExpression *featureIdentifierVariableExpression;

/**
`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>
Expand Down
43 changes: 36 additions & 7 deletions platform/darwin/src/NSExpression+MGLAdditions.mm
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,10 @@ + (NSExpression *)lineProgressVariableExpression {
return [NSExpression expressionForVariable:@"lineProgress"];
}

+ (NSExpression *)featureAccumulatedVariableExpression {
return [NSExpression expressionForVariable:@"featureAccumulated"];
}

+ (NSExpression *)geometryTypeVariableExpression {
return [NSExpression expressionForVariable:@"geometryType"];
}
Expand Down Expand Up @@ -648,7 +652,6 @@ + (instancetype)expressionWithMGLJSONObject:(id)object {
@"let": @"MGL_LET",
};
});

if (!object || object == [NSNull null]) {
return [NSExpression expressionForConstantValue:nil];
}
Expand All @@ -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];
Expand Down Expand Up @@ -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"]) {
Expand Down Expand Up @@ -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"];
}
Expand Down Expand Up @@ -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;
Expand All @@ -1057,24 +1066,44 @@ - (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;
} else if ([function isEqualToString:@"trunc:"]) {
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;
Expand Down
34 changes: 33 additions & 1 deletion platform/darwin/test/MGLDocumentationExampleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
}
Loading

0 comments on commit e4e2a78

Please sign in to comment.