This is an automated email from the ASF dual-hosted git repository.

jiayu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sedona.git


The following commit(s) were added to refs/heads/master by this push:
     new 7ec2e1e172 [GH-2183] Fix ST_Buffer logic cap_style when specifying 
'side' parameter + Implement rest of geoseries.buffer() (#2184)
7ec2e1e172 is described below

commit 7ec2e1e172833a39c70ec1fcdfac43bbe9d3b8c4
Author: Peter Nguyen <[email protected]>
AuthorDate: Tue Jul 29 11:28:07 2025 -0700

    [GH-2183] Fix ST_Buffer logic cap_style when specifying 'side' parameter + 
Implement rest of geoseries.buffer() (#2184)
    
    * Implement geoseries.buffer() properly with additional parameters
    
    * Fix ST_Buffer 'side' handling logic
    
    * Add java tests
    
    * mvn spotless
    
    * Update python/sedona/geopandas/geoseries.py
    
    Co-authored-by: Copilot <[email protected]>
    
    * Make docs more clear
    
    ---------
    
    Co-authored-by: Jia Yu <[email protected]>
    Co-authored-by: Copilot <[email protected]>
---
 .../java/org/apache/sedona/common/Functions.java   | 10 ++++--
 .../org/apache/sedona/common/FunctionsTest.java    | 13 ++++++++
 docs/api/flink/Function.md                         |  2 +-
 docs/api/snowflake/vector-data/Function.md         |  2 +-
 docs/api/sql/Function.md                           |  2 +-
 python/sedona/geopandas/base.py                    | 37 ++++++++++++++++++++++
 python/sedona/geopandas/geoseries.py               | 23 +++++++++++++-
 python/sedona/spark/sql/st_functions.py            |  2 +-
 python/tests/geopandas/test_geodataframe.py        | 16 ++--------
 python/tests/geopandas/test_geoseries.py           | 20 ++++++++----
 .../tests/geopandas/test_match_geopandas_series.py | 34 +++++++++++++++++---
 11 files changed, 129 insertions(+), 32 deletions(-)

diff --git a/common/src/main/java/org/apache/sedona/common/Functions.java 
b/common/src/main/java/org/apache/sedona/common/Functions.java
index 16db38b9d7..0bab24344b 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -318,6 +318,7 @@ public class Functions {
 
     BufferParameters bufferParameters = new BufferParameters();
     String[] listParams = params.split(" ");
+    boolean endCapSpecified = false;
 
     for (String param : listParams) {
       String[] singleParam = param.split("=");
@@ -355,6 +356,7 @@ public class Functions {
                   "%s is not a valid option. Accepted options are %s.",
                   singleParam[1], Arrays.toString(endcapOptions)));
         }
+        endCapSpecified = true;
       }
       // Set join style
       else if (singleParam[0].equalsIgnoreCase(listBufferParameters[2])) {
@@ -387,12 +389,16 @@ public class Functions {
       // Set side to add buffer
       else if (singleParam[0].equalsIgnoreCase(listBufferParameters[5])) {
         if (singleParam[1].equalsIgnoreCase(sideOptions[0])) {
-          // It defaults to square end cap style when side is specified
-          bufferParameters.setEndCapStyle(BufferParameters.CAP_SQUARE);
+          // Default value is 'both'
           continue;
         } else if (singleParam[1].equalsIgnoreCase(sideOptions[1])
             || singleParam[1].equalsIgnoreCase(sideOptions[2])) {
           bufferParameters.setSingleSided(true);
+
+          // Specifying 'left' or 'right' defaults to square end cap style 
when side is specified
+          if (!endCapSpecified) {
+            bufferParameters.setEndCapStyle(BufferParameters.CAP_SQUARE);
+          }
         } else {
           throw new IllegalArgumentException(
               String.format(
diff --git a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java 
b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
index 2b5eaed681..6de3d30669 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -2863,6 +2863,19 @@ public class FunctionsTest extends TestBase {
     expected =
         "POLYGON ((107.0711 82.9289, 100 80, 92.9289 82.9289, 90 90, 92.9289 
97.0711, 100 100, 107.0711 97.0711, 110 90, 107.0711 82.9289))";
     assertEquals(expected, actual);
+
+    // Using reducePrecision in the following tests modified the geometries 
coordinates the
+    // coordinates causing the comparison to fail,
+    // so we just compare for exact match with postgis output
+    actual = Functions.asWKT(Functions.buffer(lineString, 10, false, 
"side=right endcap=flat"));
+    expected =
+        "POLYGON ((0 0, 50 70, 70 -3, 60.35541713163109 -5.642351470786002, 
45.912786454208465 47.0732505018066, 8.137334712067348 -5.812381937190963, 0 
0))";
+    assertEquals(expected, actual);
+
+    actual = Functions.asWKT(Functions.buffer(lineString, 5, false, "side=both 
endcap=square"));
+    expected =
+        "POLYGON ((45.93133264396633 72.90619096859548, 46.607775042289525 
73.6732560265092, 47.42614312881237 74.2866374708669, 48.35219759000809 
74.72067232686456, 49.34719367328685 74.9572012163925, 50.36950221028991 
74.9863281196278, 51.37635131988457 74.80683440990555, 52.32561592057597 
74.4262298392609, 53.17758018177616 73.86043834148188, 53.896599175569165 
73.13313179820986, 54.452590207913495 72.27473964233116, 54.82229143418446 
71.321175735393, 74.82229143418445 -1.678824264606 [...]
+    assertEquals(expected, actual);
   }
 
   @Test
diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md
index 567007de7d..8c63885f94 100644
--- a/docs/api/flink/Function.md
+++ b/docs/api/flink/Function.md
@@ -714,7 +714,7 @@ The optional forth parameter controls the buffer accuracy 
and style. Buffer accu
 - `endcap=round|flat|square` : End cap style (default is `round`). `butt` is 
an accepted synonym for `flat`.
 - `join=round|mitre|bevel` : Join style (default is `round`). `miter` is an 
accepted synonym for `mitre`.
 - `mitre_limit=#.#` : mitre ratio limit and it only affects mitred join style. 
`miter_limit` is an accepted synonym for `mitre_limit`.
-- `side=both|left|right` : The option `left` or `right` enables a single-sided 
buffer operation on the geometry, with the buffered side aligned according to 
the direction of the line. This functionality is specific to LINESTRING 
geometry and has no impact on POINT or POLYGON geometries. By default, square 
end caps are applied.
+- `side=both|left|right` : Defaults to `both`. Setting `left` or `right` 
enables a single-sided buffer operation on the geometry, with the buffered side 
aligned according to the direction of the line. This functionality is specific 
to LINESTRING geometry and has no impact on POINT or POLYGON geometries. By 
default, square end caps are applied when `left` or `right` are specified.
 
 !!!note
     `ST_Buffer` throws an `IllegalArgumentException` if the correct format, 
parameters, or options are not provided.
diff --git a/docs/api/snowflake/vector-data/Function.md 
b/docs/api/snowflake/vector-data/Function.md
index de22dcc5a7..34b06a9e96 100644
--- a/docs/api/snowflake/vector-data/Function.md
+++ b/docs/api/snowflake/vector-data/Function.md
@@ -546,7 +546,7 @@ The optional forth parameter controls the buffer accuracy 
and style. Buffer accu
 - `endcap=round|flat|square` : End cap style (default is `round`). `butt` is 
an accepted synonym for `flat`.
 - `join=round|mitre|bevel` : Join style (default is `round`). `miter` is an 
accepted synonym for `mitre`.
 - `mitre_limit=#.#` : mitre ratio limit and it only affects mitred join style. 
`miter_limit` is an accepted synonym for `mitre_limit`.
-- `side=both|left|right` : The option `left` or `right` enables a single-sided 
buffer operation on the geometry, with the buffered side aligned according to 
the direction of the line. This functionality is specific to LINESTRING 
geometry and has no impact on POINT or POLYGON geometries. By default, square 
end caps are applied.
+- `side=both|left|right` : Defaults to `both`. Setting `left` or `right` 
enables a single-sided buffer operation on the geometry, with the buffered side 
aligned according to the direction of the line. This functionality is specific 
to LINESTRING geometry and has no impact on POINT or POLYGON geometries. By 
default, square end caps are applied when `left` or `right` are specified.
 
 !!!note
     `ST_Buffer` throws an `IllegalArgumentException` if the correct format, 
parameters, or options are not provided.
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index c95ae468d8..7f3e2c08dc 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -788,7 +788,7 @@ The optional forth parameter controls the buffer accuracy 
and style. Buffer accu
 - `endcap=round|flat|square` : End cap style (default is `round`). `butt` is 
an accepted synonym for `flat`.
 - `join=round|mitre|bevel` : Join style (default is `round`). `miter` is an 
accepted synonym for `mitre`.
 - `mitre_limit=#.#` : mitre ratio limit and it only affects mitred join style. 
`miter_limit` is an accepted synonym for `mitre_limit`.
-- `side=both|left|right` : The option `left` or `right` enables a single-sided 
buffer operation on the geometry, with the buffered side aligned according to 
the direction of the line. This functionality is specific to LINESTRING 
geometry and has no impact on POINT or POLYGON geometries. By default, square 
end caps are applied.
+- `side=both|left|right` : Defaults to `both`. Setting `left` or `right` 
enables a single-sided buffer operation on the geometry, with the buffered side 
aligned according to the direction of the line. This functionality is specific 
to LINESTRING geometry and has no impact on POINT or POLYGON geometries. By 
default, square end caps are applied when `left` or `right` are specified.
 
 !!!note
     `ST_Buffer` throws an `IllegalArgumentException` if the correct format, 
parameters, or options are not provided.
diff --git a/python/sedona/geopandas/base.py b/python/sedona/geopandas/base.py
index 040287d862..3c874816b7 100644
--- a/python/sedona/geopandas/base.py
+++ b/python/sedona/geopandas/base.py
@@ -2177,6 +2177,43 @@ class GeoFrame(metaclass=ABCMeta):
         single_sided=False,
         **kwargs,
     ):
+        """
+        Returns a GeoSeries with all geometries buffered by the specified 
distance.
+
+        Parameters
+        ----------
+        distance : float
+            The distance to buffer by. Negative distances will create inward 
buffers.
+        resolution : int, default 16
+            The resolution of the buffer around each vertex. Specifies the 
number of
+            linear segments in a quarter circle in the approximation of 
circular arcs.
+        cap_style : str, default "round"
+            The style of the buffer cap. One of 'round', 'flat', 'square'.
+        join_style : str, default "round"
+            The style of the buffer join. One of 'round', 'mitre', 'bevel'.
+        mitre_limit : float, default 5.0
+            The mitre limit ratio for joins when join_style='mitre'.
+        single_sided : bool, default False
+            Whether to create a single-sided buffer. In Sedona, True will 
default to left-sided buffer.
+            However, 'right' may be specified to use a right-sided buffer.
+
+        Returns
+        -------
+        GeoSeries
+            A new GeoSeries with buffered geometries.
+
+        Examples
+        --------
+        >>> from shapely.geometry import Point
+        >>> from sedona.geopandas import GeoDataFrame
+        >>>
+        >>> data = {
+        ...     'geometry': [Point(0, 0), Point(1, 1)],
+        ...     'value': [1, 2]
+        ... }
+        >>> gdf = GeoDataFrame(data)
+        >>> buffered = gdf.buffer(0.5)
+        """
         return _delegate_to_geometry_column(
             "buffer",
             self,
diff --git a/python/sedona/geopandas/geoseries.py 
b/python/sedona/geopandas/geoseries.py
index d6fbb15d5c..54bf9ecc6a 100644
--- a/python/sedona/geopandas/geoseries.py
+++ b/python/sedona/geopandas/geoseries.py
@@ -1451,7 +1451,28 @@ class GeoSeries(GeoFrame, pspd.Series):
         single_sided=False,
         **kwargs,
     ) -> "GeoSeries":
-        spark_col = stf.ST_Buffer(self.spark.column, distance)
+        if single_sided:
+            # Reverse the following logic in 
common/src/main/java/org/apache/sedona/common/Functions.java buffer() to avoid 
negating the distance
+            #   if (bufferParameters.isSingleSided()
+            #       && (params.toLowerCase().contains("left") && radius < 0
+            #           || params.toLowerCase().contains("right") && radius > 
0)) {
+            #     radius = -radius;
+            #   }
+            side = "left" if distance >= 0 else "right"
+        else:
+            side = "both"
+        assert side in [
+            "left",
+            "right",
+            "both",
+        ], "single-sided must be one of 'left', 'right', or 'both', True, or 
False"
+
+        parameters = F.lit(
+            f"quad_segs={resolution} endcap={cap_style} join={join_style} 
mitre_limit={mitre_limit} side={side}"
+        )
+        spark_col = stf.ST_Buffer(
+            self.spark.column, distance, useSpheroid=False, 
parameters=parameters
+        )
         return self._query_geometry_column(
             spark_col,
             returns_geom=True,
diff --git a/python/sedona/spark/sql/st_functions.py 
b/python/sedona/spark/sql/st_functions.py
index 62a76945f6..43153377b7 100644
--- a/python/sedona/spark/sql/st_functions.py
+++ b/python/sedona/spark/sql/st_functions.py
@@ -335,7 +335,7 @@ def ST_Boundary(geometry: ColumnOrName) -> Column:
 def ST_Buffer(
     geometry: ColumnOrName,
     buffer: ColumnOrNameOrNumber,
-    useSpheroid: Optional[Union[ColumnOrName, bool]] = None,
+    useSpheroid: Optional[Union[ColumnOrName, bool]] = False,
     parameters: Optional[Union[ColumnOrName, str]] = None,
 ) -> Column:
     """Calculate a geometry that represents all points whose distance from the
diff --git a/python/tests/geopandas/test_geodataframe.py 
b/python/tests/geopandas/test_geodataframe.py
index 0c649d7909..6c7b7e7c11 100644
--- a/python/tests/geopandas/test_geodataframe.py
+++ b/python/tests/geopandas/test_geodataframe.py
@@ -329,31 +329,21 @@ class TestDataframe(TestGeopandasBase):
         self.assert_almost_equal(area_values[1], 4.0)
 
     def test_buffer(self):
-        # Create a GeoDataFrame with geometries to test buffer operation
-
-        # Create input geometries
         point = Point(0, 0)
         square = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
 
-        data = {"geometry1": [point, square], "id": [1, 2], "value": ["a", 
"b"]}
-        df = GeoDataFrame(data, geometry="geometry1")
+        data = {"geometry": [point, square], "id": [1, 2], "value": ["a", "b"]}
+        df = GeoDataFrame(data)
 
-        # Apply buffer with distance 0.5
         result = df.buffer(0.5)
 
-        # Verify result is a GeoDataFrame
         assert type(result) is GeoSeries
-
-        # Convert to pandas to extract individual geometries
-        pd_series = result.to_pandas()
-
         # Calculate areas to verify buffer was applied correctly
         # Point buffer with radius 0.5 should have area approximately π * 0.5² 
≈ 0.785
         # Square buffer with radius 0.5 should expand the 1x1 square to 2x2 
square with rounded corners
-        areas = [geom.area for geom in pd_series]
 
         # Check that square buffer area is greater than original (1.0)
-        assert areas[1] > 1.0
+        assert result.area[1] > 1.0
 
     def test_to_parquet(self):
         pass
diff --git a/python/tests/geopandas/test_geoseries.py 
b/python/tests/geopandas/test_geoseries.py
index 76e7eaf164..261ac304fb 100644
--- a/python/tests/geopandas/test_geoseries.py
+++ b/python/tests/geopandas/test_geoseries.py
@@ -105,19 +105,25 @@ class TestGeoSeries(TestGeopandasBase):
         assert_series_equal(result, df_result)
 
     def test_buffer(self):
-        result = self.geoseries.buffer(1)
+
+        s = GeoSeries(
+            [
+                Point(0, 0),
+                LineString([(1, -1), (1, 0), (2, 0), (2, 1)]),
+                Polygon([(3, -1), (4, 0), (3, 1)]),
+            ]
+        )
+        result = s.buffer(0.2)
         expected = [
-            "POLYGON ((3.300000000000000 -1.000000000000000, 3.280785280403230 
-1.195090322016128, 3.223879532511287 -1.382683432365090, 3.131469612302545 
-1.555570233019602, 3.007106781186547 -1.707106781186547, 2.855570233019602 
-1.831469612302545, 2.682683432365089 -1.923879532511287, 2.495090322016128 
-1.980785280403230, 2.300000000000000 -2.000000000000000, 2.104909677983872 
-1.980785280403230, 1.917316567634910 -1.923879532511287, 1.744429766980398 
-1.831469612302545, 1.59289321881 [...]
-            "POLYGON ((0.986393923832144 -3.164398987305357, 0.935367989801224 
-3.353676015097457, 0.848396388482656 -3.529361471973156, 0.728821389740875 
-3.684703864350261, 0.581238193719096 -3.813733471206735, 0.411318339874827 
-3.911491757111723, 0.225591752899151 -3.974221925961374, 0.031195801372873 
-3.999513292546280, -0.164398987305357 -3.986393923832144, -0.353676015097457 
-3.935367989801224, -0.529361471973156 -3.848396388482656, -0.684703864350260 
-3.728821389740875, -0.813733 [...]
-            "POLYGON ((-0.260059926604056 -1.672672793996312, 
-0.403493516968407 -1.802608257932399, -0.569270104475049 -1.902480890158382, 
-0.751180291696993 -1.968549819451744, -0.942410374326119 -1.998340340272165, 
-1.135797558140999 -1.990736606370705, -1.324098251632999 -1.946023426395157, 
-1.500259385009482 -1.865875595977814, -1.657682592935656 -1.753295165887471, 
-1.790471365675451 -1.612498995956065, -1.893651911234561 -1.448760806607280, 
-1.963359455800552 -1.268213644171327, - [...]
-            "POLYGON ((-0.844303230213814 -1.983056850984667, 
-0.942410374326119 -1.998340340272165, -1.135797558140999 -1.990736606370705, 
-1.324098251632999 -1.946023426395157, -1.500259385009482 -1.865875595977814, 
-1.657682592935656 -1.753295165887471, -1.790471365675451 -1.612498995956065, 
-1.893651911234561 -1.448760806607280, -1.963359455800552 -1.268213644171327, 
-1.996983004332570 -1.077620158927971, -1.993263139087243 -0.884119300439822, 
-1.293263139087243 5.115880699560178, -1 [...]
+            "POLYGON ((0.2 0, 0.1990369453344394 -0.0196034280659121, 
0.1961570560806461 -0.0390180644032256, 0.1913880671464418 -0.0580569354508925, 
0.1847759065022574 -0.076536686473018, 0.176384252869671 -0.0942793473651995, 
0.1662939224605091 -0.1111140466039204, 0.1546020906725474 -0.1268786568327291, 
0.1414213562373095 -0.1414213562373095, 0.1268786568327291 -0.1546020906725474, 
0.1111140466039205 -0.1662939224605091, 0.0942793473651996 -0.176384252869671, 
0.076536686473018 -0.1847 [...]
+            "POLYGON ((0.8 0, 0.8009630546655606 0.0196034280659122, 
0.803842943919354 0.0390180644032257, 0.8086119328535583 0.0580569354508925, 
0.8152240934977426 0.076536686473018, 0.823615747130329 0.0942793473651996, 
0.8337060775394909 0.1111140466039204, 0.8453979093274526 0.1268786568327291, 
0.8585786437626906 0.1414213562373095, 0.8731213431672709 0.1546020906725474, 
0.8888859533960796 0.1662939224605091, 0.9057206526348005 0.176384252869671, 
0.9234633135269821 0.1847759065022574 [...]
+            "POLYGON ((2.8 -1, 2.8 1, 2.8009630546655604 1.019603428065912, 
2.8038429439193537 1.0390180644032256, 2.8086119328535584 1.0580569354508924, 
2.8152240934977426 1.076536686473018, 2.823615747130329 1.0942793473651995, 
2.833706077539491 1.1111140466039204, 2.8453979093274526 1.1268786568327291, 
2.8585786437626903 1.1414213562373094, 2.873121343167271 1.1546020906725474, 
2.8888859533960796 1.1662939224605091, 2.9057206526348005 1.176384252869671, 
2.923463313526982 1.18477590650 [...]
         ]
         expected = gpd.GeoSeries([wkt.loads(wkt_str) for wkt_str in expected])
-        assert result.count() > 0
         self.check_sgpd_equals_gpd(result, expected)
 
         # Check that GeoDataFrame works too
-        df_result = self.geoseries.to_geoframe().buffer(1)
+        df_result = s.to_geoframe().buffer(0.2)
         self.check_sgpd_equals_gpd(df_result, expected)
 
     def test_simplify(self):
diff --git a/python/tests/geopandas/test_match_geopandas_series.py 
b/python/tests/geopandas/test_match_geopandas_series.py
index ddb71752d5..6420643ed1 100644
--- a/python/tests/geopandas/test_match_geopandas_series.py
+++ b/python/tests/geopandas/test_match_geopandas_series.py
@@ -189,11 +189,6 @@ class TestMatchGeopandasSeries(TestGeopandasBase):
             self.check_pd_series_equal(sgpd_result, gpd_result)
 
     def test_buffer(self):
-        buffer = self.g1.buffer(0.2)
-        assert buffer is not None
-        assert type(buffer) is GeoSeries
-        assert buffer.count() == 2
-
         for geom in self.geoms:
             dist = 0.2
             sgpd_result = GeoSeries(geom).buffer(dist)
@@ -201,6 +196,35 @@ class TestMatchGeopandasSeries(TestGeopandasBase):
 
             self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
 
+        # Check that the parameters work properly
+        sgpd_result = GeoSeries(self.linestrings).buffer(
+            0.2, resolution=20, cap_style="flat", join_style="bevel"
+        )
+        gpd_result = gpd.GeoSeries(self.linestrings).buffer(
+            0.2, resolution=20, cap_style="flat", join_style="bevel"
+        )
+        self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+        sgpd_result = GeoSeries(self.linestrings).buffer(0.2, 
single_sided=True)
+        gpd_result = gpd.GeoSeries(self.linestrings).buffer(0.2, 
single_sided=True)
+        self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+        sgpd_result = GeoSeries(self.linestrings).buffer(
+            0.2, join_style="mitre", mitre_limit=10.0
+        )
+        gpd_result = gpd.GeoSeries(self.linestrings).buffer(
+            0.2, join_style="mitre", mitre_limit=10.0
+        )
+        self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+        sgpd_result = GeoSeries(self.linestrings).buffer(
+            0.2, single_sided=True, cap_style="round"
+        )
+        gpd_result = gpd.GeoSeries(self.linestrings).buffer(
+            0.2, single_sided=True, cap_style="round"
+        )
+        self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
     def test_buffer_then_area(self):
         area = self.g1.buffer(0.2).area
         assert area is not None

Reply via email to