Skip to content

Commit

Permalink
[SEDONA-413] Add buffer parameters to ST_Buffer (#1066)
Browse files Browse the repository at this point in the history
  • Loading branch information
furqaankhan authored Nov 2, 2023
1 parent 3412696 commit d756a9a
Show file tree
Hide file tree
Showing 15 changed files with 250 additions and 21 deletions.
100 changes: 99 additions & 1 deletion common/src/main/java/org/apache/sedona/common/Functions.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
import org.locationtech.jts.io.gml2.GMLWriter;
import org.locationtech.jts.io.kml.KMLWriter;
import org.locationtech.jts.linearref.LengthIndexedLine;
import org.locationtech.jts.operation.buffer.BufferOp;
import org.locationtech.jts.operation.buffer.BufferParameters;
import org.locationtech.jts.operation.distance.DistanceOp;
import org.locationtech.jts.operation.distance3d.Distance3DOp;
import org.locationtech.jts.operation.linemerge.LineMerger;
Expand Down Expand Up @@ -92,7 +94,103 @@ public static Geometry boundary(Geometry geometry) {
}

public static Geometry buffer(Geometry geometry, double radius) {
return geometry.buffer(radius);
return buffer(geometry, radius, "");
}

public static Geometry buffer(Geometry geometry, double radius, String params) {
if (params.isEmpty()) {
return BufferOp.bufferOp(geometry, radius);
}

BufferParameters bufferParameters = parseBufferParams(params);

// convert the sign to the appropriate direction
// left - radius should be positive
// right - radius should be negative
if (bufferParameters.isSingleSided() &&
(params.toLowerCase().contains("left") && radius < 0 || params.toLowerCase().contains("right") && radius > 0)) {
radius = -radius;
}

return BufferOp.bufferOp(geometry, radius, bufferParameters);
}

private static BufferParameters parseBufferParams(String params) {

String[] listBufferParameters = {"quad_segs", "endcap", "join", "mitre_limit", "miter_limit", "side"};
String[] endcapOptions = {"round", "flat", "butt", "square"};
String[] joinOptions = {"round", "mitre", "miter", "bevel"};
String[] sideOptions = {"both", "left", "right"};

BufferParameters bufferParameters = new BufferParameters();
String[] listParams = params.split(" ");

for (String param: listParams) {
String[] singleParam = param.split("=");

if (singleParam.length != 2) {
throw new IllegalArgumentException(String.format("%s is not the valid format. The valid format is key=value, for example `endcap=butt quad_segs=4`.", param));
}

// Set quadrant segment
if (singleParam[0].equalsIgnoreCase(listBufferParameters[0])) {
try {
bufferParameters.setQuadrantSegments(Integer.parseInt(singleParam[1]));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(String.format("%1$s is not an integer. Quadrant segment should be an integer.", singleParam[1]));
}
}
// Set end cap style
else if (singleParam[0].equalsIgnoreCase(listBufferParameters[1])) {
if (singleParam[1].equalsIgnoreCase(endcapOptions[0])) {
bufferParameters.setEndCapStyle(BufferParameters.CAP_ROUND);
} else if (singleParam[1].equalsIgnoreCase(endcapOptions[1]) || singleParam[1].equalsIgnoreCase(endcapOptions[2])) {
bufferParameters.setEndCapStyle(BufferParameters.CAP_FLAT);
} else if (singleParam[1].equalsIgnoreCase(endcapOptions[3])) {
bufferParameters.setEndCapStyle(BufferParameters.CAP_SQUARE);
} else {
throw new IllegalArgumentException(String.format("%s is not a valid option. Accepted options are %s.", singleParam[1], Arrays.toString(endcapOptions)));
}
}
// Set join style
else if (singleParam[0].equalsIgnoreCase(listBufferParameters[2])) {
if (singleParam[1].equalsIgnoreCase(joinOptions[0])) {
bufferParameters.setJoinStyle(BufferParameters.JOIN_ROUND);
} else if (singleParam[1].equalsIgnoreCase(joinOptions[1]) || singleParam[1].equalsIgnoreCase(joinOptions[2])) {
bufferParameters.setJoinStyle(BufferParameters.JOIN_MITRE);
} else if (singleParam[1].equalsIgnoreCase(joinOptions[3])) {
bufferParameters.setJoinStyle(BufferParameters.JOIN_BEVEL);
} else {
throw new IllegalArgumentException(String.format("%s is not a valid option. Accepted options are %s", singleParam[1], Arrays.toString(joinOptions)));
}
}
// Set mitre ratio limit
else if (singleParam[0].equalsIgnoreCase(listBufferParameters[3]) || singleParam[0].equalsIgnoreCase(listBufferParameters[4])) {
try {
bufferParameters.setMitreLimit(Double.parseDouble(singleParam[1]));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(String.format("%1$s is not a double. Mitre limit should be a double.", singleParam[1]));
}
continue;
}
// 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);
continue;
} else if (singleParam[1].equalsIgnoreCase(sideOptions[1]) || singleParam[1].equalsIgnoreCase(sideOptions[2])) {
bufferParameters.setSingleSided(true);
} else {
throw new IllegalArgumentException(String.format("%s is not a valid option. Accepted options are %s ", singleParam[1], Arrays.toString(sideOptions)));
}
}
// everything else
else {
throw new IllegalArgumentException(String.format("%s is not a valid style parameter. Accepted style parameters are %s.", singleParam[0], Arrays.toString(listBufferParameters)));
}
}
return bufferParameters;
}

public static Geometry envelope(Geometry geometry) {
Expand Down
23 changes: 23 additions & 0 deletions common/src/test/java/org/apache/sedona/common/FunctionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,29 @@ public void nRingsMultiPolygonMixed() throws Exception {
assertEquals(expected, actual);
}

@Test
public void testBuffer() {
Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray(50, 50, 50, 150, 150, 150, 150, 50, 50, 50));
String actual = Functions.asWKT(Functions.buffer(polygon, 15));
String expected = "POLYGON ((50 35, 47.07364516975807 35.288220793951545, 44.25974851452364 36.1418070123307, 41.666446504705966 37.52795581546182, 39.39339828220179 39.39339828220179, 37.527955815461816 41.66644650470597, 36.141807012330695 44.25974851452366, 35.288220793951545 47.07364516975807, 35 50, 35 150, 35.288220793951545 152.92635483024193, 36.1418070123307 155.74025148547634, 37.52795581546182 158.33355349529404, 39.39339828220179 160.6066017177982, 41.66644650470597 162.4720441845382, 44.25974851452365 163.8581929876693, 47.07364516975808 164.71177920604845, 50 165, 150 165, 152.92635483024193 164.71177920604845, 155.74025148547634 163.8581929876693, 158.33355349529404 162.4720441845382, 160.6066017177982 160.6066017177982, 162.4720441845382 158.33355349529404, 163.8581929876693 155.74025148547634, 164.71177920604845 152.92635483024193, 165 150, 165 50, 164.71177920604845 47.07364516975807, 163.8581929876693 44.25974851452365, 162.4720441845382 41.666446504705966, 160.6066017177982 39.39339828220179, 158.33355349529404 37.52795581546182, 155.74025148547634 36.1418070123307, 152.92635483024193 35.288220793951545, 150 35, 50 35))";
assertEquals(expected, actual);

LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray(0, 0, 50, 70, 100, 100));
actual = Functions.asWKT(Functions.buffer(lineString, 10, "side=left"));
expected = "POLYGON ((100 100, 50 70, 0 0, -8.137334712067348 5.812381937190963, 41.86266528793265 75.81238193719096, 43.21673095875923 77.34760240582902, 44.855042445724735 78.57492925712545, 94.85504244572473 108.57492925712545, 100 100))";
assertEquals(expected, actual);

lineString = GEOMETRY_FACTORY.createLineString(coordArray(0, 0, 50, 70, 70, -3));
actual = Functions.asWKT(Functions.buffer(lineString, 10, "endcap=square"));
expected = "POLYGON ((41.86266528793265 75.81238193719096, 43.21555008457904 77.3465120530184, 44.85228625762473 78.57327494173381, 46.70439518001618 79.44134465372912, 48.69438734657371 79.914402432785, 50.73900442057982 79.9726562392556, 52.75270263976913 79.6136688198111, 54.65123184115194 78.8524596785218, 56.355160363552315 77.72087668296376, 57.79319835113832 76.26626359641972, 58.90518041582699 74.54947928466231, 59.64458286836891 72.642351470786, 79.64458286836891 -0.3576485292139977, 82.28693433915491 -10.002231397582907, 62.997768602417096 -15.28693433915491, 45.912786454208465 47.07325050180659, 8.137334712067348 -5.812381937190963, 2.324952774876386 -13.949716649258315, -13.94971664925831 -2.3249527748763885, 41.86266528793265 75.81238193719096))";
assertEquals(expected, actual);

Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(100, 90));
actual = Functions.asWKT(Functions.buffer(point, 10, "quad_segs=2"));
expected = "POLYGON ((110 90, 107.07106781186548 82.92893218813452, 100 80, 92.92893218813452 82.92893218813452, 90 90, 92.92893218813452 97.07106781186548, 100 100, 107.07106781186548 97.07106781186548, 110 90))";
assertEquals(expected, actual);
}

