Make sure polygons contain valid rings

According to the GeoJSON specification [1]:

> * A linear ring is a closed LineString with four or more positions.
> * The first and last positions are equivalent, and they MUST contain
>   identical values; their representation SHOULD also be identical.
> (...)
> * For type "Polygon", the "coordinates" member MUST be an array of
>   linear ring coordinate arrays.

Note that, for simplicity, right now we aren't checking whether the
coordinates are defined counterclockwise for exterior rings and
clockwise for interior rings, which is what the specification expects.

[1] https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.6
This commit is contained in:
Javi Martín
2024-12-02 13:21:49 +01:00
parent c3bda443a6
commit 1f627d34f1
5 changed files with 228 additions and 8 deletions

View File

@@ -115,8 +115,13 @@ class GeojsonFormatValidator < ActiveModel::EachValidator
end
def valid_polygon_coordinates?(polygon_coordinates)
polygon_coordinates.all? do |ring_coordinates|
valid_coordinates_array?(ring_coordinates)
polygon_coordinates.is_a?(Array) &&
polygon_coordinates.all? { |ring_coordinates| valid_ring_coordinates?(ring_coordinates) }
end
def valid_ring_coordinates?(ring_coordinates)
valid_coordinates_array?(ring_coordinates) &&
ring_coordinates.size >= 4 &&
ring_coordinates.first == ring_coordinates.last
end
end

View File

@@ -25,7 +25,7 @@ FactoryBot.define do
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[0.117, 51.513], [0.118, 51.512], [0.119, 51.514]]]
"coordinates": [[[0.117, 51.513], [0.118, 51.512], [0.119, 51.514], [0.117, 51.513]]]
}
}
JSON

View File

@@ -217,6 +217,221 @@ describe GeojsonFormatValidator do
end
end
context "Polygon geometry" do
it "is not valid with a ring having less than four elements" do
record.geojson = <<~JSON
{
"type": "Polygon",
"coordinates": [[
[1.23, 4.56],
[7.89, 10.11],
[1.23, 4.56]
]]
}
JSON
expect(record).not_to be_valid
end
it "is not valid with a ring which with different starting and end points" do
record.geojson = <<~JSON
{
"type": "Polygon",
"coordinates": [[
[1.23, 4.56],
[7.89, 10.11],
[12.13, 14.15],
[16.17, 18.19]
]]
}
JSON
expect(record).not_to be_valid
end
it "is valid with one valid ring" do
record.geojson = <<~JSON
{
"type": "Polygon",
"coordinates": [[
[1.23, 4.56],
[7.89, 10.11],
[12.13, 14.15],
[1.23, 4.56]
]]
}
JSON
expect(record).to be_valid
end
it "is valid with multiple valid rings" do
record.geojson = <<~JSON
{
"type": "Polygon",
"coordinates": [
[
[100.0, 0.0],
[101.0, 0.0],
[101.0, 1.0],
[100.0, 1.0],
[100.0, 0.0]
],
[
[100.8, 0.8],
[100.8, 0.2],
[100.2, 0.2],
[100.2, 0.8],
[100.8, 0.8]
]
]
}
JSON
expect(record).to be_valid
end
it "is not valid with multiple rings if some rings are invalid" do
record.geojson = <<~JSON
{
"type": "Polygon",
"coordinates": [
[
[100.0, 0.0],
[101.0, 0.0],
[101.0, 1.0],
[100.0, 1.0],
[100.0, 0.0]
],
[
[100.8, 0.8],
[100.8, 0.2],
[100.2, 0.2]
]
]
}
JSON
expect(record).not_to be_valid
end
end
context "MultiPolygon geometry" do
it "is not valid with a one-dimensional array of coordinates" do
record.geojson = '{ "type": "MultiPolygon", "coordinates": [1.23, 4.56] }'
expect(record).not_to be_valid
end
it "is not valid with a two-dimensional array of coordinates" do
record.geojson = '{ "type": "MultiPolygon", "coordinates": [[1.23, 4.56], [7.89, 4.56]] }'
expect(record).not_to be_valid
end
it "is not valid with a three-dimensional polygon coordinates array" do
record.geojson = <<~JSON
{
"type": "MultiPolygon",
"coordinates": [[
[1.23, 4.56],
[7.89, 10.11],
[12.13, 14.15],
[1.23, 4.56]
]]
}
JSON
expect(record).not_to be_valid
end
it "is valid with a valid polygon" do
record.geojson = <<~JSON
{
"type": "MultiPolygon",
"coordinates": [[[
[1.23, 4.56],
[7.89, 10.11],
[12.13, 14.15],
[1.23, 4.56]
]]]
}
JSON
expect(record).to be_valid
end
it "is valid with multiple valid polygons" do
record.geojson = <<~JSON
{
"type": "MultiPolygon",
"coordinates": [
[
[
[1.23, 4.56],
[7.89, 10.11],
[12.13, 14.15],
[1.23, 4.56]
]
],
[
[
[100.0, 0.0],
[101.0, 0.0],
[101.0, 1.0],
[100.0, 1.0],
[100.0, 0.0]
],
[
[100.8, 0.8],
[100.8, 0.2],
[100.2, 0.2],
[100.2, 0.8],
[100.8, 0.8]
]
]
]
}
JSON
expect(record).to be_valid
end
it "is not valid with multiple polygons if some polygons are invalid" do
record.geojson = <<~JSON
{
"type": "MultiPolygon",
"coordinates": [
[
[
[1.23, 4.56],
[7.89, 10.11],
[12.13, 14.15],
[1.23, 4.56]
]
],
[
[
[100.0, 0.0],
[101.0, 0.0],
[101.0, 1.0],
[100.0, 1.0],
[100.0, 0.0]
],
[
[100.8, 0.8],
[100.8, 0.2],
[100.2, 0.2]
]
]
]
}
JSON
expect(record).not_to be_valid
end
end
context "GeometryCollection" do
it "is not valid if it doesn't contain geometries" do
record.geojson = '{ "type": "GeometryCollection" }'

View File

@@ -115,7 +115,7 @@ describe "Admin geozones", :admin do
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[-0.1, 51.5], [-0.2, 51.4], [-0.3, 51.6]]]
"coordinates": [[[-0.1, 51.5], [-0.2, 51.4], [-0.3, 51.6], [-0.1, 51.5]]]
}
}
JSON
@@ -158,7 +158,7 @@ describe "Admin geozones", :admin do
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[-0.1, 51.5], [-0.2, 51.4], [-0.3, 51.6]]]
"coordinates": [[[-0.1, 51.5], [-0.2, 51.4], [-0.3, 51.6], [-0.1, 51.5]]]
}
}
JSON

View File

@@ -1661,7 +1661,7 @@ describe "Budget Investments" do
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[-0.1, 51.5], [-0.2, 51.4], [-0.3, 51.6]]]
"coordinates": [[[-0.1, 51.5], [-0.2, 51.4], [-0.3, 51.6], [-0.1, 51.5]]]
}
}
JSON
@@ -1671,7 +1671,7 @@ describe "Budget Investments" do
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[-0.1, 51.5], [-0.2, 51.5], [-0.2, 51.6], [-0.1, 51.6]]]
"coordinates": [[[-0.1, 51.5], [-0.2, 51.5], [-0.2, 51.6], [-0.1, 51.6], [-0.1, 51.5]]]
}
}
JSON