Skip to content
Snippets Groups Projects
variograms.R 41.75 KiB
#### theoretical variogram ---------------------

#' gmGeostats Variogram models
#' set up a D-variate variogram models 
#'
#' @param type constant indicating the model of correlation function
#' @param nugget (DxD)-matrix for the nugget effect
#' @param sill (DxD)-matrix for the partial sills of the correlation function
#' @param anisRanges 2x2 or 3x3 matrix of ranges (see details)
#' @param extraPar for certain correlation functions, extra parameters (smoothness, period, etc)
#'
#' @return an object of class "gmCgram" containing the linear model of corregionalization
#' of the nugget and the structure given. 
#' @details The argument `type` must be an integer indicating the model to be used. 
#' Some constants are available to make reading code more understandable. That is, you can 
#' either write `1`, `vg.Sph` or `vg.Spherical`, they will all work and produce
#' a spherical model. The same applies for the following models: `vg.Gauss=0`; 
#' `vg.Exp=vg.Exponential=2`. These constants are available after calling
#' `data("variogramModels")`. 
#' No other model is currently available, but this data object will be 
#' regularly updated.
#' The constant vector `gsi.validModels` contains all currently valid models.
#' 
#' Argument `anisRange` expects a matrix $M$ such that 
#' \deqn{
#' h^2 = (\mathbf{x}_i-\mathbf{x}_j)\cdot M^{-1}\cdot (\mathbf{x}_i-\mathbf{x}_j)^t
#' }
#' is the (square of) the lag distance to be fed into the correlation function.
#' @family gmCgram 
#' @export
#' @aliases vg.Exp vg.exp vg.Exponential vg.Gau vg.gauss 
#' vg.Gauss vg.Sph vg.sph vg.Spherical gsi.validModels
#'
#' @examples
#' utils::data("variogramModels") # shortcut for all model constants
#' v1 = setCgram(type=vg.Gau, sill=diag(2), anisRanges = 3*diag(c(3,1)))
#' v2 = setCgram(type=vg.Exp, sill=0.3*diag(2), anisRanges = 0.5*diag(2))
#' vm = v1+v2
#' plot(vm)
setCgram = function(type, nugget=sill*0, sill, anisRanges, extraPar=0){
  utils::data("variogramModels")
  stopifnot(all(dim(sill)==dim(nugget)), 
            ncol(sill)==nrow(sill),
            type %in% gsi.validModels)
  dim(sill) = c(1,dim(sill))
  dim(anisRanges) = c(1,dim(anisRanges))
  vgout <- list(type=type,
                data=extraPar,
                nugget=nugget,
                sill=sill, #(nstru, nvar, nvar)
                M=anisRanges     # these are "classical" ranges (i.e. distances)
  )
  class(vgout) = "gmCgram"
  return(vgout)
}



#' Subsetting of gmCgram variogram structures
#' 
#' Extraction or combination of nested structures of a gmCgram object
#'
#' @param x `gmCgram` variogram object
#' @param i indices of the structures that are desired to be kept (0=nugget) or removed (see details)
#' @param ... extra arguments for generic functionality
#'
#' @return a `gmCgram` variogram object with the desired structures only. 
#' @details This function can be used to: extract the nugget (i=0), extract some 
#' structures (i=indices of the structures, possibly including 0 for the nugget),
#' or filter some structures out (i=negative indices of the structures to remove; 
#' nugget will always removed in this case). If you want to extract "slots" or 
#' "elements" of the variogram, use the $-notation. If you want to extract variables of the
#' variogram matrices, use the `[`-notation. The contrary operation (adding structures together)
#' is obtained by summing (+) two `gmCgram` objects.
#' @export
#' @method [[ gmCgram
#' @family gmCgram functions
#' @examples
#' utils::data("variogramModels")
#' v1 = setCgram(type=vg.Gau, sill=diag(2), anisRanges = 3*diag(c(3,1)))
#' v2 = setCgram(type=vg.Exp, sill=0.3*diag(2), anisRanges = 0.5*diag(2))
#' vm = v1+v2
#' vm[[1]]
"[[.gmCgram"<- function(x,i,...){
  nG = dim(x$M)[3]
  nD = dim(x$nugget)[1]
  nSo = length(i)
  nSi = dim(x$M)[1]
  if(i==0){# case only nugget wanted
    out = with(x, list(type=type[i], 
                       data=data[i], 
                       nugget=nugget, 
                       sill=structure(0*sill[1,,], dim=c(nSo, nD, nD)), 
                       M=structure(M[1,,], dim=c(nSo, nG, nG))))
  }else if( (0 %in% i) & !any(i<0)){
    j = i[i>0] # case some structures wanted, including nugget
    out = with(x, list(type=type[j], 
                       data=data[j], 
                       nugget=nugget, 
                       sill=structure(sill[j,,], dim=c(nSo-1, nD, nD)), 
                       M=structure(M[j,,], dim=c(nSo-1, nG, nG))))
  }else if(all(i>0) | all(i<0)){# case some structured (un)wanted, nugget surely not wanted
    if(all(i<0)) nSo = nSi-nSo
    out = with(x, list(type=type[i], 
                       data=data[i], 
                       nugget=nugget*0, 
                       sill=structure(sill[i,,], dim=c(nSo, nD, nD)), 
                       M=structure(M[i,,], dim=c(nSo, nG, nG))))
  }else{# unsolvable case
    stop("index set i cannot merge 0 and negative numbers")
  }
  class(out) = class(x)
  return(out)
}


#' Combination of gmCgram variogram structures
#' 
#' combination of nested structures of a gmCgram object
#'
#' @param x `gmCgram` variogram object
#' @param y `gmCgram` variogram object
#' @export
#' @return The combined nested structures
#' @method + gmCgram
#' @examples
#' utils::data("variogramModels")
#' v1 = setCgram(type=vg.Gau, sill=diag(2), anisRanges = 3*diag(c(3,1)))
#' v2 = setCgram(type=vg.Exp, sill=0.3*diag(2), anisRanges = 0.5*diag(2))
#' vm = v1+v2
"+.gmCgram" <- function(x,y) {
  y = as.gmCgram(y)
  stopifnot(class(y)=="gmCgram", 
            dim(x$sill)[-1]==dim(y$sill)[-1],
            dim(x$M)[-1]==dim(y$M)[-1])
  myfun = function(A,B){
    D = dim(A)[2]
    nA = dim(A)[1]
    nB = dim(B)[1]
    dim(A) = c(nA, D^2)
    dim(B) = c(nB, D^2)
    out = rbind(A, B)
    dim(out) = c(nA+nB, D, D)
    return(out)
  }
  x$type = c(x$type, y$type)
  x$data= c(x$data, y$data)
  x$nugget = x$nugget + y$nugget
  x$sill = myfun(x$sill, y$sill)
  x$M = myfun(x$M, y$M)
  return(x)
}




