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    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
41/// A reusable coordinate transformation between two CRS.
42pub 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/// Trait for `geo-types` geometries that can be transformed as whole values.
56///
57/// Implementations transform coordinates in storage order and return the first
58/// coordinate error without producing a partially transformed geometry.
59#[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    /// Create a transform from authority code strings (e.g., `"EPSG:4326"`).
103    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    /// Create a transform with explicit selection options.
108    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    /// Create a transform from an explicit registry operation id.
119    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    /// Create a transform from EPSG codes directly.
132    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    /// Create a transform from explicit CRS definitions.
141    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    /// Create a horizontal-only transform from explicit CRS definitions.
146    ///
147    /// Compound CRS inputs are reduced to their horizontal component before
148    /// operation selection. This is intended for XY-only workflows where
149    /// vertical transformation is deliberately out of scope.
150    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    /// Create a horizontal-only transform from explicit CRS definitions with
159    /// operation-selection options.
160    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    /// Create a transform from explicit CRS definitions with operation-selection options.
175    ///
176    /// Use this when a custom CRS references grid resources and the transform
177    /// needs an application-supplied [`crate::grid::GridProvider`].
178    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    /// 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    ///
213    /// `geo_types::Rect` is treated as a source bounds envelope and converted
214    /// to sampled axis-aligned target bounds with 21 intermediate points per
215    /// edge. This is an approximation for nonlinear projections: extrema can
216    /// occur between samples. Use [`Self::convert_rect`] when the rect sampling
217    /// density should be chosen by the caller.
218    #[cfg(feature = "geo-types")]
219    pub fn convert_geometry<T: TransformableGeometry>(&self, geometry: T) -> Result<T> {
220        geometry.transform_geometry(self)
221    }
222
223    /// Transform a `geo_types::Rect` to sampled axis-aligned target bounds.
224    ///
225    /// This method is available only with the `geo-types` feature. A rect
226    /// represents an envelope, not a true geometry, so nonlinear projections can
227    /// have edge extrema between samples. Increase `densify_points` to sample
228    /// edges more finely for higher-fidelity bounds, or use
229    /// [`Self::transform_bounds`] directly when working with [`Bounds`].
230    ///
231    /// `densify_points` is the number of intermediate samples added per edge
232    /// and must be no larger than [`MAX_BOUNDS_DENSIFY_POINTS`].
233    #[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    /// Transform a single 3D coordinate.
243    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    /// Transform a single coordinate and report the operation actually used.
250    ///
251    /// This 2D API is XY-only: it does not apply or sample configured vertical
252    /// transforms.
253    ///
254    /// When the selected grid-backed operation misses grid coverage, this
255    /// reports the coverage misses and the lower-ranked fallback operation that
256    /// produced the result.
257    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    /// Transform a single 3D coordinate and report the operation actually used.
272    ///
273    /// When the selected grid-backed operation misses grid coverage, this
274    /// reports the coverage misses and the lower-ranked fallback operation that
275    /// produced the result.
276    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    /// Return the source CRS definition for this transform.
291    pub fn source_crs(&self) -> &CrsDef {
292        &self.source
293    }
294
295    /// Return the target CRS definition for this transform.
296    pub fn target_crs(&self) -> &CrsDef {
297        &self.target
298    }
299
300    /// Return metadata for the selected coordinate operation.
301    pub fn selected_operation(&self) -> &CoordinateOperationMetadata {
302        &self.selected_operation
303    }
304
305    /// Return selection diagnostics for this transform.
306    pub fn selection_diagnostics(&self) -> &OperationSelectionDiagnostics {
307        &self.diagnostics
308    }
309
310    /// Return diagnostics for the vertical component of this transform.
311    pub fn vertical_diagnostics(&self) -> &VerticalTransformDiagnostics {
312        self.vertical_transform.diagnostics()
313    }
314
315    /// Build the inverse transform by swapping the source and target CRS.
316    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    /// Reproject a 2D bounding box by sampling its perimeter.
392    ///
393    /// `densify_points` is the number of intermediate samples added per edge
394    /// and must be no larger than [`MAX_BOUNDS_DENSIFY_POINTS`].
395    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    /// Reproject a geographic bounding box that crosses the antimeridian.
406    ///
407    /// `bounds` is interpreted as west/south/east/north in source geographic
408    /// degrees and must satisfy `west > east`. Projected and normal
409    /// non-wrapped bounds should use [`Self::transform_bounds`].
410    ///
411    /// `densify_points` is the number of intermediate samples added per edge
412    /// and must be no larger than [`MAX_BOUNDS_DENSIFY_POINTS`].
413    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    /// Batch transform (sequential).
658    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    /// Batch transform of 3D coordinates (sequential).
663    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    /// Transform 2D coordinates in place without allocating.
668    ///
669    /// Coordinates before a failing coordinate are left converted; the failing
670    /// coordinate and subsequent coordinates are left unchanged.
671    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    /// Transform 3D coordinates in place without allocating.
679    ///
680    /// Coordinates before a failing coordinate are left converted; the failing
681    /// coordinate and subsequent coordinates are left unchanged.
682    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    /// Transform 2D coordinates from `input` into an existing `output` slice.
690    ///
691    /// `output` must have exactly the same length as `input`. This API performs
692    /// no allocation and does not require cloning input coordinates.
693    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    /// Transform 3D coordinates from `input` into an existing `output` slice.
702    ///
703    /// `output` must have exactly the same length as `input`. This API performs
704    /// no allocation and does not require cloning input coordinates.
705    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    /// Batch transform with Rayon parallelism.
714    #[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    /// Batch transform of 3D coordinates with adaptive Rayon parallelism.
732    #[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}