diff --git a/R/geojson.R b/R/geojson.R index cda0647..f067ef7 100644 --- a/R/geojson.R +++ b/R/geojson.R @@ -3,26 +3,32 @@ #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #' Options for reading in GeoJSON #' +#' @param type 'sf' or 'sfc' #' @param property_promotion What is the most general container type to use when #' properties differ across a FEATURECOLLECTION? E.g. if the property #' exists both as a numeric and a string, should all values be promoted #' to a 'string', or contained as different types in a 'list'. -#' Default: 'string' will behave like 'geojsonsf' -#' @param type 'sf' or 'sfc' -#' @param property_promotion_lgl_as_int when promoting properties into a string, -#' should logical values become strings e.g. "TRUE" or integers -#' e.g. "1". Default: "integer" in order to match `geojsonsf` packages +#' Default: 'string' will behave like \code{geojsonsf} package. +#' @param property_promotion_lgl when \code{property_promotion = "string"} +#' should logical values become words (i.e. \code{"TRUE"}/\code{"FALSE"}) +#' or integers (i.e. \code{"1"}/\code{"0"}). +#' Default: "integer" in order to match \code{geojsonsf} package #' -#' @return named list +#' @return Named list of options specific to reading GeoJSON #' @export +#' +#' @examples +#' # Create a set of options to use when reading geojson +#' opts_read_geojson() #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -opts_read_geojson <- function(type = c('sf', 'sfc'), - property_promotion = c('string', 'list'), - property_promotion_lgl_as_int = c('integer', 'string')) { +opts_read_geojson <- function(type = c('sf', 'sfc'), + property_promotion = c('string', 'list'), + property_promotion_lgl = c('integer', 'string')) { structure( list( - type = match.arg(type), - property_promotion = match.arg(property_promotion) + type = match.arg(type), + property_promotion = match.arg(property_promotion), + property_promotion_lgl = match.arg(property_promotion_lgl) ), class = "opts_read_geojson" ) @@ -33,8 +39,12 @@ opts_read_geojson <- function(type = c('sf', 'sfc'), #' #' Currently no options available. #' -#' @return named list of options +#' @return Named list of options specific to writing GeoJSON #' @export +#' +#' @examples +#' # Create a set of options to use when writing geojson +#' opts_write_geojson() #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ opts_write_geojson <- function() { structure( @@ -49,23 +59,33 @@ opts_write_geojson <- function() { #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #' Load GeoJSON as \code{sf} object #' -#' @param filename filename -#' @param str single character string containing GeoJSON -#' @param opts named list of options. Usually created with \code{opts_read_geojson()}. +#' @param filename Filename +#' @param str Single string containing GeoJSON +#' @param opts Named list of GeoJSON-specific options. Usually created +#' with \code{opts_read_geojson()}. #' Default: empty \code{list()} to use the default options. -#' @param ... any extra named options override those in \code{opts} +#' @param ... Any extra named options override those in GeoJSON-specific options +#' - \code{opts} +#' @param json_opts Named list of vanilla JSON options as used by \code{read_json_str()}. +#' This is usually created with \code{opts_read_json()}. Default value is +#' an empty \code{list()} which means to use all the default JSON parsing +#' options which is usually the correct thing to do when reading GeoJSON. #' #' @return \code{sf} object #' @export +#' +#' @examples +#' geojson_file <- system.file("geojson-example.json", package = 'yyjsonr') +#' read_geojson_file(geojson_file) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -read_geojson_str <- function(str, opts = list(), ...) { +read_geojson_str <- function(str, opts = list(), ..., json_opts = list()) { opts <- modify_list(opts, list(...)) .Call( parse_geojson_str_, str, opts, # geojson parse opts - list(yyjson_read_flag = 0L) # general parse opts + json_opts ) } @@ -74,14 +94,14 @@ read_geojson_str <- function(str, opts = list(), ...) { #' @rdname read_geojson_str #' @export #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -read_geojson_file <- function(filename, opts = list(), ...) { +read_geojson_file <- function(filename, opts = list(), ..., json_opts = list()) { opts <- modify_list(opts, list(...)) .Call( parse_geojson_file_, - filename, + normalizePath(filename), opts, # geojson parse opts - list(yyjson_read_flag = 0L) # general parse opts + json_opts ) } @@ -93,22 +113,28 @@ read_geojson_file <- function(filename, opts = list(), ...) { #' @param opts named list of options. Usually created with \code{opts_write_geojson()}. #' Default: empty \code{list()} to use the default options. #' @param ... any extra named options override those in \code{opts} -#' @param digits decimal places to keep for floating point numbers. Default: -1. -#' Positive values specify number of decimal places. Using zero will -#' write the numeric value as an integer. Values less than zero mean that -#' the floating point value should be written as-is (the default). +#' @param json_opts Named list of vanilla JSON options as used by \code{write_json_str()}. +#' This is usually created with \code{opts_write_json()}. Default value is +#' an empty \code{list()} which means to use all the default JSON writing +#' options which is usually the correct thing to do when writing GeoJSON. #' -#' @return character string containing json +#' @return Character string containing GeoJSON, or \code{NULL} if GeoJSON +#' written to file. #' @export +#' +#' @examples +#' geojson_file <- system.file("geojson-example.json", package = 'yyjsonr') +#' sf <- read_geojson_file(geojson_file) +#' cat(write_geojson_str(sf, json_opts = opts_write_json(pretty = TRUE))) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -write_geojson_str <- function(x, opts = list(), ..., digits = -1) { +write_geojson_str <- function(x, opts = list(), ..., json_opts = list()) { opts <- modify_list(opts, list(...)) .Call( serialize_sf_to_str_, x, - opts, # geojson serialize opts - list(yyjson_write_flag = 0L, digits = digits) # general serialize opts + opts, # geojson serialize opts + json_opts # general serialize opts ) } @@ -116,15 +142,15 @@ write_geojson_str <- function(x, opts = list(), ..., digits = -1) { #' @rdname write_geojson_str #' @export #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -write_geojson_file <- function(x, filename, opts = list(), ..., digits = -1) { +write_geojson_file <- function(x, filename, opts = list(), ..., json_opts = list()) { opts <- modify_list(opts, list(...)) .Call( serialize_sf_to_file_, x, - filename, - opts, # geojson serialize opts - list(yyjson_write_flag = 0L, digits = digits) # general serialize opts + normalizePath(filename, mustWork = FALSE), + opts, # geojson serialize opts + json_opts # general serialize opts ) invisible() diff --git a/README.Rmd b/README.Rmd index 1cfe93c..baf3a28 100644 --- a/README.Rmd +++ b/README.Rmd @@ -58,10 +58,10 @@ It is a wrapper for the [`yyjson`](https://github.com/ibireme/yyjson) C library ### What's in the box -This package contains specialised functions for each type of operation (read/write/validate) and the +This package contains specialized functions for each type of operation (read/write/validate) and the storage location of the JSON (string/file/raw vector/connection). -The matrix of available operations and storage is shown below: +#### Vanilla JSON | | string | file | raw | conn | options | |----------|---------------------|----------------------|-----------------|------------------|----------------------| @@ -70,19 +70,40 @@ The matrix of available operations and storage is shown below: | validate | validate_json_str() | validate_json_file() | | | | +#### NDJSON + +| | string | file | raw | conn | options | +|----------|---------------------|----------------------|-----------------|------------------|----------------------| +| read | read_ndjson_str() | read_ndjson_file() | | | opts_read_json() | +| write | write_ndjson_str() | write_ndjson_file() | | | opts_write_json() | + + +#### GeoJSON + +| | string | file | raw | conn | options | +|----------|---------------------|----------------------|-----------------|------------------|----------------------| +| read | read_geojson_str() | read_geojson_file() | | | opts_read_geojson() | +| write | write_geojson_str() | write_geojson_file() | | | opts_write_geojson() | -### Comparison to other packages with read/write JSON -| | Write JSON| Read JSON | -|--------------|-----------|-----------| -| yyjsonr | Fast! | Fast! | -| jsonlite | Yes | Yes | -| jsonify | Yes | Yes | +### Speed +In the following plots, bigger is better, with `yyjsonr` results in blue. + + +#### JSON -Note: Benchmarks were run on Apple M2 Mac. See file "man/benchmark/benchmark.Rmd" for details. +#### NDJSON + + + +#### GeoJSON + + + +Note: Benchmarks were run on Apple M2 Mac. See files `man/benchmark/benchmark*.Rmd` for details. ## Installation @@ -108,15 +129,6 @@ read_json_str(str) ``` - -## Future - -* Re-introduce NDJSON support - * NDJSON support was removed for the initial CRAN release for the sake of my sanity. - * See the `ndjson` branch of this repository -* Re-introduce GeoJSON support - * GeoJSON support was removed for the initial CRAN release for the sake of my sanity. - * See the `geojson` branch of this repository ## Limitations diff --git a/README.md b/README.md index da77e2c..881a3c6 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,11 @@ this package for more details. ### What’s in the box -This package contains specialised functions for each type of operation +This package contains specialized functions for each type of operation (read/write/validate) and the storage location of the JSON (string/file/raw vector/connection). -The matrix of available operations and storage is shown below: +#### Vanilla JSON | | string | file | raw | conn | options | |----------|---------------------|----------------------|-----------------|------------------|-------------------| @@ -35,18 +35,39 @@ The matrix of available operations and storage is shown below: | write | write_json_str() | write_json_file() | | | opts_write_json() | | validate | validate_json_str() | validate_json_file() | | | | -### Comparison to other packages with read/write JSON +#### NDJSON -| | Write JSON | Read JSON | -|----------|------------|-----------| -| yyjsonr | Fast! | Fast! | -| jsonlite | Yes | Yes | -| jsonify | Yes | Yes | +| | string | file | raw | conn | options | +|-------|--------------------|---------------------|-----|------|-------------------| +| read | read_ndjson_str() | read_ndjson_file() | | | opts_read_json() | +| write | write_ndjson_str() | write_ndjson_file() | | | opts_write_json() | + +#### GeoJSON + +| | string | file | raw | conn | options | +|-------|---------------------|----------------------|-----|------|----------------------| +| read | read_geojson_str() | read_geojson_file() | | | opts_read_geojson() | +| write | write_geojson_str() | write_geojson_file() | | | opts_write_geojson() | + +### Speed + +In the following plots, bigger is better, with `yyjsonr` results in +blue. + +#### JSON -Note: Benchmarks were run on Apple M2 Mac. See file -“man/benchmark/benchmark.Rmd” for details. +#### NDJSON + + + +#### GeoJSON + + + +Note: Benchmarks were run on Apple M2 Mac. See files +`man/benchmark/benchmark*.Rmd` for details. ## Installation @@ -96,17 +117,6 @@ read_json_str(str) #> 3 4.7 3.2 1.3 0.2 setosa ``` -## Future - -- Re-introduce NDJSON support - - NDJSON support was removed for the initial CRAN release for the sake - of my sanity. - - See the `ndjson` branch of this repository -- Re-introduce GeoJSON support - - GeoJSON support was removed for the initial CRAN release for the - sake of my sanity. - - See the `geojson` branch of this repository - ## Limitations - Some datatypes not currently supported. Please file an issue on GitHub diff --git a/inst/geojson-example.json b/inst/geojson-example.json new file mode 100644 index 0000000..04e817f --- /dev/null +++ b/inst/geojson-example.json @@ -0,0 +1,35 @@ +{ "type": "FeatureCollection", + "features": [ + { "type": "Feature", + "geometry": {"type": "Point", "coordinates": [102.0, 0.5]}, + "properties": {"prop0": "value0"} + }, + { "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0] + ] + }, + "properties": { + "prop0": "value0", + "prop1": 0.0 + } + }, + { "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], + [100.0, 1.0], [100.0, 0.0] ] + ] + + }, + "properties": { + "prop0": "value0", + "prop1": {"this": "that"} + } + } + ] + } + diff --git a/man/benchmark/benchmark-geoson.Rmd b/man/benchmark/benchmark-geoson.Rmd new file mode 100644 index 0000000..343f179 --- /dev/null +++ b/man/benchmark/benchmark-geoson.Rmd @@ -0,0 +1,136 @@ +--- +title: "Benchmarks" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Benchmarks} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + fig.width = 6, + fig.height = 4 +) +``` + +```{r setup} +library(yyjsonr) +library(bench) +library(ggplot2) +library(tidyr) +library(ggbeeswarm) +library(geojsonsf) +library(sf) + +library(ndjson) +``` + + +Benchmark overview +============================================================================== + +* Benchmarking was done an Apple M2 Silicon. +* Test-cases were drawn from other packages and other examples seen in the wild + + + +From GeoJSON String +---------------------------------------------------------------------------- + +```{r} +res13 <- bench::mark( + geojsonsf = geojsonsf::geojson_sf(geojsonsf::geo_melbourne), + yyjsonr = yyjsonr::read_geojson_str(geojsonsf::geo_melbourne), + check = FALSE +) + +``` + +```{r echo=FALSE} +res13$benchmark <- 'from GeoJSON string' +knitr::kable(res13[,1:5]) +plot(res13) + theme_bw() + theme(legend.position = 'none') +``` + + +To GeoJSON String +---------------------------------------------------------------------------- + +```{r} +sf_obj <- sf::st_read(system.file("shape/nc.shp", package="sf"), quiet = TRUE) + +res14 <- bench::mark( + geojsonsf = geojsonsf::sf_geojson(sf_obj), + yyjsonr = yyjsonr::write_geojson_str(sf_obj), + check = FALSE +) + +``` + +```{r echo=FALSE} +res14$benchmark <- 'to GeoJSON string' +knitr::kable(res14[,1:5]) +plot(res14) + theme_bw() + theme(legend.position = 'none') +``` + + + + + +Summary +=============================================================================== + +```{r echo = FALSE, fig.width = 8, fig.height = 6} +library(dplyr) +plot_df <- bind_rows( + res13, res14 +) + +plot_df$benchmark <- factor( + plot_df$benchmark, + levels = unique(plot_df$benchmark) +) + +plot_df <- plot_df %>% + mutate( + package = as.character(expression), + iters = `itr/sec`, + speed = iters + ) %>% + select(benchmark, package, iters, speed) + +plot_df <- plot_df %>% + group_by(benchmark) %>% + mutate( + ref_speed = speed[which(package %in% c('jsonlite', 'geojsonsf'))], + speed = speed / ref_speed + ) %>% + ungroup() + +ggplot(plot_df) + + geom_col(aes(package, speed, fill = package), + position = position_dodge2(preserve = "single")) + + facet_wrap(~benchmark, scales = 'free_y', ncol = 3) + + theme_bw(15) + + theme(legend.position = 'none') + + scale_fill_manual(values = c(rep(grey(0.5), 1), 'dodgerblue3')) + + geom_hline(yintercept = 1, color = 'red', alpha = 0.5, linetype = 2) + + labs( + x = NULL, + y = "Factor speed increase\nover reference implementation", + title = "Speed-up compared to reference implementation", + subtitle = "Red line indicates reference implementation {geojsonsf}" + ) + + scale_y_continuous(breaks = scales::pretty_breaks()) + +if (FALSE) { + ggsave("./man/figures/benchmark-geojson.png", width = 7, height = 5) + # saveRDS(plot_df, "man/benchmark/cache-df-types.rds") +} +``` + + + diff --git a/man/benchmark/benchmarks.Rmd b/man/benchmark/benchmark-json.Rmd similarity index 97% rename from man/benchmark/benchmarks.Rmd rename to man/benchmark/benchmark-json.Rmd index 416285f..1024c7d 100644 --- a/man/benchmark/benchmarks.Rmd +++ b/man/benchmark/benchmark-json.Rmd @@ -280,8 +280,8 @@ plot_df <- plot_df %>% ggplot(plot_df) + geom_col(aes(package, speed, fill = package), position = position_dodge2(preserve = "single")) + - facet_wrap(~benchmark, scales = 'free_y', ncol = 3) + - theme_bw() + + facet_wrap(~benchmark, scales = 'free_y', nrow = 2) + + theme_bw(15) + theme(legend.position = 'none') + scale_fill_manual(values = c(rep(grey(0.5), 2), 'dodgerblue3')) + geom_hline(yintercept = 1, color = 'red', alpha = 0.5, linetype = 2) + @@ -290,11 +290,12 @@ ggplot(plot_df) + y = "Factor speed increase over reference implementation", title = "Speed-up compared to reference implementation", subtitle = "Red line indicates reference implementation {jsonlite}" - ) + ) + + scale_y_continuous(breaks = scales::pretty_breaks()) if (FALSE) { - ggsave("./man/figures/benchmark-summary.png", width = 12, height = 10) - saveRDS(plot_df, "man/benchmark/cache-df-types.rds") + ggsave("./man/figures/benchmark-summary.png", width = 12, height = 7) + # saveRDS(plot_df, "man/benchmark/cache-df-types.rds") } ``` diff --git a/man/benchmark/benchmark-ndjson.Rmd b/man/benchmark/benchmark-ndjson.Rmd new file mode 100644 index 0000000..88f6b40 --- /dev/null +++ b/man/benchmark/benchmark-ndjson.Rmd @@ -0,0 +1,193 @@ +--- +title: "Benchmarks" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Benchmarks} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + fig.width = 6, + fig.height = 4 +) +``` + +```{r setup} +library(yyjsonr) +library(bench) +library(ggplot2) +library(tidyr) +library(ggbeeswarm) +library(geojsonsf) +library(sf) + +library(ndjson) +``` + + +Benchmark overview +============================================================================== + +* Benchmarking was done an Apple M2 Silicon. +* Test-cases were drawn from other packages and other examples seen in the wild + + + +From NDJSON file +------------------------------------------------------------------------------- + + +```{r include = FALSE} +ndjson_filename <- tempfile() +df <- head( nycflights13::flights, 1000) +jsonlite::stream_out(df, file(ndjson_filename), verbose = FALSE) + +res06 <- bench::mark( + ndjson = ndjson::stream_in(ndjson_filename), + jsonlite = jsonlite::stream_in(file(ndjson_filename), verbose = FALSE), + jsonify = jsonify::from_ndjson(ndjson_filename), + # arrow = arrow::read_json_arrow(ndjson_filename), + yyjsonr = yyjsonr::read_ndjson_file (ndjson_filename), + # relative = TRUE, + check = FALSE +) +``` + +```{r echo=FALSE} +res06$benchmark <- 'From NDJSON File' +knitr::kable(res06[, 1:5]) +plot(res06) + theme_bw() + theme(legend.position = 'none') +``` + + +```{r echo=FALSE, eval=FALSE} +library(dplyr) +plot_df <- res06 %>% + mutate(package = as.character(expression)) %>% + rename(speed = `itr/sec`) + +ggplot(plot_df) + + geom_col(aes(package, speed, fill = package), + position = position_dodge2(preserve = "single")) + + theme_bw(20) + + theme(legend.position = 'none') + + scale_fill_manual(values = c(rep(grey(0.5), 4), 'dodgerblue3')) + + geom_hline(yintercept = 1, color = 'red', alpha = 0.5, linetype = 2) + + labs( + x = NULL, + y = "Factor speed increase over {jsonlite}", + title = "Reading NDJSON. Speed-up compared to {jsonlite}", + subtitle = "Red line indicates reference implementation {jsonlite}" + ) +``` + + + + +To NDJSON File +------------------------------------------------------------------------------- + + +```{r include = FALSE} +ndjson_filename <- tempfile() +df <- head( nycflights13::flights, 1000) + +res07 <- bench::mark( + jsonlite = jsonlite::stream_out(df, file(ndjson_filename), verbose = FALSE), + yyjsonr = yyjsonr::write_ndjson_file(df, ndjson_filename), + check = FALSE +) +``` + + +```{r echo=FALSE} +res07$benchmark <- 'To NDJSON File' +knitr::kable(res07[, 1:5]) +plot(res07) + theme_bw() + theme(legend.position = 'none') +``` + + +To NDJSON String +------------------------------------------------------------------------------- + + +```{r message=FALSE, include = FALSE} +ndjson_filename <- tempfile() +df <- head( nycflights13::flights, 1000) + +res08 <- bench::mark( + jsonify = jsonify::to_ndjson(df), + jsonlite = jsonlite::stream_out(df, con = textConnection(NULL, "w"), name = "greg", local = TRUE, verbose = FALSE), + yyjsonr = yyjsonr::write_ndjson_str(df), + check = FALSE +) +``` + +```{r echo=FALSE} +res08$benchmark <- 'To NDJSON String' +knitr::kable(res08[, 1:5]) +plot(res08) + theme_bw() + theme(legend.position = 'none') +``` + + + + + +Summary +=============================================================================== + +```{r echo = FALSE, fig.width = 8, fig.height = 6} +library(dplyr) +plot_df <- bind_rows( + res06, res07, res08 +) + +plot_df$benchmark <- factor( + plot_df$benchmark, + levels = unique(plot_df$benchmark) +) + +plot_df <- plot_df %>% + mutate( + package = as.character(expression), + iters = `itr/sec`, + speed = iters + ) %>% + select(benchmark, package, iters, speed) + +plot_df <- plot_df %>% + group_by(benchmark) %>% + mutate( + ref_speed = speed[which(package %in% c('jsonlite', 'geojsonsf'))], + speed = speed / ref_speed + ) %>% + ungroup() + +ggplot(plot_df) + + geom_col(aes(package, speed, fill = package), + position = position_dodge2(preserve = "single")) + + facet_wrap(~benchmark, scales = 'free_y', ncol = 3) + + theme_bw(15) + + theme(legend.position = 'none') + + scale_fill_manual(values = c(rep(grey(0.5), 3), 'dodgerblue3')) + + geom_hline(yintercept = 1, color = 'red', alpha = 0.5, linetype = 2) + + labs( + x = NULL, + y = "Factor speed increase\nover reference implementation", + title = "Speed-up compared to reference implementation", + subtitle = "Red line indicates reference implementation {jsonlite}" + ) + + scale_y_continuous(breaks = scales::pretty_breaks()) + +if (FALSE) { + ggsave("./man/figures/benchmark-ndjson.png", width = 9, height = 5) + # saveRDS(plot_df, "man/benchmark/cache-df-types.rds") +} +``` + + + diff --git a/man/figures/benchmark-geojson.png b/man/figures/benchmark-geojson.png new file mode 100644 index 0000000..0a591ad Binary files /dev/null and b/man/figures/benchmark-geojson.png differ diff --git a/man/figures/benchmark-ndjson.png b/man/figures/benchmark-ndjson.png new file mode 100644 index 0000000..e91cbbb Binary files /dev/null and b/man/figures/benchmark-ndjson.png differ diff --git a/man/figures/benchmark-summary.png b/man/figures/benchmark-summary.png index 233d29a..12a2311 100644 Binary files a/man/figures/benchmark-summary.png and b/man/figures/benchmark-summary.png differ diff --git a/man/opts_read_geojson.Rd b/man/opts_read_geojson.Rd index 6fc476b..f93912f 100644 --- a/man/opts_read_geojson.Rd +++ b/man/opts_read_geojson.Rd @@ -7,7 +7,7 @@ opts_read_geojson( type = c("sf", "sfc"), property_promotion = c("string", "list"), - property_promotion_lgl_as_int = c("integer", "string") + property_promotion_lgl = c("integer", "string") ) } \arguments{ @@ -17,15 +17,20 @@ opts_read_geojson( properties differ across a FEATURECOLLECTION? E.g. if the property exists both as a numeric and a string, should all values be promoted to a 'string', or contained as different types in a 'list'. -Default: 'string' will behave like 'geojsonsf'} +Default: 'string' will behave like \code{geojsonsf} package.} -\item{property_promotion_lgl_as_int}{when promoting properties into a string, -should logical values become strings e.g. "TRUE" or integers -e.g. "1". Default: "integer" in order to match \code{geojsonsf} packages} +\item{property_promotion_lgl}{when \code{property_promotion = "string"} +should logical values become words (i.e. \code{"TRUE"}/\code{"FALSE"}) +or integers (i.e. \code{"1"}/\code{"0"}). +Default: "integer" in order to match \code{geojsonsf} package} } \value{ -named list +Named list of options specific to reading GeoJSON } \description{ Options for reading in GeoJSON } +\examples{ +# Create a set of options to use when reading geojson +opts_read_geojson() +} diff --git a/man/opts_write_geojson.Rd b/man/opts_write_geojson.Rd index 4185595..e435790 100644 --- a/man/opts_write_geojson.Rd +++ b/man/opts_write_geojson.Rd @@ -7,8 +7,12 @@ opts_write_geojson() } \value{ -named list of options +Named list of options specific to writing GeoJSON } \description{ Currently no options available. } +\examples{ +# Create a set of options to use when writing geojson +opts_write_geojson() +} diff --git a/man/read_geojson_str.Rd b/man/read_geojson_str.Rd index 748110b..a71146c 100644 --- a/man/read_geojson_str.Rd +++ b/man/read_geojson_str.Rd @@ -5,19 +5,26 @@ \alias{read_geojson_file} \title{Load GeoJSON as \code{sf} object} \usage{ -read_geojson_str(str, opts = list(), ...) +read_geojson_str(str, opts = list(), ..., json_opts = list()) -read_geojson_file(filename, opts = list(), ...) +read_geojson_file(filename, opts = list(), ..., json_opts = list()) } \arguments{ -\item{str}{single character string containing GeoJSON} +\item{str}{Single string containing GeoJSON} -\item{opts}{named list of options. Usually created with \code{opts_read_geojson()}. +\item{opts}{Named list of GeoJSON-specific options. Usually created +with \code{opts_read_geojson()}. Default: empty \code{list()} to use the default options.} -\item{...}{any extra named options override those in \code{opts}} +\item{...}{Any extra named options override those in GeoJSON-specific options +- \code{opts}} -\item{filename}{filename} +\item{json_opts}{Named list of vanilla JSON options as used by \code{read_json_str()}. +This is usually created with \code{opts_read_json()}. Default value is +an empty \code{list()} which means to use all the default JSON parsing +options which is usually the correct thing to do when reading GeoJSON.} + +\item{filename}{Filename} } \value{ \code{sf} object @@ -25,3 +32,7 @@ Default: empty \code{list()} to use the default options.} \description{ Load GeoJSON as \code{sf} object } +\examples{ +geojson_file <- system.file("geojson-example.json", package = 'yyjsonr') +read_geojson_file(geojson_file) +} diff --git a/man/write_geojson_str.Rd b/man/write_geojson_str.Rd index 4475c07..f956c7d 100644 --- a/man/write_geojson_str.Rd +++ b/man/write_geojson_str.Rd @@ -5,9 +5,9 @@ \alias{write_geojson_file} \title{Write SF to GeoJSON string} \usage{ -write_geojson_str(x, opts = list(), ..., digits = -1) +write_geojson_str(x, opts = list(), ..., json_opts = list()) -write_geojson_file(x, filename, opts = list(), ..., digits = -1) +write_geojson_file(x, filename, opts = list(), ..., json_opts = list()) } \arguments{ \item{x}{\code{sf} object. Supports \code{sf} or \code{sfc}} @@ -17,16 +17,22 @@ Default: empty \code{list()} to use the default options.} \item{...}{any extra named options override those in \code{opts}} -\item{digits}{decimal places to keep for floating point numbers. Default: -1. -Positive values specify number of decimal places. Using zero will -write the numeric value as an integer. Values less than zero mean that -the floating point value should be written as-is (the default).} +\item{json_opts}{Named list of vanilla JSON options as used by \code{write_json_str()}. +This is usually created with \code{opts_write_json()}. Default value is +an empty \code{list()} which means to use all the default JSON writing +options which is usually the correct thing to do when writing GeoJSON.} \item{filename}{filename} } \value{ -character string containing json +Character string containing GeoJSON, or \code{NULL} if GeoJSON +written to file. } \description{ Write SF to GeoJSON string } +\examples{ +geojson_file <- system.file("geojson-example.json", package = 'yyjsonr') +sf <- read_geojson_file(geojson_file) +cat(write_geojson_str(sf, json_opts = opts_write_json(pretty = TRUE))) +} diff --git a/src/geojson-parse.c b/src/geojson-parse.c index 987107b..384cbdb 100644 --- a/src/geojson-parse.c +++ b/src/geojson-parse.c @@ -56,7 +56,7 @@ SEXP make_crs(void) { //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ typedef struct { bool property_promotion; - bool property_promotion_lgl_as_int; // When promoting properties to string, should lgl be '1' or 'TRUE"? + bool property_promotion_lgl; // When promoting properties to string, should lgl be '1' or 'TRUE"? unsigned int type; unsigned int yyjson_read_flag; parse_options *parse_opt; @@ -76,7 +76,7 @@ typedef struct { geo_parse_options create_geo_parse_options(SEXP geo_opts_) { geo_parse_options opt = { .property_promotion = PROP_TYPE_STRING, // emulate 'geojsonsf' behaviour - .property_promotion_lgl_as_int = PROP_LGL_AS_INT, // emulate 'geojsonsf' behaviour + .property_promotion_lgl = PROP_LGL_AS_INT, // emulate 'geojsonsf' behaviour .type = SF_TYPE, .yyjson_read_flag = 0, .xmin = INFINITY, @@ -109,14 +109,14 @@ geo_parse_options create_geo_parse_options(SEXP geo_opts_) { if (strcmp(opt_name, "property_promotion") == 0) { const char *val = CHAR(STRING_ELT(val_, 0)); opt.property_promotion = strcmp(val, "string") == 0 ? PROP_TYPE_STRING : PROP_TYPE_LIST; - } else if (strcmp(opt_name, "property_promotion_lgl_as_int") == 0) { + } else if (strcmp(opt_name, "property_promotion_lgl") == 0) { const char *val = CHAR(STRING_ELT(val_, 0)); - opt.property_promotion_lgl_as_int = strcmp(val, "string") == 0 ? PROP_LGL_AS_STR : PROP_LGL_AS_INT; + opt.property_promotion_lgl = strcmp(val, "string") == 0 ? PROP_LGL_AS_STR : PROP_LGL_AS_INT; } else if (strcmp(opt_name, "type") == 0) { const char *val = CHAR(STRING_ELT(val_, 0)); opt.type = strcmp(val, "sf") == 0 ? SF_TYPE : SFC_TYPE; } else { - warning("geo_opt: Unknown option ignored: '%s'\n", opt_name); + warning("opt_geojson_read(): Unknown option ignored: '%s'\n", opt_name); } } @@ -622,7 +622,7 @@ SEXP prop_to_rchar(yyjson_val *prop_val, geo_parse_options *opt) { break; case YYJSON_TYPE_BOOL: { - if (opt->property_promotion_lgl_as_int == PROP_LGL_AS_INT) { + if (opt->property_promotion_lgl == PROP_LGL_AS_INT) { int tmp = yyjson_get_bool(prop_val); return mkChar(bool_int[tmp]); } else { diff --git a/src/geojson-serialize.c b/src/geojson-serialize.c index 4d76448..af81e0f 100644 --- a/src/geojson-serialize.c +++ b/src/geojson-serialize.c @@ -19,7 +19,6 @@ // - current bounding box accumulations //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ typedef struct { - unsigned int yyjson_write_flag; serialize_options *serialize_opt; } geo_serialize_options; @@ -28,7 +27,6 @@ typedef struct { //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ geo_serialize_options create_geo_serialize_options(SEXP to_geo_opts_) { geo_serialize_options opt = { - .yyjson_write_flag = YYJSON_WRITE_NOFLAG, .serialize_opt = NULL }; @@ -47,24 +45,8 @@ geo_serialize_options create_geo_serialize_options(SEXP to_geo_opts_) { for (unsigned int i = 0; i < length(to_geo_opts_); i++) { const char *opt_name = CHAR(STRING_ELT(nms_, i)); - SEXP val_ = VECTOR_ELT(to_geo_opts_, i); - - // if (strcmp(opt_name, "property_promotion") == 0) { - // const char *val = CHAR(STRING_ELT(val_, 0)); - // opt.property_promotion = strcmp(val, "string") == 0 ? PROP_TYPE_STRING : PROP_TYPE_LIST; - // } else if (strcmp(opt_name, "property_promotion_lgl_as_int") == 0) { - // const char *val = CHAR(STRING_ELT(val_, 0)); - // opt.property_promotion_lgl_as_int = strcmp(val, "string") == 0 ? PROP_LGL_AS_STR : PROP_LGL_AS_INT; - // } else if (strcmp(opt_name, "type") == 0) { - // const char *val = CHAR(STRING_ELT(val_, 0)); - // opt.type = strcmp(val, "sf") == 0 ? SF_TYPE : SFC_TYPE; - if (strcmp(opt_name, "pretty") == 0) { - if (asLogical(val_)) { - opt.yyjson_write_flag |= YYJSON_WRITE_PRETTY_TWO_SPACES; - } - } else { - warning("geo_opt: Unknown option ignored: '%s'\n", opt_name); - } + // SEXP val_ = VECTOR_ELT(to_geo_opts_, i); + warning("opt_geojson_write(): Unknown option ignored: '%s'\n", opt_name); } return opt; } @@ -148,7 +130,7 @@ SEXP sfc_to_str(SEXP sfc_, geo_serialize_options *opt) { yyjson_mut_doc_set_root(doc, val); yyjson_write_err err; - char *json = yyjson_mut_write_opts(doc, opt->yyjson_write_flag, NULL, NULL, &err); + char *json = yyjson_mut_write_opts(doc, opt->serialize_opt->yyjson_write_flag, NULL, NULL, &err); if (json == NULL) { yyjson_mut_doc_free(doc); error("Write to string error: %s code: %u\n", err.msg, err.code); @@ -251,7 +233,7 @@ SEXP sf_to_str(SEXP sf_, geo_serialize_options *opt) { // Write the doc to a string //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ yyjson_write_err err; - char *json = yyjson_mut_write_opts(doc, opt->yyjson_write_flag, NULL, NULL, &err); + char *json = yyjson_mut_write_opts(doc, opt->serialize_opt->yyjson_write_flag, NULL, NULL, &err); if (json == NULL) { yyjson_mut_doc_free(doc); error("serialize_sf() Write to string error: %s code: %u\n", err.msg, err.code); @@ -278,7 +260,7 @@ SEXP sf_to_file(SEXP sf_, SEXP filename_, geo_serialize_options *opt) { //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ const char *filename = CHAR(STRING_ELT(filename_, 0)); yyjson_write_err err; - bool success = yyjson_mut_write_file(filename, doc, opt->yyjson_write_flag, NULL, &err); + bool success = yyjson_mut_write_file(filename, doc, opt->serialize_opt->yyjson_write_flag, NULL, &err); if (!success) { yyjson_mut_doc_free(doc); error("Write to file error '%s': %s code: %u\n", filename, err.msg, err.code); diff --git a/tests/testthat/geojson/standard-example.json.gz b/tests/testthat/geojson/standard-example.json.gz new file mode 100644 index 0000000..ed6d041 Binary files /dev/null and b/tests/testthat/geojson/standard-example.json.gz differ diff --git a/tests/testthat/test-basic.R b/tests/testthat/test-basic.R index fa6be87..6c3b5a1 100644 --- a/tests/testthat/test-basic.R +++ b/tests/testthat/test-basic.R @@ -58,6 +58,11 @@ test_that("write_json_str works", { # Vectors with specials + + # Version string is sensible + version <- yyjson_version() + expect_length(version, 1) + expect_true(is.character(version)) }) diff --git a/tests/testthat/test-geojson-read-write.R b/tests/testthat/test-geojson-read-write.R index c1a785e..98cfb94 100644 --- a/tests/testthat/test-geojson-read-write.R +++ b/tests/testthat/test-geojson-read-write.R @@ -97,6 +97,8 @@ test_that("Compare parsing of GeoJSON to {geojsonsf}", { #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test_that("Basic writing works for lots of different geojson", { + tmp <- tempfile() + for (i in seq_along(json)) { # Take the JSON @@ -113,8 +115,63 @@ test_that("Basic writing works for lots of different geojson", { # Test 'sf' representations are equal expect_identical(geojson1, geojson2, label = names(json)[i]) + + # write the json to file + write_geojson_file(geojson1, tmp) + + geojson3 <- read_geojson_file(tmp) + + expect_identical(geojson1, geojson3) } }) +test_that("opts", { + + opts <- opts_read_geojson() + expect_true(is.list(opts)) + expect_true(inherits(opts, "opts_read_geojson")) + + opts <- opts_write_geojson() + expect_true(is.list(opts)) + expect_true(inherits(opts, "opts_write_geojson")) + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # geojson read options + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + geo <- read_geojson_file( + testthat::test_path("geojson/standard-example.json"), + opts = opts_read_geojson(property_promotion = 'string') + ) + expect_true(is.character(geo$prop1)) + + geo <- read_geojson_file( + testthat::test_path("geojson/standard-example.json"), + opts = opts_read_geojson(property_promotion = 'list') + ) + expect_true(is.list(geo$prop1)) + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # unknown options spark warning + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + expect_warning( + write_geojson_str(geo, bad = TRUE), + "Unknown option ignored" + ) + + expect_warning( + write_geojson_str(geo, pretty = TRUE), + "Unknown option ignored" + ) + + if (FALSE) { + write_geojson_str(geo, json_opts = opts_write_json(pretty = TRUE)) |> cat() + } + + +}) + + + +