Repository: geopandas/geopandas
Pull request #2910 (2023-05-30 06:51:51)

ENH: add segmentize as a GeoSeries method
Implements the shapely segmentize method as part of https://github.com/geopandas/geopandas/issues/2010


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fd5777aa..8bf10b95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 New methods:
 
+- Added ``segmentize`` method from shapely to GeoSeries/GeoDataFrame (#2910).
 - Added ``extract_unique_points`` method from shapely to GeoSeries/GeoDataframe (#2915).
 - Added ``hausdorff_distance`` method from shapely to GeoSeries/GeoDataframe (#2909).
 - Added ``delaunay_triangles`` method from shapely to GeoSeries/GeoDataframe (#2907).
diff --git a/doc/source/docs/reference/geoseries.rst b/doc/source/docs/reference/geoseries.rst
index b6726b6b..755b56a2 100644
--- a/doc/source/docs/reference/geoseries.rst
+++ b/doc/source/docs/reference/geoseries.rst
@@ -97,6 +97,7 @@ Constructive methods and attributes
    GeoSeries.minimum_bounding_circle
    GeoSeries.normalize
    GeoSeries.sample_points
+   GeoSeries.segmentize
    GeoSeries.simplify
 
 Affine transformations
diff --git a/doc/source/docs/user_guide/geometric_manipulations.rst b/doc/source/docs/user_guide/geometric_manipulations.rst
index 8aa07a7d..4d49e1ed 100644
--- a/doc/source/docs/user_guide/geometric_manipulations.rst
+++ b/doc/source/docs/user_guide/geometric_manipulations.rst
@@ -65,6 +65,11 @@ Constructive methods
   Returns a :class:`~geopandas.GeoSeries` containing a simplified representation of
   each object.
 
+.. method:: GeoSeries.segmentize(max_segment_length)
+
+  Returns a :class:`~geopandas.GeoSeries` with additional vertices added to line
+  segments based on max_segment_length.
+
 .. attribute:: GeoSeries.unary_union
 
   Return a geometry containing the union of all geometries in the :class:`~geopandas.GeoSeries`.
diff --git a/doc/source/gallery/polygon_plotting_with_folium.ipynb b/doc/source/gallery/polygon_plotting_with_folium.ipynb
index 3a71ffb1..1da13782 100644
--- a/doc/source/gallery/polygon_plotting_with_folium.ipynb
+++ b/doc/source/gallery/polygon_plotting_with_folium.ipynb
@@ -64,7 +64,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Notice that the values of the polygon geometries do not directly represent the values of latitude of longitude in a geographic coordinate system.\n",
+    "Notice that the values of the polygon geometries do not directly represent the values of latitude or longitude in a geographic coordinate system.\n",
     "To view the coordinate reference system of the geometry column, access the `crs` attribute:"
    ]
   },
diff --git a/doc/source/getting_started/introduction.ipynb b/doc/source/getting_started/introduction.ipynb
index b97baacf..a23a0946 100644
--- a/doc/source/getting_started/introduction.ipynb
+++ b/doc/source/getting_started/introduction.ipynb
@@ -319,7 +319,7 @@
    "source": [
     "# saving the first plot as an axis and setting alpha (transparency) to 0.5\n",
     "ax = gdf[\"convex_hull\"].plot(alpha=0.5)\n",
-    "# passing the first plot and setting linewitdth to 0.5\n",
+    "# passing the first plot and setting linewidth to 0.5\n",
     "gdf[\"boundary\"].plot(ax=ax, color=\"white\", linewidth=0.5)"
    ]
   },
@@ -355,7 +355,7 @@
     "ax = gdf[\"buffered\"].plot(alpha=0.5)\n",
     "# passing the first plot as an axis to the second\n",
     "gdf[\"buffered_centroid\"].plot(ax=ax, color=\"red\", alpha=0.5)\n",
-    "# passing the first plot and setting linewitdth to 0.5\n",
+    "# passing the first plot and setting linewidth to 0.5\n",
     "gdf[\"boundary\"].plot(ax=ax, color=\"white\", linewidth=0.5)"
    ]
   },
@@ -457,7 +457,7 @@
     "ax = gdf.plot(\n",
     "    \"within\", legend=True, categorical=True, legend_kwds={\"loc\": \"upper left\"}\n",
     ")\n",
-    "# passing the first plot and setting linewitdth to 0.5\n",
+    "# passing the first plot and setting linewidth to 0.5\n",
     "gdf[\"boundary\"].plot(ax=ax, color=\"black\", linewidth=0.5)"
    ]
   },
diff --git a/geopandas/_vectorized.py b/geopandas/_vectorized.py
index e667584f..6ec06bbf 100644
--- a/geopandas/_vectorized.py
+++ b/geopandas/_vectorized.py
@@ -759,6 +759,18 @@ def minimum_bounding_radius(data):
         )
 
 
