Commit 01a16846 authored by Dorchies David's avatar Dorchies David
Browse files

ci: debug R CMD check

Refs #6
Showing with 126 additions and 200 deletions
+126 -200
^griwrm\.Rproj$ ^griwrm\.Rproj$
^\.Rproj\.user$ ^\.Rproj\.user$
^LICENSE\.md$ ^LICENSE\.md$
\.gitlab-ci\.yml
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
# Man pages generated by Roxygen # Man pages generated by Roxygen
man/*.Rd man/*.Rd
/.vscode/
############################################################################### ###############################################################################
...@@ -52,3 +53,4 @@ vignettes/*.pdf ...@@ -52,3 +53,4 @@ vignettes/*.pdf
# pkgdown site # pkgdown site
docs/ docs/
inst/doc
...@@ -2,7 +2,7 @@ default: ...@@ -2,7 +2,7 @@ default:
tags: [docker] tags: [docker]
image: r-base:latest image: r-base:latest
before_script: before_script:
- Rscript -e 'install.packages("remotes")' - Rscript -e 'install.packages("remotes", "dplyr")'
- Rscript -e 'remotes::install_gitlab("HYCAR-Hydro/airgr@sd", host = "gitlab.irstea.fr")' - Rscript -e 'remotes::install_gitlab("HYCAR-Hydro/airgr@sd", host = "gitlab.irstea.fr")'
- R CMD build ../griwrm - R CMD build ../griwrm
......
...@@ -8,7 +8,7 @@ Authors@R: ...@@ -8,7 +8,7 @@ Authors@R:
email = "david.dorchies@inrae.fr", email = "david.dorchies@inrae.fr",
comment = c(ORCID = "0000-0002-6595-7984")) comment = c(ORCID = "0000-0002-6595-7984"))
Description: This R package aims to model water basin with a airGR based semi-distributive hydrology with the integration of human infrastructures and their management. Description: This R package aims to model water basin with a airGR based semi-distributive hydrology with the integration of human infrastructures and their management.
License: LGPL (>= 2.1) License: AGPL-3
Encoding: UTF-8 Encoding: UTF-8
LazyData: true LazyData: true
Roxygen: list(markdown = TRUE) Roxygen: list(markdown = TRUE)
...@@ -16,8 +16,11 @@ RoxygenNote: 7.1.0 ...@@ -16,8 +16,11 @@ RoxygenNote: 7.1.0
Imports: Imports:
dplyr, dplyr,
utils, utils,
airGR (>= 1.6.1.11) airGR (>= 1.6.1.11),
Suggests: methods
Suggests:
knitr, knitr,
rmarkdown rmarkdown,
lattice
VignetteBuilder: knitr VignetteBuilder: knitr
URL: https://gitlab.irstea.fr/in-wop/griwrm
#' Create InputsModel object for a GRIWRM network #' Create InputsModel object for a GRIWRM network
#' #'
#' @param ginet #' @param x Ginet object describing the diagram of the semi-distributed model, see \code{[Ginet]}.
#' @param girop #' @param girop Girop object giving the run-off model parameters, see \code{[Girop]}.
#' @param gits #' @param gits Gits object giving the observation time series, see \code{[Gits]}.
#' @param ... further arguments passed to \code{\link[airGR]{CreateInputsModel}}.
#' #'
#' @return #' @return GriwrmInputsModel object equivalent to airGR InputsModel object for a semi-distributed model (See \code{\link[airGR]{CreateInputsModel}})
#' @export #' @export
#' CreateInputsModel.Griwrm <- function(x, girop, gits, ...) {
#' @examples
CreateInputsModel.Griwrm <- function(ginet, girop, gits, verbose = TRUE) {
InputsModel <- CreateEmptyGriwrmInputsModel() InputsModel <- CreateEmptyGriwrmInputsModel()
for(id in getNodeRanking(ginet)) { for(id in getNodeRanking(x)) {
if(verbose) cat("CreateInputsModel.griwrm: Treating sub-basin", id, "...\n") if(verbose) cat("CreateInputsModel.griwrm: Treating sub-basin", id, "...\n")
InputsModel[[id]] <- CreateOneGriwrmInputsModel(id, ginet, girop, gits) InputsModel[[id]] <- CreateOneGriwrmInputsModel(id, x, girop, gits, ...)
} }
return(InputsModel) return(InputsModel)
} }
...@@ -22,9 +21,7 @@ CreateInputsModel.Griwrm <- function(ginet, girop, gits, verbose = TRUE) { ...@@ -22,9 +21,7 @@ CreateInputsModel.Griwrm <- function(ginet, girop, gits, verbose = TRUE) {
#' Create an empty InputsModel object for GRIWRM nodes #' Create an empty InputsModel object for GRIWRM nodes
#' #'
#' @return #' @return \emph{GriwrmInputsModel} empty object
#'
#' @examples
CreateEmptyGriwrmInputsModel <- function() { CreateEmptyGriwrmInputsModel <- function() {
InputsModel <- list() InputsModel <- list()
class(InputsModel) <- append(class(InputsModel), "GriwrmInputsModel") class(InputsModel) <- append(class(InputsModel), "GriwrmInputsModel")
...@@ -34,14 +31,12 @@ CreateEmptyGriwrmInputsModel <- function() { ...@@ -34,14 +31,12 @@ CreateEmptyGriwrmInputsModel <- function() {
#' Create one InputsModel for a GRIWRM node #' Create one InputsModel for a GRIWRM node
#' #'
#' @param ginet #' @param id string of the node identifier
#' @param girop #' @param ginet See \code{[Ginet]}.
#' @param gits #' @param girop See \code{[Girop]}.
#' @param id #' @param gits See \code{[Gits]}.
#'
#' @return
#' #'
#' @examples #' @return \emph{InputsModel} object for one.
CreateOneGriwrmInputsModel <- function(id, ginet, girop, gits) { CreateOneGriwrmInputsModel <- function(id, ginet, girop, gits) {
node <- ginet[ginet$id == id,] node <- ginet[ginet$id == id,]
FUN_MOD <- girop$model[girop$id == id] FUN_MOD <- girop$model[girop$id == id]
......
#' Create InputsModel object for either airGR or GRIWRM #' Create InputsModel object for either airGR or GR-IWRM
#' #'
#' @param x #' @param x First parameter determining which InputsModel object is created
#' @param ... #' @param ... further arguments passed to or from other methods.
#' #'
#' @return #' @return InputsModel or GriwrmInputsObject object
#' @export #' @export
#'
#' @examples
CreateInputsModel <- function(x, ...) { CreateInputsModel <- function(x, ...) {
UseMethod("CreateInputsModel", x) UseMethod("CreateInputsModel", x)
} }
#' Wrapper for the airGR::CreateInputsModel function #' Wrapper for the airGR::CreateInputsModel function
#' #'
#' @param FUN_MOD #' @param x hydrological model function (e.g. \code{\link[airGR]{RunModel_GR4J}}, \code{\link[airGR]{RunModel_CemaNeigeGR4J}})
#' @param DatesR #' @param ... further arguments passed to \code{\link[airGR]{CreateInputsModel}}.
#' @param Precip
#' @param PrecipScale
#' @param PotEvap
#' @param TempMean
#' @param TempMin
#' @param TempMax
#' @param ZInputs
#' @param HypsoData
#' @param NLayers
#' @param QobsUpstr
#' @param LengthHydro
#' @param BasinAreas
#' @param verbose
#' #'
#' @return #' @return object of class \emph{InputsModel}, see \code{\link[airGR]{CreateInputsModel}} for details.
#' @import airGR #' @import airGR
#' @export #' @export
#' @seealso The original function in airGR package: \code{\link[airGR]{CreateInputsModel}}.
#' #'
#' @examples CreateInputsModel.default <- function(x,
CreateInputsModel.default <- function(FUN_MOD, ...) {
DatesR,
Precip, PrecipScale = TRUE, airGR::CreateInputsModel(FUN_MOD = x, ...)
PotEvap = NULL,
TempMean = NULL, TempMin = NULL, TempMax = NULL,
ZInputs = NULL, HypsoData = NULL, NLayers = 5,
QobsUpstr = NULL, LengthHydro = NULL, BasinAreas = NULL,
verbose = TRUE) {
airGR::CreateInputsModel(FUN_MOD, DatesR, Precip, PrecipScale, PotEvap,
TempMean, TempMin, TempMax, ZInputs, HypsoData, NLayers,
QobsUpstr, LengthHydro, BasinAreas, verbose)
} }
#' Title #' Create \emph{GriwrmRunOptions} object for running and calibrating model in GR-IWRM.
#' #'
#' @param InputsModel #' @param InputsModel object of class \emph{GriwrmInputsModel}, see \code{\link{CreateInputsModel.Griwrm}} for details.
#' @param IndPeriod_WarmUp #' @param ... further arguments passed to \code{\link[airGR]{CreateOptions}}.
#' @param IndPeriod_Run
#' @param IniStates
#' @param IniResLevels
#' @param Imax
#' @param Outputs_Cal
#' @param Outputs_Sim
#' @param MeanAnSolidPrecip
#' @param IsHyst
#' @param warnings
#' @param verbose
#' #'
#' @return #' @return \emph{GriwrmRunOptions} object for running and calibrating model in GR-IWRM.
#' @export #' @export
#' CreateRunOptions.GriwrmInputsModel <- function(InputsModels, ...) {
#' @examples
CreateRunOptions.GriwrmInputsModel <- function(InputsModel, IndPeriod_WarmUp = NULL, IndPeriod_Run,
IniStates = NULL, IniResLevels = NULL, Imax = NULL,
Outputs_Cal = NULL, Outputs_Sim = "all",
MeanAnSolidPrecip = NULL, IsHyst = FALSE,
warnings = TRUE, verbose = TRUE) {
RunOptions <- list() RunOptions <- list()
class(RunOptions) <- append(class(RunOptions), "GriwrmRunOptions") class(RunOptions) <- append(class(RunOptions), "GriwrmRunOptions")
for(InputsModelBasin in InputsModel) {
RunOptions[[InputsModelBasin$id]] <- CreateRunOptions( for(InputsModelBasin in InputsModels) {
InputsModel = InputsModelBasin, RunOptions[[InputsModelBasin$id]] <- CreateRunOptions(InputsModel = InputsModelBasin, ...)
IndPeriod_WarmUp = IndPeriod_WarmUp,
IndPeriod_Run = IndPeriod_Run,
IniStates = IniStates,
IniResLevels = IniResLevels,
Imax = Imax,
Outputs_Cal = Outputs_Cal,
Outputs_Sim = Outputs_Sim,
MeanAnSolidPrecip = MeanAnSolidPrecip,
IsHyst = IsHyst,
warnings = warnings,
verbose = verbose
)
} }
return(RunOptions) return(RunOptions)
} }
#' Title #' Create \emph{RunOptions} object for airGR. See \code{\link[airGR]{CreateOptions}}.
#' #'
#' @param FUN_MOD #' @param InputsModel object of class \emph{InputsModel}, see \code{\link[airGR]{CreateInputsModel}} for details.
#' @param InputsModel #' @param ... further arguments passed to \code{\link[airGR]{CreateOptions}}.
#' @param IndPeriod_WarmUp
#' @param IndPeriod_Run
#' @param IniStates
#' @param IniResLevels
#' @param Imax
#' @param Outputs_Cal
#' @param Outputs_Sim
#' @param MeanAnSolidPrecip
#' @param IsHyst
#' @param warnings
#' @param verbose
#' #'
#' @return #' @return See \code{\link[airGR]{CreateOptions}}.
#' @export #' @export
#' CreateRunOptions.InputsModel <- function(InputsModel, ...) {
#' @examples
CreateRunOptions.InputsModel <- function(InputsModel,
IndPeriod_WarmUp = NULL, IndPeriod_Run,
IniStates = NULL, IniResLevels = NULL, Imax = NULL,
Outputs_Cal = NULL, Outputs_Sim = "all",
MeanAnSolidPrecip = NULL, IsHyst = FALSE,
warnings = TRUE, verbose = TRUE) {
airGR::CreateRunOptions(FUN_MOD = InputsModel$FUN_MOD, airGR::CreateRunOptions(FUN_MOD = InputsModel$FUN_MOD,
InputsModel = InputsModel, InputsModel = InputsModel,
IndPeriod_WarmUp = IndPeriod_WarmUp, ...)
IndPeriod_Run = IndPeriod_Run,
IniStates = IniStates,
IniResLevels = IniResLevels,
Imax = Imax,
Outputs_Cal = Outputs_Cal,
Outputs_Sim = Outputs_Sim,
MeanAnSolidPrecip = MeanAnSolidPrecip,
IsHyst = IsHyst,
warnings = warnings,
verbose = verbose)
} }
#' Title #' Create \emph{RunOptions} object for airGR and GR-IWRM. See \code{\link[airGR]{CreateOptions}} and \code{[CreateOptions.GriwrmInputsModel]}.
#' #'
#' @param ... #' @param InputsModel object of class \emph{InputsModel} (see \code{\link[airGR]{CreateInputsModel}}) or \emph{GriwrmInputsModel} (See \code{[CreateInputsModel.Griwrm]}).
#' @param InputsModel #' @param ... further arguments passed to or from other methods.
#' #'
#' @return #' @return Object of \emph{RunOptions} class family
#' @export #' @export
#'
#' @examples
CreateRunOptions <- function(InputsModel, ...) { CreateRunOptions <- function(InputsModel, ...) {
UseMethod("CreateRunOptions", InputsModel) UseMethod("CreateRunOptions", InputsModel)
} }
#' Title #' Title
#' #'
#' @param ginet #' @param InputsModel object of class \emph{GriwrmInputsModel}, see \code{[CreateInputsModel.Griwrm]} for details.
#' @param girop #' @param RunOptions object of class \emph{GriwrmRunOptions}, see \code{[CreateRunOptions.Griwrm]} for details.
#' @param gits #' @param girop Girop object giving the run-off model parameters, see \code{[Girop]}.
#' @param IndPeriod_Run #' @param verbose (optional) boolean indicating if the function is run in verbose mode or not, default = \code{TRUE}
#' @param IndPeriod_WarmUp #' @param ... Mandatory for S3 method signature function compatibility with generic.
#' #'
#' @return #' @return \emph{GriwrmOutputsModel} object which is a list of \emph{OutputsModel} objects (See \code{\link[airGR]{RunModel}}) for each node of the semi-distributed model.
#' @export #' @export
#' RunModel.GriwrmInputsModel <- function(InputsModel, RunOptions, girop, verbose = TRUE, ...) {
#' @examples
RunModel.GriwrmInputsModel <- function(InputsModel, RunOptions, girop, verbose = TRUE) {
OutputsModels <- list() OutputsModel <- list()
class(OutputsModel) <- append(class(OutputsModel), "GriwrmOutputsModel")
for(IM in InputsModel) { for(IM in InputsModel) {
if(verbose) cat("RunModel.GriwrmInputsModel: Treating sub-basin", IM$id, "...\n") if(verbose) cat("RunModel.GriwrmInputsModel: Treating sub-basin", IM$id, "...\n")
# Update InputsModel$QobsUpstr with simulated upstream flows # Update InputsModel$QobsUpstr with simulated upstream flows
if(length(IM$UpstreamNodes) > 0) { IM <- UpdateQsimUpstream(IM, OutputsModel)
for(i in 1:length(IM$UpstreamNodes)) {
QobsUpstr1 <- matrix(
c(
rep(0, length(RunOptions[[IM$id]]$IndPeriod_WarmUp)),
OutputsModels[[IM$UpstreamNodes[i]]]$Qsim
), ncol = 1
)
if(i == 1) {
IM$QobsUpstr <- QobsUpstr1
} else {
IM$QobsUpstr <- cbind(IM$QobsUpstr, QobsUpstr1)
}
}
}
# Run the model for the sub-basin # Run the model for the sub-basin
OutputsModels[[IM$id]] <- RunModel( OutputsModel[[IM$id]] <- RunModel(
InputsModel = IM, InputsModel = IM,
RunOptions = RunOptions[[IM$id]], RunOptions = RunOptions[[IM$id]],
Param = unlist(girop$params[girop$id == IM$id]) Param = unlist(girop$params[girop$id == IM$id])
) )
} }
return(OutputsModels) return(OutputsModel)
} }
...@@ -7,9 +7,7 @@ ...@@ -7,9 +7,7 @@
#' #'
#' @return #' @return
#' @export #' @export
#' RunModel.InputsModel <- function(InputsModel, RunOptions, Param, FUN_MOD = NULL, ...) {
#' @examples
RunModel.InputsModel <- function(InputsModel, RunOptions, Param, FUN_MOD = NULL) {
if(is.null(FUN_MOD)) { if(is.null(FUN_MOD)) {
FUN_MOD <- InputsModel$FUN_MOD FUN_MOD <- InputsModel$FUN_MOD
} }
......
#' RunModel function for both airGR and GriwrmInputsModel object #' RunModel function for both airGR and GriwrmInputsModel object
#' #'
#' @param InputsModel #' @param InputsModel object of class \emph{InputsModel}, see \code{\link[airGR]{CreateInputsModel}} for details.
#' @param ... #' @param ... further arguments passed to or from other methods.
#' #'
#' @return #' @return
#' @export #' @export
#'
#' @examples
RunModel <- function(InputsModel, ...) { RunModel <- function(InputsModel, ...) {
UseMethod("RunModel", InputsModel) UseMethod("RunModel", InputsModel)
} }
#' Update InputsModel$QobsUpstr with simulated upstream flows provided by GriwrmOutputsModels object.
#'
#' @param InputsModel \emph{GriwrmInputsModel} object. See \code{[CreateInputsModel.Griwrm]}.
#' @param OutputsModels \emph{GriwrmOutputsModel} object provided by \code{[RunModel.GriwrmInputsModel]}.
#'
#' @description This function is used by \code{\link{RunModel.GriwrmInputsModel}} and \code{\link{Calibration.GriwrmInputsModel}} in order to provide upstream simulated flows to a node.
#'
#' @return InputsModel object with updated QobsUpsr
#'
UpdateQsimUpstream <- function(InputsModel, OutputsModels) {
if(length(InputsModel$UpstreamNodes) > 0) {
for(i in 1:length(InputsModel$UpstreamNodes)) {
QobsUpstr1 <- matrix(
c(
rep(0, length(RunOptions[[InputsModel$id]]$IndPeriod_WarmUp)),
OutputsModels[[InputsModel$UpstreamNodes[i]]]$Qsim
), ncol = 1
)
if(i == 1) {
InputsModel$QobsUpstr <- QobsUpstr1
} else {
InputsModel$QobsUpstr <- cbind(InputsModel$QobsUpstr, QobsUpstr1)
}
}
}
return(InputsModel)
}
#' Generate a network description containing all hydraulic nodes and the description #' Generate a network description containing all hydraulic nodes and the description
#' of their connections #' of their connections
#' #'
#' @param db A tibble or a dataframe containing at least the id and the description of the connections #' @param db A tibble or a data frame containing at least columns with the id (column `id`), the id and the hydraulic distance to the node downstream (columns `down` and `length`) and a boolean determining whether the node is a rainfall run-off model or not (column `runoff`). The last downstream node should have fields `down` and `length` set to `NA`.
#' @param cols #' @param cols named list or vector for matching columns of `db` parameter. By default, mandatory columns names are: `id`, `down`, `length`. But other names can be handled with a named list or vector containing items defined as `"required name" = "column name in db"`.
#' @param keep_all Keep all column of `db` or keep only columns defined in `cols` #' @param keep_all Keep all column of `db` or keep only columns defined in `cols`
#' #'
#' @return #' @return `Ginet` class object containing the description of diagram of the semi-distributed catchment model
#' @export #' @export
#'
#' @examples
Ginet <- function(db, cols = list(id = "id", down = "down", length = "length", runoff = "runoff"), keep_all = FALSE) { Ginet <- function(db, cols = list(id = "id", down = "down", length = "length", runoff = "runoff"), keep_all = FALSE) {
colsDefault <- list(id = "id", down = "down", length = "length", runoff = "runoff") colsDefault <- list(id = "id", down = "down", length = "length", runoff = "runoff")
cols <- utils::modifyList(colsDefault, as.list(cols)) cols <- utils::modifyList(colsDefault, as.list(cols))
...@@ -20,14 +18,12 @@ Ginet <- function(db, cols = list(id = "id", down = "down", length = "length", r ...@@ -20,14 +18,12 @@ Ginet <- function(db, cols = list(id = "id", down = "down", length = "length", r
db db
} }
#' Title #' Sort the nodes from upstream to downstream.
#' #'
#' @param ginet #' @param ginet See \code{[Ginet]}.
#' #'
#' @return #' @return vector with the ordered node names.
#' @export #' @export
#'
#' @examples
getNodeRanking <- function(ginet) { getNodeRanking <- function(ginet) {
if(!is(ginet, "Ginet")) { if(!is(ginet, "Ginet")) {
stop("getNodeRanking: ginet argument should be of class Ginet") stop("getNodeRanking: ginet argument should be of class Ginet")
......
#' Generate the list of run-off models and their parameters #' Generate the list of run-off models and their parameters
#' #'
#' @param db dataframe containing at least the id and the area of the sub-basin #' @param db data frame containing at least the id the area and the model of the sub-basin.
#' @param cols #' @param cols named list or vector for matching columns of `db` parameter. By default, mandatory columns names are: `id`, `area`, `model`. But other names can be handled with a named list or vector containing items defined as `"required name" = "column name in db"`.
#' @param keep_all Keep all column of `db` or keep only columns defined in `cols` #' @param keep_all Keep all column of `db` or keep only columns defined in `cols`
#' #'
#' @return #' @return \emph{Girop} object.
#' @export #' @export
#'
#' @examples
Girop <- function(db, cols = c(id = "id", area = "area", model = "model", params = "params"), keep_all = FALSE) { Girop <- function(db, cols = c(id = "id", area = "area", model = "model", params = "params"), keep_all = FALSE) {
colsDefault <- list(id = "id", area = "area", model = "model", params = "params") colsDefault <- list(id = "id", area = "area", model = "model", params = "params")
cols <- utils::modifyList(colsDefault, as.list(cols)) cols <- utils::modifyList(colsDefault, as.list(cols))
......
#' Title #' Title
#' #'
#' @param id #' @param id string of the id of the node
#' @param ts #' @param ts numeric matrix or data frame containing 3 columns for precipitation, evaporation, and observed flow
#' @param cols #' @param cols named list or vector used for matching the columns of ts with the required columns names which are "Precip", "PotEvap", and "Qobs".
#' #'
#' @return #' @return \emph{Gits} class object which is a list containing a `date` element (Vector of PosiXlt timestamps) and an element named the id of the node containing a dataframe with observed data.
#' @export #' @export
#'
#' @examples
Gits <- function(id, ts, Gits <- function(id, ts,
cols = list(date = "date", Precip = "Precip", PotEvap = "PotEvap", Qobs = "Qobs")) { cols = list(date = "date", Precip = "Precip", PotEvap = "PotEvap", Qobs = "Qobs")) {
...@@ -28,16 +26,15 @@ Gits <- function(id, ts, ...@@ -28,16 +26,15 @@ Gits <- function(id, ts,
gitsOut gitsOut
} }
#' Title #' Merge two gits objects with identical date time series.
#' #'
#' @param x #' @param x Gits object to merge (See [Gits]).
#' @param y #' @param y Gits object to merge (See [Gits]).
#' @param ... For merge generic function compatibility.
#' #'
#' @return #' @return Gits object merged with one item `Date` and Items corresponding to each node.
#' @export #' @export
#' merge.Gits <- function(x, y, ...) {
#' @examples
merge.Gits <- function(x, y) {
if(!is(y, "Gits")) { if(!is(y, "Gits")) {
stop("A Gits class object can only be merged with a Gits class object") stop("A Gits class object can only be merged with a Gits class object")
} }
......
--- ---
title: 'Tutorial: structuration of a semi-distributive GR model' title: 'Tutorial: structuration of a semi-distributive GR model'
author: "David Dorchies" author: "David Dorchies"
date: "19 mai 2020" vignette: >
output: html_document %\VignetteIndexEntry{Tutorial: structuration of a semi-distributive GR model}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
--- ---
```{r setup, include=FALSE} ```{r setup, include=FALSE}
......
--- ---
title: "First run of the model" title: "First run of the model"
author: "David Dorchies" author: "David Dorchies"
date: "20 mai 2020" vignette: >
output: html_document %\VignetteIndexEntry{First run of the model}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
--- ---
```{r setup, include=FALSE} ```{r setup, include=FALSE}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment