1#[cfg(feature = "std")]
15pub mod azimuthal;
16#[cfg(feature = "std")]
17pub mod conic;
18#[cfg(feature = "std")]
19pub mod cylindrical;
20#[cfg(feature = "std")]
21pub mod pseudocylindrical;
22#[cfg(feature = "std")]
23pub mod simd;
24
25#[cfg(feature = "std")]
26use crate::area_of_use::area_of_use_for_epsg;
27#[cfg(feature = "std")]
28use crate::crs::{Crs, CrsSource};
29use crate::error::{Error, Result};
30#[cfg(not(feature = "std"))]
31use alloc::format;
32use core::fmt;
33#[cfg(feature = "std")]
34use std::sync::Mutex;
35
36#[cfg(feature = "std")]
37use crate::proj_string::ProjString;
38
39#[cfg(feature = "std")]
41pub use azimuthal::{AzimuthalEquidistant, Gnomonic, LambertAzimuthalEqualArea};
42#[cfg(feature = "std")]
43pub use conic::{EquidistantConic, LambertConformalConic};
44#[cfg(feature = "std")]
45pub use cylindrical::{CassineSoldner, GaussKruger, TransverseMercator};
46#[cfg(feature = "std")]
47pub use pseudocylindrical::{EckertIV, EckertVI, Mollweide, Robinson, Sinusoidal};
48
49#[derive(Debug, Clone, Copy, PartialEq)]
51pub struct Coordinate {
52 pub x: f64,
54 pub y: f64,
56}
57
58impl Coordinate {
59 pub fn new(x: f64, y: f64) -> Self {
61 Self { x, y }
62 }
63
64 pub fn from_lon_lat(lon: f64, lat: f64) -> Self {
66 Self::new(lon, lat)
67 }
68
69 pub fn lon(&self) -> f64 {
71 self.x
72 }
73
74 pub fn lat(&self) -> f64 {
76 self.y
77 }
78
79 pub fn validate_geographic(&self) -> Result<()> {
81 if !(-180.0..=180.0).contains(&self.x) {
82 return Err(Error::coordinate_out_of_bounds(self.x, self.y));
83 }
84 if !(-90.0..=90.0).contains(&self.y) {
85 return Err(Error::coordinate_out_of_bounds(self.x, self.y));
86 }
87 Ok(())
88 }
89
90 pub fn is_valid(&self) -> bool {
92 self.x.is_finite() && self.y.is_finite()
93 }
94}
95
96impl fmt::Display for Coordinate {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 write!(f, "({}, {})", self.x, self.y)
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq)]
104pub struct Coordinate3D {
105 pub x: f64,
107 pub y: f64,
109 pub z: f64,
111}
112
113impl Coordinate3D {
114 pub fn new(x: f64, y: f64, z: f64) -> Self {
116 Self { x, y, z }
117 }
118
119 pub fn to_2d(&self) -> Coordinate {
121 Coordinate::new(self.x, self.y)
122 }
123
124 pub fn is_valid(&self) -> bool {
126 self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
127 }
128}
129
130impl From<Coordinate> for Coordinate3D {
131 fn from(coord: Coordinate) -> Self {
132 Self::new(coord.x, coord.y, 0.0)
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq)]
138pub struct BoundingBox {
139 pub min_x: f64,
141 pub min_y: f64,
143 pub max_x: f64,
145 pub max_y: f64,
147}
148
149impl BoundingBox {
150 pub fn new(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Result<Self> {
156 if min_x > max_x {
157 return Err(Error::invalid_bounding_box(format!(
158 "min_x ({}) > max_x ({})",
159 min_x, max_x
160 )));
161 }
162 if min_y > max_y {
163 return Err(Error::invalid_bounding_box(format!(
164 "min_y ({}) > max_y ({})",
165 min_y, max_y
166 )));
167 }
168
169 Ok(Self {
170 min_x,
171 min_y,
172 max_x,
173 max_y,
174 })
175 }
176
177 pub fn from_coordinates(c1: Coordinate, c2: Coordinate) -> Result<Self> {
179 let min_x = c1.x.min(c2.x);
180 let min_y = c1.y.min(c2.y);
181 let max_x = c1.x.max(c2.x);
182 let max_y = c1.y.max(c2.y);
183 Self::new(min_x, min_y, max_x, max_y)
184 }
185
186 pub fn width(&self) -> f64 {
188 self.max_x - self.min_x
189 }
190
191 pub fn height(&self) -> f64 {
193 self.max_y - self.min_y
194 }
195
196 pub fn center(&self) -> Coordinate {
198 Coordinate::new(
199 (self.min_x + self.max_x) / 2.0,
200 (self.min_y + self.max_y) / 2.0,
201 )
202 }
203
204 pub fn corners(&self) -> [Coordinate; 4] {
206 [
207 Coordinate::new(self.min_x, self.min_y),
208 Coordinate::new(self.max_x, self.min_y),
209 Coordinate::new(self.max_x, self.max_y),
210 Coordinate::new(self.min_x, self.max_y),
211 ]
212 }
213
214 pub fn contains(&self, coord: &Coordinate) -> bool {
216 coord.x >= self.min_x
217 && coord.x <= self.max_x
218 && coord.y >= self.min_y
219 && coord.y <= self.max_y
220 }
221
222 pub fn expand_to_include(&mut self, coord: &Coordinate) {
224 self.min_x = self.min_x.min(coord.x);
225 self.min_y = self.min_y.min(coord.y);
226 self.max_x = self.max_x.max(coord.x);
227 self.max_y = self.max_y.max(coord.y);
228 }
229}
230
231#[cfg(feature = "std")]
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
239pub enum AreaOfUseCheck {
240 #[default]
242 Off,
243 Warn,
246 Strict,
249}
250
251#[cfg(feature = "std")]
257#[derive(Debug, Clone, Copy, PartialEq)]
258pub struct AreaOfUseWarning {
259 pub lon: f64,
261 pub lat: f64,
263 pub epsg: u32,
265 pub west: f64,
267 pub south: f64,
269 pub east: f64,
271 pub north: f64,
273}
274
275#[cfg(feature = "std")]
277pub struct Transformer {
278 source_crs: Crs,
279 target_crs: Crs,
280 proj: Option<proj4rs::Proj>,
281 strict: bool,
286 area_of_use_check: AreaOfUseCheck,
290 last_warning: Mutex<Option<AreaOfUseWarning>>,
299 source_epoch: Option<f64>,
301 target_epoch: Option<f64>,
303 itrf_params: Option<(crate::datum_transform::ItrfTransformParams, f64)>,
307 geoid: Option<std::sync::Arc<crate::geoid::GeoidGrid>>,
312}
313
314#[cfg(feature = "std")]
315impl Transformer {
316 pub fn new(source_crs: Crs, target_crs: Crs) -> Result<Self> {
327 let is_compound = matches!(source_crs.source(), CrsSource::Compound { .. })
332 || matches!(target_crs.source(), CrsSource::Compound { .. });
333
334 let either_engineering = source_crs.is_engineering() || target_crs.is_engineering();
339
340 let proj = if is_compound || either_engineering || source_crs.is_equivalent(&target_crs) {
341 None
342 } else {
343 let source_proj_str = source_crs.to_proj_string()?;
345 let target_proj_str = target_crs.to_proj_string()?;
346
347 let _source_proj = proj4rs::Proj::from_proj_string(&source_proj_str)
348 .map_err(|e| Error::projection_init_error(format!("Source CRS: {:?}", e)))?;
349
350 let target_proj = proj4rs::Proj::from_proj_string(&target_proj_str)
351 .map_err(|e| Error::projection_init_error(format!("Target CRS: {:?}", e)))?;
352
353 Some(target_proj)
355 };
356
357 Ok(Self {
358 source_crs,
359 target_crs,
360 proj,
361 strict: true,
362 area_of_use_check: AreaOfUseCheck::default(),
363 last_warning: Mutex::new(None),
364 source_epoch: None,
365 target_epoch: None,
366 itrf_params: None,
367 geoid: None,
368 })
369 }
370
371 pub fn with_geoid(mut self, grid: std::sync::Arc<crate::geoid::GeoidGrid>) -> Self {
394 self.geoid = Some(grid);
395 self
396 }
397
398 pub fn geoid(&self) -> Option<&std::sync::Arc<crate::geoid::GeoidGrid>> {
400 self.geoid.as_ref()
401 }
402
403 pub fn with_strict(mut self, strict: bool) -> Self {
409 self.strict = strict;
410 self
411 }
412
413 pub fn is_strict(&self) -> bool {
415 self.strict
416 }
417
418 pub fn with_area_of_use_check(mut self, mode: AreaOfUseCheck) -> Self {
434 self.area_of_use_check = mode;
435 if let Ok(mut slot) = self.last_warning.lock() {
436 *slot = None;
437 }
438 self
439 }
440
441 pub fn area_of_use_check(&self) -> AreaOfUseCheck {
443 self.area_of_use_check
444 }
445
446 pub fn last_warning(&self) -> Option<AreaOfUseWarning> {
453 self.last_warning.lock().ok().and_then(|guard| *guard)
457 }
458
459 pub fn clear_warning(&self) {
461 if let Ok(mut slot) = self.last_warning.lock() {
462 *slot = None;
463 }
464 }
465
466 fn check_area_of_use(&self, lon: f64, lat: f64) -> Result<()> {
477 if self.area_of_use_check == AreaOfUseCheck::Off {
478 return Ok(());
479 }
480 let epsg = match self.source_crs.epsg_code() {
481 Some(c) => c,
482 None => return Ok(()),
483 };
484 let aou = match area_of_use_for_epsg(epsg) {
485 Some(a) => a,
486 None => return Ok(()),
487 };
488 if aou.contains(lon, lat) {
489 return Ok(());
490 }
491 match self.area_of_use_check {
492 AreaOfUseCheck::Off => Ok(()),
493 AreaOfUseCheck::Warn => {
494 if let Ok(mut slot) = self.last_warning.lock() {
495 *slot = Some(AreaOfUseWarning {
496 lon,
497 lat,
498 epsg,
499 west: aou.west,
500 south: aou.south,
501 east: aou.east,
502 north: aou.north,
503 });
504 }
505 Ok(())
506 }
507 AreaOfUseCheck::Strict => Err(Error::OutsideAreaOfUse {
508 lon,
509 lat,
510 epsg,
511 west: aou.west,
512 south: aou.south,
513 east: aou.east,
514 north: aou.north,
515 }),
516 }
517 }
518
519 pub fn with_epoch(mut self, source_epoch: f64, target_epoch: f64) -> Result<Self> {
540 let src_itrf = self.source_crs.itrf_name().ok_or_else(|| {
541 Error::transformation_error(
542 "with_epoch requires the source CRS to be an ITRF realisation",
543 )
544 })?;
545 let dst_itrf = self.target_crs.itrf_name().ok_or_else(|| {
546 Error::transformation_error(
547 "with_epoch requires the target CRS to be an ITRF realisation",
548 )
549 })?;
550
551 let params_ref = crate::datum_transform::find_itrf_params(&src_itrf, &dst_itrf)
552 .ok_or_else(|| {
553 Error::transformation_error(format!(
554 "no ITRF parameters registered for {src_itrf} \u{2192} {dst_itrf}"
555 ))
556 })?;
557
558 self.source_epoch = Some(source_epoch);
559 self.target_epoch = Some(target_epoch);
560 self.itrf_params = Some(params_ref);
561 Ok(self)
562 }
563
564 pub fn source_epoch(&self) -> Option<f64> {
566 self.source_epoch
567 }
568
569 pub fn target_epoch(&self) -> Option<f64> {
571 self.target_epoch
572 }
573
574 pub fn from_epsg(source_epsg: u32, target_epsg: u32) -> Result<Self> {
585 let source_crs = Crs::from_epsg(source_epsg)?;
586 let target_crs = Crs::from_epsg(target_epsg)?;
587 Self::new(source_crs, target_crs)
588 }
589
590 pub fn source_crs(&self) -> &Crs {
592 &self.source_crs
593 }
594
595 pub fn target_crs(&self) -> &Crs {
597 &self.target_crs
598 }
599
600 pub fn transform(&self, coord: &Coordinate) -> Result<Coordinate> {
611 self.check_area_of_use(coord.x, coord.y)?;
615
616 if self.proj.is_none() {
618 return Ok(*coord);
619 }
620
621 if !coord.is_valid() {
623 return Err(Error::invalid_coordinate(
624 "Coordinate contains non-finite values",
625 ));
626 }
627
628 if self.strict && self.source_crs.is_geographic() {
635 if let Some(aou) = self.source_crs.area_of_use() {
636 if !aou.contains(coord.x, coord.y) {
637 return Err(Error::out_of_area_of_use(
638 coord.x,
639 coord.y,
640 self.source_crs.to_string(),
641 ));
642 }
643 }
644 }
645
646 self.transform_impl(coord)
648 }
649
650 pub fn transform_3d(&self, coord: &Coordinate3D) -> Result<Coordinate3D> {
673 if let (Some((params, ref_epoch)), Some(t0), Some(t1)) =
675 (&self.itrf_params, self.source_epoch, self.target_epoch)
676 {
677 if !coord.is_valid() {
678 return Err(Error::invalid_coordinate(
679 "Coordinate contains non-finite values",
680 ));
681 }
682
683 if (t1 - t0).abs() < f64::EPSILON {
686 return Ok(*coord);
687 }
688
689 let lat_rad = coord.y.to_radians();
692 let lon_rad = coord.x.to_radians();
693
694 let ellipsoid = crate::datum_transform::Ellipsoid::GRS80;
696
697 let bw_t1 = params.params_at_epoch(t1, *ref_epoch);
705 let bw_t0 = params.params_at_epoch(t0, *ref_epoch);
706
707 let net_bw = crate::datum_transform::BursaWolfParams {
710 tx: bw_t1.tx - bw_t0.tx,
711 ty: bw_t1.ty - bw_t0.ty,
712 tz: bw_t1.tz - bw_t0.tz,
713 rx: bw_t1.rx - bw_t0.rx,
714 ry: bw_t1.ry - bw_t0.ry,
715 rz: bw_t1.rz - bw_t0.rz,
716 ds: bw_t1.ds - bw_t0.ds,
717 };
718
719 let (lat_out_rad, lon_out_rad, h_out) =
722 net_bw.transform_geodetic(lat_rad, lon_rad, coord.z, &ellipsoid, &ellipsoid);
723
724 return Ok(Coordinate3D::new(
725 lon_out_rad.to_degrees(),
726 lat_out_rad.to_degrees(),
727 h_out,
728 ));
729 }
730
731 if let (
733 CrsSource::Compound {
734 horizontal: src_h,
735 vertical: src_v,
736 },
737 CrsSource::Compound {
738 horizontal: dst_h,
739 vertical: dst_v,
740 },
741 ) = (self.source_crs.source(), self.target_crs.source())
742 {
743 if !coord.is_valid() {
744 return Err(Error::invalid_coordinate(
745 "Coordinate contains non-finite values",
746 ));
747 }
748
749 let h_transformer = Transformer::new((**src_h).clone(), (**dst_h).clone())?;
751 let xy_2d = Coordinate::new(coord.x, coord.y);
752 let transformed_xy = h_transformer.transform(&xy_2d)?;
753
754 let z = if src_v.is_equivalent(dst_v) {
764 coord.z
765 } else {
766 use crate::geoid::{VerticalDatumKind, classify_vertical_datum};
767 let src_kind = classify_vertical_datum(src_v.name().unwrap_or(""));
768 let dst_kind = classify_vertical_datum(dst_v.name().unwrap_or(""));
769 match (src_kind, dst_kind, self.geoid.as_ref()) {
770 (
772 VerticalDatumKind::Orthometric,
773 VerticalDatumKind::Ellipsoidal,
774 Some(grid),
775 ) => grid.orthometric_to_ellipsoidal(coord.y, coord.x, coord.z),
776 (
778 VerticalDatumKind::Ellipsoidal,
779 VerticalDatumKind::Orthometric,
780 Some(grid),
781 ) => grid.ellipsoidal_to_orthometric(coord.y, coord.x, coord.z),
782 _ => coord.z,
784 }
785 };
786
787 return Ok(Coordinate3D::new(transformed_xy.x, transformed_xy.y, z));
788 }
789
790 if self.proj.is_none() {
791 return Ok(*coord);
792 }
793
794 if !coord.is_valid() {
795 return Err(Error::invalid_coordinate(
796 "Coordinate contains non-finite values",
797 ));
798 }
799
800 let coord_2d = coord.to_2d();
802 let transformed_2d = self.transform_impl(&coord_2d)?;
803
804 Ok(Coordinate3D::new(
806 transformed_2d.x,
807 transformed_2d.y,
808 coord.z,
809 ))
810 }
811
812 pub fn transform_batch(&self, coords: &[Coordinate]) -> Result<Vec<Coordinate>> {
827 if self.area_of_use_check != AreaOfUseCheck::Off {
832 for c in coords {
833 self.check_area_of_use(c.x, c.y)?;
834 }
835 }
836 if let Some(result) = self.try_simd_batch(coords) {
837 return result;
838 }
839 coords.iter().map(|c| self.transform(c)).collect()
840 }
841
842 fn try_simd_batch(&self, coords: &[Coordinate]) -> Option<Result<Vec<Coordinate>>> {
848 if coords.is_empty() {
849 return Some(Ok(Vec::new()));
850 }
851
852 if !self.source_crs.is_geographic() || !self.target_crs.is_projected() {
855 return None;
856 }
857
858 let proj_str = match self.target_crs.to_proj_string() {
860 Ok(s) => s,
861 Err(_) => return None,
862 };
863
864 let parsed = match ProjString::parse(&proj_str) {
865 Ok(p) => p,
866 Err(_) => return None,
867 };
868
869 let proj_type = parsed.proj()?;
870
871 match proj_type {
872 "tmerc" | "utm" => Some(self.simd_tmerc_forward(coords, &parsed)),
873 "merc" => Some(self.simd_merc_forward(coords, &parsed)),
874 "lcc" => Some(self.simd_lcc_forward(coords, &parsed)),
875 _ => None,
876 }
877 }
878
879 fn simd_tmerc_forward(
881 &self,
882 coords: &[Coordinate],
883 parsed: &ProjString,
884 ) -> Result<Vec<Coordinate>> {
885 use simd::{WGS84_A, WGS84_E2, tmerc_forward_batch};
886
887 let proj_type = parsed.proj().unwrap_or("tmerc");
890
891 let (lon0_rad, k0, false_easting, false_northing, a, e2) = if proj_type == "utm" {
892 let zone = parsed.zone().unwrap_or(32) as f64;
894 let lon0_deg = zone * 6.0 - 183.0;
895 let false_northing = if parsed.has("south") {
896 10_000_000.0
897 } else {
898 0.0
899 };
900 (
901 lon0_deg.to_radians(),
902 0.9996,
903 500_000.0,
904 false_northing,
905 WGS84_A,
906 WGS84_E2,
907 )
908 } else {
909 let lon0_deg = parsed
911 .get("lon_0")
912 .and_then(|s| s.parse::<f64>().ok())
913 .unwrap_or(0.0);
914 let k0 = parsed
915 .get("k")
916 .or_else(|| parsed.get("k_0"))
917 .and_then(|s| s.parse::<f64>().ok())
918 .unwrap_or(1.0);
919 let fe = parsed
920 .get("x_0")
921 .and_then(|s| s.parse::<f64>().ok())
922 .unwrap_or(0.0);
923 let fn_ = parsed
924 .get("y_0")
925 .and_then(|s| s.parse::<f64>().ok())
926 .unwrap_or(0.0);
927
928 let (a, e2) = parse_ellipsoid(parsed);
930 (lon0_deg.to_radians(), k0, fe, fn_, a, e2)
931 };
932
933 let lons: Vec<f64> = coords.iter().map(|c| c.x.to_radians()).collect();
935 let lats: Vec<f64> = coords.iter().map(|c| c.y.to_radians()).collect();
936
937 let (xs, ys) = tmerc_forward_batch(
938 &lons,
939 &lats,
940 k0,
941 lon0_rad,
942 false_easting,
943 false_northing,
944 a,
945 e2,
946 );
947
948 let result: Vec<Coordinate> = xs
949 .into_iter()
950 .zip(ys)
951 .map(|(x, y)| {
952 let c = Coordinate::new(x, y);
953 if c.is_valid() {
954 Ok(c)
955 } else {
956 Err(Error::transformation_error(
957 "tmerc_batch: non-finite result",
958 ))
959 }
960 })
961 .collect::<Result<Vec<_>>>()?;
962
963 Ok(result)
964 }
965
966 fn simd_merc_forward(
968 &self,
969 coords: &[Coordinate],
970 parsed: &ProjString,
971 ) -> Result<Vec<Coordinate>> {
972 use simd::{WGS84_A, WGS84_E, merc_forward_batch};
973
974 let lon0_deg = parsed
975 .get("lon_0")
976 .and_then(|s| s.parse::<f64>().ok())
977 .unwrap_or(0.0);
978 let k0 = parsed
979 .get("k")
980 .or_else(|| parsed.get("k_0"))
981 .and_then(|s| s.parse::<f64>().ok())
982 .unwrap_or(1.0);
983
984 let (a, e2) = parse_ellipsoid(parsed);
985 let e = e2.sqrt();
986
987 let lons: Vec<f64> = coords.iter().map(|c| c.x.to_radians()).collect();
988 let lats: Vec<f64> = coords.iter().map(|c| c.y.to_radians()).collect();
989
990 let e_eff = if e < 1e-10 { 0.0 } else { e };
993 let _ = (WGS84_E, WGS84_A); let (xs, ys) = merc_forward_batch(&lons, &lats, lon0_deg.to_radians(), k0, a, e_eff);
996
997 let result: Vec<Coordinate> = xs
998 .into_iter()
999 .zip(ys)
1000 .map(|(x, y)| {
1001 let c = Coordinate::new(x, y);
1002 if c.is_valid() {
1003 Ok(c)
1004 } else {
1005 Err(Error::transformation_error("merc_batch: non-finite result"))
1006 }
1007 })
1008 .collect::<Result<Vec<_>>>()?;
1009
1010 Ok(result)
1011 }
1012
1013 fn simd_lcc_forward(
1015 &self,
1016 coords: &[Coordinate],
1017 parsed: &ProjString,
1018 ) -> Result<Vec<Coordinate>> {
1019 use simd::{WGS84_A, lcc_cone_params, lcc_forward_batch};
1020
1021 let lon0_deg = parsed
1022 .get("lon_0")
1023 .and_then(|s| s.parse::<f64>().ok())
1024 .unwrap_or(0.0);
1025 let lat0_deg = parsed
1026 .get("lat_0")
1027 .and_then(|s| s.parse::<f64>().ok())
1028 .unwrap_or(0.0);
1029 let lat1_deg = parsed
1031 .get("lat_1")
1032 .and_then(|s| s.parse::<f64>().ok())
1033 .unwrap_or(lat0_deg);
1034 let lat2_deg = parsed
1035 .get("lat_2")
1036 .and_then(|s| s.parse::<f64>().ok())
1037 .unwrap_or(lat1_deg);
1038 let fe = parsed
1039 .get("x_0")
1040 .and_then(|s| s.parse::<f64>().ok())
1041 .unwrap_or(0.0);
1042 let fn_ = parsed
1043 .get("y_0")
1044 .and_then(|s| s.parse::<f64>().ok())
1045 .unwrap_or(0.0);
1046
1047 let (a, _e2) = parse_ellipsoid(parsed);
1048 let _ = WGS84_A; let lat0_rad = lat0_deg.to_radians();
1051 let lat1_rad = lat1_deg.to_radians();
1052 let lat2_rad = lat2_deg.to_radians();
1053
1054 let (n, big_f, rho0_normalised) = match lcc_cone_params(lat0_rad, lat1_rad, lat2_rad) {
1055 Some(p) => p,
1056 None => {
1057 return Err(Error::projection_init_error(
1058 "lcc_batch: degenerate cone constant",
1059 ));
1060 }
1061 };
1062 let rho0 = rho0_normalised * a;
1063
1064 let lons: Vec<f64> = coords.iter().map(|c| c.x.to_radians()).collect();
1065 let lats: Vec<f64> = coords.iter().map(|c| c.y.to_radians()).collect();
1066
1067 let (xs, ys) = lcc_forward_batch(
1068 &lons,
1069 &lats,
1070 n,
1071 big_f,
1072 rho0,
1073 lon0_deg.to_radians(),
1074 fe,
1075 fn_,
1076 a,
1077 );
1078
1079 let result: Vec<Coordinate> = xs
1080 .into_iter()
1081 .zip(ys)
1082 .map(|(x, y)| {
1083 let c = Coordinate::new(x, y);
1084 if c.is_valid() {
1085 Ok(c)
1086 } else {
1087 Err(Error::transformation_error("lcc_batch: non-finite result"))
1088 }
1089 })
1090 .collect::<Result<Vec<_>>>()?;
1091
1092 Ok(result)
1093 }
1094
1095 pub fn transform_bbox(&self, bbox: &BoundingBox) -> Result<BoundingBox> {
1107 if self.proj.is_none() {
1108 return Ok(*bbox);
1109 }
1110
1111 let corners = bbox.corners();
1113 let transformed_corners = self.transform_batch(&corners)?;
1114
1115 let mut min_x = f64::INFINITY;
1117 let mut min_y = f64::INFINITY;
1118 let mut max_x = f64::NEG_INFINITY;
1119 let mut max_y = f64::NEG_INFINITY;
1120
1121 for corner in &transformed_corners {
1122 min_x = min_x.min(corner.x);
1123 min_y = min_y.min(corner.y);
1124 max_x = max_x.max(corner.x);
1125 max_y = max_y.max(corner.y);
1126 }
1127
1128 BoundingBox::new(min_x, min_y, max_x, max_y)
1129 }
1130
1131 fn transform_impl(&self, coord: &Coordinate) -> Result<Coordinate> {
1133 let source_proj_str = self.source_crs.to_proj_string()?;
1134 let target_proj_str = self.target_crs.to_proj_string()?;
1135
1136 let source_proj = proj4rs::Proj::from_proj_string(&source_proj_str)
1137 .map_err(|e| Error::from_proj4rs(format!("{:?}", e)))?;
1138
1139 let target_proj = proj4rs::Proj::from_proj_string(&target_proj_str)
1140 .map_err(|e| Error::from_proj4rs(format!("{:?}", e)))?;
1141
1142 let mut x = coord.x;
1144 let mut y = coord.y;
1145
1146 if self.source_crs.is_geographic() {
1147 x = x.to_radians();
1148 y = y.to_radians();
1149 }
1150
1151 let mut points = [(x, y)];
1153 proj4rs::transform::transform(&source_proj, &target_proj, &mut points[..])
1154 .map_err(|e| Error::transformation_error(format!("{:?}", e)))?;
1155
1156 let (mut result_x, mut result_y) = points[0];
1157
1158 if self.target_crs.is_geographic() {
1160 result_x = result_x.to_degrees();
1161 result_y = result_y.to_degrees();
1162 }
1163
1164 let transformed = Coordinate::new(result_x, result_y);
1165
1166 if !transformed.is_valid() {
1167 return Err(Error::transformation_error(
1168 "Transformation resulted in non-finite values",
1169 ));
1170 }
1171
1172 Ok(transformed)
1173 }
1174}
1175
1176#[cfg(feature = "std")]
1186fn parse_ellipsoid(parsed: &ProjString) -> (f64, f64) {
1187 use simd::{WGS84_A, WGS84_E2};
1188
1189 if let Some(a_val) = parsed.get("a").and_then(|s| s.parse::<f64>().ok()) {
1191 if let Some(b_val) = parsed.get("b").and_then(|s| s.parse::<f64>().ok()) {
1193 let f = 1.0 - b_val / a_val;
1194 let e2 = 2.0 * f - f * f;
1195 return (a_val, e2);
1196 }
1197 if let Some(f_val) = parsed.get("f").and_then(|s| s.parse::<f64>().ok()) {
1199 let e2 = 2.0 * f_val - f_val * f_val;
1200 return (a_val, e2);
1201 }
1202 if let Some(rf) = parsed.get("rf").and_then(|s| s.parse::<f64>().ok()) {
1204 let f = 1.0 / rf;
1205 let e2 = 2.0 * f - f * f;
1206 return (a_val, e2);
1207 }
1208 return (a_val, 0.0);
1210 }
1211
1212 if let Some(ellps) = parsed.get("ellps") {
1214 match ellps {
1215 "WGS84" | "wgs84" => return (WGS84_A, WGS84_E2),
1216 "GRS80" | "grs80" => {
1217 let a = 6_378_137.0_f64;
1219 let f = 1.0_f64 / 298.257_222_101;
1220 let e2 = 2.0 * f - f * f;
1221 return (a, e2);
1222 }
1223 "bessel" => {
1224 let a = 6_377_397.155_f64;
1226 let f = 1.0_f64 / 299.152_812_8;
1227 let e2 = 2.0 * f - f * f;
1228 return (a, e2);
1229 }
1230 "airy" => {
1231 let a = 6_377_563.396_f64;
1233 let b = 6_356_256.910_f64;
1234 let f = 1.0 - b / a;
1235 let e2 = 2.0 * f - f * f;
1236 return (a, e2);
1237 }
1238 _ => {}
1239 }
1240 }
1241
1242 if let Some(datum) = parsed.get("datum") {
1244 match datum {
1245 "WGS84" | "wgs84" => return (WGS84_A, WGS84_E2),
1246 "NAD83" | "nad83" => {
1247 let a = 6_378_137.0_f64;
1249 let f = 1.0_f64 / 298.257_222_101;
1250 let e2 = 2.0 * f - f * f;
1251 return (a, e2);
1252 }
1253 _ => {}
1254 }
1255 }
1256
1257 (WGS84_A, WGS84_E2)
1259}
1260
1261#[cfg(feature = "std")]
1263pub fn transform_coordinate(
1274 coord: &Coordinate,
1275 source_crs: &Crs,
1276 target_crs: &Crs,
1277) -> Result<Coordinate> {
1278 let transformer = Transformer::new(source_crs.clone(), target_crs.clone())?;
1279 transformer.transform(coord)
1280}
1281
1282#[cfg(feature = "std")]
1284pub fn transform_epsg(
1295 coord: &Coordinate,
1296 source_epsg: u32,
1297 target_epsg: u32,
1298) -> Result<Coordinate> {
1299 let transformer = Transformer::from_epsg(source_epsg, target_epsg)?;
1300 transformer.transform(coord)
1301}
1302
1303#[cfg(test)]
1304#[allow(clippy::expect_used)]
1305mod tests {
1306 use super::*;
1307 use approx::assert_relative_eq;
1308
1309 #[test]
1310 fn test_coordinate_creation() {
1311 let coord = Coordinate::new(10.0, 20.0);
1312 assert_eq!(coord.x, 10.0);
1313 assert_eq!(coord.y, 20.0);
1314 }
1315
1316 #[test]
1317 fn test_coordinate_from_lon_lat() {
1318 let coord = Coordinate::from_lon_lat(-122.4194, 37.7749);
1319 assert_eq!(coord.lon(), -122.4194);
1320 assert_eq!(coord.lat(), 37.7749);
1321 }
1322
1323 #[test]
1324 fn test_coordinate_validation() {
1325 let valid = Coordinate::new(0.0, 0.0);
1326 assert!(valid.validate_geographic().is_ok());
1327
1328 let invalid_lon = Coordinate::new(200.0, 0.0);
1329 assert!(invalid_lon.validate_geographic().is_err());
1330
1331 let invalid_lat = Coordinate::new(0.0, 100.0);
1332 assert!(invalid_lat.validate_geographic().is_err());
1333 }
1334
1335 #[test]
1336 fn test_coordinate_is_valid() {
1337 let valid = Coordinate::new(1.0, 2.0);
1338 assert!(valid.is_valid());
1339
1340 let invalid = Coordinate::new(f64::NAN, 2.0);
1341 assert!(!invalid.is_valid());
1342
1343 let infinite = Coordinate::new(f64::INFINITY, 2.0);
1344 assert!(!infinite.is_valid());
1345 }
1346
1347 #[test]
1348 fn test_coordinate3d() {
1349 let coord = Coordinate3D::new(1.0, 2.0, 3.0);
1350 assert_eq!(coord.x, 1.0);
1351 assert_eq!(coord.y, 2.0);
1352 assert_eq!(coord.z, 3.0);
1353
1354 let coord_2d = coord.to_2d();
1355 assert_eq!(coord_2d.x, 1.0);
1356 assert_eq!(coord_2d.y, 2.0);
1357 }
1358
1359 #[test]
1360 fn test_bounding_box() {
1361 let bbox = BoundingBox::new(0.0, 0.0, 10.0, 20.0);
1362 assert!(bbox.is_ok());
1363
1364 let bbox = bbox.expect("should be valid");
1365 assert_eq!(bbox.width(), 10.0);
1366 assert_eq!(bbox.height(), 20.0);
1367
1368 let center = bbox.center();
1369 assert_eq!(center.x, 5.0);
1370 assert_eq!(center.y, 10.0);
1371 }
1372
1373 #[test]
1374 fn test_bounding_box_invalid() {
1375 let result = BoundingBox::new(10.0, 0.0, 0.0, 20.0);
1376 assert!(result.is_err());
1377
1378 let result = BoundingBox::new(0.0, 20.0, 10.0, 0.0);
1379 assert!(result.is_err());
1380 }
1381
1382 #[test]
1383 fn test_bounding_box_contains() {
1384 let bbox = BoundingBox::new(0.0, 0.0, 10.0, 10.0).expect("valid bbox");
1385
1386 assert!(bbox.contains(&Coordinate::new(5.0, 5.0)));
1387 assert!(bbox.contains(&Coordinate::new(0.0, 0.0)));
1388 assert!(bbox.contains(&Coordinate::new(10.0, 10.0)));
1389 assert!(!bbox.contains(&Coordinate::new(-1.0, 5.0)));
1390 assert!(!bbox.contains(&Coordinate::new(5.0, 11.0)));
1391 }
1392
1393 #[test]
1394 fn test_bounding_box_expand() {
1395 let mut bbox = BoundingBox::new(0.0, 0.0, 10.0, 10.0).expect("valid bbox");
1396
1397 bbox.expand_to_include(&Coordinate::new(15.0, 5.0));
1398 assert_eq!(bbox.max_x, 15.0);
1399
1400 bbox.expand_to_include(&Coordinate::new(5.0, -5.0));
1401 assert_eq!(bbox.min_y, -5.0);
1402 }
1403
1404 #[test]
1405 fn test_transformer_same_crs() {
1406 let wgs84 = Crs::wgs84();
1407 let transformer = Transformer::new(wgs84.clone(), wgs84.clone());
1408 assert!(transformer.is_ok());
1409
1410 let transformer = transformer.expect("should create transformer");
1411 let coord = Coordinate::new(10.0, 20.0);
1412 let result = transformer.transform(&coord);
1413 assert!(result.is_ok());
1414
1415 let result = result.expect("should transform");
1416 assert_eq!(result, coord);
1417 }
1418
1419 #[test]
1420 fn test_transformer_wgs84_to_web_mercator() {
1421 let transformer = Transformer::from_epsg(4326, 3857);
1422 assert!(transformer.is_ok());
1423
1424 let transformer = transformer.expect("should create transformer");
1425
1426 let london = Coordinate::from_lon_lat(0.0, 51.5);
1428 let result = transformer.transform(&london);
1429 assert!(result.is_ok());
1430
1431 let result = result.expect("should transform");
1432 assert_relative_eq!(result.x, 0.0, epsilon = 1.0);
1435 assert!(result.y > 6_000_000.0 && result.y < 7_000_000.0);
1437 }
1438
1439 #[test]
1440 fn test_transform_batch() {
1441 let transformer = Transformer::from_epsg(4326, 4326).expect("same CRS");
1442
1443 let coords = vec![
1444 Coordinate::new(0.0, 0.0),
1445 Coordinate::new(10.0, 10.0),
1446 Coordinate::new(20.0, 20.0),
1447 ];
1448
1449 let result = transformer.transform_batch(&coords);
1450 assert!(result.is_ok());
1451
1452 let result = result.expect("should transform");
1453 assert_eq!(result.len(), 3);
1454 assert_eq!(result[0], coords[0]);
1455 assert_eq!(result[1], coords[1]);
1456 assert_eq!(result[2], coords[2]);
1457 }
1458
1459 #[test]
1460 fn test_transform_bbox() {
1461 let transformer = Transformer::from_epsg(4326, 4326).expect("same CRS");
1462
1463 let bbox = BoundingBox::new(0.0, 0.0, 10.0, 10.0).expect("valid bbox");
1464 let result = transformer.transform_bbox(&bbox);
1465 assert!(result.is_ok());
1466
1467 let result = result.expect("should transform");
1468 assert_eq!(result, bbox);
1469 }
1470
1471 #[test]
1472 fn test_convenience_functions() {
1473 let wgs84 = Crs::wgs84();
1474 let coord = Coordinate::new(0.0, 0.0);
1475
1476 let result = transform_coordinate(&coord, &wgs84, &wgs84);
1477 assert!(result.is_ok());
1478 assert_eq!(result.expect("should transform"), coord);
1479
1480 let result = transform_epsg(&coord, 4326, 4326);
1481 assert!(result.is_ok());
1482 assert_eq!(result.expect("should transform"), coord);
1483 }
1484
1485 #[test]
1486 fn test_transform_invalid_coordinate() {
1487 let transformer = Transformer::from_epsg(4326, 3857).expect("should create");
1488
1489 let invalid = Coordinate::new(f64::NAN, 0.0);
1490 let result = transformer.transform(&invalid);
1491 assert!(result.is_err());
1492 }
1493
1494 fn make_compound_wgs84_egm96() -> crate::crs::Crs {
1503 let horiz = Crs::wgs84(); let vert_wkt = r#"VERTCRS["EGM96 height",VDATUM["EGM96 geoid"],UNIT["metre",1]]"#;
1505 let vert = crate::crs::Crs::from_wkt(vert_wkt).expect("vert parse");
1506 crate::crs::Crs::compound(horiz, vert).expect("compound CRS should build")
1507 }
1508
1509 #[test]
1510 fn test_transform_3d_compound_same_vertical_datum_passes_z_through() {
1511 let crs = make_compound_wgs84_egm96();
1513 let transformer = Transformer::new(crs.clone(), crs).expect("same-CRS transformer");
1514 let input = Coordinate3D::new(13.4050, 52.5200, 34.567);
1515 let output = transformer
1516 .transform_3d(&input)
1517 .expect("transform should succeed");
1518 assert!((output.x - input.x).abs() < 1e-9, "x should be unchanged");
1520 assert!((output.y - input.y).abs() < 1e-9, "y should be unchanged");
1521 assert!(
1523 (output.z - input.z).abs() < 1e-9,
1524 "z should be passed through"
1525 );
1526 }
1527
1528 #[test]
1529 fn test_transform_3d_compound_different_vertical_datum_silently_passes_through_when_no_geoid() {
1530 let horiz = Crs::wgs84(); let vert1_wkt = r#"VERTCRS["EGM96 height",VDATUM["EGM96 geoid"],UNIT["metre",1]]"#;
1538 let vert2_wkt = r#"VERTCRS["EGM2008 height",VDATUM["EGM2008 geoid"],UNIT["metre",1]]"#;
1539
1540 let vert1 = Crs::from_wkt(vert1_wkt).expect("vert1 parse");
1541 let vert2 = Crs::from_wkt(vert2_wkt).expect("vert2 parse");
1542
1543 let crs1 = Crs::compound(horiz.clone(), vert1).expect("compound crs1");
1544 let crs2 = Crs::compound(horiz, vert2).expect("compound crs2");
1545
1546 let transformer = Transformer::new(crs1, crs2).expect("different-vertical transformer");
1547 let input = Coordinate3D::new(0.0, 51.5, 50.0);
1548 let output = transformer
1549 .transform_3d(&input)
1550 .expect("must succeed without geoid (silent passthrough)");
1551 assert!((output.x - input.x).abs() < 1e-9);
1552 assert!((output.y - input.y).abs() < 1e-9);
1553 assert!(
1554 (output.z - input.z).abs() < 1e-9,
1555 "z must pass through when no geoid attached"
1556 );
1557 }
1558
1559 #[test]
1560 fn test_transform_3d_simple_non_compound_crs_unaffected() {
1561 let transformer = Transformer::from_epsg(4326, 4326).expect("same EPSG");
1563 let input = Coordinate3D::new(10.0, 50.0, 100.0);
1564 let output = transformer.transform_3d(&input).expect("should transform");
1565 assert!((output.x - input.x).abs() < 1e-9);
1566 assert!((output.y - input.y).abs() < 1e-9);
1567 assert!((output.z - input.z).abs() < 1e-9);
1568 }
1569
1570 #[test]
1571 fn test_transform_3d_compound_to_horizontal_still_works() {
1572 let non_compound = Crs::wgs84();
1576 let transformer = Transformer::new(non_compound.clone(), non_compound).expect("same CRS");
1577 let input = Coordinate3D::new(5.0, 45.0, 200.0);
1578 let output = transformer.transform_3d(&input).expect("should transform");
1579 assert!((output.x - input.x).abs() < 1e-9);
1580 assert!((output.y - input.y).abs() < 1e-9);
1581 assert!((output.z - input.z).abs() < 1e-9);
1582 }
1583}