Skip to content

Commit f4429be

Browse files
authored
Merge pull request #7 from meggart/gdalmissings
Fix attributes for bands
2 parents 8c37aa6 + da49506 commit f4429be

File tree

8 files changed

+284
-13
lines changed

8 files changed

+284
-13
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "YAXArrayBase"
22
uuid = "90b8fcef-0c2d-428d-9c56-5f86629e9d14"
33
authors = ["Fabian Gans <[email protected]>"]
4-
version = "0.2.2"
4+
version = "0.3.0"
55

66
[deps]
77
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"

src/YAXArrayBase.jl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ function __init__()
3131

3232
@require AxisIndices="f52c9ee2-1b1c-4fd8-8546-6350938c7f11" include("axisarrays/axisindices.jl")
3333

34-
@require ArchGDAL="c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3" include("axisarrays/archgdal.jl")
34+
@require ArchGDAL="c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3" begin
35+
include("axisarrays/archgdal.jl")
36+
include("datasets/archgdal.jl")
37+
end
38+
3539

3640
@require Zarr="0a941bbe-ad1d-11e8-39d9-ab76183a1d99" include("datasets/zarr.jl")
3741

src/axisarrays/archgdal.jl

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import .ArchGDAL: RasterDataset, AbstractRasterBand,
22
getgeotransform, width, height, getname, getcolorinterp,
33
getband, nraster, getdataset
4+
45
function dimname(a::RasterDataset, i)
56
if i == 1
67
return :Y
@@ -30,8 +31,17 @@ function dimvals(a::RasterDataset, i)
3031
end
3132
end
3233
iscontdim(a::RasterDataset, i) = i < 3 ? true : nraster(a)<8
33-
getattributes(a::RasterDataset) =
34-
Dict{String,Any}("projection"=>ArchGDAL.toPROJ4(ArchGDAL.newspatialref(ArchGDAL.getproj(a))))
34+
function getattributes(a::RasterDataset)
35+
globatts = Dict{String,Any}(
36+
"projection_PROJ4"=>ArchGDAL.toPROJ4(ArchGDAL.newspatialref(ArchGDAL.getproj(a))),
37+
"projection_WKT"=>ArchGDAL.toWKT(ArchGDAL.newspatialref(ArchGDAL.getproj(a))),
38+
)
39+
bands = (getbandattributes(ArchGDAL.getband(a, i)) for i in 1:size(a, 3))
40+
allbands = mergewith(bands...) do a1,a2
41+
isequal(a1,a2) ? a1 : missing
42+
end
43+
merge(globatts, allbands)
44+
end
3545

3646

3747
function dimname(::AbstractRasterBand, i)
@@ -54,5 +64,24 @@ function dimvals(b::AbstractRasterBand, i)
5464
end
5565
end
5666
iscontdim(a::AbstractRasterBand, i) = true
57-
getattributes(a::AbstractRasterBand) =
58-
getattributes(ArchGDAL.RasterDataset(ArchGDAL.getdataset(a)))
67+
function getattributes(a::AbstractRasterBand)
68+
atts = getattributes(ArchGDAL.RasterDataset(ArchGDAL.getdataset(a)))
69+
bandatts = getbandattributes(a)
70+
merge(atts, bandatts)
71+
end
72+
73+
function insertattifnot!(attrs, val, name, condition)
74+
if !condition(val)
75+
attrs[name] = val
76+
end
77+
end
78+
function getbandattributes(a::AbstractRasterBand)
79+
atts = Dict{String,Any}()
80+
catdict = Dict((i-1)=>v for (i,v) in enumerate(AG.getcategorynames(a)))
81+
insertattifnot!(atts, AG.getnodatavalue(a), "missing_value", isnothing)
82+
insertattifnot!(atts, catdict, "labels", isempty)
83+
insertattifnot!(atts, AG.getunittype(a), "units", isempty)
84+
insertattifnot!(atts, AG.getoffset(a), "add_offset", iszero)
85+
insertattifnot!(atts, AG.getscale(a), "scale_factor", x->isequal(x, one(x)))
86+
atts
87+
end

