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

Conversation

1ec5
Copy link
Contributor

@1ec5 1ec5 commented Feb 29, 2020

Added support for the NSPredicate syntax SELF IN shape or shape CONTAINS SELF, where shape is an MGLShape object or MGLFeature instance. These syntaxes are interpreted as within expressions in JSON format.

Fixed an issue where GeoJSON geometries were converted to MGLFeature instances instead of MGLShape objects. This issue probably hasn’t come up in the wild because the conversion code was previously only part of a feature querying code path, and feature querying methods return features anyways. But since it’s possible for the shape in SELF IN shape to be an MGLShape, that shape should get round-tripped as an MGLShape rather than an MGLFeature.

Fixes #175. Depends on #183, which in turn depends on #181. Retarget and rebase this PR onto master before merging.

/cc @mapbox/maps-ios

@1ec5 1ec5 added this to the release-vanillashake milestone Feb 29, 2020
@1ec5 1ec5 requested review from fabian-guerra, jmkiley and a team February 29, 2020 02:55
@1ec5 1ec5 self-assigned this Feb 29, 2020
Comment on lines +679 to +682
NSData *shapeData = [NSJSONSerialization dataWithJSONObject:object options:0 error:&error];
MGLShape *shape;
if (shapeData && !error) {
shape = [MGLShape shapeWithData:shapeData encoding:NSUTF8StringEncoding error:&error];
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.

@chloekraw
Copy link
Contributor

@1ec5 will there be a developer-facing API that references within in its name?

@1ec5
Copy link
Contributor Author

1ec5 commented Feb 29, 2020

will there be a developer-facing API that references within in its name?

The SELF IN syntax is available for developers to use like this (notice lovely highlighting of NSExpression format string syntax):

[NSPredicate predicateWithFormat:@"SELF IN %@", [MGLPolygon polygonWithCoodinates:coords count:4]];

But if you’re referring to something more reminiscent of the JSON expression format, it’s also possible to use within expressions directly, using either of these syntaxes:

  •  [NSPredicate predicateWithFormat:@"MGL_FUNCTION('within', %@)", [MGLPolygon polygonWithCoodinates:coords count:4]];
  •  [NSPredicate predicateWithMGLJSONObject:@[@"within", @{@"type": @"Polygon", @"coordinates": @[@[@[@0, @0], @[@0, @1], @[@1, @1], @[@1, @0]]]}]];

The latter syntax is already possible without this PR, now that #183 has landed.

I suppose we could install an aftermarket function like mgl_within: to avoid MGL_FUNCTION(), but I’m not sure of the use case given the IN and CONTAINS operators.

@@ -354,6 +354,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`
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As always, if a developer is already familiar with the style specification’s JSON expression format or a similar syntax on Android, they can consult the “Information for Style Authors” guide to learn the corresponding NSExpression syntax.

@1ec5
Copy link
Contributor Author

1ec5 commented Mar 2, 2020

Per #175 (comment), MGLConversion needs to convert from mbgl::GeoJSON to MGLShape:

static optional<GeoJSON> toGeoJSON(const Holder&, Error& error) {
error = { "toGeoJSON not implemented" };
return {};
}

This code is called as part of -[NSPredicate(MGLAdditions) mgl_filter]. The lack of a conversion probably explains why I was unable to preserve the filter roundtripping assertions as part of the unit tests in this PR.

Copy link
Contributor

@julianrex julianrex left a comment

Choose a reason for hiding this comment

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

Thanks! Just the question from @fabian-guerra

Copy link
Contributor

@fabian-guerra fabian-guerra left a comment

Choose a reason for hiding this comment

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

I agree that we could treat optimizations as tail work. Thank you for explaining. LGTM in that case.

@1ec5 1ec5 force-pushed the 1ec5-expr-in-168 branch from 12903be to d7058e7 Compare March 11, 2020 23:05
@1ec5 1ec5 force-pushed the 1ec5-expr-within-175 branch from 534d64c to aa95fdb Compare March 11, 2020 23:25
@1ec5
Copy link
Contributor Author

1ec5 commented Mar 12, 2020

As of 11424ded73745e734c2d1f58b9fad8d7bd8ae635, the Debug ‣ Manipulate Style command adds a circle layer that should mark each POI around here. However, nothing shows up because the within expression always evaluates to false. The conversion from NSExpression to mbgl::style::expression::Within is working correctly, as is the conversion from GeoJSON-formatted NSDictionary to mapbox::geometry. Here’s what the GeoJSON representation of the shape now that toGeoJSON() has been implemented:

{"type":"Polygon","coordinates":[[[-84.520899999999997,39.127899999999997],[-84.511200000000002,39.127299999999998],[-84.510199999999998,39.1355],[-84.521199999999993,39.136000000000003],[-84.520899999999997,39.127899999999997]]]}

geojson.io

Within::evaluate() returns false because pointsWithinPolygons() returns false because boxWithinBox() returns false because isBBoxValid() returns false because Within.polygonBBox is never set. Something in the call to convertGeometry() causes the polygon’s only ring to go missing. In this part of the method, pg is empty.

As far as I can tell, the SDK code is behaving correctly and the shape is valid, but the expression never evaluates to true. I’ve also tried reversing the polygon and removing the closing point to no avail.

/cc @zmiao

@@ -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.

@1ec5
Copy link
Contributor Author

1ec5 commented Mar 12, 2020

However, nothing shows up because the within expression always evaluates to false.

I’ve confirmed that this will also be fixed by mapbox/mapbox-gl-native#16232 once we pull it in.

uc

@1ec5
Copy link
Contributor Author

1ec5 commented Mar 13, 2020

However, nothing shows up because the within expression always evaluates to false.

I’ve confirmed that this will also be fixed by mapbox/mapbox-gl-native#16232 once we pull it in.

#210 will pull in the fix. No additional changes are needed for within support at the SDK level, but this PR depends on #183, which is waiting for #210.

@1ec5 1ec5 force-pushed the 1ec5-expr-in-168 branch from d7058e7 to 887f10e Compare March 17, 2020 02:40
@1ec5 1ec5 force-pushed the 1ec5-expr-within-175 branch from 11424de to d4041d2 Compare March 17, 2020 02:41
@1ec5 1ec5 force-pushed the 1ec5-expr-in-168 branch from 887f10e to 342606d Compare March 17, 2020 08:48
Fixed an issue where GeoJSON geometries were converted to MGLFeature instances instead of MGLShape objects.
@1ec5 1ec5 force-pushed the 1ec5-expr-within-175 branch from d4041d2 to 9e28d66 Compare March 17, 2020 09:02
1ec5 added 3 commits March 17, 2020 02:13
Added support for the expression syntax “SELF IN shape” or “shape CONTAINS SELF”, which is interpreted as a “within” expression in JSON format.
@1ec5 1ec5 force-pushed the 1ec5-expr-within-175 branch from 9e28d66 to e27b889 Compare March 17, 2020 09:14
@1ec5 1ec5 changed the base branch from 1ec5-expr-in-168 to master March 17, 2020 09:36
@1ec5 1ec5 merged commit e27b889 into master Mar 17, 2020
@1ec5 1ec5 deleted the 1ec5-expr-within-175 branch March 17, 2020 15:13
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants