diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 9c7e095a..00000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.Rbuildignore b/.Rbuildignore index 8617ec08..25909df6 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -3,3 +3,6 @@ ^LICENSE\.md$ ^\.github$ ^README\.Rmd$ +^_pkgdown\.yml$ +^docs$ +^pkgdown$ diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml new file mode 100644 index 00000000..a7276e85 --- /dev/null +++ b/.github/workflows/pkgdown.yaml @@ -0,0 +1,48 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + release: + types: [published] + workflow_dispatch: + +name: pkgdown + +jobs: + pkgdown: + runs-on: ubuntu-latest + # Only restrict concurrency for non-PR jobs + concurrency: + group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::pkgdown, local::. + needs: website + + - name: Build site + run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) + shell: Rscript {0} + + - name: Deploy to GitHub pages 🚀 + if: github.event_name != 'pull_request' + uses: JamesIves/github-pages-deploy-action@v4.5.0 + with: + clean: false + branch: gh-pages + folder: docs diff --git a/.gitignore b/.gitignore index 7cc8b6a3..cbd632e0 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ po/*~ # RStudio Connect folder rsconnect/ .Rproj.user + +# Mac OS +.DS_Store diff --git a/DESCRIPTION b/DESCRIPTION index ac4c3004..31f36595 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -2,8 +2,10 @@ Package: camtrapdp Title: Read and Convert Camera Trap Data Packages (Camtrap DP) Version: 0.0.0.9000 Authors@R: c( + person("Peter", "Desmet", email = "peter.desmet@inbo.be", + role = c("aut", "cre"), comment = c(ORCID = "0000-0002-8442-8025")), person("Damiano", "Oldoni", email = "damiano.oldoni@inbo.be", - role = c("aut", "cre"), comment = c(ORCID = "0000-0003-3445-7562")), + role = "aut", comment = c(ORCID = "0000-0003-3445-7562")), person("Research Institute for Nature and Forest (INBO)", role = "cph", comment = "https://www.vlaanderen.be/inbo/en-gb/"), person("LifeWatch Belgium", @@ -11,9 +13,14 @@ Authors@R: c( ) Description: What the package does (one paragraph). License: MIT + file LICENSE +URL: https://github.com/inbo/camtrapdp, https://inbo.github.io/camtrapdp/ +BugReports: https://github.com/inbo/camtrapdp/issues +Imports: + cli, + frictionless +Suggests: + testthat (>= 3.0.0) Encoding: UTF-8 Roxygen: list(markdown = TRUE) RoxygenNote: 7.3.0 -Suggests: - testthat (>= 3.0.0) Config/testthat/edition: 3 diff --git a/NAMESPACE b/NAMESPACE index 6ae92683..3c206d97 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,2 +1,4 @@ # Generated by roxygen2: do not edit by hand +export(read_camtrap_dp) +export(version) diff --git a/R/convert.R b/R/convert.R new file mode 100644 index 00000000..5b24a91b --- /dev/null +++ b/R/convert.R @@ -0,0 +1,28 @@ +#' Convert a Camera Trap Data Package +#' +#' Converts a Camera Trap Data Package object that uses an older version of +#' the Camtrap DP standard to a newer version. +#' +#' @inheritParams version +#' @param convert_to Version to convert to. +#' @return Converted Camera Trap Data Package object. +#' @noRd +convert <- function(x, convert_to = "1.0") { + # Convert until the version number matches the expected version + while(version(x) != convert_to) { + x <- switch( + version(x), + "0.1.6" = convert_0.1.6_to_1.0(x), + "1.0" = x + ) + } + x +} + +#' Convert a Camtrap DP 0.1.6 to 1.0 +#' @noRd +convert_0.1.6_to_1.0 <- function(x) { + # TODO: conversion steps for 0.1.6 + attr(x, "version") <- "1.0" + x +} diff --git a/R/read_camtrap_dp.R b/R/read_camtrap_dp.R index e69de29b..35af9e22 100644 --- a/R/read_camtrap_dp.R +++ b/R/read_camtrap_dp.R @@ -0,0 +1,41 @@ +#' Read a Camera Trap Data Package +#' +#' Reads files from a [Camera Trap Data Package (Camtrap DP)]( +#' https://camtrap-dp.tdwg.org) into memory. +#' +#' @param file Path or URL to a `datapackage.json` file. +#' @return Camera Trap Data Package object. +#' @export +read_camtrap_dp <- function(file) { + # Read datapackage.json + package <- frictionless::read_package(file) + + # Check version + version <- version(package) + supported_versions <- c("1.0") + if (!version %in% supported_versions) { + cli::cli_abort( + c( + "{.val {version}} is not a supported Camtrap DP version.", + "i" = "Supported version{?s}: {.val {supported_versions}}." + ), + class = "camtrapdp_error_unsupported_version" + ) + } + + # Create camtrapdp object + x <- package + class(x) <- c("camtrapdp", class(x)) + attr(x, "version") <- version + + # Read and attach csv data + x$data$deployments <- + frictionless::read_resource(package, "deployments") + x$data$media <- + frictionless::read_resource(package, "media") + x$data$observations <- + frictionless::read_resource(package, "observations") + + # Convert + convert(x, convert_to = "1.0") +} diff --git a/R/version.R b/R/version.R new file mode 100644 index 00000000..4f6dbba3 --- /dev/null +++ b/R/version.R @@ -0,0 +1,44 @@ +#' Get Camtrap DP version +#' +#' Extracts the version number used by a Camera Trap Data Package object. +#' This version number indicates what version of the [Camtrap DP standard]( +#' https://camtrap-dp.tdwg.org) was used. +#' +#' The version number is derived as follows: +#' 1. The `version` attribute, if defined. +#' 2. A version number contained in `x$profile`, which is expected to +#' contain the URL to the used Camtrap DP standard. +#' 3. `x$profile` in its entirety (can be `NULL`). +#' +#' @param x Camera Trap Data Package object, as returned by +#' `read_camtrap_dp()`. +#' Also works on a Frictionless Data Package, as returned by +#' `frictionless::read_package()`. +#' @return Camtrap DP version number (e.g. `1.0`). +#' @export +version <- function(x) { + # Get version from attribute + attr_version <- attr(x, "version") + if (!is.null(attr_version)) { + return(attr_version) + } + + # Get version from profile + profile <- x$profile + if (is.null(profile)) { + return(NA) + } + + # Find pattern "camtrap-dp//" in e.g. + # https://raw.githubusercontent.com/tdwg/camtrap-dp/1.0/camtrap-dp-profile.json + pattern <- "camtrap-dp\\/([0-9A-Za-z]|\\.|-)+\\/" + match <- grep(pattern, profile) + if (length(match) > 0) { + extracted_version <- regmatches(profile, regexpr(pattern, profile)) + extracted_version <- sub("camtrap-dp/", "", extracted_version, fixed = TRUE) + extracted_version <- sub("/", "", extracted_version, fixed = TRUE) + extracted_version + } else { + profile + } +} diff --git a/_pkgdown.yml b/_pkgdown.yml new file mode 100644 index 00000000..6e27821c --- /dev/null +++ b/_pkgdown.yml @@ -0,0 +1,4 @@ +url: https://inbo.github.io/camtrapdp/ +template: + bootstrap: 5 + diff --git a/man/read_camtrap_dp.Rd b/man/read_camtrap_dp.Rd new file mode 100644 index 00000000..29176152 --- /dev/null +++ b/man/read_camtrap_dp.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/read_camtrap_dp.R +\name{read_camtrap_dp} +\alias{read_camtrap_dp} +\title{Read a Camera Trap Data Package} +\usage{ +read_camtrap_dp(file) +} +\arguments{ +\item{file}{Path or URL to a \code{datapackage.json} file.} +} +\value{ +Camera Trap Data Package object. +} +\description{ +Reads files from a \href{https://camtrap-dp.tdwg.org}{Camera Trap Data Package (Camtrap DP)} into memory. +} diff --git a/man/version.Rd b/man/version.Rd new file mode 100644 index 00000000..6723fa34 --- /dev/null +++ b/man/version.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/version.R +\name{version} +\alias{version} +\title{Get Camtrap DP version} +\usage{ +version(x) +} +\arguments{ +\item{x}{Camera Trap Data Package object, as returned by +\code{read_camtrap_dp()}. +Also works on a Frictionless Data Package, as returned by +\code{frictionless::read_package()}.} +} +\value{ +Camtrap DP version number (e.g. \code{1.0}). +} +\description{ +Extracts the version number used by a Camera Trap Data Package object. +This version number indicates what version of the \href{https://camtrap-dp.tdwg.org}{Camtrap DP standard} was used. +} +\details{ +The version number is derived as follows: +\enumerate{ +\item The \code{version} attribute, if defined. +\item A version number contained in \code{x$profile}, which is expected to +contain the URL to the used Camtrap DP standard. +\item \code{x$profile} in its entirety (can be \code{NULL}). +} +} diff --git a/tests/testthat/test-read_camtrap_dp.R b/tests/testthat/test-read_camtrap_dp.R index 8849056e..b4f1bab7 100644 --- a/tests/testthat/test-read_camtrap_dp.R +++ b/tests/testthat/test-read_camtrap_dp.R @@ -1,3 +1,40 @@ -test_that("multiplication works", { - expect_equal(2 * 2, 4) +test_that("read_camtrap_dp() reads a Camtrap DP", { + skip_if_offline() + file <- + "https://raw.githubusercontent.com/tdwg/camtrap-dp/1.0/example/datapackage.json" + expect_no_error(read_camtrap_dp(file)) + + # TODO: add more tests on returned object +}) + +test_that("read_camtrap_dp() returns error on unsupported Camtrap DP version", { + skip_if_offline() + + # Unsupported version + camtrap_dp_1.0_rc.1 <- + "https://raw.githubusercontent.com/tdwg/camtrap-dp/1.0-rc.1/example/datapackage.json" + expect_error( + read_camtrap_dp(camtrap_dp_1.0_rc.1), + class = "camtrapdp_error_unsupported_version" + ) + expect_error( + read_camtrap_dp(camtrap_dp_1.0_rc.1), + regexp = "\"1.0-rc.1\" is not a supported Camtrap DP version.", + fixed = TRUE + ) + + # Not a Camtrap DP + o_assen <- "https://zenodo.org/records/10053903/files/datapackage.json" + expect_error( + read_camtrap_dp(o_assen), + class = "camtrapdp_error_unsupported_version" + ) +}) + +test_that("read_camtrap_dp() does not convert 1.0", { + skip_if_offline() + camtrap_dp_1.0 <- + "https://raw.githubusercontent.com/tdwg/camtrap-dp/1.0/example/datapackage.json" + x <- read_camtrap_dp(camtrap_dp_1.0) + expect_identical(version(x), "1.0") }) diff --git a/tests/testthat/test-version.R b/tests/testthat/test-version.R new file mode 100644 index 00000000..142ca808 --- /dev/null +++ b/tests/testthat/test-version.R @@ -0,0 +1,35 @@ +test_that("version() extracts version from attribute", { + x <- list() + attr(x, "version") <- "1.0" + expect_identical(version(x), "1.0") +}) + +test_that("version() extracts version from x$profile on match", { + x <- list() + x$profile <- "https://raw.githubusercontent.com/tdwg/camtrap-dp/1.0/camtrap-dp-profile.json" + expect_identical(version(x), "1.0") + x$profile <- "https://rs.gbif.org/sandbox/data-packages/camtrap-dp/1.0/profile/camtrap-dp-profile.json" + expect_identical(version(x), "1.0") + x$profile <- "foo/camtrap-dp/2/foo" + expect_identical(version(x), "2") + x$profile <- "foo/camtrap-dp/2.30/foo" + expect_identical(version(x), "2.30") + x$profile <- "foo/camtrap-dp/2.30.5/foo" + expect_identical(version(x), "2.30.5") + x$profile <- "foo/camtrap-dp/1.0-rc.1/foo" + expect_identical(version(x), "1.0-rc.1") +}) + +test_that("version() returns x$profile when no match is found", { + x <- list() + x$profile <- NULL + expect_identical(version(x), NA) + x$profile <- NA + expect_identical(version(x), NA) + x$profile <- 1 + expect_identical(version(x), 1) + x$profile <- "tabular-data-package" + expect_identical(version(x), "tabular-data-package") + x$profile <- "foo/2.30/foo" # Must contain camtrap-dp/ + expect_identical(version(x), "foo/2.30/foo") +})