src/datasets/archgdal.jl

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import .ArchGDAL: RasterDataset, AbstractRasterBand,
2+
getgeotransform, width, height, getname, getcolorinterp,
3+
getband, nraster, getdataset, ArchGDAL
4+
using .ArchGDAL.DiskArrays: GridChunks, DiskArrays, eachchunk
5+
const AG = ArchGDAL
6+
7+
struct GDALBand{T} <: AG.DiskArrays.AbstractDiskArray{T,2}
8+
filename::String
9+
band::Int
10+
size::Tuple{Int,Int}
11+
attrs::Dict{String,Any}
12+
cs::GridChunks{2}
13+
end
14+
function GDALBand(b, filename, i)
15+
s = size(b)
16+
atts = getbandattributes(b)
17+
GDALBand{AG.pixeltype(b)}(filename, i, s, atts, eachchunk(b))
18+
end
19+
Base.size(b::GDALBand) = b.size
20+
DiskArrays.eachchunk(b::GDALBand) = b.cs
21+
DiskArrays.haschunks(::GDALBand) = DiskArrays.Chunked()
22+
function DiskArrays.readblock!(b::GDALBand, aout, r::AbstractUnitRange...)
23+
AG.read(b.filename) do ds
24+
AG.getband(ds, b.band) do bh
25+
DiskArrays.readblock!(bh, aout, r...)
26+
end
27+
end
28+
end
29+
function DiskArrays.writeblock!(b::GDALBand, ain, r::AbstractUnitRange...)
30+
AG.read(b.filename, flags=AG.OF_Update) do ds
31+
AG.getband(ds, b.band) do bh
32+
DiskArrays.writeblock!(bh, ain, r...)
33+
end
34+
end
35+
end
36+
37+
struct GDALDataset
38+
filename::String
39+
bandsize::Tuple{Int,Int}
40+
projection::String
41+
trans::Vector{Float64}
42+
bands::OrderedDict{String}
43+
end
44+
45+
function GDALDataset(filename)
46+
AG.read(filename) do r
47+
nb = AG.nraster(r)
48+
allbands = map(1:nb) do iband
49+
b = AG.getband(r, iband)
50+
gb = GDALBand(b, filename, iband)
51+
name = AG.GDAL.gdalgetdescription(b.ptr)
52+
if isempty(name)
53+
name = AG.getname(AG.getcolorinterp(b))
54+
end
55+
name => gb
56+
end
57+
proj = AG.getproj(r)
58+
trans = AG.getgeotransform(r)
59+
s = AG._common_size(r)
60+
allnames = first.(allbands)
61+
if !allunique(allnames)
62+
allbands = ["Band$i"=>last(v) for (i,v) in enumerate(allbands)]
63+
end
64+
GDALDataset(filename, s[1:end-1], proj, trans, OrderedDict(allbands))
65+
end
66+
end
67+
Base.haskey(ds::GDALDataset, k) = in(k, ("X", "Y")) || haskey(ds.bands, k)
68+
#Implement Dataset interface
69+
function get_var_handle(ds::GDALDataset, name)
70+
if name == "X"
71+
range(ds.trans[1], length = ds.bandsize[1], step = ds.trans[2])
72+
elseif name == "Y"
73+
range(ds.trans[4], length = ds.bandsize[2], step = ds.trans[6])
74+
else
75+
ds.bands[name]
76+
end
77+
end
78+
79+
80+
get_varnames(ds::GDALDataset) = collect(keys(ds.bands))
81+
82+
function get_var_dims(ds::GDALDataset, d)
83+
if d === "X"
84+
return ("X",)
85+
elseif d==="Y"
86+
return ("Y",)
87+
else
88+
return ("X", "Y")
89+
end
90+
end
91+
92+
get_global_attrs(ds::GDALDataset) = Dict("projection"=>ds.projection)
93+
94+
function get_var_attrs(ds::GDALDataset, name)
95+
if name in ("Y", "X")
96+
Dict{String,Any}()
97+
else
98+
ds.bands[name].attrs
99+
end
100+
end
101+
102+
const colornames = AG.getname.(AG.GDALColorInterp.(0:16))
103+
104+
islat(s) = startswith(uppercase(s), "LAT")
105+
islon(s) = startswith(uppercase(s), "LON")
106+
isx(s) = uppercase(s) == "X"
107+
isy(s) = uppercase(s) == "Y"
108+
109+
function totransform(x, y)
110+
xstep = diff(x)
111+
ystep = diff(y)
112+
if !all(isapprox(first(xstep)), xstep) || !all(isapprox(first(ystep)), ystep)
113+
throw(ArgumentError("Grid must have regular spacing"))
114+
end
115+
Float64[first(x), first(xstep), 0.0, first(y), 0.0, first(ystep)]
116+
end
117+
totransform(x::AbstractRange, y::AbstractRange) =
118+
Float64[first(x), step(x), 0.0, first(y), 0.0, step(y)]
119+
getproj(userproj::String, attrs) = AG.importPROJ4(userproj)
120+
getproj(userproj::AG.AbstractSpatialRef, attrs) = userproj
121+
function getproj(userproj::Nothing, attrs)
122+
if haskey(attr, "projection_PROJ4")
123+
return AG.importPROJ4(attr["projection_PROJ4"])
124+
elseif haskey(attr, "projection_WKT")
125+
return AG.importWKT(attr["projection_WKT"])
126+
else
127+
error(
128+
"Could not determine output projection from attributes, please specify userproj",
129+
)
130+
end
131+
end
132+
133+
134+
function create_dataset(
135+
::Type{<:GDALDataset},
136+
outpath,
137+
gatts,
138+
dimnames,
139+
dimvals,
140+
dimattrs,
141+
vartypes,
142+
varnames,
143+
vardims,
144+
varattrs,
145+
varchunks;
146+
userproj = nothing,
147+
kwargs...,
148+
)
149+
@assert length(dimnames) == 2
150+
proj, trans = if islon(dimnames[1]) && islat(dimnames[2])
151+
#Lets set srs to EPSG:4326
152+
proj = AG.importEPSG(4326)
153+
trans = totransform(dimvals[1], dimvals[2])
154+
proj, trans
155+
elseif isx(dimnames[1]) && isy(dimnames[2])
156+
#Try to find out srs
157+
proj = getproj(userproj, gatts)
158+
trans = totransform(dimvals[1], dimvals[2])
159+
proj, trans
160+
else
161+
error("Did not find x, y or lon, lat dimensions in dataset")
162+
end
163+
cs = first(varchunks)
164+
@assert all(isequal(varchunks[1]), varchunks)
165+
166+
driver = AG.getdriver(AG.extensiondriver(outpath))
167+
168+
nbands = length(varnames)
169+
dtype = promote_type(vartypes...)
170+
s = (length.(dimvals)...,)
171+
bands = AG.create(
172+
outpath;
173+
driver = driver,
174+
width = length(dimvals[1]),
175+
height = length(dimvals[2]),
176+
nbands = nbands,
177+
dtype = dtype,
178+
options = ["BLOCKXSIZE=$(cs[1])", "BLOCKYSIZE=$(cs[2])"]
179+
) do ds
180+
AG.setgeotransform!(ds, trans)
181+
bands = map(1:length(varnames)) do i
182+
b = AG.getband(ds, i)
183+
icol = findfirst(isequal(varnames[i]), colornames)
184+
if isnothing(icol)
185+
AG.setcolorinterp!(b, AG.GDAL.GDALColorInterp(0))
186+
else
187+
AG.setcolorinterp!(b, AG.GDAL.GDALColorInterp(icol - 1))
188+
end
189+
AG.GDAL.gdalsetdescription(b.ptr, varnames[i])
190+
atts = varattrs[i]
191+
haskey(atts, "missing_value") && AG.setnodatavalue!(b, atts["missing_value"])
192+
if haskey(atts, "labels")
193+
labeldict = atts[labels]
194+
maxlabel = maximum(keys(labeldict))
195+
kt = keytype(labeldict)
196+
labelvec = [haskey(labeldict, et(i)) ? labeldict[et(i)] : "" for i = 0:maxlabel]
197+
AG.setcategorynames!(b, labelvec)
198+
end
199+
haskey(atts, "units") && AG.setunittype!(b, atts["units"])
200+
haskey(atts, "scale_factor") && AG.setscale!(b, atts["scale_factor"])
201+
haskey(atts, "add_offset") && AG.setoffset!(b, atts["add_offset"])
202+
GDALBand{dtype}(outpath, i, s, atts, AG.DiskArrays.GridChunks(s,cs))
203+
end
204+
end
205+
return GDALDataset(outpath, s, AG.toPROJ4(proj), trans, OrderedDict(vn=>b for (vn,b) in zip(varnames, bands)))
206+
end
207+
208+
allow_parallel_write(::Type{<:GDALDataset}) = false
209+
allow_parallel_write(::GDALDataset) = false
210+
211+
allow_missings(::Type{<:GDALDataset}) = false
212+
allow_missings(::GDALDataset) = false
213+
214+
backendlist[:gdal] = GDALDataset
215+
push!(backendregex,r".tif$"=>GDALDataset)
216+
push!(backendregex,r".gtif$"=>GDALDataset)
217+
push!(backendregex,r".tiff$"=>GDALDataset)
218+
push!(backendregex,r".gtiff$"=>GDALDataset)

