Skip to content

Commit a3dcc12

Browse files
authored
feat(rust/sedona-raster-functions): Add RS_RasterToWorldCoord (#383)
1 parent 56c2081 commit a3dcc12

File tree

7 files changed

+355
-9
lines changed

7 files changed

+355
-9
lines changed

rust/sedona-raster-functions/benches/native-raster-functions.rs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,33 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717
use criterion::{criterion_group, criterion_main, Criterion};
18-
use sedona_testing::benchmark_util::{benchmark, BenchmarkArgSpec::*};
18+
use sedona_testing::benchmark_util::{benchmark, BenchmarkArgSpec::*, BenchmarkArgs};
1919

2020
fn criterion_benchmark(c: &mut Criterion) {
2121
let f = sedona_raster_functions::register::default_function_set();
2222

23-
benchmark::scalar(c, &f, "native", "rs_height", Raster(64, 64));
24-
benchmark::scalar(c, &f, "native", "rs_scalex", Raster(64, 64));
25-
benchmark::scalar(c, &f, "native", "rs_scaley", Raster(64, 64));
26-
benchmark::scalar(c, &f, "native", "rs_skewx", Raster(64, 64));
27-
benchmark::scalar(c, &f, "native", "rs_skewy", Raster(64, 64));
28-
benchmark::scalar(c, &f, "native", "rs_upperleftx", Raster(64, 64));
29-
benchmark::scalar(c, &f, "native", "rs_upperlefty", Raster(64, 64));
30-
benchmark::scalar(c, &f, "native", "rs_width", Raster(64, 64));
23+
benchmark::scalar(c, &f, "native-raster", "rs_height", Raster(64, 64));
24+
benchmark::scalar(
25+
c,
26+
&f,
27+
"native-raster",
28+
"rs_rastertoworldcoordx",
29+
BenchmarkArgs::ArrayScalarScalar(Raster(64, 64), Int32(0, 63), Int32(0, 63)),
30+
);
31+
benchmark::scalar(
32+
c,
33+
&f,
34+
"native-raster",
35+
"rs_rastertoworldcoordy",
36+
BenchmarkArgs::ArrayScalarScalar(Raster(64, 64), Int32(0, 63), Int32(0, 63)),
37+
);
38+
benchmark::scalar(c, &f, "native-raster", "rs_scalex", Raster(64, 64));
39+
benchmark::scalar(c, &f, "native-raster", "rs_scaley", Raster(64, 64));
40+
benchmark::scalar(c, &f, "native-raster", "rs_skewx", Raster(64, 64));
41+
benchmark::scalar(c, &f, "native-raster", "rs_skewy", Raster(64, 64));
42+
benchmark::scalar(c, &f, "native-raster", "rs_upperleftx", Raster(64, 64));
43+
benchmark::scalar(c, &f, "native-raster", "rs_upperlefty", Raster(64, 64));
44+
benchmark::scalar(c, &f, "native-raster", "rs_width", Raster(64, 64));
3145
}
3246

3347
criterion_group!(benches, criterion_benchmark);

rust/sedona-raster-functions/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ pub mod register;
2020
pub mod rs_example;
2121
pub mod rs_geotransform;
2222
pub mod rs_size;
23+
pub mod rs_worldcoordinate;

rust/sedona-raster-functions/src/register.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ pub fn default_function_set() -> FunctionSet {
4747
crate::rs_geotransform::rs_upperlefty_udf,
4848
crate::rs_size::rs_height_udf,
4949
crate::rs_size::rs_width_udf,
50+
crate::rs_worldcoordinate::rs_rastertoworldcoordx_udf,
51+
crate::rs_worldcoordinate::rs_rastertoworldcoordy_udf,
5052
);
5153

5254
register_aggregate_udfs!(function_set,);
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
use std::{sync::Arc, vec};
18+
19+
use crate::executor::RasterExecutor;
20+
use arrow_array::builder::Float64Builder;
21+
use arrow_schema::DataType;
22+
use datafusion_common::{error::Result, exec_err, ScalarValue};
23+
use datafusion_expr::{
24+
scalar_doc_sections::DOC_SECTION_OTHER, ColumnarValue, Documentation, Volatility,
25+
};
26+
use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
27+
use sedona_raster::affine_transformation::to_world_coordinate;
28+
use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
29+
30+
/// RS_RasterToWorldCoordY() scalar UDF implementation
31+
///
32+
/// Converts pixel coordinates to world Y coordinate
33+
pub fn rs_rastertoworldcoordy_udf() -> SedonaScalarUDF {
34+
SedonaScalarUDF::new(
35+
"rs_rastertoworldcoordy",
36+
vec![Arc::new(RsCoordinateMapper { coord: Coord::Y })],
37+
Volatility::Immutable,
38+
Some(rs_rastertoworldcoordy_doc()),
39+
)
40+
}
41+
42+
/// RS_RasterToWorldCoordX() scalar UDF documentation
43+
///
44+
/// Converts pixel coordinates to world X coordinate
45+
pub fn rs_rastertoworldcoordx_udf() -> SedonaScalarUDF {
46+
SedonaScalarUDF::new(
47+
"rs_rastertoworldcoordx",
48+
vec![Arc::new(RsCoordinateMapper { coord: Coord::X })],
49+
Volatility::Immutable,
50+
Some(rs_rastertoworldcoordx_doc()),
51+
)
52+
}
53+
54+
fn rs_rastertoworldcoordy_doc() -> Documentation {
55+
Documentation::builder(
56+
DOC_SECTION_OTHER,
57+
"Returns the upper left Y coordinate of the given row and column of the given raster geometric units of the geo-referenced raster. If any out of bounds values are given, the Y coordinate of the assumed point considering existing raster pixel size and skew values will be returned.".to_string(),
58+
"RS_RasterToWorldCoordY(raster: Raster)".to_string(),
59+
)
60+
.with_argument("raster", "Raster: Input raster")
61+
.with_argument("x", "Integer: Column x into the raster")
62+
.with_argument("y", "Integer: Row y into the raster")
63+
.with_sql_example("SELECT RS_RasterToWorldCoordY(RS_Example(), 0, 0)".to_string())
64+
.build()
65+
}
66+
67+
fn rs_rastertoworldcoordx_doc() -> Documentation {
68+
Documentation::builder(
69+
DOC_SECTION_OTHER,
70+
"Returns the upper left X coordinate of the given row and column of the given raster geometric units of the geo-referenced raster. If any out of bounds values are given, the X coordinate of the assumed point considering existing raster pixel size and skew values will be returned.".to_string(),
71+
"RS_RasterToWorldCoordX(raster: Raster)".to_string(),
72+
)
73+
.with_argument("raster", "Raster: Input raster")
74+
.with_argument("x", "Integer: Column x into the raster")
75+
.with_argument("y", "Integer: Row y into the raster")
76+
.with_sql_example("SELECT RS_RasterToWorldCoordX(RS_Example(), 0, 0)".to_string())
77+
.build()
78+
}
79+
80+
#[derive(Debug, Clone)]
81+
enum Coord {
82+
X,
83+
Y,
84+
}
85+
86+
#[derive(Debug)]
87+
struct RsCoordinateMapper {
88+
coord: Coord,
89+
}
90+
91+
impl SedonaScalarKernel for RsCoordinateMapper {
92+
fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
93+
let matcher = ArgMatcher::new(
94+
vec![
95+
ArgMatcher::is_raster(),
96+
ArgMatcher::is_integer(),
97+
ArgMatcher::is_integer(),
98+
],
99+
SedonaType::Arrow(DataType::Float64),
100+
);
101+
102+
matcher.match_args(args)
103+
}
104+
105+
fn invoke_batch(
106+
&self,
107+
arg_types: &[SedonaType],
108+
args: &[ColumnarValue],
109+
) -> Result<ColumnarValue> {
110+
let executor = RasterExecutor::new(arg_types, args);
111+
let mut builder = Float64Builder::with_capacity(executor.num_iterations());
112+
113+
let (x_opt, y_opt) = get_scalar_coord(&args[1], &args[2])?;
114+
115+
executor.execute_raster_void(|_i, raster_opt| {
116+
match (raster_opt, x_opt, y_opt) {
117+
(Some(raster), Some(x), Some(y)) => {
118+
let (world_x, world_y) = to_world_coordinate(&raster, x, y);
119+
match self.coord {
120+
Coord::X => builder.append_value(world_x),
121+
Coord::Y => builder.append_value(world_y),
122+
};
123+
}
124+
(_, _, _) => builder.append_null(),
125+
}
126+
Ok(())
127+
})?;
128+
129+
executor.finish(Arc::new(builder.finish()))
130+
}
131+
}
132+
133+
fn extract_int_scalar(arg: &ColumnarValue) -> Result<Option<i64>> {
134+
match arg {
135+
ColumnarValue::Scalar(scalar) => {
136+
let i64_val = scalar.cast_to(&DataType::Int64)?;
137+
match i64_val {
138+
ScalarValue::Int64(Some(v)) => Ok(Some(v)),
139+
_ => Ok(None),
140+
}
141+
}
142+
_ => exec_err!("Expected scalar integer argument for coordinate"),
143+
}
144+
}
145+
146+
fn get_scalar_coord(
147+
x_arg: &ColumnarValue,
148+
y_arg: &ColumnarValue,
149+
) -> Result<(Option<i64>, Option<i64>)> {
150+
let x_opt = extract_int_scalar(x_arg)?;
151+
let y_opt = extract_int_scalar(y_arg)?;
152+
Ok((x_opt, y_opt))
153+
}
154+
155+
#[cfg(test)]
156+
mod tests {
157+
use super::*;
158+
use datafusion_expr::ScalarUDF;
159+
use rstest::rstest;
160+
use sedona_schema::datatypes::RASTER;
161+
use sedona_testing::compare::assert_array_equal;
162+
use sedona_testing::rasters::generate_test_rasters;
163+
use sedona_testing::testers::ScalarUdfTester;
164+
165+
#[test]
166+
fn udf_docs() {
167+
let udf: ScalarUDF = rs_rastertoworldcoordy_udf().into();
168+
assert_eq!(udf.name(), "rs_rastertoworldcoordy");
169+
assert!(udf.documentation().is_some());
170+
171+
let udf: ScalarUDF = rs_rastertoworldcoordx_udf().into();
172+
assert_eq!(udf.name(), "rs_rastertoworldcoordx");
173+
assert!(udf.documentation().is_some());
174+
}
175+
176+
#[rstest]
177+
fn udf_invoke(#[values(Coord::Y, Coord::X)] coord: Coord) {
178+
let udf = match coord {
179+
Coord::X => rs_rastertoworldcoordx_udf(),
180+
Coord::Y => rs_rastertoworldcoordy_udf(),
181+
};
182+
let tester = ScalarUdfTester::new(
183+
udf.into(),
184+
vec![
185+
RASTER,
186+
SedonaType::Arrow(DataType::Int32),
187+
SedonaType::Arrow(DataType::Int32),
188+
],
189+
);
190+
191+
let rasters = generate_test_rasters(3, Some(1)).unwrap();
192+
// At 0,0 expect the upper left corner of the test values
193+
let expected_values = match coord {
194+
Coord::X => vec![Some(1.0), None, Some(3.0)],
195+
Coord::Y => vec![Some(2.0), None, Some(4.0)],
196+
};
197+
let expected: Arc<dyn arrow_array::Array> =
198+
Arc::new(arrow_array::Float64Array::from(expected_values));
199+
200+
let result = tester
201+
.invoke_array_scalar_scalar(Arc::new(rasters), 0_i32, 0_i32)
202+
.unwrap();
203+
assert_array_equal(&result, &expected);
204+
}
205+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
use crate::traits::RasterRef;
19+
20+
/// Performs an affine transformation on the provided x and y coordinates based on the geotransform
21+
/// data in the raster.
22+
///
23+
/// # Arguments
24+
/// * `raster` - Reference to the raster containing metadata
25+
/// * `x` - X coordinate in pixel space (column)
26+
/// * `y` - Y coordinate in pixel space (row)
27+
#[inline]
28+
pub fn to_world_coordinate(raster: &dyn RasterRef, x: i64, y: i64) -> (f64, f64) {
29+
let metadata = raster.metadata();
30+
let x_f64 = x as f64;
31+
let y_f64 = y as f64;
32+
33+
let world_x = metadata.upper_left_x() + x_f64 * metadata.scale_x() + y_f64 * metadata.skew_x();
34+
let world_y = metadata.upper_left_y() + x_f64 * metadata.skew_y() + y_f64 * metadata.scale_y();
35+
36+
(world_x, world_y)
37+
}
38+
39+
#[cfg(test)]
40+
mod tests {
41+
use super::*;
42+
use crate::traits::{MetadataRef, RasterMetadata};
43+
44+
struct TestRaster {
45+
metadata: RasterMetadata,
46+
}
47+
48+
impl RasterRef for TestRaster {
49+
fn metadata(&self) -> &dyn MetadataRef {
50+
&self.metadata
51+
}
52+
fn crs(&self) -> Option<&str> {
53+
None
54+
}
55+
fn bands(&self) -> &dyn crate::traits::BandsRef {
56+
unimplemented!()
57+
}
58+
}
59+
60+
#[test]
61+
fn test_to_world_coordinate_basic() {
62+
// Test case with rotation/skew
63+
let raster = TestRaster {
64+
metadata: RasterMetadata {
65+
width: 10,
66+
height: 20,
67+
upperleft_x: 100.0,
68+
upperleft_y: 200.0,
69+
scale_x: 1.0,
70+
scale_y: -2.0,
71+
skew_x: 0.25,
72+
skew_y: 0.5,
73+
},
74+
};
75+
76+
let (wx, wy) = to_world_coordinate(&raster, 0, 0);
77+
assert_eq!((wx, wy), (100.0, 200.0));
78+
79+
let (wx, wy) = to_world_coordinate(&raster, 5, 10);
80+
assert_eq!((wx, wy), (107.5, 182.5));
81+
82+
let (wx, wy) = to_world_coordinate(&raster, 9, 19);
83+
assert_eq!((wx, wy), (113.75, 166.5));
84+
85+
let (wx, wy) = to_world_coordinate(&raster, 1, 0);
86+
assert_eq!((wx, wy), (101.0, 200.5));
87+
88+
let (wx, wy) = to_world_coordinate(&raster, 0, 1);
89+
assert_eq!((wx, wy), (100.25, 198.0));
90+
}
91+
}

rust/sedona-raster/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18+
pub mod affine_transformation;
1819
pub mod array;
1920
pub mod builder;
2021
pub mod traits;

0 commit comments

Comments
 (0)