diff --git a/R/RcppExports.R b/R/RcppExports.R index aa9e80abf..1ee3c878f 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -397,6 +397,10 @@ CPL_read_wkb <- function(wkb_list, EWKB = FALSE, spatialite = FALSE) { .Call(`_sf_CPL_read_wkb`, wkb_list, EWKB, spatialite) } +CPL_read_wkb2 <- function(wkb_list, options) { + .Call(`_sf_CPL_read_wkb2`, wkb_list, options) +} + CPL_write_wkb <- function(sfc, EWKB = FALSE) { .Call(`_sf_CPL_write_wkb`, sfc, EWKB) } diff --git a/R/read.R b/R/read.R index 93a73fdb7..f4635b5a7 100644 --- a/R/read.R +++ b/R/read.R @@ -76,7 +76,7 @@ set_utf8 = function(x) { #' #' In case of problems reading shapefiles from USB drives on OSX, please see #' \url{https://github.com/r-spatial/sf/issues/252}. Reading shapefiles (or other -#' data sources) directly from zip files can be done by prepending the path +#' data sources) directly from zip files can be done by prepending the path #' with \code{/vsizip/}. This is part of the GDAL Virtual File Systems interface #' that also supports .gz, curl, and other operations, including chaining; see #' \url{https://gdal.org/user/virtual_file_systems.html} for a complete @@ -225,42 +225,38 @@ process_cpl_read_ogr_stream = function(x, geom_column_info, num_features, fid_co function(s) identical(s$metadata[["ARROW:extension:name"]], "ogc.wkb"), logical(1) ) - + geom_column_info$index = which(is_geometry_column) - + if (num_features == -1) { num_features = NULL } - + # Suppress warnings about extension type conversion (since we want the # default behaviour of converting the storage type) df = suppressWarnings(nanoarrow::convert_array_stream(x, size = num_features)) - + for (i in seq_len(nrow(geom_column_info))) { crs = if (is.null(crs)) st_crs(geom_column_info$crs[[i]]) else st_crs(crs) name = geom_column_info$name[[i]] index = geom_column_info$index[[i]] - + column_wkb = df[[index]] - attributes(column_wkb) = NULL - column_sfc = wk::wk_handle( - wk::new_wk_wkb(column_wkb), - wk::sfc_writer(promote_multi = promote_to_multi) - ) - - df[[index]] = st_set_crs(column_sfc, crs) + class(column_wkb) <- "WKB" + column_sfc = sf::st_as_sfc(column_wkb, crs = crs, promote_to_multi = promote_to_multi) + df[[index]] = column_sfc names(df)[index] = name } - + # Rename OGC_FID to fid_column_name and move to end if (length(fid_column_name) == 1 && "OGC_FID" %in% names(df)) { df = df[c(setdiff(names(df), "OGC_FID"), "OGC_FID")] names(df)[names(df) == "OGC_FID"] = fid_column_name } - + # All geometry columns to the end df = df[c(setdiff(seq_along(df), geom_column_info$index), geom_column_info$index)] - + process_cpl_read_ogr(df, ...) } diff --git a/R/wkb.R b/R/wkb.R index 16582ab0d..bfa3dcab6 100644 --- a/R/wkb.R +++ b/R/wkb.R @@ -25,6 +25,8 @@ skip0x = function(x) { #' @param EWKB logical; if `TRUE`, parse as EWKB (extended WKB; PostGIS: ST_AsEWKB), otherwise as ISO WKB (PostGIS: ST_AsBinary) #' @param spatialite logical; if \code{TRUE}, WKB is assumed to be in the spatialite dialect, see \url{https://www.gaia-gis.it/gaia-sins/BLOB-Geometry.html}; this is only supported in native endian-ness (i.e., files written on system with the same endian-ness as that on which it is being read). #' @param pureR logical; if `TRUE`, use only R code, if `FALSE`, use compiled (C++) code; use `TRUE` when the endian-ness of the binary differs from the host machine (\code{.Platform$endian}). +#' @param promote_to_multi logical; if `TRUE`, attempt to promote combinations of simple/multi +#' such that all output geometries are multi geometries of the same type. #' @details When converting from WKB, the object \code{x} is either a character vector such as typically obtained from PostGIS (either with leading "0x" or without), or a list with raw vectors representing the features in binary (raw) form. #' @examples #' wkb = structure(list("01010000204071000000000000801A064100000000AC5C1441"), class = "WKB") @@ -32,7 +34,8 @@ skip0x = function(x) { #' wkb = structure(list("0x01010000204071000000000000801A064100000000AC5C1441"), class = "WKB") #' st_as_sfc(wkb, EWKB = TRUE) #' @export -st_as_sfc.WKB = function(x, ..., EWKB = FALSE, spatialite = FALSE, pureR = FALSE, crs = NA_crs_) { +st_as_sfc.WKB = function(x, ..., EWKB = FALSE, spatialite = FALSE, pureR = FALSE, crs = NA_crs_, + promote_to_multi = FALSE) { if (EWKB && spatialite) stop("arguments EWKB and spatialite cannot both be TRUE") if (spatialite && pureR) @@ -49,7 +52,10 @@ st_as_sfc.WKB = function(x, ..., EWKB = FALSE, spatialite = FALSE, pureR = FALSE ret = if (pureR) R_read_wkb(x, readWKB, EWKB = EWKB) else - CPL_read_wkb(x, EWKB, spatialite) + CPL_read_wkb2(x, + list(EWKB = EWKB, + spatialite = spatialite, + promote_to_multi = promote_to_multi)) if (is.na(crs) && (EWKB || spatialite) && !is.null(attr(ret, "srid")) && attr(ret, "srid") != 0) crs = attr(ret, "srid") if (! is.na(st_crs(crs))) { diff --git a/inst/include/sf_RcppExports.h b/inst/include/sf_RcppExports.h index 21aa39db0..aa63b7899 100644 --- a/inst/include/sf_RcppExports.h +++ b/inst/include/sf_RcppExports.h @@ -45,6 +45,27 @@ namespace sf { return Rcpp::as(rcpp_result_gen); } + inline Rcpp::List CPL_read_wkb2(Rcpp::List wkb_list, Rcpp::List options) { + typedef SEXP(*Ptr_CPL_read_wkb2)(SEXP,SEXP); + static Ptr_CPL_read_wkb2 p_CPL_read_wkb2 = NULL; + if (p_CPL_read_wkb2 == NULL) { + validateSignature("Rcpp::List(*CPL_read_wkb2)(Rcpp::List,Rcpp::List)"); + p_CPL_read_wkb2 = (Ptr_CPL_read_wkb2)R_GetCCallable("sf", "_sf_CPL_read_wkb2"); + } + RObject rcpp_result_gen; + { + RNGScope RCPP_rngScope_gen; + rcpp_result_gen = p_CPL_read_wkb2(Shield(Rcpp::wrap(wkb_list)), Shield(Rcpp::wrap(options))); + } + if (rcpp_result_gen.inherits("interrupted-error")) + throw Rcpp::internal::InterruptedException(); + if (Rcpp::internal::isLongjumpSentinel(rcpp_result_gen)) + throw Rcpp::LongjumpException(rcpp_result_gen); + if (rcpp_result_gen.inherits("try-error")) + throw Rcpp::exception(Rcpp::as(rcpp_result_gen).c_str()); + return Rcpp::as(rcpp_result_gen); + } + inline Rcpp::List CPL_write_wkb(Rcpp::List sfc, bool EWKB = false) { typedef SEXP(*Ptr_CPL_write_wkb)(SEXP,SEXP); static Ptr_CPL_write_wkb p_CPL_write_wkb = NULL; diff --git a/man/st_as_sfc.Rd b/man/st_as_sfc.Rd index 6b71a53b3..71a923925 100644 --- a/man/st_as_sfc.Rd +++ b/man/st_as_sfc.Rd @@ -41,7 +41,8 @@ EWKB = FALSE, spatialite = FALSE, pureR = FALSE, - crs = NA_crs_ + crs = NA_crs_, + promote_to_multi = FALSE ) \method{st_as_sfc}{raw}(x, ...) @@ -84,6 +85,9 @@ st_as_sfc(x, ...) \item{crs}{coordinate reference system to be assigned; object of class \code{crs}} +\item{promote_to_multi}{logical; if \code{TRUE}, attempt to promote combinations of simple/multi +such that all output geometries are multi geometries of the same type.} + \item{GeoJSON}{logical; if \code{TRUE}, try to read geometries from GeoJSON text strings geometry, see \code{\link[=st_crs]{st_crs()}}} diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp index 1b7adf161..abf19d937 100644 --- a/src/RcppExports.cpp +++ b/src/RcppExports.cpp @@ -1378,6 +1378,41 @@ RcppExport SEXP _sf_CPL_read_wkb(SEXP wkb_listSEXP, SEXP EWKBSEXP, SEXP spatiali UNPROTECT(1); return rcpp_result_gen; } +// CPL_read_wkb2 +Rcpp::List CPL_read_wkb2(Rcpp::List wkb_list, Rcpp::List options); +static SEXP _sf_CPL_read_wkb2_try(SEXP wkb_listSEXP, SEXP optionsSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::traits::input_parameter< Rcpp::List >::type wkb_list(wkb_listSEXP); + Rcpp::traits::input_parameter< Rcpp::List >::type options(optionsSEXP); + rcpp_result_gen = Rcpp::wrap(CPL_read_wkb2(wkb_list, options)); + return rcpp_result_gen; +END_RCPP_RETURN_ERROR +} +RcppExport SEXP _sf_CPL_read_wkb2(SEXP wkb_listSEXP, SEXP optionsSEXP) { + SEXP rcpp_result_gen; + { + Rcpp::RNGScope rcpp_rngScope_gen; + rcpp_result_gen = PROTECT(_sf_CPL_read_wkb2_try(wkb_listSEXP, optionsSEXP)); + } + Rboolean rcpp_isInterrupt_gen = Rf_inherits(rcpp_result_gen, "interrupted-error"); + if (rcpp_isInterrupt_gen) { + UNPROTECT(1); + Rf_onintr(); + } + bool rcpp_isLongjump_gen = Rcpp::internal::isLongjumpSentinel(rcpp_result_gen); + if (rcpp_isLongjump_gen) { + Rcpp::internal::resumeJump(rcpp_result_gen); + } + Rboolean rcpp_isError_gen = Rf_inherits(rcpp_result_gen, "try-error"); + if (rcpp_isError_gen) { + SEXP rcpp_msgSEXP_gen = Rf_asChar(rcpp_result_gen); + UNPROTECT(1); + Rf_error("%s", CHAR(rcpp_msgSEXP_gen)); + } + UNPROTECT(1); + return rcpp_result_gen; +} // CPL_write_wkb Rcpp::List CPL_write_wkb(Rcpp::List sfc, bool EWKB); static SEXP _sf_CPL_write_wkb_try(SEXP sfcSEXP, SEXP EWKBSEXP) { @@ -1443,6 +1478,7 @@ static int _sf_RcppExport_validate(const char* sig) { static std::set signatures; if (signatures.empty()) { signatures.insert("Rcpp::List(*CPL_read_wkb)(Rcpp::List,bool,bool)"); + signatures.insert("Rcpp::List(*CPL_read_wkb2)(Rcpp::List,Rcpp::List)"); signatures.insert("Rcpp::List(*CPL_write_wkb)(Rcpp::List,bool)"); } return signatures.find(sig) != signatures.end(); @@ -1451,6 +1487,7 @@ static int _sf_RcppExport_validate(const char* sig) { // registerCCallable (register entry points for exported C++ functions) RcppExport SEXP _sf_RcppExport_registerCCallable() { R_RegisterCCallable("sf", "_sf_CPL_read_wkb", (DL_FUNC)_sf_CPL_read_wkb_try); + R_RegisterCCallable("sf", "_sf_CPL_read_wkb2", (DL_FUNC)_sf_CPL_read_wkb2_try); R_RegisterCCallable("sf", "_sf_CPL_write_wkb", (DL_FUNC)_sf_CPL_write_wkb_try); R_RegisterCCallable("sf", "_sf_RcppExport_validate", (DL_FUNC)_sf_RcppExport_validate); return R_NilValue; @@ -1556,6 +1593,7 @@ static const R_CallMethodDef CallEntries[] = { {"_sf_CPL_extract", (DL_FUNC) &_sf_CPL_extract, 3}, {"_sf_CPL_create", (DL_FUNC) &_sf_CPL_create, 6}, {"_sf_CPL_read_wkb", (DL_FUNC) &_sf_CPL_read_wkb, 3}, + {"_sf_CPL_read_wkb2", (DL_FUNC) &_sf_CPL_read_wkb2, 2}, {"_sf_CPL_write_wkb", (DL_FUNC) &_sf_CPL_write_wkb, 2}, {"_sf_CPL_get_z_range", (DL_FUNC) &_sf_CPL_get_z_range, 2}, {"_sf_CPL_get_m_range", (DL_FUNC) &_sf_CPL_get_m_range, 2}, diff --git a/src/gdal.cpp b/src/gdal.cpp index 802d5cc4e..d89026f56 100644 --- a/src/gdal.cpp +++ b/src/gdal.cpp @@ -330,7 +330,7 @@ Rcpp::List CPL_crs_parameters(Rcpp::List crs) { Rcpp::IntegerVector orientation(ac); for (int i = 0; i < ac; i++) { OGRAxisOrientation peOrientation; - const char *ret = srs->GetAxis(srs->IsGeographic() ? "GEOGCS" : "PROJCS", + const char *ret = srs->GetAxis(srs->IsGeographic() ? "GEOGCS" : "PROJCS", i, &peOrientation); if (ret != NULL) { nms[i] = ret; diff --git a/src/geos.cpp b/src/geos.cpp index 703cf1d6a..e9ff03a02 100644 --- a/src/geos.cpp +++ b/src/geos.cpp @@ -871,7 +871,7 @@ Rcpp::List CPL_geos_op(std::string op, Rcpp::List sfc, #ifdef HAVE310 if (op == "triangulate_constrained") { for (size_t i = 0; i < g.size(); i++) - out[i] = geos_ptr(chkNULL(GEOSConstrainedDelaunayTriangulation_r(hGEOSCtxt, g[i].get())), + out[i] = geos_ptr(chkNULL(GEOSConstrainedDelaunayTriangulation_r(hGEOSCtxt, g[i].get())), hGEOSCtxt); } else #endif @@ -1307,14 +1307,14 @@ Rcpp::List CPL_nary_intersection(Rcpp::List sfc) { errors++; else if (!chk_(GEOSisEmpty_r(hGEOSCtxt, inters.get()))) { // i and k intersection // cut out inters from geom: - geom = geos_ptr(GEOSDifference_r(hGEOSCtxt, geom.get(), inters.get()), hGEOSCtxt); + geom = geos_ptr(GEOSDifference_r(hGEOSCtxt, geom.get(), inters.get()), hGEOSCtxt); if (geom == nullptr) Rcpp::stop("GEOS exception"); // #nocov // cut out inters from out[k]: #ifndef HAVE_390 - GeomPtr g = geos_ptr(GEOSDifference_r(hGEOSCtxt, out[k].get(), inters.get()), hGEOSCtxt); + GeomPtr g = geos_ptr(GEOSDifference_r(hGEOSCtxt, out[k].get(), inters.get()), hGEOSCtxt); #else - GeomPtr g = geos_ptr(GEOSDifferencePrec_r(hGEOSCtxt, out[k].get(), inters.get(), grid_size), hGEOSCtxt); + GeomPtr g = geos_ptr(GEOSDifferencePrec_r(hGEOSCtxt, out[k].get(), inters.get(), grid_size), hGEOSCtxt); #endif if (g == nullptr) Rcpp::warning("GEOS difference returns NULL"); // #nocov diff --git a/src/wkb.cpp b/src/wkb.cpp index c91277ff2..d65fe8c8e 100644 --- a/src/wkb.cpp +++ b/src/wkb.cpp @@ -400,11 +400,108 @@ int native_endian(void) { return (int) *cp; } -// [[Rcpp::export]] -Rcpp::List CPL_read_wkb(Rcpp::List wkb_list, bool EWKB = false, bool spatialite = false) { +static int64_t sf_type_bitmask(int sf_type) { + return static_cast(1) << sf_type; +} + +static Rcpp::NumericMatrix read_wkb_promote_sfg_multipoint(Rcpp::NumericVector item) { + SEXP cls_old = Rf_getAttrib(item, R_ClassSymbol); + Rcpp::CharacterVector cls_new = Rf_duplicate(cls_old); + cls_new[1] = "MULTIPOINT"; + + bool is_empty = true; + for (const auto ordinate : item) { + if (!ISNAN(ordinate)) { + is_empty = false; + break; + } + } + + Rcpp::NumericMatrix multi; + if (is_empty) { + multi = Rcpp::NumericMatrix(0, item.size()); + } else { + multi = Rcpp::NumericMatrix(1, item.size()); + memcpy(REAL(multi), REAL(item), item.size() * sizeof(double)); + } + + multi.attr("class") = cls_new; + return multi; +} + +static Rcpp::List read_wkb_promote_sfg_multipolygon_or_linestring(SEXP item, const char* cls_multi) { + // Technically we could mutate this class because we allocated it above; however, + // safer to not make this assumption. + SEXP cls_old = Rf_getAttrib(item, R_ClassSymbol); + Rcpp::CharacterVector cls_new = Rf_duplicate(cls_old); + cls_new[1] = cls_multi; + + Rcpp::List multi; + if (Rf_length(item) == 0) { + multi = Rcpp::List::create(); + } else { + multi = Rcpp::List::create(item); + } + + multi.attr("class") = cls_new; + return multi; +} + +static void read_wkb_promote_multi_if_possible(Rcpp::List output, int64_t* all_types) { + int64_t can_promote_multipoint = sf_type_bitmask(SF_Point) | + sf_type_bitmask(SF_MultiPoint); + int64_t can_promote_multilinestring = sf_type_bitmask(SF_LineString) | + sf_type_bitmask(SF_MultiLineString); + int64_t can_promote_multipolygon = sf_type_bitmask(SF_Polygon) | + sf_type_bitmask(SF_MultiPolygon); + + const char* cls_simple; + int sf_type_multi; + if (*all_types == can_promote_multipoint) { + cls_simple = "POINT"; + sf_type_multi = SF_MultiPoint; + } else if (*all_types == can_promote_multilinestring) { + cls_simple = "LINESTRING"; + sf_type_multi = SF_MultiLineString; + } else if (*all_types == can_promote_multipolygon) { + cls_simple = "POLYGON"; + sf_type_multi = SF_MultiPolygon; + } else { + // Promotion is not possible or is not necessary + return; + } + + for (int i = 0; i < output.size(); i++) { + SEXP item = output[i]; + if (!Rf_inherits(item, cls_simple)) { + continue; + } + + switch (sf_type_multi) { + case SF_MultiPoint: + output[i] = read_wkb_promote_sfg_multipoint(item); + break; + case SF_MultiLineString: + output[i] = read_wkb_promote_sfg_multipolygon_or_linestring(item, "MULTILINESTRING"); + break; + case SF_MultiPolygon: + output[i] = read_wkb_promote_sfg_multipolygon_or_linestring(item, "MULTIPOLYGON"); + break; + default: + Rcpp::stop("promote to multi not implemented"); + } + } + + *all_types = sf_type_bitmask(sf_type_multi); +} + +static Rcpp::List CPL_read_wkb_internal(Rcpp::List wkb_list, bool EWKB = false, bool spatialite = false, bool promote_multi = false) { Rcpp::List output(wkb_list.size()); - int type = 0, last_type = 0, n_types = 0, n_empty = 0; + int type = 0, n_empty = 0; + // An integer such that (all_types & (1 << sf_type)) != 0 if sf_type was + // encountered while reading the wkb items. + int64_t all_types = 0; int endian = native_endian(); uint32_t srid = 0; @@ -421,11 +518,20 @@ Rcpp::List CPL_read_wkb(Rcpp::List wkb_list, bool EWKB = false, bool spatialite n_empty++; } // Rcpp::Rcout << "type is " << type << "\n"; - if (n_types <= 1 && type != last_type) { - last_type = type; - n_types++; // check if there's more than 1 type: + all_types |= sf_type_bitmask(type); + } + + if (promote_multi) { + read_wkb_promote_multi_if_possible(output, &all_types); + } + + int n_types = 0; + for (int i = 0; i < SF_Type_Max; i++) { + if (all_types & sf_type_bitmask(i)) { + n_types++; } } + output.attr("single_type") = n_types <= 1; // if 0, we have only empty geometrycollections output.attr("n_empty") = (int) n_empty; if ((EWKB || spatialite) && srid != 0) @@ -433,6 +539,37 @@ Rcpp::List CPL_read_wkb(Rcpp::List wkb_list, bool EWKB = false, bool spatialite return output; } + +// This function is used by other packages that link to sf, so its signature cannot be changed +// [[Rcpp::export]] +Rcpp::List CPL_read_wkb(Rcpp::List wkb_list, bool EWKB = false, bool spatialite = false) { + return CPL_read_wkb_internal(wkb_list, EWKB, spatialite); +} + + +// This version of the function is designed to allow evolution of options without +// breaking compatability with existing usage +// [[Rcpp::export]] +Rcpp::List CPL_read_wkb2(Rcpp::List wkb_list, Rcpp::List options) { + bool EWKB = false; + bool spatialite = false; + bool promote_to_multi = false; + + if (options.containsElementNamed("EWKB")) { + EWKB = options("EWKB"); + } + + if (options.containsElementNamed("spatialite")) { + spatialite = options("spatialite"); + } + + if (options.containsElementNamed("promote_to_multi")) { + promote_to_multi = options("promote_to_multi"); + } + + return CPL_read_wkb_internal(wkb_list, EWKB, spatialite, promote_to_multi); +} + // // write wkb: // @@ -679,8 +816,8 @@ Rcpp::List CPL_write_wkb(Rcpp::List sfc, bool EWKB = false) { } int srid = 0; - if (EWKB) { - // get SRID from crs[["input"]], either of the form "4326" + if (EWKB) { + // get SRID from crs[["input"]], either of the form "4326" // or "XXXX:4326" with arbitrary XXXX string, // or else from the wkt field of the crs using srid_from_crs() Rcpp::List crs = sfc.attr("crs"); diff --git a/src/wkb.h b/src/wkb.h index ff718b6dc..445f4ba6e 100644 --- a/src/wkb.h +++ b/src/wkb.h @@ -17,6 +17,7 @@ #define SF_PolyhedralSurface 15 #define SF_TIN 16 #define SF_Triangle 17 +#define SF_Type_Max 17 Rcpp::List CPL_read_wkb(Rcpp::List wkb_list, bool EWKB, bool spatialite); Rcpp::List CPL_write_wkb(Rcpp::List sfc, bool EWKB); diff --git a/tests/testthat/test-wkb.R b/tests/testthat/test-wkb.R index a5b10ca65..87dfc9d04 100644 --- a/tests/testthat/test-wkb.R +++ b/tests/testthat/test-wkb.R @@ -60,3 +60,67 @@ test_that("st_as_sfc() honors crs argument", { expect_identical(st_as_sfc(list, crs = 2056), st_as_sfc(wkb, crs = 2056)) expect_identical(st_as_sfc(blob, crs = 2056), st_as_sfc(wkb, crs = 2056)) }) + +test_that("st_as_sfc() for WKB can promote_to_multi for multipoint + point", { + sfc_mixed = st_sfc( + st_point(), + st_point(c(2, 3)), + st_multipoint() + ) + + wkb_mixed = st_as_binary(sfc_mixed) + + expect_identical( + st_as_sfc(wkb_mixed, promote_to_multi = FALSE), + sfc_mixed + ) + + expect_identical( + st_as_sfc(wkb_mixed, promote_to_multi = TRUE), + st_cast(sfc_mixed, "MULTIPOINT") + ) +}) + +test_that("st_as_sfc() for WKB can promote_to_multi for multilinestring + linestring", { + sfc_mixed = st_sfc( + st_linestring(), + st_linestring(rbind(c(2, 3), c(4, 5))), + st_multilinestring() + ) + + wkb_mixed = st_as_binary(sfc_mixed) + + expect_identical( + st_as_sfc(wkb_mixed, promote_to_multi = FALSE), + sfc_mixed + ) + + # st_as_sfc() assigns different attribute order than st_cast, but we ony care + # about whether the geometries are correct + expect_equivalent( + st_as_sfc(wkb_mixed, promote_to_multi = TRUE), + st_cast(sfc_mixed, "MULTILINESTRING") + ) +}) + +test_that("st_as_sfc() for WKB can promote_to_multi for multipolygon + polygon", { + sfc_mixed = st_sfc( + st_polygon(), + st_polygon(list(rbind(c(0, 0), c(1, 0), c(0, 1), c(0, 0)))), + st_multipolygon() + ) + + wkb_mixed = st_as_binary(sfc_mixed) + + expect_identical( + st_as_sfc(wkb_mixed, promote_to_multi = FALSE), + sfc_mixed + ) + + # st_as_sfc() assigns different attribute order than st_cast, but we ony care + # about whether the geometries are correct + expect_equivalent( + st_as_sfc(wkb_mixed, promote_to_multi = TRUE), + st_cast(sfc_mixed, "MULTIPOLYGON") + ) +})