1use crate::pj::Pj;
4use oxiproj_core::{Coord, IoUnits, ProjError, TissotParams};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum TissotMethod {
10 Exact,
11 Numeric,
12 Failed,
13}
14
15impl TissotMethod {
16 pub fn name(&self) -> &'static str {
23 match self {
24 TissotMethod::Exact => "Exact",
25 TissotMethod::Numeric => "Numeric",
26 TissotMethod::Failed => "Failed",
27 }
28 }
29}
30
31#[derive(Debug, Clone)]
33pub struct TissotCell {
34 pub lon: f64,
35 pub lat: f64,
36 pub semi_major: f64,
37 pub semi_minor: f64,
38 pub angular_distortion: f64,
39 pub areal_scale: f64,
40 pub meridian_scale: f64,
41 pub parallel_scale: f64,
42 pub meridian_convergence: f64,
43 pub is_conformal: bool,
44 pub is_equal_area: bool,
45 pub method: TissotMethod,
46}
47
48pub struct DistortionRaster {
50 pub lon_min: f64,
51 pub lon_max: f64,
52 pub lat_min: f64,
53 pub lat_max: f64,
54 pub ncols: usize,
55 pub nrows: usize,
56 pub cells: Vec<TissotCell>,
58}
59
60impl DistortionRaster {
61 pub fn compute(
67 pj: &Pj,
68 lon_min: f64,
69 lat_min: f64,
70 lon_max: f64,
71 lat_max: f64,
72 step_deg: f64,
73 ) -> Self {
74 let ncols = ((lon_max - lon_min) / step_deg).ceil() as usize + 1;
75 let nrows = ((lat_max - lat_min) / step_deg).ceil() as usize + 1;
76 let use_radians = pj.left == IoUnits::Radians;
77
78 let mut cells = Vec::with_capacity(nrows * ncols);
79
80 for r in 0..nrows {
81 let lat = lat_min + r as f64 * step_deg;
82 for c in 0..ncols {
83 let lon = lon_min + c as f64 * step_deg;
84
85 let coord = if use_radians {
86 Coord::new(lon.to_radians(), lat.to_radians(), 0.0, 0.0)
87 } else {
88 Coord::new(lon, lat, 0.0, 0.0)
89 };
90
91 let cell = compute_cell(pj, lon, lat, coord);
92 cells.push(cell);
93 }
94 }
95
96 DistortionRaster {
97 lon_min,
98 lon_max,
99 lat_min,
100 lat_max,
101 ncols,
102 nrows,
103 cells,
104 }
105 }
106
107 pub fn get(&self, row: usize, col: usize) -> Option<&TissotCell> {
109 if row < self.nrows && col < self.ncols {
110 Some(&self.cells[row * self.ncols + col])
111 } else {
112 None
113 }
114 }
115}
116
117fn failed_cell(lon: f64, lat: f64) -> TissotCell {
118 TissotCell {
119 lon,
120 lat,
121 semi_major: f64::NAN,
122 semi_minor: f64::NAN,
123 angular_distortion: f64::NAN,
124 areal_scale: f64::NAN,
125 meridian_scale: f64::NAN,
126 parallel_scale: f64::NAN,
127 meridian_convergence: f64::NAN,
128 is_conformal: false,
129 is_equal_area: false,
130 method: TissotMethod::Failed,
131 }
132}
133
134fn tissot_to_cell(lon: f64, lat: f64, t: TissotParams, method: TissotMethod) -> TissotCell {
135 TissotCell {
136 lon,
137 lat,
138 semi_major: t.semi_major,
139 semi_minor: t.semi_minor,
140 angular_distortion: t.angular_distortion,
141 areal_scale: t.areal_scale,
142 meridian_scale: t.meridian_scale,
143 parallel_scale: t.parallel_scale,
144 meridian_convergence: t.meridian_convergence,
145 is_conformal: t.is_conformal,
146 is_equal_area: t.is_equal_area,
147 method,
148 }
149}
150
151fn compute_cell(pj: &Pj, lon: f64, lat: f64, coord: Coord) -> TissotCell {
152 match pj.factors_exact(coord) {
153 Ok(fe) => {
154 let t = TissotParams::from_factors(&fe);
155 tissot_to_cell(lon, lat, t, TissotMethod::Exact)
156 }
157 Err(ProjError::UnsupportedOperation) => {
158 match pj.factors(coord) {
160 Ok(f) => {
161 let t = TissotParams::from_exact(f.h, f.k, f.s, f.theta_prime, f.conv);
164 tissot_to_cell(lon, lat, t, TissotMethod::Numeric)
165 }
166 Err(_) => failed_cell(lon, lat),
167 }
168 }
169 Err(_) => failed_cell(lon, lat),
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::create::create;
177
178 #[test]
179 fn mercator_3x3_equator() {
180 let pj = create("+proj=merc +datum=WGS84").unwrap();
181 let raster = DistortionRaster::compute(&pj, -5.0, -5.0, 5.0, 5.0, 5.0);
182 assert_eq!(raster.ncols, 3);
183 assert_eq!(raster.nrows, 3);
184
185 for cell in &raster.cells {
190 assert_ne!(
191 cell.method,
192 TissotMethod::Failed,
193 "expected non-Failed at lon={} lat={}",
194 cell.lon,
195 cell.lat
196 );
197 }
198
199 let center = raster.get(1, 1).expect("center cell");
201 assert!(
202 center.angular_distortion.abs() < 1e-6,
203 "angular_distortion at origin should be ~0, got {}",
204 center.angular_distortion
205 );
206 assert!(
207 (center.semi_major - center.semi_minor).abs() < 1e-4,
208 "semi_major ≈ semi_minor for conformal at origin, got {} vs {}",
209 center.semi_major,
210 center.semi_minor
211 );
212 }
213
214 #[test]
215 fn europe_5x5_raster() {
216 let pj = create("+proj=merc +datum=WGS84").unwrap();
217 let raster = DistortionRaster::compute(&pj, -10.0, 35.0, 30.0, 75.0, 10.0);
218 assert_eq!(raster.ncols, 5);
219 assert_eq!(raster.nrows, 5);
220
221 for cell in &raster.cells {
222 assert_ne!(
223 cell.method,
224 TissotMethod::Failed,
225 "cell at lon={} lat={} should not fail",
226 cell.lon,
227 cell.lat
228 );
229 assert!(
231 cell.angular_distortion >= 0.0,
232 "angular_distortion should be >= 0 at lon={} lat={}",
233 cell.lon,
234 cell.lat
235 );
236 }
237 }
238
239 #[test]
240 fn get_out_of_bounds_returns_none() {
241 let pj = create("+proj=merc +datum=WGS84").unwrap();
242 let raster = DistortionRaster::compute(&pj, -5.0, -5.0, 5.0, 5.0, 5.0);
243 assert!(raster.get(10, 10).is_none());
244 assert!(raster.get(0, 0).is_some());
245 }
246
247 #[test]
248 fn tissot_method_name_matches_variants() {
249 assert_eq!(TissotMethod::Exact.name(), "Exact");
253 assert_eq!(TissotMethod::Numeric.name(), "Numeric");
254 assert_eq!(TissotMethod::Failed.name(), "Failed");
255 }
256}