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, execute_pipeline_xyz, validate_output_len,
25 validate_pipeline_coord3d, validate_vertical_ordinate, CompiledOperationFallback,
26 CompiledOperationPipeline, 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
41fn validate_vertical_composition(
48 vertical: &VerticalTransform,
49 pipeline: &CompiledOperationPipeline,
50 fallbacks: &[CompiledOperationFallback],
51) -> Result<()> {
52 if !matches!(vertical, VerticalTransform::GridShiftList { .. }) {
53 return Ok(());
54 }
55 if pipeline.transforms_ellipsoidal_height
56 || fallbacks
57 .iter()
58 .any(|fallback| fallback.pipeline.transforms_ellipsoidal_height)
59 {
60 return Err(Error::OperationSelection(
61 "geoid-grid vertical transforms cannot yet be composed with a horizontal datum shift that changes ellipsoidal height; transform the horizontal datum and the vertical reference in separate steps"
62 .into(),
63 ));
64 }
65 Ok(())
66}
67
68#[derive(Clone)]
70pub struct Transform {
71 source: CrsDef,
72 target: CrsDef,
73 selected_operation_kind: SelectedOperationKind,
74 selected_direction: OperationStepDirection,
75 selected_operation: std::sync::Arc<CoordinateOperationMetadata>,
76 diagnostics: OperationSelectionDiagnostics,
77 vertical_transform: VerticalTransform,
78 selection_options: SelectionOptions,
79 pipeline: CompiledOperationPipeline,
80 fallback_pipelines: Vec<CompiledOperationFallback>,
81}
82
83impl std::fmt::Debug for Transform {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.debug_struct("Transform")
88 .field("source", &self.source)
89 .field("target", &self.target)
90 .field("selected_operation", &self.selected_operation)
91 .finish_non_exhaustive()
92 }
93}
94
95fn assert_transform_auto_traits<T: Send + Sync + Clone + std::fmt::Debug>() {}
99const _: fn() = assert_transform_auto_traits::<Transform>;
100
101#[cfg(feature = "geo-types")]
106pub trait TransformableGeometry: Sized {
107 fn transform_geometry(self, transform: &Transform) -> Result<Self>;
108}
109
110fn validate_wrapped_geographic_transform_bounds(bounds: Bounds) -> Result<()> {
111 if !bounds.min_x.is_finite()
112 || !bounds.min_y.is_finite()
113 || !bounds.max_x.is_finite()
114 || !bounds.max_y.is_finite()
115 || bounds.min_x <= bounds.max_x
116 || bounds.min_y > bounds.max_y
117 {
118 return Err(Error::OutOfRange(
119 "wrapped geographic bounds must be finite and satisfy west > east and south <= north"
120 .into(),
121 ));
122 }
123
124 for point in [
125 Coord::new(bounds.min_x, bounds.min_y),
126 Coord::new(bounds.min_x, bounds.max_y),
127 Coord::new(bounds.max_x, bounds.min_y),
128 Coord::new(bounds.max_x, bounds.max_y),
129 ] {
130 if !(-180.0..=180.0).contains(&point.x) {
131 return Err(Error::OutOfRange(format!(
132 "wrapped geographic bounds longitude {:.8} degrees is outside [-180, 180]",
133 point.x
134 )));
135 }
136 if !(-90.0..=90.0).contains(&point.y) {
137 return Err(Error::OutOfRange(format!(
138 "wrapped geographic bounds latitude {:.8} degrees is outside [-90, 90]",
139 point.y
140 )));
141 }
142 }
143
144 Ok(())
145}
146
147impl Transform {
148 pub fn new(from_crs: &str, to_crs: &str) -> Result<Self> {
150 Self::with_selection_options(from_crs, to_crs, SelectionOptions::default())
151 }
152
153 pub fn new_horizontal(from_crs: &str, to_crs: &str) -> Result<Self> {
159 Self::new_horizontal_with_selection_options(from_crs, to_crs, SelectionOptions::default())
160 }
161
162 pub fn with_selection_options(
164 from_crs: &str,
165 to_crs: &str,
166 options: SelectionOptions,
167 ) -> Result<Self> {
168 let source = registry::lookup_authority_code(from_crs)?;
169 let target = registry::lookup_authority_code(to_crs)?;
170 Self::from_crs_defs_with_selection_options(&source, &target, options)
171 }
172
173 pub fn new_horizontal_with_selection_options(
176 from_crs: &str,
177 to_crs: &str,
178 options: SelectionOptions,
179 ) -> Result<Self> {
180 let source = registry::lookup_authority_code(from_crs)?;
181 let target = registry::lookup_authority_code(to_crs)?;
182 Self::from_horizontal_components_with_selection_options(&source, &target, options)
183 }
184
185 pub fn from_operation(
187 operation_id: CoordinateOperationId,
188 from_crs: &str,
189 to_crs: &str,
190 ) -> Result<Self> {
191 Self::with_selection_options(
192 from_crs,
193 to_crs,
194 SelectionOptions::new().with_operation(operation_id),
195 )
196 }
197
198 pub fn from_epsg(from: u32, to: u32) -> Result<Self> {
200 let source = registry::lookup_epsg(from)
201 .ok_or_else(|| Error::UnknownCrs(format!("unknown EPSG code: {from}")))?;
202 let target = registry::lookup_epsg(to)
203 .ok_or_else(|| Error::UnknownCrs(format!("unknown EPSG code: {to}")))?;
204 Self::from_crs_defs(&source, &target)
205 }
206
207 pub fn from_epsg_horizontal(from: u32, to: u32) -> Result<Self> {
209 let source = registry::lookup_epsg(from)
210 .ok_or_else(|| Error::UnknownCrs(format!("unknown EPSG code: {from}")))?;
211 let target = registry::lookup_epsg(to)
212 .ok_or_else(|| Error::UnknownCrs(format!("unknown EPSG code: {to}")))?;
213 Self::from_horizontal_components(&source, &target)
214 }
215
216 pub fn from_crs_defs(from: &CrsDef, to: &CrsDef) -> Result<Self> {
218 Self::from_crs_defs_with_selection_options(from, to, SelectionOptions::default())
219 }
220
221 pub fn from_horizontal_components(from: &CrsDef, to: &CrsDef) -> Result<Self> {
227 Self::from_horizontal_components_with_selection_options(
228 from,
229 to,
230 SelectionOptions::default(),
231 )
232 }
233
234 pub fn from_horizontal_components_with_selection_options(
237 from: &CrsDef,
238 to: &CrsDef,
239 options: SelectionOptions,
240 ) -> Result<Self> {
241 let source = from.horizontal_crs().ok_or_else(|| {
242 Error::InvalidDefinition("source CRS does not contain a horizontal component".into())
243 })?;
244 let target = to.horizontal_crs().ok_or_else(|| {
245 Error::InvalidDefinition("target CRS does not contain a horizontal component".into())
246 })?;
247 Self::from_crs_defs_with_selection_options(&source, &target, options)
248 }
249
250 pub fn from_crs_defs_with_selection_options(
255 from: &CrsDef,
256 to: &CrsDef,
257 options: SelectionOptions,
258 ) -> Result<Self> {
259 let grid_runtime = GridRuntime::new(options.grid_provider.clone());
260 let vertical_transform = compile_vertical_transform(from, to, &options, &grid_runtime)?;
261 let selected = compile_selected_pipelines(from, to, &options, &grid_runtime)?;
262 validate_vertical_composition(
263 &vertical_transform,
264 &selected.pipeline,
265 &selected.fallback_pipelines,
266 )?;
267 Ok(Self {
268 source: from.clone(),
269 target: to.clone(),
270 selected_operation_kind: selected.operation,
271 selected_direction: selected.direction,
272 selected_operation: std::sync::Arc::new(selected.metadata),
273 diagnostics: selected.diagnostics,
274 vertical_transform,
275 selection_options: options,
276 pipeline: selected.pipeline,
277 fallback_pipelines: selected.fallback_pipelines,
278 })
279 }
280
281 pub fn convert<T: Transformable>(&self, coord: T) -> Result<T> {
283 let c = coord.to_coord();
284 let result = self.convert_coord(c)?;
285 Ok(T::from_coord(result))
286 }
287
288 #[cfg(feature = "geo-types")]
300 pub fn convert_geometry<T: TransformableGeometry>(&self, geometry: T) -> Result<T> {
301 geometry.transform_geometry(self)
302 }
303
304 #[cfg(feature = "geo-types")]
315 pub fn convert_rect(
316 &self,
317 rect: geo_types::Rect<f64>,
318 densify_points: usize,
319 ) -> Result<geo_types::Rect<f64>> {
320 geo_adapters::transform_geo_rect_with_densification(self, rect, densify_points)
321 }
322
323 pub fn convert_3d<T: Transformable3D>(&self, coord: T) -> Result<T> {
325 let c = coord.to_coord3d();
326 let result = self.convert_coord3d(c)?;
327 Ok(T::from_coord3d(result))
328 }
329
330 pub fn convert_with_diagnostics<T: Transformable>(
339 &self,
340 coord: T,
341 ) -> Result<TransformOutcome<T>> {
342 let c = coord.to_coord();
343 let outcome = self.convert_coord_with_diagnostics(c)?;
344 Ok(TransformOutcome {
345 coord: T::from_coord(outcome.coord),
346 operation: outcome.operation,
347 vertical: outcome.vertical,
348 grid_coverage_misses: outcome.grid_coverage_misses,
349 })
350 }
351
352 pub fn convert_3d_with_diagnostics<T: Transformable3D>(
358 &self,
359 coord: T,
360 ) -> Result<TransformOutcome<T>> {
361 let c = coord.to_coord3d();
362 let outcome = self.convert_coord3d_with_diagnostics(c)?;
363 Ok(TransformOutcome {
364 coord: T::from_coord3d(outcome.coord),
365 operation: outcome.operation,
366 vertical: outcome.vertical,
367 grid_coverage_misses: outcome.grid_coverage_misses,
368 })
369 }
370
371 pub fn source_crs(&self) -> &CrsDef {
373 &self.source
374 }
375
376 pub fn target_crs(&self) -> &CrsDef {
378 &self.target
379 }
380
381 pub fn selected_operation(&self) -> &CoordinateOperationMetadata {
383 &self.selected_operation
384 }
385
386 pub fn selection_diagnostics(&self) -> &OperationSelectionDiagnostics {
388 &self.diagnostics
389 }
390
391 pub fn vertical_diagnostics(&self) -> &VerticalTransformDiagnostics {
393 self.vertical_transform.diagnostics()
394 }
395
396 pub fn inverse(&self) -> Result<Self> {
398 let grid_runtime = GridRuntime::new(self.selection_options.grid_provider.clone());
399 let inverse_options = self.selection_options.inverse();
400 let vertical_transform = compile_vertical_transform(
401 &self.target,
402 &self.source,
403 &inverse_options,
404 &grid_runtime,
405 )?;
406 let selected_direction = self.selected_direction.inverse();
407 let selected_operation_kind = self.selected_operation_kind.clone().into_owned();
408 let pipeline = compile_pipeline(
409 &self.target,
410 &self.source,
411 &selected_operation_kind,
412 selected_direction,
413 &grid_runtime,
414 )?;
415 let selected_operation = selected_metadata(
416 &selected_operation_kind,
417 &self.source,
418 &self.target,
419 selected_direction,
420 self.selected_operation.area_of_use.clone(),
421 );
422 let mut fallback_pipelines = Vec::with_capacity(self.fallback_pipelines.len());
423 for fallback in &self.fallback_pipelines {
424 let direction = fallback.direction.inverse();
425 let pipeline = compile_pipeline(
426 &self.target,
427 &self.source,
428 &fallback.operation,
429 direction,
430 &grid_runtime,
431 )?;
432 let metadata = selected_metadata(
433 &fallback.operation,
434 &self.source,
435 &self.target,
436 direction,
437 fallback.metadata.area_of_use.clone(),
438 );
439 fallback_pipelines.push(CompiledOperationFallback {
440 operation: fallback.operation.clone(),
441 direction,
442 metadata: std::sync::Arc::new(metadata),
443 pipeline,
444 });
445 }
446 let diagnostics = OperationSelectionDiagnostics {
447 selected_operation: selected_operation.clone(),
448 selected_match_kind: self.diagnostics.selected_match_kind,
449 selected_reasons: self.diagnostics.selected_reasons.clone(),
450 fallback_operations: fallback_pipelines
451 .iter()
452 .map(|fallback| (*fallback.metadata).clone())
453 .collect(),
454 skipped_operations: self
455 .diagnostics
456 .skipped_operations
457 .iter()
458 .cloned()
459 .map(|mut skipped| {
460 skipped.metadata.direction = skipped.metadata.direction.inverse();
461 std::mem::swap(
462 &mut skipped.metadata.source_crs_epsg,
463 &mut skipped.metadata.target_crs_epsg,
464 );
465 std::mem::swap(
466 &mut skipped.metadata.source_datum_epsg,
467 &mut skipped.metadata.target_datum_epsg,
468 );
469 skipped
470 })
471 .collect(),
472 approximate: self.diagnostics.approximate,
473 missing_required_grid: self.diagnostics.missing_required_grid.clone(),
474 };
475 validate_vertical_composition(&vertical_transform, &pipeline, &fallback_pipelines)?;
476 Ok(Self {
477 source: self.target.clone(),
478 target: self.source.clone(),
479 selected_operation_kind,
480 selected_direction,
481 selected_operation: std::sync::Arc::new(selected_operation),
482 diagnostics,
483 vertical_transform,
484 selection_options: inverse_options,
485 pipeline,
486 fallback_pipelines,
487 })
488 }
489
490 pub fn transform_bounds(&self, bounds: Bounds, densify_points: usize) -> Result<Bounds> {
495 if !bounds.is_valid() {
496 return Err(Error::OutOfRange(
497 "bounds must be finite and satisfy min <= max".into(),
498 ));
499 }
500
501 self.transform_valid_bounds(bounds, densify_points)
502 }
503
504 pub fn transform_geographic_wrapped_bounds(
513 &self,
514 bounds: Bounds,
515 densify_points: usize,
516 ) -> Result<Bounds> {
517 if !self.source.is_geographic() {
518 return Err(Error::InvalidDefinition(
519 "wrapped geographic bounds require a geographic source CRS".into(),
520 ));
521 }
522 validate_wrapped_geographic_transform_bounds(bounds)?;
523
524 let west_segment = Bounds::new(bounds.min_x, bounds.min_y, 180.0, bounds.max_y);
525 let east_segment = Bounds::new(-180.0, bounds.min_y, bounds.max_x, bounds.max_y);
526 let mut transformed = self.transform_valid_bounds(west_segment, densify_points)?;
527 let east_transformed = self.transform_valid_bounds(east_segment, densify_points)?;
528 transformed.expand_to_include(Coord::new(east_transformed.min_x, east_transformed.min_y));
529 transformed.expand_to_include(Coord::new(east_transformed.max_x, east_transformed.max_y));
530 Ok(transformed)
531 }
532
533 fn transform_valid_bounds(&self, bounds: Bounds, densify_points: usize) -> Result<Bounds> {
534 let segments = bounds_densify_segments(densify_points)?;
535
536 let mut transformed: Option<Bounds> = None;
537 for i in 0..=segments {
538 let t = i as f64 / segments as f64;
539 let x = bounds.min_x + bounds.width() * t;
540 let y = bounds.min_y + bounds.height() * t;
541
542 for sample in [
543 Coord::new(x, bounds.min_y),
544 Coord::new(x, bounds.max_y),
545 Coord::new(bounds.min_x, y),
546 Coord::new(bounds.max_x, y),
547 ] {
548 let coord = self.convert_coord(sample)?;
549 if let Some(accum) = &mut transformed {
550 accum.expand_to_include(coord);
551 } else {
552 transformed = Some(Bounds::new(coord.x, coord.y, coord.x, coord.y));
553 }
554 }
555 }
556
557 transformed.ok_or_else(|| Error::OutOfRange("failed to sample bounds".into()))
558 }
559
560 fn convert_coord(&self, c: Coord) -> Result<Coord> {
561 match execute_pipeline_xy(&self.pipeline, Coord3D::new(c.x, c.y, 0.0)) {
562 Ok(coord) => return Ok(coord),
563 Err(error) => {
564 if !is_grid_coverage_miss(&error) {
565 return Err(error);
566 }
567 }
568 }
569
570 for fallback in &self.fallback_pipelines {
571 match execute_pipeline_xy(&fallback.pipeline, Coord3D::new(c.x, c.y, 0.0)) {
572 Ok(coord) => return Ok(coord),
573 Err(error) => {
574 if !is_grid_coverage_miss(&error) {
575 return Err(error);
576 }
577 }
578 }
579 }
580
581 Err(Error::Grid(GridError::OutsideCoverage(
582 "grid coverage miss".into(),
583 )))
584 }
585
586 fn convert_coord3d(&self, c: Coord3D) -> Result<Coord3D> {
587 match self.execute_pipeline_coord3d(&self.pipeline, c) {
588 Ok(coord) => return Ok(coord),
589 Err(error) => {
590 if !is_grid_coverage_miss(&error) {
591 return Err(error);
592 }
593 }
594 }
595
596 for fallback in &self.fallback_pipelines {
597 match self.execute_pipeline_coord3d(&fallback.pipeline, c) {
598 Ok(coord) => return Ok(coord),
599 Err(error) => {
600 if !is_grid_coverage_miss(&error) {
601 return Err(error);
602 }
603 }
604 }
605 }
606
607 Err(Error::Grid(GridError::OutsideCoverage(
608 "grid coverage miss".into(),
609 )))
610 }
611
612 fn convert_coord_with_diagnostics(&self, c: Coord) -> Result<TransformOutcome<Coord>> {
613 let mut grid_coverage_misses = Vec::new();
614 let c = Coord3D::new(c.x, c.y, 0.0);
615
616 match execute_pipeline_xy(&self.pipeline, c) {
617 Ok(coord) => {
618 return Ok(TransformOutcome {
619 coord,
620 operation: self.selected_operation.clone(),
621 vertical: vertical_diagnostics(VerticalTransformAction::None, None, None, None),
622 grid_coverage_misses,
623 });
624 }
625 Err(error) => {
626 if let Some(detail) = grid_coverage_miss_detail(&error) {
627 grid_coverage_misses.push(GridCoverageMiss {
628 operation: self.selected_operation.clone(),
629 detail,
630 });
631 } else {
632 return Err(error);
633 }
634 }
635 }
636
637 for fallback in &self.fallback_pipelines {
638 match execute_pipeline_xy(&fallback.pipeline, c) {
639 Ok(coord) => {
640 return Ok(TransformOutcome {
641 coord,
642 operation: fallback.metadata.clone(),
643 vertical: vertical_diagnostics(
644 VerticalTransformAction::None,
645 None,
646 None,
647 None,
648 ),
649 grid_coverage_misses,
650 });
651 }
652 Err(error) => {
653 if let Some(detail) = grid_coverage_miss_detail(&error) {
654 grid_coverage_misses.push(GridCoverageMiss {
655 operation: fallback.metadata.clone(),
656 detail,
657 });
658 } else {
659 return Err(error);
660 }
661 }
662 }
663 }
664
665 Err(Error::Grid(GridError::OutsideCoverage(
666 grid_coverage_misses
667 .last()
668 .map(|miss| miss.detail.clone())
669 .unwrap_or_else(|| "grid coverage miss".into()),
670 )))
671 }
672
673 fn convert_coord3d_with_diagnostics(&self, c: Coord3D) -> Result<TransformOutcome<Coord3D>> {
674 let mut grid_coverage_misses = Vec::new();
675 match self.execute_pipeline(&self.pipeline, c) {
676 Ok(outcome) => {
677 return Ok(TransformOutcome {
678 coord: outcome.coord,
679 operation: self.selected_operation.clone(),
680 vertical: outcome.vertical,
681 grid_coverage_misses,
682 });
683 }
684 Err(error) => {
685 if let Some(detail) = grid_coverage_miss_detail(&error) {
686 grid_coverage_misses.push(GridCoverageMiss {
687 operation: self.selected_operation.clone(),
688 detail,
689 });
690 } else {
691 return Err(error);
692 }
693 }
694 }
695
696 for fallback in &self.fallback_pipelines {
697 match self.execute_pipeline(&fallback.pipeline, c) {
698 Ok(outcome) => {
699 return Ok(TransformOutcome {
700 coord: outcome.coord,
701 operation: fallback.metadata.clone(),
702 vertical: outcome.vertical,
703 grid_coverage_misses,
704 });
705 }
706 Err(error) => {
707 if let Some(detail) = grid_coverage_miss_detail(&error) {
708 grid_coverage_misses.push(GridCoverageMiss {
709 operation: fallback.metadata.clone(),
710 detail,
711 });
712 } else {
713 return Err(error);
714 }
715 }
716 }
717 }
718
719 Err(Error::Grid(GridError::OutsideCoverage(
720 grid_coverage_misses
721 .last()
722 .map(|miss| miss.detail.clone())
723 .unwrap_or_else(|| "grid coverage miss".into()),
724 )))
725 }
726
727 fn pipeline_owns_height(&self, pipeline: &CompiledOperationPipeline) -> bool {
734 pipeline.transforms_ellipsoidal_height
735 && matches!(self.vertical_transform, VerticalTransform::None { .. })
736 }
737
738 fn execute_pipeline(
739 &self,
740 pipeline: &CompiledOperationPipeline,
741 c: Coord3D,
742 ) -> Result<PipelineExecutionOutcome> {
743 validate_vertical_ordinate(c.z)?;
744 if self.pipeline_owns_height(pipeline) {
745 let coord = execute_pipeline_xyz(pipeline, c)?;
746 return Ok(PipelineExecutionOutcome {
747 coord,
748 vertical: self.vertical_transform.diagnostics().clone(),
749 });
750 }
751 let xy = execute_pipeline_xy(pipeline, c)?;
752 let vertical = self.vertical_transform.apply(c)?;
753 let coord = Coord3D::new(xy.x, xy.y, vertical.z);
754 validate_pipeline_coord3d("pipeline final output", coord)?;
755 Ok(PipelineExecutionOutcome {
756 coord,
757 vertical: vertical.diagnostics,
758 })
759 }
760
761 fn execute_pipeline_coord3d(
762 &self,
763 pipeline: &CompiledOperationPipeline,
764 c: Coord3D,
765 ) -> Result<Coord3D> {
766 validate_vertical_ordinate(c.z)?;
767 if self.pipeline_owns_height(pipeline) {
768 return execute_pipeline_xyz(pipeline, c);
769 }
770 let xy = execute_pipeline_xy(pipeline, c)?;
771 let z = self.vertical_transform.apply_z(c)?;
772 let coord = Coord3D::new(xy.x, xy.y, z);
773 validate_pipeline_coord3d("pipeline final output", coord)?;
774 Ok(coord)
775 }
776
777 pub fn convert_batch<T: Transformable>(&self, coords: &[T]) -> Result<Vec<T>> {
779 coords
780 .iter()
781 .map(|c| self.convert_coord(c.to_coord()).map(T::from_coord))
782 .collect()
783 }
784
785 pub fn convert_batch_3d<T: Transformable3D>(&self, coords: &[T]) -> Result<Vec<T>> {
787 coords
788 .iter()
789 .map(|c| self.convert_coord3d(c.to_coord3d()).map(T::from_coord3d))
790 .collect()
791 }
792
793 pub fn convert_coords_in_place(&self, coords: &mut [Coord]) -> Result<()> {
798 for coord in coords {
799 *coord = self.convert_coord(*coord)?;
800 }
801 Ok(())
802 }
803
804 pub fn convert_coords_3d_in_place(&self, coords: &mut [Coord3D]) -> Result<()> {
809 for coord in coords {
810 *coord = self.convert_coord3d(*coord)?;
811 }
812 Ok(())
813 }
814
815 pub fn convert_coords_into(&self, input: &[Coord], output: &mut [Coord]) -> Result<()> {
821 validate_output_len(input.len(), output.len())?;
822 for (source, target) in input.iter().zip(output.iter_mut()) {
823 *target = self.convert_coord(*source)?;
824 }
825 Ok(())
826 }
827
828 pub fn convert_coords_3d_into(&self, input: &[Coord3D], output: &mut [Coord3D]) -> Result<()> {
834 validate_output_len(input.len(), output.len())?;
835 for (source, target) in input.iter().zip(output.iter_mut()) {
836 *target = self.convert_coord3d(*source)?;
837 }
838 Ok(())
839 }
840
841 #[cfg(feature = "rayon")]
843 pub fn convert_batch_parallel<T: Transformable + Send + Sync>(
844 &self,
845 coords: &[T],
846 ) -> Result<Vec<T>> {
847 if !should_parallelize(coords.len()) {
848 return self.convert_batch(coords);
849 }
850
851 use rayon::prelude::*;
852
853 coords
854 .par_iter()
855 .map(|coord| self.convert_coord(coord.to_coord()).map(T::from_coord))
856 .collect()
857 }
858
859 #[cfg(feature = "rayon")]
861 pub fn convert_batch_parallel_3d<T: Transformable3D + Send + Sync>(
862 &self,
863 coords: &[T],
864 ) -> Result<Vec<T>> {
865 if !should_parallelize(coords.len()) {
866 return self.convert_batch_3d(coords);
867 }
868
869 use rayon::prelude::*;
870
871 coords
872 .par_iter()
873 .map(|coord| {
874 self.convert_coord3d(coord.to_coord3d())
875 .map(T::from_coord3d)
876 })
877 .collect()
878 }
879}
880
881pub(crate) fn bounds_densify_segments(densify_points: usize) -> Result<usize> {
882 if densify_points > MAX_BOUNDS_DENSIFY_POINTS {
883 return Err(Error::OutOfRange(format!(
884 "densify point count {densify_points} exceeds maximum {MAX_BOUNDS_DENSIFY_POINTS}"
885 )));
886 }
887 densify_points
888 .checked_add(1)
889 .ok_or_else(|| Error::OutOfRange("densify point count is too large".into()))
890}