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