Skip to content

Commit 3aecd65

Browse files
Merge pull request #76 from ncss-tech/horizon-id-madness
Horizon id madness
2 parents ebe33d4 + be2011d commit 3aecd65

26 files changed

+720
-308
lines changed

NAMESPACE

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ importFrom(methods,
2525
setMethod,
2626
as,
2727
show,
28-
slot
28+
slot,
29+
.hasSlot,
30+
slotNames
2931
)
3032

3133

NEWS

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
* new functions: previewColors(), colorQuantiles(), plotColorQuantiles()
55
* new function: horizonDepths()<-, edit top/bottom names after SPC init
66
* new function: profile_id()<-, edit profile IDs after init; be careful!
7-
* rbind method for SoilProfileCollection objects [...]
7+
* new functions: hzID() and hzID()<-, get/set unique horizon IDs
8+
* new functions: hzidname() and hzidname()<-, get/set column containing unique horizon IDs
9+
* rbind.SoilProfileCollection() has been deprecated in favor of union(), gains new functionality:
810
* bug fixes in sanity checks for horizonNames()<-
11+
* !!! SoilProfileCollection internal structure has changed:
12+
*
913

1014
-------------------------- aqp 1.16-6 (2018-12-12) ------------------------
1115
* partial bug fix in test_hz_logic() related to missing top AND bottom depths, needs work: https://github.com/ncss-tech/aqp/issues/65

