Skip to content

Commit 00449b1

Browse files
committed
initial half-complete projjson for GEOGCRS
1 parent e052fa8 commit 00449b1

File tree

5 files changed

+176
-6
lines changed

5 files changed

+176
-6
lines changed

src/GeoIO.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ function formats(io::IO=stdout; sortby::Symbol=:extension)
113113
end
114114

115115
include("utils.jl")
116+
include("wkt2json.jl")
116117

117118
# conversions
118119
include("conversion.jl")

src/wkt2json.jl

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
get_main_key(d) = nothing
2+
function get_main_key(d::Dict)
3+
@assert d |> keys |> length == 1
4+
return d |> keys |> first
5+
end
6+
7+
# From a Vector{Dict}, get Dict items that all has a particular key
8+
function get_items_with_key(key::Symbol, list::Vector)
9+
filter(x->get_main_key(x)==key, list)
10+
end
11+
12+
wktdict_crs_type(wkt::Dict) = get_main_key(wkt)
13+
14+
###
15+
# Construct projjson compliant Dict from From WKT2 Dict
16+
###
17+
18+
function wktdict2jsondict(wkt::Dict)
19+
type = get_main_key(wkt)
20+
if type == :GEOGCRS
21+
return wktdict2jsondict_geog(wkt)
22+
elseif type == :GEODCRS
23+
error("Unimplemented CRS type: $type")
24+
elseif type == :PROJCRS
25+
error("Unimplemented CRS type: $type")
26+
else
27+
error("Unimplemented CRS type: $type")
28+
end
29+
end
30+
31+
function wktdict2jsondict_geog(wkt::Dict)
32+
@assert wkt |> keys |> collect == [:GEOGCRS]
33+
jsondict = Dict{String, Any}()
34+
jsondict["type"] = "GeographicCRS"
35+
jsondict["name"] = wkt[:GEOGCRS][1]
36+
37+
# design: bit of redundancy between the [2] and [:ENSEMBLE] inside the function
38+
jsondict["datum_ensemble"] = wktdict2jsondict_ensemble(wkt[:GEOGCRS][2])
39+
40+
# decision, either pass specifically the CS and AXIS arr elements or all the dict. Caveate, for later projected there is cs nested inside
41+
# jsondict["coordinate_system"] = wktdict2jsondict_cs(wkt)
42+
43+
jsondict["id"] = wktdict2jsondict_id(wkt[:GEOGCRS][end])
44+
# return Dict(:root => jsondict)
45+
return jsondict
46+
end
47+
48+
function wktdict2jsondict_ellipsoid(wkt::Dict)
49+
@assert wkt |> keys |> collect == [:ELLIPSOID]
50+
jsondict = Dict()
51+
jsondict["ellipsoid"] = Dict()
52+
jsondict["ellipsoid"]["name"] = wkt[:ELLIPSOID][1]
53+
jsondict["ellipsoid"]["semi_major_axis"] = wkt[:ELLIPSOID][2]
54+
jsondict["ellipsoid"]["inverse_flattening"] = wkt[:ELLIPSOID][3]
55+
return jsondict
56+
end
57+
58+
function wktdict2jsondict_id(id_dict::Dict)::Dict
59+
@assert id_dict |> keys |> collect == [:ID]
60+
jsondict = Dict{String, Any}() # "list" of key value pairs
61+
jsondict["authority"] = id_dict[:ID][1]
62+
jsondict["code"] = id_dict[:ID][2]
63+
return jsondict
64+
end
65+
66+
function wktdict2jsondict_ensemble(wkt::Dict)
67+
@assert wkt |> keys |> collect == [:ENSEMBLE]
68+
jsondict = Dict{String, Any}()
69+
jsondict["name"] = wkt[:ENSEMBLE][1]
70+
71+
members = get_items_with_key(:MEMBER, wkt[:ENSEMBLE])
72+
jsondict["members"] = []
73+
for m in members
74+
mdict = Dict{String, Any}()
75+
mdict["name"] = m[:MEMBER][1]
76+
mdict["id"] = wktdict2jsondict_id(m[:MEMBER][2])
77+
push!(jsondict["members"], mdict)
78+
end
79+
80+
jsondict["id"] = wktdict2jsondict_id(wkt[:ENSEMBLE][end])
81+
82+
return jsondict
83+
end
84+
85+
86+
###
87+
# From WKT2 string to Julia Dict conversion
88+
###
89+
90+
function epsg2wktdict(epsg::Int)::Dict
91+
str = CoordRefSystems.wkt2(EPSG{epsg})
92+
expr = Meta.parse(str)
93+
dict = expr2dict(expr)
94+
end
95+
epsg2wktdict(::Type{EPSG{I}}) where I = epsg2wktdict(I)
96+
97+
function expr2dict(e::Expr)
98+
p = Dict(:root => [])
99+
process_expr(e, p)
100+
return p[:root][1]
101+
end
102+
103+
function process_expr(elem, dict::Dict)
104+
k = dict |> keys |> collect |> first
105+
if elem isa Expr
106+
expr_name = elem.args[1]
107+
child_dict = Dict(expr_name => [])
108+
push!(dict[k], child_dict)
109+
# dict[k] = child_dict
110+
for child_elem in elem.args[2:end]
111+
process_expr(child_elem, child_dict)
112+
end
113+
elseif elem isa Union{String, Number, Symbol}
114+
push!(dict[k], elem)
115+
else
116+
@error "Unhandled case"
117+
end
118+
return dict
119+
end

