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(Clone)]
312pub struct SelectionOptions {
313    pub area_of_interest: Option<AreaOfInterest>,
314    pub policy: SelectionPolicy,
315    pub grid_provider: Option<Arc<dyn crate::grid::GridProvider>>,
316}
317
318impl Default for SelectionOptions {
319    fn default() -> Self {
320        Self {
321            area_of_interest: None,
322            policy: SelectionPolicy::BestAvailable,
323            grid_provider: None,
324        }
325    }
326}
327
328impl SelectionOptions {
329    pub fn inverse(&self) -> Self {
330        Self {
331            area_of_interest: self.area_of_interest.map(AreaOfInterest::inverse),
332            policy: self.policy.clone(),
333            grid_provider: self.grid_provider.clone(),
334        }
335    }
336}
337
338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
339pub enum SelectionReason {
340    ExplicitOperation,
341    ExactSourceTarget,
342    AreaOfUseMatch,
343    AccuracyPreferred,
344    NonDeprecated,
345    PreferredOperation,
346    ApproximateFallback,
347}
348
349#[derive(Debug, Clone, PartialEq, Eq)]
350pub enum SkippedOperationReason {
351    AreaOfUseMismatch,
352    MissingGrid,
353    UnsupportedGridFormat,
354    PolicyFiltered,
355    LessPreferred,
356    Deprecated,
357}
358
359#[derive(Debug, Clone, PartialEq)]
360pub struct SkippedOperation {
361    pub metadata: CoordinateOperationMetadata,
362    pub reason: SkippedOperationReason,
363    pub detail: String,
364}
365
366#[derive(Debug, Clone, PartialEq)]
367pub struct OperationSelectionDiagnostics {
368    pub selected_operation: CoordinateOperationMetadata,
369    pub selected_match_kind: OperationMatchKind,
370    pub selected_reasons: SmallVec<[SelectionReason; 4]>,
371    pub fallback_operations: Vec<CoordinateOperationMetadata>,
372    pub skipped_operations: Vec<SkippedOperation>,
373    pub approximate: bool,
374    pub missing_required_grid: Option<String>,
375}
376
377#[derive(Debug, Clone, PartialEq)]
378pub struct GridCoverageMiss {
379    pub operation: CoordinateOperationMetadata,
380    pub detail: String,
381}
382
383#[derive(Debug, Clone, PartialEq)]
384pub struct TransformOutcome<T> {
385    pub coord: T,
386    pub operation: CoordinateOperationMetadata,
387    pub grid_coverage_misses: Vec<GridCoverageMiss>,
388}