Skip to content

st_cast() drops polygons when an earlier MULTIPOLYGON is empty #2555

@kendonB

Description

@kendonB

st_cast() drops polygons when an earlier MULTIPOLYGON is empty

Summary

sf::st_cast(..., "POLYGON") loses parts and misaligns attributes when a data frame contains an empty MULTIPOLYGON followed by a multi-part MULTIPOLYGON. The second and third features are collapsed to a single polygon each and its attributes no longer line up with the expected rows.

Steps to Reproduce

Run the minimal example (no external data):

library(sf)
#> Linking to GEOS 3.12.1, GDAL 3.8.4, PROJ 9.4.0; sf_use_s2() is TRUE
library(sessioninfo)

square <- function(offset) {
  rbind(
    c(offset, 0), c(offset + 1, 0), c(offset + 1, 1),
    c(offset, 1), c(offset, 0)
  )
}

mp_empty <- st_multipolygon()
mp_two_a <- st_multipolygon(list(list(square(0)), list(square(10))))
mp_two_b <- st_multipolygon(list(list(square(20)), list(square(30))))

x <- st_sf(
  name = c("empty", "first_multi", "second_multi"),
  value = c("a", "b", "c"),
  geometry = st_sfc(mp_empty, mp_two_a, mp_two_b, crs = 4326)
)

bbox_before <- do.call(
  rbind,
  lapply(x$geometry, function(g) sf::st_bbox(g)[c("xmin", "xmax")])
)
#> Before cast (per-row xmin/xmax):
print(data.frame(name = x$name, value = x$value, xmin = bbox_before[, "xmin"], xmax = bbox_before[, "xmax"]))
#>           name value xmin xmax
#> 1        empty     a   NA   NA
#> 2  first_multi     b    0   11
#> 3 second_multi     c   20   31

expected_names <- c("empty", "first_multi", "first_multi", "second_multi", "second_multi")
#> Expected names after cast: empty, first_multi, first_multi, second_multi, second_multi

cast <- st_cast(x, "POLYGON")
#> Warning in x[!e] <- ret: number of items to replace is not a multiple of
#> replacement length
print(cast)
#> Simple feature collection with 3 features and 2 fields (with 1 geometry empty)
#> Geometry type: POLYGON
#> Dimension:     XY
#> Bounding box:  xmin: 0 ymin: 0 xmax: 11 ymax: 1
#> Geodetic CRS:  WGS 84
#>           name value                       geometry
#> 1        empty     a                  POLYGON EMPTY
#> 2  first_multi     b POLYGON ((0 0, 1 0, 1 1, 0 ...
#> 3 second_multi     c POLYGON ((10 0, 11 0, 11 1,...

print(out)
#>           name value xmin xmax
#> 1        empty     a   NA   NA
#> 2  first_multi     b    0    1
#> 3 second_multi     c   10   11 # polygon from first_multi

cast_corrected <- st_cast(x |> dplyr::filter(!sf::st_is_empty(x$geometry)), "POLYGON")
#> Warning in st_cast.sf(dplyr::filter(x, !sf::st_is_empty(x$geometry)),
#> "POLYGON"): repeating attributes for all sub-geometries for which they may not
#> be constant
# works
print(cast_corrected)
#> Simple feature collection with 4 features and 2 fields
#> Geometry type: POLYGON
#> Dimension:     XY
#> Bounding box:  xmin: 0 ymin: 0 xmax: 31 ymax: 1
#> Geodetic CRS:  WGS 84
#>             name value                       geometry
#> 1    first_multi     b POLYGON ((0 0, 1 0, 1 1, 0 ...
#> 1.1  first_multi     b POLYGON ((10 0, 11 0, 11 1,...
#> 2   second_multi     c POLYGON ((20 0, 21 0, 21 1,...
#> 2.1 second_multi     c POLYGON ((30 0, 31 0, 31 1,...

Created on 2025-11-27 with reprex v2.1.1