src/datasets/datasetinterface.jl

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ function get_var_dims(ds, name) end
1111
"Return a dict with the attributes for a given variable"
1212
function get_var_attrs(ds,name) end
1313

14+
"Return a dict with global attributes for the dataset"
15+
function get_global_attrs(ds) end
1416

1517
#Functions to be implemented for Dataset sinks
1618
"Initialize and return a handle to a new empty dataset"
17-
create_empty(T::Type,path) =
18-
error("create_empty not implemented for $T")
19+
function create_empty(T::Type,path,gatts) end
1920

2021
"""
2122
add_var(ds, T, name, s, dimlist, atts)
@@ -41,13 +42,28 @@ Returns true if Union{T,Missing} is an allowed data type for the backend
4142
and if an array containing missings can be written to the array.
4243
"""
4344
allow_missings(ds) = true
45+
4446
#Fallback for writing array
4547
function add_var(ds,x::AbstractArray,name,dimlist,atts;kwargs...)
4648
a = add_var(ds,eltype(x),name,size(x),dimlist,atts;kwargs...)
4749
a .= x
4850
a
4951
end
5052

53+
function create_dataset(T::Type, path, gatts, dimnames, dimvals, dimattrs, vartypes, varnames, vardims, varattrs, varchunks; kwargs...)
54+
ds = create_empty(T, path, gatts)
55+
axlengths = Dict{String, Int}()
56+
for (dname, dval, dattr) in zip(dimnames, dimvals, dimattrs)
57+
add_var(ds, dval, dname, (dname,), dattr)
58+
axlengths[dname] = length(dval)
59+
end
60+
for (T, vn, vd, va, vc) in zip(vartypes, varnames, vardims, varattrs, varchunks)
61+
s = getindex.(Ref(axlengths),vd)
62+
add_var(ds, T, vn, (s...,), vd, va; chunksize = vc, kwargs...)
63+
end
64+
ds
65+
end
66+
5167
using DataStructures: OrderedDict
5268
"""
5369
YAXArrayBase.backendlist

src/datasets/netcdf.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Base.size(v::NetCDFVariable) = v.size
3838
get_var_dims(ds::NetCDFDataset,name) = NetCDF.open(v->map(i->i.name,v[name].dim),ds.filename)
3939
get_varnames(ds::NetCDFDataset) = NetCDF.open(v->collect(keys(v.vars)),ds.filename)
4040
get_var_attrs(ds::NetCDFDataset, name) = NetCDF.open(v->v[name].atts,ds.filename)
41+
get_global_attrs(ds::NetCDFDataset) = NetCDF.open(nc->nc.gatts, ds.filename)
4142
function Base.getindex(ds::NetCDFDataset, i)
4243
s,et = NetCDF.open(j->(size(j),eltype(j)),ds.filename,i)
4344
NetCDFVariable{et,length(s)}(ds.filename, i, s)
@@ -51,13 +52,15 @@ function add_var(p::NetCDFDataset, T::Type, varname, s, dimnames, attr;
5152
NetCDFVariable{T,length(s)}(p.filename,varname,(s...,))
5253
end
5354

54-
function create_empty(::Type{NetCDFDataset}, path)
55-
NetCDF.create(_->nothing, path, NcVar[])
55+
function create_empty(::Type{NetCDFDataset}, path, gatts=Dict())
56+
NetCDF.create(_->nothing, path, NcVar[], gatts = gatts)
5657
NetCDFDataset(path)
5758
end
5859

60+
allow_parallel_write(::Type{<:NetCDFDataset}) = false
5961
allow_parallel_write(::NetCDFDataset) = false
6062

63+
allow_missings(::Type{<:NetCDFDataset}) = false
6164
allow_missings(::NetCDFDataset) = false
6265

6366
backendlist[:netcdf] = NetCDFDataset

src/datasets/zarr.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ ZarrDataset(g::String) = ZarrDataset(zopen(g))
99
get_var_dims(ds::ZarrDataset,name) = reverse(ds[name].attrs["_ARRAY_DIMENSIONS"])
1010
get_varnames(ds::ZarrDataset) = collect(keys(ds.g.arrays))
1111
get_var_attrs(ds::ZarrDataset, name) = ds[name].attrs
12+
get_global_attrs(ds::ZarrDataset) = ds.g.attrs
1213
Base.getindex(ds::ZarrDataset, i) = ds.g[i]
1314
Base.haskey(ds::ZarrDataset,k) = haskey(ds.g,k)
1415

@@ -33,7 +34,7 @@ function add_var(p::ZarrDataset, a::AbstractArray, varname, dimnames, attr;
3334
a
3435
end
3536

36-
create_empty(::Type{ZarrDataset}, path) = ZarrDataset(zgroup(path))
37+
create_empty(::Type{ZarrDataset}, path, gatts=Dict()) = ZarrDataset(zgroup(path, attrs=gatts))
3738

3839
backendlist[:zarr] = ZarrDataset
3940
push!(backendregex, r"(.zarr$)|(.zarr/$)"=>ZarrDataset)

0 commit comments

Comments
 (0)