use super::pipeline::{compile_pipeline, CompiledOperationFallback, CompiledOperationPipeline};
use crate::crs::CrsDef;
use crate::error::{Error, Result};
use crate::grid::{GridError, GridRuntime};
use crate::operation::{
CoordinateOperation, CoordinateOperationMetadata, OperationSelectionDiagnostics,
OperationStepDirection, SelectionOptions, SelectionPolicy, SkippedOperation,
SkippedOperationReason,
};
use crate::registry;
use crate::selector;
use smallvec::SmallVec;
pub(super) struct SelectedOperationPipelines {
pub(super) operation: CoordinateOperation,
pub(super) direction: OperationStepDirection,
pub(super) metadata: CoordinateOperationMetadata,
pub(super) diagnostics: OperationSelectionDiagnostics,
pub(super) pipeline: CompiledOperationPipeline,
pub(super) fallback_pipelines: Vec<CompiledOperationFallback>,
}
pub(super) fn compile_selected_pipelines(
from: &CrsDef,
to: &CrsDef,
options: &SelectionOptions,
grid_runtime: &GridRuntime,
) -> Result<SelectedOperationPipelines> {
let candidate_set = selector::rank_operation_candidates(from, to, options)?;
if candidate_set.ranked.is_empty() {
return Err(no_ranked_operation_error(
from,
to,
options,
&candidate_set.skipped,
));
}
let mut skipped_operations = candidate_set.skipped;
let mut missing_required_grid = None;
let mut selected: Option<(
usize,
&selector::RankedOperationCandidate,
CoordinateOperationMetadata,
CompiledOperationPipeline,
)> = None;
let mut fallback_pipelines = Vec::new();
for (index, candidate) in candidate_set.ranked.iter().enumerate() {
if let Some((_, selected_candidate, ..)) = &selected {
if !selected_candidate.operation.uses_grids() {
skipped_operations.push(skipped_for_unselected_candidate(
candidate,
!selected_candidate.operation.deprecated,
));
continue;
}
}
match compile_pipeline(
from,
to,
candidate.operation.as_ref(),
candidate.direction,
grid_runtime,
) {
Ok(pipeline) => {
let metadata = selected_metadata(
candidate.operation.as_ref(),
candidate.direction,
candidate.matched_area_of_use.clone(),
);
if let Some((_, selected_candidate, ..)) = &selected {
skipped_operations.push(skipped_for_unselected_candidate(
candidate,
!selected_candidate.operation.deprecated,
));
fallback_pipelines.push(CompiledOperationFallback {
operation: candidate.operation.clone().into_owned(),
direction: candidate.direction,
metadata,
pipeline,
});
} else {
selected = Some((index, candidate, metadata, pipeline));
}
}
Err(Error::Grid(error)) => {
if selected.is_none() && missing_required_grid.is_none() {
missing_required_grid = Some(error.to_string());
}
skipped_operations.push(SkippedOperation {
metadata: selected_metadata(
candidate.operation.as_ref(),
candidate.direction,
candidate.matched_area_of_use.clone(),
),
reason: match error {
crate::grid::GridError::UnsupportedFormat(_) => {
SkippedOperationReason::UnsupportedGridFormat
}
_ => SkippedOperationReason::MissingGrid,
},
detail: error.to_string(),
});
}
Err(error) => {
skipped_operations.push(SkippedOperation {
metadata: selected_metadata(
candidate.operation.as_ref(),
candidate.direction,
candidate.matched_area_of_use.clone(),
),
reason: SkippedOperationReason::LessPreferred,
detail: error.to_string(),
});
}
}
}
if let Some((index, candidate, metadata, pipeline)) = selected {
let selected_reasons = selected_reasons_for(candidate, &candidate_set.ranked[index + 1..]);
let diagnostics = OperationSelectionDiagnostics {
selected_operation: metadata.clone(),
selected_match_kind: candidate.match_kind,
selected_reasons,
fallback_operations: fallback_pipelines
.iter()
.map(|fallback| fallback.metadata.clone())
.collect(),
skipped_operations,
approximate: candidate.operation.approximate,
missing_required_grid,
};
return Ok(SelectedOperationPipelines {
operation: candidate.operation.clone().into_owned(),
direction: candidate.direction,
metadata,
diagnostics,
pipeline,
fallback_pipelines,
});
}
if let Some(message) = missing_required_grid {
return Err(Error::OperationSelection(format!(
"better operations were skipped because required grids were unavailable: {message}"
)));
}
Err(Error::OperationSelection(format!(
"unable to compile an operation for source EPSG:{} target EPSG:{}",
from.epsg(),
to.epsg()
)))
}
fn no_ranked_operation_error(
from: &CrsDef,
to: &CrsDef,
options: &SelectionOptions,
skipped_operations: &[SkippedOperation],
) -> Error {
let approximate_fallback_disabled = skipped_operations
.iter()
.any(|skipped| skipped.detail == selector::APPROXIMATE_HELMERT_FALLBACK_DISABLED_DETAIL);
match options.policy {
SelectionPolicy::Operation(id) => match registry::lookup_operation(id) {
Some(_) => Error::OperationSelection(format!(
"operation id {} is not compatible with source EPSG:{} target EPSG:{}",
id.0,
from.epsg(),
to.epsg()
)),
None => Error::UnknownOperation(format!("unknown operation id {}", id.0)),
},
_ if approximate_fallback_disabled => Error::OperationSelection(format!(
"no non-approximate compatible operation found for source EPSG:{} target EPSG:{}; {}",
from.epsg(),
to.epsg(),
selector::APPROXIMATE_HELMERT_FALLBACK_DISABLED_DETAIL
)),
_ => Error::OperationSelection(format!(
"no compatible operation found for source EPSG:{} target EPSG:{}",
from.epsg(),
to.epsg()
)),
}
}
pub(super) fn selected_metadata(
operation: &CoordinateOperation,
direction: OperationStepDirection,
matched_area_of_use: Option<crate::operation::AreaOfUse>,
) -> CoordinateOperationMetadata {
let mut metadata = operation.metadata_for_direction(direction);
metadata.area_of_use = matched_area_of_use.or_else(|| operation.areas_of_use.first().cloned());
metadata
}
pub(super) fn is_grid_coverage_miss(error: &Error) -> bool {
matches!(error, Error::Grid(GridError::OutsideCoverage(_)))
}
pub(super) fn grid_coverage_miss_detail(error: &Error) -> Option<String> {
match error {
Error::Grid(GridError::OutsideCoverage(detail)) => Some(detail.clone()),
_ => None,
}
}
fn selected_reasons_for(
selected: &selector::RankedOperationCandidate,
alternatives: &[selector::RankedOperationCandidate],
) -> SmallVec<[crate::operation::SelectionReason; 4]> {
let mut reasons = selected.reasons.clone();
if selected_accuracy_preferred(selected, alternatives)
&& !reasons.contains(&crate::operation::SelectionReason::AccuracyPreferred)
{
reasons.push(crate::operation::SelectionReason::AccuracyPreferred);
}
reasons
}
fn selected_accuracy_preferred(
selected: &selector::RankedOperationCandidate,
alternatives: &[selector::RankedOperationCandidate],
) -> bool {
let Some(selected_accuracy) = selected.operation.accuracy.map(|value| value.meters) else {
return false;
};
alternatives.iter().any(|alternative| {
same_pre_accuracy_priority(selected, alternative)
&& alternative
.operation
.accuracy
.map(|value| selected_accuracy < value.meters)
.unwrap_or(false)
})
}
fn same_pre_accuracy_priority(
left: &selector::RankedOperationCandidate,
right: &selector::RankedOperationCandidate,
) -> bool {
match_kind_priority(left.match_kind) == match_kind_priority(right.match_kind)
&& left.matched_area_of_use.is_some() == right.matched_area_of_use.is_some()
}
fn match_kind_priority(kind: crate::operation::OperationMatchKind) -> u8 {
match kind {
crate::operation::OperationMatchKind::Explicit => 4,
crate::operation::OperationMatchKind::ExactSourceTarget => 3,
crate::operation::OperationMatchKind::DerivedGeographic => 2,
crate::operation::OperationMatchKind::DatumCompatible => 1,
crate::operation::OperationMatchKind::ApproximateFallback => 0,
}
}
fn skipped_for_unselected_candidate(
candidate: &selector::RankedOperationCandidate,
prefer_non_deprecated: bool,
) -> SkippedOperation {
let reason = if prefer_non_deprecated && candidate.operation.deprecated {
SkippedOperationReason::Deprecated
} else {
SkippedOperationReason::LessPreferred
};
let detail = match reason {
SkippedOperationReason::Deprecated => {
"not selected because a non-deprecated higher-ranked operation compiled successfully"
.into()
}
_ => "not selected because a higher-ranked operation compiled successfully".into(),
};
SkippedOperation {
metadata: selected_metadata(
candidate.operation.as_ref(),
candidate.direction,
candidate.matched_area_of_use.clone(),
),
reason,
detail,
}
}