@Test
public void nRingsUnsupported() {
LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray3d(0, 1, 1, 1, 2, 1, 1, 2, 2));
Expand Down
40 changes: 35 additions & 5 deletions docs/api/flink/Function.md
Original file line number Diff line number Diff line change
Expand Up @@ -530,21 +530,51 @@ Output: `LINESTRING Z(-1 -1 0, 10 5 5)`

Introduction: Returns a geometry/geography that represents all points whose distance from this Geometry/geography is less than or equal to distance.

Format: `ST_Buffer (A: Geometry, buffer: Double)`
The optional third parameter controls the buffer accuracy and style. Buffer accuracy is specified by the number of line segments approximating a quarter circle, with a default of 8 segments. Buffer style can be set by providing blank-separated key=value pairs in a list format.

Since: `v1.2.0`
- `quad_segs=#` : Number of line segments utilized to approximate a quarter circle (default is 8).
- `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.

!!!note
`ST_Buffer` throws an `IllegalArgumentException` if the correct format, parameters, or options are not provided.

Format:

```
ST_Buffer (A: Geometry, buffer: Double, bufferStyleParameters: String [Optional])
```

Since: `v1.5.1`

Example:

```sql
SELECT ST_Buffer(ST_GeomFromWKT("POINT(0 0)"), 1)
SELECT ST_Buffer(ST_GeomFromWKT('POINT(0 0)'), 10)
SELECT ST_Buffer(ST_GeomFromWKT('POINT(0 0)'), 10, 'quad_segs=2')
```

Output:

<img alt="Point buffer with 8 quadrant segments" src="../../../image/point-buffer-quad-8.png" width="100" height=""/>
<img alt="Point buffer with 2 quadrant segments" src="../../../image/point-buffer-quad-2.png" width="100" height=""/>

8 Segments &ensp; 2 Segments

Example:

```sql
SELECT ST_Buffer(ST_GeomFromWKT('LINESTRING(0 0, 50 70, 100 100)'), 10, 'side=left')
```
POLYGON ((1 0, 0.9807852804032304 -0.1950903220161282, 0.9238795325112867 -0.3826834323650898, 0.8314696123025452 -0.5555702330196022, 0.7071067811865476 -0.7071067811865475, 0.5555702330196023 -0.8314696123025452, 0.3826834323650898 -0.9238795325112867, 0.1950903220161283 -0.9807852804032304, 0.0000000000000001 -1, -0.1950903220161282 -0.9807852804032304, -0.3826834323650897 -0.9238795325112867, -0.555570233019602 -0.8314696123025453, -0.7071067811865475 -0.7071067811865476, -0.8314696123025453 -0.5555702330196022, -0.9238795325112867 -0.3826834323650899, -0.9807852804032304 -0.1950903220161286, -1 -0.0000000000000001, -0.9807852804032304 0.1950903220161284, -0.9238795325112868 0.3826834323650897, -0.8314696123025455 0.555570233019602, -0.7071067811865477 0.7071067811865475, -0.5555702330196022 0.8314696123025452, -0.3826834323650903 0.9238795325112865, -0.1950903220161287 0.9807852804032303, -0.0000000000000002 1, 0.1950903220161283 0.9807852804032304, 0.38268343236509 0.9238795325112866, 0.5555702330196018 0.8314696123025455, 0.7071067811865474 0.7071067811865477, 0.8314696123025452 0.5555702330196022, 0.9238795325112865 0.3826834323650904, 0.9807852804032303 0.1950903220161287, 1 0))
```

Output:

<img alt="Original Linestring" src="../../../image/linestring-og.png" width="150"/>
<img alt="Original Linestring with buffer on the left side" src="../../../image/linestring-left-side.png" width="150"/>

Original Linestring &emsp; Left side buffed Linestring

