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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user