Skip to content

Commit

Permalink
[SEDONA-631] Add ST_Expand (#1527)
Browse files Browse the repository at this point in the history
* feat: add ST_Expand

* fix: snowflake tests

* fix: simplify the implementation, remove BBox implementation

* chore: undo changes in BBox

* docs: add additional behavior details

* add test to check preservation of SRID
  • Loading branch information
furqaankhan authored Jul 24, 2024
1 parent 9cdb052 commit c332019
Show file tree
Hide file tree
Showing 21 changed files with 486 additions and 0 deletions.
54 changes: 54 additions & 0 deletions common/src/main/java/org/apache/sedona/common/Functions.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.uber.h3core.exceptions.H3Exception;
import com.uber.h3core.util.LatLng;
import java.util.*;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.sedona.common.geometryObjects.Circle;
Expand All @@ -37,6 +38,8 @@
import org.locationtech.jts.algorithm.construct.MaximumInscribedCircle;
import org.locationtech.jts.algorithm.hull.ConcaveHull;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.util.AffineTransformation;
import org.locationtech.jts.geom.util.GeometryFixer;
import org.locationtech.jts.io.ByteOrderValues;
Expand Down Expand Up @@ -90,6 +93,57 @@ public static Geometry boundary(Geometry geometry) {
return boundary;
}

public static Geometry expand(Geometry geometry, double uniformDelta) {
return expand(geometry, uniformDelta, uniformDelta, uniformDelta);
}

public static Geometry expand(Geometry geometry, double deltaX, double deltaY) {
return expand(geometry, deltaX, deltaY, 0);
}

public static Geometry expand(Geometry geometry, double deltaX, double deltaY, double deltaZ) {
if (geometry == null || geometry.isEmpty()) {
return geometry;
}

Coordinate[] coordinates = geometry.getCoordinates();
double minX = Double.MAX_VALUE;
double maxX = -Double.MAX_VALUE;
double minY = Double.MAX_VALUE;
double maxY = -Double.MAX_VALUE;
double minZ = Double.MAX_VALUE;
double maxZ = -Double.MAX_VALUE;

for (int i = 0; i < coordinates.length; i++) {
minX = Math.min(minX, coordinates[i].x);
maxX = Math.max(maxX, coordinates[i].x);
minY = Math.min(minY, coordinates[i].y);
maxY = Math.max(maxY, coordinates[i].y);
minZ = Math.min(minZ, coordinates[i].z);
maxZ = Math.max(maxZ, coordinates[i].z);
}

minX = minX - deltaX;
maxX = maxX + deltaX;
minY = minY - deltaY;
maxY = maxY + deltaY;

if (Functions.hasZ(geometry)) {
minZ = minZ - deltaZ;
maxZ = maxZ + deltaZ;
Coordinate[] newCoords = new Coordinate[5];
newCoords[0] = new Coordinate(minX, minY, minZ);
newCoords[1] = new Coordinate(minX, maxY, minZ);
newCoords[2] = new Coordinate(maxX, maxY, maxZ);
newCoords[3] = new Coordinate(maxX, minY, maxZ);
newCoords[4] = newCoords[0];
return geometry.getFactory().createPolygon(newCoords);
}
Geometry result = Constructors.polygonFromEnvelope(minX, minY, maxX, maxY);
result.setSRID(geometry.getSRID());
return result;
}

public static Geometry buffer(Geometry geometry, double radius) {
return buffer(geometry, radius, false, "");
}
Expand Down
58 changes: 58 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 @@ -2250,6 +2250,64 @@ public void nRingsMultiPolygonMixed() throws Exception {
assertEquals(expected, actual);
}

@Test
public void testExpand() throws ParseException {
Geometry geometry =
GEOMETRY_FACTORY.createPolygon(coordArray(50, 50, 50, 80, 80, 80, 80, 50, 50, 50));
String actual = Functions.asWKT(Functions.expand(geometry, 10, 0));
String expected = "POLYGON ((40 50, 40 80, 90 80, 90 50, 40 50))";
assertEquals(expected, actual);

geometry = Constructors.geomFromWKT("POINT (10 20 1)", 4326);
Geometry result = Functions.expand(geometry, 10);
actual = Functions.asWKT(result);
expected = "POLYGON Z((0 10 -9, 0 30 -9, 20 30 11, 20 10 11, 0 10 -9))";
assertEquals(expected, actual);
assertEquals(4326, result.getSRID());

geometry = Constructors.geomFromWKT("LINESTRING (0 0, 1 1, 2 2)", 0);
actual = Functions.asWKT(Functions.expand(geometry, 10, 10));
expected = "POLYGON ((-10 -10, -10 12, 12 12, 12 -10, -10 -10))";
assertEquals(expected, actual);

geometry =
Constructors.geomFromWKT(
"MULTIPOLYGON (((52 68 1, 42 64 1, 66 62 2, 88 64 2, 85 68 2, 72 70 1, 52 68 1)), ((50 50 2, 50 80 2, 80 80 3, 80 50 2, 50 50 2)))",
4326);
actual = Functions.asWKT(Functions.expand(geometry, 10.5, 2, 5));
expected = "POLYGON Z((31.5 48 -4, 31.5 82 -4, 98.5 82 8, 98.5 48 8, 31.5 48 -4))";
assertEquals(expected, actual);

geometry = Constructors.geomFromWKT("MULTIPOINT((10 20 1), (20 30 2))", 0);
actual = Functions.asWKT(Functions.expand(geometry, 9.5, 3.5));
expected = "POLYGON Z((0.5 16.5 1, 0.5 33.5 1, 29.5 33.5 2, 29.5 16.5 2, 0.5 16.5 1))";
assertEquals(expected, actual);

geometry =
Constructors.geomFromWKT(
"MULTILINESTRING ((1 0 4, 2 0 4, 4 0 4),(1 0 4, 2 0 4, 4 0 4))", 0);
actual = Functions.asWKT(Functions.expand(geometry, 0));
expected = "POLYGON Z((1 0 4, 1 0 4, 4 0 4, 4 0 4, 1 0 4))";
assertEquals(expected, actual);

geometry =
Constructors.geomFromWKT(
"GEOMETRYCOLLECTION (POINT (10 10),LINESTRING (20 20, 30 30),POLYGON ((25 25, 35 35, 35 35, 25 25)),MULTIPOINT (30 30, 40 40),MULTILINESTRING ((40 40, 50 50), (45 45, 55 55)),MULTIPOLYGON (((50 50, 60 60, 60 60, 50 50)), ((55 55, 65 65, 65 65, 55 55))))",
1234);
result = Functions.expand(geometry, 10);
actual = Functions.asWKT(result);
expected = "POLYGON ((0 0, 0 75, 75 75, 75 0, 0 0))";
assertEquals(expected, actual);
assertEquals(1234, result.getSRID());

// The function drops the M dimension
geometry =
Constructors.geomFromWKT("POLYGON M((50 50 1, 50 80 2, 80 80 3, 80 50 2, 50 50 1))", 0);
actual = Functions.asWKT(Functions.expand(geometry, 0));
expected = "POLYGON ((50 50, 50 80, 80 80, 80 50, 50 50))";
assertEquals(expected, actual);
}

@Test
public void testBuffer() {
Polygon polygon =
Expand Down
39 changes: 39 additions & 0 deletions docs/api/flink/Function.md
Original file line number Diff line number Diff line change
Expand Up @@ -1211,6 +1211,45 @@ Output:
POLYGON ((0 0, 0 3, 1 3, 1 0, 0 0))
```

## ST_Expand

Introduction: Returns a geometry expanded from the bounding box of the input. The expansion can be specified in two ways:

1. By individual axis using `deltaX`, `deltaY`, or `deltaZ` parameters.
2. Uniformly across all axes using the `uniformDelta` parameter.

!!!Note
Things to consider when using this function:

1. The `uniformDelta` parameter expands Z dimensions for XYZ geometries; otherwise, it only affects XY dimensions.
2. For XYZ geometries, specifying only `deltaX` and `deltaY` will preserve the original Z dimension.
3. If the input geometry has an M dimension then using this function will drop the said M dimension.

Format:

`ST_Expand(geometry: Geometry, uniformDelta: Double)`

`ST_Expand(geometry: Geometry, deltaX: Double, deltaY: Double)`

`ST_Expand(geometry: Geometry, deltaX: Double, deltaY: Double, deltaZ: Double)`

Since: `v1.6.1`

SQL Example:

```sql
SELECT ST_Expand(
ST_GeomFromWKT('POLYGON Z((50 50 1, 50 80 2, 80 80 3, 80 50 2, 50 50 1))'),
10
)
```

Output:

```
POLYGON Z((40 40 -9, 40 90 -9, 90 90 13, 90 40 13, 40 40 -9))
```

## ST_ExteriorRing

Introduction: Returns a LINESTRING representing the exterior ring (shell) of a POLYGON. Returns NULL if the geometry is not a polygon.
Expand Down
37 changes: 37 additions & 0 deletions docs/api/snowflake/vector-data/Function.md
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,43 @@ SELECT ST_Envelope(polygondf.countyshape)
FROM polygondf
```

## ST_Expand

Introduction: Returns a geometry expanded from the bounding box of the input. The expansion can be specified in two ways:

1. By individual axis using `deltaX`, `deltaY`, or `deltaZ` parameters.
2. Uniformly across all axes using the `uniformDelta` parameter.

Format:

`ST_Expand(geometry: Geometry, uniformDelta: Double)`

`ST_Expand(geometry: Geometry, deltaX: Double, deltaY: Double)`

`ST_Expand(geometry: Geometry, deltaX: Double, deltaY: Double, deltaZ: Double)`

!!!Note
Things to consider when using this function:

1. The `uniformDelta` parameter expands Z dimensions for XYZ geometries; otherwise, it only affects XY dimensions.
2. For XYZ geometries, specifying only `deltaX` and `deltaY` will preserve the original Z dimension.
3. If the input geometry has an M dimension then using this function will drop the said M dimension.

SQL Example:

```sql
SELECT ST_Expand(
ST_GeomFromWKT('POLYGON Z((50 50 1, 50 80 2, 80 80 3, 80 50 2, 50 50 1))'),
10
)
```

Output:

```
POLYGON Z((40 40 -9, 40 90 -9, 90 90 13, 90 40 13, 40 40 -9))
```

## ST_ExteriorRing

Introduction: Returns a line string representing the exterior ring of the POLYGON geometry. Return NULL if the geometry is not a polygon.
Expand Down
39 changes: 39 additions & 0 deletions docs/api/sql/Function.md
Original file line number Diff line number Diff line change
Expand Up @@ -1216,6 +1216,45 @@ Output:
POLYGON ((0 0, 0 3, 1 3, 1 0, 0 0))
```

## ST_Expand

Introduction: Returns a geometry expanded from the bounding box of the input. The expansion can be specified in two ways:

1. By individual axis using `deltaX`, `deltaY`, or `deltaZ` parameters.
2. Uniformly across all axes using the `uniformDelta` parameter.

!!!Note
Things to consider when using this function:

1. The `uniformDelta` parameter expands Z dimensions for XYZ geometries; otherwise, it only affects XY dimensions.
2. For XYZ geometries, specifying only `deltaX` and `deltaY` will preserve the original Z dimension.
3. If the input geometry has an M dimension then using this function will drop the said M dimension.

Format:

`ST_Expand(geometry: Geometry, uniformDelta: Double)`

`ST_Expand(geometry: Geometry, deltaX: Double, deltaY: Double)`

`ST_Expand(geometry: Geometry, deltaX: Double, deltaY: Double, deltaZ: Double)`

Since: `v1.6.1`

SQL Example:

```sql
SELECT ST_Expand(
ST_GeomFromWKT('POLYGON Z((50 50 1, 50 80 2, 80 80 3, 80 50 2, 50 50 1))'),
10
)
```

Output:

```
POLYGON Z((40 40 -9, 40 90 -9, 90 90 13, 90 40 13, 40 40 -9))
```

## ST_ExteriorRing

Introduction: Returns a line string representing the exterior ring of the POLYGON geometry. Return NULL if the geometry is not a polygon.
Expand Down
1 change: 1 addition & 0 deletions flink/src/main/java/org/apache/sedona/flink/Catalog.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public static UserDefinedFunction[] getFuncs() {
new Functions.ST_ConcaveHull(),
new Functions.ST_ConvexHull(),
new Functions.ST_CrossesDateLine(),
new Functions.ST_Expand(),
new Functions.ST_Envelope(),
new Functions.ST_Difference(),
new Functions.ST_Dimension(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,35 @@ public Geometry eval(
}
}

public static class ST_Expand extends ScalarFunction {
@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(value = "Double") Double uniformDelta) {
Geometry geom = (Geometry) o;
return org.apache.sedona.common.Functions.expand(geom, uniformDelta);
}

@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(value = "Double") Double deltaX,
@DataTypeHint(value = "Double") Double deltaY) {
Geometry geom = (Geometry) o;
return org.apache.sedona.common.Functions.expand(geom, deltaX, deltaY);
}

@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(value = "Double") Double deltaX,
@DataTypeHint(value = "Double") Double deltaY,
@DataTypeHint(value = "Double") Double deltaZ) {
Geometry geom = (Geometry) o;
return org.apache.sedona.common.Functions.expand(geom, deltaX, deltaY, deltaZ);
}
}

public static class ST_Dimension extends ScalarFunction {
@DataTypeHint("Integer")
public Integer eval(
Expand Down
39 changes: 39 additions & 0 deletions flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,45 @@ public void testEnvelope() {
first(linestringTable).getField(0).toString());
}

@Test
public void testExpand() {
Table baseTable =
tableEnv.sqlQuery(
"SELECT ST_GeomFromWKT('POLYGON ((50 50 1, 50 80 2, 80 80 3, 80 50 2, 50 50 1))') as geom");
String actual =
(String)
first(
baseTable
.select(call(Functions.ST_Expand.class, $("geom"), 10))
.as("geom")
.select(call(Functions.ST_AsText.class, $("geom"))))
.getField(0);
String expected = "POLYGON Z((40 40 -9, 40 90 -9, 90 90 13, 90 40 13, 40 40 -9))";
assertEquals(expected, actual);

actual =
(String)
first(
baseTable
.select(call(Functions.ST_Expand.class, $("geom"), 5, 6))
.as("geom")
.select(call(Functions.ST_AsText.class, $("geom"))))
.getField(0);
expected = "POLYGON Z((45 44 1, 45 86 1, 85 86 3, 85 44 3, 45 44 1))";
assertEquals(expected, actual);

actual =
(String)
first(
baseTable
.select(call(Functions.ST_Expand.class, $("geom"), 6, 5, -3))
.as("geom")
.select(call(Functions.ST_AsText.class, $("geom"))))
.getField(0);
expected = "POLYGON Z((44 45 4, 44 85 4, 86 85 0, 86 45 0, 44 45 4))";
assertEquals(expected, actual);
}

@Test
public void testFlipCoordinates() {
Table pointTable = createPointTable_real(testDataSize);
Expand Down
25 changes: 25 additions & 0 deletions python/sedona/sql/st_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,31 @@ def ST_Envelope(geometry: ColumnOrName) -> Column:
return _call_st_function("ST_Envelope", geometry)


@validate_argument_types
def ST_Expand(geometry: ColumnOrName, deltaX_uniformDelta: Union[ColumnOrName, float], deltaY: Optional[Union[ColumnOrName, float]] = None, deltaZ: Optional[Union[ColumnOrName, float]] = None) -> Column:
"""Expand the given geometry column by a constant unit in each direction
:param geometry: Geometry column to calculate the envelope of.
:type geometry: ColumnOrName
:param deltaX_uniformDelta: it is either deltaX or uniformDelta depending on the number of arguments provided
:type deltaX_uniformDelta: Union[ColumnOrName, float]
:param deltaY: Constant unit of deltaY
:type deltaY: Union[ColumnOrName, float]
:param deltaZ: Constant unit of deltaZ
:type deltaZ: Union[ColumnOrName, float]
:return: Envelope of geometry as a geometry column.
:rtype: Column
"""
if deltaZ is None:
args = (geometry, deltaX_uniformDelta, deltaY)
if deltaY is None:
args = (geometry, deltaX_uniformDelta)
else:
args = (geometry, deltaX_uniformDelta, deltaY, deltaZ)

return _call_st_function("ST_Expand", args)


@validate_argument_types
def ST_ExteriorRing(polygon: ColumnOrName) -> Column:
"""Get a linestring representing the exterior ring of a polygon geometry
Expand Down
Loading

0 comments on commit c332019

Please sign in to comment.