score:3

Accepted answer

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, i.e., 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.)

score:1

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.


Related Query