#' Subsetting of gmCgram variogram structures
#' 
#' Extraction of some variables of a gmCgram object
#'
#' @param x \code{gmCgram} variogram object
#' @param i row-indices of the variables to be kept/removed
#' @param j column-indices of the variables to be kept/removed (if only \code{i}
#' is specified, \code{j} will be taken as equal to \code{i}!)
#' @param ...  extra arguments for generic functionality
#'
#' @return a \code{gmCgram} variogram object with the desired variables only. 
#' @details This function can be used to extract the model for a a subset of variables. 
#' If only \code{i} is specified, \code{j} will be taken as equal to \code{i}. 
#' If you want to select all \code{i}'s for certain \code{j}'s or vice versa, give
#' \code{i=1:dim(x$nugget)[1]} and \code{j=} your desired indices, respectively  
#' \code{j=1:dim(x$nugget)[2]} and \code{i=} your desired indices; replace \code{x} by the
#' object you are giving. If \code{i!=j}, the output will be a \code{c("gmXCgram","gmCgram")} 
#' object, otherwise it will be a regular class \code{"gmCgram"} object.
#' If you want to extract "slots" or 
#' "elements" of the variogram, use the $-notation. If you want to extract variables of the
#' variogram matrices, use the `[`-notation.
#' @export
#' @method [ gmCgram
#' @family gmCgram functions
#' @examples
#' utils::data("variogramModels")
#' v1 = setCgram(type=vg.Gau, sill=diag(2), anisRanges = 3*diag(c(3,1)))
#' v2 = setCgram(type=vg.Exp, sill=0.3*diag(2), anisRanges = 0.5*diag(2))
#' vm = v1+v2
#' vm[1,1]
"[.gmCgram"<- function(x,i,j=i,...){
  nDi = length(i)
  nDj = length(j)
  nS = dim(x$M)[1]
  out = with(x, list(type=type, 
                     data=data, 
                     nugget=structure(nugget[i,j, drop=F], dim=c(nDi, nDj)), 
                     sill=structure(sill[,i,j, drop=F], dim=c(nS, nDi, nDj)), 
                     M=M))
  class(out) = class(x)
  if(!all(i==j)) class(out) = unique(c("gmXCgram", class(out)))
  return(out)
}

#' Length, and number of columns or rows
#' 
#' Provide number of structures, and nr of variables of an LMC of class gmCgram 
#'
#' @param x gmCgram object
#'
#' @return \code{length} returns the number of structures (nugget not counted), while
#' \code{ncol} and \code{nrow} return these values for the nugget (assuming that they will
#' be also valid for the sill).
#' @export
#' @aliases ncol.gmCgram nrow.gmCgram
#' @family gmCgram functions
#'
#' @method length gmCgram
#' @examples
#' utils::data("variogramModels")
#' v1 = setCgram(type=vg.Gau, sill=diag(3)+0.5, anisRanges = 2*diag(c(3,0.5)))
#' v2 = setCgram(type=vg.Exp, sill=0.3*diag(3), anisRanges = 0.5*diag(2))
#' vm = v1+v2
#' length(vm)
#' ncol(vm)
#' nrow(vm)
length.gmCgram = function(x)  length(x$type)
if(!isGeneric("ncol")){
  ncol <- function(x) UseMethod("ncol",x)
  ncol.default <- base::ncol
}
if(!isGeneric("nrow")){
  nrow <- function(x) UseMethod("nrow",x)
  nrow.default <- base::nrow
}
ncol.gmCgram = function(x) ncol(x$nugget)
nrow.gmCgram = function(x) nrow(x$nugget)


#' Convert a gmCgram object to an (evaluable) function
#' 
#' Evaluate a gmCgram on some h values, or convert the gmCgram object into an evaluable function
#'
#' @param x a gmCgram object
#' @param ... extra arguments for generic functionality
#'
#' @return a \code{function} that can be evaluated normally, with an argument \code{X}
#' and possibly another argument \code{Y}; both must have the same number of columns 
#' than the geographic dimension of the variogram (i.e. \code{dim(x$M)[3]}).
#' @export
#' @method as.function gmCgram
#' @family gmCgram functions
#' @examples
#' utils::data("variogramModels")
#' v1 = setCgram(type=vg.Gau, sill=diag(2)+0.5, anisRanges = 2*diag(c(3,0.5)))
#' v2 = setCgram(type=vg.Exp, sill=0.3*diag(2), anisRanges = 0.5*diag(2))
#' vm = v1+v2
#' vgf = as.function(vm)
#' (h = rbind(c(0,1), c(0,0), c(1,1)))
#' vgf(h)
#' predict(vm, h)
as.function.gmCgram = function(x,...){
    f <- function(X,Y=X){
      if(is(X,"Spatial")) X = sp::coordinates(X)
      X = as.matrix(X)
      if(is(Y,"Spatial")) Y = sp::coordinates(Y)
      Y = as.matrix(Y)
      stopifnot(ncol(X)==ncol(Y), ncol(X)==dim(x$M)[3])
      ijEqual = ifelse(nrow(X)==nrow(Y), all(X==Y), FALSE)
      o = gsi.calcCgram(X,Y,x,ijEqual)
      return(o)
    }
  return(f)
}

#' @describeIn as.function.gmCgram predict a gmCgram object on some lag vector coordinates
#' @param object gmCgram object
#' @param newdata matrix, data.frame or Spatial object containing coordinates
#' @include gmAnisotropy.R
#' @method predict gmCgram
#' @export
predict.gmCgram = function(object, newdata, ...){
  as.function(object)(X=newdata)
}


