Skip to content

Commit

Permalink
feat: add simplify_polygon_hull method (#366)
Browse files Browse the repository at this point in the history
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).

- libgeos/geos#603
- locationtech/jts#861
- libgeos/geos@1b3521c
  • Loading branch information
oleksii-leonov committed Jan 7, 2024
1 parent f7f1451 commit b931b95
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 0 deletions.
6 changes: 6 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -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**
Expand Down
1 change: 1 addition & 0 deletions ext/geos_c_impl/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions ext/geos_c_impl/polygon.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions ext/geos_c_impl/preface.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
154 changes: 154 additions & 0 deletions test/geos_capi/polygon_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down

0 comments on commit b931b95

Please sign in to comment.