Skip to main content

proj_core/
transform.rs

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
40/// A reusable coordinate transformation between two CRS.
41pub 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/// Trait for `geo-types` geometries that can be transformed as whole values.
55///
56/// Implementations transform coordinates in storage order and return the first
57/// coordinate error without producing a partially transformed geometry.
58#[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    /// Create a transform from authority code strings (e.g., `"EPSG:4326"`).
102    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    /// Create a transform with explicit selection options.
107    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    /// Create a transform from an explicit registry operation id.
118    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    /// Create a transform from EPSG codes directly.
131    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    /// Create a transform from explicit CRS definitions.
140    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    /// Create a horizontal-only transform from explicit CRS definitions.
145    ///
146    /// Compound CRS inputs are reduced to their horizontal component before
147    /// operation selection. This is intended for XY-only workflows where
148    /// vertical transformation is deliberately out of scope.
149    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    /// Create a horizontal-only transform from explicit CRS definitions with
158    /// operation-selection options.
159    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    /// Create a transform from explicit CRS definitions with operation-selection options.
174    ///
175    /// Use this when a custom CRS references grid resources and the transform
176    /// needs an application-supplied [`crate::grid::GridProvider`].
177    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    /// Transform a single coordinate.
203    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    /// Transform a whole `geo-types` geometry.
210    ///
211    /// This method is available only with the `geo-types` feature. It
212    /// transforms coordinates in geometry storage order and returns the first
213    /// coordinate error without producing a partial result.
214    ///
215    /// `geo_types::Rect` is treated as a source bounds envelope and converted
216    /// to sampled axis-aligned target bounds with 21 intermediate points per
217    /// edge. This is an approximation for nonlinear projections: extrema can
218    /// occur between samples. Use [`Self::convert_rect`] when the rect sampling
219    /// density should be chosen by the caller.
220    #[cfg(feature = "geo-types")]
221    pub fn convert_geometry<T: TransformableGeometry>(&self, geometry: T) -> Result<T> {
222        geometry.transform_geometry(self)
223    }
224
225    /// Transform a `geo_types::Rect` to sampled axis-aligned target bounds.
226    ///
227    /// This method is available only with the `geo-types` feature. A rect
228    /// represents an envelope, not a true geometry, so nonlinear projections can
229    /// have edge extrema between samples. Increase `densify_points` to sample
230    /// edges more finely for higher-fidelity bounds, or use
231    /// [`Self::transform_bounds`] directly when working with [`Bounds`].
232    ///
233    /// `densify_points` is the number of intermediate samples added per edge
234    /// and must be no larger than [`MAX_BOUNDS_DENSIFY_POINTS`].
235    #[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    /// Transform a single 3D coordinate.
245    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    /// Transform a single coordinate and report the operation actually used.
252    ///
253    /// This 2D API is XY-only: it does not apply or sample configured vertical
254    /// transforms.
255    ///
256    /// When the selected grid-backed operation misses grid coverage, this
257    /// reports the coverage misses and the lower-ranked fallback operation that
258    /// produced the result.
259    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    /// Transform a single 3D coordinate and report the operation actually used.
274    ///
275    /// When the selected grid-backed operation misses grid coverage, this
276    /// reports the coverage misses and the lower-ranked fallback operation that
277    /// produced the result.
278    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    /// Return the source CRS definition for this transform.
293    pub fn source_crs(&self) -> &CrsDef {
294        &self.source
295    }
296
297    /// Return the target CRS definition for this transform.
298    pub fn target_crs(&self) -> &CrsDef {
299        &self.target
300    }
301
302    /// Return metadata for the selected coordinate operation.
303    pub fn selected_operation(&self) -> &CoordinateOperationMetadata {
304        &self.selected_operation
305    }
306
307    /// Return selection diagnostics for this transform.
308    pub fn selection_diagnostics(&self) -> &OperationSelectionDiagnostics {
309        &self.diagnostics
310    }
311
312    /// Return diagnostics for the vertical component of this transform.
313    pub fn vertical_diagnostics(&self) -> &VerticalTransformDiagnostics {
314        self.vertical_transform.diagnostics()
315    }
316
317    /// Build the inverse transform by swapping the source and target CRS.
318    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    /// Reproject a 2D bounding box by sampling its perimeter.
389    ///
390    /// `densify_points` is the number of intermediate samples added per edge
391    /// and must be no larger than [`MAX_BOUNDS_DENSIFY_POINTS`].
392    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    /// Reproject a geographic bounding box that crosses the antimeridian.
403    ///
404    /// `bounds` is interpreted as west/south/east/north in source geographic
405    /// degrees and must satisfy `west > east`. Projected and normal
406    /// non-wrapped bounds should use [`Self::transform_bounds`].
407    ///
408    /// `densify_points` is the number of intermediate samples added per edge
409    /// and must be no larger than [`MAX_BOUNDS_DENSIFY_POINTS`].
410    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    /// Batch transform (sequential).
655    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    /// Batch transform of 3D coordinates (sequential).
660    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    /// Transform 2D coordinates in place without allocating.
665    ///
666    /// Coordinates before a failing coordinate are left converted; the failing
667    /// coordinate and subsequent coordinates are left unchanged.
668    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    /// Transform 3D coordinates in place without allocating.
676    ///
677    /// Coordinates before a failing coordinate are left converted; the failing
678    /// coordinate and subsequent coordinates are left unchanged.
679    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    /// Transform 2D coordinates from `input` into an existing `output` slice.
687    ///
688    /// `output` must have exactly the same length as `input`. This API performs
689    /// no allocation and does not require cloning input coordinates.
690    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    /// Transform 3D coordinates from `input` into an existing `output` slice.
699    ///
700    /// `output` must have exactly the same length as `input`. This API performs
701    /// no allocation and does not require cloning input coordinates.
702    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    /// Batch transform with Rayon parallelism.
711    #[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    /// Batch transform of 3D coordinates with adaptive Rayon parallelism.
729    #[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}