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

paleolimbot 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 45b3162c feat(sedona-raster-functions): Add RS_Rotation (#418)
45b3162c is described below

commit 45b3162c38515f918909f24f8054bf6c05c91201
Author: jp <[email protected]>
AuthorDate: Thu Dec 11 07:19:28 2025 -0800

    feat(sedona-raster-functions): Add RS_Rotation (#418)
---
 Cargo.lock                                         |  1 +
 .../benches/native-raster-functions.rs             |  1 +
 rust/sedona-raster-functions/src/register.rs       |  1 +
 .../sedona-raster-functions/src/rs_geotransform.rs | 39 ++++++++++++++++
 rust/sedona-raster/Cargo.toml                      |  1 +
 rust/sedona-raster/src/affine_transformation.rs    | 53 ++++++++++++++++++++++
 6 files changed, 96 insertions(+)

diff --git a/Cargo.lock b/Cargo.lock
index fba6d35f..bdca5d16 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5150,6 +5150,7 @@ dependencies = [
 name = "sedona-raster"
 version = "0.3.0"
 dependencies = [
+ "approx",
  "arrow-array",
  "arrow-buffer",
  "arrow-schema",
diff --git a/rust/sedona-raster-functions/benches/native-raster-functions.rs 
b/rust/sedona-raster-functions/benches/native-raster-functions.rs
index 4892ae2c..790b1e3d 100644
--- a/rust/sedona-raster-functions/benches/native-raster-functions.rs
+++ b/rust/sedona-raster-functions/benches/native-raster-functions.rs
@@ -41,6 +41,7 @@ fn criterion_benchmark(c: &mut Criterion) {
         "rs_rastertoworldcoordy",
         BenchmarkArgs::ArrayScalarScalar(Raster(64, 64), Int32(0, 63), 
Int32(0, 63)),
     );
+    benchmark::scalar(c, &f, "native-raster", "rs_rotation", Raster(64, 64));
     benchmark::scalar(c, &f, "native-raster", "rs_scalex", Raster(64, 64));
     benchmark::scalar(c, &f, "native-raster", "rs_scaley", Raster(64, 64));
     benchmark::scalar(c, &f, "native-raster", "rs_skewx", Raster(64, 64));
diff --git a/rust/sedona-raster-functions/src/register.rs 
b/rust/sedona-raster-functions/src/register.rs
index e2a21ba6..6db4dee4 100644
--- a/rust/sedona-raster-functions/src/register.rs
+++ b/rust/sedona-raster-functions/src/register.rs
@@ -39,6 +39,7 @@ pub fn default_function_set() -> FunctionSet {
     register_scalar_udfs!(
         function_set,
         crate::rs_example::rs_example_udf,
+        crate::rs_geotransform::rs_rotation_udf,
         crate::rs_geotransform::rs_scalex_udf,
         crate::rs_geotransform::rs_scaley_udf,
         crate::rs_geotransform::rs_skewx_udf,
diff --git a/rust/sedona-raster-functions/src/rs_geotransform.rs 
b/rust/sedona-raster-functions/src/rs_geotransform.rs
index 805d4e32..8d3f7b8f 100644
--- a/rust/sedona-raster-functions/src/rs_geotransform.rs
+++ b/rust/sedona-raster-functions/src/rs_geotransform.rs
@@ -24,6 +24,7 @@ use datafusion_expr::{
     scalar_doc_sections::DOC_SECTION_OTHER, ColumnarValue, Documentation, 
Volatility,
 };
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
+use sedona_raster::affine_transformation::rotation;
 use sedona_raster::traits::RasterRef;
 use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
 
@@ -117,6 +118,21 @@ pub fn rs_skewy_udf() -> SedonaScalarUDF {
     )
 }
 
+/// RS_Rotation() scalar UDF implementation
+///
+/// Calculate the uniform rotation of the raster
+/// in radians based on the skew parameters.
+pub fn rs_rotation_udf() -> SedonaScalarUDF {
+    SedonaScalarUDF::new(
+        "rs_rotation",
+        vec![Arc::new(RsGeoTransform {
+            param: GeoTransformParam::Rotation,
+        })],
+        Volatility::Immutable,
+        Some(rs_rotation_doc()),
+    )
+}
+
 fn rs_upperleftx_doc() -> Documentation {
     Documentation::builder(
         DOC_SECTION_OTHER,
@@ -183,8 +199,20 @@ fn rs_skewy_doc() -> Documentation {
     .build()
 }
 
+fn rs_rotation_doc() -> Documentation {
+    Documentation::builder(
+        DOC_SECTION_OTHER,
+        "Returns the uniform rotation of the raster in radians.".to_string(),
+        "RS_Rotation(raster: Raster)".to_string(),
+    )
+    .with_argument("raster", "Raster: Input raster")
+    .with_sql_example("SELECT RS_Rotation(RS_Example())".to_string())
+    .build()
+}
+
 #[derive(Debug, Clone)]
 enum GeoTransformParam {
+    Rotation,
     ScaleX,
     ScaleY,
     SkewX,
@@ -222,6 +250,10 @@ impl SedonaScalarKernel for RsGeoTransform {
                 Some(raster) => {
                     let metadata = raster.metadata();
                     match self.param {
+                        GeoTransformParam::Rotation => {
+                            let rotation = rotation(&raster);
+                            builder.append_value(rotation);
+                        }
                         GeoTransformParam::ScaleX => 
builder.append_value(metadata.scale_x()),
                         GeoTransformParam::ScaleY => 
builder.append_value(metadata.scale_y()),
                         GeoTransformParam::SkewX => 
builder.append_value(metadata.skew_x()),
@@ -255,6 +287,10 @@ mod tests {
 
     #[test]
     fn udf_info() {
+        let udf: ScalarUDF = rs_rotation_udf().into();
+        assert_eq!(udf.name(), "rs_rotation");
+        assert!(udf.documentation().is_some());
+
         let udf: ScalarUDF = rs_scalex_udf().into();
         assert_eq!(udf.name(), "rs_scalex");
         assert!(udf.documentation().is_some());
@@ -283,6 +319,7 @@ mod tests {
     #[rstest]
     fn udf_invoke(
         #[values(
+            GeoTransformParam::Rotation,
             GeoTransformParam::ScaleX,
             GeoTransformParam::ScaleY,
             GeoTransformParam::SkewX,
@@ -293,6 +330,7 @@ mod tests {
         g: GeoTransformParam,
     ) {
         let udf = match g {
+            GeoTransformParam::Rotation => rs_rotation_udf(),
             GeoTransformParam::ScaleX => rs_scalex_udf(),
             GeoTransformParam::ScaleY => rs_scaley_udf(),
             GeoTransformParam::SkewX => rs_skewx_udf(),
@@ -304,6 +342,7 @@ mod tests {
 
         let rasters = generate_test_rasters(3, Some(1)).unwrap();
         let expected_values = match g {
+            GeoTransformParam::Rotation => vec![Some(-0.0), None, 
Some(-1.2490457723982544)],
             GeoTransformParam::ScaleX => vec![Some(0.0), None, Some(0.2)],
             GeoTransformParam::ScaleY => vec![Some(0.0), None, Some(0.4)],
             GeoTransformParam::SkewX => vec![Some(0.0), None, Some(0.6)],
diff --git a/rust/sedona-raster/Cargo.toml b/rust/sedona-raster/Cargo.toml
index 6a8d8896..f2757739 100644
--- a/rust/sedona-raster/Cargo.toml
+++ b/rust/sedona-raster/Cargo.toml
@@ -35,4 +35,5 @@ sedona-common = { workspace = true }
 sedona-schema = { workspace = true }
 
 [dev-dependencies]
+approx = { workspace = true }
 sedona-testing = { path = "../sedona-testing" }
diff --git a/rust/sedona-raster/src/affine_transformation.rs 
b/rust/sedona-raster/src/affine_transformation.rs
index a5787a94..4152d455 100644
--- a/rust/sedona-raster/src/affine_transformation.rs
+++ b/rust/sedona-raster/src/affine_transformation.rs
@@ -18,6 +18,13 @@
 use crate::traits::RasterRef;
 use arrow_schema::ArrowError;
 
+/// Computes the rotation angle (in radians) of the raster based on its 
geotransform metadata.
+#[inline]
+pub fn rotation(raster: &dyn RasterRef) -> f64 {
+    let metadata = raster.metadata();
+    (-metadata.skew_x()).atan2(metadata.scale_x())
+}
+
 /// Performs an affine transformation on the provided x and y coordinates 
based on the geotransform
 /// data in the raster.
 ///
@@ -75,6 +82,9 @@ pub fn to_raster_coordinate(
 mod tests {
     use super::*;
     use crate::traits::{MetadataRef, RasterMetadata};
+    use approx::assert_relative_eq;
+    use std::f64::consts::FRAC_1_SQRT_2;
+    use std::f64::consts::PI;
 
     struct TestRaster {
         metadata: RasterMetadata,
@@ -92,6 +102,34 @@ mod tests {
         }
     }
 
+    #[test]
+    fn test_rotation() {
+        // 0 degree rotation -> gt[1.0, 0.0, 0.0, -1.0]
+        let raster = rotation_raster(1.0, -1.0, 0.0, 0.0);
+        let rot = rotation(&raster);
+        assert_eq!(rot, 0.0);
+
+        // pi/2 -> gt[0.0, -1.0, 1.0, 0.0]
+        let raster = rotation_raster(0.0, 0.0, -1.0, 1.0);
+        let rot = rotation(&raster);
+        assert_relative_eq!(rot, PI / 2.0, epsilon = 1e-6); // 90 degrees in 
radians
+
+        // pi/4 -> gt[0.70710678, -0.70710678, 0.70710678, 0.70710678]
+        let raster = rotation_raster(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 
-FRAC_1_SQRT_2, FRAC_1_SQRT_2);
+        let rot = rotation(&raster);
+        assert_relative_eq!(rot, PI / 4.0, epsilon = 1e-6); // 45 degrees in 
radians
+
+        // pi/3 -> gt[0.5, -0.866025, 0.866025, 0.5]
+        let raster = rotation_raster(0.5, 0.5, -0.866025, 0.866025);
+        let rot = rotation(&raster);
+        assert_relative_eq!(rot, PI / 3.0, epsilon = 1e-6); // 60 degrees in 
radians
+
+        // pi -> gt[-1.0, 0.0, 0.0, -1.0]
+        let raster = rotation_raster(-1.0, -1.0, 0.0, 0.0);
+        let rot = rotation(&raster);
+        assert_relative_eq!(rot, -PI, epsilon = 1e-6); // 180 degrees in 
radians
+    }
+
     #[test]
     fn test_to_world_coordinate() {
         // Test case with rotation/skew
@@ -177,4 +215,19 @@ mod tests {
             .to_string()
             .contains("determinant is zero."));
     }
+
+    fn rotation_raster(scale_x: f64, scale_y: f64, skew_x: f64, skew_y: f64) 
-> TestRaster {
+        TestRaster {
+            metadata: RasterMetadata {
+                width: 10,
+                height: 20,
+                upperleft_x: 0.0,
+                upperleft_y: 0.0,
+                scale_x,
+                scale_y,
+                skew_x,
+                skew_y,
+            },
+        }
+    }
 }

Reply via email to