#' Convert theoretical structural functions to gmCgram format 
#' 
#' Convert covariance function or variogram models to the format gmCgram 
#' of package gmGeostats
#' 
#' @param m model to be converted
#' @param ... further parameters
#'
#' @return the covariance/variogram model, recasted to class \code{gmCgram}. 
#' This is a generic function. Methods exist for objects of class
#' \code{LMCAnisCompo} (for compositional data) and \code{variogramModelList}
#' (as provided by package \code{gstat}).  
#' @export
#' @family gmCgram functions
as.gmCgram <- function(m, ...)  UseMethod("as.gmCgram",m) 

#' @describeIn as.gmCgram Convert theoretical structural functions to gmCgram format 
#' @method as.gmCgram default
#' @export
as.gmCgram.default <- function(m,...) m 



#' Draw cuves for covariance/variogram models
#' 
#' Represent a gmCgram object as a matrix of lines in several plots
#'
#' @param x object to draw, of class gmCgram // curently only valid for symmetric functions
#' @param xlim.up range of lag values to use in plots of the upper triangle
#' @param xlim.lo range of lag values to use in plots of the lower triangle
#' @param vdir.up geograohic directions to represent in the upper triangle 
#' @param vdir.lo geograohic directions to represent in the lower triangle
#' @param xlength number of discretization points to use for the curves (defaults to 200)
#' @param varnames string vector, variable names to use in the labelling of axes
#' @param add logical, should a new plot be created or stuff be added to an existing one?
#' @param commonAxis logical, is a common Y axis for all plots in a row desired?
#' @param cov logical, should the covariance function (=TRUE) or the variogram (=FALSE) be plotted?   
#' @param closeplot logical, should the plot be left open (FALSE) for further changes, or be frozen (TRUE)? 
#' defaults to TRUE
#' @param ... further graphical parameters for the plotting function
#'
#' @return This function is called for its side effect of producing a plot: the plot will be
#' open to further changes if you provide `closeplot=FALSE`. Additionally, the function
#' invisibly returns the graphical parameters that were active before starting the plot. Hence, 
#' if you want to freeze a plot and not add anymore to it, you can do \code{par(plot(x, closeplot=FALSE, ...))},
#' or \code{plot(x, closeplot=TRUE, ...)}.
#' If you want to further add stuff to it, better just call \code{plot(x, closeplot=FALSE,...)}. The difference
#' is only relevant when working with the screen graphical device. 
#' @export 
#' @method plot gmCgram
#' @family gmCgram functions
#' @examples
#' utils::data("variogramModels")
#' v1 = setCgram(type=vg.Gau, sill=diag(3)-0.5, anisRanges = 2*diag(c(3,0.5)))
#' v2 = setCgram(type=vg.Exp, sill=0.3*diag(3), anisRanges = 0.5*diag(2))
#' vm = v1+v2
#' plot(vm)
#' plot(vm, cov=FALSE)
plot.gmCgram = function(x, xlim.up=NULL, xlim.lo=NULL, vdir.up= NULL, vdir.lo= NULL, xlength=200, varnames = colnames(x$nugget), 
                        add=FALSE, commonAxis=TRUE, cov =TRUE, closeplot=TRUE, ...){
  Dg = dim(x$M)[2]
  Ns = dim(x$M)[1]
  Dv = dim(x$nugget)[1]
  if(is.null(varnames)) varnames = paste("v", 1:Dv, sep="")
  if(is.null(vdir.up) & is.null(vdir.lo)){
    vdir.lo = rep(0, Dg)
    vdir.lo[1] = 1
    dim(vdir.lo) = c(1,Dg)
    if(!is.isotropic(x)){
      aux = diag(Dg)
      if(Dg==3){
        vdir.lo = aux[3,]
        vdir.up = aux[-3,]
      }else
        vdir.lo = aux
    }
  }
  if(!is.null(vdir.up)) vdir.up = compositions::oneOrDataset(compositions::normalize(vdir.up))
  if(!is.null(vdir.lo)) vdir.lo = compositions::oneOrDataset(compositions::normalize(vdir.lo))
  fk = c(sqrt(3),1,3)[x$type+1]*1.25  # range to effective range factor expansion
  if(is.null(xlim.up)){
    if(add){
      par(mfg=c(1,2))
      xlim.up = par()$usr[1:2]
    }else if(!is.null(vdir.up)){
      maxdist = sapply(1:Ns, function(i) max(sapply(1:nrow(vdir.up), function(j) vdir.up[j,]%*%x$M[i,,]%*%vdir.up[j,])))
      # here we must compute the max M projected on vd.up
      xlim.up = c(0, max(fk*maxdist))
    }
  }
  if(is.null(xlim.lo)){
    if(add){
      par(mfg=c(2,1))
      xlim.lo = par()$usr[1:2]
    }else if(!is.null(vdir.lo)){
      maxdist = sapply(1:Ns, function(i) max(sapply(1:nrow(vdir.lo), function(j) vdir.lo[j,]%*%x$M[i,,]%*%vdir.lo[j,])))
      # here we must compute the max M projected on vd.lo
      xlim.lo = c(0, max(fk*maxdist))
    }
  }
  if(!is.null(xlim.up)){
    xseq.up = seq(from=xlim.up[1], to=xlim.up[2], length.out=xlength)
  }else{ xseq.up=NULL}
  if(!is.null(xlim.lo)){
    xseq.lo = seq(from=xlim.lo[1], to=xlim.lo[2], length.out=xlength)
  }else{ xseq.lo=NULL}
  
  opar = par()
  opar = par_remove_readonly(opar)
  
  if(closeplot) on.exit(par(opar))
  
  getVdens = function(vdir, xseq){
    if(is.null(vdir)|is.null(xseq)) return(NULL)
    Vdens = sapply(1:nrow(vdir), function(k){
      X = outer(xseq, vdir[k,])
      Y = X[1,,drop=F]*0
      gsi.calcCgram(X,Y,x,FALSE)
    })
    dim(Vdens) = c(Dv,xlength,Dv,nrow(vdir))
    if(!cov){
      # convert to variogram if cov=FALSE
      Y = matrix(rep(0,Dg), ncol=Dg)
      C0 = gsi.calcCgram(Y,Y,x,FALSE)
      dim(C0) = c(Dv,Dv)
      Vdens = sweep(-Vdens, c(1,3), C0, "+") ## this must be corrected when we allow non-symmetric covariances
    }
    return(Vdens)    
  }
  Vdens.up = getVdens(vdir.up, xseq.up)
  Vdens.lo = getVdens(vdir.lo, xseq.lo)
  
  myplot = function(...) matplot(type="l",ylab="", xlab="",xaxt="n", ...)
  if(add) myplot = function(...) matlines(...)
  if(!add){
    par(mfrow=c(Dv+1,Dv+1), mar=c(2,3,0,0), oma=c(1,4,1,1), xpd=NA)
    myplot(c(0,0), c(0,0), pch="", ann=FALSE, bty="n", yaxt="n") 
  }
  for(i in 1:Dv){
      for(j in 1:Dv){
        if((i>=j)&!(is.null(vdir.lo)|is.null(xlim.lo))){
            par(mfg=c(i+1,j,Dv+1,Dv+1))
            ylim = range(Vdens.lo[,,j,])
            if(commonAxis) ylim=range(Vdens.lo[,,j,])
            myplot(xseq.lo, Vdens.lo[i,,j,], ylim=ylim, ...)
            if(i==j & !add){
              axis(side = 3)
              mtext(text=varnames[i], side = 3, line=3)
              mtext(text=varnames[i], side = 4, line=3)
            }
        }
        if((i<=j)&!(is.null(vdir.up)|is.null(xlim.up))){
            par(mfg=c(i,j+1,Dv+1,Dv+1))
            ylim = range(Vdens.up[,,j,])
            if(commonAxis) ylim=range(Vdens.up[,,j,])
            myplot(xseq.up, Vdens.up[i,,j,], ylim=ylim,...)
            if(i==j & !add){
              axis(side = 1)
              mtext(text=varnames[i], side = 1, line=3)
              mtext(text=varnames[i], side = 2, line=3)
            }
        } 
      }}  
  mtext(text="lag distance", side=1, outer = TRUE, line=0)
  mtext(text=c("semivariogram","covariance")[cov+1], side=2, outer = TRUE, line=2)
  invisible(opar)
}




