1use crate::coord::{Bounds, Coord, Coord3D, Transformable, Transformable3D};
2use crate::crs::CrsDef;
3use crate::error::{Error, Result};
4use crate::grid::{GridError, GridRuntime};
5use crate::operation::{
6 CoordinateOperation, CoordinateOperationId, CoordinateOperationMetadata, GridCoverageMiss,
7 OperationSelectionDiagnostics, OperationStepDirection, SelectionOptions, TransformOutcome,
8 VerticalTransformAction, VerticalTransformDiagnostics,
9};
10use crate::registry;
11
12#[cfg(feature = "geo-types")]
13mod geo_adapters;
14mod pipeline;
15mod selection;
16#[cfg(test)]
17mod tests;
18mod vertical;
19
20use pipeline::{
21 compile_pipeline, execute_pipeline_xy, validate_output_len, validate_pipeline_coord3d,
22 validate_transform_crs_definition, validate_vertical_ordinate, CompiledOperationFallback,
23 CompiledOperationPipeline, PipelineExecutionOutcome,
24};
25use selection::{
26 compile_selected_pipelines, grid_coverage_miss_detail, is_grid_coverage_miss, selected_metadata,
27};
28use vertical::{compile_vertical_transform, vertical_diagnostics, VerticalTransform};
29
30#[cfg(feature = "rayon")]
31use pipeline::should_parallelize;
32
33#[cfg(all(test, feature = "rayon"))]
34use pipeline::PARALLEL_MIN_ITEMS_PER_THREAD;
35#[cfg(test)]
36use pipeline::{PipelineSourceXyUnits, PipelineTargetXyUnits};
37
38pub struct Transform {
40 source: CrsDef,
41 target: CrsDef,
42 selected_operation_definition: CoordinateOperation,
43 selected_direction: OperationStepDirection,
44 selected_operation: CoordinateOperationMetadata,
45 diagnostics: OperationSelectionDiagnostics,
46 vertical_transform: VerticalTransform,
47 selection_options: SelectionOptions,
48 pipeline: CompiledOperationPipeline,
49 fallback_pipelines: Vec<CompiledOperationFallback>,
50}
51
52#[cfg(feature = "geo-types")]
57pub trait TransformableGeometry: Sized {
58 fn transform_geometry(self, transform: &Transform) -> Result<Self>;
59}
60
61fn validate_wrapped_geographic_transform_bounds(bounds: Bounds) -> Result<()> {
62 if !bounds.min_x.is_finite()
63 || !bounds.min_y.is_finite()
64 || !bounds.max_x.is_finite()
65 || !bounds.max_y.is_finite()
66 || bounds.min_x <= bounds.max_x
67 || bounds.min_y > bounds.max_y
68 {
69 return Err(Error::OutOfRange(
70 "wrapped geographic bounds must be finite and satisfy west > east and south <= north"
71 .into(),
72 ));
73 }
74
75 for point in [
76 Coord::new(bounds.min_x, bounds.min_y),
77 Coord::new(bounds.min_x, bounds.max_y),
78 Coord::new(bounds.max_x, bounds.min_y),
79 Coord::new(bounds.max_x, bounds.max_y),
80 ] {
81 if !(-180.0..=180.0).contains(&point.x) {
82 return Err(Error::OutOfRange(format!(
83 "wrapped geographic bounds longitude {:.8} degrees is outside [-180, 180]",
84 point.x
85 )));
86 }
87 if !(-90.0..=90.0).contains(&point.y) {
88 return Err(Error::OutOfRange(format!(
89 "wrapped geographic bounds latitude {:.8} degrees is outside [-90, 90]",
90 point.y
91 )));
92 }
93 }
94
95 Ok(())
96}
97
98impl Transform {
99 pub fn new(from_crs: &str, to_crs: &str) -> Result<Self> {
101 Self::with_selection_options(from_crs, to_crs, SelectionOptions::default())
102 }
103
104 pub fn with_selection_options(
106 from_crs: &str,
107 to_crs: &str,
108 options: SelectionOptions,
109 ) -> Result<Self> {
110 let source = registry::lookup_authority_code(from_crs)?;
111 let target = registry::lookup_authority_code(to_crs)?;
112 Self::from_crs_defs_with_selection_options(&source, &target, options)
113 }
114
115 pub fn from_operation(
117 operation_id: CoordinateOperationId,
118 from_crs: &str,
119 to_crs: &str,
120 ) -> Result<Self> {
121 Self::with_selection_options(
122 from_crs,
123 to_crs,
124 SelectionOptions::new().with_operation(operation_id),
125 )
126 }
127
128 pub fn from_epsg(from: u32, to: u32) -> Result<Self> {
130 let source = registry::lookup_epsg(from)
131 .ok_or_else(|| Error::UnknownCrs(format!("unknown EPSG code: {from}")))?;
132 let target = registry::lookup_epsg(to)
133 .ok_or_else(|| Error::UnknownCrs(format!("unknown EPSG code: {to}")))?;
134 Self::from_crs_defs(&source, &target)
135 }
136
137 pub fn from_crs_defs(from: &CrsDef, to: &CrsDef) -> Result<Self> {
139 Self::from_crs_defs_with_selection_options(from, to, SelectionOptions::default())
140 }
141
142 pub fn from_horizontal_components(from: &CrsDef, to: &CrsDef) -> Result<Self> {
148 Self::from_horizontal_components_with_selection_options(
149 from,
150 to,
151 SelectionOptions::default(),
152 )
153 }
154
155 pub fn from_horizontal_components_with_selection_options(
158 from: &CrsDef,
159 to: &CrsDef,
160 options: SelectionOptions,
161 ) -> Result<Self> {
162 let source = from.horizontal_crs().ok_or_else(|| {
163 Error::InvalidDefinition("source CRS does not contain a horizontal component".into())
164 })?;
165 let target = to.horizontal_crs().ok_or_else(|| {
166 Error::InvalidDefinition("target CRS does not contain a horizontal component".into())
167 })?;
168 Self::from_crs_defs_with_selection_options(&source, &target, options)
169 }
170
171 pub fn from_crs_defs_with_selection_options(
176 from: &CrsDef,
177 to: &CrsDef,
178 options: SelectionOptions,
179 ) -> Result<Self> {
180 validate_transform_crs_definition(from)?;
181 validate_transform_crs_definition(to)?;
182
183 let grid_runtime = GridRuntime::new(options.grid_provider.clone());
184 let vertical_transform = compile_vertical_transform(from, to, &options, &grid_runtime)?;
185 let selected = compile_selected_pipelines(from, to, &options, &grid_runtime)?;
186 Ok(Self {
187 source: from.clone(),
188 target: to.clone(),
189 selected_operation_definition: selected.operation,
190 selected_direction: selected.direction,
191 selected_operation: selected.metadata,
192 diagnostics: selected.diagnostics,
193 vertical_transform,
194 selection_options: options,
195 pipeline: selected.pipeline,
196 fallback_pipelines: selected.fallback_pipelines,
197 })
198 }
199
200 pub fn convert<T: Transformable>(&self, coord: T) -> Result<T> {
202 let c = coord.into_coord();
203 let result = self.convert_coord(c)?;
204 Ok(T::from_coord(result))
205 }
206
207 #[cfg(feature = "geo-types")]
213 pub fn convert_geometry<T: TransformableGeometry>(&self, geometry: T) -> Result<T> {
214 geometry.transform_geometry(self)
215 }
216
217 pub fn convert_3d<T: Transformable3D>(&self, coord: T) -> Result<T> {
219 let c = coord.into_coord3d();
220 let result = self.convert_coord3d(c)?;
221 Ok(T::from_coord3d(result))
222 }
223
224 pub fn convert_with_diagnostics<T: Transformable>(
233 &self,
234 coord: T,
235 ) -> Result<TransformOutcome<T>> {
236 let c = coord.into_coord();
237 let outcome = self.convert_coord_with_diagnostics(c)?;
238 Ok(TransformOutcome {
239 coord: T::from_coord(outcome.coord),
240 operation: outcome.operation,
241 vertical: outcome.vertical,
242 grid_coverage_misses: outcome.grid_coverage_misses,
243 })
244 }
245
246 pub fn convert_3d_with_diagnostics<T: Transformable3D>(
252 &self,
253 coord: T,
254 ) -> Result<TransformOutcome<T>> {
255 let c = coord.into_coord3d();
256 let outcome = self.convert_coord3d_with_diagnostics(c)?;
257 Ok(TransformOutcome {
258 coord: T::from_coord3d(outcome.coord),
259 operation: outcome.operation,
260 vertical: outcome.vertical,
261 grid_coverage_misses: outcome.grid_coverage_misses,
262 })
263 }
264
265 pub fn source_crs(&self) -> &CrsDef {
267 &self.source
268 }
269
270 pub fn target_crs(&self) -> &CrsDef {
272 &self.target
273 }
274
275 pub fn selected_operation(&self) -> &CoordinateOperationMetadata {
277 &self.selected_operation
278 }
279
280 pub fn selection_diagnostics(&self) -> &OperationSelectionDiagnostics {
282 &self.diagnostics
283 }
284
285 pub fn vertical_diagnostics(&self) -> &VerticalTransformDiagnostics {
287 self.vertical_transform.diagnostics()
288 }
289
290 pub fn inverse(&self) -> Result<Self> {
292 let grid_runtime = GridRuntime::new(self.selection_options.grid_provider.clone());
293 let inverse_options = self.selection_options.inverse();
294 let vertical_transform = compile_vertical_transform(
295 &self.target,
296 &self.source,
297 &inverse_options,
298 &grid_runtime,
299 )?;
300 let selected_direction = self.selected_direction.inverse();
301 let pipeline = compile_pipeline(
302 &self.target,
303 &self.source,
304 &self.selected_operation_definition,
305 selected_direction,
306 &grid_runtime,
307 )?;
308 let selected_operation = selected_metadata(
309 &self.selected_operation_definition,
310 selected_direction,
311 self.selected_operation.area_of_use.clone(),
312 );
313 let mut fallback_pipelines = Vec::with_capacity(self.fallback_pipelines.len());
314 for fallback in &self.fallback_pipelines {
315 let direction = fallback.direction.inverse();
316 let pipeline = compile_pipeline(
317 &self.target,
318 &self.source,
319 &fallback.operation,
320 direction,
321 &grid_runtime,
322 )?;
323 let metadata = selected_metadata(
324 &fallback.operation,
325 direction,
326 fallback.metadata.area_of_use.clone(),
327 );
328 fallback_pipelines.push(CompiledOperationFallback {
329 operation: fallback.operation.clone(),
330 direction,
331 metadata,
332 pipeline,
333 });
334 }
335 let diagnostics = OperationSelectionDiagnostics {
336 selected_operation: selected_operation.clone(),
337 selected_match_kind: self.diagnostics.selected_match_kind,
338 selected_reasons: self.diagnostics.selected_reasons.clone(),
339 fallback_operations: fallback_pipelines
340 .iter()
341 .map(|fallback| fallback.metadata.clone())
342 .collect(),
343 skipped_operations: Vec::new(),
344 approximate: self.diagnostics.approximate,
345 missing_required_grid: self.diagnostics.missing_required_grid.clone(),
346 };
347 Ok(Self {
348 source: self.target.clone(),
349 target: self.source.clone(),
350 selected_operation_definition: self.selected_operation_definition.clone(),
351 selected_direction,
352 selected_operation,
353 diagnostics,
354 vertical_transform,
355 selection_options: inverse_options,
356 pipeline,
357 fallback_pipelines,
358 })
359 }
360
361 pub fn transform_bounds(&self, bounds: Bounds, densify_points: usize) -> Result<Bounds> {
363 if !bounds.is_valid() {
364 return Err(Error::OutOfRange(
365 "bounds must be finite and satisfy min <= max".into(),
366 ));
367 }
368
369 self.transform_valid_bounds(bounds, densify_points)
370 }
371
372 pub fn transform_geographic_wrapped_bounds(
378 &self,
379 bounds: Bounds,
380 densify_points: usize,
381 ) -> Result<Bounds> {
382 if !self.source.is_geographic() {
383 return Err(Error::InvalidDefinition(
384 "wrapped geographic bounds require a geographic source CRS".into(),
385 ));
386 }
387 validate_wrapped_geographic_transform_bounds(bounds)?;
388
389 let west_segment = Bounds::new(bounds.min_x, bounds.min_y, 180.0, bounds.max_y);
390 let east_segment = Bounds::new(-180.0, bounds.min_y, bounds.max_x, bounds.max_y);
391 let mut transformed = self.transform_valid_bounds(west_segment, densify_points)?;
392 let east_transformed = self.transform_valid_bounds(east_segment, densify_points)?;
393 transformed.expand_to_include(Coord::new(east_transformed.min_x, east_transformed.min_y));
394 transformed.expand_to_include(Coord::new(east_transformed.max_x, east_transformed.max_y));
395 Ok(transformed)
396 }
397
398 fn transform_valid_bounds(&self, bounds: Bounds, densify_points: usize) -> Result<Bounds> {
399 let segments = densify_points
400 .checked_add(1)
401 .ok_or_else(|| Error::OutOfRange("densify point count is too large".into()))?;
402
403 let mut transformed: Option<Bounds> = None;
404 for i in 0..=segments {
405 let t = i as f64 / segments as f64;
406 let x = bounds.min_x + bounds.width() * t;
407 let y = bounds.min_y + bounds.height() * t;
408
409 for sample in [
410 Coord::new(x, bounds.min_y),
411 Coord::new(x, bounds.max_y),
412 Coord::new(bounds.min_x, y),
413 Coord::new(bounds.max_x, y),
414 ] {
415 let coord = self.convert_coord(sample)?;
416 if let Some(accum) = &mut transformed {
417 accum.expand_to_include(coord);
418 } else {
419 transformed = Some(Bounds::new(coord.x, coord.y, coord.x, coord.y));
420 }
421 }
422 }
423
424 transformed.ok_or_else(|| Error::OutOfRange("failed to sample bounds".into()))
425 }
426
427 fn convert_coord(&self, c: Coord) -> Result<Coord> {
428 match execute_pipeline_xy(&self.pipeline, Coord3D::new(c.x, c.y, 0.0)) {
429 Ok(coord) => return Ok(coord),
430 Err(error) => {
431 if !is_grid_coverage_miss(&error) {
432 return Err(error);
433 }
434 }
435 }
436
437 for fallback in &self.fallback_pipelines {
438 match execute_pipeline_xy(&fallback.pipeline, Coord3D::new(c.x, c.y, 0.0)) {
439 Ok(coord) => return Ok(coord),
440 Err(error) => {
441 if !is_grid_coverage_miss(&error) {
442 return Err(error);
443 }
444 }
445 }
446 }
447
448 Err(Error::Grid(GridError::OutsideCoverage(
449 "grid coverage miss".into(),
450 )))
451 }
452
453 fn convert_coord3d(&self, c: Coord3D) -> Result<Coord3D> {
454 match self.execute_pipeline_coord3d(&self.pipeline, c) {
455 Ok(coord) => return Ok(coord),
456 Err(error) => {
457 if !is_grid_coverage_miss(&error) {
458 return Err(error);
459 }
460 }
461 }
462
463 for fallback in &self.fallback_pipelines {
464 match self.execute_pipeline_coord3d(&fallback.pipeline, c) {
465 Ok(coord) => return Ok(coord),
466 Err(error) => {
467 if !is_grid_coverage_miss(&error) {
468 return Err(error);
469 }
470 }
471 }
472 }
473
474 Err(Error::Grid(GridError::OutsideCoverage(
475 "grid coverage miss".into(),
476 )))
477 }
478
479 fn convert_coord_with_diagnostics(&self, c: Coord) -> Result<TransformOutcome<Coord>> {
480 let mut grid_coverage_misses = Vec::new();
481 let c = Coord3D::new(c.x, c.y, 0.0);
482
483 match execute_pipeline_xy(&self.pipeline, c) {
484 Ok(coord) => {
485 return Ok(TransformOutcome {
486 coord,
487 operation: self.selected_operation.clone(),
488 vertical: vertical_diagnostics(VerticalTransformAction::None, None, None, None),
489 grid_coverage_misses,
490 });
491 }
492 Err(error) => {
493 if let Some(detail) = grid_coverage_miss_detail(&error) {
494 grid_coverage_misses.push(GridCoverageMiss {
495 operation: self.selected_operation.clone(),
496 detail,
497 });
498 } else {
499 return Err(error);
500 }
501 }
502 }
503
504 for fallback in &self.fallback_pipelines {
505 match execute_pipeline_xy(&fallback.pipeline, c) {
506 Ok(coord) => {
507 return Ok(TransformOutcome {
508 coord,
509 operation: fallback.metadata.clone(),
510 vertical: vertical_diagnostics(
511 VerticalTransformAction::None,
512 None,
513 None,
514 None,
515 ),
516 grid_coverage_misses,
517 });
518 }
519 Err(error) => {
520 if let Some(detail) = grid_coverage_miss_detail(&error) {
521 grid_coverage_misses.push(GridCoverageMiss {
522 operation: fallback.metadata.clone(),
523 detail,
524 });
525 } else {
526 return Err(error);
527 }
528 }
529 }
530 }
531
532 Err(Error::Grid(GridError::OutsideCoverage(
533 grid_coverage_misses
534 .last()
535 .map(|miss| miss.detail.clone())
536 .unwrap_or_else(|| "grid coverage miss".into()),
537 )))
538 }
539
540 fn convert_coord3d_with_diagnostics(&self, c: Coord3D) -> Result<TransformOutcome<Coord3D>> {
541 let mut grid_coverage_misses = Vec::new();
542 match self.execute_pipeline(&self.pipeline, c) {
543 Ok(outcome) => {
544 return Ok(TransformOutcome {
545 coord: outcome.coord,
546 operation: self.selected_operation.clone(),
547 vertical: outcome.vertical,
548 grid_coverage_misses,
549 });
550 }
551 Err(error) => {
552 if let Some(detail) = grid_coverage_miss_detail(&error) {
553 grid_coverage_misses.push(GridCoverageMiss {
554 operation: self.selected_operation.clone(),
555 detail,
556 });
557 } else {
558 return Err(error);
559 }
560 }
561 }
562
563 for fallback in &self.fallback_pipelines {
564 match self.execute_pipeline(&fallback.pipeline, c) {
565 Ok(outcome) => {
566 return Ok(TransformOutcome {
567 coord: outcome.coord,
568 operation: fallback.metadata.clone(),
569 vertical: outcome.vertical,
570 grid_coverage_misses,
571 });
572 }
573 Err(error) => {
574 if let Some(detail) = grid_coverage_miss_detail(&error) {
575 grid_coverage_misses.push(GridCoverageMiss {
576 operation: fallback.metadata.clone(),
577 detail,
578 });
579 } else {
580 return Err(error);
581 }
582 }
583 }
584 }
585
586 Err(Error::Grid(GridError::OutsideCoverage(
587 grid_coverage_misses
588 .last()
589 .map(|miss| miss.detail.clone())
590 .unwrap_or_else(|| "grid coverage miss".into()),
591 )))
592 }
593
594 fn execute_pipeline(
595 &self,
596 pipeline: &CompiledOperationPipeline,
597 c: Coord3D,
598 ) -> Result<PipelineExecutionOutcome> {
599 validate_vertical_ordinate(c.z)?;
600 let xy = execute_pipeline_xy(pipeline, c)?;
601 let vertical = self.vertical_transform.apply(c)?;
602 let coord = Coord3D::new(xy.x, xy.y, vertical.z);
603 validate_pipeline_coord3d("pipeline final output", coord)?;
604 Ok(PipelineExecutionOutcome {
605 coord,
606 vertical: vertical.diagnostics,
607 })
608 }
609
610 fn execute_pipeline_coord3d(
611 &self,
612 pipeline: &CompiledOperationPipeline,
613 c: Coord3D,
614 ) -> Result<Coord3D> {
615 validate_vertical_ordinate(c.z)?;
616 let xy = execute_pipeline_xy(pipeline, c)?;
617 let z = self.vertical_transform.apply_z(c)?;
618 let coord = Coord3D::new(xy.x, xy.y, z);
619 validate_pipeline_coord3d("pipeline final output", coord)?;
620 Ok(coord)
621 }
622
623 pub fn convert_batch<T: Transformable + Clone>(&self, coords: &[T]) -> Result<Vec<T>> {
625 coords.iter().map(|c| self.convert(c.clone())).collect()
626 }
627
628 pub fn convert_batch_3d<T: Transformable3D + Clone>(&self, coords: &[T]) -> Result<Vec<T>> {
630 coords.iter().map(|c| self.convert_3d(c.clone())).collect()
631 }
632
633 pub fn convert_coords_in_place(&self, coords: &mut [Coord]) -> Result<()> {
638 for coord in coords {
639 *coord = self.convert_coord(*coord)?;
640 }
641 Ok(())
642 }
643
644 pub fn convert_coords_3d_in_place(&self, coords: &mut [Coord3D]) -> Result<()> {
649 for coord in coords {
650 *coord = self.convert_coord3d(*coord)?;
651 }
652 Ok(())
653 }
654
655 pub fn convert_coords_into(&self, input: &[Coord], output: &mut [Coord]) -> Result<()> {
660 validate_output_len(input.len(), output.len())?;
661 for (source, target) in input.iter().zip(output.iter_mut()) {
662 *target = self.convert_coord(*source)?;
663 }
664 Ok(())
665 }
666
667 pub fn convert_coords_3d_into(&self, input: &[Coord3D], output: &mut [Coord3D]) -> Result<()> {
672 validate_output_len(input.len(), output.len())?;
673 for (source, target) in input.iter().zip(output.iter_mut()) {
674 *target = self.convert_coord3d(*source)?;
675 }
676 Ok(())
677 }
678
679 #[cfg(feature = "rayon")]
681 pub fn convert_batch_parallel<T: Transformable + Send + Sync + Clone>(
682 &self,
683 coords: &[T],
684 ) -> Result<Vec<T>> {
685 if !should_parallelize(coords.len()) {
686 return self.convert_batch(coords);
687 }
688
689 use rayon::prelude::*;
690
691 coords
692 .par_iter()
693 .map(|coord| self.convert(coord.clone()))
694 .collect()
695 }
696
697 #[cfg(feature = "rayon")]
699 pub fn convert_batch_parallel_3d<T: Transformable3D + Send + Sync + Clone>(
700 &self,
701 coords: &[T],
702 ) -> Result<Vec<T>> {
703 if !should_parallelize(coords.len()) {
704 return self.convert_batch_3d(coords);
705 }
706
707 use rayon::prelude::*;
708
709 coords
710 .par_iter()
711 .map(|coord| self.convert_3d(coord.clone()))
712 .collect()
713 }
714}