Repository: shapely/shapely
Pull request #2106 (2024-08-12 21:15:59)

ENH: add function minimum_clearance_line
ref #1570

coveralls: ## Pull Request Test Coverage Report for [Build 10359937307](https://coveralls.io/builds/69182685)


### Details

* **3** of **3**   **(100.0%)**  changed or added relevant lines in **1** file are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.3%**) to **88.17%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/69182685/badge)](https://coveralls.io/builds/69182685) |
| :-- | --: |
| Change from base [Build 10262616924](https://coveralls.io/builds/69061291): |  0.3% |
| Covered Lines: | 2631 |
| Relevant Lines: | 2984 |

---
##### 💛  - [Coveralls](https://coveralls.io)

theroggy: > Thanks @theroggy!
> 
> For the API, I was checking PostGIS and they seem to expose two versions: one that returns just the distance ([`ST_MinimumClearance`](https://postgis.net/docs/ST_MinimumClearance.html), which might often be what one wants), and one that returns the line ([`ST_MinimumClearanceLine`](https://postgis.net/docs/ST_MinimumClearanceLine.html)).
> 
> So we need to decide whether we want to do that here as well or not.

The version that just returns the distance is already available in shapely: https://shapely.readthedocs.io/en/stable/reference/shapely.minimum_clearance.html#shapely.minimum_clearance
jorisvandenbossche: Whoops, sorry ;) Was going through it a bit too quickly, missed that the function being added is already called `.._line` and that we already have the basic form!

Patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index d602d07..9aa1599 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -27,6 +27,7 @@ Improvements:
 - The ``voronoi_polygons`` now accepts the ``ordered`` keyword, optionally forcing the
   order of polygons within the GeometryCollection to follow the order of input
   coordinates. Requires at least GEOS 3.12. (#1968)
+- Add function ``minimum_clearance_line`` (#2106)
 
 Breaking changes in GEOS 3.12:
 
diff --git a/shapely/constructive.py b/shapely/constructive.py
index 2b921c0..acfd2d5 100644
--- a/shapely/constructive.py
+++ b/shapely/constructive.py
@@ -25,6 +25,7 @@ __all__ = [
     "build_area",
     "make_valid",
     "normalize",
+    "minimum_clearance_line",
     "node",
     "point_on_surface",
     "polygonize",
@@ -642,6 +643,41 @@ def make_valid(geometry, method="linework", keep_collapsed=True, **kwargs):
         raise ValueError(f"Unknown method: {method}")
 
 
+@multithreading_enabled
+def minimum_clearance_line(geometry, **kwargs):
+    """Return a LineString whose endpoints define the minimum clearance.
+
+    A geometry's "minimum clearance" is the smallest distance by which a vertex
+    of the geometry could be moved to produce an invalid geometry.
+
+    If the geometry has no minimum clearance, an empty LineString will be
+    returned.
+
+    .. versionadded:: 2.1.0
+
+    Parameters
+    ----------
+    geometry : Geometry or array_like
+        Geometry or geometries to determine the minimum clearance line for.
+    **kwargs
+        For other keyword-only arguments, see the
+        `NumPy ufunc docs <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
+
+    Examples
+    --------
+    >>> from shapely import Polygon
+    >>> poly = Polygon([(0, 0), (10, 0), (10, 10), (5, 5), (0, 10), (0, 0)])
+    >>> minimum_clearance_line(poly)
+    <LINESTRING (5 5, 5 0)>
+
+    See Also
+    --------
+    minimum_clearance
+
+    """
+    return lib.minimum_clearance_line(geometry, **kwargs)
+
+
 @multithreading_enabled
 def normalize(geometry, **kwargs):
     """Convert Geometry to strict normal form (or canonical form).
diff --git a/shapely/measurement.py b/shapely/measurement.py
index 09744dd..066d966 100644
--- a/shapely/measurement.py
+++ b/shapely/measurement.py
@@ -301,6 +301,10 @@ def minimum_clearance(geometry, **kwargs):
     >>> minimum_clearance(None)
     nan
 
+    See Also
+    --------
+    minimum_clearance_line
+
     """
     return lib.minimum_clearance(geometry, **kwargs)
 
diff --git a/src/ufuncs.c b/src/ufuncs.c
index 0c96bb5..5ea07b7 100644
--- a/src/ufuncs.c
+++ b/src/ufuncs.c
@@ -509,6 +509,7 @@ static void* unary_union_data[1] = {GEOSUnaryUnion_r};
 static void* point_on_surface_data[1] = {GEOSPointOnSurface_r};
 static void* centroid_data[1] = {GEOSGetCentroid_r};
 static void* line_merge_data[1] = {GEOSLineMerge_r};
+static void* minimum_clearance_line_data[1] = {GEOSMinimumClearanceLine_r};
 static void* node_data[1] = {GEOSNode_r};
 static void* extract_unique_points_data[1] = {GEOSGeom_extractUniquePoints_r};
 static void* GetExteriorRing(void* context, void* geom) {
@@ -3701,6 +3702,7 @@ int init_ufuncs(PyObject* m, PyObject* d) {
   DEFINE_Y_Y(point_on_surface);
   DEFINE_Y_Y(centroid);
   DEFINE_Y_Y(line_merge);
+  DEFINE_Y_Y(minimum_clearance_line);
   DEFINE_Y_Y(node);
   DEFINE_Y_Y(extract_unique_points);
   DEFINE_Y_Y(get_exterior_ring);


Test patch:
diff --git a/shapely/tests/test_constructive.py b/shapely/tests/test_constructive.py
index 9cb265e..a4d195f 100644
--- a/shapely/tests/test_constructive.py
+++ b/shapely/tests/test_constructive.py
@@ -42,6 +42,7 @@ CONSTRUCTIVE_NO_ARGS = (
     ),
     shapely.envelope,
     shapely.extract_unique_points,
+    shapely.minimum_clearance_line,
     shapely.node,
     shapely.normalize,
     shapely.point_on_surface,