geonative_processing/raster/
warp.rs1use geonative_core::raster::{Band, GeoTransform, RasterTile, ResamplingMethod};
30use geonative_core::{Coord, Crs};
31use geonative_proj::Transformer;
32
33use crate::raster::{pixel, resample};
34
35#[derive(Debug, Clone)]
36pub struct WarpOptions {
37 pub method: ResamplingMethod,
38 pub nodata: f64,
42}
43
44impl Default for WarpOptions {
45 fn default() -> Self {
46 Self {
47 method: ResamplingMethod::Bilinear,
48 nodata: 0.0,
49 }
50 }
51}
52
53pub fn warp_tile(
57 source: &RasterTile,
58 target_crs: &Crs,
59 target_geo_transform: GeoTransform,
60 target_width: u32,
61 target_height: u32,
62 opts: &WarpOptions,
63) -> Result<RasterTile, geonative_proj::ProjError> {
64 let transformer = Transformer::from_crs(target_crs, &source.crs)?;
67
68 let bands_out: Vec<Band> = source
69 .bands
70 .iter()
71 .map(|src_band| {
72 let bpp = src_band.descriptor.dtype.size_bytes();
73 Band::new(
74 src_band.descriptor.clone(),
75 vec![0u8; (target_width as usize) * (target_height as usize) * bpp],
76 )
77 })
78 .collect();
79
80 let mut result = RasterTile {
81 width: target_width,
82 height: target_height,
83 bands: bands_out,
84 geo_transform: target_geo_transform,
85 crs: target_crs.clone(),
86 };
87
88 let src_gt = source.geo_transform;
90
91 for ty in 0..target_height as usize {
92 for tx in 0..target_width as usize {
93 let tw = target_geo_transform.pixel_to_world(tx as f64 + 0.5, ty as f64 + 0.5);
95 let mut sw = tw;
97 if transformer.transform(&mut sw).is_err() {
98 continue;
99 }
100 let [sc, sr] = src_gt.world_to_pixel(Coord {
102 x: sw.x,
103 y: sw.y,
104 z: None,
105 m: None,
106 });
107 for (band_idx, src_band) in source.bands.iter().enumerate() {
109 let value =
110 resample::sample(src_band, source.width, source.height, sc, sr, opts.method)
111 .unwrap_or(opts.nodata);
112 pixel::write(
113 &mut result.bands[band_idx],
114 tx,
115 ty,
116 target_width as usize,
117 value,
118 );
119 }
120 }
121 }
122
123 Ok(result)
124}
125
126pub fn compute_target_grid(
133 source: &RasterTile,
134 target_crs: &Crs,
135 target_width: u32,
136) -> Result<(GeoTransform, u32, u32), geonative_proj::ProjError> {
137 let transformer = Transformer::from_crs(&source.crs, target_crs)?;
138 let source_bounds = source.bounds();
139 let mut corners = [
141 Coord::xy(source_bounds[0], source_bounds[1]),
142 Coord::xy(source_bounds[2], source_bounds[1]),
143 Coord::xy(source_bounds[0], source_bounds[3]),
144 Coord::xy(source_bounds[2], source_bounds[3]),
145 Coord::xy(
147 (source_bounds[0] + source_bounds[2]) * 0.5,
148 (source_bounds[1] + source_bounds[3]) * 0.5,
149 ),
150 ];
151 for c in corners.iter_mut() {
152 transformer.transform(c)?;
153 }
154 let xs: Vec<f64> = corners.iter().map(|c| c.x).collect();
155 let ys: Vec<f64> = corners.iter().map(|c| c.y).collect();
156 let xmin = xs.iter().cloned().fold(f64::INFINITY, f64::min);
157 let xmax = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
158 let ymin = ys.iter().cloned().fold(f64::INFINITY, f64::min);
159 let ymax = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
160
161 let pixel_w = (xmax - xmin) / target_width as f64;
162 let target_height = (((ymax - ymin) / pixel_w).round() as u32).max(1);
164 let pixel_h = -(ymax - ymin) / target_height as f64;
165
166 let gt = GeoTransform {
167 origin: [xmin, ymax],
168 pixel_size: [pixel_w, pixel_h],
169 rotation: [0.0, 0.0],
170 };
171 Ok((gt, target_width, target_height))
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use geonative_core::raster::{BandDescriptor, PixelType};
178
179 fn melbourne_tile_4326() -> RasterTile {
181 let mut data = Vec::with_capacity(16);
182 for r in 0..4u8 {
183 for c in 0..4u8 {
184 data.push(r * 64 + c * 16);
185 }
186 }
187 RasterTile {
188 width: 4,
189 height: 4,
190 bands: vec![Band::new(
191 BandDescriptor::new(Some("v".into()), PixelType::U8),
192 data,
193 )],
194 geo_transform: GeoTransform::north_up(144.9, -37.85, 0.01, 0.01),
195 crs: Crs::Epsg(4326),
196 }
197 }
198
199 #[test]
200 fn warp_4326_to_3857_preserves_shape() {
201 let src = melbourne_tile_4326();
202 let (gt, w, h) = compute_target_grid(&src, &Crs::Epsg(3857), 8).unwrap();
203 let result = warp_tile(&src, &Crs::Epsg(3857), gt, w, h, &WarpOptions::default()).unwrap();
204 assert_eq!(result.width, w);
205 assert_eq!(result.height, h);
206 assert_eq!(result.crs, Crs::Epsg(3857));
207 assert!(result.bands[0].data.iter().any(|&v| v != 0));
210 }
211
212 #[test]
213 fn warp_round_trip_4326_to_3857_back_to_4326_is_close() {
214 let src = melbourne_tile_4326();
215 let (gt_3857, w_3857, h_3857) = compute_target_grid(&src, &Crs::Epsg(3857), 16).unwrap();
217 let in_3857 = warp_tile(
218 &src,
219 &Crs::Epsg(3857),
220 gt_3857,
221 w_3857,
222 h_3857,
223 &WarpOptions::default(),
224 )
225 .unwrap();
226 let (gt_back, w_back, h_back) =
228 compute_target_grid(&in_3857, &Crs::Epsg(4326), 16).unwrap();
229 let back = warp_tile(
230 &in_3857,
231 &Crs::Epsg(4326),
232 gt_back,
233 w_back,
234 h_back,
235 &WarpOptions::default(),
236 )
237 .unwrap();
238 assert_eq!(back.crs, Crs::Epsg(4326));
239 assert!(back.width > 0);
240 }
241
242 #[test]
243 fn warp_identity_crs_keeps_pixels_intact() {
244 let src = melbourne_tile_4326();
245 let result = warp_tile(
248 &src,
249 &Crs::Epsg(4326),
250 src.geo_transform,
251 src.width,
252 src.height,
253 &WarpOptions::default(),
254 )
255 .unwrap();
256 assert_eq!(result.width, src.width);
257 assert!((result.bands[0].data[0] as i32 - src.bands[0].data[0] as i32).abs() < 5);
259 }
260}