1use crate::error::{SpatialError, SpatialResult};
35use scirs2_core::numeric::Float;
36use std::f64::consts::PI;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct UTMZone {
41 pub number: u8,
43 pub north: bool,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq)]
49pub enum ProjectionType {
50 UTM,
52 WebMercator,
54 LambertConformalConic,
56 AlbersEqualArea,
58 Stereographic,
60 Mercator,
62}
63
64#[derive(Debug, Clone, Copy)]
66pub struct Ellipsoid {
67 pub a: f64,
69 pub f: f64,
71}
72
73impl Ellipsoid {
74 pub const WGS84: Ellipsoid = Ellipsoid {
76 a: 6378137.0,
77 f: 1.0 / 298.257223563,
78 };
79
80 pub const GRS80: Ellipsoid = Ellipsoid {
82 a: 6378137.0,
83 f: 1.0 / 298.257222101,
84 };
85
86 pub fn b(&self) -> f64 {
88 self.a * (1.0 - self.f)
89 }
90
91 pub fn e2(&self) -> f64 {
93 2.0 * self.f - self.f * self.f
94 }
95
96 pub fn e_prime2(&self) -> f64 {
98 let e2 = self.e2();
99 e2 / (1.0 - e2)
100 }
101}
102
103pub fn geographic_to_utm(latitude: f64, longitude: f64) -> SpatialResult<(UTMZone, f64, f64)> {
123 if !(-80.0..=84.0).contains(&latitude) {
124 return Err(SpatialError::ValueError(
125 "Latitude must be between -80 and 84 degrees for UTM".to_string(),
126 ));
127 }
128
129 if !(-180.0..=180.0).contains(&longitude) {
130 return Err(SpatialError::ValueError(
131 "Longitude must be between -180 and 180 degrees".to_string(),
132 ));
133 }
134
135 let zone_number = (((longitude + 180.0) / 6.0).floor() as u8 % 60) + 1;
137 let is_north = latitude >= 0.0;
138
139 let zone = UTMZone {
140 number: zone_number,
141 north: is_north,
142 };
143
144 let lat_rad = latitude * PI / 180.0;
146 let lon_rad = longitude * PI / 180.0;
147
148 let lon0 = ((zone_number as f64 - 1.0) * 6.0 - 180.0 + 3.0) * PI / 180.0;
150
151 let ellipsoid = Ellipsoid::WGS84;
153 let a = ellipsoid.a;
154 let e2 = ellipsoid.e2();
155 let e = e2.sqrt();
156
157 let k0 = 0.9996;
159
160 let n = a / (1.0 - e2 * lat_rad.sin().powi(2)).sqrt();
162 let t = lat_rad.tan();
163 let c = ellipsoid.e_prime2() * lat_rad.cos().powi(2);
164 let aa = (lon_rad - lon0) * lat_rad.cos();
165
166 let m = meridional_arc(lat_rad, &ellipsoid);
168
169 let easting = k0
171 * n
172 * (aa
173 + (1.0 - t * t + c) * aa.powi(3) / 6.0
174 + (5.0 - 18.0 * t * t + t.powi(4) + 72.0 * c - 58.0 * ellipsoid.e_prime2())
175 * aa.powi(5)
176 / 120.0)
177 + 500000.0; let mut northing = k0
181 * (m + n
182 * t
183 * (aa.powi(2) / 2.0
184 + (5.0 - t * t + 9.0 * c + 4.0 * c * c) * aa.powi(4) / 24.0
185 + (61.0 - 58.0 * t * t + t.powi(4) + 600.0 * c - 330.0 * ellipsoid.e_prime2())
186 * aa.powi(6)
187 / 720.0));
188
189 if !is_north {
191 northing += 10000000.0;
192 }
193
194 Ok((zone, easting, northing))
195}
196
197pub fn utm_to_geographic(easting: f64, northing: f64, zone: UTMZone) -> SpatialResult<(f64, f64)> {
209 let ellipsoid = Ellipsoid::WGS84;
210 let a = ellipsoid.a;
211 let e2 = ellipsoid.e2();
212 let e = e2.sqrt();
213 let k0 = 0.9996;
214
215 let x = easting - 500000.0;
217 let mut y = northing;
218 if !zone.north {
219 y -= 10000000.0;
220 }
221
222 let lon0 = ((zone.number as f64 - 1.0) * 6.0 - 180.0 + 3.0) * PI / 180.0;
224
225 let m = y / k0;
227 let mu = m / (a * (1.0 - e2 / 4.0 - 3.0 * e2 * e2 / 64.0 - 5.0 * e2.powi(3) / 256.0));
228
229 let e1 = (1.0 - (1.0 - e2).sqrt()) / (1.0 + (1.0 - e2).sqrt());
230 let phi1 = mu
231 + (3.0 * e1 / 2.0 - 27.0 * e1.powi(3) / 32.0) * (2.0 * mu).sin()
232 + (21.0 * e1 * e1 / 16.0 - 55.0 * e1.powi(4) / 32.0) * (4.0 * mu).sin()
233 + (151.0 * e1.powi(3) / 96.0) * (6.0 * mu).sin();
234
235 let n1 = a / (1.0 - e2 * phi1.sin().powi(2)).sqrt();
237 let t1 = phi1.tan();
238 let c1 = ellipsoid.e_prime2() * phi1.cos().powi(2);
239 let r1 = a * (1.0 - e2) / (1.0 - e2 * phi1.sin().powi(2)).powf(1.5);
240 let d = x / (n1 * k0);
241
242 let latitude = phi1
243 - (n1 * t1 / r1)
244 * (d * d / 2.0
245 - (5.0 + 3.0 * t1 * t1 + 10.0 * c1 - 4.0 * c1 * c1 - 9.0 * ellipsoid.e_prime2())
246 * d.powi(4)
247 / 24.0
248 + (61.0 + 90.0 * t1 * t1 + 298.0 * c1 + 45.0 * t1 * t1 * t1 * t1
249 - 252.0 * ellipsoid.e_prime2()
250 - 3.0 * c1 * c1)
251 * d.powi(6)
252 / 720.0);
253
254 let longitude = lon0
255 + (d - (1.0 + 2.0 * t1 * t1 + c1) * d.powi(3) / 6.0
256 + (5.0 - 2.0 * c1 + 28.0 * t1 * t1 - 3.0 * c1 * c1
257 + 8.0 * ellipsoid.e_prime2()
258 + 24.0 * t1 * t1 * t1 * t1)
259 * d.powi(5)
260 / 120.0)
261 / phi1.cos();
262
263 Ok((latitude * 180.0 / PI, longitude * 180.0 / PI))
264}
265
266fn meridional_arc(lat_rad: f64, ellipsoid: &Ellipsoid) -> f64 {
268 let a = ellipsoid.a;
269 let e2 = ellipsoid.e2();
270
271 let m0 = a * (1.0 - e2 / 4.0 - 3.0 * e2 * e2 / 64.0 - 5.0 * e2.powi(3) / 256.0);
272 let m2 = a * (3.0 * e2 / 8.0 + 3.0 * e2 * e2 / 32.0 + 45.0 * e2.powi(3) / 1024.0);
273 let m4 = a * (15.0 * e2 * e2 / 256.0 + 45.0 * e2.powi(3) / 1024.0);
274 let m6 = a * (35.0 * e2.powi(3) / 3072.0);
275
276 m0 * lat_rad - m2 * (2.0 * lat_rad).sin() + m4 * (4.0 * lat_rad).sin()
277 - m6 * (6.0 * lat_rad).sin()
278}
279
280pub fn geographic_to_web_mercator(latitude: f64, longitude: f64) -> SpatialResult<(f64, f64)> {
293 const MAX_LAT: f64 = 85.051_128_779_806_59; if latitude.abs() > MAX_LAT {
296 return Err(SpatialError::ValueError(format!(
297 "Latitude must be between -{} and {} degrees for Web Mercator",
298 MAX_LAT, MAX_LAT
299 )));
300 }
301
302 let r = 6378137.0; let x = r * longitude * PI / 180.0;
305 let y = r * ((PI / 4.0 + latitude * PI / 360.0).tan().ln());
306
307 Ok((x, y))
308}
309
310pub fn web_mercator_to_geographic(x: f64, y: f64) -> SpatialResult<(f64, f64)> {
321 let r = 6378137.0;
322
323 let longitude = x / r * 180.0 / PI;
324 let latitude = (2.0 * (y / r).exp().atan() - PI / 2.0) * 180.0 / PI;
325
326 Ok((latitude, longitude))
327}
328
329pub fn lambert_conformal_conic(
344 latitude: f64,
345 longitude: f64,
346 lat0: f64,
347 lon0: f64,
348 lat1: f64,
349 lat2: f64,
350) -> SpatialResult<(f64, f64)> {
351 let ellipsoid = Ellipsoid::WGS84;
352 let a = ellipsoid.a;
353 let e = ellipsoid.e2().sqrt();
354
355 let lat = latitude * PI / 180.0;
357 let lon = longitude * PI / 180.0;
358 let lat_0 = lat0 * PI / 180.0;
359 let lon_0 = lon0 * PI / 180.0;
360 let lat_1 = lat1 * PI / 180.0;
361 let lat_2 = lat2 * PI / 180.0;
362
363 let m1 = lat_1.cos() / (1.0 - e * e * lat_1.sin().powi(2)).sqrt();
365 let m2 = lat_2.cos() / (1.0 - e * e * lat_2.sin().powi(2)).sqrt();
366
367 let t = |phi: f64| {
368 (PI / 4.0 - phi / 2.0).tan() / ((1.0 - e * phi.sin()) / (1.0 + e * phi.sin())).powf(e / 2.0)
369 };
370
371 let t1 = t(lat_1);
372 let t2 = t(lat_2);
373 let t0 = t(lat_0);
374 let t_phi = t(lat);
375
376 let n = (m1.ln() - m2.ln()) / (t1.ln() - t2.ln());
377 let f = m1 / (n * t1.powf(n));
378 let rho_0 = a * f * t0.powf(n);
379 let rho = a * f * t_phi.powf(n);
380
381 let theta = n * (lon - lon_0);
382
383 let x = rho * theta.sin();
384 let y = rho_0 - rho * theta.cos();
385
386 Ok((x, y))
387}
388
389pub fn albers_equal_area(
404 latitude: f64,
405 longitude: f64,
406 lat0: f64,
407 lon0: f64,
408 lat1: f64,
409 lat2: f64,
410) -> SpatialResult<(f64, f64)> {
411 let ellipsoid = Ellipsoid::WGS84;
412 let a = ellipsoid.a;
413 let e = ellipsoid.e2().sqrt();
414
415 let lat = latitude * PI / 180.0;
417 let lon = longitude * PI / 180.0;
418 let lat_0 = lat0 * PI / 180.0;
419 let lon_0 = lon0 * PI / 180.0;
420 let lat_1 = lat1 * PI / 180.0;
421 let lat_2 = lat2 * PI / 180.0;
422
423 let q = |phi: f64| {
425 let sin_phi = phi.sin();
426 (1.0 - e * e)
427 * (sin_phi / (1.0 - e * e * sin_phi.powi(2))
428 - (1.0 / (2.0 * e)) * ((1.0 - e * sin_phi) / (1.0 + e * sin_phi)).ln())
429 };
430
431 let m = |phi: f64| phi.cos() / (1.0 - e * e * phi.sin().powi(2)).sqrt();
432
433 let q0 = q(lat_0);
434 let q1 = q(lat_1);
435 let q2 = q(lat_2);
436 let q_phi = q(lat);
437
438 let m1 = m(lat_1);
439 let m2 = m(lat_2);
440
441 let n = (m1.powi(2) - m2.powi(2)) / (q2 - q1);
442 let c = m1.powi(2) + n * q1;
443 let rho_0 = a * (c - n * q0).sqrt() / n;
444 let rho = a * (c - n * q_phi).sqrt() / n;
445
446 let theta = n * (lon - lon_0);
447
448 let x = rho * theta.sin();
449 let y = rho_0 - rho * theta.cos();
450
451 Ok((x, y))
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use approx::assert_relative_eq;
458
459 #[test]
460 fn test_geographic_to_utm() {
461 let (zone, easting, northing) =
463 geographic_to_utm(40.7128, -74.0060).expect("Failed to convert");
464
465 assert_eq!(zone.number, 18);
466 assert!(zone.north);
467 assert!(easting > 500000.0 && easting < 600000.0);
468 assert!(northing > 4500000.0 && northing < 4600000.0);
469 }
470
471 #[test]
472 fn test_utm_roundtrip() {
473 let lat = 40.7128;
474 let lon = -74.0060;
475
476 let (zone, easting, northing) =
477 geographic_to_utm(lat, lon).expect("Failed to convert to UTM");
478
479 let (lat2, lon2) =
480 utm_to_geographic(easting, northing, zone).expect("Failed to convert from UTM");
481
482 assert_relative_eq!(lat, lat2, epsilon = 1e-6);
483 assert_relative_eq!(lon, lon2, epsilon = 1e-6);
484 }
485
486 #[test]
487 fn test_geographic_to_web_mercator() {
488 let (x, y) = geographic_to_web_mercator(0.0, 0.0).expect("Failed to convert");
489
490 assert_relative_eq!(x, 0.0, epsilon = 1.0);
492 assert_relative_eq!(y, 0.0, epsilon = 1.0);
493 }
494
495 #[test]
496 fn test_web_mercator_roundtrip() {
497 let lat = 40.7128;
498 let lon = -74.0060;
499
500 let (x, y) =
501 geographic_to_web_mercator(lat, lon).expect("Failed to convert to Web Mercator");
502
503 let (lat2, lon2) =
504 web_mercator_to_geographic(x, y).expect("Failed to convert from Web Mercator");
505
506 assert_relative_eq!(lat, lat2, epsilon = 1e-6);
507 assert_relative_eq!(lon, lon2, epsilon = 1e-6);
508 }
509
510 #[test]
511 fn test_utm_zone_calculation() {
512 let test_cases = vec![
514 (40.7128, -74.0060, 18), (51.5074, -0.1278, 30), (35.6762, 139.6503, 54), (-33.8688, 151.2093, 56), ];
519
520 for (lat, lon, expected_zone) in test_cases {
521 let (zone, _, _) = geographic_to_utm(lat, lon).expect("Failed to convert");
522 assert_eq!(
523 zone.number, expected_zone,
524 "Wrong zone for ({}, {})",
525 lat, lon
526 );
527 }
528 }
529
530 #[test]
531 fn test_lambert_conformal_conic() {
532 let result = lambert_conformal_conic(
533 40.0, -100.0, 35.0, -100.0, 33.0, 45.0, );
537
538 assert!(result.is_ok());
539 let (x, y) = result.expect("computation failed");
540 assert!(x.abs() < 10_000_000.0);
542 assert!(y.abs() < 10_000_000.0);
543 }
544
545 #[test]
546 fn test_albers_equal_area() {
547 let result = albers_equal_area(
548 40.0, -100.0, 23.0, -96.0, 29.5, 45.5, );
552
553 assert!(result.is_ok());
554 let (x, y) = result.expect("computation failed");
555 assert!(x.abs() < 10_000_000.0);
557 assert!(y.abs() < 10_000_000.0);
558 }
559
560 #[test]
561 fn test_ellipsoid_parameters() {
562 let wgs84 = Ellipsoid::WGS84;
563
564 assert!(wgs84.b() < wgs84.a);
566
567 assert!(wgs84.e2() > 0.0 && wgs84.e2() < 0.01);
569 }
570}