#' Check for anisotropy of a theoretical variogram
#' 
#' Checks for anisotropy of a theoretical variogram or covariance function model

#' @param x variogram or covariance model object
#' @param tol tolerance for 
#' @param ... extra arguments for generic  functionality
#'
#' @return Generic function. Returns of boolean answering the question of the name,
#' or NA if object \code{x} does not contain a known theoretical variogram
#' @export
is.isotropic <- function(x, tol=1e-10, ...){ UseMethod("is.isotropic", x) }

#' @method is.isotropic default
#' @export
is.isotropic.default = function(x, tol=1e-10, ...) NA

#' @method is.isotropic gmCgram
#' @export
is.isotropic.gmCgram = function(x, tol=1e-10, ...){
   all(apply(x$M, 1, function(y){
     ev = eigen(y, only.values=TRUE)[[1]]
     all(abs(ev-ev[1])<tol)
   }))
}

#' @method is.isotropic variogramModel
#' @export
is.isotropic.variogramModel = function(x, tol=1e-10, ...){
  anis = x[,grep("anis", colnames(x))]
  all(apply(anis, 2, function(y) all(abs(y-y[1])<tol) ) )
}

#' @method is.isotropic variogramModelList
#' @export
is.isotropic.variogramModelList = function(x, tol=1e-10, ...) is.isotropic(x[[1]], tol=tol)
#' @method is.isotropic LMCAnisCompo
#' @export
is.isotropic.LMCAnisCompo = function(x, tol=1e-10, ...){
  all(sapply(x["A",], 1, function(y){
    ev = eigen(y$A, only.values=TRUE)[[1]]
    all(abs(ev-ev[1])<tol)
  }))
}









#### empirical variogram ---------------------




#' Variogram method for gmSpatialModel objects
#' 
#' Compute the empirical variogram of the conditioning data contained in a [gmSpatialModel-class] object
#'
#' @param object a gmSpatialModel object containing spatial data.
#' @param methodPars (currently ignored)
#' @param ... further parameters to [gstat::variogram()]
#'
#' @return Currently the function is just a convenience wrapper on
#' the variogram calculation functionalities of package "gstat", 
#' and returns objects of class "\code{gstatVariogram}". Check the
#' help of \code{gstat::variogram} for further information.
#' In the near future, methods will be created, which will depend on
#' the properties of the two arguments provided,  \code{object} and
#' \code{methodPars}.
#' @export
#' @importFrom gstat variogram
variogram_gmSpatialModel <-  function(object, methodPars=NULL, ...){
  if(!is.null(methodPars)) stop("use 'variogram' with named parameters only")
  gstat::variogram(as.gstat(object), ...)
}


# Variogram calculations
# 
# Compute empricial variograms out of a spatial data object
# 
# @param object spatial data container
# @param ... further parameters for variogram calculation
# 
# @return depending on the input data, different kinds of empirical variograms 
# will be produced. See appropriate method descriptions.
# 
# @importFrom sp variogram
# @export
##variogram <- function(object, ...) UseMethod("variogram", object)

# @describeIn variogram
# @method variogram default
# @export
#variogram.default <- function(object, ...){
#  return(variogram_gmSpatialModel(object, ...))
#}




