1use crate::crs::RasterType;
4
5#[derive(Debug, Clone, Copy)]
15pub struct GeoTransform {
16 pub origin_x: f64,
17 pub pixel_width: f64,
18 pub skew_x: f64,
19 pub origin_y: f64,
20 pub skew_y: f64,
21 pub pixel_height: f64,
22}
23
24impl GeoTransform {
25 pub fn from_tiepoint_and_scale(tiepoint: &[f64; 6], pixel_scale: &[f64; 3]) -> Self {
27 Self::from_tiepoint_and_scale_with_raster_type(
28 tiepoint,
29 pixel_scale,
30 RasterType::PixelIsArea,
31 )
32 }
33
34 pub fn from_tiepoint_and_scale_with_raster_type(
40 tiepoint: &[f64; 6],
41 pixel_scale: &[f64; 3],
42 raster_type: RasterType,
43 ) -> Self {
44 let pixel_offset = match raster_type {
47 RasterType::PixelIsPoint => 0.5,
48 RasterType::PixelIsArea | RasterType::Unknown(_) => 0.0,
49 };
50 Self {
51 origin_x: tiepoint[3] - (tiepoint[0] + pixel_offset) * pixel_scale[0],
52 pixel_width: pixel_scale[0],
53 skew_x: 0.0,
54 origin_y: tiepoint[4] + (tiepoint[1] + pixel_offset) * pixel_scale[1],
55 skew_y: 0.0,
56 pixel_height: -pixel_scale[1],
57 }
58 }
59
60 pub fn from_transformation_matrix(matrix: &[f64; 16]) -> Self {
62 Self {
63 origin_x: matrix[3],
64 pixel_width: matrix[0],
65 skew_x: matrix[1],
66 origin_y: matrix[7],
67 skew_y: matrix[4],
68 pixel_height: matrix[5],
69 }
70 }
71
72 pub fn from_origin_and_pixel_size(
74 origin_x: f64,
75 origin_y: f64,
76 pixel_width: f64,
77 pixel_height: f64,
78 ) -> Self {
79 Self {
80 origin_x,
81 pixel_width,
82 skew_x: 0.0,
83 origin_y,
84 skew_y: 0.0,
85 pixel_height,
86 }
87 }
88
89 pub fn pixel_to_geo(&self, col: f64, row: f64) -> (f64, f64) {
91 let x = self.origin_x + col * self.pixel_width + row * self.skew_x;
92 let y = self.origin_y + col * self.skew_y + row * self.pixel_height;
93 (x, y)
94 }
95
96 pub fn geo_to_pixel(&self, x: f64, y: f64) -> Option<(f64, f64)> {
100 let det = self.pixel_width * self.pixel_height - self.skew_x * self.skew_y;
101 let determinant_scale =
102 (self.pixel_width * self.pixel_height).abs() + (self.skew_x * self.skew_y).abs();
103 if !det.is_finite() || det == 0.0 || det.abs() <= f64::EPSILON * determinant_scale {
104 return None;
105 }
106 let dx = x - self.origin_x;
107 let dy = y - self.origin_y;
108 let col = (self.pixel_height * dx - self.skew_x * dy) / det;
109 let row = (-self.skew_y * dx + self.pixel_width * dy) / det;
110 Some((col, row))
111 }
112
113 pub fn bounds(&self, width: u32, height: u32) -> [f64; 4] {
116 let corners = [
117 self.pixel_to_geo(0.0, 0.0),
118 self.pixel_to_geo(width as f64, 0.0),
119 self.pixel_to_geo(0.0, height as f64),
120 self.pixel_to_geo(width as f64, height as f64),
121 ];
122 let min_x = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
123 let max_x = corners
124 .iter()
125 .map(|c| c.0)
126 .fold(f64::NEG_INFINITY, f64::max);
127 let min_y = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
128 let max_y = corners
129 .iter()
130 .map(|c| c.1)
131 .fold(f64::NEG_INFINITY, f64::max);
132 [min_x, min_y, max_x, max_y]
133 }
134
135 pub fn to_tiepoint_and_scale(&self) -> Option<([f64; 6], [f64; 3])> {
142 self.to_tiepoint_and_scale_with_raster_type(RasterType::PixelIsArea)
143 }
144
145 pub fn to_tiepoint_and_scale_with_raster_type(
153 &self,
154 raster_type: RasterType,
155 ) -> Option<([f64; 6], [f64; 3])> {
156 if self.skew_x != 0.0
160 || self.skew_y != 0.0
161 || !self.pixel_width.is_finite()
162 || !self.pixel_height.is_finite()
163 || self.pixel_width <= 0.0
164 || self.pixel_height >= 0.0
165 {
166 return None;
167 }
168 let scale = [self.pixel_width, -self.pixel_height, 0.0];
169 let pixel_offset = match raster_type {
170 RasterType::PixelIsPoint => 0.5,
171 RasterType::PixelIsArea | RasterType::Unknown(_) => 0.0,
172 };
173 let tiepoint = [
174 0.0,
175 0.0,
176 0.0,
177 self.origin_x + pixel_offset * scale[0],
178 self.origin_y - pixel_offset * scale[1],
179 0.0,
180 ];
181 Some((tiepoint, scale))
182 }
183
184 pub fn to_transformation_matrix(&self) -> [f64; 16] {
186 [
187 self.pixel_width,
188 self.skew_x,
189 0.0,
190 self.origin_x,
191 self.skew_y,
192 self.pixel_height,
193 0.0,
194 self.origin_y,
195 0.0,
196 0.0,
197 0.0,
198 0.0,
199 0.0,
200 0.0,
201 0.0,
202 1.0,
203 ]
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::crs::RasterType;
211
212 #[test]
213 fn tiepoint_and_scale_roundtrip() {
214 let tp = [0.0, 0.0, 0.0, -180.0, 90.0, 0.0];
215 let scale = [0.1, 0.1, 0.0];
216 let gt = GeoTransform::from_tiepoint_and_scale(&tp, &scale);
217
218 let (x, y) = gt.pixel_to_geo(0.0, 0.0);
219 assert!((x - (-180.0)).abs() < 1e-10);
220 assert!((y - 90.0).abs() < 1e-10);
221
222 let (x2, y2) = gt.pixel_to_geo(10.0, 10.0);
223 assert!((x2 - (-179.0)).abs() < 1e-10);
224 assert!((y2 - 89.0).abs() < 1e-10);
225
226 let (col, row) = gt.geo_to_pixel(x2, y2).unwrap();
227 assert!((col - 10.0).abs() < 1e-10);
228 assert!((row - 10.0).abs() < 1e-10);
229 }
230
231 #[test]
232 fn bounds_calculation() {
233 let tp = [0.0, 0.0, 0.0, 0.0, 10.0, 0.0];
234 let scale = [1.0, 1.0, 0.0];
235 let gt = GeoTransform::from_tiepoint_and_scale(&tp, &scale);
236 let bounds = gt.bounds(10, 10);
237 assert!((bounds[0] - 0.0).abs() < 1e-10);
238 assert!((bounds[1] - 0.0).abs() < 1e-10);
239 assert!((bounds[2] - 10.0).abs() < 1e-10);
240 assert!((bounds[3] - 10.0).abs() < 1e-10);
241 }
242
243 #[test]
244 fn pixel_is_point_tiepoint_is_normalized_to_outer_bounds() {
245 let tp = [0.0, 0.0, 0.0, 100.0, 200.0, 0.0];
246 let scale = [2.0, 2.0, 0.0];
247 let gt = GeoTransform::from_tiepoint_and_scale_with_raster_type(
248 &tp,
249 &scale,
250 RasterType::PixelIsPoint,
251 );
252
253 let (min_x, max_y) = gt.pixel_to_geo(0.0, 0.0);
254 assert!((min_x - 99.0).abs() < 1e-10);
255 assert!((max_y - 201.0).abs() < 1e-10);
256
257 let (center_x, center_y) = gt.pixel_to_geo(0.5, 0.5);
258 assert!((center_x - 100.0).abs() < 1e-10);
259 assert!((center_y - 200.0).abs() < 1e-10);
260 }
261
262 #[test]
263 fn to_tiepoint_and_scale_roundtrips() {
264 let gt = GeoTransform::from_origin_and_pixel_size(-180.0, 90.0, 0.1, -0.1);
265 let (tp, scale) = gt.to_tiepoint_and_scale().unwrap();
266 let gt2 = GeoTransform::from_tiepoint_and_scale(&tp, &scale);
267 assert!((gt2.origin_x - gt.origin_x).abs() < 1e-10);
268 assert!((gt2.origin_y - gt.origin_y).abs() < 1e-10);
269 assert!((gt2.pixel_width - gt.pixel_width).abs() < 1e-10);
270 assert!((gt2.pixel_height - gt.pixel_height).abs() < 1e-10);
271 }
272
273 #[test]
274 fn pixel_is_point_tiepoint_and_scale_roundtrips_normalized_transform() {
275 let gt = GeoTransform::from_origin_and_pixel_size(99.0, 201.0, 2.0, -2.0);
276 let (tp, scale) = gt
277 .to_tiepoint_and_scale_with_raster_type(RasterType::PixelIsPoint)
278 .unwrap();
279 assert!((tp[3] - 100.0).abs() < 1e-10);
280 assert!((tp[4] - 200.0).abs() < 1e-10);
281
282 let gt2 = GeoTransform::from_tiepoint_and_scale_with_raster_type(
283 &tp,
284 &scale,
285 RasterType::PixelIsPoint,
286 );
287 assert!((gt2.origin_x - gt.origin_x).abs() < 1e-10);
288 assert!((gt2.origin_y - gt.origin_y).abs() < 1e-10);
289 assert!((gt2.pixel_width - gt.pixel_width).abs() < 1e-10);
290 assert!((gt2.pixel_height - gt.pixel_height).abs() < 1e-10);
291 }
292
293 #[test]
294 fn skewed_transform_returns_none_for_tiepoint_scale() {
295 let gt = GeoTransform {
296 origin_x: 0.0,
297 pixel_width: 1.0,
298 skew_x: 0.5,
299 origin_y: 0.0,
300 skew_y: 0.0,
301 pixel_height: -1.0,
302 };
303 assert!(gt.to_tiepoint_and_scale().is_none());
304 }
305
306 #[test]
307 fn tiny_but_invertible_transform_roundtrips() {
308 let gt = GeoTransform::from_origin_and_pixel_size(10.0, 20.0, 1e-12, -1e-12);
309 let (x, y) = gt.pixel_to_geo(3.0, 4.0);
310 let (col, row) = gt.geo_to_pixel(x, y).unwrap();
311 assert!((col - 3.0).abs() < 1e-3);
312 assert!((row - 4.0).abs() < 1e-3);
313 }
314
315 #[test]
316 fn flipped_or_tiny_skewed_transforms_require_a_matrix() {
317 let flipped = GeoTransform::from_origin_and_pixel_size(0.0, 0.0, -1.0, -1.0);
318 assert!(flipped.to_tiepoint_and_scale().is_none());
319
320 let skewed = GeoTransform {
321 origin_x: 0.0,
322 pixel_width: 1.0,
323 skew_x: 1e-16,
324 origin_y: 0.0,
325 skew_y: 0.0,
326 pixel_height: -1.0,
327 };
328 assert!(skewed.to_tiepoint_and_scale().is_none());
329 }
330
331 #[test]
332 fn transformation_matrix_roundtrips() {
333 let gt = GeoTransform::from_origin_and_pixel_size(100.0, 200.0, 0.5, -0.5);
334 let matrix = gt.to_transformation_matrix();
335 let gt2 = GeoTransform::from_transformation_matrix(&matrix);
336 assert!((gt2.origin_x - gt.origin_x).abs() < 1e-10);
337 assert!((gt2.origin_y - gt.origin_y).abs() < 1e-10);
338 assert!((gt2.pixel_width - gt.pixel_width).abs() < 1e-10);
339 assert!((gt2.pixel_height - gt.pixel_height).abs() < 1e-10);
340 }
341}