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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct CoordinateOperationId(pub u32);
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct GridId(pub u32);
15
16#[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#[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 GeographicDegrees,
96 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 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#[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 BestAvailable,
357 RequireGrids,
359 RequireExactAreaMatch,
361 AllowApproximateHelmertFallback,
364 Operation(CoordinateOperationId),
366}
367
368#[derive(Debug, Clone, Copy, PartialEq, Eq)]
369pub enum VerticalGridOffsetConvention {
370 GeoidHeightMeters,
373}
374
375#[derive(Debug, Clone, PartialEq)]
376pub struct VerticalGridOperation {
377 pub name: String,
379 pub grid: crate::grid::GridDefinition,
381 pub grid_horizontal_crs_epsg: Option<u32>,
383 pub source_vertical_crs_epsg: Option<u32>,
385 pub target_vertical_crs_epsg: Option<u32>,
387 pub source_vertical_datum_epsg: Option<u32>,
389 pub target_vertical_datum_epsg: Option<u32>,
391 pub accuracy: Option<OperationAccuracy>,
393 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 pub fn new() -> Self {
435 Self::default()
436 }
437
438 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 pub fn with_policy(mut self, policy: SelectionPolicy) -> Self {
446 self.policy = policy;
447 self
448 }
449
450 pub fn best_available(self) -> Self {
455 self.with_policy(SelectionPolicy::BestAvailable)
456 }
457
458 pub fn require_grids(self) -> Self {
460 self.with_policy(SelectionPolicy::RequireGrids)
461 }
462
463 pub fn require_exact_area_match(self) -> Self {
465 self.with_policy(SelectionPolicy::RequireExactAreaMatch)
466 }
467
468 pub fn allow_approximate_helmert_fallback(self) -> Self {
475 self.with_policy(SelectionPolicy::AllowApproximateHelmertFallback)
476 }
477
478 pub fn with_operation(self, operation_id: CoordinateOperationId) -> Self {
480 self.with_policy(SelectionPolicy::Operation(operation_id))
481 }
482
483 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 pub fn with_vertical_grid_operation(mut self, operation: VerticalGridOperation) -> Self {
491 self.vertical_grid_operations.push(operation);
492 self
493 }
494
495 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 None,
550 Preserved,
552 UnitConverted,
554 Transformed,
556}
557
558#[derive(Debug, Clone, PartialEq)]
559pub struct VerticalGridProvenance {
560 pub name: String,
561 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}