Skip to content

Commit 7c0ab61

Browse files
authored
Handle xy(NA, NA) as null instead of empty (#205)
* xy NA NA as empty * writer/handler consistency for xy * with maybe working nullability/empty split in xy * fix the vertex filter * add example * fix some rough edges * stronger NA/NaN testing for sfc * fix NA/NaN for custom conversion to xy * fix copy/paste error
1 parent 886a36d commit 7c0ab61

15 files changed

+108
-100
lines changed

NAMESPACE

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ S3method(grd_tile,wk_grd_rct)
9292
S3method(grd_tile,wk_grd_xy)
9393
S3method(is.na,wk_rcrd)
9494
S3method(is.na,wk_wkb)
95+
S3method(is.na,wk_xy)
9596
S3method(length,wk_rcrd)
9697
S3method(names,wk_rcrd)
9798
S3method(plot,wk_crc)

R/pkg-sf.R

+5-1
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ as_xy.sfc <- function(x, ...) {
163163
coords <- sf::st_coordinates(x)
164164
dims <- colnames(coords)
165165
dimnames(coords) <- NULL
166+
if (anyNA(coords)) {
167+
coords[is.na(coords)] <- NaN
168+
}
166169

167170
if (identical(dims, c("X", "Y"))) {
168171
new_wk_xy(
@@ -263,7 +266,8 @@ st_as_sfc.wk_xy <- function(x, ...) {
263266
}
264267

265268
st_as_sf.wk_xy <- function(x, ...) {
266-
if ((length(x) > 0) && all(!is.na(x))) {
269+
is_na_or_nan <- Reduce("&", lapply(unclass(x), is.na))
270+
if ((length(x) > 0) && all(!is_na_or_nan)) {
267271
sf::st_as_sf(as.data.frame(x), coords = xy_dims(x), crs = sf_crs_from_wk(x))
268272
} else {
269273
sf::st_as_sf(

R/utils.R

-6
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,3 @@ recycle_common <- function(...) {
3434
is_vector_class <- function(x) {
3535
identical(class(x[integer(0)]), class(x))
3636
}
37-
38-
# This helps when comparing with sf package objects, which tend to use
39-
# NA rather than NaN.
40-
expect_equal_ignore_na_nan <- function(actual, expected) {
41-
testthat::expect_true(all.equal(actual, expected))
42-
}

R/vertex-filter.R

+5-4
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,12 @@ wk_vertex_filter <- function(handler, add_details = FALSE) {
9494
#' @export
9595
wk_coords.wk_xy <- function(handleable, ...) {
9696
feature_id <- seq_along(handleable)
97-
not_empty <- !is.na(handleable)
97+
is_na <- Reduce("&", lapply(unclass(handleable), is.na))
98+
has_coord <- !is_na
9899

99-
if (!all(not_empty)) {
100-
handleable <- handleable[not_empty]
101-
feature_id <- feature_id[not_empty]
100+
if (!all(has_coord)) {
101+
handleable <- handleable[has_coord]
102+
feature_id <- feature_id[has_coord]
102103
}
103104

104105
new_data_frame(

R/xyzm.R

+11
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
#' xym(1:5, 1:5, 10)
1616
#' xyzm(1:5, 1:5, 10, 12)
1717
#'
18+
#' # NA, NA maps to a null/na feature; NaN, NaN maps to EMPTY
19+
#' as_wkt(xy(NaN, NaN))
20+
#' as_wkt(xy(NA, NA))
21+
#'
1822
xy <- function(x = double(), y = double(), crs = wk_crs_auto()) {
1923
vec <- new_wk_xy(recycle_common(x = as.double(x), y = as.double(y)), crs = wk_crs_auto_value(x, crs))
2024
validate_wk_xy(vec)
@@ -309,6 +313,13 @@ format.wk_xyzm <- function(x, ...) {
309313
structure(result, class = class(x), crs = wk_crs_output(x, replacement))
310314
}
311315

316+
#' @export
317+
is.na.wk_xy <- function(x, ...) {
318+
is_na <- Reduce("&", lapply(unclass(x), is.na))
319+
is_nan <- Reduce("&", lapply(unclass(x), is.nan))
320+
is_na & !is_nan
321+
}
322+
312323

313324
#' XY vector extractors
314325
#'

man/xy.Rd

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/handle-xy.c

+14-11
Original file line numberDiff line numberDiff line change
@@ -66,29 +66,32 @@ SEXP wk_read_xy(SEXP data, wk_handler_t* handler) {
6666
#endif
6767

6868
int coord_empty = 1;
69+
int coord_null = 1;
6970
for (int j = 0; j < coord_size; j++) {
7071
coord[j] = data_ptr[j][data_ptr_i];
7172
meta.bounds_min[j] = data_ptr[j][data_ptr_i];
7273
meta.bounds_max[j] = data_ptr[j][data_ptr_i];
7374

74-
if (!ISNAN(coord[j])) {
75-
coord_empty = 0;
76-
}
75+
coord_null = coord_null && ISNA(coord[j]);
76+
coord_empty = coord_empty && ISNAN(coord[j]);
7777
}
7878

79-
if (coord_empty) {
79+
if (coord_null) {
80+
HANDLE_CONTINUE_OR_BREAK(handler->null_feature(handler->handler_data));
81+
} else if (coord_empty) {
8082
meta.size = 0;
83+
HANDLE_CONTINUE_OR_BREAK(
84+
handler->geometry_start(&meta, WK_PART_ID_NONE, handler->handler_data));
85+
HANDLE_CONTINUE_OR_BREAK(
86+
handler->geometry_end(&meta, WK_PART_ID_NONE, handler->handler_data));
8187
} else {
8288
meta.size = 1;
83-
}
84-
85-
HANDLE_CONTINUE_OR_BREAK(
86-
handler->geometry_start(&meta, WK_PART_ID_NONE, handler->handler_data));
87-
if (!coord_empty) {
89+
HANDLE_CONTINUE_OR_BREAK(
90+
handler->geometry_start(&meta, WK_PART_ID_NONE, handler->handler_data));
8891
HANDLE_CONTINUE_OR_BREAK(handler->coord(&meta, coord, 0, handler->handler_data));
92+
HANDLE_CONTINUE_OR_BREAK(
93+
handler->geometry_end(&meta, WK_PART_ID_NONE, handler->handler_data));
8994
}
90-
HANDLE_CONTINUE_OR_BREAK(
91-
handler->geometry_end(&meta, WK_PART_ID_NONE, handler->handler_data));
9295

9396
if (handler->feature_end(&vector_meta, i, handler->handler_data) == WK_ABORT) {
9497
break;

src/vertex-filter.c

-6
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,6 @@ int wk_vertex_filter_feature_start(const wk_vector_meta_t* meta, R_xlen_t feat_i
128128
return WK_CONTINUE;
129129
}
130130

131-
int wk_vertex_filter_feature_null(void* handler_data) {
132-
vertex_filter_t* vertex_filter = (vertex_filter_t*)handler_data;
133-
return vertex_filter->next->null_feature(vertex_filter->next->handler_data);
134-
}
135-
136131
int wk_vertex_filter_feature_end(const wk_vector_meta_t* meta, R_xlen_t feat_id,
137132
void* handler_data) {
138133
return WK_CONTINUE;
@@ -234,7 +229,6 @@ SEXP wk_c_vertex_filter_new(SEXP handler_xptr, SEXP add_details) {
234229
handler->vector_end = &wk_vertex_filter_vector_end;
235230

236231
handler->feature_start = &wk_vertex_filter_feature_start;
237-
handler->null_feature = &wk_vertex_filter_feature_null;
238232
handler->feature_end = &wk_vertex_filter_feature_end;
239233

240234
handler->geometry_start = &wk_vertex_filter_geometry_start;

src/xy-writer.c

+13-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ static inline void xy_writer_append_empty(xy_writer_t* writer) {
8282

8383
for (int i = 0; i < 4; i++) {
8484
if (writer->result_ptr[i]) {
85-
writer->result_ptr[i][writer->feat_id] = NA_REAL;
85+
writer->result_ptr[i][writer->feat_id] = R_NaN;
8686
}
8787
}
8888
writer->feat_id++;
@@ -135,6 +135,17 @@ int xy_writer_feature_start(const wk_vector_meta_t* meta, R_xlen_t feat_id,
135135
return WK_CONTINUE;
136136
}
137137

138+
int xy_writer_null_feature(void* handler_data) {
139+
xy_writer_t* data = (xy_writer_t*)handler_data;
140+
for (int i = 0; i < 4; i++) {
141+
if (data->result_ptr[i]) {
142+
data->result_ptr[i][data->feat_id - 1] = NA_REAL;
143+
}
144+
}
145+
146+
return WK_ABORT_FEATURE;
147+
}
148+
138149
int xy_writer_geometry_start(const wk_meta_t* meta, uint32_t part_id,
139150
void* handler_data) {
140151
xy_writer_t* data = (xy_writer_t*)handler_data;
@@ -289,6 +300,7 @@ SEXP wk_c_xy_writer_new(void) {
289300

290301
handler->vector_start = &xy_writer_vector_start;
291302
handler->feature_start = &xy_writer_feature_start;
303+
handler->null_feature = &xy_writer_null_feature;
292304
handler->geometry_start = &xy_writer_geometry_start;
293305
handler->coord = &xy_writer_coord;
294306
handler->vector_end = &xy_writer_vector_end;

tests/testthat/test-handle-xy.R

+17-8
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
11

22
test_that("wk_handle.wk_xy() works", {
33
expect_identical(
4-
wk_handle(xy(c(NA, 2, 3, NA), c(NA, NA, 4, 5)), wkt_writer()),
5-
wkt(c("POINT EMPTY", "POINT (2 nan)", "POINT (3 4)", "POINT (nan 5)"))
4+
wk_handle(xy(c(NA, NaN, 2, 3, NA), c(NA, NaN, NA, 4, 5)), wkt_writer()),
5+
wkt(c(NA, "POINT EMPTY", "POINT (2 nan)", "POINT (3 4)", "POINT (nan 5)"))
66
)
77

88
expect_identical(
9-
wk_handle(xyz(c(NA, 2, 3, NA), c(NA, NA, 4, 5), c(NA, NA, NA, NA)), wkt_writer()),
10-
wkt(c("POINT Z EMPTY", "POINT Z (2 nan nan)", "POINT Z (3 4 nan)", "POINT Z (nan 5 nan)"))
9+
wk_handle(
10+
xyz(c(NA, NaN, 2, 3, NA), c(NA, NaN, NA, 4, 5), c(NA, NaN, NA, NA, NA)),
11+
wkt_writer()
12+
),
13+
wkt(c(NA, "POINT Z EMPTY", "POINT Z (2 nan nan)", "POINT Z (3 4 nan)", "POINT Z (nan 5 nan)"))
1114
)
1215

1316
expect_identical(
14-
wk_handle(xym(c(NA, 2, 3, NA), c(NA, NA, 4, 5), c(NA, NA, NA, NA)), wkt_writer()),
15-
wkt(c("POINT M EMPTY", "POINT M (2 nan nan)", "POINT M (3 4 nan)", "POINT M (nan 5 nan)"))
17+
wk_handle(
18+
xym(c(NA, NaN, 2, 3, NA), c(NA, NaN, NA, 4, 5), c(NA, NaN, NA, NA, NA)),
19+
wkt_writer()
20+
),
21+
wkt(c(NA, "POINT M EMPTY", "POINT M (2 nan nan)", "POINT M (3 4 nan)", "POINT M (nan 5 nan)"))
1622
)
1723

1824
expect_identical(
19-
wk_handle(xyzm(c(NA, 2, 3, NA), c(NA, NA, 4, 5), c(NA, NA, NA, NA), c(NA, rep(1, 3))), wkt_writer()),
20-
wkt(c("POINT ZM EMPTY", "POINT ZM (2 nan nan 1)", "POINT ZM (3 4 nan 1)", "POINT ZM (nan 5 nan 1)"))
25+
wk_handle(
26+
xyzm(c(NA, NaN, 2, 3, NA), c(NA, NaN, NA, 4, 5), c(NA, NaN, NA, NA, NA), c(NA, NaN, rep(1, 3))),
27+
wkt_writer()
28+
),
29+
wkt(c(NA, "POINT ZM EMPTY", "POINT ZM (2 nan nan 1)", "POINT ZM (3 4 nan 1)", "POINT ZM (nan 5 nan 1)"))
2130
)
2231
})

tests/testthat/test-pkg-sf.R

+14-8
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,16 @@ test_that("conversion from sf to xy works", {
9494

9595
sfc <- sf::st_sfc(sf::st_point(), sf::st_point(c(0, 1)))
9696
expect_s3_class(as_xy(sfc), "wk_xy")
97-
expect_identical(as_xy(sfc), xy(c(NA, 0), c(NA, 1)))
97+
expect_identical(as_xy(sfc), xy(c(NaN, 0), c(NaN, 1)))
9898

9999
sf <- sf::st_as_sf(new_data_frame(list(geometry = sfc)))
100-
expect_identical(as_xy(sf), xy(c(NA, 0), c(NA, 1)))
100+
expect_identical(as_xy(sf), xy(c(NaN, 0), c(NaN, 1)))
101101

102102
expect_identical(as_xy(sf::st_sfc()), xy(crs = NULL))
103-
expect_identical(as_xy(sf::st_sfc(sf::st_linestring())), xy(NA, NA, crs = sf::NA_crs_))
103+
expect_identical(
104+
as_xy(sf::st_sfc(sf::st_linestring())),
105+
xy(NaN, NaN, crs = sf::NA_crs_)
106+
)
104107

105108
# check all dimensions
106109
expect_identical(as_xy(sf::st_sfc(sf::st_point(c(1, 2, 3, 4), dim = "XYZM"))), xyzm(1, 2, 3, 4))
@@ -119,26 +122,29 @@ test_that("conversion from bbox to rct works", {
119122
test_that("conversion to sf works", {
120123
skip_if_not_installed("sf")
121124

122-
# Use NaN/NaN instead of NA/NA because Waldo cares about this comparison
123125
sfc <- sf::st_sfc(sf::st_point(), sf::st_point(c(0, 1)), NULL, crs = 4326)
124126
sf <- sf::st_as_sf(new_data_frame(list(geometry = sfc)))
125127
wkb <- as_wkb(c("POINT EMPTY", "POINT (0 1)", NA), crs = 4326)
126128
wkt <- as_wkt(c("POINT EMPTY", "POINT (0 1)", NA), crs = 4326)
127129

128-
expect_equal_ignore_na_nan(sf::st_as_sf(wkb), sf)
129-
expect_equal_ignore_na_nan(sf::st_as_sfc(wkb), sfc)
130+
expect_equal(sf::st_as_sf(wkb), sf)
131+
expect_equal(sf::st_as_sfc(wkb), sfc)
130132
expect_equal(sf::st_as_sf(wkt), sf)
131133
expect_equal(sf::st_as_sfc(wkt), sfc)
132134

133135
# xy
134-
expect_equal_ignore_na_nan(
136+
expect_equal(
135137
sf::st_as_sf(xy(c(NA, 0, NA), c(NA, 1, NA), crs = 4326)),
136138
sf
137139
)
138-
expect_equal_ignore_na_nan(
140+
expect_equal(
139141
sf::st_as_sfc(xy(c(NA, 0, NA), c(NA, 1, NA), crs = 4326)),
140142
sfc
141143
)
144+
expect_equal(
145+
sf::st_as_sfc(xy(c(NaN, 0, NA), c(NaN, 1, NA), crs = 4326)),
146+
sfc
147+
)
142148

143149
# xy with all !is.na() uses faster sf conversion with coords
144150
expect_equal(sf::st_as_sf(xy(0, 1, crs = 4326)), sf[2,, , drop = FALSE])

tests/testthat/test-sfc-writer.R

+5-45
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,7 @@ test_that("sfc_writer() works with fixed-length input", {
55
# zero-length
66
expect_identical(wk_handle(wkb(), sfc_writer()), sf::st_sfc())
77

8-
# empties (equal because of NaN/NA difference for POINT)
9-
expect_equal_ignore_na_nan(
10-
wk_handle(
11-
as_wkb(
12-
c("POINT EMPTY", "LINESTRING EMPTY", "POLYGON EMPTY",
13-
"MULTIPOINT EMPTY", "MULTILINESTRING EMPTY", "MULTIPOLYGON EMPTY",
14-
"GEOMETRYCOLLECTION EMPTY"
15-
)
16-
),
17-
sfc_writer()
18-
),
19-
sf::st_sfc(
20-
sf::st_point(), sf::st_linestring(), sf::st_polygon(),
21-
sf::st_multipoint(), sf::st_multilinestring(), sf::st_multipolygon(),
22-
sf::st_geometrycollection()
23-
)
24-
)
25-
26-
# subtely different for WKT, since a point will fire zero coordinates
27-
# whereas for WKB it will fire (NaN, NaN)
28-
expect_equal_ignore_na_nan(
8+
expect_identical(
299
wk_handle(
3010
as_wkt(
3111
c("POINT EMPTY", "LINESTRING EMPTY", "POLYGON EMPTY",
@@ -102,27 +82,7 @@ test_that("sfc_writer() works with fixed-length input", {
10282
test_that("sfc_writer() works with promote_multi = TRUE", {
10383
skip_if_not_installed("sf")
10484

105-
# empties (equal because of NaN/NA difference for POINT)
106-
expect_equal_ignore_na_nan(
107-
wk_handle(
108-
as_wkb(
109-
c("POINT EMPTY", "LINESTRING EMPTY", "POLYGON EMPTY",
110-
"MULTIPOINT EMPTY", "MULTILINESTRING EMPTY", "MULTIPOLYGON EMPTY",
111-
"GEOMETRYCOLLECTION EMPTY"
112-
)
113-
),
114-
sfc_writer(promote_multi = TRUE)
115-
),
116-
sf::st_sfc(
117-
sf::st_multipoint(), sf::st_multilinestring(), sf::st_multipolygon(),
118-
sf::st_multipoint(), sf::st_multilinestring(), sf::st_multipolygon(),
119-
sf::st_geometrycollection()
120-
)
121-
)
122-
123-
# subtely different for WKT, since a point will fire zero coordinates
124-
# whereas for WKB it will fire (NaN, NaN)
125-
expect_equal_ignore_na_nan(
85+
expect_identical(
12686
wk_handle(
12787
as_wkt(
12888
c("POINT EMPTY", "LINESTRING EMPTY", "POLYGON EMPTY",
@@ -240,7 +200,7 @@ test_that("sfc_writer() turns NULLs into EMPTY", {
240200
)
241201

242202
for (i in seq_along(all_types)) {
243-
expect_equal_ignore_na_nan(
203+
expect_identical(
244204
wk_handle(c(all_types[i], wkb(list(NULL))), sfc_writer()),
245205
wk_handle(c(all_types[i], all_types[i]), sfc_writer())
246206
)
@@ -270,7 +230,7 @@ test_that("sfc_writer() turns NULLs into EMPTY", {
270230

271231
for (i in seq_along(all_types)) {
272232
vec <- wk_handle(c(all_types_non_empty[i], wkb(list(NULL))), sfc_writer())
273-
expect_equal_ignore_na_nan(vec[[2]], wk_handle(all_types[i], sfc_writer())[[1]])
233+
expect_identical(vec[[2]], wk_handle(all_types[i], sfc_writer())[[1]])
274234
expect_s3_class(vec, paste0("sfc_", types[i]))
275235
}
276236

@@ -284,7 +244,7 @@ test_that("sfc_writer() turns NULLs into EMPTY", {
284244
)
285245

286246
for (i in seq_along(all_types)) {
287-
expect_equal_ignore_na_nan(
247+
expect_identical(
288248
wk_handle(c(zm_types[i], wkb(list(NULL))), sfc_writer()),
289249
wk_handle(c(zm_types[i], zm_types_empty[i]), sfc_writer())
290250
)

tests/testthat/test-vertex-filter.R

+7-4
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
test_that("wk_vertices() works", {
33
expect_identical(
44
wk_vertices(wkt(c("POINT (0 0)", "POINT (1 1)", NA))),
5-
wkt(c("POINT (0 0)", "POINT (1 1)", NA))
5+
wkt(c("POINT (0 0)", "POINT (1 1)"))
66
)
77
expect_identical(
88
wk_vertices(wkt(c("LINESTRING (0 0, 1 1)", NA))),
9-
wkt(c("POINT (0 0)", "POINT (1 1)", NA))
9+
wkt(c("POINT (0 0)", "POINT (1 1)"))
1010
)
1111
expect_error(wk_vertices(new_wk_wkt("POINT ENTPY")), "ENTPY")
1212

@@ -141,6 +141,9 @@ test_that("optimized wk_coords() for xy() works", {
141141
xys <- xy(1:5, 6:10)
142142
expect_identical(wk_coords(xys), wk_coords.default(xys))
143143

144-
xys_with_empty <- c(xys, xy(NA, NA))
145-
expect_identical(wk_coords(xys_with_empty), wk_coords.default(xys_with_empty))
144+
xys_with_empty_and_null <- c(xys, xy(NA, NA), xy(NaN, NaN))
145+
expect_identical(
146+
wk_coords(xys_with_empty_and_null),
147+
wk_coords.default(xys_with_empty_and_null)
148+
)
146149
})

0 commit comments

Comments
 (0)