diff --git a/DESCRIPTION b/DESCRIPTION index 51b2bb08..fc8d8f71 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -38,7 +38,9 @@ Depends: R (>= 3.5.0) Imports: assertthat, + DBI, dplyr, + EML, frictionless, glue, htmltools, @@ -48,8 +50,11 @@ Imports: purrr, readr, rlang, + RSQLite, stringr, tidyr +Remotes: + inbo/movepub Suggests: covr, knitr, @@ -61,5 +66,5 @@ Encoding: UTF-8 LazyData: true LazyDataCompression: bzip2 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.1.2 +RoxygenNote: 7.2.0 Config/testthat/edition: 3 diff --git a/NAMESPACE b/NAMESPACE index 5ba538c2..59158582 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -29,6 +29,7 @@ export(pred_notna) export(pred_or) export(read_camtrap_dp) export(transform_effort_to_common_units) +export(write_dwc) importFrom(assertthat,assert_that) importFrom(dplyr,"%>%") importFrom(dplyr,.data) diff --git a/R/get_species.R b/R/get_species.R index af1e9e8a..7f33d000 100644 --- a/R/get_species.R +++ b/R/get_species.R @@ -14,19 +14,17 @@ #' #' @examples #' get_species(mica) -#' get_species <- function(datapkg) { - - # check input data package + # Check input data package check_datapkg(datapkg) - - # get vernacular names and scientific names from datapackage (taxonomic - # slot) - if (!"taxonomic" %in% names(datapkg$datapackage)) return(NULL) - else { + + # Get taxonomic information from package metadata + if (!"taxonomic" %in% names(datapkg$datapackage)) { + return(NULL) + } else { taxonomy <- datapkg$datapackage$taxonomic if ("vernacularNames" %in% names(taxonomy[[1]])) { - # get all languages used in vernacularNames + # Get all languages used in vernacularNames langs <- map(taxonomy, function(x) { vernacular_languages <- NULL if ("vernacularNames" %in% names(x)) { @@ -34,20 +32,18 @@ get_species <- function(datapkg) { } }) langs <- unique(unlist(langs)) - - # fill empty vernacular names with NA - taxonomy <- map(taxonomy, - function(x) { - missing_langs <- langs[!langs %in% names(x$vernacularNames)] - for (i in missing_langs) { - x$vernacularNames[[i]] <- NA_character_ - } - x - }) + + # Fill empty vernacular names with NA + taxonomy <- map(taxonomy, function(x) { + missing_langs <- langs[!langs %in% names(x$vernacularNames)] + for (i in missing_langs) { + x$vernacularNames[[i]] <- NA_character_ + } + x + }) } - map_dfr( - taxonomy, - function(x) x %>% as.data.frame()) %>% - tibble() + map_dfr(taxonomy, function(x) { + tibble(as.data.frame(x)) + }) } } diff --git a/R/read_camtrap_dp.R b/R/read_camtrap_dp.R index e9b7100c..ca1159be 100644 --- a/R/read_camtrap_dp.R +++ b/R/read_camtrap_dp.R @@ -1,7 +1,7 @@ -#' Read camtrap-dp formatted data +#' Read Camtrap DP formatted data #' #' This function reads camera trap data formatted following the [Camera Trap -#' Data Package (Camtrap DP)](https://github.com/tdwg/camtrap-dp) format. The +#' Data Package (Camtrap DP)](https://tdwg.github.io/camtrap-dpdp) format. The #' function is built upon the functions \link[frictionless]{read_package} and #' \link[frictionless]{read_resource}. This means a.o. that all datetime #' information included in the camera trap data package is automatically diff --git a/R/write_dwc.R b/R/write_dwc.R new file mode 100644 index 00000000..ef843d38 --- /dev/null +++ b/R/write_dwc.R @@ -0,0 +1,223 @@ +#' Transform camera trap data to Darwin Core +#' +#' Transforms a published [Camera Trap Data Package +#' (Camtrap DP)](https://github.com/tdwg/camtrap-dp) to Darwin Core CSV and EML +#' files that can be uploaded to a [GBIF IPT](https://www.gbif.org/ipt) for +#' publication. +#' A `meta.xml` file is not created. +#' +#' @param package A Camtrap DP, as read by [read_camtrap_dp()]. +#' @param directory Path to local directory to write files to. +#' @param doi DOI of the original dataset, used to get metadata. +#' @param contact Person to be set as resource contact and metadata provider. +#' To be provided as a `person()`. +#' @param rights_holder Acronym of the organization owning or managing the +#' rights over the data. +#' @return CSV (data) and EML (metadata) files written to disk. +#' @export +#' @section Metadata: +#' +#' Metadata are derived from the original dataset by looking up its `doi` in +#' DataCite ([example](https://doi.org/10.5281/zenodo.5590881)) and transforming +#' these to EML. +#' Uses `movepub::datacite_to_eml()` under the hood. +#' The following properties are set: +#' +#' - **title**: Original title + `[animal observations]`. +#' - **description**: Automatically created first paragraph describing this is +#' a derived dataset, followed by the original dataset description. +#' - **license**: License of the original dataset. +#' - **creators**: Creators of the original dataset. +#' - **contact**: `contact` or first creator of the original dataset. +#' - **metadata provider**: `contact` or first creator of the original dataset. +#' - **keywords**: Keywords of the original dataset. +#' - **associated parties**: Organizations as defined in +#' `package$organizations`. +#' - **geographic coverage**: Bounding box as defined `package$spatial`. +#' - **taxonomic coverage**: Species as defined in `package$taxonomic`. +#' - **temporal coverage**: Date range as defined in `package$temporal`. +#' - **project data**: Title, identifier, description, and sampling design +#' information as defined in `package$project`. +#' - **alternative identifier**: DOI of the original dataset. This way, no new +#' DOI will be created when publishing to GBIF. +#' - **external link**: URL of the project as defined in `package$project$path`. +#' +#' To be set manually in the GBIF IPT: **type**, **subtype**, +#' **update frequency**, and **publishing organization**. +#' +#' Not set: sampling methods and citations. +#' Not applicable: collection data. +#' +#' @section Data: +#' +#' `package` is expected to contain the resources `deployments`, `media` and +#' `observations`. +#' Their CSV data are loaded in to a SQLite database, +#' [transformed to Darwin Core using SQL](https://github.com/inbo/camtraptor/tree/main/inst/sql) +#' and written to disk as CSV file(s). +#' +#' Key features of the Darwin Core transformation: +#' - TODO +#' @examples +#' # TODO +write_dwc <- function(package, directory = ".", doi = package$id, + contact = NULL, rights_holder = package$rightsHolder) { + # TODO: Hotfix to deal with 1 level deep metadata + orig_package <- package + package <- package$datapackage + + # Retrieve metadata from DataCite and build EML + assertthat::assert_that( + !is.null(doi), + msg = "No DOI found in `package$id`, provide one in `doi` parameter." + ) + message("Creating EML metadata.") + eml <- movepub::datacite_to_eml(doi) + + # Set platform + platform <- package$platform$title # Use in DwC + + # Update title + title <- paste(eml$dataset$title, "[animal observations]") # Used in DwC + eml$dataset$title <- title + + # Update license + license_url <- eml$dataset$intellectualRights$rightsUri # Used in DwC + license_code <- eml$dataset$intellectualRights$rightsIdentifier + eml$dataset$intellectualRights <- NULL # Remove original license elements that make EML invalid + eml$dataset$intellectualRights$para <- license_code + + # Set media license + media_license_url <- purrr::keep(package$licenses, ~ .$scope == "media")[[1]]$path + + # Add extra paragraph to description + first_author <- eml$dataset$creator[[1]]$individualName$surName + pub_year <- substr(eml$dataset$pubDate, 1, 4) + doi_url <- eml$dataset$alternateIdentifier[[1]] # Used in DwC + first_para <- glue::glue( + # Add span to circumvent https://github.com/ropensci/EML/issues/342 + "This camera trap dataset is derived from ", + "{first_author} et al. ({pub_year}, {doi_url}), ", + "a Camera Trap Data Package ", + "(Camtrap DP). ", + "Data have been standardized to Darwin Core using the ", + "camtraptor R package ", + "and only include observations (and associated media) of animals. ", + "Excluded are records that document blank or unclassified media, ", + "vehicles and observations of humans. ", + "The original dataset description follows.", + .null = "" + ) + eml$dataset$abstract$para <- purrr::prepend( + eml$dataset$abstract$para, + paste0("") + ) + + # Update contact and set metadata provider + if (!is.null(contact)) { + eml$dataset$contact <- EML::set_responsibleParty( + givenName = contact$given, + surName = contact$family, + electronicMailAddress = contact$email, + userId = if (!is.null(contact$comment[["ORCID"]])) { + list(directory = "http://orcid.org/", contact$comment[["ORCID"]]) + } else { + NULL + } + ) + } + eml$dataset$metadataProvider <- eml$dataset$contact + + # Add organizations as associated parties + eml$dataset$associatedParty <- + purrr::map(package$organizations, ~ EML::set_responsibleParty( + givenName = "", # Circumvent https://github.com/ropensci/EML/issues/345 + organizationName = .$title, + onlineUrl = .$path + )) + + # Set coverage + bbox <- package$spatial$bbox + taxonomy <- get_species(orig_package) + if ("taxonRank" %in% names(taxonomy)) { + taxonomy <- dplyr::filter(taxonomy, taxonRank == "species") + } + sci_names <- + dplyr::rename(taxonomy, Species = scientificName) %>% + dplyr::select(Species) + + eml$dataset$coverage <- EML::set_coverage( + begin = package$temporal$start, + end = package$temporal$end, + west = bbox[1], + south = bbox[2], + east = bbox[3], + north = bbox[4], + sci_names = sci_names + ) + + # Set project metadata + project <- package$project + capture_method <- paste(package$project$captureMethod, collapse = " and ") + animal_type <- paste(package$project$animalTypes, collapse = " and ") + design_para <- glue::glue( + "This project uses a {project$samplingDesign} sampling design, ", + "with {animal_type} animals and ", + "camera traps taking media using {capture_method}. ", + "Media are classified at {project$classificationLevel} level." + ) + eml$dataset$project <- list( + id = project$id, # Can be NULL, assigned as + title = project$title, + abstract = list(para = project$description), # Can be NULL + designDescription = list(description = list(para = design_para)) + ) + + # Set external link to project URL (can be NULL) + if (!is.null(project$path)) { + eml$dataset$distribution = list( + scope = "document", online = list( + url = list("function" = "information", project$path) + ) + ) + } + + # Read data from package + # Already read with read_camtrap_dp() + + # Create database + message("Creating database and transforming to Darwin Core.") + con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") + DBI::dbWriteTable(con, "deployments", dplyr::tibble(orig_package$deployments)) + DBI::dbWriteTable(con, "media", dplyr::tibble(orig_package$media)) + DBI::dbWriteTable(con, "observations", dplyr::tibble(orig_package$observations)) + + # Query database + dwc_occurrence_sql <- glue::glue_sql( + readr::read_file( + system.file("sql/dwc_occurrence.sql", package = "camtraptor") + ), + .con = con + ) + dwc_multimedia_sql <- glue::glue_sql( + readr::read_file( + system.file("sql/dwc_multimedia.sql", package = "camtraptor") + ), + .con = con + ) + dwc_occurrence <- DBI::dbGetQuery(con, dwc_occurrence_sql) + dwc_multimedia <- DBI::dbGetQuery(con, dwc_multimedia_sql) + DBI::dbDisconnect(con) + + # Write files + if (!dir.exists(directory)) { + dir.create(directory, recursive = TRUE) + } + EML::write_eml(eml, file.path(directory, "eml.xml")) + readr::write_csv( + dwc_occurrence, file.path(directory, "dwc_occurrence.csv"), na = "" + ) + readr::write_csv( + dwc_multimedia, file.path(directory, "dwc_multimedia.csv"), na = "" + ) +} diff --git a/inst/sql/dwc_multimedia.sql b/inst/sql/dwc_multimedia.sql new file mode 100644 index 00000000..b0e42fac --- /dev/null +++ b/inst/sql/dwc_multimedia.sql @@ -0,0 +1,61 @@ +/* +Schema: https://rs.gbif.org/extension/ac/audubon_2020_10_06.xml +Camtrap DP terms and whether they are included in DwC (Y) or not (N): + +media.mediaID Y: as link to observation +media.deploymentID N: included at observation level +media.sequenceID Y: as link to observation +media.captureMethod Y +media.timestamp Y +media.filePath Y +media.fileName Y: to sort data +media.fileMediatype Y +media.exifData N +media.favourite N +media.comments Y +media._id N +*/ + +-- Observations can be based on sequences (sequenceID) or individual files (mediaID) +-- Make two joins and union to capture both cases without overlap +WITH observations_media AS ( +-- Sequence based observations + SELECT obs.observationID, obs.timestamp AS observationTimestamp, med.* + FROM observations AS obs + LEFT JOIN media AS med ON obs.sequenceID = med.sequenceID + WHERE obs.observationType = 'animal' AND obs.mediaID IS NULL + UNION +-- File based observations + SELECT obs.observationID, obs.timestamp AS observationTimestamp, med.* + FROM observations AS obs + LEFT JOIN media AS med ON obs.mediaID = med.mediaID + WHERE obs.observationType = 'animal' AND obs.mediaID IS NOT NULL +) + +SELECT + obs_med.observationID AS occurrenceID, +-- provider: can be org managing the platform, but that info is not available + {media_license_url} AS rights, + obs_med.mediaID AS identifier, + CASE + WHEN obs_med.fileMediatype LIKE '%video%' THEN 'MovingImage' + ELSE 'StillImage' + END AS type, + obs_med._id AS providerManagedID, + obs_med.comments AS comments, + dep.cameraModel AS captureDevice, + obs_med.captureMethod AS resourceCreationTechnique, + obs_med.filePath AS accessURI, + obs_med.fileMediatype AS format, + STRFTIME('%Y-%m-%dT%H:%M:%SZ', datetime(obs_med.timestamp, 'unixepoch')) AS CreateDate + +FROM + observations_media AS obs_med + LEFT JOIN deployments AS dep + ON obs_med.deploymentID = dep.deploymentID + +ORDER BY +-- Order is not retained in observations_media, so important to sort + obs_med.observationTimestamp, + obs_med.timestamp, + obs_med.fileName diff --git a/inst/sql/dwc_occurrence.sql b/inst/sql/dwc_occurrence.sql new file mode 100644 index 00000000..aabfcba3 --- /dev/null +++ b/inst/sql/dwc_occurrence.sql @@ -0,0 +1,124 @@ +/* +Schema: https://rs.gbif.org/core/dwc_occurrence_2022-02-02.xml +Camtrap DP terms and whether they are included in DwC (Y) or not (N): + +deployments.deploymentID Y +deployments.locationID Y +deployments.locationName Y +deployments.longitude Y +deployments.latitude Y +deployments.coordinateUncertainty Y +deployments.start Y +deployments.end Y +deployments.setupBy N +deployments.cameraID N +deployments.cameraModel Y: in dwc_multimedia +deployments.cameraInterval N +deployments.cameraHeight N +deployments.cameraTilt N +deployments.cameraHeading N +deployments.timestampIssues N +deployments.baitUse Y +deployments.session N +deployments.array N +deployments.featureType Y +deployments.habitat Y +deployments.tags Y +deployments.comments Y +deployments._id N +observations.observationID Y +observations.deploymentID Y +observations.sequenceID Y +observations.mediaID N: in dwc_multimedia +observations.timestamp Y +observations.observationType Y: as filter +observations.cameraSetup N +observations.taxonID Y +observations.scientificName Y +observations.count Y +observations.countNew N +observations.lifeStage Y +observations.sex Y +observations.behaviour Y +observations.individualID Y +observations.classificationMethod Y +observations.classifiedBy Y +observations.classificationTimestamp Y +observations.classificationConfidence Y +observations.comments Y +observations._id N +*/ + +SELECT +-- RECORD-LEVEL + 'Event' AS type, + {license_url} AS license, + {rights_holder} AS rightsHolder, +-- bibliographicCitation: how *record* should be cited, so not package bibliographicCitation + {doi_url} AS datasetID, +-- institutionCode: org managing the platform/collection, but that info is not available + {platform} AS collectionCode, + {title} AS datasetName, + 'MachineObservation' AS basisOfRecord, + 'see metadata' AS informationWithheld, +-- OCCURRENCE + obs.observationID AS occurrenceID, + obs.count AS individualCount, + obs.sex AS sex, + obs.lifeStage AS lifeStage, + obs.behaviour AS behavior, + 'present' AS occurrenceStatus, + obs.comments AS occurrenceRemarks, +-- ORGANISM + obs.individualID AS organismID, +-- EVENT + obs.sequenceID AS eventID, + obs.deploymentID AS parentEventID, + strftime('%Y-%m-%dT%H:%M:%SZ', datetime(obs.timestamp, 'unixepoch')) AS eventDate, + dep.habitat AS habitat, + 'camera trap' || + CASE + WHEN dep.baitUse IS 'none' THEN ' without bait' + WHEN dep.baitUse IS NOT NULL THEN ' with bait' + ELSE '' + END AS samplingProtocol, + strftime('%Y-%m-%dT%H:%M:%SZ', datetime(dep.start, 'unixepoch')) || + '/' || + strftime('%Y-%m-%dT%H:%M:%SZ', datetime(dep.end, 'unixepoch')) AS samplingEffort, -- Duration of deployment + COALESCE( + dep.comments || ' | tags: ' || dep.tags, + 'tags: ' || dep.tags, + dep.comments + ) AS eventRemarks, +-- LOCATION + dep.locationID AS locationID, + dep.locationName AS locality, + dep.featureType AS locationRemarks, + dep.latitude AS decimalLatitude, + dep.longitude AS decimalLongitude, + 'WGS84' AS geodeticDatum, + dep.coordinateUncertainty AS coordinateUncertaintyInMeters, +-- IDENTIFICATION + obs.classifiedBy AS identifiedBy, + strftime('%Y-%m-%dT%H:%M:%SZ', datetime(obs.classificationTimestamp, 'unixepoch')) AS dateIdentified, + COALESCE( + 'classified by ' || obs.classificationMethod || ' with ' || obs.classificationConfidence || ' confidence', + 'classified by ' || obs.classificationMethod + ) AS identificationRemarks, +-- TAXON + obs.taxonID AS taxonID, + obs.scientificName AS scientificName, + 'Animalia' AS kingdom + +FROM + observations AS obs + LEFT JOIN deployments AS dep + ON obs.deploymentID = dep.deploymentID + +WHERE + -- Select biological observations only (excluding observations marked as human, blank, vehicle) + -- Same filter should be used in dwc_multimedia.sql + obs.observationType = 'animal' + +ORDER BY + obs.timestamp diff --git a/man/filter_predicate.Rd b/man/filter_predicate.Rd index bee6d4ff..8cd12bdf 100644 --- a/man/filter_predicate.Rd +++ b/man/filter_predicate.Rd @@ -117,7 +117,9 @@ Internally, the input to \verb{pred*} functions turn into a character string, which forms the body of a filter expression. For example: -\code{pred("tags", "boven de stroom")} gives:\preformatted{$arg +\code{pred("tags", "boven de stroom")} gives: + +\if{html}{\out{
}}\preformatted{$arg [1] "tags" $value @@ -129,16 +131,22 @@ $type $expr (tags == "boven de stroom") -} +}\if{html}{\out{
}} -\code{pred_gt("latitude", 51.27)} gives, (only \code{expr} slot shown):\preformatted{(latitude > 51.27) -} +\code{pred_gt("latitude", 51.27)} gives, (only \code{expr} slot shown): -\code{pred_or()} gives:\preformatted{((tags == "boven de stroom") | (latitude > 51.28)) -} +\if{html}{\out{
}}\preformatted{(latitude > 51.27) +}\if{html}{\out{
}} -\code{pred_or()} gives:\preformatted{((tags == "boven de stroom") & (latitude > 51.28)) -} +\code{pred_or()} gives: + +\if{html}{\out{
}}\preformatted{((tags == "boven de stroom") | (latitude > 51.28)) +}\if{html}{\out{
}} + +\code{pred_or()} gives: + +\if{html}{\out{
}}\preformatted{((tags == "boven de stroom") & (latitude > 51.28)) +}\if{html}{\out{
}} } \section{Keys}{ diff --git a/man/get_species.Rd b/man/get_species.Rd index 1660d0e6..8e99a165 100644 --- a/man/get_species.Rd +++ b/man/get_species.Rd @@ -19,5 +19,4 @@ Function to get all identified species } \examples{ get_species(mica) - } diff --git a/man/read_camtrap_dp.Rd b/man/read_camtrap_dp.Rd index 53224695..96bdecaf 100644 --- a/man/read_camtrap_dp.Rd +++ b/man/read_camtrap_dp.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/read_camtrap_dp.R \name{read_camtrap_dp} \alias{read_camtrap_dp} -\title{Read camtrap-dp formatted data} +\title{Read Camtrap DP formatted data} \usage{ read_camtrap_dp(file = NULL, media = TRUE, path = lifecycle::deprecated()) } @@ -26,7 +26,7 @@ A list containing three (tibble) data.frames: and a list with metadata: \code{datapackage}. } \description{ -This function reads camera trap data formatted following the \href{https://github.com/tdwg/camtrap-dp}{Camera Trap Data Package (Camtrap DP)} format. The +This function reads camera trap data formatted following the \href{https://tdwg.github.io/camtrap-dpdp}{Camera Trap Data Package (Camtrap DP)} format. The function is built upon the functions \link[frictionless]{read_package} and \link[frictionless]{read_resource}. This means a.o. that all datetime information included in the camera trap data package is automatically diff --git a/man/write_dwc.Rd b/man/write_dwc.Rd new file mode 100644 index 00000000..a5819c6e --- /dev/null +++ b/man/write_dwc.Rd @@ -0,0 +1,90 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/write_dwc.R +\name{write_dwc} +\alias{write_dwc} +\title{Transform camera trap data to Darwin Core} +\usage{ +write_dwc( + package, + directory = ".", + doi = package$id, + contact = NULL, + rights_holder = package$rightsHolder +) +} +\arguments{ +\item{package}{A Camtrap DP, as read by \code{\link[=read_camtrap_dp]{read_camtrap_dp()}}.} + +\item{directory}{Path to local directory to write files to.} + +\item{doi}{DOI of the original dataset, used to get metadata.} + +\item{contact}{Person to be set as resource contact and metadata provider. +To be provided as a \code{person()}.} + +\item{rights_holder}{Acronym of the organization owning or managing the +rights over the data.} +} +\value{ +CSV (data) and EML (metadata) files written to disk. +} +\description{ +Transforms a published \href{https://github.com/tdwg/camtrap-dp}{Camera Trap Data Package (Camtrap DP)} to Darwin Core CSV and EML +files that can be uploaded to a \href{https://www.gbif.org/ipt}{GBIF IPT} for +publication. +A \code{meta.xml} file is not created. +} +\section{Metadata}{ + + +Metadata are derived from the original dataset by looking up its \code{doi} in +DataCite (\href{https://doi.org/10.5281/zenodo.5590881}{example}) and transforming +these to EML. +Uses \code{movepub::datacite_to_eml()} under the hood. +The following properties are set: +\itemize{ +\item \strong{title}: Original title + \verb{[animal observations]}. +\item \strong{description}: Automatically created first paragraph describing this is +a derived dataset, followed by the original dataset description. +\item \strong{license}: License of the original dataset. +\item \strong{creators}: Creators of the original dataset. +\item \strong{contact}: \code{contact} or first creator of the original dataset. +\item \strong{metadata provider}: \code{contact} or first creator of the original dataset. +\item \strong{keywords}: Keywords of the original dataset. +\item \strong{associated parties}: Organizations as defined in +\code{package$organizations}. +\item \strong{geographic coverage}: Bounding box as defined \code{package$spatial}. +\item \strong{taxonomic coverage}: Species as defined in \code{package$taxonomic}. +\item \strong{temporal coverage}: Date range as defined in \code{package$temporal}. +\item \strong{project data}: Title, identifier, description, and sampling design +information as defined in \code{package$project}. +\item \strong{alternative identifier}: DOI of the original dataset. This way, no new +DOI will be created when publishing to GBIF. +\item \strong{external link}: URL of the project as defined in \code{package$project$path}. +} + +To be set manually in the GBIF IPT: \strong{type}, \strong{subtype}, +\strong{update frequency}, and \strong{publishing organization}. + +Not set: sampling methods and citations. +Not applicable: collection data. +} + +\section{Data}{ + + +\code{package} is expected to contain the resources \code{deployments}, \code{media} and +\code{observations}. +Their CSV data are loaded in to a SQLite database, +\href{https://github.com/inbo/camtraptor/tree/main/inst/sql}{transformed to Darwin Core using SQL} +and written to disk as CSV file(s). + +Key features of the Darwin Core transformation: +\itemize{ +\item TODO +} +} + +\examples{ +# TODO +}