You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ji...@apache.org on 2026/06/11 07:44:08 UTC

(sedona) branch master updated: [GH-3039] Flink Box3D accessors, ST_AsText, and ST_3DExtent (#3040)

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

jiayuasu 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 1a633fccbe [GH-3039] Flink Box3D accessors, ST_AsText, and ST_3DExtent (#3040)
1a633fccbe is described below

commit 1a633fccbe7be1c65b36480f9d03b75f0cab7336
Author: Jia Yu <ji...@apache.org>
AuthorDate: Thu Jun 11 00:44:03 2026 -0700

    [GH-3039] Flink Box3D accessors, ST_AsText, and ST_3DExtent (#3040)
---
 .../main/java/org/apache/sedona/flink/Catalog.java |  1 +
 .../sedona/flink/expressions/Accumulators.java     | 18 ++++++
 .../sedona/flink/expressions/Aggregators.java      | 65 ++++++++++++++++++++
 .../apache/sedona/flink/expressions/Functions.java | 70 ++++++++++++++++++++++
 .../org/apache/sedona/flink/AggregatorTest.java    | 33 ++++++++++
 .../java/org/apache/sedona/flink/FunctionTest.java | 18 ++++++
 6 files changed, 205 insertions(+)

diff --git a/flink/src/main/java/org/apache/sedona/flink/Catalog.java b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
index 65097b0e44..e66ff5dfaa 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -26,6 +26,7 @@ public class Catalog {
     return new UserDefinedFunction[] {
       new Aggregators.ST_Envelope_Aggr(),
       new Aggregators.ST_Extent(),
+      new Aggregators.ST_3DExtent(),
       new Aggregators.ST_Intersection_Aggr(),
       new Aggregators.ST_Union_Aggr(),
       // Aliases for *_Aggr functions with *_Agg suffix
diff --git a/flink/src/main/java/org/apache/sedona/flink/expressions/Accumulators.java b/flink/src/main/java/org/apache/sedona/flink/expressions/Accumulators.java
index f51784870a..9550934db9 100644
--- a/flink/src/main/java/org/apache/sedona/flink/expressions/Accumulators.java
+++ b/flink/src/main/java/org/apache/sedona/flink/expressions/Accumulators.java
@@ -39,6 +39,24 @@ public class Accumulators {
     }
   }
 
+  public static class Envelope3D {
+    public double minX = Double.MAX_VALUE;
+    public double minY = Double.MAX_VALUE;
+    public double minZ = Double.MAX_VALUE;
+    public double maxX = -Double.MAX_VALUE;
+    public double maxY = -Double.MAX_VALUE;
+    public double maxZ = -Double.MAX_VALUE;
+
+    void reset() {
+      minX = Double.MAX_VALUE;
+      minY = Double.MAX_VALUE;
+      minZ = Double.MAX_VALUE;
+      maxX = -Double.MAX_VALUE;
+      maxY = -Double.MAX_VALUE;
+      maxZ = -Double.MAX_VALUE;
+    }
+  }
+
   public static class AccGeometry {
     @DataTypeHint(
         value = "RAW",
diff --git a/flink/src/main/java/org/apache/sedona/flink/expressions/Aggregators.java b/flink/src/main/java/org/apache/sedona/flink/expressions/Aggregators.java
index 1e15c04f37..ff13fd8e73 100644
--- a/flink/src/main/java/org/apache/sedona/flink/expressions/Aggregators.java
+++ b/flink/src/main/java/org/apache/sedona/flink/expressions/Aggregators.java
@@ -21,7 +21,9 @@ package org.apache.sedona.flink.expressions;
 import org.apache.flink.table.annotation.DataTypeHint;
 import org.apache.flink.table.functions.AggregateFunction;
 import org.apache.sedona.common.geometryObjects.Box2D;
+import org.apache.sedona.common.geometryObjects.Box3D;
 import org.apache.sedona.flink.Box2DTypeSerializer;
+import org.apache.sedona.flink.Box3DTypeSerializer;
 import org.apache.sedona.flink.GeometryTypeSerializer;
 import org.locationtech.jts.geom.Coordinate;
 import org.locationtech.jts.geom.Envelope;
@@ -168,6 +170,69 @@ public class Aggregators {
     }
   }
 
+  // Aggregate the 3D bounding box of all input geometries as a Box3D. Mirrors PostGIS
+  // ST_3DExtent. Geometries without a Z dimension fold into z = 0 per coordinate (via
+  // Functions.box3D). Returns null when there are no rows or all inputs are null/empty.
+  @DataTypeHint(value = "RAW", rawSerializer = Box3DTypeSerializer.class, bridgedTo = Box3D.class)
+  public static class ST_3DExtent extends AggregateFunction<Box3D, Accumulators.Envelope3D> {
+
+    @Override
+    public Accumulators.Envelope3D createAccumulator() {
+      return new Accumulators.Envelope3D();
+    }
+
+    @Override
+    @DataTypeHint(value = "RAW", rawSerializer = Box3DTypeSerializer.class, bridgedTo = Box3D.class)
+    public Box3D getValue(Accumulators.Envelope3D acc) {
+      if (acc.minX > acc.maxX) return null;
+      return new Box3D(acc.minX, acc.minY, acc.minZ, acc.maxX, acc.maxY, acc.maxZ);
+    }
+
+    public void accumulate(
+        Accumulators.Envelope3D acc,
+        @DataTypeHint(
+                value = "RAW",
+                rawSerializer = GeometryTypeSerializer.class,
+                bridgedTo = Geometry.class)
+            Object o) {
+      if (o == null) return;
+      // Functions.box3D folds missing Z to 0 and returns null for empty geometries.
+      Box3D box = org.apache.sedona.common.Functions.box3D((Geometry) o);
+      if (box == null) return;
+      acc.minX = Math.min(acc.minX, box.getXMin());
+      acc.minY = Math.min(acc.minY, box.getYMin());
+      acc.minZ = Math.min(acc.minZ, box.getZMin());
+      acc.maxX = Math.max(acc.maxX, box.getXMax());
+      acc.maxY = Math.max(acc.maxY, box.getYMax());
+      acc.maxZ = Math.max(acc.maxZ, box.getZMax());
+    }
+
+    public void retract(
+        Accumulators.Envelope3D acc,
+        @DataTypeHint(
+                value = "RAW",
+                rawSerializer = GeometryTypeSerializer.class,
+                bridgedTo = Geometry.class)
+            Object o) {
+      assert (false);
+    }
+
+    public void merge(Accumulators.Envelope3D acc, Iterable<Accumulators.Envelope3D> it) {
+      for (Accumulators.Envelope3D a : it) {
+        acc.minX = Math.min(acc.minX, a.minX);
+        acc.minY = Math.min(acc.minY, a.minY);
+        acc.minZ = Math.min(acc.minZ, a.minZ);
+        acc.maxX = Math.max(acc.maxX, a.maxX);
+        acc.maxY = Math.max(acc.maxY, a.maxY);
+        acc.maxZ = Math.max(acc.maxZ, a.maxZ);
+      }
+    }
+
+    public void resetAccumulator(Accumulators.Envelope3D acc) {
+      acc.reset();
+    }
+  }
+
   // Compute the Union boundary of numbers of geometries
   //
   @DataTypeHint(
diff --git a/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java b/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
index 6d6560910a..e32835a780 100644
--- a/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
+++ b/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
@@ -984,6 +984,16 @@ public class Functions {
             Box2D box) {
       return org.apache.sedona.common.Functions.yMin(box);
     }
+
+    @DataTypeHint("Double")
+    public Double eval(
+        @DataTypeHint(
+                value = "RAW",
+                rawSerializer = Box3DTypeSerializer.class,
+                bridgedTo = Box3D.class)
+            Box3D box) {
+      return org.apache.sedona.common.Functions.yMin(box);
+    }
   }
 
   public static class ST_YMax extends ScalarFunction {
@@ -1007,6 +1017,16 @@ public class Functions {
             Box2D box) {
       return org.apache.sedona.common.Functions.yMax(box);
     }
+
+    @DataTypeHint("Double")
+    public Double eval(
+        @DataTypeHint(
+                value = "RAW",
+                rawSerializer = Box3DTypeSerializer.class,
+                bridgedTo = Box3D.class)
+            Box3D box) {
+      return org.apache.sedona.common.Functions.yMax(box);
+    }
   }
 
   public static class ST_ZMax extends ScalarFunction {
@@ -1020,6 +1040,16 @@ public class Functions {
       Geometry geom = (Geometry) o;
       return org.apache.sedona.common.Functions.zMax(geom);
     }
+
+    @DataTypeHint("Double")
+    public Double eval(
+        @DataTypeHint(
+                value = "RAW",
+                rawSerializer = Box3DTypeSerializer.class,
+                bridgedTo = Box3D.class)
+            Box3D box) {
+      return org.apache.sedona.common.Functions.zMax(box);
+    }
   }
 
   public static class ST_ZMin extends ScalarFunction {
@@ -1033,6 +1063,16 @@ public class Functions {
       Geometry geom = (Geometry) o;
       return org.apache.sedona.common.Functions.zMin(geom);
     }
+
+    @DataTypeHint("Double")
+    public Double eval(
+        @DataTypeHint(
+                value = "RAW",
+                rawSerializer = Box3DTypeSerializer.class,
+                bridgedTo = Box3D.class)
+            Box3D box) {
+      return org.apache.sedona.common.Functions.zMin(box);
+    }
   }
 
   public static class ST_NDims extends ScalarFunction {
@@ -1370,6 +1410,16 @@ public class Functions {
             Box2D box) {
       return org.apache.sedona.common.Functions.box2dAsText(box);
     }
+
+    @DataTypeHint("String")
+    public String eval(
+        @DataTypeHint(
+                value = "RAW",
+                rawSerializer = Box3DTypeSerializer.class,
+                bridgedTo = Box3D.class)
+            Box3D box) {
+      return org.apache.sedona.common.Functions.box3dAsText(box);
+    }
   }
 
   public static class ST_AsEWKB extends ScalarFunction {
@@ -1592,6 +1642,16 @@ public class Functions {
             Box2D box) {
       return org.apache.sedona.common.Functions.xMax(box);
     }
+
+    @DataTypeHint("Double")
+    public Double eval(
+        @DataTypeHint(
+                value = "RAW",
+                rawSerializer = Box3DTypeSerializer.class,
+                bridgedTo = Box3D.class)
+            Box3D box) {
+      return org.apache.sedona.common.Functions.xMax(box);
+    }
   }
 
   public static class ST_XMin extends ScalarFunction {
@@ -1615,6 +1675,16 @@ public class Functions {
             Box2D box) {
       return org.apache.sedona.common.Functions.xMin(box);
     }
+
+    @DataTypeHint("Double")
+    public Double eval(
+        @DataTypeHint(
+                value = "RAW",
+                rawSerializer = Box3DTypeSerializer.class,
+                bridgedTo = Box3D.class)
+            Box3D box) {
+      return org.apache.sedona.common.Functions.xMin(box);
+    }
   }
 
   public static class ST_BuildArea extends ScalarFunction {
diff --git a/flink/src/test/java/org/apache/sedona/flink/AggregatorTest.java b/flink/src/test/java/org/apache/sedona/flink/AggregatorTest.java
index e1af138fdc..1fb0509a84 100644
--- a/flink/src/test/java/org/apache/sedona/flink/AggregatorTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/AggregatorTest.java
@@ -25,6 +25,7 @@ import static org.junit.Assert.assertNull;
 import org.apache.flink.table.api.*;
 import org.apache.flink.types.Row;
 import org.apache.sedona.common.geometryObjects.Box2D;
+import org.apache.sedona.common.geometryObjects.Box3D;
 import org.apache.sedona.flink.expressions.Functions;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -64,6 +65,38 @@ public class AggregatorTest extends TestBase {
     assertEquals(5.0, bbox.getYMax(), 0.0);
   }
 
+  @Test
+  public void test3DExtent() {
+    tableEnv.executeSql(
+        "CREATE OR REPLACE TEMPORARY VIEW extent3d_view AS "
+            + "SELECT ST_GeomFromWKT(wkt) as geom FROM ("
+            + "VALUES ('POINT Z (1 2 3)'), ('POINT Z (4 5 -1)'), ('LINESTRING (-3 0, 0 0)')"
+            + ") AS t(wkt)");
+    Table result = tableEnv.sqlQuery("SELECT ST_3DExtent(geom) FROM extent3d_view");
+    Row last = last(result);
+    Box3D bbox = (Box3D) last.getField(0);
+    assertEquals(-3.0, bbox.getXMin(), 0.0);
+    assertEquals(0.0, bbox.getYMin(), 0.0);
+    // z-min is -1.0 from POINT Z (4 5 -1); the XY-only linestring folds to z = 0, which sits
+    // between the -1 and 3 of the two POINT Z rows and so does not move either Z bound.
+    assertEquals(-1.0, bbox.getZMin(), 0.0);
+    assertEquals(4.0, bbox.getXMax(), 0.0);
+    assertEquals(5.0, bbox.getYMax(), 0.0);
+    assertEquals(3.0, bbox.getZMax(), 0.0);
+  }
+
+  @Test
+  public void test3DExtent_EmptyAndNullGeometries() {
+    tableEnv.executeSql(
+        "CREATE OR REPLACE TEMPORARY VIEW null_extent3d_view AS "
+            + "SELECT ST_GeomFromWKT(wkt) as geom FROM ("
+            + "VALUES (CAST(NULL AS STRING)), ('POINT EMPTY'), ('POLYGON EMPTY')"
+            + ") AS t(wkt)");
+    Table result = tableEnv.sqlQuery("SELECT ST_3DExtent(geom) FROM null_extent3d_view");
+    Row last = last(result);
+    assertNull(last.getField(0));
+  }
+
   @Test
   public void testExtent_EmptyAndNullGeometries() {
     tableEnv.executeSql(
diff --git a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
index f565b591d5..e0621869e3 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -506,6 +506,24 @@ public class FunctionTest extends TestBase {
     assertEquals(5.0, row.getField(4));
   }
 
+  @Test
+  public void testBox3DAsTextAndAccessors() {
+    Table t =
+        tableEnv.sqlQuery(
+            "WITH bx AS ("
+                + " SELECT ST_Box3D(ST_GeomFromText('LINESTRING Z(0 0 -3, 5 10 7)')) AS b)"
+                + " SELECT ST_AsText(b), ST_XMin(b), ST_YMin(b), ST_ZMin(b),"
+                + " ST_XMax(b), ST_YMax(b), ST_ZMax(b) FROM bx");
+    Row row = first(t);
+    assertEquals("BOX3D(0.0 0.0 -3.0, 5.0 10.0 7.0)", row.getField(0));
+    assertEquals(0.0, row.getField(1));
+    assertEquals(0.0, row.getField(2));
+    assertEquals(-3.0, row.getField(3));
+    assertEquals(5.0, row.getField(4));
+    assertEquals(10.0, row.getField(5));
+    assertEquals(7.0, row.getField(6));
+  }
+
   @Test
   public void testEnvelope() {
     Table linestringTable = createLineStringTable(1);