1use crate::coord::{Bounds, Coord};
2use crate::crs::{LinearUnit, ProjectionMethod};
3use crate::datum::{DatumToWgs84, HelmertParams};
4use crate::error::{Error, Result};
5use smallvec::SmallVec;
6use std::collections::HashSet;
7use std::sync::Arc;
8
9const DEFAULT_AREA_BOUNDS_DENSIFY_POINTS: usize = 21;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct CoordinateOperationId(pub u32);
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub struct GridId(pub u32);
20
21#[derive(Debug, Clone, PartialEq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub struct AreaOfUse {
25 pub west: f64,
26 pub south: f64,
27 pub east: f64,
28 pub north: f64,
29 pub name: String,
30}
31
32impl AreaOfUse {
33 pub fn contains_point(&self, point: Coord) -> bool {
34 longitude_range_contains_point(self.west, self.east, point.x)
35 && point.y >= self.south
36 && point.y <= self.north
37 }
38
39 pub fn contains_bounds(&self, bounds: Bounds) -> bool {
40 longitude_range_contains_range(self.west, self.east, bounds.min_x, bounds.max_x)
41 && bounds.min_y >= self.south
42 && bounds.max_y <= self.north
43 }
44}
45
46fn longitude_range_contains_point(west: f64, east: f64, longitude: f64) -> bool {
47 longitude_delta(west, longitude) <= longitude_span(west, east)
48}
49
50fn longitude_range_contains_range(
51 outer_west: f64,
52 outer_east: f64,
53 inner_west: f64,
54 inner_east: f64,
55) -> bool {
56 let outer_span = longitude_span(outer_west, outer_east);
57 if outer_span >= 360.0 {
58 return true;
59 }
60 let inner_start = longitude_delta(outer_west, inner_west);
61 let inner_span = longitude_span(inner_west, inner_east);
62 inner_start + inner_span <= outer_span
63}
64
65fn longitude_span(west: f64, east: f64) -> f64 {
66 if east >= west {
67 east - west
68 } else {
69 east + 360.0 - west
70 }
71}
72
73fn longitude_delta(west: f64, east: f64) -> f64 {
74 (east - west).rem_euclid(360.0)
75}
76
77#[derive(Debug, Clone, Copy, PartialEq)]
79#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
80pub struct OperationAccuracy {
81 pub meters: f64,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
86pub enum OperationStepDirection {
87 Forward,
88 Reverse,
89}
90
91impl OperationStepDirection {
92 pub fn inverse(self) -> Self {
93 match self {
94 Self::Forward => Self::Reverse,
95 Self::Reverse => Self::Forward,
96 }
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum AreaOfInterestCrs {
102 GeographicDegrees,
104 GeographicDegreesWrapped,
107 SourceCrs,
108 TargetCrs,
109}
110
111impl AreaOfInterestCrs {
112 pub fn inverse(self) -> Self {
113 match self {
114 Self::GeographicDegrees => Self::GeographicDegrees,
115 Self::GeographicDegreesWrapped => Self::GeographicDegreesWrapped,
116 Self::SourceCrs => Self::TargetCrs,
117 Self::TargetCrs => Self::SourceCrs,
118 }
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq)]
123pub struct AreaOfInterest {
124 pub crs: AreaOfInterestCrs,
125 pub point: Option<Coord>,
126 pub bounds: Option<Bounds>,
127}
128
129impl AreaOfInterest {
130 pub fn geographic_point(point: Coord) -> Self {
131 Self {
132 crs: AreaOfInterestCrs::GeographicDegrees,
133 point: Some(point),
134 bounds: None,
135 }
136 }
137
138 pub fn geographic_bounds(bounds: Bounds) -> Self {
139 Self {
140 crs: AreaOfInterestCrs::GeographicDegrees,
141 point: None,
142 bounds: Some(bounds),
143 }
144 }
145
146 pub fn geographic_wrapped_bounds(bounds: Bounds) -> Self {
152 Self {
153 crs: AreaOfInterestCrs::GeographicDegreesWrapped,
154 point: None,
155 bounds: Some(bounds),
156 }
157 }
158
159 pub fn source_crs_point(point: Coord) -> Self {
160 Self {
161 crs: AreaOfInterestCrs::SourceCrs,
162 point: Some(point),
163 bounds: None,
164 }
165 }
166
167 pub fn source_crs_bounds(bounds: Bounds) -> Self {
168 Self {
169 crs: AreaOfInterestCrs::SourceCrs,
170 point: None,
171 bounds: Some(bounds),
172 }
173 }
174
175 pub fn target_crs_point(point: Coord) -> Self {
176 Self {
177 crs: AreaOfInterestCrs::TargetCrs,
178 point: Some(point),
179 bounds: None,
180 }
181 }
182
183 pub fn target_crs_bounds(bounds: Bounds) -> Self {
184 Self {
185 crs: AreaOfInterestCrs::TargetCrs,
186 point: None,
187 bounds: Some(bounds),
188 }
189 }
190
191 pub fn inverse(self) -> Self {
192 Self {
193 crs: self.crs.inverse(),
194 point: self.point,
195 bounds: self.bounds,
196 }
197 }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
201pub enum GridInterpolation {
202 Bilinear,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub enum GridShiftDirection {
207 Forward,
208 Reverse,
209}
210
211impl GridShiftDirection {
212 pub fn inverse(self) -> Self {
213 match self {
214 Self::Forward => Self::Reverse,
215 Self::Reverse => Self::Forward,
216 }
217 }
218}
219
220#[derive(Debug, Clone, PartialEq)]
221pub struct OperationStep {
222 pub operation_id: CoordinateOperationId,
223 pub direction: OperationStepDirection,
224}
225
226#[derive(Debug, Clone, Copy, PartialEq)]
232pub struct GeocentricAffineParams {
233 translation: [f64; 3],
234 matrix: [f64; 9],
235}
236
237impl GeocentricAffineParams {
238 pub fn new(translation: [f64; 3], matrix: [f64; 9]) -> Result<Self> {
239 let params = Self {
240 translation,
241 matrix,
242 };
243 params.validate()?;
244 Ok(params)
245 }
246
247 pub const fn translation(&self) -> [f64; 3] {
248 self.translation
249 }
250
251 pub const fn matrix(&self) -> [f64; 9] {
252 self.matrix
253 }
254
255 pub fn validate(&self) -> Result<()> {
256 if !self.translation.iter().all(|value| value.is_finite())
257 || !self.matrix.iter().all(|value| value.is_finite())
258 {
259 return Err(Error::InvalidDefinition(
260 "geocentric affine parameters must be finite".into(),
261 ));
262 }
263 if self.determinant().abs() <= 1e-24 {
264 return Err(Error::InvalidDefinition(
265 "geocentric affine matrix must be invertible".into(),
266 ));
267 }
268 Ok(())
269 }
270
271 pub(crate) fn forward(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) {
272 let m = self.matrix;
273 (
274 self.translation[0] + m[0] * x + m[1] * y + m[2] * z,
275 self.translation[1] + m[3] * x + m[4] * y + m[5] * z,
276 self.translation[2] + m[6] * x + m[7] * y + m[8] * z,
277 )
278 }
279
280 pub(crate) fn inverse(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) {
281 let m = self.matrix;
282 let ux = x - self.translation[0];
283 let uy = y - self.translation[1];
284 let uz = z - self.translation[2];
285 let det = self.determinant();
286 (
287 ((m[4] * m[8] - m[5] * m[7]) * ux
288 + (m[2] * m[7] - m[1] * m[8]) * uy
289 + (m[1] * m[5] - m[2] * m[4]) * uz)
290 / det,
291 ((m[5] * m[6] - m[3] * m[8]) * ux
292 + (m[0] * m[8] - m[2] * m[6]) * uy
293 + (m[2] * m[3] - m[0] * m[5]) * uz)
294 / det,
295 ((m[3] * m[7] - m[4] * m[6]) * ux
296 + (m[1] * m[6] - m[0] * m[7]) * uy
297 + (m[0] * m[4] - m[1] * m[3]) * uz)
298 / det,
299 )
300 }
301
302 fn determinant(&self) -> f64 {
303 let m = self.matrix;
304 m[0] * (m[4] * m[8] - m[5] * m[7]) - m[1] * (m[3] * m[8] - m[5] * m[6])
305 + m[2] * (m[3] * m[7] - m[4] * m[6])
306 }
307}
308
309#[derive(Debug, Clone, PartialEq)]
311pub enum OperationMethod {
312 Identity,
313 Helmert {
314 params: HelmertParams,
315 },
316 GeocentricAffine {
317 params: GeocentricAffineParams,
318 },
319 GridShift {
320 grid_id: GridId,
321 interpolation: GridInterpolation,
322 direction: GridShiftDirection,
323 },
324 DatumShift {
325 source_to_wgs84: DatumToWgs84,
326 target_to_wgs84: DatumToWgs84,
327 },
328 Projection {
329 forward: bool,
330 method: ProjectionMethod,
331 linear_unit: LinearUnit,
332 },
333 AxisUnitNormalize,
334 Concatenated {
335 steps: SmallVec<[OperationStep; 4]>,
336 },
337}
338
339#[derive(Debug, Clone, Copy, PartialEq, Eq)]
340pub enum OperationMatchKind {
341 Custom,
342 ExactSourceTarget,
343 DerivedGeographic,
344 DatumCompatible,
345 Explicit,
346}
347
348#[derive(Debug, Clone, PartialEq)]
349pub struct CoordinateOperation {
350 pub id: Option<CoordinateOperationId>,
351 pub name: String,
352 pub source_crs_epsg: Option<u32>,
353 pub target_crs_epsg: Option<u32>,
354 pub source_datum_epsg: Option<u32>,
355 pub target_datum_epsg: Option<u32>,
356 pub accuracy: Option<OperationAccuracy>,
357 pub areas_of_use: SmallVec<[AreaOfUse; 1]>,
358 pub deprecated: bool,
359 pub preferred: bool,
360 pub approximate: bool,
361 pub superseded: bool,
364 pub method: OperationMethod,
365}
366
367impl CoordinateOperation {
368 pub fn metadata(&self) -> CoordinateOperationMetadata {
369 CoordinateOperationMetadata {
370 id: self.id,
371 name: self.name.clone(),
372 direction: OperationStepDirection::Forward,
373 source_crs_epsg: self.source_crs_epsg,
374 target_crs_epsg: self.target_crs_epsg,
375 source_datum_epsg: self.source_datum_epsg,
376 target_datum_epsg: self.target_datum_epsg,
377 accuracy: self.accuracy,
378 area_of_use: self.areas_of_use.first().cloned(),
379 deprecated: self.deprecated,
380 preferred: self.preferred,
381 approximate: self.approximate,
382 uses_grids: self.uses_grids(),
383 }
384 }
385
386 pub fn metadata_for_direction(
387 &self,
388 direction: OperationStepDirection,
389 ) -> CoordinateOperationMetadata {
390 let mut metadata = self.metadata();
391 metadata.direction = direction;
392 if matches!(direction, OperationStepDirection::Reverse) {
393 std::mem::swap(&mut metadata.source_crs_epsg, &mut metadata.target_crs_epsg);
394 std::mem::swap(
395 &mut metadata.source_datum_epsg,
396 &mut metadata.target_datum_epsg,
397 );
398 }
399 metadata
400 }
401
402 pub fn uses_grids(&self) -> bool {
403 let mut visited = HashSet::new();
404 self.uses_grids_with_visited(&mut visited)
405 }
406
407 fn uses_grids_with_visited(&self, visited: &mut HashSet<CoordinateOperationId>) -> bool {
408 match &self.method {
409 OperationMethod::GridShift { .. } => true,
410 OperationMethod::DatumShift {
411 source_to_wgs84,
412 target_to_wgs84,
413 } => source_to_wgs84.uses_grid_shift() || target_to_wgs84.uses_grid_shift(),
414 OperationMethod::Concatenated { steps } => steps.iter().any(|step| {
415 if !visited.insert(step.operation_id) {
416 return false;
417 }
418 let uses_grids = crate::registry::lookup_operation(step.operation_id)
419 .map(|operation| operation.uses_grids_with_visited(visited))
420 .unwrap_or(false);
421 visited.remove(&step.operation_id);
422 uses_grids
423 }),
424 _ => false,
425 }
426 }
427}
428
429#[derive(Debug, Clone, PartialEq)]
430#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
431pub struct CoordinateOperationMetadata {
432 pub id: Option<CoordinateOperationId>,
433 pub name: String,
434 pub direction: OperationStepDirection,
435 pub source_crs_epsg: Option<u32>,
436 pub target_crs_epsg: Option<u32>,
437 pub source_datum_epsg: Option<u32>,
438 pub target_datum_epsg: Option<u32>,
439 pub accuracy: Option<OperationAccuracy>,
440 pub area_of_use: Option<AreaOfUse>,
441 pub deprecated: bool,
442 pub preferred: bool,
443 pub approximate: bool,
444 pub uses_grids: bool,
445}
446
447#[derive(Debug, Clone)]
448pub enum SelectionPolicy {
449 BestAvailable,
452 RequireGrids,
454 RequireExactAreaMatch,
456 Operation(CoordinateOperationId),
458}
459
460#[derive(Debug, Clone, Copy, PartialEq, Eq)]
461pub enum VerticalGridOffsetConvention {
462 GeoidHeightMeters,
465}
466
467#[derive(Debug, Clone, PartialEq)]
468pub struct VerticalGridOperation {
469 pub name: String,
471 pub grid: crate::grid::GridDefinition,
473 pub grid_horizontal_crs_epsg: Option<u32>,
475 pub source_vertical_crs_epsg: Option<u32>,
477 pub target_vertical_crs_epsg: Option<u32>,
479 pub source_vertical_datum_epsg: Option<u32>,
481 pub target_vertical_datum_epsg: Option<u32>,
483 pub accuracy: Option<OperationAccuracy>,
485 pub area_of_use: Option<AreaOfUse>,
487 pub offset_convention: VerticalGridOffsetConvention,
488}
489
490impl VerticalGridOperation {
491 pub fn inverse(&self) -> Self {
492 let mut inverse = self.clone();
493 std::mem::swap(
494 &mut inverse.source_vertical_crs_epsg,
495 &mut inverse.target_vertical_crs_epsg,
496 );
497 std::mem::swap(
498 &mut inverse.source_vertical_datum_epsg,
499 &mut inverse.target_vertical_datum_epsg,
500 );
501 inverse
502 }
503}
504
505#[derive(Clone)]
506pub struct SelectionOptions {
507 pub area_of_interest: Option<AreaOfInterest>,
508 pub area_bounds_densify_points: usize,
514 pub policy: SelectionPolicy,
515 pub grid_provider: Option<Arc<dyn crate::grid::GridProvider>>,
516 pub coordinate_operations: Vec<CoordinateOperation>,
517 pub vertical_grid_operations: Vec<VerticalGridOperation>,
518}
519
520impl Default for SelectionOptions {
521 fn default() -> Self {
522 Self {
523 area_of_interest: None,
524 area_bounds_densify_points: DEFAULT_AREA_BOUNDS_DENSIFY_POINTS,
525 policy: SelectionPolicy::BestAvailable,
526 grid_provider: None,
527 coordinate_operations: Vec::new(),
528 vertical_grid_operations: Vec::new(),
529 }
530 }
531}
532
533impl SelectionOptions {
534 pub fn new() -> Self {
536 Self::default()
537 }
538
539 pub fn with_area_of_interest(mut self, area_of_interest: AreaOfInterest) -> Self {
541 self.area_of_interest = Some(area_of_interest);
542 self
543 }
544
545 pub fn with_area_bounds_densify_points(mut self, densify_points: usize) -> Self {
551 self.area_bounds_densify_points = densify_points;
552 self
553 }
554
555 pub fn with_policy(mut self, policy: SelectionPolicy) -> Self {
557 self.policy = policy;
558 self
559 }
560
561 pub fn best_available(self) -> Self {
566 self.with_policy(SelectionPolicy::BestAvailable)
567 }
568
569 pub fn require_grids(self) -> Self {
571 self.with_policy(SelectionPolicy::RequireGrids)
572 }
573
574 pub fn require_exact_area_match(self) -> Self {
576 self.with_policy(SelectionPolicy::RequireExactAreaMatch)
577 }
578
579 pub fn with_operation(self, operation_id: CoordinateOperationId) -> Self {
581 self.with_policy(SelectionPolicy::Operation(operation_id))
582 }
583
584 pub fn with_grid_provider(mut self, provider: Arc<dyn crate::grid::GridProvider>) -> Self {
586 self.grid_provider = Some(provider);
587 self
588 }
589
590 pub fn with_coordinate_operation(mut self, operation: CoordinateOperation) -> Self {
592 self.coordinate_operations.push(operation);
593 self
594 }
595
596 pub fn with_coordinate_operations(
598 mut self,
599 operations: impl IntoIterator<Item = CoordinateOperation>,
600 ) -> Self {
601 self.coordinate_operations.extend(operations);
602 self
603 }
604
605 pub fn with_vertical_grid_operation(mut self, operation: VerticalGridOperation) -> Self {
607 self.vertical_grid_operations.push(operation);
608 self
609 }
610
611 pub fn with_vertical_grid_operations(
613 mut self,
614 operations: impl IntoIterator<Item = VerticalGridOperation>,
615 ) -> Self {
616 self.vertical_grid_operations.extend(operations);
617 self
618 }
619
620 pub fn inverse(&self) -> Self {
621 Self {
622 area_of_interest: self.area_of_interest.map(AreaOfInterest::inverse),
623 area_bounds_densify_points: self.area_bounds_densify_points,
624 policy: self.policy.clone(),
625 grid_provider: self.grid_provider.clone(),
626 coordinate_operations: self.coordinate_operations.clone(),
627 vertical_grid_operations: self
628 .vertical_grid_operations
629 .iter()
630 .map(VerticalGridOperation::inverse)
631 .collect(),
632 }
633 }
634}
635
636#[derive(Debug, Clone, Copy, PartialEq, Eq)]
637pub enum SelectionReason {
638 CustomOperation,
639 ExplicitOperation,
640 ExactSourceTarget,
641 AreaOfUseMatch,
642 AccuracyPreferred,
643 NonDeprecated,
644 PreferredOperation,
645}
646
647#[derive(Debug, Clone, PartialEq, Eq)]
648pub enum SkippedOperationReason {
649 AreaOfUseMismatch,
650 MissingGrid,
651 UnsupportedGridFormat,
652 PolicyFiltered,
653 LessPreferred,
654 Deprecated,
655}
656
657#[derive(Debug, Clone, PartialEq)]
658pub struct SkippedOperation {
659 pub metadata: CoordinateOperationMetadata,
660 pub reason: SkippedOperationReason,
661 pub detail: String,
662}
663
664#[derive(Debug, Clone, Copy, PartialEq, Eq)]
665pub enum VerticalTransformAction {
666 None,
668 Preserved,
670 UnitConverted,
672 Transformed,
674}
675
676#[derive(Debug, Clone, PartialEq)]
677pub struct VerticalGridProvenance {
678 pub name: String,
679 pub checksum: Option<String>,
681 pub accuracy: Option<OperationAccuracy>,
682 pub area_of_use: Option<AreaOfUse>,
683 pub area_of_use_match: Option<bool>,
684}
685
686#[derive(Debug, Clone, PartialEq)]
687pub struct VerticalTransformDiagnostics {
688 pub action: VerticalTransformAction,
689 pub operation_name: Option<String>,
690 pub source_vertical_crs_epsg: Option<u32>,
691 pub target_vertical_crs_epsg: Option<u32>,
692 pub source_vertical_datum_epsg: Option<u32>,
693 pub target_vertical_datum_epsg: Option<u32>,
694 pub source_unit_to_meter: Option<f64>,
695 pub target_unit_to_meter: Option<f64>,
696 pub accuracy: Option<OperationAccuracy>,
697 pub area_of_use: Option<AreaOfUse>,
698 pub area_of_use_match: Option<bool>,
699 pub grids: Vec<VerticalGridProvenance>,
700}
701
702#[derive(Debug, Clone, PartialEq)]
703pub struct OperationSelectionDiagnostics {
704 pub selected_operation: CoordinateOperationMetadata,
705 pub selected_match_kind: OperationMatchKind,
706 pub selected_reasons: SmallVec<[SelectionReason; 4]>,
707 pub fallback_operations: Vec<CoordinateOperationMetadata>,
708 pub skipped_operations: Vec<SkippedOperation>,
709 pub approximate: bool,
710 pub missing_required_grid: Option<String>,
711}
712
713#[derive(Debug, Clone, PartialEq)]
714pub struct GridCoverageMiss {
715 pub operation: std::sync::Arc<CoordinateOperationMetadata>,
718 pub detail: String,
719}
720
721#[derive(Debug, Clone, PartialEq)]
722pub struct TransformOutcome<T> {
723 pub coord: T,
724 pub operation: std::sync::Arc<CoordinateOperationMetadata>,
727 pub vertical: VerticalTransformDiagnostics,
728 pub grid_coverage_misses: Vec<GridCoverageMiss>,
729}
730
731#[cfg(test)]
732mod tests {
733 use super::*;
734 use crate::grid::{EmbeddedGridProvider, GridDefinition, GridFormat};
735
736 #[test]
737 fn geocentric_affine_roundtrips_a_general_matrix() {
738 let params = GeocentricAffineParams::new(
739 [1.0, -2.0, 3.0],
740 [1.1, 0.2, -0.1, -0.3, 0.9, 0.4, 0.05, -0.2, 1.2],
741 )
742 .unwrap();
743 let point = (4_000_000.0, 1_000_000.0, 5_000_000.0);
744 let transformed = params.forward(point.0, point.1, point.2);
745 let roundtrip = params.inverse(transformed.0, transformed.1, transformed.2);
746
747 assert!((roundtrip.0 - point.0).abs() < 1e-9);
748 assert!((roundtrip.1 - point.1).abs() < 1e-9);
749 assert!((roundtrip.2 - point.2).abs() < 1e-9);
750 }
751
752 #[test]
753 fn geocentric_affine_rejects_non_finite_and_singular_parameters() {
754 assert!(GeocentricAffineParams::new(
755 [f64::NAN, 0.0, 0.0],
756 [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]
757 )
758 .is_err());
759 assert!(GeocentricAffineParams::new(
760 [0.0; 3],
761 [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]
762 )
763 .is_err());
764 }
765
766 fn vertical_grid_operation(
767 name: &str,
768 source_vertical_crs_epsg: Option<u32>,
769 target_vertical_crs_epsg: Option<u32>,
770 ) -> VerticalGridOperation {
771 VerticalGridOperation {
772 name: name.into(),
773 grid: GridDefinition {
774 id: GridId(1),
775 name: format!("{name}.gtx"),
776 format: GridFormat::Gtx,
777 interpolation: GridInterpolation::Bilinear,
778 area_of_use: None,
779 resource_names: smallvec::SmallVec::from_vec(vec![format!("{name}.gtx")]),
780 },
781 grid_horizontal_crs_epsg: Some(4326),
782 source_vertical_crs_epsg,
783 target_vertical_crs_epsg,
784 source_vertical_datum_epsg: Some(1),
785 target_vertical_datum_epsg: Some(2),
786 accuracy: Some(OperationAccuracy { meters: 0.1 }),
787 area_of_use: None,
788 offset_convention: VerticalGridOffsetConvention::GeoidHeightMeters,
789 }
790 }
791
792 fn coordinate_operation(name: &str) -> CoordinateOperation {
793 CoordinateOperation {
794 id: None,
795 name: name.into(),
796 source_crs_epsg: None,
797 target_crs_epsg: None,
798 source_datum_epsg: None,
799 target_datum_epsg: None,
800 accuracy: Some(OperationAccuracy { meters: 0.0 }),
801 areas_of_use: smallvec::SmallVec::new(),
802 deprecated: false,
803 preferred: true,
804 approximate: false,
805 superseded: false,
806 method: OperationMethod::Identity,
807 }
808 }
809
810 #[test]
811 fn selection_options_builders_chain_advanced_options() {
812 let area = AreaOfInterest::geographic_point(Coord::new(-74.0, 40.0));
813 let provider: Arc<dyn crate::grid::GridProvider> = Arc::new(EmbeddedGridProvider);
814 let first_operation = coordinate_operation("first operation");
815 let second_operation = coordinate_operation("second operation");
816 let first = vertical_grid_operation("first", Some(4979), Some(5703));
817 let second = vertical_grid_operation("second", Some(4979), Some(5703));
818
819 let options = SelectionOptions::new()
820 .with_area_of_interest(area)
821 .with_area_bounds_densify_points(32)
822 .require_grids()
823 .with_grid_provider(provider.clone())
824 .with_coordinate_operation(first_operation.clone())
825 .with_coordinate_operations([second_operation.clone()])
826 .with_vertical_grid_operation(first.clone())
827 .with_vertical_grid_operations([second.clone()]);
828
829 assert_eq!(options.area_of_interest, Some(area));
830 assert_eq!(options.area_bounds_densify_points, 32);
831 assert!(matches!(options.policy, SelectionPolicy::RequireGrids));
832 assert!(Arc::ptr_eq(
833 options.grid_provider.as_ref().unwrap(),
834 &provider
835 ));
836 assert_eq!(
837 options.coordinate_operations,
838 vec![first_operation, second_operation]
839 );
840 assert_eq!(options.vertical_grid_operations, vec![first, second]);
841 }
842
843 #[test]
844 fn geographic_wrapped_bounds_constructor_marks_antimeridian_aoi() {
845 let bounds = Bounds::new(170.0, -20.0, -170.0, -10.0);
846 let area = AreaOfInterest::geographic_wrapped_bounds(bounds);
847
848 assert_eq!(area.crs, AreaOfInterestCrs::GeographicDegreesWrapped);
849 assert_eq!(area.bounds, Some(bounds));
850 assert_eq!(area.point, None);
851 assert_eq!(area.inverse(), area);
852 }
853
854 #[test]
855 fn area_of_use_contains_antimeridian_points_and_bounds() {
856 let area = AreaOfUse {
857 west: 160.0,
858 south: -25.0,
859 east: -160.0,
860 north: -5.0,
861 name: "Pacific antimeridian test area".into(),
862 };
863
864 assert!(area.contains_point(Coord::new(170.0, -15.0)));
865 assert!(area.contains_point(Coord::new(-170.0, -15.0)));
866 assert!(!area.contains_point(Coord::new(0.0, -15.0)));
867 assert!(area.contains_bounds(Bounds::new(170.0, -20.0, -170.0, -10.0)));
868 assert!(!area.contains_bounds(Bounds::new(150.0, -20.0, -170.0, -10.0)));
869
870 let world = AreaOfUse {
871 west: -180.0,
872 south: -90.0,
873 east: 180.0,
874 north: 90.0,
875 name: "World".into(),
876 };
877 assert!(world.contains_bounds(Bounds::new(170.0, -20.0, -170.0, -10.0)));
878 }
879
880 #[test]
881 fn selection_options_policy_builders_cover_all_modes() {
882 assert!(matches!(
883 SelectionOptions::new().best_available().policy,
884 SelectionPolicy::BestAvailable
885 ));
886 assert!(matches!(
887 SelectionOptions::new().require_exact_area_match().policy,
888 SelectionPolicy::RequireExactAreaMatch
889 ));
890 assert!(matches!(
891 SelectionOptions::new()
892 .with_operation(CoordinateOperationId(1234))
893 .policy,
894 SelectionPolicy::Operation(CoordinateOperationId(1234))
895 ));
896 }
897
898 #[test]
899 fn selection_options_inverse_preserves_builder_values() {
900 let options = SelectionOptions::new()
901 .with_area_of_interest(AreaOfInterest::source_crs_point(Coord::new(1.0, 2.0)))
902 .with_area_bounds_densify_points(32)
903 .with_policy(SelectionPolicy::RequireExactAreaMatch)
904 .with_coordinate_operation(coordinate_operation("operation"))
905 .with_vertical_grid_operation(vertical_grid_operation("grid", Some(4979), Some(5703)));
906
907 let inverse = options.inverse();
908
909 assert!(matches!(
910 inverse.area_of_interest,
911 Some(AreaOfInterest {
912 crs: AreaOfInterestCrs::TargetCrs,
913 point: Some(Coord { x: 1.0, y: 2.0 }),
914 bounds: None,
915 })
916 ));
917 assert!(matches!(
918 inverse.policy,
919 SelectionPolicy::RequireExactAreaMatch
920 ));
921 assert_eq!(inverse.area_bounds_densify_points, 32);
922 assert_eq!(inverse.coordinate_operations, options.coordinate_operations);
923 assert_eq!(
924 inverse.vertical_grid_operations[0].source_vertical_crs_epsg,
925 Some(5703)
926 );
927 assert_eq!(
928 inverse.vertical_grid_operations[0].target_vertical_crs_epsg,
929 Some(4979)
930 );
931 }
932}