Skip to main content

sedona_testing/
rasters.rs

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.
17use arrow_array::StructArray;
18use datafusion_common::Result;
19use fastrand::Rng;
20use sedona_raster::array::RasterStructArray;
21use sedona_raster::builder::RasterBuilder;
22use sedona_raster::traits::{BandMetadata, RasterMetadata, RasterRef};
23use sedona_schema::crs::lnglat;
24use sedona_schema::raster::{BandDataType, StorageType};
25
26/// Generate a StructArray of rasters with sequentially increasing dimensions and pixel values
27/// These tiny rasters are to provide fast, easy and predictable test data for unit tests.
28pub fn generate_test_rasters(
29    count: usize,
30    null_raster_index: Option<usize>,
31) -> Result<StructArray> {
32    let mut builder = RasterBuilder::new(count);
33    let crs = lnglat().unwrap().to_crs_string();
34    for i in 0..count {
35        // If a null raster index is specified and that matches the current index,
36        // append a null raster
37        if matches!(null_raster_index, Some(index) if index == i) {
38            builder.append_null()?;
39            continue;
40        }
41
42        let raster_metadata = RasterMetadata {
43            width: i as u64 + 1,
44            height: i as u64 + 2,
45            upperleft_x: i as f64 + 1.0,
46            upperleft_y: i as f64 + 2.0,
47            scale_x: i.max(1) as f64 * 0.1,
48            scale_y: i.max(1) as f64 * -0.2,
49            skew_x: i as f64 * 0.03,
50            skew_y: i as f64 * 0.04,
51        };
52        builder.start_raster(&raster_metadata, Some(&crs))?;
53        builder.start_band(BandMetadata {
54            datatype: BandDataType::UInt16,
55            nodata_value: Some(vec![0u8; 2]),
56            storage_type: StorageType::InDb,
57            outdb_url: None,
58            outdb_band_id: None,
59        })?;
60
61        let pixel_count = (i + 1) * (i + 2); // width * height
62        let mut band_data = Vec::with_capacity(pixel_count * 2); // 2 bytes per u16
63        for pixel_value in 0..pixel_count as u16 {
64            band_data.extend_from_slice(&pixel_value.to_le_bytes());
65        }
66
67        builder.band_data_writer().append_value(&band_data);
68        builder.finish_band()?;
69        builder.finish_raster()?;
70    }
71
72    Ok(builder.finish()?)
73}
74
75/// Generates a set of tiled rasters arranged in a grid
76/// - Each raster tile has specified dimensions and random pixel values
77/// - Each raster has 3 bands which can be interpreted as RGB values
78///   and the result can be visualized as a mosaic of tiles.
79/// - There are nodata values at the 4 corners of the overall mosaic.
80pub fn generate_tiled_rasters(
81    tile_size: (usize, usize),
82    number_of_tiles: (usize, usize),
83    data_type: BandDataType,
84    seed: Option<u64>,
85) -> Result<StructArray> {
86    let mut rng = match seed {
87        Some(s) => Rng::with_seed(s),
88        None => Rng::new(),
89    };
90    let (tile_width, tile_height) = tile_size;
91    let (x_tiles, y_tiles) = number_of_tiles;
92    let mut raster_builder = RasterBuilder::new(x_tiles * y_tiles);
93    let band_count = 3;
94    let crs = lnglat().unwrap().to_crs_string();
95
96    for tile_y in 0..y_tiles {
97        for tile_x in 0..x_tiles {
98            let origin_x = (tile_x * tile_width) as f64;
99            let origin_y = (tile_y * tile_height) as f64;
100
101            let raster_metadata = RasterMetadata {
102                width: tile_width as u64,
103                height: tile_height as u64,
104                upperleft_x: origin_x,
105                upperleft_y: origin_y,
106                scale_x: 1.0,
107                scale_y: 1.0,
108                skew_x: 0.0,
109                skew_y: 0.0,
110            };
111
112            raster_builder.start_raster(&raster_metadata, Some(&crs))?;
113
114            for _ in 0..band_count {
115                // Set a nodata value appropriate for the data type
116                let nodata_value = get_nodata_value_for_type(&data_type);
117
118                let nodata_value_bytes = nodata_value.clone();
119
120                let band_metadata = BandMetadata {
121                    nodata_value,
122                    storage_type: StorageType::InDb,
123                    datatype: data_type,
124                    outdb_url: None,
125                    outdb_band_id: None,
126                };
127
128                raster_builder.start_band(band_metadata)?;
129
130                let pixel_count = tile_width * tile_height;
131
132                // Determine which corner position (if any) should have nodata in this tile
133                let corner_position =
134                    get_corner_position(tile_x, tile_y, x_tiles, y_tiles, tile_width, tile_height);
135                let band_data = generate_random_band_data(
136                    pixel_count,
137                    &data_type,
138                    nodata_value_bytes.as_deref(),
139                    corner_position,
140                    &mut rng,
141                );
142
143                raster_builder.band_data_writer().append_value(&band_data);
144                raster_builder.finish_band()?;
145            }
146
147            raster_builder.finish_raster()?;
148        }
149    }
150
151    Ok(raster_builder.finish()?)
152}
153
154/// Builds a 1x1 single-band raster with a non-invertible geotransform (zero scales and skews).
155/// Useful for testing error handling of inverse affine transforms.
156pub fn build_noninvertible_raster() -> StructArray {
157    let mut builder = RasterBuilder::new(1);
158    let metadata = RasterMetadata {
159        width: 1,
160        height: 1,
161        upperleft_x: 0.0,
162        upperleft_y: 0.0,
163        scale_x: 0.0,
164        scale_y: 0.0,
165        skew_x: 0.0,
166        skew_y: 0.0,
167    };
168    let crs = lnglat().unwrap().to_crs_string();
169    builder
170        .start_raster(&metadata, Some(&crs))
171        .expect("start raster");
172    builder
173        .start_band(BandMetadata {
174            datatype: BandDataType::UInt8,
175            nodata_value: None,
176            storage_type: StorageType::InDb,
177            outdb_url: None,
178            outdb_band_id: None,
179        })
180        .expect("start band");
181    builder.band_data_writer().append_value([0u8]);
182    builder.finish_band().expect("finish band");
183    builder.finish_raster().expect("finish raster");
184    builder.finish().expect("finish")
185}
186
187/// Determine if this tile contains a corner of the overall grid and return its position
188/// Returns Some(position) if this tile contains a corner, None otherwise
189fn get_corner_position(
190    tile_x: usize,
191    tile_y: usize,
192    x_tiles: usize,
193    y_tiles: usize,
194    tile_width: usize,
195    tile_height: usize,
196) -> Option<usize> {
197    // Top-left corner (tile 0,0, pixel 0)
198    if tile_x == 0 && tile_y == 0 {
199        return Some(0);
200    }
201    // Top-right corner (tile x_tiles-1, 0, pixel tile_width-1)
202    if tile_x == x_tiles - 1 && tile_y == 0 {
203        return Some(tile_width - 1);
204    }
205    // Bottom-left corner (tile 0, y_tiles-1, pixel (tile_height-1)*tile_width)
206    if tile_x == 0 && tile_y == y_tiles - 1 {
207        return Some((tile_height - 1) * tile_width);
208    }
209    // Bottom-right corner (tile x_tiles-1, y_tiles-1, pixel tile_height*tile_width-1)
210    if tile_x == x_tiles - 1 && tile_y == y_tiles - 1 {
211        return Some(tile_height * tile_width - 1);
212    }
213    None
214}
215
216fn generate_random_band_data(
217    pixel_count: usize,
218    data_type: &BandDataType,
219    nodata_bytes: Option<&[u8]>,
220    corner_position: Option<usize>,
221    rng: &mut Rng,
222) -> Vec<u8> {
223    /// Generate random band data for a given pixel type and set the corner pixel
224    /// to the nodata value if applicable.
225    macro_rules! gen_band {
226        ($byte_size:expr, $rng_expr:expr) => {{
227            let byte_size: usize = $byte_size;
228            let mut data = Vec::with_capacity(pixel_count * byte_size);
229            for _ in 0..pixel_count {
230                data.extend_from_slice(&$rng_expr.to_ne_bytes());
231            }
232            if let (Some(nodata), Some(pos)) = (nodata_bytes, corner_position) {
233                if nodata.len() >= byte_size && pos * byte_size + byte_size <= data.len() {
234                    data[pos * byte_size..(pos * byte_size) + byte_size]
235                        .copy_from_slice(&nodata[0..byte_size]);
236                }
237            }
238            data
239        }};
240    }
241
242    match data_type {
243        BandDataType::UInt8 => gen_band!(1, rng.u8(..)),
244        BandDataType::Int8 => gen_band!(1, rng.i8(..)),
245        BandDataType::UInt16 => gen_band!(2, rng.u16(..)),
246        BandDataType::Int16 => gen_band!(2, rng.i16(..)),
247        BandDataType::UInt32 => gen_band!(4, rng.u32(..)),
248        BandDataType::Int32 => gen_band!(4, rng.i32(..)),
249        BandDataType::UInt64 => gen_band!(8, rng.u64(..)),
250        BandDataType::Int64 => gen_band!(8, rng.i64(..)),
251        BandDataType::Float32 => gen_band!(4, rng.f32()),
252        BandDataType::Float64 => gen_band!(8, rng.f64()),
253    }
254}
255
256fn get_nodata_value_for_type(data_type: &BandDataType) -> Option<Vec<u8>> {
257    match data_type {
258        BandDataType::UInt8 => Some(vec![255u8]),
259        BandDataType::Int8 => Some(i8::MIN.to_ne_bytes().to_vec()),
260        BandDataType::UInt16 => Some(u16::MAX.to_ne_bytes().to_vec()),
261        BandDataType::Int16 => Some(i16::MIN.to_ne_bytes().to_vec()),
262        BandDataType::UInt32 => Some(u32::MAX.to_ne_bytes().to_vec()),
263        BandDataType::Int32 => Some(i32::MIN.to_ne_bytes().to_vec()),
264        BandDataType::UInt64 => Some(u64::MAX.to_ne_bytes().to_vec()),
265        BandDataType::Int64 => Some(i64::MIN.to_ne_bytes().to_vec()),
266        BandDataType::Float32 => Some(f32::NAN.to_ne_bytes().to_vec()),
267        BandDataType::Float64 => Some(f64::NAN.to_ne_bytes().to_vec()),
268    }
269}
270
271/// Compare two RasterStructArrays for equality
272pub fn assert_raster_arrays_equal(
273    raster_array1: &RasterStructArray,
274    raster_array2: &RasterStructArray,
275) {
276    assert_eq!(
277        raster_array1.len(),
278        raster_array2.len(),
279        "Raster array lengths do not match"
280    );
281
282    for i in 0..raster_array1.len() {
283        let raster1 = raster_array1.get(i).unwrap();
284        let raster2 = raster_array2.get(i).unwrap();
285        assert_raster_equal(&raster1, &raster2);
286    }
287}
288
289/// Compare two rasters for equality
290pub fn assert_raster_equal(raster1: &impl RasterRef, raster2: &impl RasterRef) {
291    // Compare metadata
292    let meta1 = raster1.metadata();
293    let meta2 = raster2.metadata();
294    assert_eq!(meta1.width(), meta2.width(), "Raster widths do not match");
295    assert_eq!(
296        meta1.height(),
297        meta2.height(),
298        "Raster heights do not match"
299    );
300    assert_eq!(
301        meta1.upper_left_x(),
302        meta2.upper_left_x(),
303        "Raster upper left x does not match"
304    );
305    assert_eq!(
306        meta1.upper_left_y(),
307        meta2.upper_left_y(),
308        "Raster upper left y does not match"
309    );
310    assert_eq!(
311        meta1.scale_x(),
312        meta2.scale_x(),
313        "Raster scale x does not match"
314    );
315    assert_eq!(
316        meta1.scale_y(),
317        meta2.scale_y(),
318        "Raster scale y does not match"
319    );
320    assert_eq!(
321        meta1.skew_x(),
322        meta2.skew_x(),
323        "Raster skew x does not match"
324    );
325    assert_eq!(
326        meta1.skew_y(),
327        meta2.skew_y(),
328        "Raster skew y does not match"
329    );
330
331    // Compare bands
332    let bands1 = raster1.bands();
333    let bands2 = raster2.bands();
334    assert_eq!(bands1.len(), bands2.len(), "Number of bands do not match");
335
336    for band_index in 0..bands1.len() {
337        let band1 = bands1.band(band_index + 1).unwrap();
338        let band2 = bands2.band(band_index + 1).unwrap();
339
340        let band_meta1 = band1.metadata();
341        let band_meta2 = band2.metadata();
342        assert_eq!(
343            band_meta1.data_type().unwrap(),
344            band_meta2.data_type().unwrap(),
345            "Band data types do not match"
346        );
347        assert_eq!(
348            band_meta1.nodata_value(),
349            band_meta2.nodata_value(),
350            "Band nodata values do not match"
351        );
352        assert_eq!(
353            band_meta1.storage_type().unwrap(),
354            band_meta2.storage_type().unwrap(),
355            "Band storage types do not match"
356        );
357        assert_eq!(
358            band_meta1.outdb_url(),
359            band_meta2.outdb_url(),
360            "Band outdb URLs do not match"
361        );
362        assert_eq!(
363            band_meta1.outdb_band_id(),
364            band_meta2.outdb_band_id(),
365            "Band outdb band IDs do not match"
366        );
367
368        assert_eq!(band1.data(), band2.data(), "Band data does not match");
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375    use sedona_raster::array::RasterStructArray;
376    use sedona_raster::traits::RasterRef;
377
378    #[test]
379    fn test_generate_test_rasters() {
380        let count = 5;
381        let struct_array = generate_test_rasters(count, None).unwrap();
382        let raster_array = RasterStructArray::new(&struct_array);
383        assert_eq!(raster_array.len(), count);
384
385        for i in 0..count {
386            let raster = raster_array.get(i).unwrap();
387            let metadata = raster.metadata();
388            assert_eq!(metadata.width(), i as u64 + 1);
389            assert_eq!(metadata.height(), i as u64 + 2);
390            assert_eq!(metadata.upper_left_x(), i as f64 + 1.0);
391            assert_eq!(metadata.upper_left_y(), i as f64 + 2.0);
392            assert_eq!(metadata.scale_x(), (i.max(1) as f64) * 0.1);
393            assert_eq!(metadata.scale_y(), (i.max(1) as f64) * -0.2);
394            assert_eq!(metadata.skew_x(), (i as f64) * 0.03);
395            assert_eq!(metadata.skew_y(), (i as f64) * 0.04);
396
397            let bands = raster.bands();
398            let band = bands.band(1).unwrap();
399            let band_metadata = band.metadata();
400            assert_eq!(band_metadata.data_type().unwrap(), BandDataType::UInt16);
401            assert_eq!(band_metadata.nodata_value(), Some(&[0u8, 0u8][..]));
402            assert_eq!(band_metadata.storage_type().unwrap(), StorageType::InDb);
403            assert_eq!(band_metadata.outdb_url(), None);
404            assert_eq!(band_metadata.outdb_band_id(), None);
405
406            let band_data = band.data();
407            let expected_pixel_count = (i + 1) * (i + 2); // width * height
408
409            // Convert raw bytes back to u16 values for comparison
410            let mut actual_pixel_values = Vec::new();
411            for chunk in band_data.chunks_exact(2) {
412                let value = u16::from_le_bytes([chunk[0], chunk[1]]);
413                actual_pixel_values.push(value);
414            }
415            let expected_pixel_values: Vec<u16> = (0..expected_pixel_count as u16).collect();
416            assert_eq!(actual_pixel_values, expected_pixel_values);
417        }
418    }
419
420    #[test]
421    fn test_generate_tiled_rasters() {
422        let tile_size = (64, 64);
423        let number_of_tiles = (4, 4);
424        let data_type = BandDataType::UInt8;
425        let struct_array =
426            generate_tiled_rasters(tile_size, number_of_tiles, data_type, Some(43)).unwrap();
427        let raster_array = RasterStructArray::new(&struct_array);
428        assert_eq!(raster_array.len(), 16); // 4x4 tiles
429        for i in 0..16 {
430            let raster = raster_array.get(i).unwrap();
431            let metadata = raster.metadata();
432            assert_eq!(metadata.width(), 64);
433            assert_eq!(metadata.height(), 64);
434            assert_eq!(metadata.upper_left_x(), ((i % 4) * 64) as f64);
435            assert_eq!(metadata.upper_left_y(), ((i / 4) * 64) as f64);
436            let bands = raster.bands();
437            assert_eq!(bands.len(), 3);
438            for band_index in 0..3 {
439                let band = bands.band(band_index + 1).unwrap();
440                let band_metadata = band.metadata();
441                assert_eq!(band_metadata.data_type().unwrap(), BandDataType::UInt8);
442                assert_eq!(band_metadata.storage_type().unwrap(), StorageType::InDb);
443                let band_data = band.data();
444                assert_eq!(band_data.len(), 64 * 64); // 4096 pixels
445            }
446        }
447    }
448
449    #[test]
450    fn test_raster_arrays_equal() {
451        let raster_array1 = generate_test_rasters(3, None).unwrap();
452        let raster_struct_array1 = RasterStructArray::new(&raster_array1);
453        // Test that identical arrays are equal
454        assert_raster_arrays_equal(&raster_struct_array1, &raster_struct_array1);
455    }
456
457    #[test]
458    #[should_panic = "Raster array lengths do not match"]
459    fn test_raster_arrays_not_equal() {
460        let raster_array1 = generate_test_rasters(3, None).unwrap();
461        let raster_struct_array1 = RasterStructArray::new(&raster_array1);
462
463        // Test that arrays with different lengths are not equal
464        let raster_array2 = generate_test_rasters(4, None).unwrap();
465        let raster_struct_array2 = RasterStructArray::new(&raster_array2);
466        assert_raster_arrays_equal(&raster_struct_array1, &raster_struct_array2);
467    }
468
469    #[test]
470    fn test_raster_equal() {
471        let raster_array1 =
472            generate_tiled_rasters((256, 256), (1, 1), BandDataType::UInt8, Some(43)).unwrap();
473        let raster1 = RasterStructArray::new(&raster_array1).get(0).unwrap();
474
475        // Assert that the rasters are equal to themselves
476        assert_raster_equal(&raster1, &raster1);
477    }
478
479    #[test]
480    #[should_panic = "Band data does not match"]
481    fn test_raster_different_band_data() {
482        let raster_array1 =
483            generate_tiled_rasters((128, 128), (1, 1), BandDataType::UInt8, Some(43)).unwrap();
484        let raster_array2 =
485            generate_tiled_rasters((128, 128), (1, 1), BandDataType::UInt8, Some(47)).unwrap();
486
487        let raster1 = RasterStructArray::new(&raster_array1).get(0).unwrap();
488        let raster2 = RasterStructArray::new(&raster_array2).get(0).unwrap();
489        assert_raster_equal(&raster1, &raster2);
490    }
491
492    #[test]
493    #[should_panic = "Raster upper left x does not match"]
494    fn test_raster_different_metadata() {
495        let raster_array =
496            generate_tiled_rasters((128, 128), (2, 1), BandDataType::UInt8, Some(43)).unwrap();
497        let raster1 = RasterStructArray::new(&raster_array).get(0).unwrap();
498        let raster2 = RasterStructArray::new(&raster_array).get(1).unwrap();
499        assert_raster_equal(&raster1, &raster2);
500    }
501}