1use raffine::Affine;
2
3#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct Bbox {
6 pub xmin: f64,
8 pub ymin: f64,
10 pub xmax: f64,
12 pub ymax: f64,
14}
15
16impl Bbox {
17 pub fn new(xmin: f64, ymin: f64, xmax: f64, ymax: f64) -> Self {
19 Self {
20 xmin,
21 ymin,
22 xmax,
23 ymax,
24 }
25 }
26 pub fn width(&self) -> f64 {
28 self.xmax - self.xmin
29 }
30 pub fn height(&self) -> f64 {
32 self.ymax - self.ymin
33 }
34
35 pub fn from_corners(corners: &[(f64, f64); 4]) -> Self {
37 let mut xmin = f64::INFINITY;
38 let mut ymin = f64::INFINITY;
39 let mut xmax = f64::NEG_INFINITY;
40 let mut ymax = f64::NEG_INFINITY;
41 for &(x, y) in corners {
42 if x < xmin {
43 xmin = x;
44 }
45 if y < ymin {
46 ymin = y;
47 }
48 if x > xmax {
49 xmax = x;
50 }
51 if y > ymax {
52 ymax = y;
53 }
54 }
55 Self {
56 xmin,
57 ymin,
58 xmax,
59 ymax,
60 }
61 }
62}
63
64#[derive(Debug, Clone, Copy)]
69pub struct GeoTransform(pub Affine);
70
71impl GeoTransform {
72 pub fn from_gdal(gt: &[f64; 6]) -> Self {
74 Self(Affine::from_gdal(gt))
75 }
76
77 #[inline]
79 pub fn forward(&self, col: f64, row: f64) -> (f64, f64) {
80 self.0 * (col, row)
81 }
82
83 pub fn translated(&self, col_off: f64, row_off: f64) -> Self {
85 Self(self.0 * Affine::translation(col_off, row_off))
86 }
87
88 pub fn scaled(&self, sx: f64, sy: f64) -> Self {
90 Self(self.0 * Affine::scale(sx, Some(sy)))
91 }
92
93 pub fn inverse(&self) -> Option<GeoTransform> {
95 (!self.0).ok().map(GeoTransform)
96 }
97
98 pub fn window_for_bbox(&self, bbox: &Bbox) -> Option<(i64, i64, u64, u64)> {
102 let inv = self.inverse()?;
103 let (c0, r0) = inv.forward(bbox.xmin, bbox.ymax);
104 let (c1, r1) = inv.forward(bbox.xmax, bbox.ymin);
105 let (c2, r2) = inv.forward(bbox.xmin, bbox.ymin);
106 let (c3, r3) = inv.forward(bbox.xmax, bbox.ymax);
107 if [c0, r0, c1, r1, c2, r2, c3, r3]
108 .iter()
109 .any(|v| !v.is_finite())
110 {
111 return None;
112 }
113 let cmin = c0.min(c1).min(c2).min(c3);
120 let cmax = c0.max(c1).max(c2).max(c3);
121 let rmin = r0.min(r1).min(r2).min(r3);
122 let rmax = r0.max(r1).max(r2).max(r3);
123 let endpoints = [cmin.floor(), rmin.floor(), cmax.ceil(), rmax.ceil()];
124 if endpoints
126 .iter()
127 .any(|&v| v < i64::MIN as f64 || v >= i64::MAX as f64)
128 {
129 return None;
130 }
131 let [col_off, row_off, col_end, row_end] = endpoints.map(|v| v as i64);
132 let width = col_end.abs_diff(col_off);
133 let height = row_end.abs_diff(row_off);
134 Some((col_off, row_off, width, height))
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 #[test]
141 fn sheared_window_covers_all_four_corners() {
142 let transform = super::GeoTransform::from_gdal(&[0.0, 1.0, 0.25, 0.0, 0.0, -1.0]);
143 assert_eq!(
144 transform.window_for_bbox(&super::Bbox::new(10.0, -20.0, 20.0, -10.0)),
145 Some((5, 10, 13, 10))
146 );
147 }
148 use super::*;
149 use approx::assert_relative_eq;
150
151 #[test]
152 fn window_rejects_non_finite_and_unrepresentable_endpoints() {
153 let gt = GeoTransform::from_gdal(&[0.0, 1e-18, 0.0, 0.0, 0.0, -1e-18]);
154 assert!(gt
155 .window_for_bbox(&Bbox::new(-180.0, -90.0, 0.0, 90.0))
156 .is_none());
157 let identity = GeoTransform::from_gdal(&[0.0, 1.0, 0.0, 0.0, 0.0, 1.0]);
158 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, i64::MAX as f64] {
159 assert!(identity
160 .window_for_bbox(&Bbox::new(0.0, 0.0, value, 1.0))
161 .is_none());
162 }
163 }
164
165 #[test]
166 fn bbox_from_corners_encloses_all() {
167 let b = Bbox::from_corners(&[(0.0, 0.0), (10.0, 5.0), (-1.0, 3.0), (4.0, 8.0)]);
168 assert_eq!(b, Bbox::new(-1.0, 0.0, 10.0, 8.0));
169 }
170
171 #[test]
172 fn geotransform_forward_inverse_roundtrip() {
173 let gt = GeoTransform::from_gdal(&[100.0, 1.0, 0.0, 200.0, 0.0, -1.0]);
175 let (x, y) = gt.forward(0.0, 0.0);
176 assert_relative_eq!(x, 100.0);
177 assert_relative_eq!(y, 200.0);
178
179 let inv = gt.inverse().unwrap();
180 let (c, r) = inv.forward(100.0, 200.0);
181 assert_relative_eq!(c, 0.0, epsilon = 1e-9);
182 assert_relative_eq!(r, 0.0, epsilon = 1e-9);
183 }
184
185 #[test]
186 fn window_for_bbox_clamps_correctly() {
187 let gt = GeoTransform::from_gdal(&[0.0, 1.0, 0.0, 100.0, 0.0, -1.0]);
188 let bbox = Bbox::new(10.0, 50.0, 30.0, 90.0);
189 let (c, r, w, h) = gt.window_for_bbox(&bbox).unwrap();
190 assert_eq!((c, r, w, h), (10, 10, 20, 40));
191 }
192}