Skip to content

Commit 61387a1

Browse files
authored
feat(rust/sedona-raster-functions): Add RS_Envelope (apache#444)
1 parent f93b954 commit 61387a1

File tree

8 files changed

+189
-10
lines changed

8 files changed

+189
-10
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/sedona-raster-functions/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ datafusion-common = { workspace = true }
3838
datafusion-expr = { workspace = true }
3939
sedona-common = { workspace = true }
4040
sedona-expr = { workspace = true }
41+
sedona-geometry = { workspace = true }
4142
sedona-raster = { workspace = true }
4243
sedona-schema = { workspace = true }
4344

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use sedona_testing::benchmark_util::{benchmark, BenchmarkArgSpec::*, BenchmarkAr
1919

2020
fn criterion_benchmark(c: &mut Criterion) {
2121
let f = sedona_raster_functions::register::default_function_set();
22+
benchmark::scalar(c, &f, "native-raster", "rs_envelope", Raster(64, 64));
2223
benchmark::scalar(c, &f, "native-raster", "rs_height", Raster(64, 64));
2324
benchmark::scalar(
2425
c,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
mod executor;
1919
pub mod register;
20+
pub mod rs_envelope;
2021
pub mod rs_example;
2122
pub mod rs_geotransform;
2223
pub mod rs_rastercoordinate;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub fn default_function_set() -> FunctionSet {
3838

3939
register_scalar_udfs!(
4040
function_set,
41+
crate::rs_envelope::rs_envelope_udf,
4142
crate::rs_example::rs_example_udf,
4243
crate::rs_geotransform::rs_rotation_udf,
4344
crate::rs_geotransform::rs_scalex_udf,
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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;
18+
19+
use crate::executor::RasterExecutor;
20+
use arrow_array::builder::BinaryBuilder;
21+
use datafusion_common::DataFusionError;
22+
use datafusion_common::Result;
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_geometry::wkb_factory::write_wkb_polygon;
28+
use sedona_raster::affine_transformation::to_world_coordinate;
29+
use sedona_raster::traits::RasterRef;
30+
use sedona_schema::datatypes::Edges;
31+
use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
32+
33+
/// RS_Envelope() scalar UDF documentation
34+
///
35+
/// Returns the envelope (bounding box) of the given raster as a WKB Polygon.
36+
pub fn rs_envelope_udf() -> SedonaScalarUDF {
37+
SedonaScalarUDF::new(
38+
"rs_envelope",
39+
vec![Arc::new(RsEnvelope {})],
40+
Volatility::Immutable,
41+
Some(rs_envelope_doc()),
42+
)
43+
}
44+
45+
fn rs_envelope_doc() -> Documentation {
46+
Documentation::builder(
47+
DOC_SECTION_OTHER,
48+
"Returns the envelope of the raster as a Geometry.".to_string(),
49+
"RS_Envelope(raster: Raster)".to_string(),
50+
)
51+
.with_argument("raster", "Raster: Input raster")
52+
.with_sql_example("SELECT RS_Envelope(RS_Example())".to_string())
53+
.build()
54+
}
55+
56+
#[derive(Debug)]
57+
struct RsEnvelope {}
58+
59+
impl SedonaScalarKernel for RsEnvelope {
60+
fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
61+
let matcher = ArgMatcher::new(
62+
vec![ArgMatcher::is_raster()],
63+
SedonaType::Wkb(Edges::Planar, None),
64+
);
65+
66+
matcher.match_args(args)
67+
}
68+
69+
fn invoke_batch(
70+
&self,
71+
arg_types: &[SedonaType],
72+
args: &[ColumnarValue],
73+
) -> Result<ColumnarValue> {
74+
let executor = RasterExecutor::new(arg_types, args);
75+
// 1 (byte order) + 4 (type) + 4 (num rings) + 4 (num points) + 80 (5 points * 16 bytes)
76+
let bytes_per_poly = 93;
77+
let mut builder = BinaryBuilder::with_capacity(
78+
executor.num_iterations(),
79+
executor.num_iterations() * bytes_per_poly,
80+
);
81+
82+
executor.execute_raster_void(|_i, raster_opt| {
83+
match raster_opt {
84+
Some(raster) => {
85+
create_envelope_wkb(&raster, &mut builder)?;
86+
builder.append_value([]);
87+
}
88+
None => builder.append_null(),
89+
}
90+
Ok(())
91+
})?;
92+
93+
executor.finish(Arc::new(builder.finish()))
94+
}
95+
}
96+
97+
/// Create WKB for a polygon for the raster
98+
fn create_envelope_wkb(raster: &dyn RasterRef, out: &mut impl std::io::Write) -> Result<()> {
99+
// Compute the four corners of the raster in world coordinates.
100+
// Due to skew/rotation in the affine transformation, each corner must be
101+
// computed individually.
102+
103+
let width = raster.metadata().width() as i64;
104+
let height = raster.metadata().height() as i64;
105+
106+
// Compute the four corners in pixel coordinates:
107+
// Upper-left (0, 0), Upper-right (width, 0), Lower-right (width, height), Lower-left (0, height)
108+
let (ulx, uly) = to_world_coordinate(raster, 0, 0);
109+
let (urx, ury) = to_world_coordinate(raster, width, 0);
110+
let (lrx, lry) = to_world_coordinate(raster, width, height);
111+
let (llx, lly) = to_world_coordinate(raster, 0, height);
112+
113+
write_wkb_polygon(
114+
out,
115+
[(ulx, uly), (urx, ury), (lrx, lry), (llx, lly), (ulx, uly)].into_iter(),
116+
)
117+
.map_err(|e| DataFusionError::External(e.into()))?;
118+
119+
Ok(())
120+
}
121+
122+
#[cfg(test)]
123+
mod tests {
124+
use super::*;
125+
use datafusion_expr::ScalarUDF;
126+
use rstest::rstest;
127+
use sedona_schema::datatypes::RASTER;
128+
use sedona_schema::datatypes::WKB_GEOMETRY;
129+
use sedona_testing::compare::assert_array_equal;
130+
use sedona_testing::create::create_array;
131+
use sedona_testing::rasters::generate_test_rasters;
132+
use sedona_testing::testers::ScalarUdfTester;
133+
134+
#[test]
135+
fn udf_docs() {
136+
let udf: ScalarUDF = rs_envelope_udf().into();
137+
assert_eq!(udf.name(), "rs_envelope");
138+
assert!(udf.documentation().is_some());
139+
}
140+
141+
#[rstest]
142+
fn udf_invoke() {
143+
let udf = rs_envelope_udf();
144+
let tester = ScalarUdfTester::new(udf.into(), vec![RASTER]);
145+
146+
let rasters = generate_test_rasters(3, Some(0)).unwrap();
147+
148+
// Corners computed using gdal:
149+
// Raster 1:
150+
// Envelope corner coordinates (X, Y):
151+
// (2.00000000, 3.00000000)
152+
// (2.20000000, 3.08000000)
153+
// (2.29000000, 2.48000000)
154+
// (2.09000000, 2.40000000)
155+
//
156+
// Raster 2:
157+
// (3.00000000, 4.00000000)
158+
// (3.60000000, 4.24000000)
159+
// (3.84000000, 2.64000000)
160+
// (3.24000000, 2.40000000)
161+
let expected = &create_array(
162+
&[
163+
None,
164+
Some("POLYGON ((2.0 3.0, 2.2 3.08, 2.29 2.48, 2.09 2.4, 2.0 3.0))"),
165+
Some("POLYGON ((3.0 4.0, 3.6 4.24, 3.84 2.64, 3.24 2.4, 3.0 4.0))"),
166+
],
167+
&WKB_GEOMETRY,
168+
);
169+
170+
let result = tester.invoke_array(Arc::new(rasters)).unwrap();
171+
172+
assert_array_equal(&result, expected);
173+
}
174+
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -342,11 +342,11 @@ mod tests {
342342

343343
let rasters = generate_test_rasters(3, Some(1)).unwrap();
344344
let expected_values = match g {
345-
GeoTransformParam::Rotation => vec![Some(-0.0), None, Some(-1.2490457723982544)],
345+
GeoTransformParam::Rotation => vec![Some(-0.0), None, Some(-0.29145679447786704)],
346346
GeoTransformParam::ScaleX => vec![Some(0.0), None, Some(0.2)],
347-
GeoTransformParam::ScaleY => vec![Some(0.0), None, Some(0.4)],
348-
GeoTransformParam::SkewX => vec![Some(0.0), None, Some(0.6)],
349-
GeoTransformParam::SkewY => vec![Some(0.0), None, Some(0.8)],
347+
GeoTransformParam::ScaleY => vec![Some(-0.0), None, Some(-0.4)],
348+
GeoTransformParam::SkewX => vec![Some(0.0), None, Some(0.06)],
349+
GeoTransformParam::SkewY => vec![Some(0.0), None, Some(0.08)],
350350
GeoTransformParam::UpperLeftX => vec![Some(1.0), None, Some(3.0)],
351351
GeoTransformParam::UpperLeftY => vec![Some(2.0), None, Some(4.0)],
352352
};

rust/sedona-testing/src/rasters.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ pub fn generate_test_rasters(
4545
upperleft_x: i as f64 + 1.0,
4646
upperleft_y: i as f64 + 2.0,
4747
scale_x: i as f64 * 0.1,
48-
scale_y: i as f64 * 0.2,
49-
skew_x: i as f64 * 0.3,
50-
skew_y: i as f64 * 0.4,
48+
scale_y: i as f64 * -0.2,
49+
skew_x: i as f64 * 0.03,
50+
skew_y: i as f64 * 0.04,
5151
};
5252
builder.start_raster(&raster_metadata, Some(&crs))?;
5353
builder.start_band(BandMetadata {
@@ -410,9 +410,9 @@ mod tests {
410410
assert_eq!(metadata.upper_left_x(), i as f64 + 1.0);
411411
assert_eq!(metadata.upper_left_y(), i as f64 + 2.0);
412412
assert_eq!(metadata.scale_x(), (i as f64) * 0.1);
413-
assert_eq!(metadata.scale_y(), (i as f64) * 0.2);
414-
assert_eq!(metadata.skew_x(), (i as f64) * 0.3);
415-
assert_eq!(metadata.skew_y(), (i as f64) * 0.4);
413+
assert_eq!(metadata.scale_y(), (i as f64) * -0.2);
414+
assert_eq!(metadata.skew_x(), (i as f64) * 0.03);
415+
assert_eq!(metadata.skew_y(), (i as f64) * 0.04);
416416

417417
let bands = raster.bands();
418418
let band = bands.band(1).unwrap();

0 commit comments

Comments
 (0)