Session info
sessioninfo::session_info()
#> ─ Session info ───────────────────────────────────────────────────────────────
#>  setting  value
#>  version  R version 4.5.1 (2025-06-13)
#>  os       Ubuntu 24.04.3 LTS
#>  system   x86_64, linux-gnu
#>  ui       X11
#>  language (EN)
#>  collate  C.UTF-8
#>  ctype    C.UTF-8
#>  tz       Pacific/Auckland
#>  date     2025-11-27
#>  pandoc   3.1.3 @ /usr/bin/ (via rmarkdown)
#>  quarto   1.7.31 @ /opt/quarto/bin/quarto
#> 
#> ─ Packages ───────────────────────────────────────────────────────────────────
#>  package     * version date (UTC) lib source
#>  class         7.3-23  2025-01-01 [3] RSPM (R 4.4.0)
#>  classInt      0.4-11  2025-01-08 [3] RSPM (R 4.4.0)
#>  cli           3.6.5   2025-04-23 [3] RSPM (R 4.4.0)
#>  DBI           1.2.3   2024-06-02 [3] RSPM (R 4.4.0)
#>  digest        0.6.37  2024-08-19 [3] RSPM (R 4.4.2)
#>  dplyr         1.1.4   2023-11-17 [3] RSPM (R 4.4.0)
#>  e1071         1.7-16  2024-09-16 [3] RSPM (R 4.4.0)
#>  evaluate      1.0.5   2025-08-27 [3] RSPM (R 4.5.0)
#>  fastmap       1.2.0   2024-05-15 [3] RSPM (R 4.4.2)
#>  fs            1.6.6   2025-04-12 [3] RSPM (R 4.4.0)
#>  generics      0.1.4   2025-05-09 [3] RSPM (R 4.5.0)
#>  glue          1.8.0   2024-09-30 [3] RSPM (R 4.4.2)
#>  htmltools     0.5.8.1 2024-04-04 [3] RSPM (R 4.4.0)
#>  KernSmooth    2.23-26 2025-01-01 [3] RSPM (R 4.4.0)
#>  knitr         1.50    2025-03-16 [3] RSPM (R 4.4.0)
#>  lifecycle     1.0.4   2023-11-07 [3] RSPM (R 4.4.0)
#>  magrittr      2.0.4   2025-09-12 [3] RSPM (R 4.5.0)
#>  pillar        1.11.1  2025-09-17 [3] RSPM (R 4.5.0)
#>  pkgconfig     2.0.3   2019-09-22 [3] RSPM (R 4.4.0)
#>  proxy         0.4-27  2022-06-09 [3] RSPM (R 4.4.2)
#>  R6            2.6.1   2025-02-15 [3] RSPM (R 4.4.0)
#>  Rcpp          1.1.0   2025-07-02 [3] RSPM (R 4.5.0)
#>  reprex        2.1.1   2024-07-06 [3] RSPM (R 4.4.0)
#>  rlang         1.1.6   2025-04-11 [3] RSPM (R 4.4.0)
#>  rmarkdown     2.30    2025-09-28 [3] RSPM (R 4.5.0)
#>  sessioninfo * 1.2.3   2025-02-05 [3] RSPM (R 4.4.0)
#>  sf          * 1.0-21  2025-05-15 [3] RSPM (R 4.5.0)
#>  tibble        3.3.0   2025-06-08 [3] RSPM (R 4.5.0)
#>  tidyselect    1.2.1   2024-03-11 [3] RSPM (R 4.4.0)
#>  units         0.8-7   2025-03-11 [3] RSPM (R 4.4.0)
#>  vctrs         0.6.5   2023-12-01 [3] RSPM (R 4.4.0)
#>  withr         3.0.2   2024-10-28 [3] RSPM (R 4.4.0)
#>  xfun          0.53    2025-08-19 [3] RSPM (R 4.5.0)
#>  yaml          2.3.10  2024-07-26 [3] RSPM (R 4.4.2)
#> 
#>  [1] /home/kendonb/R/x86_64-pc-linux-gnu-library/4.5
#>  [2] /usr/local/lib/R/site-library
#>  [3] /usr/lib/R/site-library
#>  [4] /usr/lib/R/library
#>  * ── Packages attached to the search path.
#> 
#> ──────────────────────────────────────────────────────────────────────────────
## Expected Behavior Three rows after casting: one empty polygon for `empty_first` and two polygons for `nonempty_second`.

Actual Behavior

Only two rows are returned: the empty polygon and the first polygon part of nonempty_second. The second polygon part is dropped and attribute-to-geometry alignment is wrong.

Environment

  • R: Rscript --vanilla
  • sf: 1.0.21
  • GEOS: 3.12.1
  • GDAL: 3.8.4
  • PROJ: 9.4.0
  • s2: enabled

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions