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 pixel_to_geo(&self, col: f64, row: f64) -> (f64, f64) {
74 let x = self.origin_x + col * self.pixel_width + row * self.skew_x;
75 let y = self.origin_y + col * self.skew_y + row * self.pixel_height;
76 (x, y)
77 }
78
79 pub fn geo_to_pixel(&self, x: f64, y: f64) -> Option<(f64, f64)> {
83 let det = self.pixel_width * self.pixel_height - self.skew_x * self.skew_y;
84 if det.abs() < 1e-15 {
85 return None;
86 }
87 let dx = x - self.origin_x;
88 let dy = y - self.origin_y;
89 let col = (self.pixel_height * dx - self.skew_x * dy) / det;
90 let row = (-self.skew_y * dx + self.pixel_width * dy) / det;
91 Some((col, row))
92 }
93
94 pub fn bounds(&self, width: u32, height: u32) -> [f64; 4] {
97 let corners = [
98 self.pixel_to_geo(0.0, 0.0),
99 self.pixel_to_geo(width as f64, 0.0),
100 self.pixel_to_geo(0.0, height as f64),
101 self.pixel_to_geo(width as f64, height as f64),
102 ];
103 let min_x = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
104 let max_x = corners
105 .iter()
106 .map(|c| c.0)
107 .fold(f64::NEG_INFINITY, f64::max);
108 let min_y = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
109 let max_y = corners
110 .iter()
111 .map(|c| c.1)
112 .fold(f64::NEG_INFINITY, f64::max);
113 [min_x, min_y, max_x, max_y]
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::crs::RasterType;
121
122 #[test]
123 fn tiepoint_and_scale_roundtrip() {
124 let tp = [0.0, 0.0, 0.0, -180.0, 90.0, 0.0];
125 let scale = [0.1, 0.1, 0.0];
126 let gt = GeoTransform::from_tiepoint_and_scale(&tp, &scale);
127
128 let (x, y) = gt.pixel_to_geo(0.0, 0.0);
129 assert!((x - (-180.0)).abs() < 1e-10);
130 assert!((y - 90.0).abs() < 1e-10);
131
132 let (x2, y2) = gt.pixel_to_geo(10.0, 10.0);
133 assert!((x2 - (-179.0)).abs() < 1e-10);
134 assert!((y2 - 89.0).abs() < 1e-10);
135
136 let (col, row) = gt.geo_to_pixel(x2, y2).unwrap();
137 assert!((col - 10.0).abs() < 1e-10);
138 assert!((row - 10.0).abs() < 1e-10);
139 }
140
141 #[test]
142 fn bounds_calculation() {
143 let tp = [0.0, 0.0, 0.0, 0.0, 10.0, 0.0];
144 let scale = [1.0, 1.0, 0.0];
145 let gt = GeoTransform::from_tiepoint_and_scale(&tp, &scale);
146 let bounds = gt.bounds(10, 10);
147 assert!((bounds[0] - 0.0).abs() < 1e-10); assert!((bounds[1] - 0.0).abs() < 1e-10); assert!((bounds[2] - 10.0).abs() < 1e-10); assert!((bounds[3] - 10.0).abs() < 1e-10); }
152
153 #[test]
154 fn pixel_is_point_tiepoint_is_normalized_to_outer_bounds() {
155 let tp = [0.0, 0.0, 0.0, 100.0, 200.0, 0.0];
156 let scale = [2.0, 2.0, 0.0];
157 let gt = GeoTransform::from_tiepoint_and_scale_with_raster_type(
158 &tp,
159 &scale,
160 RasterType::PixelIsPoint,
161 );
162
163 let (min_x, max_y) = gt.pixel_to_geo(0.0, 0.0);
164 assert!((min_x - 99.0).abs() < 1e-10);
165 assert!((max_y - 201.0).abs() < 1e-10);
166
167 let (center_x, center_y) = gt.pixel_to_geo(0.5, 0.5);
168 assert!((center_x - 100.0).abs() < 1e-10);
169 assert!((center_y - 200.0).abs() < 1e-10);
170 }
171}