#' Empirical variogram or covariance function in 2D
#' 
#' compute the empirical variogram or covariance function in a 2D case study 
#' 
#' @param X matrix of Nx2 columns with the geographic coordinates
#' @param Z matrix or data.frame of data with dimension (N,Dv)
#' @param Ff for variogram, matrix of basis functions with nrow(Ff)=N (can be a N-vector of 1s); 
#' for covariance function, a (N,Dv)-matrix or a  Dv-vector giving the mean values
#' @param maxdist maximum lag distance to consider
#' @param lagNr number of lags to consider
#' @param lags if maxdist and lagNr are not specified, either: (a) a matrix of 2 columns giving 
#' minimal and maximal lag distance defining the lag classes to consider, or (b) a vector of lag breaks 
#' @param azimuthNr number of azimuths to consider
#' @param azimuths if azimuthNr is not specified, either: (a) a matrix of 2 columns giving 
#' minimal and maximal azimuth defining the azimuth classes to consider, or (b) a vector of azimuth breaks
#' @param maxbreadth maximal breadth (in lag units) orthogonal to the lag direction
#' @param minpairs minimal number of pairs falling in each class to consider the calculation sufficient; defaults to 10
#' @param cov boolean, is covariance (TRUE) or variogram (FALSE) desired? defaults to variogram
#'
#' @return An empirical variogram for the provided data. NOTE: avoid using directly gsi.* functions! They 
#' represent either internal functions, or preliminary, not fully-tested functions. Use \code{\link{variogram}} instead.
#' @export
#' @family gmEVario functions
#'
#' @examples
#' library(gstat)
#' data("jura", package = "gstat")
#' X = as.matrix(jura.pred[,1:2])
#' Z = as.matrix(jura.pred[,c("Zn","Cd","Pb")])
#' vge = gsi.EVario2D(X,Z)
#' dim(vge)
#' dimnames(vge)
#' class(vge["gamma",1])
#' dim(vge["gamma",1][[1]])
#' vge["npairs",1]
#' vge["lags",1]
gsi.EVario2D = function(X,Z,Ff=rep(1, nrow(X)),
                      maxdist= max(dist(X[sample(nrow(X),min(nrow(X),1000)),]))/2, 
                      lagNr = 15, lags = seq(from=0, to=maxdist, length.out=lagNr+1),
                      azimuthNr=4, azimuths = seq(from=0, to=180, length.out=azimuthNr+1)[1:azimuthNr],
                      maxbreadth=Inf, minpairs=10, cov=FALSE){

  # dimensions
  N = nrow(X)
  Dv = ncol(Z)
  Dg = ncol(X)
  stopifnot(N==nrow(Z))
  if(length(dim(Ff))==0){
    stopifnot(N==length(Ff))
  }else{
    stopifnot(N==nrow(Ff))
  }
  # expand the information given into a set of columns stating conditions
  if(length(dim(lags))==0){
    lags = data.frame(minlag=lags[-length(lags)], maxlag=lags[-1]) 
    if(maxbreadth!=Inf) lags[,"maxbreadth"]=maxbreadth
  }else if(dim(lags)==2){
    lags = data.frame(lags)
    colnames(lags) = c("minlag","maxlag","maxbreadth")[1:ncol(lags)]
  }else stop("lags can be either a vector of lags or a data.frame, see ?gsi.EVario2D")

  if(length(dim(azimuths))==0){
    tol = (azimuths[2]-azimuths[1])/2
    if(is.na(tol)) tol=180
    azimuths = data.frame(minaz=azimuths-tol, maxaz=azimuths+tol) 
  }else if(dim(azimuths)==2){
    azimuths = data.frame(azimuths)
    colnames(azimuths) = c("minaz","maxaz")
  }else stop("azimuths can be either a vector of lags or a data.frame, see ?gsi.EVario2D")
  
  # compute residuals
  if(cov){ 
    kk = 1
    if(dim(Z)==dim(Ff)){
      Z = Z-Ff
    }else if(ncol(Z)==length(c(unlist(Ff)))){
      Z = sweep(Z, 2, Ff, "-")
    }
  }else{
    kk = 1 # 2 # we consider each pair only once
    Z = lm(as.matrix(Z)~as.matrix(Ff)+0)$residuals ## ideally this should be a GLS fit
  }
  # compute pairs
  ij = expand.grid(1:nrow(X), 1:nrow(X))# indices
  op = ifelse(cov, "*", "-")
  ZZ = outer(as.matrix(Z),as.matrix(Z), op) # variables
  ZZ = aperm(ZZ, c(1,3,2,4))
  dim(ZZ) = c(N*N, Dv, Dv)
  XX = X[ij[,1],]-X[ij[,2],] # locations
  XXabs = gmApply(XX, 1, function(x) sqrt(sum(x^2))) 
  XXaz = gmApply(XX, 1, function(x) pi/2-atan2(x[2],x[1])) +2*pi
  XXaz = XXaz %% pi  # residual to 180°
  
  # output

  ## ATTENTION: needs to be changed to return a structure (3,Na)-matrix of objects,
  #     like logratioVariogramAnisotropy
  Nh = nrow(lags)
  Na = nrow(azimuths)
  vg = array(0, dim=c(Nh, Dv, Dv, Na))
  n = array(0, dim=c(Nh, Na))
  azs = azimuths * pi/180
  res = sapply(1:Na, function(i){
    tk_a = (azs[i,1]<=XXaz) & (azs[i,2]>=XXaz)
    zz = ZZ[tk_a,,]
    xxabs = XXabs[tk_a]
    xxaz = XXaz[tk_a]
    tk_h = outer(xxabs, lags[,1],">=") & outer(xxabs, lags[,2],"<=")
    if(ncol(lags)>2){
      tk_b = outer(xxabs * abs(sin((xxaz-(azs[i,2]-azs[i,1])))), lags[,3], "<=")
      tk_h = tk_h & tk_b
    } 
    n[,i] = colSums(tk_h)
    for(j in 1:Nh){
      if(n[j,i]>minpairs){
        vg[j,,,i] = gmApply(zz[tk_h[,j],,], c(2,3),"sum")/(kk*n[j,i])
      }else{
        vg[j,,,i]=NA
      }
    }
    return(list(gamma=vg[,,,i], lags=gsi.lagClass(lags), npairs =n[,i]))
  })
  
  # output
  attr(res, "directions") = gsi.azimuthInterval(azimuths)
  # attr(res, "lags") = gsi.lagClass(lags)
  attr(res, "type") = ifelse(cov, "covariance","semivariogram")
  class(res) = "gmEVario"
  return(res)
}




