Skip to content

Commit edcf6ef

Browse files
furqaankhanKontinuation
authored andcommitted
[SEDONA-631] Add ST_Expand (apache#1527)
* 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
1 parent b112f77 commit edcf6ef

File tree

21 files changed

+486
-0
lines changed

21 files changed

+486
-0
lines changed

common/src/main/java/org/apache/sedona/common/Functions.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.uber.h3core.exceptions.H3Exception;
2626
import com.uber.h3core.util.LatLng;
2727
import java.util.*;
28+
import java.util.List;
2829
import java.util.stream.Collectors;
2930
import org.apache.commons.collections.IteratorUtils;
3031
import org.apache.commons.lang3.StringUtils;
@@ -41,6 +42,8 @@
4142
import org.locationtech.jts.algorithm.construct.MaximumInscribedCircle;
4243
import org.locationtech.jts.algorithm.hull.ConcaveHull;
4344
import org.locationtech.jts.geom.*;
45+
import org.locationtech.jts.geom.Point;
46+
import org.locationtech.jts.geom.Polygon;
4447
import org.locationtech.jts.geom.util.AffineTransformation;
4548
import org.locationtech.jts.geom.util.GeometryFixer;
4649
import org.locationtech.jts.io.ByteOrderValues;
@@ -94,6 +97,57 @@ public static Geometry boundary(Geometry geometry) {
9497
return boundary;
9598
}
9699

100+
public static Geometry expand(Geometry geometry, double uniformDelta) {
101+
return expand(geometry, uniformDelta, uniformDelta, uniformDelta);
102+
}
103+
104+
public static Geometry expand(Geometry geometry, double deltaX, double deltaY) {
105+
return expand(geometry, deltaX, deltaY, 0);
106+
}
107+
108+
public static Geometry expand(Geometry geometry, double deltaX, double deltaY, double deltaZ) {
109+
if (geometry == null || geometry.isEmpty()) {
110+
return geometry;
111+
}
112+
113+
Coordinate[] coordinates = geometry.getCoordinates();
114+
double minX = Double.MAX_VALUE;
115+
double maxX = -Double.MAX_VALUE;
116+
double minY = Double.MAX_VALUE;
117+
double maxY = -Double.MAX_VALUE;
118+
double minZ = Double.MAX_VALUE;
119+
double maxZ = -Double.MAX_VALUE;
120+
121+
for (int i = 0; i < coordinates.length; i++) {
122+
minX = Math.min(minX, coordinates[i].x);
123+
maxX = Math.max(maxX, coordinates[i].x);
124+
minY = Math.min(minY, coordinates[i].y);
125+
maxY = Math.max(maxY, coordinates[i].y);
126+
minZ = Math.min(minZ, coordinates[i].z);
127+
maxZ = Math.max(maxZ, coordinates[i].z);
128+
}
129+
130+
minX = minX - deltaX;
131+
maxX = maxX + deltaX;
132+
minY = minY - deltaY;
133+
maxY = maxY + deltaY;
134+
135+
if (Functions.hasZ(geometry)) {
136+
minZ = minZ - deltaZ;
137+
maxZ = maxZ + deltaZ;
138+
Coordinate[] newCoords = new Coordinate[5];
139+
newCoords[0] = new Coordinate(minX, minY, minZ);
140+
newCoords[1] = new Coordinate(minX, maxY, minZ);
141+
newCoords[2] = new Coordinate(maxX, maxY, maxZ);
142+
newCoords[3] = new Coordinate(maxX, minY, maxZ);
143+
newCoords[4] = newCoords[0];
144+
return geometry.getFactory().createPolygon(newCoords);
145+
}
146+
Geometry result = Constructors.polygonFromEnvelope(minX, minY, maxX, maxY);
147+
result.setSRID(geometry.getSRID());
148+
return result;
149+
}
150+
97151
public static Geometry buffer(Geometry geometry, double radius) {
98152
return buffer(geometry, radius, false, "");
99153
}

common/src/test/java/org/apache/sedona/common/FunctionsTest.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2250,6 +2250,64 @@ public void nRingsMultiPolygonMixed() throws Exception {
22502250
assertEquals(expected, actual);
22512251
}
22522252

2253+
@Test
2254+
public void testExpand() throws ParseException {
2255+
Geometry geometry =
2256+
GEOMETRY_FACTORY.createPolygon(coordArray(50, 50, 50, 80, 80, 80, 80, 50, 50, 50));
2257+
String actual = Functions.asWKT(Functions.expand(geometry, 10, 0));
2258+
String expected = "POLYGON ((40 50, 40 80, 90 80, 90 50, 40 50))";
2259+
assertEquals(expected, actual);
2260+
2261+
geometry = Constructors.geomFromWKT("POINT (10 20 1)", 4326);
2262+
Geometry result = Functions.expand(geometry, 10);
2263+
actual = Functions.asWKT(result);
2264+
expected = "POLYGON Z((0 10 -9, 0 30 -9, 20 30 11, 20 10 11, 0 10 -9))";
2265+
assertEquals(expected, actual);
2266+
assertEquals(4326, result.getSRID());
2267+
2268+
geometry = Constructors.geomFromWKT("LINESTRING (0 0, 1 1, 2 2)", 0);
2269+
actual = Functions.asWKT(Functions.expand(geometry, 10, 10));
2270+
expected = "POLYGON ((-10 -10, -10 12, 12 12, 12 -10, -10 -10))";
2271+
assertEquals(expected, actual);
2272+
2273+
geometry =
2274+
Constructors.geomFromWKT(
2275+
"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)))",
2276+
4326);
2277+
actual = Functions.asWKT(Functions.expand(geometry, 10.5, 2, 5));
2278+
expected = "POLYGON Z((31.5 48 -4, 31.5 82 -4, 98.5 82 8, 98.5 48 8, 31.5 48 -4))";
2279+
assertEquals(expected, actual);
2280+
2281+
geometry = Constructors.geomFromWKT("MULTIPOINT((10 20 1), (20 30 2))", 0);
2282+
actual = Functions.asWKT(Functions.expand(geometry, 9.5, 3.5));
2283+
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))";
2284+
assertEquals(expected, actual);
2285+
2286+
geometry =
2287+
Constructors.geomFromWKT(
2288+
"MULTILINESTRING ((1 0 4, 2 0 4, 4 0 4),(1 0 4, 2 0 4, 4 0 4))", 0);
2289+
actual = Functions.asWKT(Functions.expand(geometry, 0));
2290+
expected = "POLYGON Z((1 0 4, 1 0 4, 4 0 4, 4 0 4, 1 0 4))";
2291+
assertEquals(expected, actual);
2292+
2293+
geometry =
2294+
Constructors.geomFromWKT(
2295+
"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))))",
2296+
1234);
2297+
result = Functions.expand(geometry, 10);
2298+
actual = Functions.asWKT(result);
2299+
expected = "POLYGON ((0 0, 0 75, 75 75, 75 0, 0 0))";
2300+
assertEquals(expected, actual);
2301+
assertEquals(1234, result.getSRID());
2302+
2303+
// The function drops the M dimension
2304+
geometry =
2305+
Constructors.geomFromWKT("POLYGON M((50 50 1, 50 80 2, 80 80 3, 80 50 2, 50 50 1))", 0);
2306+
actual = Functions.asWKT(Functions.expand(geometry, 0));
2307+
expected = "POLYGON ((50 50, 50 80, 80 80, 80 50, 50 50))";
2308+
assertEquals(expected, actual);
2309+
}
2310+
22532311
@Test
22542312
public void testBuffer() {
22552313
Polygon polygon =

docs/api/flink/Function.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1211,6 +1211,45 @@ Output:
12111211
POLYGON ((0 0, 0 3, 1 3, 1 0, 0 0))
12121212
```
12131213

1214+
## ST_Expand
1215+
1216+
Introduction: Returns a geometry expanded from the bounding box of the input. The expansion can be specified in two ways:
1217+
1218+
1. By individual axis using `deltaX`, `deltaY`, or `deltaZ` parameters.
1219+
2. Uniformly across all axes using the `uniformDelta` parameter.
1220+
1221+
!!!Note
1222+
Things to consider when using this function:
1223+
1224+
1. The `uniformDelta` parameter expands Z dimensions for XYZ geometries; otherwise, it only affects XY dimensions.
1225+
2. For XYZ geometries, specifying only `deltaX` and `deltaY` will preserve the original Z dimension.
1226+
3. If the input geometry has an M dimension then using this function will drop the said M dimension.
1227+
1228+
Format:
1229+
1230+
`ST_Expand(geometry: Geometry, uniformDelta: Double)`
1231+
1232+
`ST_Expand(geometry: Geometry, deltaX: Double, deltaY: Double)`
1233+
1234+
`ST_Expand(geometry: Geometry, deltaX: Double, deltaY: Double, deltaZ: Double)`
1235+
1236+
Since: `v1.6.1`
1237+
1238+
SQL Example:
1239+
1240+
```sql
1241+
SELECT ST_Expand(
1242+
ST_GeomFromWKT('POLYGON Z((50 50 1, 50 80 2, 80 80 3, 80 50 2, 50 50 1))'),
1243+
10
1244+
)
1245+
```
1246+
1247+
Output:
1248+
1249+
```
1250+
POLYGON Z((40 40 -9, 40 90 -9, 90 90 13, 90 40 13, 40 40 -9))
1251+
```
1252+
12141253
## ST_ExteriorRing
12151254

12161255
Introduction: Returns a LINESTRING representing the exterior ring (shell) of a POLYGON. Returns NULL if the geometry is not a polygon.

docs/api/snowflake/vector-data/Function.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,43 @@ SELECT ST_Envelope(polygondf.countyshape)
933933
FROM polygondf
934934
```
935935

936+
## ST_Expand
937+
938+
Introduction: Returns a geometry expanded from the bounding box of the input. The expansion can be specified in two ways:
939+
940+
1. By individual axis using `deltaX`, `deltaY`, or `deltaZ` parameters.
941+
2. Uniformly across all axes using the `uniformDelta` parameter.
942+
943+
Format:
944+
945+
`ST_Expand(geometry: Geometry, uniformDelta: Double)`
946+
947+
`ST_Expand(geometry: Geometry, deltaX: Double, deltaY: Double)`
948+
949+
`ST_Expand(geometry: Geometry, deltaX: Double, deltaY: Double, deltaZ: Double)`
950+
951+
!!!Note
952+
Things to consider when using this function:
953+
954+
1. The `uniformDelta` parameter expands Z dimensions for XYZ geometries; otherwise, it only affects XY dimensions.
955+
2. For XYZ geometries, specifying only `deltaX` and `deltaY` will preserve the original Z dimension.
956+
3. If the input geometry has an M dimension then using this function will drop the said M dimension.
957+
958+
SQL Example:
959+
960+
```sql
961+
SELECT ST_Expand(
962+
ST_GeomFromWKT('POLYGON Z((50 50 1, 50 80 2, 80 80 3, 80 50 2, 50 50 1))'),
963+
10
964+
)
965+
```
966+
967+
Output:
968+
969+
```
970+
POLYGON Z((40 40 -9, 40 90 -9, 90 90 13, 90 40 13, 40 40 -9))
971+
```
972+
936973
## ST_ExteriorRing
937974

938975
Introduction: Returns a line string representing the exterior ring of the POLYGON geometry. Return NULL if the geometry is not a polygon.

docs/api/sql/Function.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,6 +1213,45 @@ Output:
12131213
POLYGON ((0 0, 0 3, 1 3, 1 0, 0 0))
12141214
```
12151215

1216+
## ST_Expand
1217+
1218+
Introduction: Returns a geometry expanded from the bounding box of the input. The expansion can be specified in two ways:
1219+
1220+
1. By individual axis using `deltaX`, `deltaY`, or `deltaZ` parameters.
1221+
2. Uniformly across all axes using the `uniformDelta` parameter.
1222+
1223+
!!!Note
1224+
Things to consider when using this function:
1225+
1226+
1. The `uniformDelta` parameter expands Z dimensions for XYZ geometries; otherwise, it only affects XY dimensions.
1227+
2. For XYZ geometries, specifying only `deltaX` and `deltaY` will preserve the original Z dimension.
1228+
3. If the input geometry has an M dimension then using this function will drop the said M dimension.
1229+
1230+
Format:
1231+
1232+
`ST_Expand(geometry: Geometry, uniformDelta: Double)`
1233+
1234+
`ST_Expand(geometry: Geometry, deltaX: Double, deltaY: Double)`
1235+
1236+
`ST_Expand(geometry: Geometry, deltaX: Double, deltaY: Double, deltaZ: Double)`
1237+
1238+
Since: `v1.6.1`
1239+
1240+
SQL Example:
1241+
1242+
```sql
1243+
SELECT ST_Expand(
1244+
ST_GeomFromWKT('POLYGON Z((50 50 1, 50 80 2, 80 80 3, 80 50 2, 50 50 1))'),
1245+
10
1246+
)
1247+
```
1248+
1249+
Output:
1250+
1251+
```
1252+
POLYGON Z((40 40 -9, 40 90 -9, 90 90 13, 90 40 13, 40 40 -9))
1253+
```
1254+
12161255
## ST_ExtentBasedSubDivide
12171256

12181257
Introduction: Return a list of geometries divided based of given extent limit.

flink/src/main/java/org/apache/sedona/flink/Catalog.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public static UserDefinedFunction[] getFuncs() {
7070
new Functions.ST_ConcaveHull(),
7171
new Functions.ST_ConvexHull(),
7272
new Functions.ST_CrossesDateLine(),
73+
new Functions.ST_Expand(),
7374
new Functions.ST_Envelope(),
7475
new Functions.ST_Difference(),
7576
new Functions.ST_Dimension(),

flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,35 @@ public Geometry eval(
244244
}
245245
}
246246

247+
public static class ST_Expand extends ScalarFunction {
248+
@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class)
249+
public Geometry eval(
250+
@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class) Object o,
251+
@DataTypeHint(value = "Double") Double uniformDelta) {
252+
Geometry geom = (Geometry) o;
253+
return org.apache.sedona.common.Functions.expand(geom, uniformDelta);
254+
}
255+
256+
@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class)
257+
public Geometry eval(
258+
@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class) Object o,
259+
@DataTypeHint(value = "Double") Double deltaX,
260+
@DataTypeHint(value = "Double") Double deltaY) {
261+
Geometry geom = (Geometry) o;
262+
return org.apache.sedona.common.Functions.expand(geom, deltaX, deltaY);
263+
}
264+
265+
@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class)
266+
public Geometry eval(
267+
@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class) Object o,
268+
@DataTypeHint(value = "Double") Double deltaX,
269+
@DataTypeHint(value = "Double") Double deltaY,
270+
@DataTypeHint(value = "Double") Double deltaZ) {
271+
Geometry geom = (Geometry) o;
272+
return org.apache.sedona.common.Functions.expand(geom, deltaX, deltaY, deltaZ);
273+
}
274+
}
275+
247276
public static class ST_Dimension extends ScalarFunction {
248277
@DataTypeHint("Integer")
249278
public Integer eval(

flink/src/test/java/org/apache/sedona/flink/FunctionTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,45 @@ public void testEnvelope() {
379379
first(linestringTable).getField(0).toString());
380380
}
381381

382+
@Test
383+
public void testExpand() {
384+
Table baseTable =
385+
tableEnv.sqlQuery(
386+
"SELECT ST_GeomFromWKT('POLYGON ((50 50 1, 50 80 2, 80 80 3, 80 50 2, 50 50 1))') as geom");
387+
String actual =
388+
(String)
389+
first(
390+
baseTable
391+
.select(call(Functions.ST_Expand.class, $("geom"), 10))
392+
.as("geom")
393+
.select(call(Functions.ST_AsText.class, $("geom"))))
394+
.getField(0);
395+
String expected = "POLYGON Z((40 40 -9, 40 90 -9, 90 90 13, 90 40 13, 40 40 -9))";
396+
assertEquals(expected, actual);
397+
398+
actual =
399+
(String)
400+
first(
401+
baseTable
402+
.select(call(Functions.ST_Expand.class, $("geom"), 5, 6))
403+
.as("geom")
404+
.select(call(Functions.ST_AsText.class, $("geom"))))
405+
.getField(0);
406+
expected = "POLYGON Z((45 44 1, 45 86 1, 85 86 3, 85 44 3, 45 44 1))";
407+
assertEquals(expected, actual);
408+
409+
actual =
410+
(String)
411+
first(
412+
baseTable
413+
.select(call(Functions.ST_Expand.class, $("geom"), 6, 5, -3))
414+
.as("geom")
415+
.select(call(Functions.ST_AsText.class, $("geom"))))
416+
.getField(0);
417+
expected = "POLYGON Z((44 45 4, 44 85 4, 86 85 0, 86 45 0, 44 45 4))";
418+
assertEquals(expected, actual);
419+
}
420+
382421
@Test
383422
public void testFlipCoordinates() {
384423
Table pointTable = createPointTable_real(testDataSize);

python/sedona/sql/st_functions.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,31 @@ def ST_Envelope(geometry: ColumnOrName) -> Column:
506506
return _call_st_function("ST_Envelope", geometry)
507507

508508

509+
@validate_argument_types
510+
def ST_Expand(geometry: ColumnOrName, deltaX_uniformDelta: Union[ColumnOrName, float], deltaY: Optional[Union[ColumnOrName, float]] = None, deltaZ: Optional[Union[ColumnOrName, float]] = None) -> Column:
511+
"""Expand the given geometry column by a constant unit in each direction
512+
513+
:param geometry: Geometry column to calculate the envelope of.
514+
:type geometry: ColumnOrName
515+
:param deltaX_uniformDelta: it is either deltaX or uniformDelta depending on the number of arguments provided
516+
:type deltaX_uniformDelta: Union[ColumnOrName, float]
517+
:param deltaY: Constant unit of deltaY
518+
:type deltaY: Union[ColumnOrName, float]
519+
:param deltaZ: Constant unit of deltaZ
520+
:type deltaZ: Union[ColumnOrName, float]
521+
:return: Envelope of geometry as a geometry column.
522+
:rtype: Column
523+
"""
524+
if deltaZ is None:
525+
args = (geometry, deltaX_uniformDelta, deltaY)
526+
if deltaY is None:
527+
args = (geometry, deltaX_uniformDelta)
528+
else:
529+
args = (geometry, deltaX_uniformDelta, deltaY, deltaZ)
530+
531+
return _call_st_function("ST_Expand", args)
532+
533+
509534
@validate_argument_types
510535
def ST_ExteriorRing(polygon: ColumnOrName) -> Column:
511536
"""Get a linestring representing the exterior ring of a polygon geometry

0 commit comments

Comments
 (0)