Skip to content

Commit f6743a9

Browse files
authored
read/write axis and standard_name for netcdf (#331)
* detect axis name for netcdf * also detect standard_name * bugfix
1 parent 71d5b34 commit f6743a9

File tree

2 files changed

+59
-11
lines changed

2 files changed

+59
-11
lines changed

src/sources/ncdatasets.jl

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const NCDAllowedType = Union{Int8,UInt8,Int16,UInt16,Int32,UInt32,Int64,UInt64,F
66

77
# CF standards don't enforce dimension names.
88
# But these are common, and should take care of most dims.
9-
const NCD_DIMMAP = Dict(
9+
const NCD_DIM_MAP = Dict(
1010
"lat" => Y,
1111
"latitude" => Y,
1212
"lon" => X,
@@ -23,6 +23,20 @@ const NCD_DIMMAP = Dict(
2323
"band" => Band,
2424
)
2525

26+
const NCD_AXIS_MAP = Dict(
27+
"X" => X,
28+
"Y" => Y,
29+
"Z" => Z,
30+
"T" => Ti,
31+
)
32+
33+
const NCD_STANDARD_NAME_MAP = Dict(
34+
"longitude" => X,
35+
"latitude" => Y,
36+
"depth" => Z,
37+
"time" => Ti,
38+
)
39+
2640
haslayers(::Type{NCDfile}) = true
2741
defaultcrs(::Type{NCDfile}) = EPSG(4326)
2842
defaultmappedcrs(::Type{NCDfile}) = EPSG(4326)
@@ -60,7 +74,7 @@ Write an NCDarray to a NetCDF file using NCDatasets.jl
6074
6175
Returns `filename`.
6276
"""
63-
function Base.write(filename::AbstractString, ::Type{NCDfile}, A::AbstractRaster; append = false, kw...)
77+
function Base.write(filename::AbstractString, ::Type{NCDfile}, A::AbstractRaster; append=false, kw...)
6478
mode = !isfile(filename) || !append ? "c" : "a";
6579
ds = NCD.Dataset(filename, mode; attrib=_attribdict(metadata(A)))
6680
try
@@ -161,7 +175,7 @@ function DD.layerdims(ds::NCD.Dataset)
161175
end
162176
function DD.layerdims(var::NCD.Variable)
163177
map(NCD.dimnames(var)) do dimname
164-
_ncddimtype(dimname)()
178+
_ncddimtype(var.attrib, dimname)()
165179
end
166180
end
167181

@@ -228,15 +242,17 @@ cleanreturn(A::NCD.CFVariable) = Array(A)
228242

229243
function _ncddim(ds, dimname::Key, crs=nothing, mappedcrs=nothing)
230244
if haskey(ds, dimname)
231-
D = _ncddimtype(dimname)
245+
var = NCD.variable(ds, dimname)
246+
D = _ncddimtype(var.attrib, dimname)
232247
lookup = _ncdlookup(ds, dimname, D, crs, mappedcrs)
233248
return D(lookup)
234249
else
235250
# The var doesn't exist. Maybe its `complex` or some other marker,
236251
# so make it a custom `Dim` with `NoLookup`
237252
len = _ncfinddimlen(ds, dimname)
238253
len === nothing && _unuseddimerror()
239-
return Dim{Symbol(dimname)}(NoLookup(Base.OneTo(len)))
254+
lookup = NoLookup(Base.OneTo(len))
255+
return Dim{Symbol(dimname)}(lookup)
240256
end
241257
end
242258

@@ -253,25 +269,42 @@ end
253269

254270
# Find the matching dimension constructor. If its an unknown name
255271
# use the generic Dim with the dim name as type parameter
256-
_ncddimtype(dimname) = haskey(NCD_DIMMAP, dimname) ? NCD_DIMMAP[dimname] : DD.basetypeof(DD.key2dim(Symbol(dimname)))
272+
function _ncddimtype(attrib, dimname)
273+
if haskey(attrib, "axis")
274+
k = attrib["axis"]
275+
if haskey(attrib, k)
276+
return NCD_AXIS_MAP[k]
277+
end
278+
end
279+
if haskey(attrib, "standard_name")
280+
k = attrib["standard_name"]
281+
if haskey(NCD_STANDARD_NAME_MAP, k)
282+
return NCD_STANDARD_NAME_MAP[k]
283+
end
284+
end
285+
if haskey(NCD_DIM_MAP, dimname)
286+
return NCD_DIM_MAP[dimname]
287+
end
288+
return DD.basetypeof(DD.key2dim(Symbol(dimname)))
289+
end
257290

258291
# _ncdlookup
259292
# Generate a `LookupArray` from a netcdf dim.
260-
function _ncdlookup(ds::NCD.Dataset, dimname, D, crs, mappedcrs)
293+
function _ncdlookup(ds::NCD.Dataset, dimname, D::Type, crs, mappedcrs)
261294
dvar = ds[dimname]
262295
index = dvar[:]
263296
metadata = _metadatadict(NCDfile, dvar.attrib)
264297
return _ncdlookup(ds, dimname, D, index, metadata, crs, mappedcrs)
265298
end
266299
# For unknown types we just make a Categorical lookup
267-
function _ncdlookup(ds::NCD.Dataset, dimname, D, index::AbstractArray, metadata, crs, mappedcrs)
300+
function _ncdlookup(ds::NCD.Dataset, dimname, D::Type, index::AbstractArray, metadata, crs, mappedcrs)
268301
Categorical(index; order=Unordered(), metadata=metadata)
269302
end
270303
# For Number and AbstractTime we generate order/span/sampling
271304
# We need to include `Missing` in unions in case `_FillValue` is used
272305
# on coordinate variables in a file and propagates here.
273306
function _ncdlookup(
274-
ds::NCD.Dataset, dimname, D, index::AbstractArray{<:Union{Missing,Number,Dates.AbstractTime}},
307+
ds::NCD.Dataset, dimname, D::Type, index::AbstractArray{<:Union{Missing,Number,Dates.AbstractTime}},
275308
metadata, crs, mappedcrs
276309
)
277310
# Assume the locus is at the center of the cell if boundaries aren't provided.
@@ -426,7 +459,10 @@ function _def_dim_var!(ds::NCD.Dataset, dim::Dimension)
426459
if dim isa Y || dim isa X
427460
dim = convertlookup(Mapped, dim)
428461
end
462+
# Attributes
429463
attrib = _attribdict(metadata(dim))
464+
_ncd_set_axis_attrib!(attrib, dim)
465+
# Bounds variables
430466
if span(dim) isa Explicit
431467
bounds = val(span(dim))
432468
boundskey = get(metadata(dim), :bounds, string(dimkey, "_bnds"))
@@ -437,6 +473,15 @@ function _def_dim_var!(ds::NCD.Dataset, dim::Dimension)
437473
return nothing
438474
end
439475

476+
# Add axis and standard name attributes to dimension variabls
477+
# We need to get better at guaranteeing if X/Y is actually measured in `longitude/latitude`
478+
# CF standards requires that we specify "units" if we use these standard names
479+
_ncd_set_axis_attrib!(atr, dim::X) = atr["axis"] = "X" # at["standard_name"] = "longitude";
480+
_ncd_set_axis_attrib!(atr, dim::Y) = atr["axis"] = "Y" # at["standard_name"] = "latitude";
481+
_ncd_set_axis_attrib!(atr, dim::Z) = (atr["axis"] = "Z"; atr["standard_name"] = "depth")
482+
_ncd_set_axis_attrib!(atr, dim::Ti) = (atr["axis"] = "T"; atr["standard_name"] = "time")
483+
_ncd_set_axis_attrib!(atr, dim) = nothing
484+
440485
_ncdshiftlocus(dim::Dimension) = _ncdshiftlocus(lookup(dim), dim)
441486
_ncdshiftlocus(::LookupArray, dim::Dimension) = dim
442487
function _ncdshiftlocus(lookup::AbstractSampled, dim::Dimension)

test/sources/ncdatasets.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,6 @@ stackkeys = (
8383
@test typeof(lookup(ncarray)) <: Tuple{<:Mapped,<:Mapped,<:Sampled}
8484
@test bounds(ncarray) == ((0.0, 360.0), (-80.0, 90.0), (DateTime360Day(2001, 1, 1), DateTime360Day(2003, 1, 1)))
8585
end
86-
tempfile = tempname() * ".nc"
87-
8886

8987
@testset "other fields" begin
9088
@test ismissing(missingval(ncarray))
@@ -211,6 +209,11 @@ stackkeys = (
211209
@test size(geoA) == size(ncarray)
212210
filename = tempname() * ".nc"
213211
write(filename, geoA)
212+
@testset "CF attributes" begin
213+
@test NCDatasets.Dataset(filename)[:x].attrib["axis"] == "X"
214+
@test NCDatasets.Dataset(filename)[:x].attrib["bounds"] == "x_bnds"
215+
# TODO better units and standard name handling
216+
end
214217
saved = read(Raster(filename))
215218
@test size(saved) == size(geoA)
216219
@test refdims(saved) == refdims(geoA)

0 commit comments

Comments
 (0)