#' Plot empirical variograms
#' 
#' Flexible plot of an empirical variogram of class gmEVario
#' 
#' @param x object to print, of class gmEVario
#' @param xlim.up range of X values to be used for the diagrams of the upper triangle
#' @param xlim.lo range of X values to be used for the diagrams of the lower triangle
#' @param vdir.up in case of anisotropic variograms, indices of the directions to be plotted 
#' on the upper triangle
#' @param vdir.lo ..., indices of the directions to be plotted on the lower triangle
#' @param varnames variable names to be used
#' @param type  string, controlling whether to plot lines, points, etc (see \code{\link{plot}})
#' @param add boolean, add stuff to an existing diagram?
#' @param commonAxis boolean, should vertical axes be shared by all plots in a row?
#' @param cov boolean, is this a covariance? (if FALSE, it is a variogram)
#' @param closeplot logical, should the plot be left open (FALSE) for further changes, or be frozen (TRUE)? 
#' defaults to TRUE
#' @param ... further parameters to \code{\link{matplot}}
#'
#' @return invisibly, the graphical parameters active before calling the function. 
#' This is useful for freezing the plot if you provided `closeplot=FALSE`.
#' 
#' How to use arguments `vdir.lo` and `vdir.up`? Each empirical variogram \code{x} has been 
#' computed along certain distances, recorded in its attributes and retrievable with command
#' \code{\link{ndirections}}.   
#' @export
#' @family gmEVario functions
#' @method plot gmEVario
#'
#' @examples
#' library(gstat)
#' data("jura", package = "gstat")
#' X = as.matrix(jura.pred[,1:2])
#' Z = as.matrix(jura.pred[,c("Zn","Cd","Pb")])
#' vge = gsi.EVario2D(X,Z)
#' plot(vge)
#' plot(vge, pch=22, lty=1, bg="grey")
plot.gmEVario = function(x, xlim.up=NULL, xlim.lo=NULL, vdir.up= NULL, vdir.lo= NULL, 
                         varnames = dimnames(x$gamma)[[2]], type="o", 
                         add=FALSE, commonAxis=TRUE, cov =attr(x,"type")=="covariance",
                         closeplot=TRUE, ...){
  Dv = dim(x[1,1][[1]])[2]
  if(is.null(varnames)) varnames = paste("v", 1:Dv, sep="")
  if(is.null(vdir.up)&is.null(vdir.lo)) vdir.lo <- 1:ndirections(x)
  if( any(c(vdir.up, vdir.lo)>ndirections(x))){
    stop("indicated directions (vdir.up or vdir.lo) do not exist in x")
  }
  if(is.null(xlim.up)){
    if(add){
      par(mfg=c(1,2))
      xlim.up = par()$usr[1:2]
    }else if(!is.null(vdir.up)){
      maxdist = max(sapply(x["lags",], gsi.midValues.lagClass ) )
      xlim.up = c(0, maxdist)
    }
  }
  if(is.null(xlim.lo)){
    if(add){
      par(mfg=c(2,1))
      xlim.lo = par()$usr[1:2]
    }else if(!is.null(vdir.lo)){
      maxdist = max(sapply(x["lags",], gsi.midValues.lagClass ) )
      xlim.lo = c(0, maxdist)
    }
  }

  opar = par()
  opar = par_remove_readonly(opar)
  
  if(closeplot) on.exit(par(opar))
  
  myplot = function(...) matplot(type=type, ylab="", xlab="",xaxt="n", ...)
  if(add) myplot = function(...) matpoints(type=type, ...)
  if(!add){
    myplot(c(0,0), c(0,0), pch="", ann=FALSE, bty="n", yaxt="n")
    par(mfrow=c(Dv+1,Dv+1), mar=c(2,3,0,0), oma=c(1,4,1,1), xpd=NA)
  }
  for(i in 1:Dv){
    for(j in 1:Dv){
      if((i>=j)&(!is.null(vdir.lo))){
        par(mfg=c(i+1,j,Dv+1,Dv+1))
        ylim = range(sapply(vdir.lo, function(kk) x["gamma",kk][[1]][,i,j]))
        if(commonAxis) ylim=range(sapply(vdir.lo, function(kk) x["gamma",kk][[1]][,,j]))
        myplot(
          sapply(vdir.lo, function(kk) gsi.midValues.lagClass(x["lags",kk][[1]])), 
          sapply(vdir.lo, function(kk) x["gamma",kk][[1]][,i,j]),  ylim=ylim, ...)
        if(i==j){
          axis(side = 3)
          mtext(text=varnames[i], side = 3, line=3)
          mtext(text=varnames[i], side = 4, line=3)
        }
      }
      if((i<=j)&(!is.null(vdir.up))){
        par(mfg=c(i,j+1,Dv+1,Dv+1))
        ylim = range(sapply(vdir.up, function(kk) x["gamma",kk][[1]][,i,j]))
        if(commonAxis) ylim=range(sapply(vdir.up, function(kk) x["gamma",kk][[1]][,,j]))
        myplot(
          sapply(vdir.up, function(kk) gsi.midValues.lagClass(x["lags",kk][[1]])), 
          sapply(vdir.up, function(kk) x["gamma",kk][[1]][,i,j]),  ylim=ylim, ...)
        if(i==j){
          axis(side = 1)
          mtext(text=varnames[i], side = 1, line=3)
          mtext(text=varnames[i], side = 2, line=3)
        }
      } 
    }}  
  mtext(text="lag distance", side=1, outer = TRUE, line=0)
  mtext(text=c("semivariogram","covariance")[cov+1], side=2, outer = TRUE, line=2)
  attr(opar, "vdir.up")=vdir.up
  attr(opar, "vdir.lo")=vdir.lo
  invisible(opar)
}






#' Convert empirical structural function to gmEVario format 
#' 
#' Convert empirical covariance functions or variograms to the format gmEVario 
#' of package gmGeostats
#' 
#' @param vgemp variogram/covariance function to be converted
#' @param ... further parameters
#'
#' @return the empirical covariance function or variogram, recasted to class 
#' \code{gmEVario}. This is a generic function. Methods exist for objects of 
#' class \code{logratioVariogram}\code{logratioVariogramAnisotropy} 
#' (for compositional data) and \code{gstatVariogram}
#' (from package \code{gstat}).  
#' @export
#' @aliases as.gmEVario.gstatVariogram as.gmEVario.logratioVariogram
#'  as.gmEVario.logratioVariogramAnisotropy
#'
#' @family gmEVario functions
as.gmEVario  <- function(vgemp,...){ UseMethod("as.gmEVario",vgemp)}
as.gmEVario.default  <- function(vgemp,...) vgemp




#' @describeIn variogramModelPlot.gmEVario Quick plotting of empirical and theoretical variograms
#' @export
variogramModelPlot <- function(vg, ...)  UseMethod("variogramModelPlot", vg)