+def segmentize(data, max_segment_length):
+    if compat.USE_SHAPELY_20:
+        return shapely.segmentize(data, max_segment_length)
+    elif compat.USE_PYGEOS:
+        return pygeos.segmentize(data, max_segment_length)
+    else:
+        raise NotImplementedError(
+            "shapely >= 2.0 or PyGEOS is required, "
+            f"version {shapely.__version__} is installed"
+        )
+
+
 #
 # Binary predicates
 #
diff --git a/geopandas/array.py b/geopandas/array.py
index fa860e62..b93f81aa 100644
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -570,6 +570,12 @@ class GeometryArray(ExtensionArray):
     def make_valid(self):
         return GeometryArray(vectorized.make_valid(self._data), crs=self.crs)
 
+    def segmentize(self, max_segment_length):
+        return GeometryArray(
+            vectorized.segmentize(self._data, max_segment_length),
+            crs=self.crs,
+        )
+
     #
     # Binary predicates
     #
diff --git a/geopandas/base.py b/geopandas/base.py
index 8f0eb4fc..c59a7473 100644
--- a/geopandas/base.py
+++ b/geopandas/base.py
@@ -1027,6 +1027,47 @@ GeometryCollection
         """
         return _delegate_geo_method("make_valid", self)
 
+    def segmentize(self, max_segment_length):
+        """Returns a ``GeoSeries`` with vertices added to line segments based on
+        maximum segment length.
+
+        Additional vertices will be added to every line segment in an input geometry so
+        that segments are no longer than the provided maximum segment length. New
+        vertices will evenly subdivide each segment. Only linear components of input
+        geometries are densified; other geometries are returned unmodified.
+
+        Parameters
+        ----------
+        max_segment_length : float | array-like
+            Additional vertices will be added so that all line segments are no longer
+            than this value. Must be greater than 0.
+
+        Returns
+        -------
+        GeoSeries
+
+        Examples
+        --------
+        >>> from shapely.geometry import Polygon, LineString
+        >>> s = geopandas.GeoSeries(
+        ...     [
+        ...         LineString([(0, 0), (0, 10)]),
+        ...         Polygon([(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)]),
+        ...     ],
+        ...     crs=3857
+        ... )
+        >>> s
+        0               LINESTRING (0.000 0.000, 0.000 10.000)
+        1    POLYGON ((0.000 0.000, 10.000 0.000, 10.000 10...
+        dtype: geometry
+
+        >>> s.segmentize(max_segment_length=5)
+        0    LINESTRING (0.000 0.000, 0.000 5.000, 0.000 10...
+        1    POLYGON ((0.000 0.000, 5.000 0.000, 10.000 0.0...
+        dtype: geometry
+        """
+        return _delegate_geo_method("segmentize", self, max_segment_length)
+
     #
     # Reduction operations that return a Shapely geometry
     #


Test patch:
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index 0e907b0e..af73065c 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -832,6 +832,70 @@ class TestGeomMethods:
         ):
             s.make_valid()
 
+    @pytest.mark.skipif(
+        (compat.USE_PYGEOS or compat.USE_SHAPELY_20),
+        reason="segmentize keyword introduced in shapely 2.0",
+    )
+    def test_segmentize_shapely_pre20(self):
+        s = GeoSeries([Point(1, 1)])
+        with pytest.raises(
+            NotImplementedError,
+            match=f"shapely >= 2.0 or PyGEOS is required, "
+            f"version {shapely.__version__} is installed",
+        ):
+            s.segmentize(max_segment_length=1)
+
+    @pytest.mark.skipif(
+        not (compat.USE_PYGEOS or compat.USE_SHAPELY_20),
+        reason="segmentize keyword introduced in shapely 2.0",
+    )
+    def test_segmentize_linestrings(self):
+        expected_g1 = GeoSeries(
+            [
+                Polygon(
+                    (
+                        (
+                            (0, 0),
+                            (0.5, 0),
+                            (1, 0),
+                            (1, 0.5),
+                            (1, 1),
+                            (0.6666666666666666, 0.6666666666666666),
+                            (0.3333333333333333, 0.3333333333333333),
+                            (0, 0),
+                        )
+                    )
+                ),
+                Polygon(
+                    (
+                        (
+                            (0, 0),
+                            (0.5, 0),
+                            (1, 0),
+                            (1, 0.5),
+                            (1, 1),
+                            (0.5, 1),
+                            (0, 1),
+                            (0, 0.5),
+                            (0, 0),
+                        )
+                    )
+                ),
+            ]
+        )
+        expected_g5 = GeoSeries(
+            [
+                LineString([(0, 0), (0, 0.5), (0, 1), (0.5, 1), (1, 1)]),
+                LineString(
+                    [(0, 0), (0.5, 0), (1, 0), (1, 0.5), (1, 1), (0.5, 1), (0, 1)]
+                ),
+            ]
+        )
+        result_g1 = self.g1.segmentize(max_segment_length=0.5)
+        result_g5 = self.g5.segmentize(max_segment_length=0.5)
+        assert_geoseries_equal(expected_g1, result_g1)
+        assert_geoseries_equal(expected_g5, result_g5)
+
     @pytest.mark.skipif(
         compat.SHAPELY_GE_20,
         reason="concave_hull is implemented for shapely >= 2.0",