blob: 69d83ca1553ccd093ad7594c171d1f15e51849a6 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use arrow_array::StructArray;
use datafusion_common::Result;
use fastrand::Rng;
use sedona_raster::array::RasterStructArray;
use sedona_raster::builder::RasterBuilder;
use sedona_raster::traits::{BandMetadata, RasterMetadata, RasterRef};
use sedona_schema::raster::{BandDataType, StorageType};
/// Generate a StructArray of rasters with sequentially increasing dimensions and pixel values
/// These tiny rasters are to provide fast, easy and predictable test data for unit tests.
pub fn generate_test_rasters(
count: usize,
null_raster_index: Option<usize>,
) -> Result<StructArray> {
let mut builder = RasterBuilder::new(count);
for i in 0..count {
// If a null raster index is specified and that matches the current index,
// append a null raster
if matches!(null_raster_index, Some(index) if index == i) {
builder.append_null()?;
continue;
}
let raster_metadata = RasterMetadata {
width: i as u64 + 1,
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,
skew_x: i as f64 * 0.3,
skew_y: i as f64 * 0.4,
};
builder.start_raster(&raster_metadata, None)?;
builder.start_band(BandMetadata {
datatype: BandDataType::UInt16,
nodata_value: Some(vec![0u8; 2]),
storage_type: StorageType::InDb,
outdb_url: None,
outdb_band_id: None,
})?;
let pixel_count = (i + 1) * (i + 2); // width * height
let mut band_data = Vec::with_capacity(pixel_count * 2); // 2 bytes per u16
for pixel_value in 0..pixel_count as u16 {
band_data.extend_from_slice(&pixel_value.to_le_bytes());
}
builder.band_data_writer().append_value(&band_data);
builder.finish_band()?;
builder.finish_raster()?;
}
Ok(builder.finish()?)
}
/// Generates a set of tiled rasters arranged in a grid
/// - Each raster tile has specified dimensions and random pixel values
/// - Each raster has 3 bands which can be interpreted as RGB values
/// and the result can be visualized as a mosaic of tiles.
/// - There are nodata values at the 4 corners of the overall mosaic.
pub fn generate_tiled_rasters(
tile_size: (usize, usize),
number_of_tiles: (usize, usize),
data_type: BandDataType,
seed: Option<u64>,
) -> Result<StructArray> {
let mut rng = match seed {
Some(s) => Rng::with_seed(s),
None => Rng::new(),
};
let (tile_width, tile_height) = tile_size;
let (x_tiles, y_tiles) = number_of_tiles;
let mut raster_builder = RasterBuilder::new(x_tiles * y_tiles);
let band_count = 3;
for tile_y in 0..y_tiles {
for tile_x in 0..x_tiles {
let origin_x = (tile_x * tile_width) as f64;
let origin_y = (tile_y * tile_height) as f64;
let raster_metadata = RasterMetadata {
width: tile_width as u64,
height: tile_height as u64,
upperleft_x: origin_x,
upperleft_y: origin_y,
scale_x: 1.0,
scale_y: 1.0,
skew_x: 0.0,
skew_y: 0.0,
};
raster_builder.start_raster(&raster_metadata, None)?;
for _ in 0..band_count {
// Set a nodata value appropriate for the data type
let nodata_value = get_nodata_value_for_type(&data_type);
let band_metadata = BandMetadata {
nodata_value: nodata_value.clone(),
storage_type: StorageType::InDb,
datatype: data_type.clone(),
outdb_url: None,
outdb_band_id: None,
};
raster_builder.start_band(band_metadata)?;
let pixel_count = tile_width * tile_height;
// Determine which corner position (if any) should have nodata in this tile
let corner_position =
get_corner_position(tile_x, tile_y, x_tiles, y_tiles, tile_width, tile_height);
let band_data = generate_random_band_data(
pixel_count,
&data_type,
nodata_value.as_deref(),
corner_position,
&mut rng,
);
raster_builder.band_data_writer().append_value(&band_data);
raster_builder.finish_band()?;
}
raster_builder.finish_raster()?;
}
}
Ok(raster_builder.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(
tile_x: usize,
tile_y: usize,
x_tiles: usize,
y_tiles: usize,
tile_width: usize,
tile_height: usize,
) -> Option<usize> {
// Top-left corner (tile 0,0, pixel 0)
if tile_x == 0 && tile_y == 0 {
return Some(0);
}
// Top-right corner (tile x_tiles-1, 0, pixel tile_width-1)
if tile_x == x_tiles - 1 && tile_y == 0 {
return Some(tile_width - 1);
}
// Bottom-left corner (tile 0, y_tiles-1, pixel (tile_height-1)*tile_width)
if tile_x == 0 && tile_y == y_tiles - 1 {
return Some((tile_height - 1) * tile_width);
}
// Bottom-right corner (tile x_tiles-1, y_tiles-1, pixel tile_height*tile_width-1)
if tile_x == x_tiles - 1 && tile_y == y_tiles - 1 {
return Some(tile_height * tile_width - 1);
}
None
}
fn generate_random_band_data(
pixel_count: usize,
data_type: &BandDataType,
nodata_bytes: Option<&[u8]>,
corner_position: Option<usize>,
rng: &mut Rng,
) -> Vec<u8> {
match data_type {
BandDataType::UInt8 => {
let mut data: Vec<u8> = (0..pixel_count).map(|_| rng.u8(..)).collect();
// Set corner pixel to nodata value if this tile contains a corner
if let (Some(nodata), Some(pos)) = (nodata_bytes, corner_position) {
if !nodata.is_empty() && pos < data.len() {
data[pos] = nodata[0];
}
}
data
}
BandDataType::UInt16 => {
let mut data = Vec::with_capacity(pixel_count * 2);
for _ in 0..pixel_count {
data.extend_from_slice(&rng.u16(..).to_ne_bytes());
}
// Set corner pixel to nodata value if this tile contains a corner
if let (Some(nodata), Some(pos)) = (nodata_bytes, corner_position) {
if nodata.len() >= 2 && pos * 2 + 2 <= data.len() {
data[pos * 2..(pos * 2) + 2].copy_from_slice(&nodata[0..2]);
}
}
data
}
BandDataType::Int16 => {
let mut data = Vec::with_capacity(pixel_count * 2);
for _ in 0..pixel_count {
data.extend_from_slice(&rng.i16(..).to_ne_bytes());
}
// Set corner pixel to nodata value if this tile contains a corner
if let (Some(nodata), Some(pos)) = (nodata_bytes, corner_position) {
if nodata.len() >= 2 && pos * 2 + 2 <= data.len() {
data[pos * 2..(pos * 2) + 2].copy_from_slice(&nodata[0..2]);
}
}
data
}
BandDataType::UInt32 => {
let mut data = Vec::with_capacity(pixel_count * 4);
for _ in 0..pixel_count {
data.extend_from_slice(&rng.u32(..).to_ne_bytes());
}
// Set corner pixel to nodata value if this tile contains a corner
if let (Some(nodata), Some(pos)) = (nodata_bytes, corner_position) {
if nodata.len() >= 4 && pos * 4 + 4 <= data.len() {
data[pos * 4..(pos * 4) + 4].copy_from_slice(&nodata[0..4]);
}
}
data
}
BandDataType::Int32 => {
let mut data = Vec::with_capacity(pixel_count * 4);
for _ in 0..pixel_count {
data.extend_from_slice(&rng.i32(..).to_ne_bytes());
}
// Set corner pixel to nodata value if this tile contains a corner
if let (Some(nodata), Some(pos)) = (nodata_bytes, corner_position) {
if nodata.len() >= 4 && pos * 4 + 4 <= data.len() {
data[pos * 4..(pos * 4) + 4].copy_from_slice(&nodata[0..4]);
}
}
data
}
BandDataType::Float32 => {
let mut data = Vec::with_capacity(pixel_count * 4);
for _ in 0..pixel_count {
data.extend_from_slice(&rng.f32().to_ne_bytes());
}
// Set corner pixel to nodata value if this tile contains a corner
if let (Some(nodata), Some(pos)) = (nodata_bytes, corner_position) {
if nodata.len() >= 4 && pos * 4 + 4 <= data.len() {
data[pos * 4..(pos * 4) + 4].copy_from_slice(&nodata[0..4]);
}
}
data
}
BandDataType::Float64 => {
let mut data = Vec::with_capacity(pixel_count * 8);
for _ in 0..pixel_count {
data.extend_from_slice(&rng.f64().to_ne_bytes());
}
// Set corner pixel to nodata value if this tile contains a corner
if let (Some(nodata), Some(pos)) = (nodata_bytes, corner_position) {
if nodata.len() >= 8 && pos * 8 + 8 <= data.len() {
data[pos * 8..(pos * 8) + 8].copy_from_slice(&nodata[0..8]);
}
}
data
}
}
}
fn get_nodata_value_for_type(data_type: &BandDataType) -> Option<Vec<u8>> {
match data_type {
BandDataType::UInt8 => Some(vec![255u8]),
BandDataType::UInt16 => Some(u16::MAX.to_ne_bytes().to_vec()),
BandDataType::Int16 => Some(i16::MIN.to_ne_bytes().to_vec()),
BandDataType::UInt32 => Some(u32::MAX.to_ne_bytes().to_vec()),
BandDataType::Int32 => Some(i32::MIN.to_ne_bytes().to_vec()),
BandDataType::Float32 => Some(f32::NAN.to_ne_bytes().to_vec()),
BandDataType::Float64 => Some(f64::NAN.to_ne_bytes().to_vec()),
}
}
/// Compare two RasterStructArrays for equality
pub fn assert_raster_arrays_equal(
raster_array1: &RasterStructArray,
raster_array2: &RasterStructArray,
) {
assert_eq!(
raster_array1.len(),
raster_array2.len(),
"Raster array lengths do not match"
);
for i in 0..raster_array1.len() {
let raster1 = raster_array1.get(i).unwrap();
let raster2 = raster_array2.get(i).unwrap();
assert_raster_equal(&raster1, &raster2);
}
}
/// Compare two rasters for equality
pub fn assert_raster_equal(raster1: &impl RasterRef, raster2: &impl RasterRef) {
// Compare metadata
let meta1 = raster1.metadata();
let meta2 = raster2.metadata();
assert_eq!(meta1.width(), meta2.width(), "Raster widths do not match");
assert_eq!(
meta1.height(),
meta2.height(),
"Raster heights do not match"
);
assert_eq!(
meta1.upper_left_x(),
meta2.upper_left_x(),
"Raster upper left x does not match"
);
assert_eq!(
meta1.upper_left_y(),
meta2.upper_left_y(),
"Raster upper left y does not match"
);
assert_eq!(
meta1.scale_x(),
meta2.scale_x(),
"Raster scale x does not match"
);
assert_eq!(
meta1.scale_y(),
meta2.scale_y(),
"Raster scale y does not match"
);
assert_eq!(
meta1.skew_x(),
meta2.skew_x(),
"Raster skew x does not match"
);
assert_eq!(
meta1.skew_y(),
meta2.skew_y(),
"Raster skew y does not match"
);
// Compare bands
let bands1 = raster1.bands();
let bands2 = raster2.bands();
assert_eq!(bands1.len(), bands2.len(), "Number of bands do not match");
for band_index in 0..bands1.len() {
let band1 = bands1.band(band_index + 1).unwrap();
let band2 = bands2.band(band_index + 1).unwrap();
let band_meta1 = band1.metadata();
let band_meta2 = band2.metadata();
assert_eq!(
band_meta1.data_type(),
band_meta2.data_type(),
"Band data types do not match"
);
assert_eq!(
band_meta1.nodata_value(),
band_meta2.nodata_value(),
"Band nodata values do not match"
);
assert_eq!(
band_meta1.storage_type(),
band_meta2.storage_type(),
"Band storage types do not match"
);
assert_eq!(
band_meta1.outdb_url(),
band_meta2.outdb_url(),
"Band outdb URLs do not match"
);
assert_eq!(
band_meta1.outdb_band_id(),
band_meta2.outdb_band_id(),
"Band outdb band IDs do not match"
);
assert_eq!(band1.data(), band2.data(), "Band data does not match");
}
}
#[cfg(test)]
mod tests {
use super::*;
use sedona_raster::array::RasterStructArray;
use sedona_raster::traits::RasterRef;
#[test]
fn test_generate_test_rasters() {
let count = 5;
let struct_array = generate_test_rasters(count, None).unwrap();
let raster_array = RasterStructArray::new(&struct_array);
assert_eq!(raster_array.len(), count);
for i in 0..count {
let raster = raster_array.get(i).unwrap();
let metadata = raster.metadata();
assert_eq!(metadata.width(), i as u64 + 1);
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.skew_x(), (i as f64) * 0.3);
assert_eq!(metadata.skew_y(), (i as f64) * 0.4);
let bands = raster.bands();
let band = bands.band(1).unwrap();
let band_metadata = band.metadata();
assert_eq!(band_metadata.data_type(), BandDataType::UInt16);
assert_eq!(band_metadata.nodata_value(), Some(&[0u8, 0u8][..]));
assert_eq!(band_metadata.storage_type(), StorageType::InDb);
assert_eq!(band_metadata.outdb_url(), None);
assert_eq!(band_metadata.outdb_band_id(), None);
let band_data = band.data();
let expected_pixel_count = (i + 1) * (i + 2); // width * height
// Convert raw bytes back to u16 values for comparison
let mut actual_pixel_values = Vec::new();
for chunk in band_data.chunks_exact(2) {
let value = u16::from_le_bytes([chunk[0], chunk[1]]);
actual_pixel_values.push(value);
}
let expected_pixel_values: Vec<u16> = (0..expected_pixel_count as u16).collect();
assert_eq!(actual_pixel_values, expected_pixel_values);
}
}
#[test]
fn test_generate_tiled_rasters() {
let tile_size = (64, 64);
let number_of_tiles = (4, 4);
let data_type = BandDataType::UInt8;
let struct_array =
generate_tiled_rasters(tile_size, number_of_tiles, data_type, Some(43)).unwrap();
let raster_array = RasterStructArray::new(&struct_array);
assert_eq!(raster_array.len(), 16); // 4x4 tiles
for i in 0..16 {
let raster = raster_array.get(i).unwrap();
let metadata = raster.metadata();
assert_eq!(metadata.width(), 64);
assert_eq!(metadata.height(), 64);
assert_eq!(metadata.upper_left_x(), ((i % 4) * 64) as f64);
assert_eq!(metadata.upper_left_y(), ((i / 4) * 64) as f64);
let bands = raster.bands();
assert_eq!(bands.len(), 3);
for band_index in 0..3 {
let band = bands.band(band_index + 1).unwrap();
let band_metadata = band.metadata();
assert_eq!(band_metadata.data_type(), BandDataType::UInt8);
assert_eq!(band_metadata.storage_type(), StorageType::InDb);
let band_data = band.data();
assert_eq!(band_data.len(), 64 * 64); // 4096 pixels
}
}
}
#[test]
fn test_raster_arrays_equal() {
let raster_array1 = generate_test_rasters(3, None).unwrap();
let raster_struct_array1 = RasterStructArray::new(&raster_array1);
// Test that identical arrays are equal
assert_raster_arrays_equal(&raster_struct_array1, &raster_struct_array1);
}
#[test]
#[should_panic = "Raster array lengths do not match"]
fn test_raster_arrays_not_equal() {
let raster_array1 = generate_test_rasters(3, None).unwrap();
let raster_struct_array1 = RasterStructArray::new(&raster_array1);
// Test that arrays with different lengths are not equal
let raster_array2 = generate_test_rasters(4, None).unwrap();
let raster_struct_array2 = RasterStructArray::new(&raster_array2);
assert_raster_arrays_equal(&raster_struct_array1, &raster_struct_array2);
}
#[test]
fn test_raster_equal() {
let raster_array1 =
generate_tiled_rasters((256, 256), (1, 1), BandDataType::UInt8, Some(43)).unwrap();
let raster1 = RasterStructArray::new(&raster_array1).get(0).unwrap();
// Assert that the rasters are equal to themselves
assert_raster_equal(&raster1, &raster1);
}
#[test]
#[should_panic = "Band data does not match"]
fn test_raster_different_band_data() {
let raster_array1 =
generate_tiled_rasters((128, 128), (1, 1), BandDataType::UInt8, Some(43)).unwrap();
let raster_array2 =
generate_tiled_rasters((128, 128), (1, 1), BandDataType::UInt8, Some(47)).unwrap();
let raster1 = RasterStructArray::new(&raster_array1).get(0).unwrap();
let raster2 = RasterStructArray::new(&raster_array2).get(0).unwrap();
assert_raster_equal(&raster1, &raster2);
}
#[test]
#[should_panic = "Raster upper left x does not match"]
fn test_raster_different_metadata() {
let raster_array =
generate_tiled_rasters((128, 128), (2, 1), BandDataType::UInt8, Some(43)).unwrap();
let raster1 = RasterStructArray::new(&raster_array).get(0).unwrap();
let raster2 = RasterStructArray::new(&raster_array).get(1).unwrap();
assert_raster_equal(&raster1, &raster2);
}
}