R/Class-SoilProfileCollection.R

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# https://github.com/ncss-tech/aqp/issues/75
12
# class def for main class within aqp
23
.SoilProfileCollectionValidity <- function(object) {
34

@@ -43,6 +44,7 @@ setClass(
4344
Class='SoilProfileCollection',
4445
representation=representation(
4546
idcol='character', # column name containing IDs
47+
hzidcol='character',
4648
depthcols='character', # 2 element vector with column names for hz top, bottom
4749
metadata='data.frame', # single-row dataframe with key-value mapping
4850
horizons='data.frame', # all horizons sorted by ID, top
@@ -52,6 +54,7 @@ setClass(
5254
),
5355
prototype=prototype(
5456
idcol='id',
57+
hzidcol='hzID',
5558
depthcols=c('top','bottom'),
5659
metadata=data.frame(stringsAsFactors=FALSE), # default units are unkown
5760
horizons=data.frame(stringsAsFactors=FALSE),

R/SoilProfileCollection-coercion.R

+33
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,37 @@
11
## Coercition methods: general
2+
3+
# safely deconstruct as list
4+
setAs("SoilProfileCollection", "list", function(from) {
5+
6+
# get slot names from prototype
7+
sn <- slotNames(from)
8+
9+
# test for presence of all slots
10+
# copy contents over to list with same name
11+
# if missing return NULL + warning
12+
s.list <- lapply(sn, function(i) {
13+
if(.hasSlot(from, name=i)) {
14+
res <- slot(from, i)
15+
} else {
16+
res <- NULL
17+
}
18+
return(res)
19+
})
20+
21+
# copy slot names
22+
names(s.list) <- sn
23+
24+
# test for missing slots
25+
if(any(sapply(s.list, is.null))) {
26+
warning("some slots were missing, use reBuildSPC to fix", call. = FALSE)
27+
}
28+
29+
return(s.list)
30+
31+
}
32+
)
33+
34+
235
setAs("SoilProfileCollection", "data.frame", function(from) {
336

437
# horizons + site + coordinates

R/SoilProfileCollection-methods.R

+22-97
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,28 @@ setMethod("idname", "SoilProfileCollection",
6767
return(object@idcol)
6868
)
6969

70+
## horizon ID name
71+
if (!isGeneric("hzidname"))
72+
setGeneric("hzidname", function(object, ...) standardGeneric("hzidname"))
73+
74+
setMethod("hzidname", "SoilProfileCollection",
75+
function(object)
76+
return(object@hzidcol)
77+
)
78+
79+
## get horizon IDs
80+
if (!isGeneric("hzID"))
81+
setGeneric("hzID", function(object, ...) standardGeneric("hzID"))
82+
83+
setMethod("hzID", "SoilProfileCollection",
84+
function(object) {
85+
h <- horizons(object)
86+
res <- h[[hzidname(object)]]
87+
return(res)
88+
}
89+
90+
)
91+
7092

7193
## distinct profile IDs
7294
if (!isGeneric("profile_id"))
@@ -189,103 +211,6 @@ setMethod("horizonNames", "SoilProfileCollection",
189211
## overloads
190212
##
191213

192-
### This will be greatly improved with new class structure
193-
## concatentation
194-
## # https://github.com/ncss-tech/aqp/issues/71
195-
## TODO: concatenation of data with duplicated IDs in @site, but unique data in other @site fields, will result in corrupt SPC
196-
## TODO: duplicates in @sp will cause errors
197-
## TODO: duplicates are removed in all other slots... does this make sense?
198-
rbind.SoilProfileCollection <- function(...) {
199-
# setup some defaults
200-
options(stringsAsFactors=FALSE)
201-
202-
# parse dots
203-
objects <- list(...)
204-
names(objects) <- NULL
205-
206-
# short-circuits
207-
if(length(objects) == 0)
208-
return(NULL)
209-
if(length(objects) == 1)
210-
return(objects[1])
211-
212-
## TODO: normalize idname and horizonDepths
213-
# profile_id() <-
214-
# horizonDepths() <-
215-
216-
217-
# combine pieces
218-
# should have length of 1
219-
o.idname <- unique(lapply(objects, idname))
220-
o.depth.units <- unique(lapply(objects, depth_units))
221-
o.hz.depths <- unique(lapply(objects, horizonDepths))
222-
o.m <- unique(lapply(objects, aqp::metadata))
223-
o.coords <- unique(lapply(objects, function(i) ncol(coordinates(i))))
224-
o.p4s <- unique(lapply(objects, proj4string))
225-
226-
# should have length > 1
227-
o.h <- lapply(objects, horizons)
228-
o.s <- lapply(objects, site)
229-
o.d <- lapply(objects, diagnostic_hz)
230-
o.sp <- lapply(objects, slot, 'sp')
231-
232-
# sanity checks:
233-
if(length(o.idname) > 1)
234-
stop('inconsistent ID names', call.=FALSE)
235-
if(length(o.depth.units) > 1)
236-
stop('inconsistent depth units', call.=FALSE)
237-
if(length(o.hz.depths) > 1)
238-
stop('inconsistent depth columns', call.=FALSE)
239-
if(length(o.m) > 1)
240-
stop('inconsistent metadata', call.=FALSE)
241-
242-
# spatial data may be missing...
243-
if(length(o.coords) > 1)
244-
stop('inconsistent spatial data', call.=FALSE)
245-
if(length(o.p4s) > 1)
246-
stop('inconsistent CRS', call.=FALSE)
247-
248-
# generate new SPC components
249-
# using plyr::rbind.fill seems to solve the problem on non-conformal DF
250-
# is it safe?
251-
# https://github.com/ncss-tech/aqp/issues/71
252-
o.h <- unique(do.call('rbind.fill', o.h)) # horizon data
253-
o.s <- unique(do.call('rbind.fill', o.s)) # site data
254-
o.d <- unique(do.call('rbind.fill', o.d)) # diagnostic data, leave as-is
255-
256-
## 2015-12-18: removed re-ordering, was creating corrupt SPC objects
257-
## site and horizon data
258-
259-
# spatial points require some more effort when spatial data are missing
260-
o.1.sp <- objects[[1]]@sp
261-
if(ncol(coordinates(o.1.sp)) == 1) # missing spatial data
262-
o.sp <- o.1.sp # copy the first filler
263-
264-
## 2015-12-18: added call to specific function: "sp::rbind.SpatialPoints"
265-
# not missing spatial data
266-
else
267-
o.sp <- do.call("rbind.SpatialPoints", o.sp) # rbind properly
268-
269-
# make SPC and return
270-
res <- SoilProfileCollection(idcol=o.idname[[1]], depthcols=o.hz.depths[[1]], metadata=o.m[[1]], horizons=o.h, site=o.s, sp=o.sp, diagnostic=o.d)
271-
272-
# # one more final check:
273-
# print(profile_id(res))
274-
# print( site(res)[[idname(res)]])
275-
# print(site(res))
276-
277-
if(length(profile_id(res)) != length(site(res)[[idname(res)]]))
278-
stop("SPC object corruption. This shouldn't happen and will be fixed in aqp 2.0", call. = FALSE)
279-
if(! all.equal(profile_id(res), site(res)[[idname(res)]]))
280-
stop("SPC object corruption. This shouldn't happen and will be fixed in aqp 2.0", call. = FALSE)
281-
282-
return(res)
283-
}
284-
285-
286-
## TODO: this doesn't work as expected ... fix in 2.0
287-
## overload rbind
288-
#setMethod("rbind", "SoilProfileCollection", .rbind.SoilProfileCollection)
289214

290215

291216

R/SoilProfileCollection-slice-methods.R

+10-1
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ slice.fast <- function(object, fm, top.down=TRUE, just.the.data=FALSE, strict=TR
153153
if(just.the.data)
154154
return(hd.slices)
155155

156+
## TODO: WTF (AGB: loafercreek[, 2])
156157
# if spatial data and only a single slice: SPDF
157158
if(nrow(coordinates(object)) == length(object) & length(z) == 1) {
158159
cat('result is a SpatialPointsDataFrame object\n')
@@ -169,7 +170,15 @@ slice.fast <- function(object, fm, top.down=TRUE, just.the.data=FALSE, strict=TR
169170

170171

171172
# otherwise return an SPC, be sure to copy over the spatial data
172-
depths(hd.slices) <- as.formula(paste(id, '~', top, '+', bottom))
173+
# NOTE: suppressing warning due to non-unique horizon IDs, don't panic
174+
suppressWarnings(depths(hd.slices) <- as.formula(paste(id, '~', top, '+', bottom)))
175+
176+
# reset auto-generated horizon ID so that we know it is now the slice ID
177+
idx <- match(hzidname(hd.slices), horizonNames(hd.slices))
178+
horizonNames(hd.slices)[idx] <- 'sliceID'
179+
hzidname(hd.slices) <- 'sliceID'
180+
181+
# copy spatial data
173182
hd.slices@sp <- object@sp
174183

175184
# if site data: return an SPC + @site

R/checkSPC.R

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
# test for valid SPC, based on presence / absense of slots as compared to
3+
# class prototype
4+
# likely only used between major versions of aqp where internal structure of SPC has changed
5+
checkSPC <- function(x) {
6+
7+
# get slot names from prototype
8+
sn <- slotNames(x)
9+
10+
# test for all slots in the prototype
11+
s.test <- sapply(sn, function(i) .hasSlot(x, name=i))
12+
13+
# a valid object will have all slots present
14+
if(all(s.test)) {
15+
res <- TRUE
16+
} else {
17+
res <- FALSE
18+
}
19+
20+
return(res)
21+
}
22+
23+

R/rebuildSPC.R

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# repair an SPC by breaking into pieces and re-assembling
2+
# likely only used to fix outdated SPC objects that are missing slots
3+
rebuildSPC <- function(x) {
4+
5+
# break into pieces as list
6+
x.list <- suppressWarnings(as(x, 'list'))
7+
8+
# seed object for new SPC
9+
res <- x.list$horizons
10+
11+
# init SPC from pieces
12+
# note: using depths<- because it will generate a horizon ID
13+
fm <- as.formula(sprintf("%s ~ %s + %s", x.list$idcol, x.list$depthcols[1], x.list$depthcols[2]))
14+
depths(res) <- fm
15+
16+
# add additional pieces
17+
metadata(res) <- x.list$metadata
18+
site(res) <- x.list$site
19+
res@sp <- x.list$sp
20+
diagnostic_hz(res) <- x.list$diagnostic
21+
22+
return(res)
23+
}
24+

0 commit comments

Comments
 (0)