diff --git a/DESCRIPTION b/DESCRIPTION index 5a40a2233..0ac1b030f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: googlePolylines Type: Package Title: Encoding Coordinates into 'Google' Polylines -Version: 0.8.8 +Version: 0.8.8.9000 Date: 2025-07-26 Authors@R: c( person("David", "Cooley", ,"dcooley@symbolix.com.au", role = c("aut", "cre")), @@ -17,7 +17,7 @@ Imports: Rcpp (>= 1.0.10) LinkingTo: Rcpp -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.1 Suggests: covr, knitr, diff --git a/NEWS.md b/NEWS.md index ce1cce9fb..2722caacd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,11 @@ +# v0.8.8.9000 + +- coordinates with missing or non-finite values (`NA`, `NaN`, `Inf`) are now + skipped during encoding, with a warning. Previously they corrupted the whole + polyline with invalid characters (undefined behaviour in the C++ code). + With `byrow = TRUE`, such rows now return `NA`. Empty `sf` points are no + longer encoded as `"??"` (which decodes to the real coordinate `(0,0)`) + # v0.8.8 - added `precision` argument to `decode()` [issue 17](https://github.com/SymbolixAU/googlePolylines/issues/17) diff --git a/R/Decode.R b/R/Decode.R index 8fab6b043..815035df0 100644 --- a/R/Decode.R +++ b/R/Decode.R @@ -3,8 +3,9 @@ #' Decodes encoded polylines into a list of data.frames. #' #' @param polylines vector of encoded polyline strings -#' @param precision -#' +#' @param precision number of decimal places the coordinates were encoded +#' with (5 is the precision used by the Google polyline encoding algorithm) +#' #' @examples #' polylines <- c( #' "ohlbDnbmhN~suq@am{tAw`qsAeyhGvkz`@fge}A", diff --git a/R/Encode.R b/R/Encode.R index 784d9d640..76283f514 100644 --- a/R/Encode.R +++ b/R/Encode.R @@ -70,7 +70,12 @@ #' #' @note When encoding an \code{sf} object, only the XY dimensions will be used, #' the Z or M (3D and/or Measure) dimensions are dropped. -#' +#' +#' @note Coordinates with missing or non-finite values (\code{NA}, \code{NaN}, +#' \code{Inf}) cannot be represented in a polyline. They are skipped during +#' encoding and a warning is issued. With \code{byrow = TRUE}, rows with such +#' coordinates return \code{NA} instead of being skipped. +#' #' @seealso \link{encodeCoordinates} #' #' @export @@ -196,8 +201,12 @@ encode.default <- function(obj, ...) { #' #' } #' +#' @note Coordinates with missing or non-finite values (\code{NA}, \code{NaN}, +#' \code{Inf}) cannot be represented in a polyline. They are skipped during +#' encoding and a warning is issued. +#' #' @seealso \link{encode} -#' +#' #' @export encodeCoordinates <- function(lon, lat) rcpp_encode_polyline(lon, lat) diff --git a/inst/i/googlePolylines.h b/inst/i/googlePolylines.h index 48da02230..b54e90198 100644 --- a/inst/i/googlePolylines.h +++ b/inst/i/googlePolylines.h @@ -57,6 +57,10 @@ void EncodeSignedNumber(std::ostringstream& os, int num); std::string encode_polyline(); +void reset_skipped_points(); + +void warn_skipped_points(); + Rcpp::List decode_data(Rcpp::StringVector pl, const char *cls = NULL); @@ -66,6 +70,7 @@ namespace global_vars { extern std::vector lats; extern std::string encodedString; extern std::vector elems; + extern R_xlen_t skipped_points; } #endif diff --git a/man/decode.Rd b/man/decode.Rd index a57e58a45..bbd4f05c4 100644 --- a/man/decode.Rd +++ b/man/decode.Rd @@ -4,10 +4,13 @@ \alias{decode} \title{Decode Polyline} \usage{ -decode(polylines) +decode(polylines, precision = 5) } \arguments{ \item{polylines}{vector of encoded polyline strings} + +\item{precision}{number of decimal places the coordinates were encoded +with (5 is the precision used by the Google polyline encoding algorithm)} } \description{ Decodes encoded polylines into a list of data.frames. diff --git a/man/encode.Rd b/man/encode.Rd index 09079342a..b61f0847b 100644 --- a/man/encode.Rd +++ b/man/encode.Rd @@ -52,6 +52,11 @@ attributes are dropped by default. See examples. When encoding an \code{sf} object, only the XY dimensions will be used, the Z or M (3D and/or Measure) dimensions are dropped. + +Coordinates with missing or non-finite values (\code{NA}, \code{NaN}, +\code{Inf}) cannot be represented in a polyline. They are skipped during +encoding and a warning is issued. With \code{byrow = TRUE}, rows with such +coordinates return \code{NA} instead of being skipped. } \examples{ diff --git a/man/encodeCoordinates.Rd b/man/encodeCoordinates.Rd index dabdf6cdd..8925b76ff 100644 --- a/man/encodeCoordinates.Rd +++ b/man/encodeCoordinates.Rd @@ -14,6 +14,11 @@ encodeCoordinates(lon, lat) \description{ Encodes a vector of lon & lat coordinates } +\note{ +Coordinates with missing or non-finite values (\code{NA}, \code{NaN}, +\code{Inf}) cannot be represented in a polyline. They are skipped during +encoding and a warning is issued. +} \examples{ \dontrun{ diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp index b8275b63f..59691463f 100644 --- a/src/RcppExports.cpp +++ b/src/RcppExports.cpp @@ -61,7 +61,7 @@ BEGIN_RCPP END_RCPP } // rcpp_encode_polyline_byrow -std::vector rcpp_encode_polyline_byrow(Rcpp::NumericVector longitude, Rcpp::NumericVector latitude); +Rcpp::StringVector rcpp_encode_polyline_byrow(Rcpp::NumericVector longitude, Rcpp::NumericVector latitude); RcppExport SEXP _googlePolylines_rcpp_encode_polyline_byrow(SEXP longitudeSEXP, SEXP latitudeSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; diff --git a/src/encode.cpp b/src/encode.cpp index 16ceefc94..6decb9eb2 100644 --- a/src/encode.cpp +++ b/src/encode.cpp @@ -301,6 +301,7 @@ Rcpp::List rcpp_encodeSfGeometry(Rcpp::List sfc, bool strip){ Rcpp::List output(sfc.size()); //Rcpp::List output_zm(sfc.size()); + reset_skipped_points(); int lastItem; Rcpp::List thisSfc; std::string str; @@ -360,5 +361,6 @@ Rcpp::List rcpp_encodeSfGeometry(Rcpp::List sfc, bool strip){ // _["XY"] = output // _["ZM"] = output_zm // ); + warn_skipped_points(); return output; } \ No newline at end of file diff --git a/src/googlePolylines.cpp b/src/googlePolylines.cpp index 22eb72dbd..d4d5b1daa 100644 --- a/src/googlePolylines.cpp +++ b/src/googlePolylines.cpp @@ -1,4 +1,5 @@ #include +#include #include "googlePolylines.h" using namespace Rcpp; @@ -8,6 +9,21 @@ namespace global_vars { std::vector lats; std::string encodedString; std::vector elems; + R_xlen_t skipped_points = 0; +} + +void reset_skipped_points() { + global_vars::skipped_points = 0; +} + +void warn_skipped_points() { + if (global_vars::skipped_points > 0) { + std::ostringstream msg; + msg << global_vars::skipped_points << + " coordinate pair(s) with missing or non-finite (NA/NaN/Inf) values" << + " could not be encoded"; + Rcpp::warning(msg.str()); + } } // [[Rcpp::export]] @@ -207,7 +223,15 @@ std::string encode_polyline(){ std::ostringstream os; for(unsigned int i = 0; i < global_vars::lats.size(); i++){ - + + // NA/NaN/Inf coordinates cannot be represented in a polyline: the + // double -> int cast below is undefined behaviour for them and (in + // practice) corrupts the whole string with out-of-range characters + if (!std::isfinite(global_vars::lats[i]) || !std::isfinite(global_vars::lons[i])) { + global_vars::skipped_points++; + continue; + } + late5 = global_vars::lats[i] * 1e5; lone5 = global_vars::lons[i] * 1e5; @@ -228,29 +252,41 @@ std::string rcpp_encode_polyline( ) { global_vars::lons = longitude; global_vars::lats = latitude; - return encode_polyline(); + reset_skipped_points(); + std::string res = encode_polyline(); + warn_skipped_points(); + return res; } // [[Rcpp::export]] -std::vector rcpp_encode_polyline_byrow( +Rcpp::StringVector rcpp_encode_polyline_byrow( Rcpp::NumericVector longitude, Rcpp::NumericVector latitude - ) { - - size_t n = longitude.length(); - std::vector res; + ) { + + R_xlen_t n = longitude.length(); + Rcpp::StringVector res(n); global_vars::lons.clear(); global_vars::lons.resize(1); global_vars::lats.clear(); global_vars::lats.resize(1); - - for ( size_t i = 0; i < n; i++ ) { + reset_skipped_points(); + + for ( R_xlen_t i = 0; i < n; i++ ) { + + // a row without finite coordinates has no polyline representation + if (!std::isfinite(longitude[i]) || !std::isfinite(latitude[i])) { + global_vars::skipped_points++; + res[i] = NA_STRING; + continue; + } global_vars::lons[0] = longitude[i]; global_vars::lats[0] = latitude[i]; - res.push_back(encode_polyline()); + res[i] = encode_polyline(); } + warn_skipped_points(); return res; } diff --git a/tests/testthat/test-Encode.R b/tests/testthat/test-Encode.R index 429775621..59875ae31 100644 --- a/tests/testthat/test-Encode.R +++ b/tests/testthat/test-Encode.R @@ -416,10 +416,12 @@ test_that("emptry geometries are handled", { empl <- sf::st_sfc(sf::st_multipolygon()) sfempl <- sf::st_sf(geometry = empl) - enc <- encode(ept) - expect_true(enc[[1]] == "??") - enc <- encode(sfept) - expect_true(enc$geometry[[1]] == "??") + ## empty points used to encode as "??", which decodes to the real + ## coordinate (0,0); they are now skipped (with a warning) instead + expect_warning(enc <- encode(ept), "could not be encoded") + expect_true(enc[[1]] == "") + expect_warning(enc <- encode(sfept), "could not be encoded") + expect_true(enc$geometry[[1]] == "") enc <- encode(emp) expect_true(length(enc[[1]]) == 0) @@ -446,3 +448,63 @@ test_that("emptry geometries are handled", { enc <- encode( sfempl ) expect_true(length(enc$geometry[[1]]) == 0) }) + + +test_that("missing and non-finite coordinates are skipped with a warning", { + + df <- data.frame(lat = c(38.5, 40.7, 43.252), lon = c(-120.2, -120.95, -126.453)) + df_nan <- df + df_nan[2, c("lat", "lon")] <- NaN + + ## the NaN pair is dropped, the remaining points encode as if it was not there + expect_warning( + enc <- encode(df_nan), + "1 coordinate pair\\(s\\) with missing or non-finite \\(NA/NaN/Inf\\) values could not be encoded" + ) + expect_equal(enc, encode(df[-2, ])) + + ## NA and Inf are treated the same way + df_na <- df + df_na$lat[1] <- NA + df_na$lon[3] <- Inf + expect_warning(enc_na <- encode(df_na), "2 coordinate pair\\(s\\)") + expect_equal(enc_na, encode(df[2, ])) + + ## nothing encodable left + expect_warning(enc_empty <- encode(data.frame(lat = NaN, lon = NaN))) + expect_equal(enc_empty, "") + + ## encodeCoordinates() goes through the same path + expect_warning( + enc_coords <- encodeCoordinates(lon = df_nan$lon, lat = df_nan$lat), + "could not be encoded" + ) + expect_equal(enc_coords, encode(df[-2, ])) + + ## no warning when everything is finite + expect_silent(encode(df)) +}) + +test_that("byrow encoding returns NA for missing coordinates", { + + df <- data.frame(lat = c(38, NaN, 43), lon = c(-120, NaN, -126)) + expect_warning(enc <- encode(df, byrow = TRUE), "could not be encoded") + expect_length(enc, 3) + expect_true(is.na(enc[2])) + expect_equal(enc[c(1, 3)], encode(df[c(1, 3), ], byrow = TRUE)) + + ## NA rows decode back to NA coordinates + dec <- decode(enc) + expect_true(is.na(dec[[2]]$lat) && is.na(dec[[2]]$lon)) +}) + +test_that("empty sf POINT encodes to an empty string with a warning", { + + testthat::skip_on_cran() + library(sf) + + pts <- sf::st_sfc(sf::st_point(), sf::st_point(c(144, -37))) + expect_warning(enc <- encode(pts), "could not be encoded") + expect_equal(unclass(enc[[1]])[1], "") + expect_equal(unclass(enc[[2]])[1], "~py`F__|mZ") +})