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>
108 lines
3.0 KiB
Ruby
108 lines
3.0 KiB
Ruby
class GeojsonFormatValidator < ActiveModel::EachValidator
|
|
def validate_each(record, attribute, value)
|
|
if value.present?
|
|
geojson = parse_json(value)
|
|
|
|
unless valid_geojson?(geojson)
|
|
record.errors.add(attribute, :invalid)
|
|
return
|
|
end
|
|
|
|
unless valid_coordinates?(geojson)
|
|
record.errors.add(attribute, :invalid_coordinates)
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def parse_json(geojson_data)
|
|
JSON.parse(geojson_data)
|
|
rescue JSON::ParserError
|
|
nil
|
|
end
|
|
|
|
def valid_geojson?(geojson)
|
|
return false unless geojson.is_a?(Hash)
|
|
|
|
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
|