test/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93"
77
GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f"
88
GeoJSON = "61d90e0f-e114-555e-ac52-39dfb47a3ef9"
99
GeoTables = "e502b557-6362-48c1-8219-d30d308dcdb0"
10+
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
1011
JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692"
1112
Meshes = "eacbb407-ea5a-433e-ab97-5258b1ca43fa"
1213
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

test/jsonutils.jl

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
# Helper function to create a spatial reference
2+
spatialref(code) = AG.importUserInput(codestring(code))
3+
codestring(::Type{EPSG{Code}}) where {Code} = "EPSG:$Code"
4+
codestring(::Type{ESRI{Code}}) where {Code} = "ESRI:$Code"
5+
6+
# Old GDAL implementation of projjsonstring for testing
7+
function gdalprojjsonstring(code; multiline=false)::String
8+
spref = spatialref(code)
9+
wktptr = Ref{Cstring}()
10+
options = ["MULTILINE=$(multiline ? "YES" : "NO")"]
11+
AG.GDAL.osrexporttoprojjson(spref, wktptr, options)
12+
unsafe_string(wktptr[])
13+
end
14+
gdalprojjsondict(code) = JSON3.read(gdalprojjsonstring(code), Dict)
15+
116
# Helper function to validate PROJJSON against schema
217
# function isvalidprojjson(json::JSON3.Object)
318
function isvalidprojjson(json)
@@ -7,3 +22,30 @@ function isvalidprojjson(json)
722
end
823

924
json_round_trip(j) = JSON3.read(j |> JSON3.write, Dict)
25+
26+
# Diff between generated json and ArchGDAL json
27+
function diff_json(j1::J, j2::J) where J<:Union{Dict}
28+
nonsig_keys = ["bbox", "area", "scope"]
29+
[delete!(j1, k) for k in nonsig_keys]
30+
# @show j1 |> keys
31+
diff = deepdiff(j1, j2)
32+
# remove non-required entries
33+
# all_changed_keys = [union(diff.removed, diff.added, keys(diff.changed))...]
34+
# sig_changed_keys = setdiff(all_changed_keys, nonsig_keys)
35+
return diff
36+
end
37+
38+
function test_diff_json(j1, j2)
39+
diff = diff_json(j1, j2)
40+
all_changed_keys = [union(diff.removed, diff.added, keys(diff.changed))...]
41+
isempty(all_changed_keys) && return true
42+
return all_changed_keys
43+
end
44+
45+
function debug_json(crs::Int)
46+
gdaljson = gdalprojjsondict(EPSG{crs})
47+
wktdict = GeoIO.epsg2wktdict(crs)
48+
jsondict = GeoIO.wktdict2jsondict(wktdict)
49+
diff_json(gdaljson, jsondict)
50+
end
51+

test/projjson.jl

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,23 @@ epsgcodes = [
1616
3035 # ETRS89 / LAEA Europe
1717
]
1818

19+
20+
wktdicts = GeoIO.epsg2wktdict.(epsgcodes); crstypes = GeoIO.get_main_key.(wktdicts)
21+
crs_structs = [(code = c, wktdict = w, type = t) for (c, w, t) in zip(epsgcodes, wktdicts, crstypes)]
1922

23+
# TODO: remove failfast, for debugging
24+
@testset "WKT2 -> PROJJSON" failfast=true begin
2025

21-
@testset "WKT2 -> PROJJSON" begin
22-
23-
@testset "code = $(crs)" for crs in epsgcodes
24-
jsondict = GeoIO.projjsonstring(EPSG{crs})
25-
ourjson = jsondict |> JSON3.read
26+
@testset for type in [:GEOGCRS, :GEODCRS, :PROJCRS]
27+
filterd_crs = filter(crs -> crs.type == type, crs_structs)
28+
29+
@testset "code = $(crs.code)" for crs in filterd_crs
30+
jsondict = GeoIO.wktdict2jsondict(crs.wktdict)
31+
ourjson = jsondict |> json_round_trip
2632

27-
@test isvalidprojjson(ourjson)
33+
@test_broken isvalidprojjson(ourjson)
2834

2935
end
3036

3137
end
38+
end

0 commit comments

Comments
 (0)