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],