From 6d095ceaf135a9c9a9b627958924bec8fecbabac Mon Sep 17 00:00:00 2001 From: Oleksii Leonov Date: Fri, 24 Nov 2023 08:59:38 -0300 Subject: [PATCH 1/3] feat: add simplify_polygon_hull method The feature is equal to [ST_SimplifyPolygonHull](https://postgis.net/docs/ST_SimplifyPolygonHull.html) in PostGIS. > Computes a simplified topology-preserving outer or inner hull of a polygonal geometry. > An outer hull completely covers the input geometry. > An inner hull is completely covered by the input geometry. > The result is a polygonal geometry formed by a subset of the input vertices. > MultiPolygons and holes are handled and produce a result with the same structure as the input. > https://postgis.net/docs/ST_SimplifyPolygonHull.html Utilizes the `GEOSPolygonHullSimplify` method introduced in [GEOS 3.11.0](https://github.com/libgeos/geos/releases/tag/3.11.0). - https://github.com/libgeos/geos/issues/603 - https://github.com/locationtech/jts/pull/861 - https://github.com/libgeos/geos/commit/1b3521ccfb4de7fb4bd15ebfa4772b2da8155f30 --- History.md | 6 ++ ext/geos_c_impl/extconf.rb | 1 + ext/geos_c_impl/geometry.c | 34 ++++++++ ext/geos_c_impl/preface.h | 3 + test/geos_capi/polygon_test.rb | 154 +++++++++++++++++++++++++++++++++ 5 files changed, 198 insertions(+) diff --git a/History.md b/History.md index fb598886..4abda101 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,9 @@ +### Unreleased + +**Minor Changes** + +* Add `simplify_polygon_hull` method to the CAPI factory (@oleksii-leonov) [#366](https://github.com/rgeo/rgeo/pull/366) + ### 3.0.1 / 2023-11-15 **Minor Changes** diff --git a/ext/geos_c_impl/extconf.rb b/ext/geos_c_impl/extconf.rb index d3134c25..b892e85e 100644 --- a/ext/geos_c_impl/extconf.rb +++ b/ext/geos_c_impl/extconf.rb @@ -46,6 +46,7 @@ def create_dummy_makefile have_func("GEOSUnaryUnion_r", "geos_c.h") have_func("GEOSCoordSeq_isCCW_r", "geos_c.h") have_func("GEOSDensify", "geos_c.h") + have_func("GEOSPolygonHullSimplify", "geos_c.h") have_func("rb_memhash", "ruby.h") have_func("rb_gc_mark_movable", "ruby.h") end diff --git a/ext/geos_c_impl/geometry.c b/ext/geos_c_impl/geometry.c index 321c01ea..8e510cf8 100644 --- a/ext/geos_c_impl/geometry.c +++ b/ext/geos_c_impl/geometry.c @@ -831,6 +831,32 @@ method_geometry_simplify_preserve_topology(VALUE self, VALUE tolerance) return result; } +static VALUE +method_geometry_simplify_polygon_hull(VALUE self, + VALUE vertex_fraction, + VALUE is_outer) +{ + VALUE result; + RGeo_GeometryData* self_data; + const GEOSGeometry* self_geom; + VALUE factory; + + unsigned int is_outer_uint = RTEST(is_outer) ? 1 : 0; + + result = Qnil; + self_data = RGEO_GEOMETRY_DATA_PTR(self); + self_geom = self_data->geom; + if (self_geom) { + factory = self_data->factory; + result = rgeo_wrap_geos_geometry( + factory, + GEOSPolygonHullSimplify( + self_geom, is_outer_uint, rb_num2dbl(vertex_fraction)), + Qnil); + } + return result; +} + static VALUE method_geometry_convex_hull(VALUE self) { @@ -1329,6 +1355,14 @@ rgeo_init_geos_geometry() geos_geometry_methods, "make_valid", method_geometry_make_valid, 0); rb_define_method( geos_geometry_methods, "polygonize", method_geometry_polygonize, 0); + +#ifdef RGEO_GEOS_SUPPORTS_POLYGON_HULL_SIMPLIFY + rb_define_method(geos_geometry_methods, + "simplify_polygon_hull", + method_geometry_simplify_polygon_hull, + 2); +#endif + #ifdef RGEO_GEOS_SUPPORTS_DENSIFY rb_define_method( geos_geometry_methods, "segmentize", method_geometry_segmentize, 1); diff --git a/ext/geos_c_impl/preface.h b/ext/geos_c_impl/preface.h index 08427600..e08e1c05 100644 --- a/ext/geos_c_impl/preface.h +++ b/ext/geos_c_impl/preface.h @@ -26,6 +26,9 @@ #ifdef HAVE_GEOSDENSIFY #define RGEO_GEOS_SUPPORTS_DENSIFY #endif +#ifdef HAVE_GEOSPOLYGONHULLSIMPLIFY +#define RGEO_GEOS_SUPPORTS_POLYGON_HULL_SIMPLIFY +#endif #ifdef HAVE_RB_GC_MARK_MOVABLE #define mark rb_gc_mark_movable #else diff --git a/test/geos_capi/polygon_test.rb b/test/geos_capi/polygon_test.rb index f69f9606..3c881473 100644 --- a/test/geos_capi/polygon_test.rb +++ b/test/geos_capi/polygon_test.rb @@ -98,6 +98,160 @@ def test_simplify_preserve_topology end end + def test_simplify_polygon_hull + skip_geos_version_less_then("3.11") + + # Input polygon (8 vertices): + # +-----+ + # | | + # +---+ | + # | | + # +---+ | + # | | + # +-----+ + input_polygon = @factory.parse_wkt("POLYGON ((0 0, 0 2, 4 2, 4 4, 0 4, 0 6, 6 6, 6 0, 0 0))") + + # Exected polygon with `is_outer` true and `vertex_fraction` 0.0 (minimum possible to cover the polygon): + # +-----+ + # | | + # | | + # | | + # | | + # | | + # +-----+ + expected_polygon_outer_true_vert0 = @factory.parse_wkt("POLYGON ((0 0, 0 6, 6 6, 6 0, 0 0))") + + # Exected polygon with `is_outer` true and `vertex_fraction` 0.500001 (4 vertices): + # +-----+ + # | | + # | | + # | | + # | | + # | | + # +-----+ + expected_polygon_outer_true_vert0500001 = @factory.parse_wkt("POLYGON ((0 0, 0 6, 6 6, 6 0, 0 0))") + + # Exected polygon with `is_outer` true and `vertex_fraction` 0.750001 (6 vertices): + # +-----+ + # | | + # + | + # | | + # + | + # | | + # +-----+ + expected_polygon_outer_true_vert0750001 = @factory.parse_wkt("POLYGON ((0 0, 0 2, 0 4, 0 6, 6 6, 6 0, 0 0))") + + # Exected polygon with `is_outer` true and `vertex_fraction` 1.0 (all vertices): + # +-----+ + # | | + # +---+ | + # | | + # +---+ | + # | | + # +-----+ + expected_polygon_outer_true_vert1 = input_polygon + + # Exected polygon with `is_outer` false and `vertex_fraction` 0 (minimum possible, triangle): + # Version 1: + # +-----+ + # \ / + # + + # + # + # + # + # Version 2: + # + + # /| + # + | + # | | + # \| + # || + # + + # NOTE: We could receve 2 different results here, depending on the GEOS version and OS. + # Both are valid results, so we check for any. + expected_polygons_outer_false_vert0 = [ + @factory.parse_wkt("POLYGON ((6 6, 0 6, 4 4, 6 6))"), + @factory.parse_wkt("POLYGON ((6 0, 6 6, 4 2, 6 0))") + ] + + # Exected polygon with `is_outer` false and `vertex_fraction` 0.5 (3 vertices): + # NOTE: `vertex_fraction` 0.5 shoud give us 4 vertices (8 * 0.5). But we have only 3 vertices in the result. + # To get 4 vertices in the result we need to use `vertex_fraction` 0.500001. + # Documenting this behavior of GEOSPolygonHullSimplify as is. + expected_polygons_outer_false_vert05 = expected_polygons_outer_false_vert0 + + # Exected polygon with `is_outer` false and `vertex_fraction` 0.500001 (4 vertices): + # Version 1: + # +-----+ + # \ | + # + | + # | | + # \| + # || + # + + # Version 2: + # + + # || + # /| + # | | + # + | + # / | + # +-----+ + # NOTE: We could receve 2 different results here, depending on the GEOS version and OS. + # Both are valid results, so we check for any. + expected_polygons_outer_false_vert0500001 = [ + @factory.parse_wkt("POLYGON ((6 0, 6 6, 0 6, 4 4, 6 0))"), + @factory.parse_wkt("POLYGON ((0 0, 6 0, 6 6, 4 2, 0 0))") + ] + + # Exected polygon with `is_outer` false and `vertex_fraction` 1.0 (all vertices): + # +-----+ + # | | + # +---+ | + # | | + # +---+ | + # | | + # +-----+ + expected_polygon_outer_false_vert1 = input_polygon + + # With `is_outer` true: + assert_equal( + expected_polygon_outer_true_vert0, + input_polygon.simplify_polygon_hull(0.0, true) + ) + assert_equal( + expected_polygon_outer_true_vert0500001, + input_polygon.simplify_polygon_hull(0.500001, true) + ) + assert_equal( + expected_polygon_outer_true_vert0750001, + input_polygon.simplify_polygon_hull(0.750001, true) + ) + assert_equal( + expected_polygon_outer_true_vert1, + input_polygon.simplify_polygon_hull(1.0, true) + ) + + # With `is_outer` false: + assert_includes( + expected_polygons_outer_false_vert0, + input_polygon.simplify_polygon_hull(0.0, false) + ) + assert_includes( + expected_polygons_outer_false_vert05, + input_polygon.simplify_polygon_hull(0.5, false) + ) + assert_includes( + expected_polygons_outer_false_vert0500001, + input_polygon.simplify_polygon_hull(0.500001, false) + ) + assert_equal( + expected_polygon_outer_false_vert1, + input_polygon.simplify_polygon_hull(1.0, false) + ) + end + def test_buffer_with_style polygon_coordinates = [[0.514589803375032, 4.299999999999999], [6.0, 4.3], From aec1d8ad1ce50d33f402e5950ac3634b0f9a3501 Mon Sep 17 00:00:00 2001 From: Oleksii Leonov Date: Wed, 29 Nov 2023 17:08:55 +0000 Subject: [PATCH 2/3] fix: wrap method_geometry_simplify_polygon_hull with a guard --- ext/geos_c_impl/geometry.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ext/geos_c_impl/geometry.c b/ext/geos_c_impl/geometry.c index 8e510cf8..141d0dd2 100644 --- a/ext/geos_c_impl/geometry.c +++ b/ext/geos_c_impl/geometry.c @@ -831,6 +831,7 @@ method_geometry_simplify_preserve_topology(VALUE self, VALUE tolerance) return result; } +#ifdef RGEO_GEOS_SUPPORTS_POLYGON_HULL_SIMPLIFY static VALUE method_geometry_simplify_polygon_hull(VALUE self, VALUE vertex_fraction, @@ -856,6 +857,7 @@ method_geometry_simplify_polygon_hull(VALUE self, } return result; } +#endif static VALUE method_geometry_convex_hull(VALUE self) From 750f38f98f64f9ce9097df30e45cfa8cfef22e32 Mon Sep 17 00:00:00 2001 From: Oleksii Leonov Date: Sat, 2 Dec 2023 15:36:58 +0000 Subject: [PATCH 3/3] refactor: move simplify_polygon_hull to polygon.c --- ext/geos_c_impl/geometry.c | 36 ------------------------------------ ext/geos_c_impl/polygon.c | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/ext/geos_c_impl/geometry.c b/ext/geos_c_impl/geometry.c index 141d0dd2..321c01ea 100644 --- a/ext/geos_c_impl/geometry.c +++ b/ext/geos_c_impl/geometry.c @@ -831,34 +831,6 @@ method_geometry_simplify_preserve_topology(VALUE self, VALUE tolerance) return result; } -#ifdef RGEO_GEOS_SUPPORTS_POLYGON_HULL_SIMPLIFY -static VALUE -method_geometry_simplify_polygon_hull(VALUE self, - VALUE vertex_fraction, - VALUE is_outer) -{ - VALUE result; - RGeo_GeometryData* self_data; - const GEOSGeometry* self_geom; - VALUE factory; - - unsigned int is_outer_uint = RTEST(is_outer) ? 1 : 0; - - result = Qnil; - self_data = RGEO_GEOMETRY_DATA_PTR(self); - self_geom = self_data->geom; - if (self_geom) { - factory = self_data->factory; - result = rgeo_wrap_geos_geometry( - factory, - GEOSPolygonHullSimplify( - self_geom, is_outer_uint, rb_num2dbl(vertex_fraction)), - Qnil); - } - return result; -} -#endif - static VALUE method_geometry_convex_hull(VALUE self) { @@ -1357,14 +1329,6 @@ rgeo_init_geos_geometry() geos_geometry_methods, "make_valid", method_geometry_make_valid, 0); rb_define_method( geos_geometry_methods, "polygonize", method_geometry_polygonize, 0); - -#ifdef RGEO_GEOS_SUPPORTS_POLYGON_HULL_SIMPLIFY - rb_define_method(geos_geometry_methods, - "simplify_polygon_hull", - method_geometry_simplify_polygon_hull, - 2); -#endif - #ifdef RGEO_GEOS_SUPPORTS_DENSIFY rb_define_method( geos_geometry_methods, "segmentize", method_geometry_segmentize, 1); diff --git a/ext/geos_c_impl/polygon.c b/ext/geos_c_impl/polygon.c index 23ee6ce2..6269e263 100644 --- a/ext/geos_c_impl/polygon.c +++ b/ext/geos_c_impl/polygon.c @@ -231,6 +231,34 @@ method_polygon_interior_rings(VALUE self) return result; } +#ifdef RGEO_GEOS_SUPPORTS_POLYGON_HULL_SIMPLIFY +static VALUE +method_polygon_simplify_polygon_hull(VALUE self, + VALUE vertex_fraction, + VALUE is_outer) +{ + VALUE result; + RGeo_GeometryData* self_data; + const GEOSGeometry* self_geom; + VALUE factory; + + unsigned int is_outer_uint = RTEST(is_outer) ? 1 : 0; + + result = Qnil; + self_data = RGEO_GEOMETRY_DATA_PTR(self); + self_geom = self_data->geom; + if (self_geom) { + factory = self_data->factory; + result = rgeo_wrap_geos_geometry( + factory, + GEOSPolygonHullSimplify( + self_geom, is_outer_uint, rb_num2dbl(vertex_fraction)), + Qnil); + } + return result; +} +#endif + static VALUE cmethod_create(VALUE module, VALUE factory, @@ -335,6 +363,13 @@ rgeo_init_geos_polygon() geos_polygon_methods, "interior_rings", method_polygon_interior_rings, 0); rb_define_method( geos_polygon_methods, "coordinates", method_polygon_coordinates, 0); + +#ifdef RGEO_GEOS_SUPPORTS_POLYGON_HULL_SIMPLIFY + rb_define_method(geos_polygon_methods, + "simplify_polygon_hull", + method_polygon_simplify_polygon_hull, + 2); +#endif } st_index_t