Support FeatureCollection and MultiPolygon in geozones

We're reworking the format validation to correctly interpret feature
collection, feature, and geometry, according to RFC 7946 [1].

Since Leaflet interprets GeoJSON format, we're rendering the GeoJSON as
a layer instead of as a set of points. For that, we're normalizing the
GeoJSON to make sure it contains either a Feature or a
FeatureCollection. We're also adding the Leaflet images to the assets
path so the markers used for point geometries are rendered correctly.

Note we no longer allow a GeoJSON containing a geometry but not a
defined type. Since there might be invalid GeoJSON in existing Consul
Democracy databases, we're normalizing these existing geometry objects
to be part of a feature object.

We're also wrapping the outline points in a FeatureCollection object
because most of the large GIS systems eg ArcGIS, QGIS export geojson as
a complete FeatureCollection.

[1] https://datatracker.ietf.org/doc/html/rfc7946

Co-authored-by: Javi Martín <javim@elretirao.net>
This commit is contained in:
CoslaJohn
2024-07-19 12:13:43 +01:00
committed by Javi Martín
parent bf4e79d42b
commit 5dbe2cbf24
12 changed files with 635 additions and 112 deletions

View File

@@ -3,8 +3,13 @@ class GeojsonFormatValidator < ActiveModel::EachValidator
if value.present?
geojson = parse_json(value)
unless geojson?(geojson)
unless valid_geojson?(geojson)
record.errors.add(attribute, :invalid)
return
end
unless valid_coordinates?(geojson)
record.errors.add(attribute, :invalid_coordinates)
end
end
end
@@ -12,12 +17,91 @@ class GeojsonFormatValidator < ActiveModel::EachValidator
private
def parse_json(geojson_data)
JSON.parse(geojson_data) rescue nil
JSON.parse(geojson_data)
rescue JSON::ParserError
nil
end
def geojson?(geojson)
def valid_geojson?(geojson)
return false unless geojson.is_a?(Hash)
geojson.dig("geometry", "coordinates").is_a?(Array)
if geojson["type"] == "FeatureCollection"
valid_feature_collection?(geojson)
elsif geojson["type"] == "Feature"
valid_feature?(geojson)
else
valid_geometry?(geojson)
end
end
def valid_feature_collection?(geojson)
return false unless geojson["features"].is_a?(Array)
geojson["features"].all? { |feature| valid_feature?(feature) }
end
def valid_feature?(feature)
feature["type"] == "Feature" && valid_geometry?(feature["geometry"])
end
def valid_geometry?(geometry)
geometry.is_a?(Hash) && valid_geometry_types.include?(geometry["type"])
end
def valid_geometry_types
[
"Point", "LineString", "Polygon", "MultiPoint", "MultiLineString", "MultiPolygon",
"GeometryCollection"
]
end
def valid_coordinates?(geojson)
if geojson["type"] == "FeatureCollection"
geojson["features"].all? { |feature| valid_coordinates?(feature) }
elsif geojson["type"] == "Feature"
valid_geometry_coordinates?(geojson["geometry"])
else
valid_geometry_coordinates?(geojson)
end
end
def valid_geometry_coordinates?(geometry)
if geometry["type"] == "GeometryCollection"
geometries = geometry["geometries"]
return geometries.is_a?(Array) && geometries.all? { |geom| valid_geometry_coordinates?(geom) }
end
coordinates = geometry["coordinates"]
return false unless coordinates.is_a?(Array)
case geometry["type"]
when "Point"
valid_wgs84_coordinates?(coordinates)
when "LineString", "MultiPoint"
coordinates.all? { |coordinates| valid_wgs84_coordinates?(coordinates) }
when "Polygon", "MultiLineString"
valid_polygon_coordinates?(coordinates)
when "MultiPolygon"
coordinates.all? do |polygon_coordinates|
valid_polygon_coordinates?(polygon_coordinates)
end
else
false
end
end
def valid_wgs84_coordinates?(coordinates)
return false unless coordinates.is_a?(Array) && coordinates.size == 2
longitude, latitude = coordinates
(-180.0..180.0).include?(longitude) && (-90.0..90.0).include?(latitude)
end
def valid_polygon_coordinates?(polygon_coordinates)
polygon_coordinates.all? do |ring|
ring.all? { |coordinates| valid_wgs84_coordinates?(coordinates) }
end
end
end