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 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 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 GeoidHeightMeters,
316}
317
318#[derive(Debug, Clone, PartialEq)]
319pub struct VerticalGridOperation {
320 pub name: String,
322 pub grid: crate::grid::GridDefinition,
324 pub grid_horizontal_crs_epsg: Option<u32>,
326 pub source_vertical_crs_epsg: Option<u32>,
328 pub target_vertical_crs_epsg: Option<u32>,
330 pub source_vertical_datum_epsg: Option<u32>,
332 pub target_vertical_datum_epsg: Option<u32>,
334 pub accuracy: Option<OperationAccuracy>,
336 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 None,
422 Preserved,
424 UnitConverted,
426 Transformed,
428}
429
430#[derive(Debug, Clone, PartialEq)]
431pub struct VerticalGridProvenance {
432 pub name: String,
433 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}