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

Add iOS bindings for cluster properties #15515

Merged
merged 41 commits into from
Oct 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
12a74fa
[ios] started
Aug 26, 2019
f1b9e2e
[ios] Start to implement dictionary for clusterProperties
Aug 28, 2019
bddc172
[ios] a little clean up
Aug 28, 2019
4249788
[ios] Use
Aug 28, 2019
da5d076
[ios] xcode keeps yelling
Aug 28, 2019
e9842c1
[ios] trying to assign a PropertyExpression, but failing
Sep 3, 2019
38c2ddb
[ios] Use auto&
Sep 3, 2019
71ce185
[ios] use make_shared
Sep 3, 2019
085be86
[ios] chipping away
Sep 4, 2019
c2b5891
[ios] update errors
Sep 5, 2019
c75bf7c
[ios] remove reset
Sep 5, 2019
15fd194
clusterProperty available with full reduce expression
zmiao Sep 10, 2019
d94643d
[ios] Create expression with expressionWithFormat. Revert changes in …
zmiao Sep 11, 2019
aea3398
[ios] check whether expression is aggregate
Sep 12, 2019
80cf880
[ios] add checks for and
Sep 16, 2019
c040be9
[ios] Add checks for collection property
Sep 16, 2019
92da8e3
[ios] Add a few tests
Sep 18, 2019
0159c40
[ios] Use firstObject
Sep 18, 2019
83faf4f
Add type checker
zmiao Sep 18, 2019
d49766b
[ios] Fix spacing, a couple of tests
Sep 18, 2019
eba26ed
[ios] Add type check
Sep 19, 2019
9d60b12
[ios] self -> NSExpression
Sep 25, 2019
866ef7f
[ios] Added a couple more tests
Sep 25, 2019
71f9287
[ios] Can't evaluate
Sep 25, 2019
ef0fee6
[ios] Started to edit docs
Sep 26, 2019
775dda7
[ios] Additional cleanup
Sep 26, 2019
fe252af
Delete earthquakes.geojson
jmkiley Sep 27, 2019
b68da85
[ios] Address Fabian's feedback
Sep 28, 2019
c2b968d
[ios] Fixed tests
Oct 1, 2019
0d007a3
[ios] formatting
Oct 3, 2019
93e25d7
[ios] added debug code, switch string to expression
Oct 4, 2019
08064be
[ios] docs tweaks
Oct 4, 2019
5f3bb39
[ios] remove colon
Oct 7, 2019
389f2f1
[ios] Address feedback
Oct 8, 2019
d956060
[ios] docs tweak
Oct 8, 2019
5b86390
[ios] Remove inline keyword
zmiao Oct 8, 2019
2ef5462
[ios] use options
Oct 10, 2019
af131d4
[ios] try
Oct 10, 2019
2d24f04
[ios] Update code snippet
Oct 10, 2019
cddac46
[ios] Rebased added changelog
Oct 11, 2019
1009c2b
[ios] moved under 5.5.0 header
Oct 11, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
zmiao marked this conversation as resolved.
Show resolved Hide resolved
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;

jmkiley marked this conversation as resolved.
Show resolved Hide resolved
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"];
}
jmkiley marked this conversation as resolved.
Show resolved Hide resolved
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"];
}
jmkiley marked this conversation as resolved.
Show resolved Hide resolved
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