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