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