Skip to main content

proj_core/
operation.rs

1use crate::coord::{Bounds, Coord};
2use crate::crs::{LinearUnit, ProjectionMethod};
3use crate::datum::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    Projection {
184        forward: bool,
185        method: ProjectionMethod,
186        linear_unit: LinearUnit,
187    },
188    AxisUnitNormalize,
189    Concatenated {
190        steps: SmallVec<[OperationStep; 4]>,
191    },
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub enum OperationMatchKind {
196    ExactSourceTarget,
197    DerivedGeographic,
198    DatumCompatible,
199    ApproximateFallback,
200    Explicit,
201}
202
203#[derive(Debug, Clone, PartialEq)]
204pub struct CoordinateOperation {
205    pub id: Option<CoordinateOperationId>,
206    pub name: String,
207    pub source_crs_epsg: Option<u32>,
208    pub target_crs_epsg: Option<u32>,
209    pub source_datum_epsg: Option<u32>,
210    pub target_datum_epsg: Option<u32>,
211    pub accuracy: Option<OperationAccuracy>,
212    pub areas_of_use: SmallVec<[AreaOfUse; 1]>,
213    pub deprecated: bool,
214    pub preferred: bool,
215    pub approximate: bool,
216    pub method: OperationMethod,
217}
218
219impl CoordinateOperation {
220    pub fn metadata(&self) -> CoordinateOperationMetadata {
221        CoordinateOperationMetadata {
222            id: self.id,
223            name: self.name.clone(),
224            source_crs_epsg: self.source_crs_epsg,
225            target_crs_epsg: self.target_crs_epsg,
226            source_datum_epsg: self.source_datum_epsg,
227            target_datum_epsg: self.target_datum_epsg,
228            accuracy: self.accuracy,
229            area_of_use: self.areas_of_use.first().cloned(),
230            deprecated: self.deprecated,
231            preferred: self.preferred,
232            approximate: self.approximate,
233            uses_grids: self.uses_grids(),
234        }
235    }
236
237    pub fn metadata_for_direction(
238        &self,
239        direction: OperationStepDirection,
240    ) -> CoordinateOperationMetadata {
241        let mut metadata = self.metadata();
242        if matches!(direction, OperationStepDirection::Reverse) {
243            std::mem::swap(&mut metadata.source_crs_epsg, &mut metadata.target_crs_epsg);
244            std::mem::swap(
245                &mut metadata.source_datum_epsg,
246                &mut metadata.target_datum_epsg,
247            );
248        }
249        metadata
250    }
251
252    pub fn uses_grids(&self) -> bool {
253        let mut visited = HashSet::new();
254        self.uses_grids_with_visited(&mut visited)
255    }
256
257    fn uses_grids_with_visited(&self, visited: &mut HashSet<CoordinateOperationId>) -> bool {
258        match &self.method {
259            OperationMethod::GridShift { .. } => true,
260            OperationMethod::Concatenated { steps } => steps.iter().any(|step| {
261                if !visited.insert(step.operation_id) {
262                    return false;
263                }
264                let uses_grids = crate::registry::lookup_operation(step.operation_id)
265                    .map(|operation| operation.uses_grids_with_visited(visited))
266                    .unwrap_or(false);
267                visited.remove(&step.operation_id);
268                uses_grids
269            }),
270            _ => false,
271        }
272    }
273}
274
275#[derive(Debug, Clone, PartialEq)]
276pub struct CoordinateOperationMetadata {
277    pub id: Option<CoordinateOperationId>,
278    pub name: String,
279    pub source_crs_epsg: Option<u32>,
280    pub target_crs_epsg: Option<u32>,
281    pub source_datum_epsg: Option<u32>,
282    pub target_datum_epsg: Option<u32>,
283    pub accuracy: Option<OperationAccuracy>,
284    pub area_of_use: Option<AreaOfUse>,
285    pub deprecated: bool,
286    pub preferred: bool,
287    pub approximate: bool,
288    pub uses_grids: bool,
289}
290
291#[derive(Debug, Clone)]
292pub enum SelectionPolicy {
293    BestAvailable,
294    RequireGrids,
295    RequireExactAreaMatch,
296    AllowApproximateHelmertFallback,
297    Operation(CoordinateOperationId),
298}
299
300#[derive(Clone)]
301pub struct SelectionOptions {
302    pub area_of_interest: Option<AreaOfInterest>,
303    pub policy: SelectionPolicy,
304    pub grid_provider: Option<Arc<dyn crate::grid::GridProvider>>,
305}
306
307impl Default for SelectionOptions {
308    fn default() -> Self {
309        Self {
310            area_of_interest: None,
311            policy: SelectionPolicy::BestAvailable,
312            grid_provider: None,
313        }
314    }
315}
316
317impl SelectionOptions {
318    pub fn inverse(&self) -> Self {
319        Self {
320            area_of_interest: self.area_of_interest.map(AreaOfInterest::inverse),
321            policy: self.policy.clone(),
322            grid_provider: self.grid_provider.clone(),
323        }
324    }
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq)]
328pub enum SelectionReason {
329    ExplicitOperation,
330    ExactSourceTarget,
331    AreaOfUseMatch,
332    AccuracyPreferred,
333    NonDeprecated,
334    PreferredOperation,
335    ApproximateFallback,
336}
337
338#[derive(Debug, Clone, PartialEq, Eq)]
339pub enum SkippedOperationReason {
340    AreaOfUseMismatch,
341    MissingGrid,
342    UnsupportedGridFormat,
343    PolicyFiltered,
344    LessPreferred,
345    Deprecated,
346}
347
348#[derive(Debug, Clone, PartialEq)]
349pub struct SkippedOperation {
350    pub metadata: CoordinateOperationMetadata,
351    pub reason: SkippedOperationReason,
352    pub detail: String,
353}
354
355#[derive(Debug, Clone, PartialEq)]
356pub struct OperationSelectionDiagnostics {
357    pub selected_operation: CoordinateOperationMetadata,
358    pub selected_match_kind: OperationMatchKind,
359    pub selected_reasons: SmallVec<[SelectionReason; 4]>,
360    pub skipped_operations: Vec<SkippedOperation>,
361    pub approximate: bool,
362    pub missing_required_grid: Option<String>,
363}