From 1765c1222357af9c9d255ff536572f8e387470c6 Mon Sep 17 00:00:00 2001 From: Oreille <33065839+Oreilles@users.noreply.github.com> Date: Mon, 9 Sep 2024 18:29:27 +0200 Subject: [PATCH] Add GEOSGeom_transformXYZ (#1157) * Add GEOSGeom_transformXYZ --- capi/geos_c.cpp | 6 + capi/geos_c.h.in | 50 ++- capi/geos_ts_c.cpp | 29 ++ tests/unit/capi/GEOSGeom_transformXYZTest.cpp | 333 ++++++++++++++++++ 4 files changed, 416 insertions(+), 2 deletions(-) create mode 100644 tests/unit/capi/GEOSGeom_transformXYZTest.cpp diff --git a/capi/geos_c.cpp b/capi/geos_c.cpp index 834f3ef51c..39d34ff5ec 100644 --- a/capi/geos_c.cpp +++ b/capi/geos_c.cpp @@ -663,6 +663,12 @@ extern "C" { } + Geometry* + GEOSGeom_transformXYZ(const GEOSGeometry* g, GEOSTransformXYZCallback callback, void* userdata) { + return GEOSGeom_transformXYZ_r(handle, g, callback, userdata); + } + + //------------------------------------------------------------------- // memory management functions //------------------------------------------------------------------ diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in index 19f0dcb277..2dab4135ae 100644 --- a/capi/geos_c.h.in +++ b/capi/geos_c.h.in @@ -284,7 +284,7 @@ typedef int (*GEOSDistanceCallback)( /** * Callback function for use in GEOSGeom_transformXY. * Allows custom function to be applied to x and y values for each coordinate -* in a geometry. Z values are unchanged by this function. +* in a geometry. Z and M values are unchanged by this function. * Extra data for the calculation can be passed via the userdata. * * \param x coordinate value to be updated @@ -299,6 +299,26 @@ typedef int (*GEOSTransformXYCallback)( void* userdata); +/** +* Callback function for use in GEOSGeom_transformXYZ. +* Allows custom function to be applied to x, y and z values for each coordinate +* in a geometry. M values are unchanged by this function. +* Extra data for the calculation can be passed via the userdata. +* +* \param x coordinate value to be updated +* \param y coordinate value to be updated +* \param z coordinate value to be updated +* \param userdata extra data for the calculation +* +* \return 1 if calculation succeeded, 0 on failure +*/ +typedef int (*GEOSTransformXYZCallback)( + double* x, + double* y, + double* z, + void* userdata); + + /* ========== Interruption ========== */ /** @@ -1821,6 +1841,13 @@ extern GEOSGeometry GEOS_DLL *GEOSGeom_transformXY_r( GEOSTransformXYCallback callback, void* userdata); +/** \see GEOSGeom_transformXYZ */ +extern GEOSGeometry GEOS_DLL *GEOSGeom_transformXYZ_r( + GEOSContextHandle_t handle, + const GEOSGeometry* g, + GEOSTransformXYZCallback callback, + void* userdata); + /* ========= Algorithms ========= */ /** \see GEOSOrientationIndex */ @@ -4696,7 +4723,7 @@ extern int GEOS_DLL GEOSHilbertCode( /** * Apply XY coordinate transform callback to all coordinates in a copy of * input geometry. If the callback returns an error, returned geometry will be -* NULL. Z values, if present, are not modified by this function. +* NULL. Z and M values, if present, are not modified by this function. * \param[in] g Input geometry * \param[in] callback a function to be executed for each coordinate in the geometry. The callback takes 3 parameters: x and y coordinate @@ -4712,6 +4739,25 @@ extern GEOSGeometry GEOS_DLL *GEOSGeom_transformXY( GEOSTransformXYCallback callback, void* userdata); +/** +* Apply XYZ coordinate transform callback to all coordinates in a copy of +* input geometry. If the callback returns an error, returned geometry will be +* NULL. M values, if present, are not modified by this function. +* \param[in] g Input geometry +* \param[in] callback a function to be executed for each coordinate in the + geometry. The callback takes 4 parameters: x, y and z coordinate + values to be updated and a void userdata pointer. +* \param userdata an optional pointer to pe passed to 'callback' as an argument +* \return a copy of the input geometry with transformed coordinates. +* Caller must free with GEOSGeom_destroy(). +* +* \since 3.13 +*/ +extern GEOSGeometry GEOS_DLL *GEOSGeom_transformXYZ( + const GEOSGeometry* g, + GEOSTransformXYZCallback callback, + void* userdata); + /** * Snaps the vertices and segments of the first geometry to vertices of the * second geometry within the given tolerance. diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp index ef238229aa..b77d872ba7 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -1655,6 +1655,35 @@ extern "C" { } + Geometry* + GEOSGeom_transformXYZ_r(GEOSContextHandle_t handle, const GEOSGeometry* g, GEOSTransformXYZCallback callback, void* userdata) { + + struct TransformFilter final: public geos::geom::CoordinateFilter { + TransformFilter(GEOSTransformXYZCallback p_callback, + void* p_userdata) : + m_callback(p_callback), + m_userdata(p_userdata) {} + + void filter_rw(Coordinate* c) const override { + if (!m_callback(&(c->x), &(c->y), &(c->z), m_userdata)) { + throw std::runtime_error(std::string("Failed to transform coordinates.")); + } + } + + GEOSTransformXYZCallback m_callback; + void* m_userdata; + }; + + return execute(handle, [&]() { + TransformFilter filter(callback, userdata); + auto ret = g->clone(); + ret->apply_rw(&filter); + ret->geometryChanged(); + return ret.release(); + }); + } + + //------------------------------------------------------------------- // memory management functions //------------------------------------------------------------------ diff --git a/tests/unit/capi/GEOSGeom_transformXYZTest.cpp b/tests/unit/capi/GEOSGeom_transformXYZTest.cpp new file mode 100644 index 0000000000..c696b4da56 --- /dev/null +++ b/tests/unit/capi/GEOSGeom_transformXYZTest.cpp @@ -0,0 +1,333 @@ +// +// Test Suite for C-API GEOSGeom_transformXYZ + +#include +// geos +#include +// std +#include +#include +#include + +#include "capi_test_utils.h" + +namespace tut { +struct test_capi_geosgeom_transformxyz : public capitest::utility {}; + +typedef test_group group; +typedef group::object object; + +group test_capi_geosgeom_transformxyz_group("capi::GEOSGeom_transformXYZ"); + +static int SCALE_2_3_4(double* x, double* y, double* z, void* userdata) { + (*x) *= 2; + (*y) *= 3; + (*z) *= 4; + (void)(userdata); // make unused parameter warning go away + return 1; +} + +// callback that doesn't update coordinates should return original values +template <> +template <> +void object::test<1>() { + GEOSGeometry* geom = GEOSGeomFromWKT("POINT (1 1 1)"); + + GEOSGeometry* out = GEOSGeom_transformXYZ( + geom, + [](double* x, double* y, double* z, void* userdata) { + (void)(x); // make unused parameter warning go away + (void)(y); // make unused parameter warning go away + (void)(z); // make unused parameter warning go away + (void)(userdata); // make unused parameter warning go away + return 1; + }, + nullptr); + + ensure(out != nullptr); + ensure_equals(GEOSEqualsExact(out, geom, 0), 1); + + GEOSGeom_destroy(geom); + GEOSGeom_destroy(out); +} + +// failed callback should return NULL +template <> +template <> +void object::test<2>() { + GEOSGeometry* geom = GEOSGeomFromWKT("POINT (1 1 1)"); + + GEOSGeometry* out = GEOSGeom_transformXYZ( + geom, + [](double* x, double* y, double* z, void* userdata) { + (void)(x); // make unused parameter warning go away + (void)(y); // make unused parameter warning go away + (void)(z); // make unused parameter warning go away + (void)(userdata); // make unused parameter warning go away + return 0; // indicates error + }, + nullptr); + + ensure(out == nullptr); + + GEOSGeom_destroy(geom); +} + +// callback should modify point coordinates +template <> +template <> +void object::test<3>() { + GEOSGeometry* geom = GEOSGeomFromWKT("POINT (1 1 1)"); + + GEOSGeometry* out = GEOSGeom_transformXYZ(geom, SCALE_2_3_4, nullptr); + + ensure(out != nullptr); + ensure_geometry_equals(out, "POINT (2 3 4)"); + + double xmin, ymin, xmax, ymax; + GEOSGeom_getExtent(out, &xmin, &ymin, &xmax, &ymax); + ensure_equals(xmin, 2); + ensure_equals(ymin, 3); + ensure_equals(xmax, 2); + ensure_equals(ymax, 3); + + GEOSGeom_destroy(geom); + GEOSGeom_destroy(out); +} + +// callback should modify linestring coordinates +template <> +template <> +void object::test<4>() { + GEOSGeometry* geom = GEOSGeomFromWKT("LINESTRING (1 1 1, 2 2 2)"); + + GEOSGeometry* out = GEOSGeom_transformXYZ(geom, SCALE_2_3_4, nullptr); + + ensure(out != nullptr); + ensure_geometry_equals(out, "LINESTRING (2 3 4, 4 6 8)"); + + double xmin, ymin, xmax, ymax; + GEOSGeom_getExtent(out, &xmin, &ymin, &xmax, &ymax); + ensure_equals(xmin, 2); + ensure_equals(ymin, 3); + ensure_equals(xmax, 4); + ensure_equals(ymax, 6); + + GEOSGeom_destroy(geom); + GEOSGeom_destroy(out); +} + +// callback should modify polygon coordinates +template <> +template <> +void object::test<5>() { + GEOSGeometry* geom = GEOSGeomFromWKT( + "POLYGON ((1 1 1, 1 10 100, 10 10 10, 10 1 .1, 1 1 1), (2 2 2, 2 4 8, 4 4 4, 4 2 1, 2 2 2))"); + + GEOSGeometry* out = GEOSGeom_transformXYZ(geom, SCALE_2_3_4, nullptr); + + ensure(out != nullptr); + ensure_geometry_equals(out, + "POLYGON ((2 3 4, 2 30 400, 20 30 40, 20 3 0.4, 2 3 4), (4 6 8, 4 12 32, 8 12 16, 8 6 4, 4 6 8))"); + + double xmin, ymin, xmax, ymax; + GEOSGeom_getExtent(out, &xmin, &ymin, &xmax, &ymax); + ensure_equals(xmin, 2); + ensure_equals(ymin, 3); + ensure_equals(xmax, 20); + ensure_equals(ymax, 30); + + GEOSGeom_destroy(geom); + GEOSGeom_destroy(out); +} + +// callback should modify multi point coordinates +template <> +template <> +void object::test<6>() { + GEOSGeometry* geom = GEOSGeomFromWKT( + "MULTIPOINT ((1 1 1), (2 2 2))"); + + GEOSGeometry* out = GEOSGeom_transformXYZ(geom, SCALE_2_3_4, nullptr); + + ensure(out != nullptr); + ensure_geometry_equals(out, + "MULTIPOINT ((2 3 4), (4 6 8))"); + + GEOSGeom_destroy(geom); + GEOSGeom_destroy(out); +} + +// callback should modify multi linestring coordinates +template <> +template <> +void object::test<7>() { + GEOSGeometry* geom = GEOSGeomFromWKT( + "MULTILINESTRING ((1 1 1, 2 2 2), (3 3 3, 4 4 4))"); + + GEOSGeometry* out = GEOSGeom_transformXYZ(geom, SCALE_2_3_4, nullptr); + + ensure(out != nullptr); + ensure_geometry_equals(out, + "MULTILINESTRING ((2 3 4, 4 6 8), (6 9 12, 8 12 16))"); + + GEOSGeom_destroy(geom); + GEOSGeom_destroy(out); +} + +// callback should modify multi polygon coordinates +template <> +template <> +void object::test<8>() { + GEOSGeometry* geom = GEOSGeomFromWKT( + "MULTIPOLYGON (((1 1 1, 1 10 100, 10 10 100, 10 1 0.1, 1 1 1), (2 2 2, 2 4 8, 4 4 4, 4 2 1, 2 2 2)), ((0 0 0, 0 100 1000, 100 100 100, 100 0 -100, 0 0 0)))"); + + GEOSGeometry* out = GEOSGeom_transformXYZ(geom, SCALE_2_3_4, nullptr); + + ensure(out != nullptr); + ensure_geometry_equals(out, + "MULTIPOLYGON (((2 3 4, 2 30 400, 20 30 40, 20 3 0.4, 2 3 4), (4 6 8, 4 12 16, 8 12 16, 8 6 4, 4 6 8)), ((0 0 0, 0 300 4000, 200 300 400, 200 0 -400, 0 0 0)))"); + + GEOSGeom_destroy(geom); + GEOSGeom_destroy(out); +} + +// callback should modify geometry collection coordinates +template <> +template <> +void object::test<9>() { + GEOSGeometry* geom = GEOSGeomFromWKT( + "GEOMETRYCOLLECTION (POINT (1 1 1), LINESTRING (1 1 1, 2 2 2), POLYGON ((1 1 1, 1 2 3, 2 2 2, 4 2 1, 1 1 1)))"); + GEOSGeometry* out = GEOSGeom_transformXYZ(geom, SCALE_2_3_4, nullptr); + + ensure(out != nullptr); + ensure_geometry_equals(out, "GEOMETRYCOLLECTION (POINT (2 3 4), LINESTRING (2 3 4, 4 6 8), POLYGON ((2 3 4, 2 6 12, 4 6 8, 8 6 4, 2 3 4)))"); + + GEOSGeom_destroy(geom); + GEOSGeom_destroy(out); +} + +// should not fail for empty geometry +template <> +template <> +void object::test<10>() { + GEOSGeometry* geom = GEOSGeomFromWKT("POINT EMPTY"); + + GEOSGeometry* out = GEOSGeom_transformXYZ(geom, SCALE_2_3_4, nullptr); + + ensure(out != nullptr); + ensure_geometry_equals(out, "POINT EMPTY"); + + GEOSGeom_destroy(geom); + GEOSGeom_destroy(out); +} + +// should retain original coords even if they collapse to same coordinate +template <> +template <> +void object::test<11>() { + GEOSGeometry* geom = GEOSGeomFromWKT("LINESTRING (1 1 1, 2 2 2)"); + + GEOSGeometry* out = GEOSGeom_transformXYZ( + geom, + [](double* x, double* y, double* z, void* userdata) { + *x = 0; + *y = 0; + *z = 0; + (void)(userdata); // make unused parameter warning go away + return 1; + }, + nullptr); + + ensure(out != nullptr); + ensure_equals(GEOSGetNumCoordinates(out), 2); + + // Cannot construct WKT for this case, must test coords directly + const GEOSCoordSequence* seq = GEOSGeom_getCoordSeq(out); + + double x, y, z; + GEOSCoordSeq_getXYZ(seq, 0, &x, &y, &z); + ensure_equals(x, 0); + ensure_equals(y, 0); + ensure_equals(z, 0); + + GEOSCoordSeq_getXYZ(seq, 1, &x, &y, &z); + ensure_equals(x, 0); + ensure_equals(y, 0); + ensure_equals(z, 0); + + GEOSGeom_destroy(geom); + GEOSGeom_destroy(out); +} + +// should pass through userdata +template <> +template <> +void object::test<12>() { + GEOSGeometry* geom = GEOSGeomFromWKT("LINESTRING (1 1 1, 2 2 2)"); + + double userdata_scale = 5.0; + + GEOSGeometry* out = GEOSGeom_transformXYZ( + geom, + [](double* x, double* y, double* z, void* userdata) { + double scale = *(double *)(userdata); + (*x) *= scale; + (*y) *= scale; + (*z) *= scale; + return 1; + }, + (void *)(&userdata_scale)); + + ensure(out != nullptr); + ensure_geometry_equals(out, "LINESTRING (5 5 5, 10 10 10)"); + + GEOSGeom_destroy(geom); + GEOSGeom_destroy(out); +} + +// transform should preserve existing M coordinate values +template <> +template <> +void object::test<13>() { + GEOSGeometry* geom = GEOSGeomFromWKT("POINT ZM (1 1 1 5)"); + + GEOSGeometry* out = GEOSGeom_transformXYZ(geom, SCALE_2_3_4, nullptr); + + ensure(out != nullptr); + ensure_geometry_equals(out, "POINT ZM (2 3 4 5)"); + + GEOSGeom_destroy(geom); + GEOSGeom_destroy(out); +} + +template <> +template <> +void object::test<14>() { + input_ = GEOSGeomFromWKT("CIRCULARSTRING Z (0 0 0, 1 1 1, 2 1 0)"); + ensure(input_); + + result_ = GEOSGeom_transformXYZ(input_, SCALE_2_3_4, nullptr); + + ensure_equals(toWKT(result_), "CIRCULARSTRING Z (0 0 0, 2 3 4, 4 3 0)"); +} + +// callback should succed on 2D geometry +template <> +template <> +void object::test<15>() { + GEOSGeometry* geom = GEOSGeomFromWKT("POINT (1 1)"); + + GEOSGeometry* out = GEOSGeom_transformXYZ(geom, SCALE_2_3_4, nullptr); + + ensure(out != nullptr); + ensure_geometry_equals(out, "POINT (2 3)"); + + int dims = GEOSGeom_getCoordinateDimension(geom); + ensure_equals(dims, 2); + + GEOSGeom_destroy(geom); + GEOSGeom_destroy(out); +} + +} // namespace tut