Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -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")),
Expand All @@ -17,7 +17,7 @@ Imports:
Rcpp (>= 1.0.10)
LinkingTo:
Rcpp
RoxygenNote: 7.2.3
RoxygenNote: 7.3.1
Suggests:
covr,
knitr,
Expand Down
8 changes: 8 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
5 changes: 3 additions & 2 deletions R/Decode.R
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 11 additions & 2 deletions R/Encode.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

5 changes: 5 additions & 0 deletions inst/i/googlePolylines.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -66,6 +70,7 @@ namespace global_vars {
extern std::vector<double> lats;
extern std::string encodedString;
extern std::vector<std::string> elems;
extern R_xlen_t skipped_points;
}

#endif
5 changes: 4 additions & 1 deletion man/decode.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions man/encode.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions man/encodeCoordinates.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/RcppExports.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ BEGIN_RCPP
END_RCPP
}
// rcpp_encode_polyline_byrow
std::vector<std::string> 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;
Expand Down
2 changes: 2 additions & 0 deletions src/encode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -360,5 +361,6 @@ Rcpp::List rcpp_encodeSfGeometry(Rcpp::List sfc, bool strip){
// _["XY"] = output
// _["ZM"] = output_zm
// );
warn_skipped_points();
return output;
}
56 changes: 46 additions & 10 deletions src/googlePolylines.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <Rcpp.h>
#include <cmath>
#include "googlePolylines.h"

using namespace Rcpp;
Expand All @@ -8,6 +9,21 @@ namespace global_vars {
std::vector<double> lats;
std::string encodedString;
std::vector<std::string> 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]]
Expand Down Expand Up @@ -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;

Expand All @@ -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<std::string> rcpp_encode_polyline_byrow(
Rcpp::StringVector rcpp_encode_polyline_byrow(
Rcpp::NumericVector longitude,
Rcpp::NumericVector latitude
) {
size_t n = longitude.length();
std::vector<std::string> 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;
}

70 changes: 66 additions & 4 deletions tests/testthat/test-Encode.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
})