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#[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 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#[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#[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}