Skip to main content

proj_core/
operation.rs

1use crate::coord::{Bounds, Coord};
2use crate::crs::{LinearUnit, ProjectionMethod};
3use crate::datum::{DatumToWgs84, HelmertParams};
4use smallvec::SmallVec;
5use std::collections::HashSet;
6use std::sync::Arc;
7
8const DEFAULT_AREA_BOUNDS_DENSIFY_POINTS: usize = 21;
9
10/// Stable identifier for a registry-backed coordinate operation.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct CoordinateOperationId(pub u32);
13
14/// Stable identifier for a grid resource referenced by an operation.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct GridId(pub u32);
17
18/// Ranked area-of-use metadata for an operation or grid.
19#[derive(Debug, Clone, PartialEq)]
20pub struct AreaOfUse {
21    pub west: f64,
22    pub south: f64,
23    pub east: f64,
24    pub north: f64,
25    pub name: String,
26}
27
28impl AreaOfUse {
29    pub fn contains_point(&self, point: Coord) -> bool {
30        longitude_range_contains_point(self.west, self.east, point.x)
31            && point.y >= self.south
32            && point.y <= self.north
33    }
34
35    pub fn contains_bounds(&self, bounds: Bounds) -> bool {
36        longitude_range_contains_range(self.west, self.east, bounds.min_x, bounds.max_x)
37            && bounds.min_y >= self.south
38            && bounds.max_y <= self.north
39    }
40}
41
42fn longitude_range_contains_point(west: f64, east: f64, longitude: f64) -> bool {
43    longitude_delta(west, longitude) <= longitude_span(west, east)
44}
45
46fn longitude_range_contains_range(
47    outer_west: f64,
48    outer_east: f64,
49    inner_west: f64,
50    inner_east: f64,
51) -> bool {
52    let outer_span = longitude_span(outer_west, outer_east);
53    if outer_span >= 360.0 {
54        return true;
55    }
56    let inner_start = longitude_delta(outer_west, inner_west);
57    let inner_span = longitude_span(inner_west, inner_east);
58    inner_start + inner_span <= outer_span
59}
60
61fn longitude_span(west: f64, east: f64) -> f64 {
62    if east >= west {
63        east - west
64    } else {
65        east + 360.0 - west
66    }
67}
68
69fn longitude_delta(west: f64, east: f64) -> f64 {
70    (east - west).rem_euclid(360.0)
71}
72
73/// Nominal operation accuracy in meters.
74#[derive(Debug, Clone, Copy, PartialEq)]
75pub struct OperationAccuracy {
76    pub meters: f64,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum OperationStepDirection {
81    Forward,
82    Reverse,
83}
84
85impl OperationStepDirection {
86    pub fn inverse(self) -> Self {
87        match self {
88            Self::Forward => Self::Reverse,
89            Self::Reverse => Self::Forward,
90        }
91    }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum AreaOfInterestCrs {
96    /// Geographic degrees with conventional west <= east bounds.
97    GeographicDegrees,
98    /// Geographic degrees with bounds crossing the antimeridian, represented
99    /// by west > east.
100    GeographicDegreesWrapped,
101    SourceCrs,
102    TargetCrs,
103}
104
105impl AreaOfInterestCrs {
106    pub fn inverse(self) -> Self {
107        match self {
108            Self::GeographicDegrees => Self::GeographicDegrees,
109            Self::GeographicDegreesWrapped => Self::GeographicDegreesWrapped,
110            Self::SourceCrs => Self::TargetCrs,
111            Self::TargetCrs => Self::SourceCrs,
112        }
113    }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq)]
117pub struct AreaOfInterest {
118    pub crs: AreaOfInterestCrs,
119    pub point: Option<Coord>,
120    pub bounds: Option<Bounds>,
121}
122
123impl AreaOfInterest {
124    pub fn geographic_point(point: Coord) -> Self {
125        Self {
126            crs: AreaOfInterestCrs::GeographicDegrees,
127            point: Some(point),
128            bounds: None,
129        }
130    }
131
132    pub fn geographic_bounds(bounds: Bounds) -> Self {
133        Self {
134            crs: AreaOfInterestCrs::GeographicDegrees,
135            point: None,
136            bounds: Some(bounds),
137        }
138    }
139
140    /// Construct a geographic area of interest that crosses the antimeridian.
141    ///
142    /// The bounds are interpreted as west/south/east/north in degrees and must
143    /// satisfy `west > east`; use [`Self::geographic_bounds`] for normal
144    /// non-wrapped geographic bounds.
145    pub fn geographic_wrapped_bounds(bounds: Bounds) -> Self {
146        Self {
147            crs: AreaOfInterestCrs::GeographicDegreesWrapped,
148            point: None,
149            bounds: Some(bounds),
150        }
151    }
152
153    pub fn source_crs_point(point: Coord) -> Self {
154        Self {
155            crs: AreaOfInterestCrs::SourceCrs,
156            point: Some(point),
157            bounds: None,
158        }
159    }
160
161    pub fn source_crs_bounds(bounds: Bounds) -> Self {
162        Self {
163            crs: AreaOfInterestCrs::SourceCrs,
164            point: None,
165            bounds: Some(bounds),
166        }
167    }
168
169    pub fn target_crs_point(point: Coord) -> Self {
170        Self {
171            crs: AreaOfInterestCrs::TargetCrs,
172            point: Some(point),
173            bounds: None,
174        }
175    }
176
177    pub fn target_crs_bounds(bounds: Bounds) -> Self {
178        Self {
179            crs: AreaOfInterestCrs::TargetCrs,
180            point: None,
181            bounds: Some(bounds),
182        }
183    }
184
185    pub fn inverse(self) -> Self {
186        Self {
187            crs: self.crs.inverse(),
188            point: self.point,
189            bounds: self.bounds,
190        }
191    }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub enum GridInterpolation {
196    Bilinear,
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum GridShiftDirection {
201    Forward,
202    Reverse,
203}
204
205impl GridShiftDirection {
206    pub fn inverse(self) -> Self {
207        match self {
208            Self::Forward => Self::Reverse,
209            Self::Reverse => Self::Forward,
210        }
211    }
212}
213
214#[derive(Debug, Clone, PartialEq)]
215pub struct OperationStep {
216    pub operation_id: CoordinateOperationId,
217    pub direction: OperationStepDirection,
218}
219
220/// Enum-backed operation method model used by selection and compilation.
221#[derive(Debug, Clone, PartialEq)]
222pub enum OperationMethod {
223    Identity,
224    Helmert {
225        params: HelmertParams,
226    },
227    GridShift {
228        grid_id: GridId,
229        interpolation: GridInterpolation,
230        direction: GridShiftDirection,
231    },
232    DatumShift {
233        source_to_wgs84: DatumToWgs84,
234        target_to_wgs84: DatumToWgs84,
235    },
236    Projection {
237        forward: bool,
238        method: ProjectionMethod,
239        linear_unit: LinearUnit,
240    },
241    AxisUnitNormalize,
242    Concatenated {
243        steps: SmallVec<[OperationStep; 4]>,
244    },
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum OperationMatchKind {
249    ExactSourceTarget,
250    DerivedGeographic,
251    DatumCompatible,
252    ApproximateFallback,
253    Explicit,
254}
255
256#[derive(Debug, Clone, PartialEq)]
257pub struct CoordinateOperation {
258    pub id: Option<CoordinateOperationId>,
259    pub name: String,
260    pub source_crs_epsg: Option<u32>,
261    pub target_crs_epsg: Option<u32>,
262    pub source_datum_epsg: Option<u32>,
263    pub target_datum_epsg: Option<u32>,
264    pub accuracy: Option<OperationAccuracy>,
265    pub areas_of_use: SmallVec<[AreaOfUse; 1]>,
266    pub deprecated: bool,
267    pub preferred: bool,
268    pub approximate: bool,
269    pub method: OperationMethod,
270}
271
272impl CoordinateOperation {
273    pub fn metadata(&self) -> CoordinateOperationMetadata {
274        CoordinateOperationMetadata {
275            id: self.id,
276            name: self.name.clone(),
277            direction: OperationStepDirection::Forward,
278            source_crs_epsg: self.source_crs_epsg,
279            target_crs_epsg: self.target_crs_epsg,
280            source_datum_epsg: self.source_datum_epsg,
281            target_datum_epsg: self.target_datum_epsg,
282            accuracy: self.accuracy,
283            area_of_use: self.areas_of_use.first().cloned(),
284            deprecated: self.deprecated,
285            preferred: self.preferred,
286            approximate: self.approximate,
287            uses_grids: self.uses_grids(),
288        }
289    }
290
291    pub fn metadata_for_direction(
292        &self,
293        direction: OperationStepDirection,
294    ) -> CoordinateOperationMetadata {
295        let mut metadata = self.metadata();
296        metadata.direction = direction;
297        if matches!(direction, OperationStepDirection::Reverse) {
298            std::mem::swap(&mut metadata.source_crs_epsg, &mut metadata.target_crs_epsg);
299            std::mem::swap(
300                &mut metadata.source_datum_epsg,
301                &mut metadata.target_datum_epsg,
302            );
303        }
304        metadata
305    }
306
307    pub fn uses_grids(&self) -> bool {
308        let mut visited = HashSet::new();
309        self.uses_grids_with_visited(&mut visited)
310    }
311
312    fn uses_grids_with_visited(&self, visited: &mut HashSet<CoordinateOperationId>) -> bool {
313        match &self.method {
314            OperationMethod::GridShift { .. } => true,
315            OperationMethod::DatumShift {
316                source_to_wgs84,
317                target_to_wgs84,
318            } => source_to_wgs84.uses_grid_shift() || target_to_wgs84.uses_grid_shift(),
319            OperationMethod::Concatenated { steps } => steps.iter().any(|step| {
320                if !visited.insert(step.operation_id) {
321                    return false;
322                }
323                let uses_grids = crate::registry::lookup_operation(step.operation_id)
324                    .map(|operation| operation.uses_grids_with_visited(visited))
325                    .unwrap_or(false);
326                visited.remove(&step.operation_id);
327                uses_grids
328            }),
329            _ => false,
330        }
331    }
332}
333
334#[derive(Debug, Clone, PartialEq)]
335pub struct CoordinateOperationMetadata {
336    pub id: Option<CoordinateOperationId>,
337    pub name: String,
338    pub direction: OperationStepDirection,
339    pub source_crs_epsg: Option<u32>,
340    pub target_crs_epsg: Option<u32>,
341    pub source_datum_epsg: Option<u32>,
342    pub target_datum_epsg: Option<u32>,
343    pub accuracy: Option<OperationAccuracy>,
344    pub area_of_use: Option<AreaOfUse>,
345    pub deprecated: bool,
346    pub preferred: bool,
347    pub approximate: bool,
348    pub uses_grids: bool,
349}
350
351#[derive(Debug, Clone)]
352pub enum SelectionPolicy {
353    /// Select the best supported registry or exact synthetic operation.
354    ///
355    /// This default policy does not synthesize approximate Helmert fallbacks.
356    /// Use [`SelectionOptions::allow_approximate_helmert_fallback`] when a
357    /// last-resort approximate datum shift is acceptable.
358    BestAvailable,
359    /// Require a grid-backed datum operation whenever a datum shift is needed.
360    RequireGrids,
361    /// Require selected registry operations to match the configured area of interest.
362    RequireExactAreaMatch,
363    /// Permit a synthetic approximate Helmert operation when no better
364    /// supported operation is available.
365    AllowApproximateHelmertFallback,
366    /// Select one explicit registry operation by id.
367    Operation(CoordinateOperationId),
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
371pub enum VerticalGridOffsetConvention {
372    /// Grid values are geoid heights in meters (`N`), applied as
373    /// gravity height `H = h - N` and ellipsoidal height `h = H + N`.
374    GeoidHeightMeters,
375}
376
377#[derive(Debug, Clone, PartialEq)]
378pub struct VerticalGridOperation {
379    /// Human-readable operation name used in diagnostics.
380    pub name: String,
381    /// Grid resource definition resolved through the configured grid provider.
382    pub grid: crate::grid::GridDefinition,
383    /// Horizontal CRS EPSG code in which the grid is sampled, when known.
384    pub grid_horizontal_crs_epsg: Option<u32>,
385    /// Optional source vertical CRS EPSG filter.
386    pub source_vertical_crs_epsg: Option<u32>,
387    /// Optional target vertical CRS EPSG filter.
388    pub target_vertical_crs_epsg: Option<u32>,
389    /// Optional source gravity-related vertical datum EPSG filter.
390    pub source_vertical_datum_epsg: Option<u32>,
391    /// Optional target gravity-related vertical datum EPSG filter.
392    pub target_vertical_datum_epsg: Option<u32>,
393    /// Expected operation accuracy in meters, when known.
394    pub accuracy: Option<OperationAccuracy>,
395    /// Operation area of use, when distinct from the grid's area.
396    pub area_of_use: Option<AreaOfUse>,
397    pub offset_convention: VerticalGridOffsetConvention,
398}
399
400impl VerticalGridOperation {
401    pub fn inverse(&self) -> Self {
402        let mut inverse = self.clone();
403        std::mem::swap(
404            &mut inverse.source_vertical_crs_epsg,
405            &mut inverse.target_vertical_crs_epsg,
406        );
407        std::mem::swap(
408            &mut inverse.source_vertical_datum_epsg,
409            &mut inverse.target_vertical_datum_epsg,
410        );
411        inverse
412    }
413}
414
415#[derive(Clone)]
416pub struct SelectionOptions {
417    pub area_of_interest: Option<AreaOfInterest>,
418    /// Intermediate points sampled per edge when source/target CRS AOI bounds
419    /// are normalized to geographic degrees for operation selection.
420    ///
421    /// Values above [`crate::MAX_BOUNDS_DENSIFY_POINTS`] are rejected during
422    /// transform construction.
423    pub area_bounds_densify_points: usize,
424    pub policy: SelectionPolicy,
425    pub grid_provider: Option<Arc<dyn crate::grid::GridProvider>>,
426    pub vertical_grid_operations: Vec<VerticalGridOperation>,
427}
428
429impl Default for SelectionOptions {
430    fn default() -> Self {
431        Self {
432            area_of_interest: None,
433            area_bounds_densify_points: DEFAULT_AREA_BOUNDS_DENSIFY_POINTS,
434            policy: SelectionPolicy::BestAvailable,
435            grid_provider: None,
436            vertical_grid_operations: Vec::new(),
437        }
438    }
439}
440
441impl SelectionOptions {
442    /// Create default selection options.
443    pub fn new() -> Self {
444        Self::default()
445    }
446
447    /// Set the area of interest used for operation ranking and filtering.
448    pub fn with_area_of_interest(mut self, area_of_interest: AreaOfInterest) -> Self {
449        self.area_of_interest = Some(area_of_interest);
450        self
451    }
452
453    /// Set how many intermediate points are sampled on each AOI bounds edge
454    /// when source/target CRS bounds are converted to geographic degrees.
455    ///
456    /// Values above [`crate::MAX_BOUNDS_DENSIFY_POINTS`] are rejected during
457    /// transform construction.
458    pub fn with_area_bounds_densify_points(mut self, densify_points: usize) -> Self {
459        self.area_bounds_densify_points = densify_points;
460        self
461    }
462
463    /// Set the operation selection policy.
464    pub fn with_policy(mut self, policy: SelectionPolicy) -> Self {
465        self.policy = policy;
466        self
467    }
468
469    /// Select the best supported registry or exact synthetic operation.
470    ///
471    /// This is the default policy. It does not synthesize approximate Helmert
472    /// fallbacks.
473    pub fn best_available(self) -> Self {
474        self.with_policy(SelectionPolicy::BestAvailable)
475    }
476
477    /// Require a grid-backed datum operation when a datum operation is needed.
478    pub fn require_grids(self) -> Self {
479        self.with_policy(SelectionPolicy::RequireGrids)
480    }
481
482    /// Require selected operations to match the configured area of interest.
483    pub fn require_exact_area_match(self) -> Self {
484        self.with_policy(SelectionPolicy::RequireExactAreaMatch)
485    }
486
487    /// Allow approximate Helmert fallback operations when no better supported
488    /// operation is available.
489    ///
490    /// This opt-in policy can synthesize a last-resort Helmert operation from
491    /// source and target datum metadata. The selected operation is marked
492    /// `approximate` in operation metadata and diagnostics.
493    pub fn allow_approximate_helmert_fallback(self) -> Self {
494        self.with_policy(SelectionPolicy::AllowApproximateHelmertFallback)
495    }
496
497    /// Select a specific registry operation by id.
498    pub fn with_operation(self, operation_id: CoordinateOperationId) -> Self {
499        self.with_policy(SelectionPolicy::Operation(operation_id))
500    }
501
502    /// Set the grid provider used to resolve grid-backed horizontal and vertical operations.
503    pub fn with_grid_provider(mut self, provider: Arc<dyn crate::grid::GridProvider>) -> Self {
504        self.grid_provider = Some(provider);
505        self
506    }
507
508    /// Add one explicit vertical grid operation candidate.
509    pub fn with_vertical_grid_operation(mut self, operation: VerticalGridOperation) -> Self {
510        self.vertical_grid_operations.push(operation);
511        self
512    }
513
514    /// Add explicit vertical grid operation candidates.
515    pub fn with_vertical_grid_operations(
516        mut self,
517        operations: impl IntoIterator<Item = VerticalGridOperation>,
518    ) -> Self {
519        self.vertical_grid_operations.extend(operations);
520        self
521    }
522
523    pub fn inverse(&self) -> Self {
524        Self {
525            area_of_interest: self.area_of_interest.map(AreaOfInterest::inverse),
526            area_bounds_densify_points: self.area_bounds_densify_points,
527            policy: self.policy.clone(),
528            grid_provider: self.grid_provider.clone(),
529            vertical_grid_operations: self
530                .vertical_grid_operations
531                .iter()
532                .map(VerticalGridOperation::inverse)
533                .collect(),
534        }
535    }
536}
537
538#[derive(Debug, Clone, Copy, PartialEq, Eq)]
539pub enum SelectionReason {
540    ExplicitOperation,
541    ExactSourceTarget,
542    AreaOfUseMatch,
543    AccuracyPreferred,
544    NonDeprecated,
545    PreferredOperation,
546    ApproximateFallback,
547}
548
549#[derive(Debug, Clone, PartialEq, Eq)]
550pub enum SkippedOperationReason {
551    AreaOfUseMismatch,
552    MissingGrid,
553    UnsupportedGridFormat,
554    PolicyFiltered,
555    LessPreferred,
556    Deprecated,
557}
558
559#[derive(Debug, Clone, PartialEq)]
560pub struct SkippedOperation {
561    pub metadata: CoordinateOperationMetadata,
562    pub reason: SkippedOperationReason,
563    pub detail: String,
564}
565
566#[derive(Debug, Clone, Copy, PartialEq, Eq)]
567pub enum VerticalTransformAction {
568    /// No explicit vertical CRS participates in the transform.
569    None,
570    /// `z` is preserved because the vertical CRS semantics and units match.
571    Preserved,
572    /// `z` is converted between units of the same vertical reference frame.
573    UnitConverted,
574    /// `z` is transformed by an explicit vertical operation.
575    Transformed,
576}
577
578#[derive(Debug, Clone, PartialEq)]
579pub struct VerticalGridProvenance {
580    pub name: String,
581    /// Content checksum of the resolved grid resource, formatted as `sha256:<hex>`.
582    pub checksum: Option<String>,
583    pub accuracy: Option<OperationAccuracy>,
584    pub area_of_use: Option<AreaOfUse>,
585    pub area_of_use_match: Option<bool>,
586}
587
588#[derive(Debug, Clone, PartialEq)]
589pub struct VerticalTransformDiagnostics {
590    pub action: VerticalTransformAction,
591    pub operation_name: Option<String>,
592    pub source_vertical_crs_epsg: Option<u32>,
593    pub target_vertical_crs_epsg: Option<u32>,
594    pub source_vertical_datum_epsg: Option<u32>,
595    pub target_vertical_datum_epsg: Option<u32>,
596    pub source_unit_to_meter: Option<f64>,
597    pub target_unit_to_meter: Option<f64>,
598    pub accuracy: Option<OperationAccuracy>,
599    pub area_of_use: Option<AreaOfUse>,
600    pub area_of_use_match: Option<bool>,
601    pub grids: Vec<VerticalGridProvenance>,
602}
603
604#[derive(Debug, Clone, PartialEq)]
605pub struct OperationSelectionDiagnostics {
606    pub selected_operation: CoordinateOperationMetadata,
607    pub selected_match_kind: OperationMatchKind,
608    pub selected_reasons: SmallVec<[SelectionReason; 4]>,
609    pub fallback_operations: Vec<CoordinateOperationMetadata>,
610    pub skipped_operations: Vec<SkippedOperation>,
611    pub approximate: bool,
612    pub missing_required_grid: Option<String>,
613}
614
615#[derive(Debug, Clone, PartialEq)]
616pub struct GridCoverageMiss {
617    pub operation: CoordinateOperationMetadata,
618    pub detail: String,
619}
620
621#[derive(Debug, Clone, PartialEq)]
622pub struct TransformOutcome<T> {
623    pub coord: T,
624    pub operation: CoordinateOperationMetadata,
625    pub vertical: VerticalTransformDiagnostics,
626    pub grid_coverage_misses: Vec<GridCoverageMiss>,
627}
628
629#[cfg(test)]
630mod tests {
631    use super::*;
632    use crate::grid::{EmbeddedGridProvider, GridDefinition, GridFormat};
633
634    fn vertical_grid_operation(
635        name: &str,
636        source_vertical_crs_epsg: Option<u32>,
637        target_vertical_crs_epsg: Option<u32>,
638    ) -> VerticalGridOperation {
639        VerticalGridOperation {
640            name: name.into(),
641            grid: GridDefinition {
642                id: GridId(1),
643                name: format!("{name}.gtx"),
644                format: GridFormat::Gtx,
645                interpolation: GridInterpolation::Bilinear,
646                area_of_use: None,
647                resource_names: smallvec::SmallVec::from_vec(vec![format!("{name}.gtx")]),
648            },
649            grid_horizontal_crs_epsg: Some(4326),
650            source_vertical_crs_epsg,
651            target_vertical_crs_epsg,
652            source_vertical_datum_epsg: Some(1),
653            target_vertical_datum_epsg: Some(2),
654            accuracy: Some(OperationAccuracy { meters: 0.1 }),
655            area_of_use: None,
656            offset_convention: VerticalGridOffsetConvention::GeoidHeightMeters,
657        }
658    }
659
660    #[test]
661    fn selection_options_builders_chain_advanced_options() {
662        let area = AreaOfInterest::geographic_point(Coord::new(-74.0, 40.0));
663        let provider: Arc<dyn crate::grid::GridProvider> = Arc::new(EmbeddedGridProvider);
664        let first = vertical_grid_operation("first", Some(4979), Some(5703));
665        let second = vertical_grid_operation("second", Some(4979), Some(5703));
666
667        let options = SelectionOptions::new()
668            .with_area_of_interest(area)
669            .with_area_bounds_densify_points(32)
670            .require_grids()
671            .with_grid_provider(provider.clone())
672            .with_vertical_grid_operation(first.clone())
673            .with_vertical_grid_operations([second.clone()]);
674
675        assert_eq!(options.area_of_interest, Some(area));
676        assert_eq!(options.area_bounds_densify_points, 32);
677        assert!(matches!(options.policy, SelectionPolicy::RequireGrids));
678        assert!(Arc::ptr_eq(
679            options.grid_provider.as_ref().unwrap(),
680            &provider
681        ));
682        assert_eq!(options.vertical_grid_operations, vec![first, second]);
683    }
684
685    #[test]
686    fn geographic_wrapped_bounds_constructor_marks_antimeridian_aoi() {
687        let bounds = Bounds::new(170.0, -20.0, -170.0, -10.0);
688        let area = AreaOfInterest::geographic_wrapped_bounds(bounds);
689
690        assert_eq!(area.crs, AreaOfInterestCrs::GeographicDegreesWrapped);
691        assert_eq!(area.bounds, Some(bounds));
692        assert_eq!(area.point, None);
693        assert_eq!(area.inverse(), area);
694    }
695
696    #[test]
697    fn area_of_use_contains_antimeridian_points_and_bounds() {
698        let area = AreaOfUse {
699            west: 160.0,
700            south: -25.0,
701            east: -160.0,
702            north: -5.0,
703            name: "Pacific antimeridian test area".into(),
704        };
705
706        assert!(area.contains_point(Coord::new(170.0, -15.0)));
707        assert!(area.contains_point(Coord::new(-170.0, -15.0)));
708        assert!(!area.contains_point(Coord::new(0.0, -15.0)));
709        assert!(area.contains_bounds(Bounds::new(170.0, -20.0, -170.0, -10.0)));
710        assert!(!area.contains_bounds(Bounds::new(150.0, -20.0, -170.0, -10.0)));
711
712        let world = AreaOfUse {
713            west: -180.0,
714            south: -90.0,
715            east: 180.0,
716            north: 90.0,
717            name: "World".into(),
718        };
719        assert!(world.contains_bounds(Bounds::new(170.0, -20.0, -170.0, -10.0)));
720    }
721
722    #[test]
723    fn selection_options_policy_builders_cover_all_modes() {
724        assert!(matches!(
725            SelectionOptions::new().best_available().policy,
726            SelectionPolicy::BestAvailable
727        ));
728        assert!(matches!(
729            SelectionOptions::new().require_exact_area_match().policy,
730            SelectionPolicy::RequireExactAreaMatch
731        ));
732        assert!(matches!(
733            SelectionOptions::new()
734                .allow_approximate_helmert_fallback()
735                .policy,
736            SelectionPolicy::AllowApproximateHelmertFallback
737        ));
738        assert!(matches!(
739            SelectionOptions::new()
740                .with_operation(CoordinateOperationId(1234))
741                .policy,
742            SelectionPolicy::Operation(CoordinateOperationId(1234))
743        ));
744    }
745
746    #[test]
747    fn selection_options_inverse_preserves_builder_values() {
748        let options = SelectionOptions::new()
749            .with_area_of_interest(AreaOfInterest::source_crs_point(Coord::new(1.0, 2.0)))
750            .with_area_bounds_densify_points(32)
751            .with_policy(SelectionPolicy::RequireExactAreaMatch)
752            .with_vertical_grid_operation(vertical_grid_operation("grid", Some(4979), Some(5703)));
753
754        let inverse = options.inverse();
755
756        assert!(matches!(
757            inverse.area_of_interest,
758            Some(AreaOfInterest {
759                crs: AreaOfInterestCrs::TargetCrs,
760                point: Some(Coord { x: 1.0, y: 2.0 }),
761                bounds: None,
762            })
763        ));
764        assert!(matches!(
765            inverse.policy,
766            SelectionPolicy::RequireExactAreaMatch
767        ));
768        assert_eq!(inverse.area_bounds_densify_points, 32);
769        assert_eq!(
770            inverse.vertical_grid_operations[0].source_vertical_crs_epsg,
771            Some(5703)
772        );
773        assert_eq!(
774            inverse.vertical_grid_operations[0].target_vertical_crs_epsg,
775            Some(4979)
776        );
777    }
778}