diff --git a/src/GeoIO.jl b/src/GeoIO.jl index 8fed8b6..81ab4b2 100644 --- a/src/GeoIO.jl +++ b/src/GeoIO.jl @@ -53,7 +53,7 @@ import GeoInterface as GI import GeoFormatTypes as GFT import ArchGDAL.GDAL -# ProjJSON CRS +# PROJJSON CRS import JSON3 # VTK extensions @@ -112,10 +112,13 @@ function formats(io=stdout; sortby=:extension) pretty_table(io, sorted, alignment=:c, crop=:none, show_subheader=false) end -# utilities +# basic utilities include("utils.jl") -# conversions +# utilities for CRS strings +include("crsstrings.jl") + +# utilities for geometry conversion include("conversion.jl") # extra code for backends diff --git a/src/conversion.jl b/src/conversion.jl index f96e89c..8ec44d9 100644 --- a/src/conversion.jl +++ b/src/conversion.jl @@ -2,6 +2,9 @@ # Licensed under the MIT License. See LICENSE in the project root. # ------------------------------------------------------------------ +raw(coords::CRS) = coords.x, coords.y +raw(coords::LatLon) = coords.lon, coords.lat + # -------------------------------------- # Minimum GeoInterface.jl to perform IO # -------------------------------------- @@ -23,9 +26,6 @@ GI.ncoord(::GI.PointTrait, p::Point) = CoordRefSystems.ncoords(crs(p)) GI.getcoord(::GI.PointTrait, p::Point) = ustrip.(raw(coords(p))) GI.getcoord(trait::GI.PointTrait, p::Point, i) = GI.getcoord(trait, p)[i] -raw(coords::CRS) = coords.x, coords.y -raw(coords::LatLon) = coords.lon, coords.lat - GI.ncoord(::GI.LineTrait, s::Segment) = CoordRefSystems.ncoords(crs(s)) GI.ngeom(::GI.LineTrait, s::Segment) = nvertices(s) GI.getgeom(::GI.LineTrait, s::Segment, i) = vertex(s, i) diff --git a/src/crsstrings.jl b/src/crsstrings.jl new file mode 100644 index 0000000..d0c5d58 --- /dev/null +++ b/src/crsstrings.jl @@ -0,0 +1,382 @@ +# ------------------------------------------------------------------ +# Licensed under the MIT License. See LICENSE in the project root. +# ------------------------------------------------------------------ + +# --------------------------------- +# Basic utilities to traverse Dict +# --------------------------------- + +rootkey(d) = nothing +function rootkey(d::Dict) + length(keys(d)) == 1 || throw(ArgumentError("Dictionary must have exactly one key.")) + first(keys(d)) +end + +# from a vector of WKT nodes, find ones that start with `key` +function finditems(key::Symbol, list::Vector) + filter(x -> rootkey(x) == key, list) +end + +function finditem(key::Symbol, list::Vector) + found = filter(x -> rootkey(x) == key, list) + isempty(found) ? nothing : found[1] +end + +function finditem(keys::Vector{Symbol}, list::Vector) + found = [] + for key in keys + i = finditem(key, list) + !isnothing(i) && push!(found, i) + end + + if length(found) == 1 + found[1] + elseif length(found) == 0 + nothing + elseif length(found) > 1 + throw(ArgumentError("Multiple items found for keys: $keys. Only one was expected")) + end +end + +# --------------------------------------- +# Convert from WKT dict to PROJJSON dict +# --------------------------------------- + +function wkt2json(wkt::Dict) + type = rootkey(wkt) + if type == :GEOGCRS + wkt2json_geog(wkt) + elseif type == :GEODCRS + wkt2json_geog(wkt) + elseif type == :PROJCRS + wkt2json_proj(wkt) + else + throw(ArgumentError("WKT to PROJJSON conversion for CRS type: $type is not supported yet.")) + end +end + +# Returns geodetic_crs PROJJSON object. +# Can be either GEOGCRS or GEODCRS WKT nodes. +# Either as top-level crs or under PROJCRS +function wkt2json_geog(wkt::Dict) + geosubtype = rootkey(wkt) + geosubtype ∈ (:GEOGCRS, :GEODCRS) || throw(ArgumentError("Expected key to be GEOGCRS or GEODCRS, got $(geosubtype)")) + jsondict = Dict{String,Any}() + jsondict["type"] = if geosubtype == :GEOGCRS + "GeographicCRS" + elseif geosubtype == :GEODCRS + "GeodeticCRS" + else + throw(ArgumentError("Should be unreachable")) + end + + jsondict["name"] = wkt[geosubtype][1] + + datum = wkt2json_general_datum(wkt) + jsondict[datum.name] = datum.json + + # in our WKT there is no CS node in PROJCRS.BASEGEOGCRS + if !isnothing(finditem(:CS, wkt[geosubtype])) + jsondict["coordinate_system"] = wkt2json_cs(wkt) + end + + jsondict["id"] = wkt2json_id(wkt[geosubtype][end]) + + jsondict +end + +# Schema requires keys: "name", "base_crs", "conversion", and "coordinate_system" +function wkt2json_proj(wkt::Dict) + rootkey(wkt) == :PROJCRS || throw(ArgumentError("Expected key PROJCRS, got $(rootkey(wkt))")) + jsondict = Dict{String,Any}() + jsondict["type"] = "ProjectedCRS" + jsondict["name"] = wkt[:PROJCRS][1] + + basecrs = Dict(:GEOGCRS => wkt[:PROJCRS][2][:BASEGEOGCRS]) + jsondict["base_crs"] = wkt2json_geog(basecrs) + + jsondict["conversion"] = wkt2json_conversion(wkt[:PROJCRS][3]) + jsondict["coordinate_system"] = wkt2json_cs(wkt) + + jsondict["id"] = wkt2json_id(wkt[:PROJCRS][end]) + + jsondict +end + +# Schema requires keys: "name" and "method" +function wkt2json_conversion(conv::Dict) + rootkey(conv) == :CONVERSION || throw(ArgumentError("Expected key CONVERSION, got $(rootkey(conv))")) + jsondict = Dict{String,Any}() + jsondict["name"] = conv[:CONVERSION][1] + + jsondict["parameters"] = [] + params = finditems(:PARAMETER, conv[:CONVERSION]) + for param in params + paramdict = Dict{String,Any}() + paramdict["name"] = param[:PARAMETER][1] + paramdict["value"] = param[:PARAMETER][2] + + unit = wkt2json_unit(param[:PARAMETER]) + if !isnothing(unit) + paramdict["unit"] = unit + end + paramdict["id"] = wkt2json_id(param[:PARAMETER][4]) + push!(jsondict["parameters"], paramdict) + end + + jsondict["method"] = Dict{String,Any}() + jsondict["method"]["name"] = conv[:CONVERSION][2][:METHOD][1] + jsondict["method"]["id"] = wkt2json_id(conv[:CONVERSION][2][:METHOD][end]) + + jsondict +end + +# This function breaks the convention by taking the parent CRS node instead of the CS node, +# because PROJJSON coordinate_system requires information from sibling AXIS and UNIT nodes. +# Schema requires keys: "subtype" and "axis" +function wkt2json_cs(wkt::Dict) + geosubtype = rootkey(wkt) + endswith(string(geosubtype), "CRS") || throw(ArgumentError("Expected base_crs key (such as GEOGCRS or PROJCRS), got $(geosubtype)")) + + jsondict = Dict{String,Any}() + cstype = finditem(:CS, wkt[geosubtype])[:CS][1] + jsondict["subtype"] = string(cstype) + + jsondict["axis"] = [] + axes = finditems(:AXIS, wkt[geosubtype]) + length(axes) > 0 || throw(ArgumentError("Axis entries are required, none are found")) + for axis in axes + axisdict = Dict{String,Any}() + + # parse axis name and abbreviation + name = split(axis[:AXIS][1], " (") + axisdict["name"] = string(name[1]) + axisdict["abbreviation"] = string(name[2][1:(end - 1)]) + + dir = string(axis[:AXIS][2]) + if dir ∈ ("North", "South", "East", "West") + dir = lowercase(dir) + end + axisdict["direction"] = dir + + # if no unit is found in AXIS node, get it from parent CS node + unit = wkt2json_unit(axis[:AXIS]) + if isnothing(unit) + unit = wkt2json_unit(wkt[geosubtype]) + end + axisdict["unit"] = unit + + meridian = finditem(:MERIDIAN, axis[:AXIS]) + if !isnothing(meridian) + axisdict["meridian"] = Dict{String,Any}() + axisdict["meridian"]["longitude"] = valueunit(meridian[:MERIDIAN][1], meridian[:MERIDIAN]) + end + + push!(jsondict["axis"], axisdict) + end + + jsondict +end + +function wkt2json_unit(axis::Vector) + unit = finditem([:ANGLEUNIT, :LENGTHUNIT, :SCALEUNIT], axis) + isnothing(unit) && return nothing + unittype = rootkey(unit) + name = unit[unittype][1] + + # "unit" PROJJSON object can be a simple string if it's one of the following + if name ∈ ("metre", "degree", "unity") + name + else + unitdict = Dict{String,Any}() + unitdict["name"] = name + unitdict["conversion_factor"] = unit[unittype][2] + unitdict["type"] = if unittype == :LENGTHUNIT + "LinearUnit" + elseif unittype == :ANGLEUNIT + "AngularUnit" + elseif unittype == :SCALEUNIT + "ScaleUnit" + else + throw(ArgumentError("Unit type $unittype is not yet supported")) + end + unitdict + end +end + +# See value_in_metre_or_value_and_unit in schema +function valueunit(value::Number, context::Vector) + unit = wkt2json_unit(context) + if unit isa String + value + else + Dict("unit" => unit, "value" => value) + end +end + +# geodetic_crs requires either datum or datum_ensemble objects, +# depending on which is present in WKT +# See one_and_only_one_of_datum_or_datum_ensemble in schema +function wkt2json_general_datum(wkt::Dict) + name = "" + jsondict = Dict{String,Any}() + geosubtype = rootkey(wkt) + + datum = finditem([:ENSEMBLE, :DATUM], wkt[geosubtype]) + if rootkey(datum) == :ENSEMBLE + name = "datum_ensemble" + jsondict = wkt2json_datumensemble(datum) + elseif rootkey(datum) == :DATUM + name = "datum" + jsondict = wkt2json_datum(wkt) + else + throw(ArgumentError("An ENSEMBLE or DATUM node is required, none is found.")) + end + + (name=name, json=jsondict) +end + +# Returns geodetic_reference_frame PROJJSON object. +# Schema requires keys: "name" and "ellipsoid", optionally "type", "anchor_epoch", "prime_meridian" +function wkt2json_datum(wkt::Dict) + geosubtype = rootkey(wkt) + jsondict = Dict{String,Any}() + datum = finditem(:DATUM, wkt[geosubtype]) + jsondict["name"] = datum[:DATUM][1] + + ellipsoid = finditem(:ELLIPSOID, datum[:DATUM]) + jsondict["ellipsoid"] = wkt2json_ellipsoid(ellipsoid) + + anchorepoch = finditem(:ANCHOREPOCH, datum[:DATUM]) + if !isnothing(anchorepoch) + jsondict["anchor_epoch"] = anchorepoch[:ANCHOREPOCH][1] + end + + dynamic = finditem(:DYNAMIC, wkt[geosubtype]) + if !isnothing(dynamic) + jsondict["type"] = "DynamicGeodeticReferenceFrame" + jsondict["frame_reference_epoch"] = dynamic[:DYNAMIC][1][:FRAMEEPOCH][1] + else + jsondict["type"] = "GeodeticReferenceFrame" + end + + prime = finditem(:PRIMEM, wkt[geosubtype]) + if !isnothing(prime) + jsondict["prime_meridian"] = Dict{String,Any}() + jsondict["prime_meridian"]["name"] = prime[:PRIMEM][1] + longitude = valueunit(prime[:PRIMEM][2], prime[:PRIMEM]) + jsondict["prime_meridian"]["longitude"] = longitude + end + + jsondict +end + +# Returns datum_ensemble PROJJSON object. +# Schema requires keys: "name", "members", "accuracy", and optionally "ellipsoid" +function wkt2json_datumensemble(wkt::Dict) + rootkey(wkt) == :ENSEMBLE || throw(ArgumentError("Expected key ENSEMBLE, got $(rootkey(wkt))")) + jsondict = Dict{String,Any}() + jsondict["name"] = wkt[:ENSEMBLE][1] + + jsondict["members"] = [] + members = finditems(:MEMBER, wkt[:ENSEMBLE]) + for m in members + mdict = Dict{String,Any}() + mdict["name"] = m[:MEMBER][1] + mdict["id"] = wkt2json_id(m[:MEMBER][2]) + push!(jsondict["members"], mdict) + end + + accuracy = finditem(:ENSEMBLEACCURACY, wkt[:ENSEMBLE]) + jsondict["accuracy"] = string(float(accuracy[:ENSEMBLEACCURACY][1])) + + ellipsoid = finditem(:ELLIPSOID, wkt[:ENSEMBLE]) + if !isnothing(ellipsoid) + jsondict["ellipsoid"] = wkt2json_ellipsoid(ellipsoid) + end + + jsondict["id"] = wkt2json_id(wkt[:ENSEMBLE][end]) + + jsondict +end + +function wkt2json_ellipsoid(ellipsoid::Dict) + rootkey(ellipsoid) == :ELLIPSOID || throw(ArgumentError("Expected key ELLIPSOID, got $(rootkey(ellipsoid))")) + jsondict = Dict{String,Any}() + jsondict["name"] = ellipsoid[:ELLIPSOID][1] + semimajor = valueunit(ellipsoid[:ELLIPSOID][2], ellipsoid[:ELLIPSOID]) + jsondict["semi_major_axis"] = semimajor + jsondict["inverse_flattening"] = ellipsoid[:ELLIPSOID][3] + + jsondict +end + +function wkt2json_id(id::Dict) + rootkey(id) == :ID || throw(ArgumentError("Expected key ID, got $(rootkey(id))")) + jsondict = Dict{String,Any}() + jsondict["authority"] = id[:ID][1] + jsondict["code"] = id[:ID][2] + + jsondict +end + +# ------------------- +# PROJJSON utilities +# ------------------- + +function projjson(CRS) + try + code = CoordRefSystems.code(CRS) + jsonstr = projjsonstring(code) + json = JSON3.read(jsonstr, Dict) + GFT.ProjJSON(json) + catch + nothing + end +end + +function projjsonstring(code) + wktstr = CoordRefSystems.wkt2(code) + wktdict = wktstr2wktdict(wktstr) + jsondict = wkt2json(wktdict) + JSON3.write(jsondict) +end + +function wktstr2wktdict(wktstr) + expr = Meta.parse(wktstr) + dict = Dict(:root => []) + processexpr(expr, dict) + dict[:root][1] +end + +function processexpr(elem, dict::Dict) + k = first(collect(keys(dict))) + if elem isa Expr + exprname = elem.args[1] + childdict = Dict(exprname => []) + push!(dict[k], childdict) + for childelem in elem.args[2:end] + processexpr(childelem, childdict) + end + elseif elem isa Union{String,Number,Symbol} + push!(dict[k], elem) + else + throw(ArgumentError("The AST representation of the WKT file contains an unexpected node.")) + end + dict +end + +projjsoncode(jsonstr::AbstractString) = projjsoncode(JSON3.read(jsonstr)) + +function projjsoncode(json) + id = json["id"] + code = Int(id["code"]) + authority = id["authority"] + if authority == "EPSG" + EPSG{code} + elseif authority == "ESRI" + ESRI{code} + else + throw(ArgumentError("unsupported authority '$authority' in PROJJSON")) + end +end diff --git a/src/extra/gdal.jl b/src/extra/gdal.jl index 084e79a..93ed163 100644 --- a/src/extra/gdal.jl +++ b/src/extra/gdal.jl @@ -10,6 +10,11 @@ const DRIVER = AG.extensions() asstrings(options::Dict{<:AbstractString,<:AbstractString}) = [uppercase(String(k)) * "=" * String(v) for (k, v) in options] +spatialref(code) = AG.importUserInput(codestring(code)) + +codestring(::Type{EPSG{Code}}) where {Code} = "EPSG:$Code" +codestring(::Type{ESRI{Code}}) where {Code} = "ESRI:$Code" + function agwrite(fname, geotable; layername="data", options=Dict("geometry_name" => "geometry")) geoms = domain(geotable) table = values(geotable) diff --git a/src/extra/gis.jl b/src/extra/gis.jl index 114ff03..90b7d1f 100644 --- a/src/extra/gis.jl +++ b/src/extra/gis.jl @@ -101,45 +101,3 @@ function geomcolumn(names) Symbol(gnames[select]) end end - -function projjsonstring(code; multiline=false) - spref = spatialref(code) - wktptr = Ref{Cstring}() - options = ["MULTILINE=$(multiline ? "YES" : "NO")"] - GDAL.osrexporttoprojjson(spref, wktptr, options) - unsafe_string(wktptr[]) -end - -spatialref(code) = AG.importUserInput(codestring(code)) - -codestring(::Type{EPSG{Code}}) where {Code} = "EPSG:$Code" -codestring(::Type{ESRI{Code}}) where {Code} = "ESRI:$Code" - -function projjsoncode(json) - id = json["id"] - code = Int(id["code"]) - authority = id["authority"] - if authority == "EPSG" - EPSG{code} - elseif authority == "ESRI" - ESRI{code} - else - throw(ArgumentError("unsupported authority '$authority' in ProjJSON")) - end -end - -function projjsoncode(jsonstr::AbstractString) - json = JSON3.read(jsonstr) - projjsoncode(json) -end - -function projjson(CRS) - try - code = CoordRefSystems.code(CRS) - jsonstr = projjsonstring(code) - json = JSON3.read(jsonstr, Dict) - GFT.ProjJSON(json) - catch - nothing - end -end diff --git a/test/Project.toml b/test/Project.toml index 68ae50d..8bedf93 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -7,6 +7,8 @@ FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" GeoJSON = "61d90e0f-e114-555e-ac52-39dfb47a3ef9" GeoTables = "e502b557-6362-48c1-8219-d30d308dcdb0" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" Meshes = "eacbb407-ea5a-433e-ab97-5258b1ca43fa" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" ReadVTK = "dc215faf-f008-4882-a9f7-a79a826fadc3" diff --git a/test/artifacts/projjson.schema.json b/test/artifacts/projjson.schema.json new file mode 100644 index 0000000..a8578c8 --- /dev/null +++ b/test/artifacts/projjson.schema.json @@ -0,0 +1,1174 @@ +{ + "$id": "https://proj.org/schemas/v0.7/projjson.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Schema for PROJJSON (v0.7)", + "$comment": "This document is copyright Even Rouault and PROJ contributors, 2019-2023, and subject to the MIT license. This file exists both in data/ and in schemas/vXXX/. Keep both in sync. And if changing the value of $id, change PROJJSON_DEFAULT_VERSION accordingly in io.cpp", + + "oneOf": [ + { "$ref": "#/definitions/crs" }, + { "$ref": "#/definitions/datum" }, + { "$ref": "#/definitions/datum_ensemble" }, + { "$ref": "#/definitions/ellipsoid" }, + { "$ref": "#/definitions/prime_meridian" }, + { "$ref": "#/definitions/single_operation" }, + { "$ref": "#/definitions/concatenated_operation" }, + { "$ref": "#/definitions/coordinate_metadata" } + ], + + "definitions": { + + "abridged_transformation": { + "type": "object", + "properties": { + "$schema" : { "type": "string" }, + "type": { "type": "string", "enum": ["AbridgedTransformation"] }, + "name": { "type": "string" }, + "source_crs": { + "$ref": "#/definitions/crs", + "$comment": "Only present when the source_crs of the bound_crs does not match the source_crs of the AbridgedTransformation. No equivalent in WKT" + }, + "method": { "$ref": "#/definitions/method" }, + "parameters": { + "type": "array", + "items": { "$ref": "#/definitions/parameter_value" } + }, + "id": { "$ref": "#/definitions/id" }, + "ids": { "$ref": "#/definitions/ids" } + }, + "required" : [ "name", "method", "parameters" ], + "allOf": [ + { "$ref": "#/definitions/id_ids_mutually_exclusive" } + ], + "additionalProperties": false + }, + + "axis": { + "type": "object", + "properties": { + "$schema" : { "type": "string" }, + "type": { "type": "string", "enum": ["Axis"] }, + "name": { "type": "string" }, + "abbreviation": { "type": "string" }, + "direction": { "type": "string", + "enum": [ "north", + "northNorthEast", + "northEast", + "eastNorthEast", + "east", + "eastSouthEast", + "southEast", + "southSouthEast", + "south", + "southSouthWest", + "southWest", + "westSouthWest", + "west", + "westNorthWest", + "northWest", + "northNorthWest", + "up", + "down", + "geocentricX", + "geocentricY", + "geocentricZ", + "columnPositive", + "columnNegative", + "rowPositive", + "rowNegative", + "displayRight", + "displayLeft", + "displayUp", + "displayDown", + "forward", + "aft", + "port", + "starboard", + "clockwise", + "counterClockwise", + "towards", + "awayFrom", + "future", + "past", + "unspecified" ] }, + "meridian": { "$ref": "#/definitions/meridian" }, + "unit": { "$ref": "#/definitions/unit" }, + "minimum_value": { "type": "number" }, + "maximum_value": { "type": "number" }, + "range_meaning": { "type": "string", "enum": [ "exact", "wraparound"] }, + "id": { "$ref": "#/definitions/id" }, + "ids": { "$ref": "#/definitions/ids" } + }, + "required" : [ "name", "abbreviation", "direction" ], + "allOf": [ + { "$ref": "#/definitions/id_ids_mutually_exclusive" } + ], + "additionalProperties": false + }, + + "bbox": { + "type": "object", + "properties": { + "east_longitude": { "type": "number" }, + "west_longitude": { "type": "number" }, + "south_latitude": { "type": "number" }, + "north_latitude": { "type": "number" } + }, + "required" : [ "east_longitude", "west_longitude", + "south_latitude", "north_latitude" ], + "additionalProperties": false + }, + + "bound_crs": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "$schema" : { "type": "string" }, + "type": { "type": "string", "enum": ["BoundCRS"] }, + "name": { "type": "string" }, + "source_crs": { "$ref": "#/definitions/crs" }, + "target_crs": { "$ref": "#/definitions/crs" }, + "transformation": { "$ref": "#/definitions/abridged_transformation" }, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "source_crs", "target_crs", "transformation" ], + "additionalProperties": false + }, + + "compound_crs": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", "enum": ["CompoundCRS"] }, + "name": { "type": "string" }, + "components": { + "type": "array", + "items": { "$ref": "#/definitions/crs" } + }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "components" ], + "additionalProperties": false + }, + + "concatenated_operation": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", "enum": ["ConcatenatedOperation"] }, + "name": { "type": "string" }, + "source_crs": { "$ref": "#/definitions/crs" }, + "target_crs": { "$ref": "#/definitions/crs" }, + "steps": { + "type": "array", + "items": { "$ref": "#/definitions/single_operation" } + }, + "accuracy": { "type": "string" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "source_crs", "target_crs", "steps" ], + "additionalProperties": false + }, + + "conversion": { + "type": "object", + "properties": { + "$schema" : { "type": "string" }, + "type": { "type": "string", "enum": ["Conversion"] }, + "name": { "type": "string" }, + "method": { "$ref": "#/definitions/method" }, + "parameters": { + "type": "array", + "items": { "$ref": "#/definitions/parameter_value" } + }, + "id": { "$ref": "#/definitions/id" }, + "ids": { "$ref": "#/definitions/ids" } + }, + "required" : [ "name", "method" ], + "allOf": [ + { "$ref": "#/definitions/id_ids_mutually_exclusive" } + ], + "additionalProperties": false + }, + + "coordinate_metadata": { + "type": "object", + "properties": { + "$schema" : { "type": "string" }, + "type": { "type": "string", "enum": ["CoordinateMetadata"] }, + "crs": { "$ref": "#/definitions/crs" }, + "coordinateEpoch": { "type": "number" } + }, + "required" : [ "crs" ], + "additionalProperties": false + }, + + "coordinate_system": { + "type": "object", + "properties": { + "$schema" : { "type": "string" }, + "type": { "type": "string", "enum": ["CoordinateSystem"] }, + "name": { "type": "string" }, + "subtype": { "type": "string", + "enum": ["Cartesian", + "spherical", + "ellipsoidal", + "vertical", + "ordinal", + "parametric", + "affine", + "TemporalDateTime", + "TemporalCount", + "TemporalMeasure"] }, + "axis": { + "type": "array", + "items": { "$ref": "#/definitions/axis" } + }, + "id": { "$ref": "#/definitions/id" }, + "ids": { "$ref": "#/definitions/ids" } + }, + "required" : [ "subtype", "axis" ], + "allOf": [ + { "$ref": "#/definitions/id_ids_mutually_exclusive" } + ], + "additionalProperties": false + }, + + "crs": { + "oneOf": [ + { "$ref": "#/definitions/bound_crs" }, + { "$ref": "#/definitions/compound_crs" }, + { "$ref": "#/definitions/derived_engineering_crs" }, + { "$ref": "#/definitions/derived_geodetic_crs" }, + { "$ref": "#/definitions/derived_parametric_crs" }, + { "$ref": "#/definitions/derived_projected_crs" }, + { "$ref": "#/definitions/derived_temporal_crs" }, + { "$ref": "#/definitions/derived_vertical_crs" }, + { "$ref": "#/definitions/engineering_crs" }, + { "$ref": "#/definitions/geodetic_crs" }, + { "$ref": "#/definitions/parametric_crs" }, + { "$ref": "#/definitions/projected_crs" }, + { "$ref": "#/definitions/temporal_crs" }, + { "$ref": "#/definitions/vertical_crs" } + ] + }, + + "datum": { + "oneOf": [ + { "$ref": "#/definitions/geodetic_reference_frame" }, + { "$ref": "#/definitions/vertical_reference_frame" }, + { "$ref": "#/definitions/dynamic_geodetic_reference_frame" }, + { "$ref": "#/definitions/dynamic_vertical_reference_frame" }, + { "$ref": "#/definitions/temporal_datum" }, + { "$ref": "#/definitions/parametric_datum" }, + { "$ref": "#/definitions/engineering_datum" } + ] + }, + + "datum_ensemble": { + "type": "object", + "properties": { + "$schema" : { "type": "string" }, + "type": { "type": "string", "enum": ["DatumEnsemble"] }, + "name": { "type": "string" }, + "members": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "id": { "$ref": "#/definitions/id" }, + "ids": { "$ref": "#/definitions/ids" } + }, + "required" : [ "name" ], + "allOf": [ + { "$ref": "#/definitions/id_ids_mutually_exclusive" } + ], + "additionalProperties": false + } + }, + "ellipsoid": { "$ref": "#/definitions/ellipsoid" }, + "accuracy": { "type": "string" }, + "id": { "$ref": "#/definitions/id" }, + "ids": { "$ref": "#/definitions/ids" } + }, + "required" : [ "name", "members", "accuracy" ], + "allOf": [ + { "$ref": "#/definitions/id_ids_mutually_exclusive" } + ], + "additionalProperties": false + }, + + "deformation_model": { + "description": "Association to a PointMotionOperation", + "type": "object", + "properties": { + "name": { "type": "string" }, + "id": { "$ref": "#/definitions/id" } + }, + "required" : [ "name" ], + "additionalProperties": false + }, + + "derived_engineering_crs": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", + "enum": ["DerivedEngineeringCRS"] }, + "name": { "type": "string" }, + "base_crs": { "$ref": "#/definitions/engineering_crs" }, + "conversion": { "$ref": "#/definitions/conversion" }, + "coordinate_system": { "$ref": "#/definitions/coordinate_system" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "base_crs", "conversion", "coordinate_system" ], + "additionalProperties": false + }, + + "derived_geodetic_crs": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", + "enum": ["DerivedGeodeticCRS", + "DerivedGeographicCRS"] }, + "name": { "type": "string" }, + "base_crs": { "$ref": "#/definitions/geodetic_crs" }, + "conversion": { "$ref": "#/definitions/conversion" }, + "coordinate_system": { "$ref": "#/definitions/coordinate_system" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "base_crs", "conversion", "coordinate_system" ], + "additionalProperties": false + }, + + "derived_parametric_crs": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", + "enum": ["DerivedParametricCRS"] }, + "name": { "type": "string" }, + "base_crs": { "$ref": "#/definitions/parametric_crs" }, + "conversion": { "$ref": "#/definitions/conversion" }, + "coordinate_system": { "$ref": "#/definitions/coordinate_system" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "base_crs", "conversion", "coordinate_system" ], + "additionalProperties": false + }, + + "derived_projected_crs": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", + "enum": ["DerivedProjectedCRS"] }, + "name": { "type": "string" }, + "base_crs": { "$ref": "#/definitions/projected_crs" }, + "conversion": { "$ref": "#/definitions/conversion" }, + "coordinate_system": { "$ref": "#/definitions/coordinate_system" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "base_crs", "conversion", "coordinate_system" ], + "additionalProperties": false + }, + + "derived_temporal_crs": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", + "enum": ["DerivedTemporalCRS"] }, + "name": { "type": "string" }, + "base_crs": { "$ref": "#/definitions/temporal_crs" }, + "conversion": { "$ref": "#/definitions/conversion" }, + "coordinate_system": { "$ref": "#/definitions/coordinate_system" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "base_crs", "conversion", "coordinate_system" ], + "additionalProperties": false + }, + + "derived_vertical_crs": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", + "enum": ["DerivedVerticalCRS"] }, + "name": { "type": "string" }, + "base_crs": { "$ref": "#/definitions/vertical_crs" }, + "conversion": { "$ref": "#/definitions/conversion" }, + "coordinate_system": { "$ref": "#/definitions/coordinate_system" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "base_crs", "conversion", "coordinate_system" ], + "additionalProperties": false + }, + + "dynamic_geodetic_reference_frame": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", "enum": ["DynamicGeodeticReferenceFrame"] }, + "name": {}, + "anchor": {}, + "anchor_epoch": {}, + "ellipsoid": {}, + "prime_meridian": {}, + "frame_reference_epoch": { "type": "number" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "ellipsoid", "frame_reference_epoch" ], + "additionalProperties": false + }, + + "dynamic_vertical_reference_frame": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", "enum": ["DynamicVerticalReferenceFrame"] }, + "name": {}, + "anchor": {}, + "anchor_epoch": {}, + "frame_reference_epoch": { "type": "number" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "frame_reference_epoch" ], + "additionalProperties": false + }, + + "ellipsoid": { + "type": "object", + "oneOf":[ + { + "properties": { + "$schema" : { "type": "string" }, + "type": { "type": "string", "enum": ["Ellipsoid"] }, + "name": { "type": "string" }, + "semi_major_axis": { "$ref": "#/definitions/value_in_metre_or_value_and_unit" }, + "semi_minor_axis": { "$ref": "#/definitions/value_in_metre_or_value_and_unit" }, + "id": { "$ref": "#/definitions/id" }, + "ids": { "$ref": "#/definitions/ids" } + }, + "required" : [ "name", "semi_major_axis", "semi_minor_axis" ], + "additionalProperties": false + }, + { + "properties": { + "$schema" : { "type": "string" }, + "type": { "type": "string", "enum": ["Ellipsoid"] }, + "name": { "type": "string" }, + "semi_major_axis": { "$ref": "#/definitions/value_in_metre_or_value_and_unit" }, + "inverse_flattening": { "type": "number" }, + "id": { "$ref": "#/definitions/id" }, + "ids": { "$ref": "#/definitions/ids" } + }, + "required" : [ "name", "semi_major_axis", "inverse_flattening" ], + "additionalProperties": false + }, + { + "properties": { + "$schema" : { "type": "string" }, + "type": { "type": "string", "enum": ["Ellipsoid"] }, + "name": { "type": "string" }, + "radius": { "$ref": "#/definitions/value_in_metre_or_value_and_unit" }, + "id": { "$ref": "#/definitions/id" }, + "ids": { "$ref": "#/definitions/ids" } + }, + "required" : [ "name", "radius" ], + "additionalProperties": false + } + ], + "allOf": [ + { "$ref": "#/definitions/id_ids_mutually_exclusive" } + ] + }, + + "engineering_crs": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", "enum": ["EngineeringCRS"] }, + "name": { "type": "string" }, + "datum": { "$ref": "#/definitions/engineering_datum" }, + "coordinate_system": { "$ref": "#/definitions/coordinate_system" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "datum" ], + "additionalProperties": false + }, + + "engineering_datum": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", "enum": ["EngineeringDatum"] }, + "name": { "type": "string" }, + "anchor": { "type": "string" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name" ], + "additionalProperties": false + }, + + "geodetic_crs": { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["GeodeticCRS", "GeographicCRS"] }, + "name": { "type": "string" }, + "datum": { + "oneOf": [ + { "$ref": "#/definitions/geodetic_reference_frame" }, + { "$ref": "#/definitions/dynamic_geodetic_reference_frame" } + ] + }, + "datum_ensemble": { "$ref": "#/definitions/datum_ensemble" }, + "coordinate_system": { "$ref": "#/definitions/coordinate_system" }, + "deformation_models": { + "type": "array", + "items": { "$ref": "#/definitions/deformation_model" } + }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name" ], + "description": "One and only one of datum and datum_ensemble must be provided", + "allOf": [ + { "$ref": "#/definitions/object_usage" }, + { "$ref": "#/definitions/one_and_only_one_of_datum_or_datum_ensemble" } + ], + "additionalProperties": false + }, + + "geodetic_reference_frame": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", "enum": ["GeodeticReferenceFrame"] }, + "name": { "type": "string" }, + "anchor": { "type": "string" }, + "anchor_epoch": { "type": "number" }, + "ellipsoid": { "$ref": "#/definitions/ellipsoid" }, + "prime_meridian": { "$ref": "#/definitions/prime_meridian" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "ellipsoid" ], + "additionalProperties": false + }, + + "geoid_model": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "interpolation_crs": { "$ref": "#/definitions/crs" }, + "id": { "$ref": "#/definitions/id" } + }, + "required" : [ "name" ], + "additionalProperties": false + }, + + "id": { + "type": "object", + "properties": { + "authority": { "type": "string" }, + "code": { + "oneOf": [ { "type": "string" }, { "type": "integer" } ] + }, + "version": { + "oneOf": [ { "type": "string" }, { "type": "number" } ] + }, + "authority_citation": { "type": "string" }, + "uri": { "type": "string" } + }, + "required" : [ "authority", "code" ], + "additionalProperties": false + }, + + "ids": { + "type": "array", + "items": { "$ref": "#/definitions/id" } + }, + + "method": { + "type": "object", + "properties": { + "$schema" : { "type": "string" }, + "type": { "type": "string", "enum": ["OperationMethod"]}, + "name": { "type": "string" }, + "id": { "$ref": "#/definitions/id" }, + "ids": { "$ref": "#/definitions/ids" } + }, + "required" : [ "name" ], + "allOf": [ + { "$ref": "#/definitions/id_ids_mutually_exclusive" } + ], + "additionalProperties": false + }, + + "id_ids_mutually_exclusive": { + "not": { + "type": "object", + "required": [ "id", "ids" ] + } + }, + + "one_and_only_one_of_datum_or_datum_ensemble": { + "allOf": [ + { + "not": { + "type": "object", + "required": [ "datum", "datum_ensemble" ] + } + }, + { + "oneOf": [ + { "type": "object", "required": ["datum"] }, + { "type": "object", "required": ["datum_ensemble"] } + ] + } + ] + }, + + "meridian": { + "type": "object", + "properties": { + "$schema" : { "type": "string" }, + "type": { "type": "string", "enum": ["Meridian"] }, + "longitude": { "$ref": "#/definitions/value_in_degree_or_value_and_unit" }, + "id": { "$ref": "#/definitions/id" }, + "ids": { "$ref": "#/definitions/ids" } + }, + "required" : [ "longitude" ], + "allOf": [ + { "$ref": "#/definitions/id_ids_mutually_exclusive" } + ], + "additionalProperties": false + }, + + "object_usage": { + "anyOf": [ + { + "type": "object", + "properties": { + "$schema" : { "type": "string" }, + "scope": { "type": "string" }, + "area": { "type": "string" }, + "bbox": { "$ref": "#/definitions/bbox" }, + "vertical_extent": { "$ref": "#/definitions/vertical_extent" }, + "temporal_extent": { "$ref": "#/definitions/temporal_extent" }, + "remarks": { "type": "string" }, + "id": { "$ref": "#/definitions/id" }, + "ids": { "$ref": "#/definitions/ids" } + }, + "allOf": [ + { "$ref": "#/definitions/id_ids_mutually_exclusive" } + ] + }, + { + "type": "object", + "properties": { + "$schema" : { "type": "string" }, + "usages": { "$ref": "#/definitions/usages" }, + "remarks": { "type": "string" }, + "id": { "$ref": "#/definitions/id" }, + "ids": { "$ref": "#/definitions/ids" } + }, + "allOf": [ + { "$ref": "#/definitions/id_ids_mutually_exclusive" } + ] + } + ] + }, + + "parameter_value": { + "type": "object", + "properties": { + "$schema" : { "type": "string" }, + "type": { "type": "string", "enum": ["ParameterValue"] }, + "name": { "type": "string" }, + "value": { + "oneOf": [ + { "type": "string" }, + { "type": "number" } + ] + }, + "unit": { "$ref": "#/definitions/unit" }, + "id": { "$ref": "#/definitions/id" }, + "ids": { "$ref": "#/definitions/ids" } + }, + "required" : [ "name", "value" ], + "allOf": [ + { "$ref": "#/definitions/id_ids_mutually_exclusive" } + ], + "additionalProperties": false + }, + + "parametric_crs": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", "enum": ["ParametricCRS"] }, + "name": { "type": "string" }, + "datum": { "$ref": "#/definitions/parametric_datum" }, + "coordinate_system": { "$ref": "#/definitions/coordinate_system" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "datum" ], + "additionalProperties": false + }, + + "parametric_datum": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", "enum": ["ParametricDatum"] }, + "name": { "type": "string" }, + "anchor": { "type": "string" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name" ], + "additionalProperties": false + }, + + "point_motion_operation": { + "$comment": "Not implemented in PROJ (at least as of PROJ 9.1)", + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", "enum": ["PointMotionOperation"] }, + "name": { "type": "string" }, + "source_crs": { "$ref": "#/definitions/crs" }, + "method": { "$ref": "#/definitions/method" }, + "parameters": { + "type": "array", + "items": { "$ref": "#/definitions/parameter_value" } + }, + "accuracy": { "type": "string" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "source_crs", "method", "parameters" ], + "additionalProperties": false + }, + + "prime_meridian": { + "type": "object", + "properties": { + "$schema" : { "type": "string" }, + "type": { "type": "string", "enum": ["PrimeMeridian"] }, + "name": { "type": "string" }, + "longitude": { "$ref": "#/definitions/value_in_degree_or_value_and_unit" }, + "id": { "$ref": "#/definitions/id" }, + "ids": { "$ref": "#/definitions/ids" } + }, + "required" : [ "name" ], + "allOf": [ + { "$ref": "#/definitions/id_ids_mutually_exclusive" } + ], + "additionalProperties": false + }, + + "single_operation": { + "oneOf": [ + { "$ref": "#/definitions/conversion" }, + { "$ref": "#/definitions/transformation" }, + { "$ref": "#/definitions/point_motion_operation" } + ] + }, + + "projected_crs": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", + "enum": ["ProjectedCRS"] }, + "name": { "type": "string" }, + "base_crs": { "$ref": "#/definitions/geodetic_crs" }, + "conversion": { "$ref": "#/definitions/conversion" }, + "coordinate_system": { "$ref": "#/definitions/coordinate_system" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "base_crs", "conversion", "coordinate_system" ], + "additionalProperties": false + }, + + "temporal_crs": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", "enum": ["TemporalCRS"] }, + "name": { "type": "string" }, + "datum": { "$ref": "#/definitions/temporal_datum" }, + "coordinate_system": { "$ref": "#/definitions/coordinate_system" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "datum" ], + "additionalProperties": false + }, + + "temporal_datum": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", "enum": ["TemporalDatum"] }, + "name": { "type": "string" }, + "calendar": { "type": "string" }, + "time_origin": { "type": "string" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "calendar" ], + "additionalProperties": false + }, + + "temporal_extent": { + "type": "object", + "properties": { + "start": { "type": "string" }, + "end": { "type": "string" } + }, + "required" : [ "start", "end" ], + "additionalProperties": false + }, + + "transformation": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", "enum": ["Transformation"] }, + "name": { "type": "string" }, + "source_crs": { "$ref": "#/definitions/crs" }, + "target_crs": { "$ref": "#/definitions/crs" }, + "interpolation_crs": { "$ref": "#/definitions/crs" }, + "method": { "$ref": "#/definitions/method" }, + "parameters": { + "type": "array", + "items": { "$ref": "#/definitions/parameter_value" } + }, + "accuracy": { "type": "string" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name", "source_crs", "target_crs", "method", "parameters" ], + "additionalProperties": false + }, + + "unit": { + "oneOf": [ + { + "type": "string", + "enum": ["metre", "degree", "unity"] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", + "enum": ["LinearUnit", "AngularUnit", "ScaleUnit", + "TimeUnit", "ParametricUnit", "Unit"] }, + "name": { "type": "string" }, + "conversion_factor": { "type": "number" }, + "id": { "$ref": "#/definitions/id" }, + "ids": { "$ref": "#/definitions/ids" } + }, + "required" : [ "type", "name" ], + "allOf": [ + { "$ref": "#/definitions/id_ids_mutually_exclusive" } + ], + "additionalProperties": false + } + ] + }, + + "usages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "scope": { "type": "string" }, + "area": { "type": "string" }, + "bbox": { "$ref": "#/definitions/bbox" }, + "vertical_extent": { "$ref": "#/definitions/vertical_extent" }, + "temporal_extent": { "$ref": "#/definitions/temporal_extent" } + }, + "additionalProperties": false + } + }, + + "value_and_unit": { + "type": "object", + "properties": { + "value": { "type": "number" }, + "unit": { "$ref": "#/definitions/unit" } + }, + "required" : [ "value", "unit" ], + "additionalProperties": false + }, + + "value_in_degree_or_value_and_unit": { + "oneOf": [ + { "type": "number" }, + { "$ref": "#/definitions/value_and_unit" } + ] + }, + + "value_in_metre_or_value_and_unit": { + "oneOf": [ + { "type": "number" }, + { "$ref": "#/definitions/value_and_unit" } + ] + }, + + "vertical_crs": { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["VerticalCRS"] }, + "name": { "type": "string" }, + "datum": { + "oneOf": [ + { "$ref": "#/definitions/vertical_reference_frame" }, + { "$ref": "#/definitions/dynamic_vertical_reference_frame" } + ] + }, + "datum_ensemble": { "$ref": "#/definitions/datum_ensemble" }, + "coordinate_system": { "$ref": "#/definitions/coordinate_system" }, + "geoid_model": { "$ref": "#/definitions/geoid_model" }, + "geoid_models": { + "type": "array", + "items": { "$ref": "#/definitions/geoid_model" } + }, + "deformation_models": { + "type": "array", + "items": { "$ref": "#/definitions/deformation_model" } + }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name"], + "description": "One and only one of datum and datum_ensemble must be provided", + "allOf": [ + { "$ref": "#/definitions/object_usage" }, + { "$ref": "#/definitions/one_and_only_one_of_datum_or_datum_ensemble" }, + { + "not": { + "type": "object", + "required": [ "geoid_model", "geoid_models" ] + } + } + ], + "additionalProperties": false + }, + + "vertical_extent": { + "type": "object", + "properties": { + "minimum": { "type": "number" }, + "maximum": { "type": "number" }, + "unit": { "$ref": "#/definitions/unit" } + }, + "required" : [ "minimum", "maximum" ], + "additionalProperties": false + }, + + "vertical_reference_frame": { + "type": "object", + "allOf": [{ "$ref": "#/definitions/object_usage" }], + "properties": { + "type": { "type": "string", "enum": ["VerticalReferenceFrame"] }, + "name": { "type": "string" }, + "anchor": { "type": "string" }, + "anchor_epoch": { "type": "number" }, + "$schema" : {}, + "scope": {}, + "area": {}, + "bbox": {}, + "vertical_extent": {}, + "temporal_extent": {}, + "usages": {}, + "remarks": {}, + "id": {}, "ids": {} + }, + "required" : [ "name" ], + "additionalProperties": false + } + + } +} diff --git a/test/convert.jl b/test/conversion.jl similarity index 99% rename from test/convert.jl rename to test/conversion.jl index 9469c85..62f52c0 100644 --- a/test/convert.jl +++ b/test/conversion.jl @@ -1,4 +1,4 @@ -@testset "convert" begin +@testset "Geometry conversion" begin points = Point.([(0, 0), (2.2, 2.2), (0.5, 2)]) outer = Point.([(0, 0), (2.2, 2.2), (0.5, 2), (0, 0)]) diff --git a/test/crsstrings.jl b/test/crsstrings.jl new file mode 100644 index 0000000..48528b6 --- /dev/null +++ b/test/crsstrings.jl @@ -0,0 +1,41 @@ +@testset "PROJJSON" begin + epsgcodes = [ + # https://github.com/JuliaEarth/CoordRefSystems.jl/blob/10b3f944ece7d5c4669eed6dc163ae8d9761afcd/src/get.jl + 2157, 2193, 3035, 3310, 3395, 3857, 4171, 4207, 4208, 4230, 4231, 4267, 4269, 4274, 4275, 4277, + 4314, 4326, 4618, 4659, 4666, 4668, 4674, 4745, 4746, 4988, 4989, 5070, 5324, 5527, 8086, 8232, + 8237, 8240, 8246, 8249, 8252, 8255, 9777, 9782, 9988, 10176, 10414, 25832, 27700, 28355, 29903, + # 32662, # deprecated in 2008, https://github.com/JuliaEarth/CoordRefSystems.jl/issues/262 + 2180, 32600, 32700, + + # CRS codes with WKT fields that do not occur in the prior codes. + # These WKT fields are only relavent in special circumstances such + # as when using custom measurment units. + 2986, # a PROJJSON with coordinate_system.axis[1].meridian + 3407, # a PROJJSON with non-standard units ("Clarke's foot") requires unit.conversion_factor + 31288, # a PROJJSON with base_crs.datum.prime_meridian + + # Additional codes the exhibit edge cases (EC) when comparing our output with GDAL. + # These edge cases are documented and worked around in the deltaprojjson function. + # In a way, these codes test our testing functions. + 2157, # EC#0 + 4267, # EC#1 + 22248, # EC#2 + ] + + # organize tests by CRS type for ease of debugging + epsgs = [EPSG{code} for code in epsgcodes] + wktstrs = CoordRefSystems.wkt2.(epsgs) + wktdicts = GeoIO.wktstr2wktdict.(wktstrs) + crstypes = GeoIO.rootkey.(wktdicts) + crstuples = [(code=c, type=t, wkt=w) for (c, t, w) in zip(epsgcodes, crstypes, wktdicts)] + + @testset for type in [:GEOGCRS, :GEODCRS, :PROJCRS] + filtered = filter(crs -> crs.type == type, crstuples) + @testset "code = $(crs.code)" for crs in filtered + ourjson = GeoIO.wkt2json(crs.wkt) + @test isvalidprojjson(ourjson) + gdaljson = gdalprojjsondict(EPSG{crs.code}) + @test isempty(deltaprojjson(gdaljson, ourjson)) + end + end +end diff --git a/test/formats.jl b/test/formats.jl index 28c2ddb..f20c2a6 100644 --- a/test/formats.jl +++ b/test/formats.jl @@ -1,4 +1,4 @@ -@testset "formats" begin +@testset "Supported formats" begin io = IOBuffer() exts = [".ply", ".kml", ".gslib", ".shp", ".geojson", ".parquet", ".gpkg", ".png", ".jpg", ".jpeg", ".tif", ".tiff"] diff --git a/test/gis.jl b/test/gisissues.jl similarity index 95% rename from test/gis.jl rename to test/gisissues.jl index 160c54d..df14db3 100644 --- a/test/gis.jl +++ b/test/gisissues.jl @@ -1,4 +1,4 @@ -@testset "GIS" begin +@testset "Known GIS issues" begin table = (float=[0.07, 0.34, 0.69, 0.62, 0.91], int=[1, 2, 3, 4, 5], string=["word1", "word2", "word3", "word4", "word5"]) points = Point.([LatLon(0, 0), LatLon(1, 1), LatLon(2, 2), LatLon(3, 3), LatLon(4, 4)]) @@ -26,14 +26,14 @@ file = joinpath(savedir, "gis-rings.shp") GeoIO.save(file, gtring, warn=false) gtb = GeoIO.load(file) - @test _isequal(gtb.geometry, gtring.geometry) + @test isequalshp(gtb.geometry, gtring.geometry) @test values(gtb) == values(gtring) # note: Shapefile saves PolyArea as MultiPolyArea file = joinpath(savedir, "gis-polys.shp") GeoIO.save(file, gtpoly, warn=false) gtb = GeoIO.load(file) - @test _isequal(gtb.geometry, gtpoly.geometry) + @test isequalshp(gtb.geometry, gtpoly.geometry) @test values(gtb) == values(gtpoly) # GeoJSON diff --git a/test/noattrs.jl b/test/novalues.jl similarity index 97% rename from test/noattrs.jl rename to test/novalues.jl index 39a7a49..b900bc8 100644 --- a/test/noattrs.jl +++ b/test/novalues.jl @@ -1,4 +1,4 @@ -@testset "GeoTables without attributes" begin +@testset "GeoTables without values" begin # Shapefile pset = [Point(0.0, 0.0), Point(1.0, 0.0), Point(0.0, 1.0)] gtb1 = georef(nothing, pset) diff --git a/test/runtests.jl b/test/runtests.jl index 376b2d4..5f8ffa3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -9,6 +9,8 @@ using FixedPointNumbers using Colors using Dates using Unitful +using JSON3 +using JSONSchema import ReadVTK import GeoInterface as GI import Shapefile as SHP @@ -23,19 +25,19 @@ islinux = Sys.islinux() datadir = joinpath(@__DIR__, "data") savedir = mktempdir() -# Note: Shapefile.jl saves Chains and Polygons as Multi -# This function is used to work around this problem -_isequal(d1::Domain, d2::Domain) = all(_isequal(g1, g2) for (g1, g2) in zip(d1, d2)) - -_isequal(g1, g2) = g1 == g2 -_isequal(m1::Multi, m2::Multi) = m1 == m2 -_isequal(g, m::Multi) = _isequal(m, g) -function _isequal(m::Multi, g) - gs = parent(m) - length(gs) == 1 && first(gs) == g -end +# test utilities +include("testutils.jl") testfiles = [ + # CRS strings + "crsstrings.jl", + + # geometry conversion + "conversion.jl", + + # supported formats + "formats.jl", + # IO tests "io/csv.jl", "io/geojson.jl", @@ -55,11 +57,11 @@ testfiles = [ "io/stl.jl", "io/vtk.jl", - # other tests - "formats.jl", - "convert.jl", - "gis.jl", - "noattrs.jl" + # known issues with GIS formats + "gisissues.jl", + + # geotables without values + "novalues.jl" ] @testset "GeoIO.jl" begin diff --git a/test/testutils.jl b/test/testutils.jl new file mode 100644 index 0000000..3499865 --- /dev/null +++ b/test/testutils.jl @@ -0,0 +1,126 @@ +# note: Shapefile.jl saves Chains and Polygons as Multi +# this function is used to work around this problem +isequalshp(d1::Domain, d2::Domain) = all(isequalshp(g1, g2) for (g1, g2) in zip(d1, d2)) +isequalshp(g1, g2) = g1 == g2 +isequalshp(m1::Multi, m2::Multi) = m1 == m2 +isequalshp(g, m::Multi) = isequalshp(m, g) +function isequalshp(m::Multi, g) + gs = parent(m) + length(gs) == 1 && first(gs) == g +end + +# old GDAL implementation of projjsonstring for testing +function gdalprojjsonstring(::Type{EPSG{Code}}; multiline=false) where {Code} + spref = AG.importUserInput("EPSG:$Code") + wktptr = Ref{Cstring}() + options = ["MULTILINE=$(multiline ? "YES" : "NO")"] + AG.GDAL.osrexporttoprojjson(spref, wktptr, options) + unsafe_string(wktptr[]) +end +gdalprojjsondict(code) = JSON3.read(gdalprojjsonstring(code), Dict) + +# validate generated json against PROJJSON schema +function isvalidprojjson(json) + path = joinpath(@__DIR__, "artifacts", "projjson.schema.json") + schema = Schema(JSON3.parsefile(path)) + isvalid(schema, json) +end + +# Return the "paths" of objects that exhibit differences between the two json inputs. +# By default, we clean the results from some optional PROJJSON objects and minor +# discrepancies with GDAL's output. Set `exact = true` to disable that behavior. +# Note: diffpaths uses isapprox to compare numbers to avoid false negatives. +function deltaprojjson(json1, json2; exact=false) + # EC#0 (example 31288): + # Sometimes there are floating point discrepancies between our WKT from the + # EPSG dataset and the GDAL's PROJJSON. This is false-negative noise and is + # dealt with properly using `isapprox` in `finddiffpaths`. For code 31288, + # this happens in datum.prime_meridian.longitude (-17.6666666666667 vs -17.666666667) + diffpaths = finddiffpaths(json1, json2) + + # return full diff in case of exact comparison + exact && return diffpaths + + # paths to ignore in approximate comparison + # bbox, area, scope, ... are not required to + # fully describe the coordinate reference system + pathstoignore = ["bbox", "area", "scope", "usages", "\$schema"] + + # EC#1 (example 4267): + # Sometimes GDAL's PROJJSON ellipsoid is specified using semi_minor_axis instead of inverse_flattening. + # Our PROJJSON ellipsoids are always specified using inverse_flattening because that is the original + # parameterization of the WKT standard. Any other parameterization introduces conversion errors. + # (e.g. semi_minor_axis is calculated from semi_major_axis * (1 - 1/inverse_flattening)) + push!(pathstoignore, "datum.ellipsoid.semi_minor_axis") + push!(pathstoignore, "datum.ellipsoid.inverse_flattening") + + # EC#2 (example 22248): + # Sometimes GDAL's PROJJSON includes an optional base_crs.coordinate_system that we can't support + push!(pathstoignore, "base_crs.coordinate_system") + + # delete paths that are irrelevant for our comparison + for p in pathstoignore + index = findfirst(endswith(p), diffpaths) + if !isnothing(index) + deleteat!(diffpaths, index) + end + end + + diffpaths +end + +# Find differences between two dictionaries and return the paths to those differences as json dot-notation strings. +# This function recursively compares two dictionaries and returns a vector of string paths pointing to the differences found. +# For numerical values, uses `isapprox` for comparison to avoid false negatives. +# +# Example: +# +# julia> d1 = Dict(:A=>[0,20], :B=>3, :C=>4) +# julia> d2 = Dict(:A=>[10,20], :C=>4) +# julia> finddiffpaths(d1, d2) +# 2-element Vector{String}: +# ".A[1]" +# ".B" +function finddiffpaths(d1::Dict, d2::Dict, path="") + paths = String[] + allkeys = union(keys(d1), keys(d2)) + for key in allkeys + newpath = string(path, ".", key) + if haskey(d1, key) && haskey(d2, key) + if !isequalvalue(d1[key], d2[key]) + append!(paths, finddiffpaths(d1[key], d2[key], newpath)) + end + else + push!(paths, newpath) + end + end + + paths +end + +function finddiffpaths(v1::Vector, v2::Vector, path) + paths = String[] + minlen = min(length(v1), length(v2)) + maxlen = max(length(v1), length(v2)) + + for i in 1:minlen + if !isequalvalue(v1[i], v2[i]) + append!(paths, finddiffpaths(v1[i], v2[i], "$(path)[$(i)]")) + end + end + + # extra elements, doesn't matter which vector + for i in (minlen + 1):maxlen + push!(paths, "$(path)[$(i)]") + end + + paths +end + +function finddiffpaths(v1, v2, path) + isequalvalue(v1, v2) ? nothing : [path] +end + +# helper function to compare values in JSON objects +isequalvalue(x::Number, y::Number) = isapprox(x, y) +isequalvalue(x, y) = isequal(x, y)