This is an automated email from the ASF dual-hosted git repository.
kontinuation pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git
The following commit(s) were added to refs/heads/main by this push:
new 0bf4ad70 fix(raster): RS_Envelope returns axis-aligned bounding box
for skewed rasters (#594)
0bf4ad70 is described below
commit 0bf4ad7005e157c96d616a5eb3e544ad698da133
Author: Kristin Cowalcijk <[email protected]>
AuthorDate: Wed Feb 11 20:39:13 2026 +0800
fix(raster): RS_Envelope returns axis-aligned bounding box for skewed
rasters (#594)
## Summary
- **Fix `RS_Envelope`** to return the axis-aligned bounding box (AABB)
instead of the convex hull for skewed/rotated rasters, matching PostGIS
`ST_Envelope` semantics.
- **Fix `generate_test_rasters`** to give raster `i=0` non-zero scales via
`i.max(1)`, so it has an invertible geotransform.
- **Add `build_noninvertible_raster()`** helper to `sedona-testing` for
tests that need a raster with zero scales/skews.
- Update cascading test expectations in `rs_envelope`, `rs_geotransform`,
and `rs_rastercoordinate`.
---
rust/sedona-raster-functions/src/rs_envelope.rs | 54 +++++++++++++---------
.../sedona-raster-functions/src/rs_geotransform.rs | 4 +-
.../src/rs_rastercoordinate.rs | 7 ++-
rust/sedona-testing/src/rasters.rs | 41 ++++++++++++++--
4 files changed, 73 insertions(+), 33 deletions(-)
diff --git a/rust/sedona-raster-functions/src/rs_envelope.rs
b/rust/sedona-raster-functions/src/rs_envelope.rs
index 8ecb60cb..7aa1d193 100644
--- a/rust/sedona-raster-functions/src/rs_envelope.rs
+++ b/rust/sedona-raster-functions/src/rs_envelope.rs
@@ -94,25 +94,37 @@ impl SedonaScalarKernel for RsEnvelope {
}
}
-/// Create WKB for a polygon for the raster
+/// Create WKB for the axis-aligned bounding box (envelope) of the raster.
+///
+/// This computes the four corners of the raster in world coordinates, then
+/// derives the min/max X and Y to produce an axis-aligned bounding box.
+/// For skewed/rotated rasters, this differs from the convex hull.
fn create_envelope_wkb(raster: &dyn RasterRef, out: &mut impl std::io::Write)
-> Result<()> {
- // Compute the four corners of the raster in world coordinates.
- // Due to skew/rotation in the affine transformation, each corner must be
- // computed individually.
-
let width = raster.metadata().width() as i64;
let height = raster.metadata().height() as i64;
- // Compute the four corners in pixel coordinates:
- // Upper-left (0, 0), Upper-right (width, 0), Lower-right (width, height),
Lower-left (0, height)
+ // Compute the four corners in world coordinates
let (ulx, uly) = to_world_coordinate(raster, 0, 0);
let (urx, ury) = to_world_coordinate(raster, width, 0);
let (lrx, lry) = to_world_coordinate(raster, width, height);
let (llx, lly) = to_world_coordinate(raster, 0, height);
+ // Compute the axis-aligned bounding box
+ let min_x = ulx.min(urx).min(lrx).min(llx);
+ let max_x = ulx.max(urx).max(lrx).max(llx);
+ let min_y = uly.min(ury).min(lry).min(lly);
+ let max_y = uly.max(ury).max(lry).max(lly);
+
write_wkb_polygon(
out,
- [(ulx, uly), (urx, ury), (lrx, lry), (llx, lly), (ulx,
uly)].into_iter(),
+ [
+ (min_x, min_y),
+ (max_x, min_y),
+ (max_x, max_y),
+ (min_x, max_y),
+ (min_x, min_y),
+ ]
+ .into_iter(),
)
.map_err(|e| DataFusionError::External(e.into()))?;
@@ -143,26 +155,22 @@ mod tests {
let udf = rs_envelope_udf();
let tester = ScalarUdfTester::new(udf.into(), vec![RASTER]);
- let rasters = generate_test_rasters(3, Some(0)).unwrap();
+ // i=0: no skew, i=1: null, i=2: skewed (scale=0.2,-0.4,
skew=0.06,0.08)
+ let rasters = generate_test_rasters(3, Some(1)).unwrap();
- // Corners computed using gdal:
- // Raster 1:
- // Envelope corner coordinates (X, Y):
- // (2.00000000, 3.00000000)
- // (2.20000000, 3.08000000)
- // (2.29000000, 2.48000000)
- // (2.09000000, 2.40000000)
+ // Reference values verified against PostGIS ST_Envelope:
+ //
+ // Raster 0 (i=0): width=1, height=2, ul=(1,2), scale=(0.1,-0.2),
skew=(0,0)
+ // No skew, so envelope = convex hull
//
- // Raster 2:
- // (3.00000000, 4.00000000)
- // (3.60000000, 4.24000000)
- // (3.84000000, 2.64000000)
- // (3.24000000, 2.40000000)
+ // Raster 2 (i=2): width=3, height=4, ul=(3,4), scale=(0.2,-0.4),
skew=(0.06,0.08)
+ // Corners: (3,4), (3.6,4.24), (3.84,2.64), (3.24,2.4)
+ // AABB: x=[3, 3.84], y=[2.4, 4.24]
let expected = &create_array(
&[
+ Some("POLYGON ((1 1.6, 1.1 1.6, 1.1 2, 1 2, 1 1.6))"),
None,
- Some("POLYGON ((2.0 3.0, 2.2 3.08, 2.29 2.48, 2.09 2.4, 2.0
3.0))"),
- Some("POLYGON ((3.0 4.0, 3.6 4.24, 3.84 2.64, 3.24 2.4, 3.0
4.0))"),
+ Some("POLYGON ((3 2.4, 3.84 2.4, 3.84 4.24, 3 4.24, 3 2.4))"),
],
&WKB_GEOMETRY,
);
diff --git a/rust/sedona-raster-functions/src/rs_geotransform.rs
b/rust/sedona-raster-functions/src/rs_geotransform.rs
index 0d98ee2b..a94e9517 100644
--- a/rust/sedona-raster-functions/src/rs_geotransform.rs
+++ b/rust/sedona-raster-functions/src/rs_geotransform.rs
@@ -343,8 +343,8 @@ mod tests {
let rasters = generate_test_rasters(3, Some(1)).unwrap();
let expected_values = match g {
GeoTransformParam::Rotation => vec![Some(-0.0), None,
Some(-0.29145679447786704)],
- GeoTransformParam::ScaleX => vec![Some(0.0), None, Some(0.2)],
- GeoTransformParam::ScaleY => vec![Some(-0.0), None, Some(-0.4)],
+ GeoTransformParam::ScaleX => vec![Some(0.1), None, Some(0.2)],
+ GeoTransformParam::ScaleY => vec![Some(-0.2), None, Some(-0.4)],
GeoTransformParam::SkewX => vec![Some(0.0), None, Some(0.06)],
GeoTransformParam::SkewY => vec![Some(0.0), None, Some(0.08)],
GeoTransformParam::UpperLeftX => vec![Some(1.0), None, Some(3.0)],
diff --git a/rust/sedona-raster-functions/src/rs_rastercoordinate.rs
b/rust/sedona-raster-functions/src/rs_rastercoordinate.rs
index bdfcf48f..b7e74356 100644
--- a/rust/sedona-raster-functions/src/rs_rastercoordinate.rs
+++ b/rust/sedona-raster-functions/src/rs_rastercoordinate.rs
@@ -240,7 +240,7 @@ mod tests {
use sedona_schema::datatypes::{RASTER, WKB_GEOMETRY};
use sedona_testing::compare::assert_array_equal;
use sedona_testing::create::create_array;
- use sedona_testing::rasters::generate_test_rasters;
+ use sedona_testing::rasters::{build_noninvertible_raster,
generate_test_rasters};
use sedona_testing::testers::ScalarUdfTester;
#[test]
@@ -290,8 +290,7 @@ mod tests {
assert_array_equal(&result, &expected);
// Test that we correctly handle non-invertible geotransforms
- // using non-invertible raster 0
- let noninvertible_rasters = generate_test_rasters(2, None).unwrap();
+ let noninvertible_rasters = build_noninvertible_raster();
let result_err =
tester.invoke_array_scalar_scalar(Arc::new(noninvertible_rasters),
2.0_f64, 3.0_f64);
assert!(result_err.is_err());
@@ -323,7 +322,7 @@ mod tests {
assert_array_equal(&result, expected);
// Test that we correctly handle non-invertible geotransforms
- let noninvertible_rasters = generate_test_rasters(2, None).unwrap();
+ let noninvertible_rasters = build_noninvertible_raster();
let result_err =
tester.invoke_array_scalar_scalar(Arc::new(noninvertible_rasters),
2.0_f64, 3.0_f64);
assert!(result_err.is_err());
diff --git a/rust/sedona-testing/src/rasters.rs
b/rust/sedona-testing/src/rasters.rs
index 777ce1ae..16c7d7e1 100644
--- a/rust/sedona-testing/src/rasters.rs
+++ b/rust/sedona-testing/src/rasters.rs
@@ -44,8 +44,8 @@ pub fn generate_test_rasters(
height: i as u64 + 2,
upperleft_x: i as f64 + 1.0,
upperleft_y: i as f64 + 2.0,
- scale_x: i as f64 * 0.1,
- scale_y: i as f64 * -0.2,
+ scale_x: i.max(1) as f64 * 0.1,
+ scale_y: i.max(1) as f64 * -0.2,
skew_x: i as f64 * 0.03,
skew_y: i as f64 * 0.04,
};
@@ -149,6 +149,39 @@ pub fn generate_tiled_rasters(
Ok(raster_builder.finish()?)
}
+/// Builds a 1x1 single-band raster with a non-invertible geotransform (zero
scales and skews).
+/// Useful for testing error handling of inverse affine transforms.
+pub fn build_noninvertible_raster() -> StructArray {
+ let mut builder = RasterBuilder::new(1);
+ let metadata = RasterMetadata {
+ width: 1,
+ height: 1,
+ upperleft_x: 0.0,
+ upperleft_y: 0.0,
+ scale_x: 0.0,
+ scale_y: 0.0,
+ skew_x: 0.0,
+ skew_y: 0.0,
+ };
+ let crs = lnglat().unwrap().to_crs_string();
+ builder
+ .start_raster(&metadata, Some(&crs))
+ .expect("start raster");
+ builder
+ .start_band(BandMetadata {
+ datatype: BandDataType::UInt8,
+ nodata_value: None,
+ storage_type: StorageType::InDb,
+ outdb_url: None,
+ outdb_band_id: None,
+ })
+ .expect("start band");
+ builder.band_data_writer().append_value([0u8]);
+ builder.finish_band().expect("finish band");
+ builder.finish_raster().expect("finish raster");
+ builder.finish().expect("finish")
+}
+
/// Determine if this tile contains a corner of the overall grid and return
its position
/// Returns Some(position) if this tile contains a corner, None otherwise
fn get_corner_position(
@@ -410,8 +443,8 @@ mod tests {
assert_eq!(metadata.height(), i as u64 + 2);
assert_eq!(metadata.upper_left_x(), i as f64 + 1.0);
assert_eq!(metadata.upper_left_y(), i as f64 + 2.0);
- assert_eq!(metadata.scale_x(), (i as f64) * 0.1);
- assert_eq!(metadata.scale_y(), (i as f64) * -0.2);
+ assert_eq!(metadata.scale_x(), (i.max(1) as f64) * 0.1);
+ assert_eq!(metadata.scale_y(), (i.max(1) as f64) * -0.2);
assert_eq!(metadata.skew_x(), (i as f64) * 0.03);
assert_eq!(metadata.skew_y(), (i as f64) * 0.04);