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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub struct CoordinateOperationId(pub u32);
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub struct GridId(pub u32);
19
20#[derive(Debug, Clone, PartialEq)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub struct AreaOfUse {
24 pub west: f64,
25 pub south: f64,
26 pub east: f64,
27 pub north: f64,
28 pub name: String,
29}
30
31impl AreaOfUse {
32 pub fn contains_point(&self, point: Coord) -> bool {
33 longitude_range_contains_point(self.west, self.east, point.x)
34 && point.y >= self.south
35 && point.y <= self.north
36 }
37
38 pub fn contains_bounds(&self, bounds: Bounds) -> bool {
39 longitude_range_contains_range(self.west, self.east, bounds.min_x, bounds.max_x)
40 && bounds.min_y >= self.south
41 && bounds.max_y <= self.north
42 }
43}
44
45fn longitude_range_contains_point(west: f64, east: f64, longitude: f64) -> bool {
46 longitude_delta(west, longitude) <= longitude_span(west, east)
47}
48
49fn longitude_range_contains_range(
50 outer_west: f64,
51 outer_east: f64,
52 inner_west: f64,
53 inner_east: f64,
54) -> bool {
55 let outer_span = longitude_span(outer_west, outer_east);
56 if outer_span >= 360.0 {
57 return true;
58 }
59 let inner_start = longitude_delta(outer_west, inner_west);
60 let inner_span = longitude_span(inner_west, inner_east);
61 inner_start + inner_span <= outer_span
62}
63
64fn longitude_span(west: f64, east: f64) -> f64 {
65 if east >= west {
66 east - west
67 } else {
68 east + 360.0 - west
69 }
70}
71
72fn longitude_delta(west: f64, east: f64) -> f64 {
73 (east - west).rem_euclid(360.0)
74}
75
76#[derive(Debug, Clone, Copy, PartialEq)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
79pub struct OperationAccuracy {
80 pub meters: f64,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
85pub enum OperationStepDirection {
86 Forward,
87 Reverse,
88}
89
90impl OperationStepDirection {
91 pub fn inverse(self) -> Self {
92 match self {
93 Self::Forward => Self::Reverse,
94 Self::Reverse => Self::Forward,
95 }
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum AreaOfInterestCrs {
101 GeographicDegrees,
103 GeographicDegreesWrapped,
106 SourceCrs,
107 TargetCrs,
108}
109
110impl AreaOfInterestCrs {
111 pub fn inverse(self) -> Self {
112 match self {
113 Self::GeographicDegrees => Self::GeographicDegrees,
114 Self::GeographicDegreesWrapped => Self::GeographicDegreesWrapped,
115 Self::SourceCrs => Self::TargetCrs,
116 Self::TargetCrs => Self::SourceCrs,
117 }
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq)]
122pub struct AreaOfInterest {
123 pub crs: AreaOfInterestCrs,
124 pub point: Option<Coord>,
125 pub bounds: Option<Bounds>,
126}
127
128impl AreaOfInterest {
129 pub fn geographic_point(point: Coord) -> Self {
130 Self {
131 crs: AreaOfInterestCrs::GeographicDegrees,
132 point: Some(point),
133 bounds: None,
134 }
135 }
136
137 pub fn geographic_bounds(bounds: Bounds) -> Self {
138 Self {
139 crs: AreaOfInterestCrs::GeographicDegrees,
140 point: None,
141 bounds: Some(bounds),
142 }
143 }
144
145 pub fn geographic_wrapped_bounds(bounds: Bounds) -> Self {
151 Self {
152 crs: AreaOfInterestCrs::GeographicDegreesWrapped,
153 point: None,
154 bounds: Some(bounds),
155 }
156 }
157
158 pub fn source_crs_point(point: Coord) -> Self {
159 Self {
160 crs: AreaOfInterestCrs::SourceCrs,
161 point: Some(point),
162 bounds: None,
163 }
164 }
165
166 pub fn source_crs_bounds(bounds: Bounds) -> Self {
167 Self {
168 crs: AreaOfInterestCrs::SourceCrs,
169 point: None,
170 bounds: Some(bounds),
171 }
172 }
173
174 pub fn target_crs_point(point: Coord) -> Self {
175 Self {
176 crs: AreaOfInterestCrs::TargetCrs,
177 point: Some(point),
178 bounds: None,
179 }
180 }
181
182 pub fn target_crs_bounds(bounds: Bounds) -> Self {
183 Self {
184 crs: AreaOfInterestCrs::TargetCrs,
185 point: None,
186 bounds: Some(bounds),
187 }
188 }
189
190 pub fn inverse(self) -> Self {
191 Self {
192 crs: self.crs.inverse(),
193 point: self.point,
194 bounds: self.bounds,
195 }
196 }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
200pub enum GridInterpolation {
201 Bilinear,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub enum GridShiftDirection {
206 Forward,
207 Reverse,
208}
209
210impl GridShiftDirection {
211 pub fn inverse(self) -> Self {
212 match self {
213 Self::Forward => Self::Reverse,
214 Self::Reverse => Self::Forward,
215 }
216 }
217}
218
219#[derive(Debug, Clone, PartialEq)]
220pub struct OperationStep {
221 pub operation_id: CoordinateOperationId,
222 pub direction: OperationStepDirection,
223}
224
225#[derive(Debug, Clone, PartialEq)]
227pub enum OperationMethod {
228 Identity,
229 Helmert {
230 params: HelmertParams,
231 },
232 GridShift {
233 grid_id: GridId,
234 interpolation: GridInterpolation,
235 direction: GridShiftDirection,
236 },
237 DatumShift {
238 source_to_wgs84: DatumToWgs84,
239 target_to_wgs84: DatumToWgs84,
240 },
241 Projection {
242 forward: bool,
243 method: ProjectionMethod,
244 linear_unit: LinearUnit,
245 },
246 AxisUnitNormalize,
247 Concatenated {
248 steps: SmallVec<[OperationStep; 4]>,
249 },
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub enum OperationMatchKind {
254 Custom,
255 ExactSourceTarget,
256 DerivedGeographic,
257 DatumCompatible,
258 Explicit,
259}
260
261#[derive(Debug, Clone, PartialEq)]
262pub struct CoordinateOperation {
263 pub id: Option<CoordinateOperationId>,
264 pub name: String,
265 pub source_crs_epsg: Option<u32>,
266 pub target_crs_epsg: Option<u32>,
267 pub source_datum_epsg: Option<u32>,
268 pub target_datum_epsg: Option<u32>,
269 pub accuracy: Option<OperationAccuracy>,
270 pub areas_of_use: SmallVec<[AreaOfUse; 1]>,
271 pub deprecated: bool,
272 pub preferred: bool,
273 pub approximate: bool,
274 pub superseded: bool,
277 pub method: OperationMethod,
278}
279
280impl CoordinateOperation {
281 pub fn metadata(&self) -> CoordinateOperationMetadata {
282 CoordinateOperationMetadata {
283 id: self.id,
284 name: self.name.clone(),
285 direction: OperationStepDirection::Forward,
286 source_crs_epsg: self.source_crs_epsg,
287 target_crs_epsg: self.target_crs_epsg,
288 source_datum_epsg: self.source_datum_epsg,
289 target_datum_epsg: self.target_datum_epsg,
290 accuracy: self.accuracy,
291 area_of_use: self.areas_of_use.first().cloned(),
292 deprecated: self.deprecated,
293 preferred: self.preferred,
294 approximate: self.approximate,
295 uses_grids: self.uses_grids(),
296 }
297 }
298
299 pub fn metadata_for_direction(
300 &self,
301 direction: OperationStepDirection,
302 ) -> CoordinateOperationMetadata {
303 let mut metadata = self.metadata();
304 metadata.direction = direction;
305 if matches!(direction, OperationStepDirection::Reverse) {
306 std::mem::swap(&mut metadata.source_crs_epsg, &mut metadata.target_crs_epsg);
307 std::mem::swap(
308 &mut metadata.source_datum_epsg,
309 &mut metadata.target_datum_epsg,
310 );
311 }
312 metadata
313 }
314
315 pub fn uses_grids(&self) -> bool {
316 let mut visited = HashSet::new();
317 self.uses_grids_with_visited(&mut visited)
318 }
319
320 fn uses_grids_with_visited(&self, visited: &mut HashSet<CoordinateOperationId>) -> bool {
321 match &self.method {
322 OperationMethod::GridShift { .. } => true,
323 OperationMethod::DatumShift {
324 source_to_wgs84,
325 target_to_wgs84,
326 } => source_to_wgs84.uses_grid_shift() || target_to_wgs84.uses_grid_shift(),
327 OperationMethod::Concatenated { steps } => steps.iter().any(|step| {
328 if !visited.insert(step.operation_id) {
329 return false;
330 }
331 let uses_grids = crate::registry::lookup_operation(step.operation_id)
332 .map(|operation| operation.uses_grids_with_visited(visited))
333 .unwrap_or(false);
334 visited.remove(&step.operation_id);
335 uses_grids
336 }),
337 _ => false,
338 }
339 }
340}
341
342#[derive(Debug, Clone, PartialEq)]
343#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
344pub struct CoordinateOperationMetadata {
345 pub id: Option<CoordinateOperationId>,
346 pub name: String,
347 pub direction: OperationStepDirection,
348 pub source_crs_epsg: Option<u32>,
349 pub target_crs_epsg: Option<u32>,
350 pub source_datum_epsg: Option<u32>,
351 pub target_datum_epsg: Option<u32>,
352 pub accuracy: Option<OperationAccuracy>,
353 pub area_of_use: Option<AreaOfUse>,
354 pub deprecated: bool,
355 pub preferred: bool,
356 pub approximate: bool,
357 pub uses_grids: bool,
358}
359
360#[derive(Debug, Clone)]
361pub enum SelectionPolicy {
362 BestAvailable,
365 RequireGrids,
367 RequireExactAreaMatch,
369 Operation(CoordinateOperationId),
371}
372
373#[derive(Debug, Clone, Copy, PartialEq, Eq)]
374pub enum VerticalGridOffsetConvention {
375 GeoidHeightMeters,
378}
379
380#[derive(Debug, Clone, PartialEq)]
381pub struct VerticalGridOperation {
382 pub name: String,
384 pub grid: crate::grid::GridDefinition,
386 pub grid_horizontal_crs_epsg: Option<u32>,
388 pub source_vertical_crs_epsg: Option<u32>,
390 pub target_vertical_crs_epsg: Option<u32>,
392 pub source_vertical_datum_epsg: Option<u32>,
394 pub target_vertical_datum_epsg: Option<u32>,
396 pub accuracy: Option<OperationAccuracy>,
398 pub area_of_use: Option<AreaOfUse>,
400 pub offset_convention: VerticalGridOffsetConvention,
401}
402
403impl VerticalGridOperation {
404 pub fn inverse(&self) -> Self {
405 let mut inverse = self.clone();
406 std::mem::swap(
407 &mut inverse.source_vertical_crs_epsg,
408 &mut inverse.target_vertical_crs_epsg,
409 );
410 std::mem::swap(
411 &mut inverse.source_vertical_datum_epsg,
412 &mut inverse.target_vertical_datum_epsg,
413 );
414 inverse
415 }
416}
417
418#[derive(Clone)]
419pub struct SelectionOptions {
420 pub area_of_interest: Option<AreaOfInterest>,
421 pub area_bounds_densify_points: usize,
427 pub policy: SelectionPolicy,
428 pub grid_provider: Option<Arc<dyn crate::grid::GridProvider>>,
429 pub coordinate_operations: Vec<CoordinateOperation>,
430 pub vertical_grid_operations: Vec<VerticalGridOperation>,
431}
432
433impl Default for SelectionOptions {
434 fn default() -> Self {
435 Self {
436 area_of_interest: None,
437 area_bounds_densify_points: DEFAULT_AREA_BOUNDS_DENSIFY_POINTS,
438 policy: SelectionPolicy::BestAvailable,
439 grid_provider: None,
440 coordinate_operations: Vec::new(),
441 vertical_grid_operations: Vec::new(),
442 }
443 }
444}
445
446impl SelectionOptions {
447 pub fn new() -> Self {
449 Self::default()
450 }
451
452 pub fn with_area_of_interest(mut self, area_of_interest: AreaOfInterest) -> Self {
454 self.area_of_interest = Some(area_of_interest);
455 self
456 }
457
458 pub fn with_area_bounds_densify_points(mut self, densify_points: usize) -> Self {
464 self.area_bounds_densify_points = densify_points;
465 self
466 }
467
468 pub fn with_policy(mut self, policy: SelectionPolicy) -> Self {
470 self.policy = policy;
471 self
472 }
473
474 pub fn best_available(self) -> Self {
479 self.with_policy(SelectionPolicy::BestAvailable)
480 }
481
482 pub fn require_grids(self) -> Self {
484 self.with_policy(SelectionPolicy::RequireGrids)
485 }
486
487 pub fn require_exact_area_match(self) -> Self {
489 self.with_policy(SelectionPolicy::RequireExactAreaMatch)
490 }
491
492 pub fn with_operation(self, operation_id: CoordinateOperationId) -> Self {
494 self.with_policy(SelectionPolicy::Operation(operation_id))
495 }
496
497 pub fn with_grid_provider(mut self, provider: Arc<dyn crate::grid::GridProvider>) -> Self {
499 self.grid_provider = Some(provider);
500 self
501 }
502
503 pub fn with_coordinate_operation(mut self, operation: CoordinateOperation) -> Self {
505 self.coordinate_operations.push(operation);
506 self
507 }
508
509 pub fn with_coordinate_operations(
511 mut self,
512 operations: impl IntoIterator<Item = CoordinateOperation>,
513 ) -> Self {
514 self.coordinate_operations.extend(operations);
515 self
516 }
517
518 pub fn with_vertical_grid_operation(mut self, operation: VerticalGridOperation) -> Self {
520 self.vertical_grid_operations.push(operation);
521 self
522 }
523
524 pub fn with_vertical_grid_operations(
526 mut self,
527 operations: impl IntoIterator<Item = VerticalGridOperation>,
528 ) -> Self {
529 self.vertical_grid_operations.extend(operations);
530 self
531 }
532
533 pub fn inverse(&self) -> Self {
534 Self {
535 area_of_interest: self.area_of_interest.map(AreaOfInterest::inverse),
536 area_bounds_densify_points: self.area_bounds_densify_points,
537 policy: self.policy.clone(),
538 grid_provider: self.grid_provider.clone(),
539 coordinate_operations: self.coordinate_operations.clone(),
540 vertical_grid_operations: self
541 .vertical_grid_operations
542 .iter()
543 .map(VerticalGridOperation::inverse)
544 .collect(),
545 }
546 }
547}
548
549#[derive(Debug, Clone, Copy, PartialEq, Eq)]
550pub enum SelectionReason {
551 CustomOperation,
552 ExplicitOperation,
553 ExactSourceTarget,
554 AreaOfUseMatch,
555 AccuracyPreferred,
556 NonDeprecated,
557 PreferredOperation,
558}
559
560#[derive(Debug, Clone, PartialEq, Eq)]
561pub enum SkippedOperationReason {
562 AreaOfUseMismatch,
563 MissingGrid,
564 UnsupportedGridFormat,
565 PolicyFiltered,
566 LessPreferred,
567 Deprecated,
568}
569
570#[derive(Debug, Clone, PartialEq)]
571pub struct SkippedOperation {
572 pub metadata: CoordinateOperationMetadata,
573 pub reason: SkippedOperationReason,
574 pub detail: String,
575}
576
577#[derive(Debug, Clone, Copy, PartialEq, Eq)]
578pub enum VerticalTransformAction {
579 None,
581 Preserved,
583 UnitConverted,
585 Transformed,
587}
588
589#[derive(Debug, Clone, PartialEq)]
590pub struct VerticalGridProvenance {
591 pub name: String,
592 pub checksum: Option<String>,
594 pub accuracy: Option<OperationAccuracy>,
595 pub area_of_use: Option<AreaOfUse>,
596 pub area_of_use_match: Option<bool>,
597}
598
599#[derive(Debug, Clone, PartialEq)]
600pub struct VerticalTransformDiagnostics {
601 pub action: VerticalTransformAction,
602 pub operation_name: Option<String>,
603 pub source_vertical_crs_epsg: Option<u32>,
604 pub target_vertical_crs_epsg: Option<u32>,
605 pub source_vertical_datum_epsg: Option<u32>,
606 pub target_vertical_datum_epsg: Option<u32>,
607 pub source_unit_to_meter: Option<f64>,
608 pub target_unit_to_meter: Option<f64>,
609 pub accuracy: Option<OperationAccuracy>,
610 pub area_of_use: Option<AreaOfUse>,
611 pub area_of_use_match: Option<bool>,
612 pub grids: Vec<VerticalGridProvenance>,
613}
614
615#[derive(Debug, Clone, PartialEq)]
616pub struct OperationSelectionDiagnostics {
617 pub selected_operation: CoordinateOperationMetadata,
618 pub selected_match_kind: OperationMatchKind,
619 pub selected_reasons: SmallVec<[SelectionReason; 4]>,
620 pub fallback_operations: Vec<CoordinateOperationMetadata>,
621 pub skipped_operations: Vec<SkippedOperation>,
622 pub approximate: bool,
623 pub missing_required_grid: Option<String>,
624}
625
626#[derive(Debug, Clone, PartialEq)]
627pub struct GridCoverageMiss {
628 pub operation: std::sync::Arc<CoordinateOperationMetadata>,
631 pub detail: String,
632}
633
634#[derive(Debug, Clone, PartialEq)]
635pub struct TransformOutcome<T> {
636 pub coord: T,
637 pub operation: std::sync::Arc<CoordinateOperationMetadata>,
640 pub vertical: VerticalTransformDiagnostics,
641 pub grid_coverage_misses: Vec<GridCoverageMiss>,
642}
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647 use crate::grid::{EmbeddedGridProvider, GridDefinition, GridFormat};
648
649 fn vertical_grid_operation(
650 name: &str,
651 source_vertical_crs_epsg: Option<u32>,
652 target_vertical_crs_epsg: Option<u32>,
653 ) -> VerticalGridOperation {
654 VerticalGridOperation {
655 name: name.into(),
656 grid: GridDefinition {
657 id: GridId(1),
658 name: format!("{name}.gtx"),
659 format: GridFormat::Gtx,
660 interpolation: GridInterpolation::Bilinear,
661 area_of_use: None,
662 resource_names: smallvec::SmallVec::from_vec(vec![format!("{name}.gtx")]),
663 },
664 grid_horizontal_crs_epsg: Some(4326),
665 source_vertical_crs_epsg,
666 target_vertical_crs_epsg,
667 source_vertical_datum_epsg: Some(1),
668 target_vertical_datum_epsg: Some(2),
669 accuracy: Some(OperationAccuracy { meters: 0.1 }),
670 area_of_use: None,
671 offset_convention: VerticalGridOffsetConvention::GeoidHeightMeters,
672 }
673 }
674
675 fn coordinate_operation(name: &str) -> CoordinateOperation {
676 CoordinateOperation {
677 id: None,
678 name: name.into(),
679 source_crs_epsg: None,
680 target_crs_epsg: None,
681 source_datum_epsg: None,
682 target_datum_epsg: None,
683 accuracy: Some(OperationAccuracy { meters: 0.0 }),
684 areas_of_use: smallvec::SmallVec::new(),
685 deprecated: false,
686 preferred: true,
687 approximate: false,
688 superseded: false,
689 method: OperationMethod::Identity,
690 }
691 }
692
693 #[test]
694 fn selection_options_builders_chain_advanced_options() {
695 let area = AreaOfInterest::geographic_point(Coord::new(-74.0, 40.0));
696 let provider: Arc<dyn crate::grid::GridProvider> = Arc::new(EmbeddedGridProvider);
697 let first_operation = coordinate_operation("first operation");
698 let second_operation = coordinate_operation("second operation");
699 let first = vertical_grid_operation("first", Some(4979), Some(5703));
700 let second = vertical_grid_operation("second", Some(4979), Some(5703));
701
702 let options = SelectionOptions::new()
703 .with_area_of_interest(area)
704 .with_area_bounds_densify_points(32)
705 .require_grids()
706 .with_grid_provider(provider.clone())
707 .with_coordinate_operation(first_operation.clone())
708 .with_coordinate_operations([second_operation.clone()])
709 .with_vertical_grid_operation(first.clone())
710 .with_vertical_grid_operations([second.clone()]);
711
712 assert_eq!(options.area_of_interest, Some(area));
713 assert_eq!(options.area_bounds_densify_points, 32);
714 assert!(matches!(options.policy, SelectionPolicy::RequireGrids));
715 assert!(Arc::ptr_eq(
716 options.grid_provider.as_ref().unwrap(),
717 &provider
718 ));
719 assert_eq!(
720 options.coordinate_operations,
721 vec![first_operation, second_operation]
722 );
723 assert_eq!(options.vertical_grid_operations, vec![first, second]);
724 }
725
726 #[test]
727 fn geographic_wrapped_bounds_constructor_marks_antimeridian_aoi() {
728 let bounds = Bounds::new(170.0, -20.0, -170.0, -10.0);
729 let area = AreaOfInterest::geographic_wrapped_bounds(bounds);
730
731 assert_eq!(area.crs, AreaOfInterestCrs::GeographicDegreesWrapped);
732 assert_eq!(area.bounds, Some(bounds));
733 assert_eq!(area.point, None);
734 assert_eq!(area.inverse(), area);
735 }
736
737 #[test]
738 fn area_of_use_contains_antimeridian_points_and_bounds() {
739 let area = AreaOfUse {
740 west: 160.0,
741 south: -25.0,
742 east: -160.0,
743 north: -5.0,
744 name: "Pacific antimeridian test area".into(),
745 };
746
747 assert!(area.contains_point(Coord::new(170.0, -15.0)));
748 assert!(area.contains_point(Coord::new(-170.0, -15.0)));
749 assert!(!area.contains_point(Coord::new(0.0, -15.0)));
750 assert!(area.contains_bounds(Bounds::new(170.0, -20.0, -170.0, -10.0)));
751 assert!(!area.contains_bounds(Bounds::new(150.0, -20.0, -170.0, -10.0)));
752
753 let world = AreaOfUse {
754 west: -180.0,
755 south: -90.0,
756 east: 180.0,
757 north: 90.0,
758 name: "World".into(),
759 };
760 assert!(world.contains_bounds(Bounds::new(170.0, -20.0, -170.0, -10.0)));
761 }
762
763 #[test]
764 fn selection_options_policy_builders_cover_all_modes() {
765 assert!(matches!(
766 SelectionOptions::new().best_available().policy,
767 SelectionPolicy::BestAvailable
768 ));
769 assert!(matches!(
770 SelectionOptions::new().require_exact_area_match().policy,
771 SelectionPolicy::RequireExactAreaMatch
772 ));
773 assert!(matches!(
774 SelectionOptions::new()
775 .with_operation(CoordinateOperationId(1234))
776 .policy,
777 SelectionPolicy::Operation(CoordinateOperationId(1234))
778 ));
779 }
780
781 #[test]
782 fn selection_options_inverse_preserves_builder_values() {
783 let options = SelectionOptions::new()
784 .with_area_of_interest(AreaOfInterest::source_crs_point(Coord::new(1.0, 2.0)))
785 .with_area_bounds_densify_points(32)
786 .with_policy(SelectionPolicy::RequireExactAreaMatch)
787 .with_coordinate_operation(coordinate_operation("operation"))
788 .with_vertical_grid_operation(vertical_grid_operation("grid", Some(4979), Some(5703)));
789
790 let inverse = options.inverse();
791
792 assert!(matches!(
793 inverse.area_of_interest,
794 Some(AreaOfInterest {
795 crs: AreaOfInterestCrs::TargetCrs,
796 point: Some(Coord { x: 1.0, y: 2.0 }),
797 bounds: None,
798 })
799 ));
800 assert!(matches!(
801 inverse.policy,
802 SelectionPolicy::RequireExactAreaMatch
803 ));
804 assert_eq!(inverse.area_bounds_densify_points, 32);
805 assert_eq!(inverse.coordinate_operations, options.coordinate_operations);
806 assert_eq!(
807 inverse.vertical_grid_operations[0].source_vertical_crs_epsg,
808 Some(5703)
809 );
810 assert_eq!(
811 inverse.vertical_grid_operations[0].target_vertical_crs_epsg,
812 Some(4979)
813 );
814 }
815}