|
19 | 19 | package org.apache.spark.sql.sedona_sql.expressions |
20 | 20 |
|
21 | 21 | import org.apache.sedona.common.Functions |
22 | | -import org.apache.sedona.common.geometryObjects.Box2D |
| 22 | +import org.apache.sedona.common.geometryObjects.{Box2D, Box3D} |
23 | 23 | import org.apache.spark.sql.{Encoder, Encoders} |
24 | 24 | import org.apache.spark.sql.catalyst.encoders.ExpressionEncoder |
25 | 25 | import org.apache.spark.sql.expressions.Aggregator |
@@ -210,6 +210,85 @@ private[apache] class ST_Extent extends Aggregator[Geometry, Option[EnvelopeBuff |
210 | 210 | def zero: Option[EnvelopeBuffer] = None |
211 | 211 | } |
212 | 212 |
|
| 213 | +/** |
| 214 | + * Aggregator-buffer for the 3D extent. Geometries without a Z dimension fold into the `z = 0` |
| 215 | + * plane on a per-coordinate basis, matching PostGIS's flat-XY-treated-as-XY[Z=0] convention. |
| 216 | + */ |
| 217 | +case class Envelope3DBuffer( |
| 218 | + minX: Double, |
| 219 | + maxX: Double, |
| 220 | + minY: Double, |
| 221 | + maxY: Double, |
| 222 | + minZ: Double, |
| 223 | + maxZ: Double) { |
| 224 | + def isNull: Boolean = minX > maxX |
| 225 | + |
| 226 | + def merge(other: Envelope3DBuffer): Envelope3DBuffer = { |
| 227 | + if (this.isNull) other |
| 228 | + else if (other.isNull) this |
| 229 | + else |
| 230 | + Envelope3DBuffer( |
| 231 | + math.min(this.minX, other.minX), |
| 232 | + math.max(this.maxX, other.maxX), |
| 233 | + math.min(this.minY, other.minY), |
| 234 | + math.max(this.maxY, other.maxY), |
| 235 | + math.min(this.minZ, other.minZ), |
| 236 | + math.max(this.maxZ, other.maxZ)) |
| 237 | + } |
| 238 | +} |
| 239 | + |
| 240 | +/** |
| 241 | + * Return the 3D bounding box (Box3D) of all geometries in the given column. Returns NULL when the |
| 242 | + * input contains no rows or all rows are null/empty geometries. Mirrors PostGIS `ST_3DExtent`. |
| 243 | + * Geometries without a Z dimension are treated as having `z = 0`. |
| 244 | + */ |
| 245 | +private[apache] class ST_3DExtent extends Aggregator[Geometry, Option[Envelope3DBuffer], Box3D] { |
| 246 | + |
| 247 | + val outputSerde: ExpressionEncoder[Box3D] = ExpressionEncoder[Box3D]() |
| 248 | + |
| 249 | + def reduce(buffer: Option[Envelope3DBuffer], input: Geometry): Option[Envelope3DBuffer] = { |
| 250 | + if (input == null || input.isEmpty) return buffer |
| 251 | + val box = Box3D.fromGeometry(input) |
| 252 | + if (box == null) return buffer |
| 253 | + val incoming = Envelope3DBuffer( |
| 254 | + box.getXMin, |
| 255 | + box.getXMax, |
| 256 | + box.getYMin, |
| 257 | + box.getYMax, |
| 258 | + box.getZMin, |
| 259 | + box.getZMax) |
| 260 | + buffer match { |
| 261 | + case Some(b) => Some(b.merge(incoming)) |
| 262 | + case None => Some(incoming) |
| 263 | + } |
| 264 | + } |
| 265 | + |
| 266 | + def merge( |
| 267 | + buffer1: Option[Envelope3DBuffer], |
| 268 | + buffer2: Option[Envelope3DBuffer]): Option[Envelope3DBuffer] = { |
| 269 | + (buffer1, buffer2) match { |
| 270 | + case (Some(b1), Some(b2)) => Some(b1.merge(b2)) |
| 271 | + case (Some(_), None) => buffer1 |
| 272 | + case (None, Some(_)) => buffer2 |
| 273 | + case (None, None) => None |
| 274 | + } |
| 275 | + } |
| 276 | + |
| 277 | + def finish(reduction: Option[Envelope3DBuffer]): Box3D = { |
| 278 | + reduction match { |
| 279 | + case Some(b) => new Box3D(b.minX, b.minY, b.minZ, b.maxX, b.maxY, b.maxZ) |
| 280 | + case None => null |
| 281 | + } |
| 282 | + } |
| 283 | + |
| 284 | + def bufferEncoder: Encoder[Option[Envelope3DBuffer]] = |
| 285 | + Encoders.product[Option[Envelope3DBuffer]] |
| 286 | + |
| 287 | + def outputEncoder: ExpressionEncoder[Box3D] = outputSerde |
| 288 | + |
| 289 | + def zero: Option[Envelope3DBuffer] = None |
| 290 | +} |
| 291 | + |
213 | 292 | /** |
214 | 293 | * Return the polygon intersection of all Polygon in the given column |
215 | 294 | */ |
|
0 commit comments