#' Quick plotting of empirical and theoretical variograms
#' Quick and dirty plotting of empirical variograms/covariances with or without their models 
#' @param vg empirical variogram or covariance function
#' @param model optional, theoretical variogram or covariance function
#' @param col colors to use for the several directional variograms
#' @param commonAxis boolean, should all plots in a row share the same vertical axis?
#' @param newfig boolean, should a new figure be created? otherwise user should ensure the device space is appropriately managed  
#' @param closeplot logical, should the plot be left open (FALSE) for further changes, or be frozen (TRUE)? 
#' defaults to TRUE
#' @param ... further parameters to underlying plot or matplot functions
#'
#' @return The function is primarily called for producing a plot. However, it 
#' invisibly returns the graphical parameters active before the call 
#' occurred. This is useful for constructing complex diagrams, by adding layers 
#' of info. If you want to "freeze" your plot, embed your call in another
#' call to \code{\link{par}}, e.g. \code{par(variogramModelPlot(...))}; if you
#' want to leave the plot open for further changes give the extra argument `closeplot=FALSE`.
#' @export
#' @family variogramModelPlot 
#' @family gmEVario functions 
#' @family gmCgram functions 
#' @seealso [logratioVariogram()]
#' @method variogramModelPlot gmEVario
#'
#' @examples
#' utils::data("variogramModels")
#' v1 = setCgram(type=vg.Gau, sill=diag(3)+0.5, anisRanges = 5e-1*diag(c(3,0.5)))
#' v2 = setCgram(type=vg.Exp, sill=0.3*diag(3), anisRanges = 5e-2*diag(2))
#' vm = v1+v2
#' plot(vm, closeplot=TRUE)
#' library(gstat)
#' data("jura", package = "gstat")
#' X = as.matrix(jura.pred[,1:2])
#' Z = as.matrix(jura.pred[,c("Zn","Cd","Pb")])
#' vge = gsi.EVario2D(X,Z)
#' variogramModelPlot(vge, vm)
#' 
#' 
variogramModelPlot.gmEVario <- function(vg, model = NULL,   # gstat  or variogramModelList object containing a variogram model fitted to vg
                                        col = rev(rainbow(ndirections(vg))),
                                        commonAxis = FALSE,
                                        newfig = TRUE, closeplot=TRUE, ...){
  opar = plot(vg, commonAxis=commonAxis, add=!newfig, col=col, closeplot=is.null(model), ...)
  opar = par_remove_readonly(opar)
  
  if(closeplot) on.exit(par(opar))
  vdir.lo = attr(opar, "vdir.lo")
  vdir.up = attr(opar, "vdir.up")
  if(is.null(model))
    return(invisible(opar))
  # OTHERWISE: add the curves for the model
  aux = as.directorVector(attr(vg, "directions"))
  if(!is.null(vdir.lo)) vdir.lo = aux[vdir.lo,] 
  if(!is.null(vdir.up)) vdir.up = aux[vdir.up,] 
  opar = plot(as.gmCgram(model), vdir.up= vdir.up, vdir.lo= vdir.lo, add=TRUE, cov =FALSE, ...)
  #f = as.function(as.gmCgram(gg))
  #Dv = dim(vg$gamma)[2]
  #dirs = as.directorVector(attr(vg, "directions"))
  #Dg = ncol(dirs)
  #for(i in 1:Dv){
  #  for(j in 1:Dv){
  #    if((i>=j)&(!is.null(vdir.lo))){
  #      par(mfg=c(i+1,j,Dv+1,Dv+1))
  #      dirs.lo = dirs[vdir.lo,, drop=F]
  #      xlim = par()$usr[1:2]
  #      hd = seq(from=xlim[1], to=xlim[2], length.out = 200)
  #      Y = outer(hd, 1:nrow(dirs.lo), function(h,k){
  #        f(rep(0,Dg), dirs.lo[k,]*h)[i,j]
  #      }) 
  #      matlines(hd, Y, col=col[vdir.lo], ...)
  #    }
  #    if((i<=j)&(!is.null(vdir.up))){
  #      par(mfg=c(i,j+1,Dv+1,Dv+1))
   #     dirs.up = dirs[vdir.up,, drop=F]
  #      xlim = par()$usr[1:2]
  #      hd = seq(from=xlim[1], to=xlim[2], length.out = 200)
  #      Y = outer(hd, 1:nrow(dirs.up), function(h,k){
  #        f(rep(0,Dg), dirs.up[k,]*h)[i,j]
  #      }) 
  #      matlines(hd, Y, col=col[vdir.up], ...)
  #    } 
  #  }}  
  invisible(opar)
} 



#' Number of directions of an empirical variogram
#' 
#' Returns the number of directions at which an empirical variogram was computed
#' 
#' @param x empirical variogram object
#'
#' @return Generic function. It provides the 
#' number of directions at which an empirical variogram was computed
#' @export
#' @family gmEVario functions 
#' @family gmCgram functions 
#' @seealso [logratioVariogram()], [gstat::variogram()]
ndirections <- function(x){ UseMethod("ndirections", x) }

#' @describeIn ndirections generic method
#' @method ndirections default
#' @export
ndirections.default = function(x) nrow(x)
#' @describeIn ndirections method for objects of class "azimuth" (vectors of single angles)
#' @method ndirections azimuth
#' @export
ndirections.azimuth = function(x) length(x)
#' @describeIn ndirections method for objects of class "azimuthInterval" (data.frames of intervals for angles)
#' @method ndirections azimuthInterval
#' @export
ndirections.azimuthInterval = function(x) length(x[[1]])
#' @describeIn ndirections method for empirical logratio variograms with anisotropy 
#' @method ndirections logratioVariogramAnisotropy
#' @export
ndirections.logratioVariogramAnisotropy = function(x) ndirections(attr(x,"directions"))
#' @describeIn ndirections method for empirical logratio variograms without anisotropy
#' @method ndirections logratioVariogram
#' @export
ndirections.logratioVariogram = function(x) 1
#' @describeIn ndirections method for empirical gmGeostats variograms
#' @method ndirections gmEVario
#' @export
ndirections.gmEVario = function(x) ndirections(attr(x,"directions"))
#' @describeIn ndirections method for empirical gstat variograms
#' @method ndirections gstatVariogram
#' @export
ndirections.gstatVariogram = function(x){
  length(unique(paste(x$dir.hor, x$dir.ver)))
} 





