From 2116683b5c21ef85add07bb9dc0cbcc41e31c13d Mon Sep 17 00:00:00 2001 From: Oleksii Leonov Date: Fri, 24 Nov 2023 08:59:38 -0300 Subject: [PATCH] feat: add simplify_polygon_hull method 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 `GEOSPolygonHullSimplify` method introducesd in [GEOS 3.11.0](https://github.com/libgeos/geos/releases/tag/3.11.0). Notes: - https://github.com/libgeos/geos/issues/603 - https://github.com/locationtech/jts/pull/861 - https://github.com/libgeos/geos/commit/1b3521ccfb4de7fb4bd15ebfa4772b2da8155f30 --- 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 | 134 +++++++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+) 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..8e64050b 100644 --- a/test/geos_capi/polygon_test.rb +++ b/test/geos_capi/polygon_test.rb @@ -8,6 +8,7 @@ require_relative "../test_helper" require_relative "skip_capi" +require "irb" class GeosPolygonTest < Minitest::Test # :nodoc: include RGeo::Tests::Common::PolygonTests @@ -98,6 +99,139 @@ 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, 6 0, 6 6, 0 6, 0 4, 4 4, 4 2, 0 2, 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): + # +-----+ + # \ / + # + + # + # + # + # + expected_polygon_outer_false_vert0 = @factory.parse_wkt("POLYGON ((6 6, 0 6, 4 4, 6 6))") + + # 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_polygon_outer_false_vert05 = @factory.parse_wkt("POLYGON ((6 6, 0 6, 4 4, 6 6))") + + # Exected polygon with `is_outer` false and `vertex_fraction` 0.500001 (4 vertices): + # +-----+ + # \ | + # + | + # | | + # \| + # || + # + + expected_polygon_outer_false_vert0500001 = @factory.parse_wkt("POLYGON ((6 0, 6 6, 0 6, 4 4, 6 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( + input_polygon.simplify_polygon_hull(0.0, true), + expected_polygon_outer_true_vert0 + ) + assert_equal( + input_polygon.simplify_polygon_hull(0.500001, true), + expected_polygon_outer_true_vert0500001 + ) + assert_equal( + input_polygon.simplify_polygon_hull(0.750001, true), + expected_polygon_outer_true_vert0750001 + ) + assert_equal( + input_polygon.simplify_polygon_hull(1.0, true), + expected_polygon_outer_true_vert1 + ) + + # With `is_outer` false: + assert_equal( + input_polygon.simplify_polygon_hull(0.0, false), + expected_polygon_outer_false_vert0 + ) + assert_equal( + input_polygon.simplify_polygon_hull(0.5, false), + expected_polygon_outer_false_vert05 + ) + assert_equal( + input_polygon.simplify_polygon_hull(0.500001, false), + expected_polygon_outer_false_vert0500001 + ) + assert_equal( + input_polygon.simplify_polygon_hull(1.0, false), + expected_polygon_outer_false_vert1 + ) + end + def test_buffer_with_style polygon_coordinates = [[0.514589803375032, 4.299999999999999], [6.0, 4.3],