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

Allow predicates to test whether a feature lies within a given shape #184

Merged
merged 5 commits into from
Mar 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions platform/darwin/docs/guides/For Style Authors.md.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ In style specification | Method, function, or predicate type | Format string syn
`case` | `+[NSExpression expressionForConditional:trueExpression:falseExpression:]` or `MGL_IF` or `+[NSExpression mgl_expressionForConditional:trueExpression:falseExpresssion:]` | `TERNARY(1 = 2, YES, NO)` or `MGL_IF(1 = 2, YES, 2 = 2, YES, NO)`
`coalesce` | `mgl_coalesce:` | `mgl_coalesce({x, y, z})`
`match` | `MGL_MATCH` or `+[NSExpression mgl_expressionForMatchingExpression:inDictionary:defaultExpression:]` | `MGL_MATCH(x, 0, 'zero match', 1, 'one match', 'two match', 'default')`
`within` | `NSInPredicateOperatorType` | `SELF IN %@` or `%@ CONTAINS SELF` where `%@` is an `MGLShape`
`interpolate` | `mgl_interpolate:withCurveType:parameters:stops:` or `+[NSExpression mgl_expressionForInterpolatingExpression:withCurveType:parameters:stops:]` |
`step` | `mgl_step:from:stops:` or `+[NSExpression mgl_expressionForSteppingExpression:fromExpression:stops:]` |
`let` | `mgl_expressionWithContext:` | `MGL_LET('ios', 11, 'macos', 10.13, $ios + $macos)`
Expand Down
12 changes: 12 additions & 0 deletions platform/darwin/docs/guides/Predicates and Expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ The following aggregate operators are supported:
`NSInPredicateOperatorType` | `key IN { 'iOS', 'macOS', 'tvOS', 'watchOS' }`
`NSContainsPredicateOperatorType` | `{ 'iOS', 'macOS', 'tvOS', 'watchOS' } CONTAINS key`

You can use the `IN` and `CONTAINS` operators to test whether a value appears in a collection, whether a string is a substring of a larger string, or whether the evaluated feature (`SELF`) lies within a given `MGLShape` or `MGLFeature`. For example, to show one delicious local chain of sandwich shops, but not similarly named steakhouses and pizzerias:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MGLFeature support was added in mapbox/mapbox-gl-native#16232, which we haven’t pulled into this repository yet.


```objc
MGLPolygon *cincinnati = [MGLPolygon polygonWithCoordinates:cincinnatiCoordinates count:sizeof(cincinnatiCoordinates) / sizeof(cincinnatiCoordinates[0])];
deliLayer.predicate = [NSPredicate predicateWithFormat:@"class = 'food_and_drink' AND name CONTAINS 'Izzy' AND SELF IN %@", cincinnati];
```

```swift
let cincinnati = MGLPolygon(coordinates: &cincinnatiCoordinates, count: UInt(cincinnatiCoordinates.count))
deliLayer.predicate = NSPredicate(format: "class = 'food_and_drink' AND name CONTAINS 'Izzy' AND SELF IN %@", cincinnati)
```

The following combinations of comparison operators and modifiers are supported:

`NSComparisonPredicateModifier` | `NSPredicateOperatorType` | Format string syntax
Expand Down
20 changes: 17 additions & 3 deletions platform/darwin/src/MGLConversion.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#import "MGLShape_Private.h"

#include <mbgl/style/conversion_impl.hpp>

NS_ASSUME_NONNULL_BEGIN
Expand Down Expand Up @@ -124,9 +126,21 @@ class ConversionTraits<Holder> {
}
}

