简体   繁体   中英

point in polygon using leaflet-pip and d3.geoContains

I am perplexed as to what I am doing wrong here. I have a geoJson object and I am trying to find which feature contains a given point. When I am using d3.geoContains I got back the false results, ie those polygons that do not contain the point.

What I do for now is to use point in polygon from https://mapbox.github.io/leaflet-pip/ which seems to work fine, however I dont know what I am missing with d3.geoContains . According to the docs it should return true if the object contains the point.

Attached is a simple case where I have 4 squares, two at the bottom row, one next to the other, and the other two at the top row. I am testing to find the polygon containing point [0.7, 0.7] and pip returns the correct answer, top-right, but d3.geoContains returns the other three squares.

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <!--d3 --> <script src='https://d3js.org/d3.v4.min.js'></script> <!-- leaflet --> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.3/dist/leaflet.css"/> <script src="https://unpkg.com/leaflet@1.0.3/dist/leaflet-src.js"></script> <script src="https://unpkg.com/leaflet-pip@1.1.0/leaflet-pip.js"></script> </head> <body> <script> var my_squares = { "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": {"id": "bottom_left"}, "geometry": { "type": "Polygon", "coordinates": [[[0.0, 0.0], [0.5, 0.0], [0.5, 0.5], [0.0, 0.5], [0.0, 0.0]]], } }, { "type": "Feature", "properties": {"id": "bottom_right"}, "geometry": { "type": "Polygon", "coordinates": [[[0.5, 0.0], [1.0, 0.0], [1.0, 0.5], [0.5, 0.5], [0.5, 0.0]]], } }, { "type": "Feature", "properties": {"id": "top_right"}, "geometry": { "type": "Polygon", "coordinates": [[[0.5, 0.5], [1.0, 0.5], [1.0, 1.0], [0.5, 1.0], [0.5, 0.5]]], } }, { "type": "Feature", "properties": {"id": "top_left"}, "geometry": { "type": "Polygon", "coordinates": [[[0.0, 0.5], [0.5, 0.5], [0.5, 1.0], [0.0, 1.0], [0.0, 0.5]]], } }, ] } var my_point = [0.7, 0.7]; console.log('Point is: ' + my_point) var geo_out = my_squares.features.filter(function(d) {return d3.geoContains(d, my_point)}); console.log('From d3.geoContains'); geo_out.forEach(function(d){console.log(d.properties.id)}); console.log('') var polygons = L.geoJson(my_squares, {}); var res = leafletPip.pointInLayer(my_point, polygons)[0].feature.properties.id; console.log('From pip'); console.log(res) </script> </body> </html>

Your coordinates are in counterclockwise order.

Because the original GeoJSON 1.0 spec had nothing to say on winding order, so d3-geo made up its own rule :

Spherical polygons also require a winding order convention to determine which side of the polygon is the inside: the exterior ring for polygons smaller than a hemisphere must be clockwise, while the exterior ring for polygons larger than a hemisphere must be anticlockwise.

Therefore, in d3-geo's view, your polygons are holes; the features cover everything everywhere except in those holes. Here's a visual example of what's happening .


Unfortunately, the GeoJSON spec in RFC7946 later did specify this, and specified exactly the opposite :

A linear ring MUST follow the right-hand rule with respect to the area it bounds, ie, exterior rings are counterclockwise, and holes are clockwise.

According to a quick reading of its test suite , it appears leaflet-pip assumes this winding order, so it understands your polygons.


If we put your rings in clockwise order, the answers are both top_right :

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <!--d3 --> <script src='https://d3js.org/d3.v4.min.js'></script> <!-- leaflet --> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.3/dist/leaflet.css"/> <script src="https://unpkg.com/leaflet@1.0.3/dist/leaflet-src.js"></script> <script src="https://unpkg.com/leaflet-pip@1.1.0/leaflet-pip.js"></script> </head> <body> <script> var my_squares = { "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": {"id": "bottom_left"}, "geometry": { "type": "Polygon", "coordinates": [[[0.0, 0.0], [0.0, 0.5], [0.5, 0.5], [0.5, 0.0], [0.0, 0.0]]], } }, { "type": "Feature", "properties": {"id": "bottom_right"}, "geometry": { "type": "Polygon", "coordinates": [[[0.5, 0.0], [0.5, 0.5], [1.0, 0.5], [1.0, 0.0], [0.5, 0.0]]], } }, { "type": "Feature", "properties": {"id": "top_right"}, "geometry": { "type": "Polygon", "coordinates": [[[0.5, 0.5], [0.5, 1.0], [1.0, 1.0], [1.0, 0.5], [0.5, 0.5]]], } }, { "type": "Feature", "properties": {"id": "top_left"}, "geometry": { "type": "Polygon", "coordinates": [[[0.0, 0.5], [0.0, 1.0], [0.5, 1.0], [0.5, 0.5], [0.0, 0.5]]], } }, ] } var my_point = [0.7, 0.7]; console.log('Point is: ' + my_point) var geo_out = my_squares.features.filter(function(d) {return d3.geoContains(d, my_point)}); console.log('From d3.geoContains'); geo_out.forEach(function(d){console.log(d.properties.id)}); console.log('') var polygons = L.geoJson(my_squares, {}); var res = leafletPip.pointInLayer(my_point, polygons)[0].feature.properties.id; console.log('From pip'); console.log(res) </script> </body> </html>

(I was surprised that pip's answer didn't reverse. Perhaps it just assumed you meant an exterior ring anyway, or maybe it doesn't support polygons with holes in the first place.)

D3, including d3.geoContains, uses spherical geometry. This is in contrast to almost every other web mapping tool which treat latitude longitude points as Cartesian.

This means every connection between two points in a geographical feature follows great circle distance (which is why it is harder to draw bounding boxes, see here , while other libraries may make it harder to draw great circle arcs, see here ).

By using spherical geometry, the antimeridian doesn't cause as many problems, but conversely, winding order matters: are you drawing everywhere except an area of interest or the area of interest itself.

This applies to d3.geoContains: Your polygons are wound in the opposite order as they are supposed to be. Instead of bounding a small box, they bound the rest of the world - which is why you get the opposite outcome as expected.

You have two options: rewind the polygons , for example with turf.js :

var fixed = features.map(function(feature) {
    return turf.rewind(feature,{reverse:true});
})

Or, if all your polygons are consistently wound in reverse, you could just invert the outcome of d3-geoContains, though this seems like a much less ideal solution.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM