-
Notifications
You must be signed in to change notification settings - Fork 693
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding queryRenderedFeatures on iOS #419
Conversation
That’s strange. The method definitely is implemented. What version of Xcode are you using? This is the SDK’s first method that uses the new
That’s fine. The iOS naming is specific to Cocoa naming conventions, where a getter method without side effects would to be named without a verb. |
I'm using XCode 7.3.1. I tested accessing other attributes and methods on |
I got it working now. I just had to remove and re-add Mapbox.framework from the XCode project, just how I originally had according to the manual installation instructions. Now it's recognizing the method properly. Is this something we'll need to warn people about? |
…ints, polylines, and polygons
I have it working on iOS by passing in an The question is, what do we want the This is what I currently have, which I think needs some improvement: map.queryRenderedFeatures(options, callback)
// example
map.queryRenderedFeatures({
point: {
screenCoordX: 212,
screenCoordY: 111
},
layers: ['building']
},
(features) => console.log('do something with GeoJSON features')
) I'm thinking of passing in a coordinate, rather than a screen point, to be more in line with Mapbox GL JS (I'm not sure if that's a concern). I started with a screen point since iOS uses Inspired by Mapbox GL JS: map.queryRenderedFeatures(geometry, parameters, callback)
// example with point
map.queryRenderedFeatures([44.57, -123.42], {
layers: ['building']
},
(features) => console.log('do something with GeoJSON features')
)
// example with bounding box
map.queryRenderedFeatures([[44.574, -123.426], [44.576, -123.424]], {
layers: ['building']
},
(features) => console.log('do something with GeoJSON features')
) Another alternative to passing One other option, that might be more than we need (but is a bit more explicit): map.queryRenderedFeatures(options, callback)
// example
// one of 'screenCoord', 'mapCoord', 'rect', and 'boundingBox' are required
map.queryRenderedFeatures({
screenCoord: {
x: 212,
y: 111
},
mapCoord: {
latitude: 44.57,
longitude: -123.43
},
rect: {
left: 0,
top: 0,
right: 10,
bottom: 10
},
boundingBox: {
southWest: {
latitude: 44.574,
longitude: -123.426
},
northEast: {
latitude: 44.576,
longitude: -123.424
}
},
layers: ['building']
},
(features) => console.log('do something with GeoJSON features')
) What do you think? Should we accept screen points, lat long coords, or both? cc @bsudekum, @dapetcu21, @tmcw |
mapbox/mapbox-gl-native#6302 would make it possible to create an MGLGeoJSONSource from an array of MGLFeatures, which is essentially opposite the direction you’d like to go. With an MGLGeoJSONSource, you could grab the In the meantime, you’re on the right track with your conversion implementation. For collection types like MGLMultiPolyline and especially MGLShapeCollection, you’ll want to do the conversion recursively.
Based on GL JS’s documentation and examples, it’s clear |
NSSet<NSString *> *styleLayerIdentifiers = options[@"layers"]; | ||
|
||
CLLocationCoordinate2D location = [_map convertPoint:[sender locationInView:_map] toCoordinateFromView:_map]; | ||
CGPoint screenCoord = [sender locationInView:_map]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two variables don’t appear to be used anywhere at the moment.
} | ||
// TODO: checks for MGLMultiPolyline and MGLMultiPolygon | ||
|
||
NSDictionary *geoJSON = @{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also set id
to the value of the MGLFeature’s identifier
property. (We don’t say “ID” much in Objective-C since id
is a keyword.)
} | ||
geometryType = @"Polygon"; | ||
} | ||
// TODO: checks for MGLMultiPolyline and MGLMultiPolygon |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also MGLMultiPoint and MGLShapeCollection. Note that MGLPolyline and MGLPolygon currently inherit from MGLMultiPoint (mapbox/mapbox-gl-native#5201), so only handle MGLMultiPoint after those two classes.
if ([feature isKindOfClass:[MGLPointFeature class]]) { | ||
geometryType = @"Point"; | ||
coordinates = (NSMutableArray *) @[@(feature.coordinate.longitude), @(feature.coordinate.latitude)]; | ||
} else if ([feature isKindOfClass:[MGLPolyline class]]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you’re checking for MGLPointFeature rather than MGLPointAnnotation in the first case above, it’d be more consistent to check for MGLPolylineFeature, MGLPolygonFeature, etc.
CLLocationCoordinate2D coord = multiPoint.coordinates[index]; | ||
[coordinates addObject:@[@(coord.longitude), @(coord.latitude)]]; | ||
} | ||
geometryType = @"Polygon"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A polygon can have multiple holes, represented as MGLPolygons inside MGLPolygon’s interiorPolygons
property.
|
||
// TODO: also accept Rect to use [mapView visibleFeaturesInRect:] | ||
|
||
// TODO: convert features to JS friendly objects | ||
// TODO: switch to using MGLFeature's geoJSONFeature method if implemented - https://github.com/mapbox/mapbox-gl-native/issues/6302 | ||
NSMutableArray *geoJSONFeatures = [[NSMutableArray alloc] init]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: you can also write this more succinctly as [NSMutableArray array]
. As a possibly premature but reasonable optimization, you can also specify the size upfront: [NSMutableArray arrayWithCapacity:features.count]
.
if (!styleLayerIdentifiers) { | ||
features = [mapView visibleFeaturesAtPoint:point]; | ||
} else { | ||
features = [mapView visibleFeaturesAtPoint:point inStyleLayersWithIdentifiers:styleLayerIdentifiers]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can call this method for both cases. Pass in nil
as the second parameter to get the same behavior as -visibleFeaturesAtPoint:
.
} | ||
NSNumber *screenCoordX = pointDict[@"screenCoordX"]; | ||
NSNumber *screenCoordY = pointDict[@"screenCoordY"]; | ||
NSSet<NSString *> *styleLayerIdentifiers = options[@"layers"]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn’t this be an NSArray rather than an NSSet, given the JavaScript representation? If this works, it’s only because the SDK happens to be calling methods common to both classes. Probably better to convert it into an NSArray using allObjects
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. I should be pulling it out of options
as an NSArray
, then convert it to an NSSet
. I just pushed some commits with that change.
Probably better to convert it into an NSArray using
allObjects
.
You mean convert the NSArray into an NSSet as expected by visibleFeaturesAtPoint
? Let me know if my most recent change to it looks good. I really appreciate your feedback!
…ng Android support
I think the PR is ready, at least on iOS. I've made all of the requested changes, got For Android, I've started looking into implementing it. Since |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking pretty good. A couple code style issues and a missing case.
@"coordinates": coordinates }; | ||
} | ||
|
||
- (NSMutableArray *)getMGLPolylineCoordinates:(MGLPolylineFeature *)polyline |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An Objective-C method name should only begin with "get" if the method fetches a resource from the network or something of that nature.
A cleaner approach would be to add a category to MGLPolylineFeature with a method called -coordinateArray
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. I've created a category for MGLPolylineFeature and MGLPolygonFeature, each in their own files. Do you have a preferred naming convention? Should I call it MGLPolylineFeature+coordinateArray.m
, or maybe MGLPolylineFeature+RCTMapboxGL.m
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about: MGL{Polyline,Polygon}Feature+RCTAdditions.{h,m}
|
||
if ([feature isKindOfClass:[MGLPointFeature class]]) { | ||
geometryType = @"Point"; | ||
coordinates = (NSMutableArray *) @[@(feature.coordinate.longitude), @(feature.coordinate.latitude)]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It isn't causing any problems now, but it's unsafe to cast this NSArray to an NSMutableArray. You could declare coordinates as an NSArray without initializing it, then initialize a local NSMutableArray in each if block below where you need one. Not quite as concise, but also reduces the risk of a crash when someone later tries to treat the mutable array as an actual mutable array down below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about not creating an NSArray in the first place? I'm thinking replacing line 680 with the following:
coordinates = [[NSMutableArray alloc] initWithArray:@[@(feature.coordinate.longitude), @(feature.coordinate.latitude)]];
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That works too. (@[]
creates an NSArray, after all.)
CLLocationCoordinate2D coord = multiPoint.coordinates[index]; | ||
[coordinates addObject:@[@(coord.longitude), @(coord.latitude)]]; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about MGLShapeCollectionFeature (FeatureCollection)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yikes, never mind, I missed that it's right up there at the top.
[coordinates addObject:outerRingCoordinates]; | ||
|
||
for (MGLPolygonFeature *interiorRing in polygon.interiorPolygons) { | ||
NSMutableArray *interiorRingCoordinates = [[NSMutableArray alloc] init]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine, although you could also call the current method recursively here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't calling it recursively nest the arrays too much?
[outerRing, interiorRing1, interiorRing2, ...]
vs
[outerRing, [interiorRing1], [interiorRing2], ...]
I've left it as is in my refactoring to using a category.
89617c3
to
97f2bb3
Compare
97f2bb3
to
d4cf5d5
Compare
I pushed a new commit addressing your concerns. I put the category on MGLPolyline and MGLPolygon, rather than their "Feature" counterparts, since MGLMultiPolyline's |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks 👌. There are some additions opportunities for refactoring, for example moving all the GeoJSON conversion logic into more MGLFeature categories, but that can always happen down the line.
Thanks again for taking up this task. @bsudekum, good to merge?
HI TANNER!!!!! |
Only thing left to do is to resolve the conflicts. |
@tsemerad want to fix the conflicts? then we can merge |
heh, didn't reload the page, 🎉 |
Glad I could help. Haha, hi @lyzidiamond! Good to hear from you! You probably didn't expect to see my name pop up in your Github feed. |
We ultimately implemented this in the iOS SDK as |
I'm trying to make a PR adding
queryRenderedFeatures
support (orvisibleFeaturesAtPoint
andvisibleFeaturesInRect
on iOS), and I need some feedback.I have the bridge set up to properly send the screen coordinate from JS to Obj C. However, it's failing on line 547 of RCTMapboxGL.m with
Exception thrown while executing UI block: -[MGLMapView visibleFeaturesAtPoint:]: unrecognized selector sent to instance
. I can't figure out why it's giving me this issue. I apologize if this is a simple issue I've overlooked - this is my first time writing React Native bridge code.I also want feedback on the name and signature on the method. On iOS, it's called
visibleFeaturesAtPoint
andvisibleFeaturesInRect
, and on Android it's calledqueryRenderedFeatures
. I'm planning on calling itqueryRenderedFeatures
, and allow passing in either a point or a rect.