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)]
12pub struct CoordinateOperationId(pub u32);
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct GridId(pub u32);
17
18#[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#[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 GeographicDegrees,
98 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 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#[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 BestAvailable,
359 RequireGrids,
361 RequireExactAreaMatch,
363 AllowApproximateHelmertFallback,
366 Operation(CoordinateOperationId),
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
371pub enum VerticalGridOffsetConvention {
372 GeoidHeightMeters,
375}
376
377#[derive(Debug, Clone, PartialEq)]
378pub struct VerticalGridOperation {
379 pub name: String,
381 pub grid: crate::grid::GridDefinition,
383 pub grid_horizontal_crs_epsg: Option<u32>,
385 pub source_vertical_crs_epsg: Option<u32>,
387 pub target_vertical_crs_epsg: Option<u32>,
389 pub source_vertical_datum_epsg: Option<u32>,
391 pub target_vertical_datum_epsg: Option<u32>,
393 pub accuracy: Option<OperationAccuracy>,
395 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 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 pub fn new() -> Self {
444 Self::default()
445 }
446
447 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 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 pub fn with_policy(mut self, policy: SelectionPolicy) -> Self {
465 self.policy = policy;
466 self
467 }
468
469 pub fn best_available(self) -> Self {
474 self.with_policy(SelectionPolicy::BestAvailable)
475 }
476
477 pub fn require_grids(self) -> Self {
479 self.with_policy(SelectionPolicy::RequireGrids)
480 }
481
482 pub fn require_exact_area_match(self) -> Self {
484 self.with_policy(SelectionPolicy::RequireExactAreaMatch)
485 }
486
487 pub fn allow_approximate_helmert_fallback(self) -> Self {
494 self.with_policy(SelectionPolicy::AllowApproximateHelmertFallback)
495 }
496
497 pub fn with_operation(self, operation_id: CoordinateOperationId) -> Self {
499 self.with_policy(SelectionPolicy::Operation(operation_id))
500 }
501
502 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 pub fn with_vertical_grid_operation(mut self, operation: VerticalGridOperation) -> Self {
510 self.vertical_grid_operations.push(operation);
511 self
512 }
513
514 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 None,
570 Preserved,
572 UnitConverted,
574 Transformed,
576}
577
578#[derive(Debug, Clone, PartialEq)]
579pub struct VerticalGridProvenance {
580 pub name: String,
581 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}