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 false_easting: f64,
643 false_northing: f64,
645 },
646
647 AlbersEqualArea {
649 lon0: f64,
651 lat0: f64,
653 lat1: f64,
655 lat2: f64,
657 false_easting: f64,
659 false_northing: f64,
661 },
662
663 LambertAzimuthalEqualArea {
665 lon0: f64,
667 lat0: f64,
669 false_easting: f64,
671 false_northing: f64,
673 },
674
675 LambertAzimuthalEqualAreaSpherical {
677 lon0: f64,
679 lat0: f64,
681 false_easting: f64,
683 false_northing: f64,
685 },
686
687 ObliqueStereographic {
689 lon0: f64,
691 lat0: f64,
693 k0: f64,
695 false_easting: f64,
697 false_northing: f64,
699 },
700
701 HotineObliqueMercator {
703 latc: f64,
705 lonc: f64,
707 azimuth: f64,
709 rectified_grid_angle: f64,
711 k0: f64,
713 false_easting: f64,
715 false_northing: f64,
717 variant_b: bool,
719 },
720
721 CassiniSoldner {
723 lon0: f64,
725 lat0: f64,
727 false_easting: f64,
729 false_northing: f64,
731 },
732
733 Mercator {
735 lon0: f64,
737 lat_ts: f64,
739 k0: f64,
741 false_easting: f64,
743 false_northing: f64,
745 },
746
747 EquidistantCylindrical {
749 lon0: f64,
751 lat_ts: f64,
753 false_easting: f64,
755 false_northing: f64,
757 },
758}
759
760fn projection_methods_equivalent(a: &ProjectionMethod, b: &ProjectionMethod) -> bool {
761 match (a, b) {
762 (ProjectionMethod::WebMercator, ProjectionMethod::WebMercator) => true,
763 (
764 ProjectionMethod::TransverseMercator {
765 lon0: a_lon0,
766 lat0: a_lat0,
767 k0: a_k0,
768 false_easting: a_false_easting,
769 false_northing: a_false_northing,
770 },
771 ProjectionMethod::TransverseMercator {
772 lon0: b_lon0,
773 lat0: b_lat0,
774 k0: b_k0,
775 false_easting: b_false_easting,
776 false_northing: b_false_northing,
777 },
778 ) => {
779 approx_eq(*a_lon0, *b_lon0)
780 && approx_eq(*a_lat0, *b_lat0)
781 && approx_eq(*a_k0, *b_k0)
782 && approx_eq(*a_false_easting, *b_false_easting)
783 && approx_eq(*a_false_northing, *b_false_northing)
784 }
785 (
786 ProjectionMethod::PolarStereographic {
787 lon0: a_lon0,
788 lat_ts: a_lat_ts,
789 k0: a_k0,
790 false_easting: a_false_easting,
791 false_northing: a_false_northing,
792 },
793 ProjectionMethod::PolarStereographic {
794 lon0: b_lon0,
795 lat_ts: b_lat_ts,
796 k0: b_k0,
797 false_easting: b_false_easting,
798 false_northing: b_false_northing,
799 },
800 ) => {
801 approx_eq(*a_lon0, *b_lon0)
802 && approx_eq(*a_lat_ts, *b_lat_ts)
803 && approx_eq(*a_k0, *b_k0)
804 && approx_eq(*a_false_easting, *b_false_easting)
805 && approx_eq(*a_false_northing, *b_false_northing)
806 }
807 (
808 ProjectionMethod::LambertConformalConic {
809 lon0: a_lon0,
810 lat0: a_lat0,
811 lat1: a_lat1,
812 lat2: a_lat2,
813 false_easting: a_false_easting,
814 false_northing: a_false_northing,
815 },
816 ProjectionMethod::LambertConformalConic {
817 lon0: b_lon0,
818 lat0: b_lat0,
819 lat1: b_lat1,
820 lat2: b_lat2,
821 false_easting: b_false_easting,
822 false_northing: b_false_northing,
823 },
824 ) => {
825 approx_eq(*a_lon0, *b_lon0)
826 && approx_eq(*a_lat0, *b_lat0)
827 && approx_eq(*a_lat1, *b_lat1)
828 && approx_eq(*a_lat2, *b_lat2)
829 && approx_eq(*a_false_easting, *b_false_easting)
830 && approx_eq(*a_false_northing, *b_false_northing)
831 }
832 (
833 ProjectionMethod::AlbersEqualArea {
834 lon0: a_lon0,
835 lat0: a_lat0,
836 lat1: a_lat1,
837 lat2: a_lat2,
838 false_easting: a_false_easting,
839 false_northing: a_false_northing,
840 },
841 ProjectionMethod::AlbersEqualArea {
842 lon0: b_lon0,
843 lat0: b_lat0,
844 lat1: b_lat1,
845 lat2: b_lat2,
846 false_easting: b_false_easting,
847 false_northing: b_false_northing,
848 },
849 ) => {
850 approx_eq(*a_lon0, *b_lon0)
851 && approx_eq(*a_lat0, *b_lat0)
852 && approx_eq(*a_lat1, *b_lat1)
853 && approx_eq(*a_lat2, *b_lat2)
854 && approx_eq(*a_false_easting, *b_false_easting)
855 && approx_eq(*a_false_northing, *b_false_northing)
856 }
857 (
858 ProjectionMethod::LambertAzimuthalEqualArea {
859 lon0: a_lon0,
860 lat0: a_lat0,
861 false_easting: a_false_easting,
862 false_northing: a_false_northing,
863 },
864 ProjectionMethod::LambertAzimuthalEqualArea {
865 lon0: b_lon0,
866 lat0: b_lat0,
867 false_easting: b_false_easting,
868 false_northing: b_false_northing,
869 },
870 ) => {
871 approx_eq(*a_lon0, *b_lon0)
872 && approx_eq(*a_lat0, *b_lat0)
873 && approx_eq(*a_false_easting, *b_false_easting)
874 && approx_eq(*a_false_northing, *b_false_northing)
875 }
876 (
877 ProjectionMethod::LambertAzimuthalEqualAreaSpherical {
878 lon0: a_lon0,
879 lat0: a_lat0,
880 false_easting: a_false_easting,
881 false_northing: a_false_northing,
882 },
883 ProjectionMethod::LambertAzimuthalEqualAreaSpherical {
884 lon0: b_lon0,
885 lat0: b_lat0,
886 false_easting: b_false_easting,
887 false_northing: b_false_northing,
888 },
889 ) => {
890 approx_eq(*a_lon0, *b_lon0)
891 && approx_eq(*a_lat0, *b_lat0)
892 && approx_eq(*a_false_easting, *b_false_easting)
893 && approx_eq(*a_false_northing, *b_false_northing)
894 }
895 (
896 ProjectionMethod::ObliqueStereographic {
897 lon0: a_lon0,
898 lat0: a_lat0,
899 k0: a_k0,
900 false_easting: a_false_easting,
901 false_northing: a_false_northing,
902 },
903 ProjectionMethod::ObliqueStereographic {
904 lon0: b_lon0,
905 lat0: b_lat0,
906 k0: b_k0,
907 false_easting: b_false_easting,
908 false_northing: b_false_northing,
909 },
910 ) => {
911 approx_eq(*a_lon0, *b_lon0)
912 && approx_eq(*a_lat0, *b_lat0)
913 && approx_eq(*a_k0, *b_k0)
914 && approx_eq(*a_false_easting, *b_false_easting)
915 && approx_eq(*a_false_northing, *b_false_northing)
916 }
917 (
918 ProjectionMethod::HotineObliqueMercator {
919 latc: a_latc,
920 lonc: a_lonc,
921 azimuth: a_azimuth,
922 rectified_grid_angle: a_rectified_grid_angle,
923 k0: a_k0,
924 false_easting: a_false_easting,
925 false_northing: a_false_northing,
926 variant_b: a_variant_b,
927 },
928 ProjectionMethod::HotineObliqueMercator {
929 latc: b_latc,
930 lonc: b_lonc,
931 azimuth: b_azimuth,
932 rectified_grid_angle: b_rectified_grid_angle,
933 k0: b_k0,
934 false_easting: b_false_easting,
935 false_northing: b_false_northing,
936 variant_b: b_variant_b,
937 },
938 ) => {
939 a_variant_b == b_variant_b
940 && approx_eq(*a_latc, *b_latc)
941 && approx_eq(*a_lonc, *b_lonc)
942 && approx_eq(*a_azimuth, *b_azimuth)
943 && approx_eq(*a_rectified_grid_angle, *b_rectified_grid_angle)
944 && approx_eq(*a_k0, *b_k0)
945 && approx_eq(*a_false_easting, *b_false_easting)
946 && approx_eq(*a_false_northing, *b_false_northing)
947 }
948 (
949 ProjectionMethod::CassiniSoldner {
950 lon0: a_lon0,
951 lat0: a_lat0,
952 false_easting: a_false_easting,
953 false_northing: a_false_northing,
954 },
955 ProjectionMethod::CassiniSoldner {
956 lon0: b_lon0,
957 lat0: b_lat0,
958 false_easting: b_false_easting,
959 false_northing: b_false_northing,
960 },
961 ) => {
962 approx_eq(*a_lon0, *b_lon0)
963 && approx_eq(*a_lat0, *b_lat0)
964 && approx_eq(*a_false_easting, *b_false_easting)
965 && approx_eq(*a_false_northing, *b_false_northing)
966 }
967 (
968 ProjectionMethod::Mercator {
969 lon0: a_lon0,
970 lat_ts: a_lat_ts,
971 k0: a_k0,
972 false_easting: a_false_easting,
973 false_northing: a_false_northing,
974 },
975 ProjectionMethod::Mercator {
976 lon0: b_lon0,
977 lat_ts: b_lat_ts,
978 k0: b_k0,
979 false_easting: b_false_easting,
980 false_northing: b_false_northing,
981 },
982 ) => {
983 approx_eq(*a_lon0, *b_lon0)
984 && approx_eq(*a_lat_ts, *b_lat_ts)
985 && approx_eq(*a_k0, *b_k0)
986 && approx_eq(*a_false_easting, *b_false_easting)
987 && approx_eq(*a_false_northing, *b_false_northing)
988 }
989 (
990 ProjectionMethod::EquidistantCylindrical {
991 lon0: a_lon0,
992 lat_ts: a_lat_ts,
993 false_easting: a_false_easting,
994 false_northing: a_false_northing,
995 },
996 ProjectionMethod::EquidistantCylindrical {
997 lon0: b_lon0,
998 lat_ts: b_lat_ts,
999 false_easting: b_false_easting,
1000 false_northing: b_false_northing,
1001 },
1002 ) => {
1003 approx_eq(*a_lon0, *b_lon0)
1004 && approx_eq(*a_lat_ts, *b_lat_ts)
1005 && approx_eq(*a_false_easting, *b_false_easting)
1006 && approx_eq(*a_false_northing, *b_false_northing)
1007 }
1008 _ => false,
1009 }
1010}
1011
1012fn approx_eq(a: f64, b: f64) -> bool {
1013 (a - b).abs() < 1e-12
1014}
1015
1016#[cfg(test)]
1017mod tests {
1018 use super::*;
1019 use crate::datum;
1020
1021 #[test]
1022 fn geographic_crs_is_geographic() {
1023 let crs = CrsDef::Geographic(GeographicCrsDef::new(4326, datum::WGS84, "WGS 84"));
1024 assert!(crs.is_geographic());
1025 assert!(!crs.is_projected());
1026 assert_eq!(crs.epsg(), 4326);
1027 }
1028
1029 #[test]
1030 fn projected_crs_is_projected() {
1031 let crs = CrsDef::Projected(ProjectedCrsDef::new(
1032 3857,
1033 datum::WGS84,
1034 ProjectionMethod::WebMercator,
1035 LinearUnit::metre(),
1036 "WGS 84 / Pseudo-Mercator",
1037 ));
1038 assert!(crs.is_projected());
1039 assert!(!crs.is_geographic());
1040 assert_eq!(crs.epsg(), 3857);
1041 }
1042
1043 #[test]
1044 fn compound_crs_exposes_horizontal_and_vertical_components() {
1045 let horizontal = GeographicCrsDef::new(4326, datum::WGS84, "WGS 84");
1046 let vertical = VerticalCrsDef::ellipsoidal_height(
1047 0,
1048 datum::WGS84,
1049 LinearUnit::metre(),
1050 "WGS 84 ellipsoidal height",
1051 );
1052 let crs = CrsDef::Compound(Box::new(CompoundCrsDef::new(
1053 4979,
1054 HorizontalCrsDef::Geographic(horizontal),
1055 vertical,
1056 "WGS 84",
1057 )));
1058
1059 assert!(crs.is_compound());
1060 assert!(crs.is_geographic());
1061 assert!(!crs.is_projected());
1062 assert_eq!(crs.epsg(), 4979);
1063 assert_eq!(crs.base_geographic_crs_epsg(), Some(4326));
1064 assert!(crs.vertical_crs().is_some());
1065 }
1066
1067 #[test]
1068 fn linear_unit_validates_positive_finite_conversion() {
1069 assert!(LinearUnit::from_meters_per_unit(0.3048).is_ok());
1070 assert!(LinearUnit::from_meters_per_unit(0.0).is_err());
1071 assert!(LinearUnit::from_meters_per_unit(f64::NAN).is_err());
1072 }
1073}