use geonative_core::raster::{Band, GeoTransform, RasterTile, ResamplingMethod};
use geonative_core::{Coord, Crs};
use geonative_proj::Transformer;
use crate::raster::{pixel, resample};
#[derive(Debug, Clone)]
pub struct WarpOptions {
pub method: ResamplingMethod,
pub nodata: f64,
}
impl Default for WarpOptions {
fn default() -> Self {
Self {
method: ResamplingMethod::Bilinear,
nodata: 0.0,
}
}
}
pub fn warp_tile(
source: &RasterTile,
target_crs: &Crs,
target_geo_transform: GeoTransform,
target_width: u32,
target_height: u32,
opts: &WarpOptions,
) -> Result<RasterTile, geonative_proj::ProjError> {
let transformer = Transformer::from_crs(target_crs, &source.crs)?;
let bands_out: Vec<Band> = source
.bands
.iter()
.map(|src_band| {
let bpp = src_band.descriptor.dtype.size_bytes();
Band::new(
src_band.descriptor.clone(),
vec![0u8; (target_width as usize) * (target_height as usize) * bpp],
)
})
.collect();
let mut result = RasterTile {
width: target_width,
height: target_height,
bands: bands_out,
geo_transform: target_geo_transform,
crs: target_crs.clone(),
};
let src_gt = source.geo_transform;
for ty in 0..target_height as usize {
for tx in 0..target_width as usize {
let tw = target_geo_transform.pixel_to_world(tx as f64 + 0.5, ty as f64 + 0.5);
let mut sw = tw;
if transformer.transform(&mut sw).is_err() {
continue;
}
let [sc, sr] = src_gt.world_to_pixel(Coord {
x: sw.x,
y: sw.y,
z: None,
m: None,
});
for (band_idx, src_band) in source.bands.iter().enumerate() {
let value =
resample::sample(src_band, source.width, source.height, sc, sr, opts.method)
.unwrap_or(opts.nodata);
pixel::write(
&mut result.bands[band_idx],
tx,
ty,
target_width as usize,
value,
);
}
}
}
Ok(result)
}
pub fn compute_target_grid(
source: &RasterTile,
target_crs: &Crs,
target_width: u32,
) -> Result<(GeoTransform, u32, u32), geonative_proj::ProjError> {
let transformer = Transformer::from_crs(&source.crs, target_crs)?;
let source_bounds = source.bounds();
let mut corners = [
Coord::xy(source_bounds[0], source_bounds[1]),
Coord::xy(source_bounds[2], source_bounds[1]),
Coord::xy(source_bounds[0], source_bounds[3]),
Coord::xy(source_bounds[2], source_bounds[3]),
Coord::xy(
(source_bounds[0] + source_bounds[2]) * 0.5,
(source_bounds[1] + source_bounds[3]) * 0.5,
),
];
for c in corners.iter_mut() {
transformer.transform(c)?;
}
let xs: Vec<f64> = corners.iter().map(|c| c.x).collect();
let ys: Vec<f64> = corners.iter().map(|c| c.y).collect();
let xmin = xs.iter().cloned().fold(f64::INFINITY, f64::min);
let xmax = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let ymin = ys.iter().cloned().fold(f64::INFINITY, f64::min);
let ymax = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let pixel_w = (xmax - xmin) / target_width as f64;
let target_height = (((ymax - ymin) / pixel_w).round() as u32).max(1);
let pixel_h = -(ymax - ymin) / target_height as f64;
let gt = GeoTransform {
origin: [xmin, ymax],
pixel_size: [pixel_w, pixel_h],
rotation: [0.0, 0.0],
};
Ok((gt, target_width, target_height))
}
#[cfg(test)]
mod tests {
use super::*;
use geonative_core::raster::{BandDescriptor, PixelType};
fn melbourne_tile_4326() -> RasterTile {
let mut data = Vec::with_capacity(16);
for r in 0..4u8 {
for c in 0..4u8 {
data.push(r * 64 + c * 16);
}
}
RasterTile {
width: 4,
height: 4,
bands: vec![Band::new(
BandDescriptor::new(Some("v".into()), PixelType::U8),
data,
)],
geo_transform: GeoTransform::north_up(144.9, -37.85, 0.01, 0.01),
crs: Crs::Epsg(4326),
}
}
#[test]
fn warp_4326_to_3857_preserves_shape() {
let src = melbourne_tile_4326();
let (gt, w, h) = compute_target_grid(&src, &Crs::Epsg(3857), 8).unwrap();
let result = warp_tile(&src, &Crs::Epsg(3857), gt, w, h, &WarpOptions::default()).unwrap();
assert_eq!(result.width, w);
assert_eq!(result.height, h);
assert_eq!(result.crs, Crs::Epsg(3857));
assert!(result.bands[0].data.iter().any(|&v| v != 0));
}
#[test]
fn warp_round_trip_4326_to_3857_back_to_4326_is_close() {
let src = melbourne_tile_4326();
let (gt_3857, w_3857, h_3857) = compute_target_grid(&src, &Crs::Epsg(3857), 16).unwrap();
let in_3857 = warp_tile(
&src,
&Crs::Epsg(3857),
gt_3857,
w_3857,
h_3857,
&WarpOptions::default(),
)
.unwrap();
let (gt_back, w_back, h_back) =
compute_target_grid(&in_3857, &Crs::Epsg(4326), 16).unwrap();
let back = warp_tile(
&in_3857,
&Crs::Epsg(4326),
gt_back,
w_back,
h_back,
&WarpOptions::default(),
)
.unwrap();
assert_eq!(back.crs, Crs::Epsg(4326));
assert!(back.width > 0);
}
#[test]
fn warp_identity_crs_keeps_pixels_intact() {
let src = melbourne_tile_4326();
let result = warp_tile(
&src,
&Crs::Epsg(4326),
src.geo_transform,
src.width,
src.height,
&WarpOptions::default(),
)
.unwrap();
assert_eq!(result.width, src.width);
assert!((result.bands[0].data[0] as i32 - src.bands[0].data[0] as i32).abs() < 5);
}
}