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        point.x >= self.west
29            && point.x <= self.east
30            && point.y >= self.south
31            && point.y <= self.north
32    }
33
34    pub fn contains_bounds(&self, bounds: Bounds) -> bool {
35        bounds.min_x >= self.west
36            && bounds.max_x <= self.east
37            && bounds.min_y >= self.south
38            && bounds.max_y <= self.north
39    }
40}
41
42/// Nominal operation accuracy in meters.
43#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct OperationAccuracy {
45    pub meters: f64,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum OperationStepDirection {
50    Forward,
51    Reverse,
52}
53
54impl OperationStepDirection {
55    pub fn inverse(self) -> Self {
56        match self {
57            Self::Forward => Self::Reverse,
58            Self::Reverse => Self::Forward,
59        }
60    }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum AreaOfInterestCrs {
65    GeographicDegrees,
66    SourceCrs,
67    TargetCrs,
68}
69
70impl AreaOfInterestCrs {
71    pub fn inverse(self) -> Self {
72        match self {
73            Self::GeographicDegrees => Self::GeographicDegrees,
74            Self::SourceCrs => Self::TargetCrs,
75            Self::TargetCrs => Self::SourceCrs,
76        }
77    }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq)]
81pub struct AreaOfInterest {
82    pub crs: AreaOfInterestCrs,
83    pub point: Option<Coord>,
84    pub bounds: Option<Bounds>,
85}
86
87impl AreaOfInterest {
88    pub fn geographic_point(point: Coord) -> Self {
89        Self {
90            crs: AreaOfInterestCrs::GeographicDegrees,
91            point: Some(point),
92            bounds: None,
93        }
94    }
95
96    pub fn geographic_bounds(bounds: Bounds) -> Self {
97        Self {
98            crs: AreaOfInterestCrs::GeographicDegrees,
99            point: None,
100            bounds: Some(bounds),
101        }
102    }
103
104    pub fn source_crs_point(point: Coord) -> Self {
105        Self {
106            crs: AreaOfInterestCrs::SourceCrs,
107            point: Some(point),
108            bounds: None,
109        }
110    }
111
112    pub fn source_crs_bounds(bounds: Bounds) -> Self {
113        Self {
114            crs: AreaOfInterestCrs::SourceCrs,
115            point: None,
116            bounds: Some(bounds),
117        }
118    }
119
120    pub fn target_crs_point(point: Coord) -> Self {
121        Self {
122            crs: AreaOfInterestCrs::TargetCrs,
123            point: Some(point),
124            bounds: None,
125        }
126    }
127
128    pub fn target_crs_bounds(bounds: Bounds) -> Self {
129        Self {
130            crs: AreaOfInterestCrs::TargetCrs,
131            point: None,
132            bounds: Some(bounds),
133        }
134    }
135
136    pub fn inverse(self) -> Self {
137        Self {
138            crs: self.crs.inverse(),
139            point: self.point,
140            bounds: self.bounds,
141        }
142    }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum GridInterpolation {
147    Bilinear,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum GridShiftDirection {
152    Forward,
153    Reverse,
154}
155
156impl GridShiftDirection {
157    pub fn inverse(self) -> Self {
158        match self {
159            Self::Forward => Self::Reverse,
160            Self::Reverse => Self::Forward,
161        }
162    }
163}
164
165#[derive(Debug, Clone, PartialEq)]
166pub struct OperationStep {
167    pub operation_id: CoordinateOperationId,
168    pub direction: OperationStepDirection,
169}
170
171/// Enum-backed operation method model used by selection and compilation.
172#[derive(Debug, Clone, PartialEq)]
173pub enum OperationMethod {
174    Identity,
175    Helmert {
176        params: HelmertParams,
177    },
178    GridShift {
179        grid_id: GridId,
180        interpolation: GridInterpolation,
181        direction: GridShiftDirection,
182    },
183    DatumShift {
184        source_to_wgs84: DatumToWgs84,
185        target_to_wgs84: DatumToWgs84,
186    },
187    Projection {
188        forward: bool,
189        method: ProjectionMethod,
190        linear_unit: LinearUnit,
191    },
192    AxisUnitNormalize,
193    Concatenated {
194        steps: SmallVec<[OperationStep; 4]>,
195    },
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub enum OperationMatchKind {
200    ExactSourceTarget,
201    DerivedGeographic,
202    DatumCompatible,
203    ApproximateFallback,
204    Explicit,
205}
206
207#[derive(Debug, Clone, PartialEq)]
208pub struct CoordinateOperation {
209    pub id: Option<CoordinateOperationId>,
210    pub name: String,
211    pub source_crs_epsg: Option<u32>,
212    pub target_crs_epsg: Option<u32>,
213    pub source_datum_epsg: Option<u32>,
214    pub target_datum_epsg: Option<u32>,
215    pub accuracy: Option<OperationAccuracy>,
216    pub areas_of_use: SmallVec<[AreaOfUse; 1]>,
217    pub deprecated: bool,
218    pub preferred: bool,
219    pub approximate: bool,
220    pub method: OperationMethod,
221}
222
223impl CoordinateOperation {
224    pub fn metadata(&self) -> CoordinateOperationMetadata {
225        CoordinateOperationMetadata {
226            id: self.id,
227            name: self.name.clone(),
228            direction: OperationStepDirection::Forward,
229            source_crs_epsg: self.source_crs_epsg,
230            target_crs_epsg: self.target_crs_epsg,
231            source_datum_epsg: self.source_datum_epsg,
232            target_datum_epsg: self.target_datum_epsg,
233            accuracy: self.accuracy,
234            area_of_use: self.areas_of_use.first().cloned(),
235            deprecated: self.deprecated,
236            preferred: self.preferred,
237            approximate: self.approximate,
238            uses_grids: self.uses_grids(),
239        }
240    }
241
242    pub fn metadata_for_direction(
243        &self,
244        direction: OperationStepDirection,
245    ) -> CoordinateOperationMetadata {
246        let mut metadata = self.metadata();
247        metadata.direction = direction;
248        if matches!(direction, OperationStepDirection::Reverse) {
249            std::mem::swap(&mut metadata.source_crs_epsg, &mut metadata.target_crs_epsg);
250            std::mem::swap(
251                &mut metadata.source_datum_epsg,
252                &mut metadata.target_datum_epsg,
253            );
254        }
255        metadata
256    }
257
258    pub fn uses_grids(&self) -> bool {
259        let mut visited = HashSet::new();
260        self.uses_grids_with_visited(&mut visited)
261    }
262
263    fn uses_grids_with_visited(&self, visited: &mut HashSet<CoordinateOperationId>) -> bool {
264        match &self.method {
265            OperationMethod::GridShift { .. } => true,
266            OperationMethod::DatumShift {
267                source_to_wgs84,
268                target_to_wgs84,
269            } => source_to_wgs84.uses_grid_shift() || target_to_wgs84.uses_grid_shift(),
270            OperationMethod::Concatenated { steps } => steps.iter().any(|step| {
271                if !visited.insert(step.operation_id) {
272                    return false;
273                }
274                let uses_grids = crate::registry::lookup_operation(step.operation_id)
275                    .map(|operation| operation.uses_grids_with_visited(visited))
276                    .unwrap_or(false);
277                visited.remove(&step.operation_id);
278                uses_grids
279            }),
280            _ => false,
281        }
282    }
283}
284
285#[derive(Debug, Clone, PartialEq)]
286pub struct CoordinateOperationMetadata {
287    pub id: Option<CoordinateOperationId>,
288    pub name: String,
289    pub direction: OperationStepDirection,
290    pub source_crs_epsg: Option<u32>,
291    pub target_crs_epsg: Option<u32>,
292    pub source_datum_epsg: Option<u32>,
293    pub target_datum_epsg: Option<u32>,
294    pub accuracy: Option<OperationAccuracy>,
295    pub area_of_use: Option<AreaOfUse>,
296    pub deprecated: bool,
297    pub preferred: bool,
298    pub approximate: bool,
299    pub uses_grids: bool,
300}
301
302#[derive(Debug, Clone)]
303pub enum SelectionPolicy {
304    BestAvailable,
305    RequireGrids,
306    RequireExactAreaMatch,
307    AllowApproximateHelmertFallback,
308    Operation(CoordinateOperationId),
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub enum VerticalGridOffsetConvention {
313    /// Grid values are geoid heights in meters (`N`), applied as
314    /// gravity height `H = h - N` and ellipsoidal height `h = H + N`.
315    GeoidHeightMeters,
316}
317
318#[derive(Debug, Clone, PartialEq)]
319pub struct VerticalGridOperation {
320    /// Human-readable operation name used in diagnostics.
321    pub name: String,
322    /// Grid resource definition resolved through the configured grid provider.
323    pub grid: crate::grid::GridDefinition,
324    /// Horizontal CRS EPSG code in which the grid is sampled, when known.
325    pub grid_horizontal_crs_epsg: Option<u32>,
326    /// Optional source vertical CRS EPSG filter.
327    pub source_vertical_crs_epsg: Option<u32>,
328    /// Optional target vertical CRS EPSG filter.
329    pub target_vertical_crs_epsg: Option<u32>,
330    /// Optional source gravity-related vertical datum EPSG filter.
331    pub source_vertical_datum_epsg: Option<u32>,
332    /// Optional target gravity-related vertical datum EPSG filter.
333    pub target_vertical_datum_epsg: Option<u32>,
334    /// Expected operation accuracy in meters, when known.
335    pub accuracy: Option<OperationAccuracy>,
336    /// Operation area of use, when distinct from the grid's area.
337    pub area_of_use: Option<AreaOfUse>,
338    pub offset_convention: VerticalGridOffsetConvention,
339}
340
341impl VerticalGridOperation {
342    pub fn inverse(&self) -> Self {
343        let mut inverse = self.clone();
344        std::mem::swap(
345            &mut inverse.source_vertical_crs_epsg,
346            &mut inverse.target_vertical_crs_epsg,
347        );
348        std::mem::swap(
349            &mut inverse.source_vertical_datum_epsg,
350            &mut inverse.target_vertical_datum_epsg,
351        );
352        inverse
353    }
354}
355
356#[derive(Clone)]
357pub struct SelectionOptions {
358    pub area_of_interest: Option<AreaOfInterest>,
359    pub policy: SelectionPolicy,
360    pub grid_provider: Option<Arc<dyn crate::grid::GridProvider>>,
361    pub vertical_grid_operations: Vec<VerticalGridOperation>,
362}
363
364impl Default for SelectionOptions {
365    fn default() -> Self {
366        Self {
367            area_of_interest: None,
368            policy: SelectionPolicy::BestAvailable,
369            grid_provider: None,
370            vertical_grid_operations: Vec::new(),
371        }
372    }
373}
374
375impl SelectionOptions {
376    pub fn inverse(&self) -> Self {
377        Self {
378            area_of_interest: self.area_of_interest.map(AreaOfInterest::inverse),
379            policy: self.policy.clone(),
380            grid_provider: self.grid_provider.clone(),
381            vertical_grid_operations: self
382                .vertical_grid_operations
383                .iter()
384                .map(VerticalGridOperation::inverse)
385                .collect(),
386        }
387    }
388}
389
390#[derive(Debug, Clone, Copy, PartialEq, Eq)]
391pub enum SelectionReason {
392    ExplicitOperation,
393    ExactSourceTarget,
394    AreaOfUseMatch,
395    AccuracyPreferred,
396    NonDeprecated,
397    PreferredOperation,
398    ApproximateFallback,
399}
400
401#[derive(Debug, Clone, PartialEq, Eq)]
402pub enum SkippedOperationReason {
403    AreaOfUseMismatch,
404    MissingGrid,
405    UnsupportedGridFormat,
406    PolicyFiltered,
407    LessPreferred,
408    Deprecated,
409}
410
411#[derive(Debug, Clone, PartialEq)]
412pub struct SkippedOperation {
413    pub metadata: CoordinateOperationMetadata,
414    pub reason: SkippedOperationReason,
415    pub detail: String,
416}
417
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
419pub enum VerticalTransformAction {
420    /// No explicit vertical CRS participates in the transform.
421    None,
422    /// `z` is preserved because the vertical CRS semantics and units match.
423    Preserved,
424    /// `z` is converted between units of the same vertical reference frame.
425    UnitConverted,
426    /// `z` is transformed by an explicit vertical operation.
427    Transformed,
428}
429
430#[derive(Debug, Clone, PartialEq)]
431pub struct VerticalGridProvenance {
432    pub name: String,
433    /// Content checksum of the resolved grid resource, formatted as `sha256:<hex>`.
434    pub checksum: Option<String>,
435    pub accuracy: Option<OperationAccuracy>,
436    pub area_of_use: Option<AreaOfUse>,
437    pub area_of_use_match: Option<bool>,
438}
439
440#[derive(Debug, Clone, PartialEq)]
441pub struct VerticalTransformDiagnostics {
442    pub action: VerticalTransformAction,
443    pub operation_name: Option<String>,
444    pub source_vertical_crs_epsg: Option<u32>,
445    pub target_vertical_crs_epsg: Option<u32>,
446    pub source_vertical_datum_epsg: Option<u32>,
447    pub target_vertical_datum_epsg: Option<u32>,
448    pub source_unit_to_meter: Option<f64>,
449    pub target_unit_to_meter: Option<f64>,
450    pub accuracy: Option<OperationAccuracy>,
451    pub area_of_use: Option<AreaOfUse>,
452    pub area_of_use_match: Option<bool>,
453    pub grids: Vec<VerticalGridProvenance>,
454}
455
456#[derive(Debug, Clone, PartialEq)]
457pub struct OperationSelectionDiagnostics {
458    pub selected_operation: CoordinateOperationMetadata,
459    pub selected_match_kind: OperationMatchKind,
460    pub selected_reasons: SmallVec<[SelectionReason; 4]>,
461    pub fallback_operations: Vec<CoordinateOperationMetadata>,
462    pub skipped_operations: Vec<SkippedOperation>,
463    pub approximate: bool,
464    pub missing_required_grid: Option<String>,
465}
466
467#[derive(Debug, Clone, PartialEq)]
468pub struct GridCoverageMiss {
469    pub operation: CoordinateOperationMetadata,
470    pub detail: String,
471}
472
473#[derive(Debug, Clone, PartialEq)]
474pub struct TransformOutcome<T> {
475    pub coord: T,
476    pub operation: CoordinateOperationMetadata,
477    pub vertical: VerticalTransformDiagnostics,
478    pub grid_coverage_misses: Vec<GridCoverageMiss>,
479}