Skip to main content

proj_core/
transform.rs

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
38/// A reusable coordinate transformation between two CRS.
39pub 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/// Trait for `geo-types` geometries that can be transformed as whole values.
53///
54/// Implementations transform coordinates in storage order and return the first
55/// coordinate error without producing a partially transformed geometry.
56#[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    /// Create a transform from authority code strings (e.g., `"EPSG:4326"`).
100    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    /// Create a transform with explicit selection options.
105    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    /// Create a transform from an explicit registry operation id.
116    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    /// Create a transform from EPSG codes directly.
129    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    /// Create a transform from explicit CRS definitions.
138    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    /// Create a horizontal-only transform from explicit CRS definitions.
143    ///
144    /// Compound CRS inputs are reduced to their horizontal component before
145    /// operation selection. This is intended for XY-only workflows where
146    /// vertical transformation is deliberately out of scope.
147    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    /// Create a horizontal-only transform from explicit CRS definitions with
156    /// operation-selection options.
157    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    /// Create a transform from explicit CRS definitions with operation-selection options.
172    ///
173    /// Use this when a custom CRS references grid resources and the transform
174    /// needs an application-supplied [`crate::grid::GridProvider`].
175    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    /// Transform a single coordinate.
201    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    /// Transform a whole `geo-types` geometry.
208    ///
209    /// This method is available only with the `geo-types` feature. It
210    /// transforms coordinates in geometry storage order and returns the first
211    /// coordinate error without producing a partial result.
212    #[cfg(feature = "geo-types")]
213    pub fn convert_geometry<T: TransformableGeometry>(&self, geometry: T) -> Result<T> {
214        geometry.transform_geometry(self)
215    }
216
217    /// Transform a single 3D coordinate.
218    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    /// Transform a single coordinate and report the operation actually used.
225    ///
226    /// This 2D API is XY-only: it does not apply or sample configured vertical
227    /// transforms.
228    ///
229    /// When the selected grid-backed operation misses grid coverage, this
230    /// reports the coverage misses and the lower-ranked fallback operation that
231    /// produced the result.
232    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    /// Transform a single 3D coordinate and report the operation actually used.
247    ///
248    /// When the selected grid-backed operation misses grid coverage, this
249    /// reports the coverage misses and the lower-ranked fallback operation that
250    /// produced the result.
251    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    /// Return the source CRS definition for this transform.
266    pub fn source_crs(&self) -> &CrsDef {
267        &self.source
268    }
269
270    /// Return the target CRS definition for this transform.
271    pub fn target_crs(&self) -> &CrsDef {
272        &self.target
273    }
274
275    /// Return metadata for the selected coordinate operation.
276    pub fn selected_operation(&self) -> &CoordinateOperationMetadata {
277        &self.selected_operation
278    }
279
280    /// Return selection diagnostics for this transform.
281    pub fn selection_diagnostics(&self) -> &OperationSelectionDiagnostics {
282        &self.diagnostics
283    }
284
285    /// Return diagnostics for the vertical component of this transform.
286    pub fn vertical_diagnostics(&self) -> &VerticalTransformDiagnostics {
287        self.vertical_transform.diagnostics()
288    }
289
290    /// Build the inverse transform by swapping the source and target CRS.
291    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    /// Reproject a 2D bounding box by sampling its perimeter.
362    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    /// Reproject a geographic bounding box that crosses the antimeridian.
373    ///
374    /// `bounds` is interpreted as west/south/east/north in source geographic
375    /// degrees and must satisfy `west > east`. Projected and normal
376    /// non-wrapped bounds should use [`Self::transform_bounds`].
377    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    /// Batch transform (sequential).
624    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    /// Batch transform of 3D coordinates (sequential).
629    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    /// Transform 2D coordinates in place without allocating.
634    ///
635    /// Coordinates before a failing coordinate are left converted; the failing
636    /// coordinate and subsequent coordinates are left unchanged.
637    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    /// Transform 3D coordinates in place without allocating.
645    ///
646    /// Coordinates before a failing coordinate are left converted; the failing
647    /// coordinate and subsequent coordinates are left unchanged.
648    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    /// Transform 2D coordinates from `input` into an existing `output` slice.
656    ///
657    /// `output` must have exactly the same length as `input`. This API performs
658    /// no allocation and does not require cloning input coordinates.
659    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    /// Transform 3D coordinates from `input` into an existing `output` slice.
668    ///
669    /// `output` must have exactly the same length as `input`. This API performs
670    /// no allocation and does not require cloning input coordinates.
671    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    /// Batch transform with Rayon parallelism.
680    #[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    /// Batch transform of 3D coordinates with adaptive Rayon parallelism.
698    #[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}