## ST_BuildArea

Expand Down
40 changes: 35 additions & 5 deletions docs/api/sql/Function.md
Original file line number Diff line number Diff line change
Expand Up @@ -529,21 +529,51 @@ Output: `LINESTRING Z(-1 -1 0, 10 5 5)`

Introduction: Returns a geometry/geography that represents all points whose distance from this Geometry/geography is less than or equal to distance.

Format: `ST_Buffer (A: Geometry, buffer: Double)`
The optional third parameter controls the buffer accuracy and style. Buffer accuracy is specified by the number of line segments approximating a quarter circle, with a default of 8 segments. Buffer style can be set by providing blank-separated key=value pairs in a list format.

Since: `v1.0.0`
- `quad_segs=#` : Number of line segments utilized to approximate a quarter circle (default is 8).
- `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.

!!!note
`ST_Buffer` throws an `IllegalArgumentException` if the correct format, parameters, or options are not provided.

Format:

```
ST_Buffer (A: Geometry, buffer: Double, bufferStyleParameters: String [Optional])
```

Since: `v1.5.1`

Spark SQL Example:

```sql
SELECT ST_Buffer(ST_GeomFromWKT("POINT(0 0)"), 1)
SELECT ST_Buffer(ST_GeomFromWKT('POINT(0 0)'), 10)
SELECT ST_Buffer(ST_GeomFromWKT('POINT(0 0)'), 10, 'quad_segs=2')
```

Output:

<img alt="Point buffer with 8 quadrant segments" src="../../../image/point-buffer-quad-8.png" width="100" height=""/>
<img alt="Point buffer with 2 quadrant segments" src="../../../image/point-buffer-quad-2.png" width="100" height=""/>

8 Segments &ensp; 2 Segments

Spark SQL Example:

```sql
SELECT ST_Buffer(ST_GeomFromWKT('LINESTRING(0 0, 50 70, 100 100)'), 10, 'side=left')
```
POLYGON ((1 0, 0.9807852804032304 -0.1950903220161282, 0.9238795325112867 -0.3826834323650898, 0.8314696123025452 -0.5555702330196022, 0.7071067811865476 -0.7071067811865475, 0.5555702330196023 -0.8314696123025452, 0.3826834323650898 -0.9238795325112867, 0.1950903220161283 -0.9807852804032304, 0.0000000000000001 -1, -0.1950903220161282 -0.9807852804032304, -0.3826834323650897 -0.9238795325112867, -0.555570233019602 -0.8314696123025453, -0.7071067811865475 -0.7071067811865476, -0.8314696123025453 -0.5555702330196022, -0.9238795325112867 -0.3826834323650899, -0.9807852804032304 -0.1950903220161286, -1 -0.0000000000000001, -0.9807852804032304 0.1950903220161284, -0.9238795325112868 0.3826834323650897, -0.8314696123025455 0.555570233019602, -0.7071067811865477 0.7071067811865475, -0.5555702330196022 0.8314696123025452, -0.3826834323650903 0.9238795325112865, -0.1950903220161287 0.9807852804032303, -0.0000000000000002 1, 0.1950903220161283 0.9807852804032304, 0.38268343236509 0.9238795325112866, 0.5555702330196018 0.8314696123025455, 0.7071067811865474 0.7071067811865477, 0.8314696123025452 0.5555702330196022, 0.9238795325112865 0.3826834323650904, 0.9807852804032303 0.1950903220161287, 1 0))
```

Output:

<img alt="Original Linestring" src="../../../image/linestring-og.png" width="150"/>
<img alt="Original Linestring with buffer on the left side" src="../../../image/linestring-left-side.png" width="150"/>

Original Linestring &emsp; Left side buffed Linestring

## ST_BuildArea

Expand Down
Binary file added docs/image/linestring-left-side.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/image/linestring-og.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/image/point-buffer-quad-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/image/point-buffer-quad-8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.j
Geometry geom = (Geometry) o;
return org.apache.sedona.common.Functions.buffer(geom, radius);
}

@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class)
public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class)
Object o, @DataTypeHint("Double") Double radius, @DataTypeHint("String") String params) {
Geometry geom = (Geometry) o;
return org.apache.sedona.common.Functions.buffer(geom, radius, params);
}
}

public static class ST_ClosestPoint extends ScalarFunction {
Expand Down
Loading

0 comments on commit d756a9a

Please sign in to comment.