1use crate::datum::Datum;
2use crate::error::{Error, Result};
3
4#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct LinearUnit {
9 meters_per_unit: f64,
10}
11
12impl LinearUnit {
13 pub const fn metre() -> Self {
15 Self {
16 meters_per_unit: 1.0,
17 }
18 }
19
20 pub const fn meter() -> Self {
22 Self::metre()
23 }
24
25 pub const fn kilometre() -> Self {
27 Self {
28 meters_per_unit: 1000.0,
29 }
30 }
31
32 pub const fn kilometer() -> Self {
34 Self::kilometre()
35 }
36
37 pub const fn foot() -> Self {
39 Self {
40 meters_per_unit: 0.3048,
41 }
42 }
43
44 pub const fn us_survey_foot() -> Self {
46 Self {
47 meters_per_unit: 0.3048006096012192,
48 }
49 }
50
51 pub fn from_meters_per_unit(meters_per_unit: f64) -> Result<Self> {
53 if !meters_per_unit.is_finite() || meters_per_unit <= 0.0 {
54 return Err(Error::InvalidDefinition(
55 "linear unit conversion factor must be a finite positive number".into(),
56 ));
57 }
58
59 Ok(Self { meters_per_unit })
60 }
61
62 pub const fn meters_per_unit(self) -> f64 {
64 self.meters_per_unit
65 }
66
67 pub const fn to_meters(self, value: f64) -> f64 {
69 value * self.meters_per_unit
70 }
71
72 pub const fn from_meters(self, value: f64) -> f64 {
74 value / self.meters_per_unit
75 }
76}
77
78#[derive(Debug, Clone)]
80pub enum CrsDef {
81 Geographic(GeographicCrsDef),
83 Projected(ProjectedCrsDef),
85 Compound(Box<CompoundCrsDef>),
87}
88
89impl CrsDef {
90 pub fn datum(&self) -> &Datum {
92 match self {
93 CrsDef::Geographic(g) => g.datum(),
94 CrsDef::Projected(p) => p.datum(),
95 CrsDef::Compound(c) => c.horizontal_datum(),
96 }
97 }
98
99 pub fn epsg(&self) -> u32 {
101 match self {
102 CrsDef::Geographic(g) => g.epsg(),
103 CrsDef::Projected(p) => p.epsg(),
104 CrsDef::Compound(c) => c.epsg(),
105 }
106 }
107
108 pub fn name(&self) -> &str {
110 match self {
111 CrsDef::Geographic(g) => g.name(),
112 CrsDef::Projected(p) => p.name(),
113 CrsDef::Compound(c) => c.name(),
114 }
115 }
116
117 pub fn is_geographic(&self) -> bool {
119 self.as_geographic().is_some()
120 }
121
122 pub fn is_projected(&self) -> bool {
124 self.as_projected().is_some()
125 }
126
127 pub fn is_compound(&self) -> bool {
129 matches!(self, CrsDef::Compound(_))
130 }
131
132 pub fn as_geographic(&self) -> Option<&GeographicCrsDef> {
134 match self {
135 CrsDef::Geographic(g) => Some(g),
136 CrsDef::Projected(_) => None,
137 CrsDef::Compound(c) => c.as_geographic(),
138 }
139 }
140
141 pub fn as_projected(&self) -> Option<&ProjectedCrsDef> {
143 match self {
144 CrsDef::Geographic(_) => None,
145 CrsDef::Projected(p) => Some(p),
146 CrsDef::Compound(c) => c.as_projected(),
147 }
148 }
149
150 pub fn vertical_crs(&self) -> Option<&VerticalCrsDef> {
152 match self {
153 CrsDef::Compound(c) => Some(c.vertical_crs()),
154 CrsDef::Geographic(_) | CrsDef::Projected(_) => None,
155 }
156 }
157
158 pub fn horizontal_crs(&self) -> Option<CrsDef> {
164 match self {
165 CrsDef::Geographic(_) | CrsDef::Projected(_) => Some(self.clone()),
166 CrsDef::Compound(c) => Some(c.horizontal().to_crs_def()),
167 }
168 }
169
170 pub fn base_geographic_crs_epsg(&self) -> Option<u32> {
172 match self {
173 CrsDef::Geographic(g) if g.epsg() != 0 => Some(g.epsg()),
174 CrsDef::Projected(p) if p.base_geographic_crs_epsg() != 0 => {
175 Some(p.base_geographic_crs_epsg())
176 }
177 CrsDef::Compound(c) => c.base_geographic_crs_epsg(),
178 _ => None,
179 }
180 }
181
182 pub fn semantically_equivalent(&self, other: &Self) -> bool {
184 match (self, other) {
185 (CrsDef::Geographic(a), CrsDef::Geographic(b)) => a.datum().same_datum(b.datum()),
186 (CrsDef::Projected(a), CrsDef::Projected(b)) => {
187 a.datum().same_datum(b.datum())
188 && approx_eq(a.linear_unit_to_meter(), b.linear_unit_to_meter())
189 && projection_methods_equivalent(&a.method(), &b.method())
190 }
191 (CrsDef::Compound(a), CrsDef::Compound(b)) => a.semantically_equivalent(b),
192 _ => false,
193 }
194 }
195}
196
197#[derive(Debug, Clone)]
199pub struct GeographicCrsDef {
200 epsg: u32,
201 datum: Datum,
202 name: &'static str,
203}
204
205impl GeographicCrsDef {
206 pub const fn new(epsg: u32, datum: Datum, name: &'static str) -> Self {
207 Self { epsg, datum, name }
208 }
209
210 pub const fn epsg(&self) -> u32 {
211 self.epsg
212 }
213
214 pub const fn datum(&self) -> &Datum {
215 &self.datum
216 }
217
218 pub const fn name(&self) -> &'static str {
219 self.name
220 }
221}
222
223#[derive(Debug, Clone)]
225pub struct ProjectedCrsDef {
226 epsg: u32,
227 base_geographic_crs_epsg: u32,
228 datum: Datum,
229 method: ProjectionMethod,
230 linear_unit: LinearUnit,
231 name: &'static str,
232}
233
234impl ProjectedCrsDef {
235 pub const fn new(
236 epsg: u32,
237 datum: Datum,
238 method: ProjectionMethod,
239 linear_unit: LinearUnit,
240 name: &'static str,
241 ) -> Self {
242 Self::new_with_base_geographic_crs(epsg, 0, datum, method, linear_unit, name)
243 }
244
245 pub const fn new_with_base_geographic_crs(
246 epsg: u32,
247 base_geographic_crs_epsg: u32,
248 datum: Datum,
249 method: ProjectionMethod,
250 linear_unit: LinearUnit,
251 name: &'static str,
252 ) -> Self {
253 Self {
254 epsg,
255 base_geographic_crs_epsg,
256 datum,
257 method,
258 linear_unit,
259 name,
260 }
261 }
262
263 pub const fn epsg(&self) -> u32 {
264 self.epsg
265 }
266
267 pub const fn datum(&self) -> &Datum {
268 &self.datum
269 }
270
271 pub const fn base_geographic_crs_epsg(&self) -> u32 {
272 self.base_geographic_crs_epsg
273 }
274
275 pub const fn method(&self) -> ProjectionMethod {
276 self.method
277 }
278
279 pub const fn linear_unit(&self) -> LinearUnit {
280 self.linear_unit
281 }
282
283 pub const fn linear_unit_to_meter(&self) -> f64 {
284 self.linear_unit.meters_per_unit()
285 }
286
287 pub const fn name(&self) -> &'static str {
288 self.name
289 }
290}
291
292#[derive(Debug, Clone)]
294pub struct CompoundCrsDef {
295 epsg: u32,
296 horizontal: HorizontalCrsDef,
297 vertical: VerticalCrsDef,
298 name: &'static str,
299}
300
301impl CompoundCrsDef {
302 pub fn new(
303 epsg: u32,
304 horizontal: HorizontalCrsDef,
305 vertical: VerticalCrsDef,
306 name: &'static str,
307 ) -> Self {
308 Self {
309 epsg,
310 horizontal,
311 vertical,
312 name,
313 }
314 }
315
316 pub fn from_crs_def(
317 epsg: u32,
318 horizontal: CrsDef,
319 vertical: VerticalCrsDef,
320 name: &'static str,
321 ) -> Result<Self> {
322 let horizontal = HorizontalCrsDef::try_from(horizontal)?;
323 Ok(Self::new(epsg, horizontal, vertical, name))
324 }
325
326 pub const fn epsg(&self) -> u32 {
327 self.epsg
328 }
329
330 pub const fn horizontal(&self) -> &HorizontalCrsDef {
331 &self.horizontal
332 }
333
334 pub const fn vertical_crs(&self) -> &VerticalCrsDef {
335 &self.vertical
336 }
337
338 pub const fn name(&self) -> &'static str {
339 self.name
340 }
341
342 pub fn as_geographic(&self) -> Option<&GeographicCrsDef> {
343 self.horizontal.as_geographic()
344 }
345
346 pub fn as_projected(&self) -> Option<&ProjectedCrsDef> {
347 self.horizontal.as_projected()
348 }
349
350 pub fn horizontal_datum(&self) -> &Datum {
351 self.horizontal.datum()
352 }
353
354 pub fn base_geographic_crs_epsg(&self) -> Option<u32> {
355 self.horizontal.base_geographic_crs_epsg()
356 }
357
358 pub fn semantically_equivalent(&self, other: &Self) -> bool {
359 self.horizontal.semantically_equivalent(&other.horizontal)
360 && self.vertical.semantically_equivalent(&other.vertical)
361 }
362}
363
364#[derive(Debug, Clone)]
366pub enum HorizontalCrsDef {
367 Geographic(GeographicCrsDef),
368 Projected(ProjectedCrsDef),
369}
370
371impl HorizontalCrsDef {
372 pub fn datum(&self) -> &Datum {
373 match self {
374 Self::Geographic(g) => g.datum(),
375 Self::Projected(p) => p.datum(),
376 }
377 }
378
379 pub fn epsg(&self) -> u32 {
380 match self {
381 Self::Geographic(g) => g.epsg(),
382 Self::Projected(p) => p.epsg(),
383 }
384 }
385
386 pub fn name(&self) -> &str {
387 match self {
388 Self::Geographic(g) => g.name(),
389 Self::Projected(p) => p.name(),
390 }
391 }
392
393 pub fn as_geographic(&self) -> Option<&GeographicCrsDef> {
394 match self {
395 Self::Geographic(g) => Some(g),
396 Self::Projected(_) => None,
397 }
398 }
399
400 pub fn as_projected(&self) -> Option<&ProjectedCrsDef> {
401 match self {
402 Self::Geographic(_) => None,
403 Self::Projected(p) => Some(p),
404 }
405 }
406
407 pub fn base_geographic_crs_epsg(&self) -> Option<u32> {
408 match self {
409 Self::Geographic(g) if g.epsg() != 0 => Some(g.epsg()),
410 Self::Projected(p) if p.base_geographic_crs_epsg() != 0 => {
411 Some(p.base_geographic_crs_epsg())
412 }
413 _ => None,
414 }
415 }
416
417 pub fn semantically_equivalent(&self, other: &Self) -> bool {
418 match (self, other) {
419 (Self::Geographic(a), Self::Geographic(b)) => a.datum().same_datum(b.datum()),
420 (Self::Projected(a), Self::Projected(b)) => {
421 a.datum().same_datum(b.datum())
422 && approx_eq(a.linear_unit_to_meter(), b.linear_unit_to_meter())
423 && projection_methods_equivalent(&a.method(), &b.method())
424 }
425 _ => false,
426 }
427 }
428
429 pub fn to_crs_def(&self) -> CrsDef {
430 match self {
431 Self::Geographic(g) => CrsDef::Geographic(g.clone()),
432 Self::Projected(p) => CrsDef::Projected(p.clone()),
433 }
434 }
435}
436
437impl TryFrom<CrsDef> for HorizontalCrsDef {
438 type Error = Error;
439
440 fn try_from(value: CrsDef) -> Result<Self> {
441 match value {
442 CrsDef::Geographic(g) => Ok(Self::Geographic(g)),
443 CrsDef::Projected(p) => Ok(Self::Projected(p)),
444 CrsDef::Compound(_) => Err(Error::InvalidDefinition(
445 "compound CRS horizontal component cannot itself be compound".into(),
446 )),
447 }
448 }
449}
450
451impl From<GeographicCrsDef> for HorizontalCrsDef {
452 fn from(value: GeographicCrsDef) -> Self {
453 Self::Geographic(value)
454 }
455}
456
457impl From<ProjectedCrsDef> for HorizontalCrsDef {
458 fn from(value: ProjectedCrsDef) -> Self {
459 Self::Projected(value)
460 }
461}
462
463#[derive(Debug, Clone)]
465pub struct VerticalCrsDef {
466 epsg: u32,
467 kind: VerticalCrsKind,
468 linear_unit: LinearUnit,
469 name: &'static str,
470}
471
472impl VerticalCrsDef {
473 pub fn ellipsoidal_height(
475 epsg: u32,
476 datum: Datum,
477 linear_unit: LinearUnit,
478 name: &'static str,
479 ) -> Self {
480 Self {
481 epsg,
482 kind: VerticalCrsKind::EllipsoidalHeight {
483 datum: Box::new(datum),
484 },
485 linear_unit,
486 name,
487 }
488 }
489
490 pub fn gravity_related_height(
492 epsg: u32,
493 vertical_datum_epsg: u32,
494 linear_unit: LinearUnit,
495 name: &'static str,
496 ) -> Result<Self> {
497 if vertical_datum_epsg == 0 {
498 return Err(Error::InvalidDefinition(
499 "gravity-related vertical CRS requires a vertical datum EPSG code".into(),
500 ));
501 }
502
503 Ok(Self {
504 epsg,
505 kind: VerticalCrsKind::GravityRelatedHeight {
506 vertical_datum_epsg,
507 },
508 linear_unit,
509 name,
510 })
511 }
512
513 pub const fn epsg(&self) -> u32 {
514 self.epsg
515 }
516
517 pub const fn kind(&self) -> &VerticalCrsKind {
518 &self.kind
519 }
520
521 pub const fn linear_unit(&self) -> LinearUnit {
522 self.linear_unit
523 }
524
525 pub const fn linear_unit_to_meter(&self) -> f64 {
526 self.linear_unit.meters_per_unit()
527 }
528
529 pub const fn name(&self) -> &'static str {
530 self.name
531 }
532
533 pub fn semantically_equivalent(&self, other: &Self) -> bool {
534 approx_eq(self.linear_unit_to_meter(), other.linear_unit_to_meter())
535 && self.kind.semantically_equivalent(&other.kind)
536 }
537
538 pub fn same_vertical_reference(&self, other: &Self) -> bool {
541 self.kind.semantically_equivalent(&other.kind)
542 }
543
544 pub fn vertical_datum_epsg(&self) -> Option<u32> {
545 self.kind.vertical_datum_epsg()
546 }
547}
548
549#[derive(Debug, Clone)]
551pub enum VerticalCrsKind {
552 EllipsoidalHeight { datum: Box<Datum> },
554 GravityRelatedHeight { vertical_datum_epsg: u32 },
556}
557
558impl VerticalCrsKind {
559 pub fn semantically_equivalent(&self, other: &Self) -> bool {
560 match (self, other) {
561 (Self::EllipsoidalHeight { datum: a }, Self::EllipsoidalHeight { datum: b }) => {
562 a.same_datum(b)
563 }
564 (
565 Self::GravityRelatedHeight {
566 vertical_datum_epsg: a,
567 },
568 Self::GravityRelatedHeight {
569 vertical_datum_epsg: b,
570 },
571 ) => a == b,
572 _ => false,
573 }
574 }
575
576 pub const fn vertical_datum_epsg(&self) -> Option<u32> {
577 match self {
578 Self::EllipsoidalHeight { .. } => None,
579 Self::GravityRelatedHeight {
580 vertical_datum_epsg,
581 } => Some(*vertical_datum_epsg),
582 }
583 }
584
585 pub const fn is_ellipsoidal_height(&self) -> bool {
586 matches!(self, Self::EllipsoidalHeight { .. })
587 }
588
589 pub const fn is_gravity_related_height(&self) -> bool {
590 matches!(self, Self::GravityRelatedHeight { .. })
591 }
592}
593
594#[derive(Debug, Clone, Copy, PartialEq)]
599pub enum ProjectionMethod {
600 WebMercator,
602
603 TransverseMercator {
605 lon0: f64,
607 lat0: f64,
609 k0: f64,
611 false_easting: f64,
613 false_northing: f64,
615 },
616
617 PolarStereographic {
619 lon0: f64,
621 lat_ts: f64,
623 k0: f64,
625 false_easting: f64,
627 false_northing: f64,
629 },
630
631 LambertConformalConic {
633 lon0: f64,
635 lat0: f64,
637 lat1: f64,
639 lat2: f64,
641 k0: f64,
643 false_easting: f64,
645 false_northing: f64,
647 },
648
649 LambertConformalConicMichigan {
652 lon0: f64,
654 lat0: f64,
656 lat1: f64,
658 lat2: f64,
660 ellipsoid_scaling_factor: f64,
662 false_easting: f64,
664 false_northing: f64,
666 },
667
668 LambertConformalConic1SPVariantB {
672 lon0: f64,
674 lat0: f64,
676 k0: f64,
678 lat_false_origin: f64,
680 false_easting: f64,
682 false_northing: f64,
684 },
685
686 AlbersEqualArea {
688 lon0: f64,
690 lat0: f64,
692 lat1: f64,
694 lat2: f64,
696 false_easting: f64,
698 false_northing: f64,
700 },
701
702 LambertAzimuthalEqualArea {
704 lon0: f64,
706 lat0: f64,
708 false_easting: f64,
710 false_northing: f64,
712 },
713
714 LambertAzimuthalEqualAreaSpherical {
716 lon0: f64,
718 lat0: f64,
720 false_easting: f64,
722 false_northing: f64,
724 },
725
726 ObliqueStereographic {
728 lon0: f64,
730 lat0: f64,
732 k0: f64,
734 false_easting: f64,
736 false_northing: f64,
738 },
739
740 HotineObliqueMercator {
742 latc: f64,
744 lonc: f64,
746 azimuth: f64,
748 rectified_grid_angle: f64,
750 k0: f64,
752 false_easting: f64,
754 false_northing: f64,
756 variant_b: bool,
758 },
759
760 CassiniSoldner {
762 lon0: f64,
764 lat0: f64,
766 false_easting: f64,
768 false_northing: f64,
770 },
771
772 Mercator {
774 lon0: f64,
776 lat_ts: f64,
778 k0: f64,
780 false_easting: f64,
782 false_northing: f64,
784 },
785
786 EquidistantCylindrical {
788 lon0: f64,
790 lat_ts: f64,
792 false_easting: f64,
794 false_northing: f64,
796 },
797 ColombiaUrban {
801 lon0: f64,
802 lat0: f64,
803 h0: f64,
804 false_easting: f64,
805 false_northing: f64,
806 },
807
808 KrovakNorthOrientated {
812 lon0: f64,
814 lat0: f64,
816 co_latitude_cone_axis: f64,
818 lat_pseudo_standard_parallel: f64,
820 k0: f64,
822 false_easting: f64,
824 false_northing: f64,
826 },
827
828 KrovakModifiedNorthOrientated {
832 lon0: f64,
834 lat0: f64,
836 co_latitude_cone_axis: f64,
838 lat_pseudo_standard_parallel: f64,
840 k0: f64,
842 false_easting: f64,
844 false_northing: f64,
846 },
847
848 EqualEarth {
851 lon0: f64,
853 false_easting: f64,
855 false_northing: f64,
857 },
858
859 AmericanPolyconic {
862 lon0: f64,
864 lat0: f64,
866 false_easting: f64,
868 false_northing: f64,
870 },
871
872 AzimuthalEquidistant {
875 lon0: f64,
877 lat0: f64,
879 false_easting: f64,
881 false_northing: f64,
883 },
884
885 GuamProjection {
888 lon0: f64,
890 lat0: f64,
892 false_easting: f64,
894 false_northing: f64,
896 },
897
898 PolarStereographicVariantC {
902 lon0: f64,
904 lat_ts: f64,
906 easting_false_origin: f64,
908 northing_false_origin: f64,
910 },
911
912 LabordeObliqueMercator {
915 lon0: f64,
917 lat0: f64,
919 azimuth: f64,
921 k0: f64,
923 false_easting: f64,
925 false_northing: f64,
927 },
928}
929
930impl ProjectionMethod {
931 fn canonical_params(&self) -> [f64; 8] {
936 match *self {
937 ProjectionMethod::WebMercator => [0.0; 8],
938 ProjectionMethod::TransverseMercator {
939 lon0,
940 lat0,
941 k0,
942 false_easting,
943 false_northing,
944 } => [lon0, lat0, k0, false_easting, false_northing, 0.0, 0.0, 0.0],
945 ProjectionMethod::PolarStereographic {
946 lon0,
947 lat_ts,
948 k0,
949 false_easting,
950 false_northing,
951 } => [
952 lon0,
953 lat_ts,
954 k0,
955 false_easting,
956 false_northing,
957 0.0,
958 0.0,
959 0.0,
960 ],
961 ProjectionMethod::LambertConformalConic {
962 lon0,
963 lat0,
964 lat1,
965 lat2,
966 k0,
967 false_easting,
968 false_northing,
969 } => [
970 lon0,
971 lat0,
972 lat1,
973 lat2,
974 k0,
975 false_easting,
976 false_northing,
977 0.0,
978 ],
979 ProjectionMethod::LambertConformalConicMichigan {
980 lon0,
981 lat0,
982 lat1,
983 lat2,
984 ellipsoid_scaling_factor,
985 false_easting,
986 false_northing,
987 } => [
988 lon0,
989 lat0,
990 lat1,
991 lat2,
992 ellipsoid_scaling_factor,
993 false_easting,
994 false_northing,
995 0.0,
996 ],
997 ProjectionMethod::LambertConformalConic1SPVariantB {
998 lon0,
999 lat0,
1000 k0,
1001 lat_false_origin,
1002 false_easting,
1003 false_northing,
1004 } => [
1005 lon0,
1006 lat0,
1007 k0,
1008 lat_false_origin,
1009 false_easting,
1010 false_northing,
1011 0.0,
1012 0.0,
1013 ],
1014 ProjectionMethod::AlbersEqualArea {
1015 lon0,
1016 lat0,
1017 lat1,
1018 lat2,
1019 false_easting,
1020 false_northing,
1021 } => [
1022 lon0,
1023 lat0,
1024 lat1,
1025 lat2,
1026 false_easting,
1027 false_northing,
1028 0.0,
1029 0.0,
1030 ],
1031 ProjectionMethod::LambertAzimuthalEqualArea {
1032 lon0,
1033 lat0,
1034 false_easting,
1035 false_northing,
1036 }
1037 | ProjectionMethod::LambertAzimuthalEqualAreaSpherical {
1038 lon0,
1039 lat0,
1040 false_easting,
1041 false_northing,
1042 }
1043 | ProjectionMethod::CassiniSoldner {
1044 lon0,
1045 lat0,
1046 false_easting,
1047 false_northing,
1048 } => [
1049 lon0,
1050 lat0,
1051 false_easting,
1052 false_northing,
1053 0.0,
1054 0.0,
1055 0.0,
1056 0.0,
1057 ],
1058 ProjectionMethod::ObliqueStereographic {
1059 lon0,
1060 lat0,
1061 k0,
1062 false_easting,
1063 false_northing,
1064 } => [lon0, lat0, k0, false_easting, false_northing, 0.0, 0.0, 0.0],
1065 ProjectionMethod::HotineObliqueMercator {
1066 latc,
1067 lonc,
1068 azimuth,
1069 rectified_grid_angle,
1070 k0,
1071 false_easting,
1072 false_northing,
1073 variant_b,
1074 } => [
1075 latc,
1076 lonc,
1077 azimuth,
1078 rectified_grid_angle,
1079 k0,
1080 false_easting,
1081 false_northing,
1082 if variant_b { 1.0 } else { 0.0 },
1083 ],
1084 ProjectionMethod::Mercator {
1085 lon0,
1086 lat_ts,
1087 k0,
1088 false_easting,
1089 false_northing,
1090 } => [
1091 lon0,
1092 lat_ts,
1093 k0,
1094 false_easting,
1095 false_northing,
1096 0.0,
1097 0.0,
1098 0.0,
1099 ],
1100 ProjectionMethod::EquidistantCylindrical {
1101 lon0,
1102 lat_ts,
1103 false_easting,
1104 false_northing,
1105 } => [
1106 lon0,
1107 lat_ts,
1108 false_easting,
1109 false_northing,
1110 0.0,
1111 0.0,
1112 0.0,
1113 0.0,
1114 ],
1115 ProjectionMethod::ColombiaUrban {
1116 lon0,
1117 lat0,
1118 h0,
1119 false_easting,
1120 false_northing,
1121 } => [lon0, lat0, h0, false_easting, false_northing, 0.0, 0.0, 0.0],
1122 ProjectionMethod::KrovakNorthOrientated {
1123 lon0,
1124 lat0,
1125 co_latitude_cone_axis,
1126 lat_pseudo_standard_parallel,
1127 k0,
1128 false_easting,
1129 false_northing,
1130 }
1131 | ProjectionMethod::KrovakModifiedNorthOrientated {
1132 lon0,
1133 lat0,
1134 co_latitude_cone_axis,
1135 lat_pseudo_standard_parallel,
1136 k0,
1137 false_easting,
1138 false_northing,
1139 } => [
1140 lon0,
1141 lat0,
1142 co_latitude_cone_axis,
1143 lat_pseudo_standard_parallel,
1144 k0,
1145 false_easting,
1146 false_northing,
1147 0.0,
1148 ],
1149 ProjectionMethod::EqualEarth {
1150 lon0,
1151 false_easting,
1152 false_northing,
1153 } => [lon0, false_easting, false_northing, 0.0, 0.0, 0.0, 0.0, 0.0],
1154 ProjectionMethod::AmericanPolyconic {
1155 lon0,
1156 lat0,
1157 false_easting,
1158 false_northing,
1159 }
1160 | ProjectionMethod::AzimuthalEquidistant {
1161 lon0,
1162 lat0,
1163 false_easting,
1164 false_northing,
1165 }
1166 | ProjectionMethod::GuamProjection {
1167 lon0,
1168 lat0,
1169 false_easting,
1170 false_northing,
1171 } => [
1172 lon0,
1173 lat0,
1174 false_easting,
1175 false_northing,
1176 0.0,
1177 0.0,
1178 0.0,
1179 0.0,
1180 ],
1181 ProjectionMethod::PolarStereographicVariantC {
1182 lon0,
1183 lat_ts,
1184 easting_false_origin,
1185 northing_false_origin,
1186 } => [
1187 lon0,
1188 lat_ts,
1189 easting_false_origin,
1190 northing_false_origin,
1191 0.0,
1192 0.0,
1193 0.0,
1194 0.0,
1195 ],
1196 ProjectionMethod::LabordeObliqueMercator {
1197 lon0,
1198 lat0,
1199 azimuth,
1200 k0,
1201 false_easting,
1202 false_northing,
1203 } => [
1204 lon0,
1205 lat0,
1206 azimuth,
1207 k0,
1208 false_easting,
1209 false_northing,
1210 0.0,
1211 0.0,
1212 ],
1213 }
1214 }
1215}
1216
1217fn projection_methods_equivalent(a: &ProjectionMethod, b: &ProjectionMethod) -> bool {
1218 if std::mem::discriminant(a) != std::mem::discriminant(b) {
1219 return false;
1220 }
1221 let (a_params, b_params) = (a.canonical_params(), b.canonical_params());
1222 a_params
1223 .iter()
1224 .zip(b_params.iter())
1225 .all(|(x, y)| approx_eq(*x, *y))
1226}
1227
1228fn approx_eq(a: f64, b: f64) -> bool {
1229 (a - b).abs() < 1e-12
1230}
1231
1232#[cfg(test)]
1233mod tests {
1234 use super::*;
1235 use crate::datum;
1236
1237 #[test]
1242 fn every_projection_method_is_self_equivalent() {
1243 let methods = [
1244 ProjectionMethod::ColombiaUrban {
1245 lon0: -74.15,
1246 lat0: 4.68,
1247 h0: 2550.0,
1248 false_easting: 92334.879,
1249 false_northing: 109320.965,
1250 },
1251 ProjectionMethod::LambertConformalConicMichigan {
1252 lon0: -84.33,
1253 lat0: 43.32,
1254 lat1: 44.18,
1255 lat2: 45.7,
1256 ellipsoid_scaling_factor: 1.0000382,
1257 false_easting: 609601.2192,
1258 false_northing: 0.0,
1259 },
1260 ProjectionMethod::LambertConformalConic1SPVariantB {
1261 lon0: 6.9,
1262 lat0: 45.15,
1263 k0: 1.0000398,
1264 lat_false_origin: 45.15,
1265 false_easting: 700000.0,
1266 false_northing: 300000.0,
1267 },
1268 ProjectionMethod::KrovakNorthOrientated {
1269 lon0: 24.833333333333332,
1270 lat0: 49.5,
1271 co_latitude_cone_axis: 30.288139752777778,
1272 lat_pseudo_standard_parallel: 78.5,
1273 k0: 0.9999,
1274 false_easting: 0.0,
1275 false_northing: 0.0,
1276 },
1277 ProjectionMethod::KrovakModifiedNorthOrientated {
1278 lon0: 24.833333333333332,
1279 lat0: 49.5,
1280 co_latitude_cone_axis: 30.288139752777778,
1281 lat_pseudo_standard_parallel: 78.5,
1282 k0: 0.9999,
1283 false_easting: 5_000_000.0,
1284 false_northing: 5_000_000.0,
1285 },
1286 ProjectionMethod::AmericanPolyconic {
1287 lon0: -54.0,
1288 lat0: 0.0,
1289 false_easting: 5_000_000.0,
1290 false_northing: 10_000_000.0,
1291 },
1292 ProjectionMethod::AzimuthalEquidistant {
1293 lon0: 21.5,
1294 lat0: 8.5,
1295 false_easting: 5_621_452.02,
1296 false_northing: 5_990_638.423,
1297 },
1298 ProjectionMethod::GuamProjection {
1299 lon0: 144.748_750_694_444_45,
1300 lat0: 13.472_466_333_333_33,
1301 false_easting: 50_000.0,
1302 false_northing: 50_000.0,
1303 },
1304 ProjectionMethod::PolarStereographicVariantC {
1305 lon0: 140.0,
1306 lat_ts: -67.0,
1307 easting_false_origin: 300_000.0,
1308 northing_false_origin: 200_000.0,
1309 },
1310 ProjectionMethod::LabordeObliqueMercator {
1311 lon0: 46.437_229_166_666_67,
1312 lat0: -18.9,
1313 azimuth: 18.9,
1314 k0: 0.9995,
1315 false_easting: 400_000.0,
1316 false_northing: 800_000.0,
1317 },
1318 ];
1319 for method in &methods {
1320 assert!(
1321 projection_methods_equivalent(method, method),
1322 "{method:?} must be self-equivalent"
1323 );
1324 }
1325 let modified_with_same_numbers = ProjectionMethod::KrovakModifiedNorthOrientated {
1327 lon0: 24.833333333333332,
1328 lat0: 49.5,
1329 co_latitude_cone_axis: 30.288139752777778,
1330 lat_pseudo_standard_parallel: 78.5,
1331 k0: 0.9999,
1332 false_easting: 0.0,
1333 false_northing: 0.0,
1334 };
1335 assert!(!projection_methods_equivalent(
1336 &methods[3],
1337 &modified_with_same_numbers
1338 ));
1339 }
1340
1341 #[test]
1342 fn geographic_crs_is_geographic() {
1343 let crs = CrsDef::Geographic(GeographicCrsDef::new(4326, datum::WGS84, "WGS 84"));
1344 assert!(crs.is_geographic());
1345 assert!(!crs.is_projected());
1346 assert_eq!(crs.epsg(), 4326);
1347 }
1348
1349 #[test]
1350 fn projected_crs_is_projected() {
1351 let crs = CrsDef::Projected(ProjectedCrsDef::new(
1352 3857,
1353 datum::WGS84,
1354 ProjectionMethod::WebMercator,
1355 LinearUnit::metre(),
1356 "WGS 84 / Pseudo-Mercator",
1357 ));
1358 assert!(crs.is_projected());
1359 assert!(!crs.is_geographic());
1360 assert_eq!(crs.epsg(), 3857);
1361 }
1362
1363 #[test]
1364 fn compound_crs_exposes_horizontal_and_vertical_components() {
1365 let horizontal = GeographicCrsDef::new(4326, datum::WGS84, "WGS 84");
1366 let vertical = VerticalCrsDef::ellipsoidal_height(
1367 0,
1368 datum::WGS84,
1369 LinearUnit::metre(),
1370 "WGS 84 ellipsoidal height",
1371 );
1372 let crs = CrsDef::Compound(Box::new(CompoundCrsDef::new(
1373 4979,
1374 HorizontalCrsDef::Geographic(horizontal),
1375 vertical,
1376 "WGS 84",
1377 )));
1378
1379 assert!(crs.is_compound());
1380 assert!(crs.is_geographic());
1381 assert!(!crs.is_projected());
1382 assert_eq!(crs.epsg(), 4979);
1383 assert_eq!(crs.base_geographic_crs_epsg(), Some(4326));
1384 assert!(crs.vertical_crs().is_some());
1385 }
1386
1387 #[test]
1388 fn linear_unit_validates_positive_finite_conversion() {
1389 assert!(LinearUnit::from_meters_per_unit(0.3048).is_ok());
1390 assert!(LinearUnit::from_meters_per_unit(0.0).is_err());
1391 assert!(LinearUnit::from_meters_per_unit(f64::NAN).is_err());
1392 }
1393}