#### internal functions -------------

gsi.azimuth = function(x){
   class(x) = c("azimuth","directionClass")
   return(x)
} 

gsi.azimuthInterval = function(x){
  class(x) = c("azimuthInterval","directionClass")
  return(x)
} 



gsi.directorVector = function(x){
  if(length(dim(x))!=2) stop("provided director vectors are not a matrix!")
  class(x) = c("directorVector", "directionClass")
  return(x)
}

print.directionClass = function(x, complete=TRUE, ...){
  cat(paste("with",nrow(as.directorVector(x)),"directions\n"))
  if(complete){
    print(unclass(x), ...)
  }
} 




as.directorVector <- function(x){ UseMethod("as.directorVector",x) }

#' @method as.directorVector default 
as.directorVector.default = function(x,...) x

#' @method as.directorVector azimuth
as.directorVector.azimuth = function(x, D=2){
  res = cbind(cos(pi/2-x), sin(pi/2-x))
  if(D>2){
    res = cbind(res, matrix(0, ncol=D-2, nrow=nrow(res)))
  }
  colnames(res) = paste("v", 1:ncol(res), sep="")
  return(gsi.directorVector(res))
}

#' @method as.directorVector azimuthInterval
as.directorVector.azimuthInterval = function(x, D=2){
  res = (x[[1]]+x[[2]])/2
  return(as.directorVector.azimuth(res))
}


gsi.lagdists = function(x){
  class(x) = c("lagdist","lagClass")
  return(x)
} 

gsi.lagClass = function(x){
  if(length(dim(x))!=2) stop("provided lag classes are not matrix-like!")
  class(x) = c("lagClass")
  return(x)
}

#' @method print lagClass
print.lagClass = function(x,...){
  if(is.list(x)){
    print(as.data.frame(unclass(x)), ...)
  }else{
    print(unclass(x), ...)
  }
}

gsi.midValues <- function(x) UseMethod("gsi.midValues",x)
#' @method gsi.midValues default
gsi.midValues.default = function(x) x

#' @method gsi.midValues lagClass
gsi.midValues.lagClass <- function(x){ (x[[1]]+x[[2]])/2 }
  
#' @method gsi.midValues azimuthIntervals
gsi.midValues.azimuthInterval <- function(x){ (x[[1]]+x[[2]])/2 }





## theoretical structural functions
# S3 -> S4 classes  
# cat("creating variogram model classes\n")


# abstract classes
#' @title Structural function model specification
#' @description Abstract class, containing any specification of a variogram (or covariance) model. 
#' Members must implement a coercion method to
#' class "gmCgram" (see [setCgram()] for an example), and (possibly) coercion to 
#' class "variogramModel" or "variogramModelList" (see [gstat::vgm()])
#' @export
#' @include compositionsCompatibility.R
#' @include gstatCompatibility.R
#' @include preparations.R
setClassUnion(name="ModelStructuralFunctionSpecification", 
              members=c("NULL","gmCgram", "LMCAnisCompo", "variogramModelList", "variogramModel"))



# 
# #### container class --------------
# # An S4 class to represent a Gaussian random field specification
# #
# # @slot structure ModelStructuralFunctionSpecification. Variogram or 
# # (generalised) covariance function specification, typically an object
# # obtained from a call to functions such as \code{\link{setCgram}},
# # \code{\link{LMCAnisCompo}} or \code{gstat::vgm}.
# # @slot formula formula specifying the structure
# # of dependence of the mean of the random field w.r.to spatial coordinates 
# # and/or covariables; typically it will have no left-hand-side term; 
# # @slot beta numeric, a vector with as many coefficients as terms the formula
# # above requires for a full specification of the trend; if unknown, these can
# # be NAs, as many as needed. 
# #
# # @return A object with the slots populated as given
# # @export
# # @seealso [gmSpatialModel-class], and the `make.gm*` functions referenced there
# setClass("gmGaussianModel", 
#          slots = list(structure = "ModelStructuralFunctionSpecification",
#                       formula="formula",
#                       beta = "structure")
# )
# 
# #setMethod("initialize", signature="gmGaussianModel",
# #          def=function(.Object, structure, formula, beta){
# #            .Object@formula = formula
# #            .Object@beta = beta
# #            if(!is.null(structure)) .Object@structure = structure
# #            return(.Object)
# #          }
# #)


#### container class --------------
# An S4 class to represent a Gaussian random field specification
#
# @slot structure ModelStructuralFunctionSpecification. Variogram or 
# (generalised) covariance function specification, typically an object
# obtained from a call to functions such as \code{\link{setCgram}},
# \code{\link{LMCAnisCompo}} or \code{gstat::vgm}.
# @slot formula formula specifying the structure
# of dependence of the mean of the random field w.r.to spatial coordinates 
# and/or covariables; typically it will have no left-hand-side term; 
# @slot beta numeric, a vector with as many coefficients as terms the formula
# above requires for a full specification of the trend; if unknown, these can
# be NAs, as many as needed. 
#
# @return A object with the slots populated as given
# @export
# @seealso [gmSpatialModel-class], and the `make.gm*` functions referenced there
setClass("gmGaussianModel", 
         slots = list(structure = "ModelStructuralFunctionSpecification",
                      formula="formula",
                      beta = "numeric")
)


## empirical structural functions
# S3 -> S4 classes
# cat("creating empirical variogram classes\n")


# abstract classes
#' @title Empirical structural function specification
#' @description Abstract class, containing any specification of an empirical variogram 
#' (or covariance function, or variations). Members must implement a coercion method to
#' class "gmEVario" (see [gsi.EVario2D()] for an example), and (possibly) coercion to 
#' class "gstatVariogram" (see [gstat::variogram()])
#' @export
#' @include compositionsCompatibility.R
#' @include gstatCompatibility.R
#' @include preparations.R
setClassUnion(name="EmpiricalStructuralFunctionSpecification", members=c("NULL","gmEVario", "logratioVariogram", "logratioVariogramAnisotropy", "gstatVariogram"))