Skip to content

Commit f11b20b

Browse files
authored
[GH-2925] Add ST_Expand(box2d, ...) overloads (#2930)
1 parent 5f3af20 commit f11b20b

7 files changed

Lines changed: 164 additions & 3 deletions

File tree

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,28 @@ public static Geometry expand(Geometry geometry, double deltaX, double deltaY) {
235235
return expand(geometry, deltaX, deltaY, 0);
236236
}
237237

238+
/** Expand a {@link Box2D} uniformly on both axes. NULL on null input. */
239+
public static Box2D expand(Box2D box, double uniformDelta) {
240+
return expand(box, uniformDelta, uniformDelta);
241+
}
242+
243+
/**
244+
* Expand a {@link Box2D} by the given per-axis deltas. Negative deltas shrink the bbox; if a
245+
* negative delta produces {@code xmin > xmax} or {@code ymin > ymax}, the resulting Box2D is
246+
* returned as-is (callers can detect the degenerate result via the accessor functions). NULL on
247+
* null input.
248+
*/
249+
public static Box2D expand(Box2D box, double deltaX, double deltaY) {
250+
if (box == null) {
251+
return null;
252+
}
253+
return new Box2D(
254+
box.getXMin() - deltaX,
255+
box.getYMin() - deltaY,
256+
box.getXMax() + deltaX,
257+
box.getYMax() + deltaY);
258+
}
259+
238260
public static Geometry expand(Geometry geometry, double deltaX, double deltaY, double deltaZ) {
239261
if (geometry == null || geometry.isEmpty()) {
240262
return geometry;

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,30 @@ public void box2dAsText() {
176176
assertNull(Functions.box2dAsText(null));
177177
}
178178

179+
@Test
180+
public void expandBox2D() {
181+
Box2D box = new Box2D(1.0, 2.0, 4.0, 5.0);
182+
183+
Box2D uniform = Functions.expand(box, 1.0);
184+
assertEquals(0.0, uniform.getXMin(), 0.0);
185+
assertEquals(1.0, uniform.getYMin(), 0.0);
186+
assertEquals(5.0, uniform.getXMax(), 0.0);
187+
assertEquals(6.0, uniform.getYMax(), 0.0);
188+
189+
Box2D perAxis = Functions.expand(box, 2.0, 0.5);
190+
assertEquals(-1.0, perAxis.getXMin(), 0.0);
191+
assertEquals(1.5, perAxis.getYMin(), 0.0);
192+
assertEquals(6.0, perAxis.getXMax(), 0.0);
193+
assertEquals(5.5, perAxis.getYMax(), 0.0);
194+
195+
// Negative deltas may produce a degenerate bbox (xmin > xmax); we return as-is.
196+
Box2D shrunkPastZero = Functions.expand(new Box2D(0.0, 0.0, 1.0, 1.0), -2.0);
197+
assertTrue(shrunkPastZero.getXMin() > shrunkPastZero.getXMax());
198+
199+
assertNull(Functions.expand((Box2D) null, 1.0));
200+
assertNull(Functions.expand((Box2D) null, 1.0, 1.0));
201+
}
202+
179203
@Test
180204
public void asWKB() throws Exception {
181205
Geometry geometry = GEOMETRY_FACTORY.createPoint(new Coordinate(1.0, 2.0));

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,29 @@ public Geometry eval(
581581
Geometry geom = (Geometry) o;
582582
return org.apache.sedona.common.Functions.expand(geom, deltaX, deltaY, deltaZ);
583583
}
584+
585+
@DataTypeHint(value = "RAW", rawSerializer = Box2DTypeSerializer.class, bridgedTo = Box2D.class)
586+
public Box2D eval(
587+
@DataTypeHint(
588+
value = "RAW",
589+
rawSerializer = Box2DTypeSerializer.class,
590+
bridgedTo = Box2D.class)
591+
Box2D box,
592+
@DataTypeHint(value = "Double") Double uniformDelta) {
593+
return org.apache.sedona.common.Functions.expand(box, uniformDelta);
594+
}
595+
596+
@DataTypeHint(value = "RAW", rawSerializer = Box2DTypeSerializer.class, bridgedTo = Box2D.class)
597+
public Box2D eval(
598+
@DataTypeHint(
599+
value = "RAW",
600+
rawSerializer = Box2DTypeSerializer.class,
601+
bridgedTo = Box2D.class)
602+
Box2D box,
603+
@DataTypeHint(value = "Double") Double deltaX,
604+
@DataTypeHint(value = "Double") Double deltaY) {
605+
return org.apache.sedona.common.Functions.expand(box, deltaX, deltaY);
606+
}
584607
}
585608

586609
public static class ST_Dimension extends ScalarFunction {

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,34 @@ public void testExpand() {
535535
assertEquals(expected, actual);
536536
}
537537

538+
@Test
539+
public void testExpandBox2D() {
540+
Table t =
541+
tableEnv.sqlQuery(
542+
"SELECT ST_Expand(ST_Box2D(ST_GeomFromText('POLYGON((1 2, 1 5, 4 5, 4 2, 1 2))')), 1.0) AS uniform,"
543+
+ " ST_Expand(ST_Box2D(ST_GeomFromText('POLYGON((1 2, 1 5, 4 5, 4 2, 1 2))')), 2.0, 0.5) AS per_axis");
544+
Row row = first(t);
545+
Box2D uniform = (Box2D) row.getField(0);
546+
assertEquals(0.0, uniform.getXMin(), 0.0);
547+
assertEquals(1.0, uniform.getYMin(), 0.0);
548+
assertEquals(5.0, uniform.getXMax(), 0.0);
549+
assertEquals(6.0, uniform.getYMax(), 0.0);
550+
Box2D perAxis = (Box2D) row.getField(1);
551+
assertEquals(-1.0, perAxis.getXMin(), 0.0);
552+
assertEquals(1.5, perAxis.getYMin(), 0.0);
553+
assertEquals(6.0, perAxis.getXMax(), 0.0);
554+
assertEquals(5.5, perAxis.getYMax(), 0.0);
555+
556+
// NULL Box2D input propagates to NULL for both signatures.
557+
Table tNull =
558+
tableEnv.sqlQuery(
559+
"SELECT ST_Expand(ST_Box2D(ST_GeomFromText(CAST(NULL AS STRING))), 1.0) AS u,"
560+
+ " ST_Expand(ST_Box2D(ST_GeomFromText(CAST(NULL AS STRING))), 1.0, 1.0) AS p");
561+
Row nullRow = first(tNull);
562+
assertNull(nullRow.getField(0));
563+
assertNull(nullRow.getField(1));
564+
}
565+
538566
@Test
539567
public void testFlipCoordinates() {
540568
Table pointTable = createPointTable_real(testDataSize);

python/tests/sql/test_function.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,29 @@ def test_st_expand(self):
243243
expected = "POLYGON Z((44 45 4, 44 85 4, 86 85 0, 86 45 0, 44 45 4))"
244244
assert expected == actual
245245

246+
def test_st_expand_box_2d(self):
247+
df = self.spark.sql("""
248+
SELECT
249+
ST_Expand(ST_Box2D(ST_GeomFromText('POLYGON((1 2, 1 5, 4 5, 4 2, 1 2))')), 1.0) AS uniform,
250+
ST_Expand(ST_Box2D(ST_GeomFromText('POLYGON((1 2, 1 5, 4 5, 4 2, 1 2))')), 2.0, 0.5) AS per_axis,
251+
ST_Expand(ST_Box2D(ST_GeomFromText(NULL)), 1.0) AS null_uniform,
252+
ST_Expand(ST_Box2D(ST_GeomFromText(NULL)), 1.0, 1.0) AS null_per_axis
253+
""")
254+
row = df.first()
255+
uniform = row[0]
256+
assert uniform.xmin == 0.0
257+
assert uniform.ymin == 1.0
258+
assert uniform.xmax == 5.0
259+
assert uniform.ymax == 6.0
260+
per_axis = row[1]
261+
assert per_axis.xmin == -1.0
262+
assert per_axis.ymin == 1.5
263+
assert per_axis.xmax == 6.0
264+
assert per_axis.ymax == 5.5
265+
# NULL Box2D input deserializes to None for both signatures.
266+
assert row[2] is None
267+
assert row[3] is None
268+
246269
def test_st_centroid(self):
247270
polygon_wkt_df = (
248271
self.spark.read.format("csv")

spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,12 @@ private[apache] case class ST_Box2D(inputExpressions: Seq[Expression])
260260

261261
private[apache] case class ST_Expand(inputExpressions: Seq[Expression])
262262
extends InferredExpression(
263-
inferrableFunction4(Functions.expand),
264-
inferrableFunction3(Functions.expand),
265-
inferrableFunction2(Functions.expand)) {
263+
inferrableFunction4((g: Geometry, dx: Double, dy: Double, dz: Double) =>
264+
Functions.expand(g, dx, dy, dz)),
265+
inferrableFunction3((g: Geometry, dx: Double, dy: Double) => Functions.expand(g, dx, dy)),
266+
inferrableFunction2((g: Geometry, delta: Double) => Functions.expand(g, delta)),
267+
inferrableFunction3((b: Box2D, dx: Double, dy: Double) => Functions.expand(b, dx, dy)),
268+
inferrableFunction2((b: Box2D, delta: Double) => Functions.expand(b, delta))) {
266269

267270
protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = {
268271
copy(inputExpressions = newChildren)

spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,44 @@ class functionTestScala
262262
assertEquals(expected, actual)
263263
}
264264

265+
it("Passed ST_Expand for Box2D") {
266+
val df = sparkSession.sql("""
267+
WITH t AS (
268+
SELECT ST_Box2D(ST_GeomFromText('POLYGON((1 2, 1 5, 4 5, 4 2, 1 2))')) AS bbox,
269+
ST_Box2D(ST_GeomFromText(NULL)) AS bbox_null
270+
)
271+
SELECT
272+
ST_Expand(bbox, 1.0) AS uniform,
273+
ST_Expand(bbox, 2.0, 0.5) AS per_axis,
274+
ST_Expand(bbox, -1.0) AS shrink,
275+
ST_Expand(bbox_null, 1.0) AS null_uniform,
276+
ST_Expand(bbox_null, 1.0, 1.0) AS null_per_axis
277+
FROM t
278+
""")
279+
val row = df.collect()(0)
280+
281+
val uniform = row.getAs[Box2D]("uniform")
282+
assert(uniform.getXMin == 0.0)
283+
assert(uniform.getYMin == 1.0)
284+
assert(uniform.getXMax == 5.0)
285+
assert(uniform.getYMax == 6.0)
286+
287+
val perAxis = row.getAs[Box2D]("per_axis")
288+
assert(perAxis.getXMin == -1.0)
289+
assert(perAxis.getYMin == 1.5)
290+
assert(perAxis.getXMax == 6.0)
291+
assert(perAxis.getYMax == 5.5)
292+
293+
val shrink = row.getAs[Box2D]("shrink")
294+
assert(shrink.getXMin == 2.0)
295+
assert(shrink.getYMin == 3.0)
296+
assert(shrink.getXMax == 3.0)
297+
assert(shrink.getYMax == 4.0)
298+
299+
assert(row.isNullAt(3))
300+
assert(row.isNullAt(4))
301+
}
302+
265303
it("Passed ST_YMax") {
266304
var test = sparkSession.sql(
267305
"SELECT ST_YMax(ST_GeomFromWKT('POLYGON ((-3 -3, 3 -3, 3 -2, -3 -1, -3 -3))'))")

0 commit comments

Comments
 (0)