static optional<GeoJSON> toGeoJSON(const Holder&, Error& error) {
error = { "toGeoJSON not implemented" };
return {};
static optional<GeoJSON> toGeoJSON(const Holder& holder, Error& error) {
id object = holder.value;
if ([object isKindOfClass:[NSDictionary class]]) {
NSError *serializationError;
NSData *data = [NSJSONSerialization dataWithJSONObject:object options:0 error:&serializationError];
if (serializationError) {
error = { std::string(serializationError.localizedDescription.UTF8String) };
return {};
}
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
return mapbox::geojson::parse(string.UTF8String);
} else {
error = { std::string([NSString stringWithFormat:@"%@ is not a GeoJSON object.", object].UTF8String) };
return {};
}
}

private:
Expand Down
71 changes: 39 additions & 32 deletions platform/darwin/src/MGLFeature.mm
Original file line number Diff line number Diff line change
Expand Up @@ -352,19 +352,20 @@ - (NSDictionary *)geoJSONDictionary {
class GeometryEvaluator {
private:
const mbgl::PropertyMap *shared_properties;
const bool is_in_feature;

public:
GeometryEvaluator(const mbgl::PropertyMap *properties = nullptr):
shared_properties(properties)
GeometryEvaluator(const mbgl::PropertyMap *properties = nullptr, const bool isInFeature = false):
shared_properties(properties),
is_in_feature(isInFeature)
{}

MGLShape <MGLFeature> * operator()(const mbgl::EmptyGeometry &) const {
MGLEmptyFeature *feature = [[MGLEmptyFeature alloc] init];
return feature;
MGLShape * operator()(const mbgl::EmptyGeometry &) const {
return is_in_feature ? [[MGLEmptyFeature alloc] init] : [[MGLShape alloc] init];
}

MGLShape <MGLFeature> * operator()(const mbgl::Point<T> &geometry) const {
Class pointFeatureClass = [MGLPointFeature class];
MGLShape * operator()(const mbgl::Point<T> &geometry) const {
Class shapeClass = is_in_feature ? [MGLPointFeature class] : [MGLPointAnnotation class];

// If we're dealing with a cluster, we should change the class type.
// This could be generic and build the subclass at runtime if it turns
Expand All @@ -375,59 +376,64 @@ - (NSDictionary *)geoJSONDictionary {
auto clusterValue = clusterIt->second;
if (clusterValue.template is<bool>()) {
if (clusterValue.template get<bool>()) {
pointFeatureClass = [MGLPointFeatureCluster class];
shapeClass = [MGLPointFeatureCluster class];
}
}
}
}

MGLPointFeature *feature = [[pointFeatureClass alloc] init];
feature.coordinate = toLocationCoordinate2D(geometry);
return feature;
MGLPointAnnotation *shape = [[shapeClass alloc] init];
shape.coordinate = toLocationCoordinate2D(geometry);
return shape;
}

MGLShape <MGLFeature> * operator()(const mbgl::LineString<T> &geometry) const {
MGLShape * operator()(const mbgl::LineString<T> &geometry) const {
std::vector<CLLocationCoordinate2D> coordinates = toLocationCoordinates2D(geometry);
return [MGLPolylineFeature polylineWithCoordinates:&coordinates[0] count:coordinates.size()];
Class shapeClass = is_in_feature ? [MGLPolylineFeature class] : [MGLPolyline class];
return [shapeClass polylineWithCoordinates:&coordinates[0] count:coordinates.size()];
}

MGLShape <MGLFeature> * operator()(const mbgl::Polygon<T> &geometry) const {
return toShape<MGLPolygonFeature>(geometry);
MGLShape * operator()(const mbgl::Polygon<T> &geometry) const {
return toShape<MGLPolygon, MGLPolygonFeature>(geometry, is_in_feature);
}

MGLShape <MGLFeature> * operator()(const mbgl::MultiPoint<T> &geometry) const {
MGLShape * operator()(const mbgl::MultiPoint<T> &geometry) const {
std::vector<CLLocationCoordinate2D> coordinates = toLocationCoordinates2D(geometry);
return [[MGLPointCollectionFeature alloc] initWithCoordinates:&coordinates[0] count:coordinates.size()];
Class shapeClass = is_in_feature ? [MGLPointCollectionFeature class] : [MGLPointCollection class];
return [[shapeClass alloc] initWithCoordinates:&coordinates[0] count:coordinates.size()];
}

MGLShape <MGLFeature> * operator()(const mbgl::MultiLineString<T> &geometry) const {
MGLShape * operator()(const mbgl::MultiLineString<T> &geometry) const {
NSMutableArray *polylines = [NSMutableArray arrayWithCapacity:geometry.size()];
for (auto &lineString : geometry) {
std::vector<CLLocationCoordinate2D> coordinates = toLocationCoordinates2D(lineString);
MGLPolyline *polyline = [MGLPolyline polylineWithCoordinates:&coordinates[0] count:coordinates.size()];
[polylines addObject:polyline];
}

return [MGLMultiPolylineFeature multiPolylineWithPolylines:polylines];
Class shapeClass = is_in_feature ? [MGLMultiPolylineFeature class] : [MGLMultiPolyline class];
return [shapeClass multiPolylineWithPolylines:polylines];
}

MGLShape <MGLFeature> * operator()(const mbgl::MultiPolygon<T> &geometry) const {
MGLShape * operator()(const mbgl::MultiPolygon<T> &geometry) const {
NSMutableArray *polygons = [NSMutableArray arrayWithCapacity:geometry.size()];
for (auto &polygon : geometry) {
[polygons addObject:toShape(polygon)];
[polygons addObject:toShape(polygon, false)];
}

return [MGLMultiPolygonFeature multiPolygonWithPolygons:polygons];
Class shapeClass = is_in_feature ? [MGLMultiPolygonFeature class] : [MGLMultiPolygon class];
return [shapeClass multiPolygonWithPolygons:polygons];
}

MGLShape <MGLFeature> * operator()(const mapbox::geometry::geometry_collection<T> &collection) const {
MGLShape * operator()(const mapbox::geometry::geometry_collection<T> &collection) const {
NSMutableArray *shapes = [NSMutableArray arrayWithCapacity:collection.size()];
for (auto &geometry : collection) {
// This is very much like the transformation that happens in MGLFeaturesFromMBGLFeatures(), but these are raw geometries with no associated feature IDs or attributes.
MGLShape <MGLFeature> *shape = mapbox::geometry::geometry<T>::visit(geometry, *this);
MGLShape *shape = mapbox::geometry::geometry<T>::visit(geometry, *this);
[shapes addObject:shape];
}
return [MGLShapeCollectionFeature shapeCollectionWithShapes:shapes];
Class shapeClass = is_in_feature ? [MGLShapeCollectionFeature class] : [MGLShapeCollection class];
return [shapeClass shapeCollectionWithShapes:shapes];
}

private:
Expand All @@ -442,8 +448,8 @@ static CLLocationCoordinate2D toLocationCoordinate2D(const mbgl::Point<T> &point
return coordinates;
}

template<typename U = MGLPolygon>
static U *toShape(const mbgl::Polygon<T> &geometry) {
template<typename U = MGLPolygon, typename V = MGLPolygonFeature>
static U *toShape(const mbgl::Polygon<T> &geometry, const bool isInFeature) {
auto &linearRing = geometry.front();
std::vector<CLLocationCoordinate2D> coordinates = toLocationCoordinates2D(linearRing);
NSMutableArray *innerPolygons;
Expand All @@ -457,16 +463,17 @@ static CLLocationCoordinate2D toLocationCoordinate2D(const mbgl::Point<T> &point
}
}

return [U polygonWithCoordinates:&coordinates[0] count:coordinates.size() interiorPolygons:innerPolygons];
Class shapeClass = isInFeature ? [V class] : [U class];
return [shapeClass polygonWithCoordinates:&coordinates[0] count:coordinates.size() interiorPolygons:innerPolygons];
}
};

template <typename T>
class GeoJSONEvaluator {
public:
MGLShape <MGLFeature> * operator()(const mbgl::Geometry<T> &geometry) const {
MGLShape * operator()(const mbgl::Geometry<T> &geometry) const {
GeometryEvaluator<T> evaluator;
MGLShape <MGLFeature> *shape = mapbox::geometry::geometry<T>::visit(geometry, evaluator);
MGLShape *shape = mapbox::geometry::geometry<T>::visit(geometry, evaluator);
return shape;
}

Expand Down Expand Up @@ -507,8 +514,8 @@ static CLLocationCoordinate2D toLocationCoordinate2D(const mbgl::Point<T> &point
ValueEvaluator evaluator;
attributes[@(pair.first.c_str())] = mbgl::Value::visit(value, evaluator);
}
GeometryEvaluator<double> evaluator(&feature.properties);
MGLShape <MGLFeature> *shape = mapbox::geometry::geometry<double>::visit(feature.geometry, evaluator);
GeometryEvaluator<double> evaluator(&feature.properties, true);
MGLShape <MGLFeature> *shape = (MGLShape <MGLFeature> *)mapbox::geometry::geometry<double>::visit(feature.geometry, evaluator);
if (!feature.id.is<mapbox::feature::null_value_t>()) {
shape.identifier = mbgl::FeatureIdentifier::visit(feature.id, ValueEvaluator());
}
Expand Down
6 changes: 6 additions & 0 deletions platform/darwin/src/MGLShape.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ NS_ASSUME_NONNULL_BEGIN
shapes collectively using a concrete instance of `MGLVectorStyleLayer`.
Alternatively, you can add some kinds of shapes directly to a map view as
annotations or overlays.

You can filter the features in a `MGLVectorStyleLayer` or vary their layout or
paint attributes based on the features’ geographies. Pass an `MGLShape` into an
`NSPredicate` with the format `SELF IN %@` or `%@ CONTAINS SELF` and set the
`MGLVectorStyleLayer.predicate` property to that predicate, or set a layout or
paint attribute to a similarly formatted `NSExpression`.
*/
MGL_EXPORT
@interface MGLShape : NSObject <MGLAnnotation, NSSecureCoding>
Expand Down
3 changes: 3 additions & 0 deletions platform/darwin/src/NSComparisonPredicate+MGLAdditions.mm
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ - (id)mgl_jsonExpressionObject {
return @[op, leftHandPredicate.mgl_jsonExpressionObject, rightHandPredicate.mgl_jsonExpressionObject];
}
case NSInPredicateOperatorType: {
if (self.leftExpression.expressionType == NSEvaluatedObjectExpressionType) {
return @[@"within", self.rightExpression.mgl_jsonExpressionObject];
}
// An “in” expression comparing two string literals is unfortunately
// misinterpreted as a legacy “in” filter due to ambiguity. Wrap one
// argument in a “literal” expression to force an expression.
Expand Down
28 changes: 27 additions & 1 deletion platform/darwin/src/NSExpression+MGLAdditions.mm
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import "MGLFoundation_Private.h"
#import "MGLGeometry_Private.h"
#import "MGLShape_Private.h"
#import "NSExpression+MGLPrivateAdditions.h"

#import "MGLTypes.h"
Expand Down Expand Up @@ -539,6 +540,14 @@ - (id)mgl_has:(id)element {

@end

@implementation MGLShape (MGLExpressionAdditions)

- (id)mgl_jsonExpressionObject {
return self.geoJSONDictionary;
}

@end

@implementation NSExpression (MGLAdditions)

+ (NSExpression *)zoomLevelVariableExpression {
Expand Down Expand Up @@ -659,11 +668,24 @@ + (instancetype)expressionWithMGLJSONObject:(id)object {
if ([object isKindOfClass:[NSString class]] ||
[object isKindOfClass:[NSNumber class]] ||
[object isKindOfClass:[NSValue class]] ||
[object isKindOfClass:[MGLColor class]]) {
[object isKindOfClass:[MGLColor class]] ||
[object isKindOfClass:[MGLShape class]]) {
return [NSExpression expressionForConstantValue:object];
}

if ([object isKindOfClass:[NSDictionary class]]) {
if (object[@"type"]) {
NSError *error;
NSData *shapeData = [NSJSONSerialization dataWithJSONObject:object options:0 error:&error];
MGLShape *shape;
if (shapeData && !error) {
shape = [MGLShape shapeWithData:shapeData encoding:NSUTF8StringEncoding error:&error];
Comment on lines +679 to +682
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, but there may be a more efficient way to convert this JSON-like NSDictionary structure into an MGLShape. Perhaps we could first convert the NSDictionary structure into a C++ representation of the GeoJSON object, then convert to an MGLShape.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any performance considerations for bridging into an C++ object then back to MGLShape? Is this something that is going to be called frequently?

Copy link
Contributor Author

@1ec5 1ec5 Mar 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the current implementation, MGLStyleValueTransformer::PropertyExpressionEvaluator converts the C++ mapbox::geojson struct to a GeoJSON-formatted NSDictionary, then this method converts it to a string, then back to a C++ mapbox::geojson struct, then finally to an MGLShape object. It is more roundabout than it should be.

This conversion process happens once when setting a layer’s predicate or paint/layout property (which can happen automatically e.g. when localizing the style). mbgl will evaluate the already-converted expression on each feature, but no SDK-side code will run as part of the rendering loop.

Ideally, mbgl would preserve the mapbox::geojson struct so that MGLJSONObjectFromMBGLValue() could convert it directly to an MGLShape. Absent such a change in mbgl, there are two alternative, purely SDK-side approaches:

  • Convert the NSDictionary directly to C++ mapbox::geojson structs then to MGLShape, skipping NSJSONSerialization
  • Parse the NSDictionary into an MGLShape, skipping C++

but I’m inclined to treat either optimization as tail work for when we know more about how this expression operator will be used. The performance characteristics could differ depending on the complexity of the passed-in GeoJSON feature.

}
if (shape && !error) {
return [NSExpression expressionForConstantValue:shape];
}
}

NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:[object count]];
[object enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
dictionary[key] = [NSExpression expressionWithMGLJSONObject:obj];
Expand Down Expand Up @@ -1031,6 +1053,10 @@ - (id)mgl_jsonExpressionObject {
}
return @[jsonObject, attributedDictionary];
}
if ([constantValue isKindOfClass:[MGLShape class]]) {
MGLShape *shape = (MGLShape *)constantValue;
return shape.geoJSONDictionary;
}
return self.constantValue;
}

Expand Down
8 changes: 8 additions & 0 deletions platform/darwin/src/NSPredicate+MGLAdditions.mm
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ + (instancetype)predicateWithMGLJSONObject:(id)object {
NSArray *subpredicates = MGLSubpredicatesWithJSONObjects([objects subarrayWithRange:NSMakeRange(1, objects.count - 1)]);
return [NSCompoundPredicate orPredicateWithSubpredicates:subpredicates];
}
if ([op isEqualToString:@"within"]) {
NSArray *subexpressions = MGLSubexpressionsWithJSONObjects([objects subarrayWithRange:NSMakeRange(1, objects.count - 1)]);
return [NSComparisonPredicate predicateWithLeftExpression:[NSExpression expressionForEvaluatedObject]
rightExpression:subexpressions[0]
modifier:NSDirectPredicateModifier
type:NSInPredicateOperatorType
options:0];
}

NSExpression *expression = [NSExpression expressionWithMGLJSONObject:object];
return [NSComparisonPredicate predicateWithLeftExpression:expression
Expand Down
Loading