use crate::coord::{Bounds, Coord};
use crate::crs::CrsDef;
use crate::error::{Error, Result};
use crate::operation::{
AreaOfInterest, AreaOfUse, CoordinateOperation, CoordinateOperationMetadata,
OperationMatchKind, OperationMethod, OperationStepDirection, SelectionOptions, SelectionPolicy,
SelectionReason, SkippedOperation, SkippedOperationReason,
};
use crate::projection::{make_projection, validate_lon_lat, validate_projected};
use crate::registry;
use smallvec::SmallVec;
use std::borrow::Cow;
use std::cmp::Ordering;
pub(crate) struct RankedOperationCandidate {
pub(crate) operation: Cow<'static, CoordinateOperation>,
pub(crate) direction: OperationStepDirection,
pub(crate) match_kind: OperationMatchKind,
pub(crate) matched_area_of_use: Option<AreaOfUse>,
pub(crate) reasons: SmallVec<[SelectionReason; 4]>,
}
pub(crate) struct OperationCandidateSet {
pub(crate) ranked: Vec<RankedOperationCandidate>,
pub(crate) skipped: Vec<SkippedOperation>,
}
pub(crate) fn rank_operation_candidates(
source: &CrsDef,
target: &CrsDef,
options: &SelectionOptions,
) -> Result<OperationCandidateSet> {
let resolved_aoi = resolve_area_of_interest(source, target, options)?;
if let SelectionPolicy::Operation(id) = options.policy {
let Some(operation) = registry::lookup_operation(id) else {
return Ok(OperationCandidateSet {
ranked: Vec::new(),
skipped: Vec::new(),
});
};
let Some(direction) = compatible_direction(source, target, &operation) else {
return Ok(OperationCandidateSet {
ranked: Vec::new(),
skipped: Vec::new(),
});
};
let matched_area_of_use = matched_area_of_use(resolved_aoi.as_ref(), &operation);
let reasons = explicit_selection_reasons(matched_area_of_use.is_some(), &operation);
return Ok(OperationCandidateSet {
ranked: vec![RankedOperationCandidate {
operation: Cow::Owned(operation),
direction,
match_kind: OperationMatchKind::Explicit,
matched_area_of_use,
reasons,
}],
skipped: Vec::new(),
});
}
let mut candidates = Vec::new();
let mut skipped = Vec::new();
if requires_no_datum_operation(source, target) {
candidates.push(RankedOperationCandidate {
operation: Cow::Owned(CoordinateOperation {
id: None,
name: "Identity".into(),
source_crs_epsg: source.base_geographic_crs_epsg(),
target_crs_epsg: target.base_geographic_crs_epsg(),
source_datum_epsg: None,
target_datum_epsg: None,
accuracy: Some(crate::operation::OperationAccuracy { meters: 0.0 }),
areas_of_use: SmallVec::new(),
deprecated: false,
preferred: true,
approximate: false,
method: OperationMethod::Identity,
}),
direction: OperationStepDirection::Forward,
match_kind: OperationMatchKind::ExactSourceTarget,
matched_area_of_use: None,
reasons: SmallVec::from_slice(&[
SelectionReason::ExactSourceTarget,
SelectionReason::PreferredOperation,
]),
});
}
let source_geo = source.base_geographic_crs_epsg();
let target_geo = target.base_geographic_crs_epsg();
for operation in registry::related_operations(source, target) {
for direction in [
OperationStepDirection::Forward,
OperationStepDirection::Reverse,
] {
let compatible = match direction {
OperationStepDirection::Forward => is_compatible(source_geo, target_geo, operation),
OperationStepDirection::Reverse => {
is_compatible_reversed(source_geo, target_geo, operation)
}
};
if !compatible {
continue;
}
let matched_area_of_use = matched_area_of_use(resolved_aoi.as_ref(), operation);
if let Some((reason, detail)) = policy_skip_reason(
source,
target,
options,
matched_area_of_use.is_some(),
operation,
) {
skipped.push(SkippedOperation {
metadata: selection_metadata(operation, direction, matched_area_of_use),
reason,
detail,
});
continue;
}
let mut reasons = SmallVec::<[SelectionReason; 4]>::new();
let match_kind = match_kind_for_candidate(source, target, direction, operation);
if matches!(match_kind, OperationMatchKind::ExactSourceTarget) {
reasons.push(SelectionReason::ExactSourceTarget);
}
if matched_area_of_use.is_some() {
reasons.push(SelectionReason::AreaOfUseMatch);
}
if !operation.deprecated {
reasons.push(SelectionReason::NonDeprecated);
}
if operation.preferred {
reasons.push(SelectionReason::PreferredOperation);
}
candidates.push(RankedOperationCandidate {
operation: Cow::Borrowed(operation),
direction,
match_kind,
matched_area_of_use,
reasons,
});
}
}
if let Some(operation) = synthetic_grid_datum_shift(source, target) {
candidates.push(RankedOperationCandidate {
operation: Cow::Owned(operation),
direction: OperationStepDirection::Forward,
match_kind: OperationMatchKind::DatumCompatible,
matched_area_of_use: None,
reasons: SmallVec::from_slice(&[SelectionReason::NonDeprecated]),
});
}
if matches!(
options.policy,
SelectionPolicy::BestAvailable | SelectionPolicy::AllowApproximateHelmertFallback
) {
if let Some(operation) = synthetic_helmert_fallback(source, target) {
candidates.push(RankedOperationCandidate {
operation: Cow::Owned(operation),
direction: OperationStepDirection::Forward,
match_kind: OperationMatchKind::ApproximateFallback,
matched_area_of_use: None,
reasons: SmallVec::from_slice(&[SelectionReason::ApproximateFallback]),
});
}
}
candidates.sort_by(compare_candidates);
Ok(OperationCandidateSet {
ranked: candidates,
skipped,
})
}
fn compatible_direction(
source: &CrsDef,
target: &CrsDef,
operation: &CoordinateOperation,
) -> Option<OperationStepDirection> {
let source_geo = source.base_geographic_crs_epsg();
let target_geo = target.base_geographic_crs_epsg();
if is_compatible(source_geo, target_geo, operation) {
Some(OperationStepDirection::Forward)
} else if is_compatible_reversed(source_geo, target_geo, operation) {
Some(OperationStepDirection::Reverse)
} else {
None
}
}
fn explicit_selection_reasons(
area_matches: bool,
operation: &CoordinateOperation,
) -> SmallVec<[SelectionReason; 4]> {
let mut reasons = SmallVec::from_slice(&[SelectionReason::ExplicitOperation]);
if area_matches {
reasons.push(SelectionReason::AreaOfUseMatch);
}
if !operation.deprecated {
reasons.push(SelectionReason::NonDeprecated);
}
if operation.preferred {
reasons.push(SelectionReason::PreferredOperation);
}
reasons
}
fn match_kind_for_candidate(
source: &CrsDef,
target: &CrsDef,
direction: OperationStepDirection,
operation: &CoordinateOperation,
) -> OperationMatchKind {
let (candidate_source, candidate_target) = match direction {
OperationStepDirection::Forward => (operation.source_crs_epsg, operation.target_crs_epsg),
OperationStepDirection::Reverse => (operation.target_crs_epsg, operation.source_crs_epsg),
};
if candidate_source == Some(source.epsg()) && candidate_target == Some(target.epsg()) {
OperationMatchKind::ExactSourceTarget
} else if candidate_source == source.base_geographic_crs_epsg()
&& candidate_target == target.base_geographic_crs_epsg()
{
OperationMatchKind::DerivedGeographic
} else {
OperationMatchKind::DatumCompatible
}
}
fn is_compatible(
source_geo: Option<u32>,
target_geo: Option<u32>,
operation: &CoordinateOperation,
) -> bool {
let source_datum = source_geo.and_then(crate::epsg_db::lookup_datum_code_for_crs);
let target_datum = target_geo.and_then(crate::epsg_db::lookup_datum_code_for_crs);
match (source_geo, target_geo) {
(Some(source_code), Some(target_code)) => {
(operation.source_crs_epsg == Some(source_code)
&& operation.target_crs_epsg == Some(target_code))
|| (source_datum.is_some()
&& target_datum.is_some()
&& operation.source_datum_epsg == source_datum
&& operation.target_datum_epsg == target_datum)
}
_ => false,
}
}
fn is_compatible_reversed(
source_geo: Option<u32>,
target_geo: Option<u32>,
operation: &CoordinateOperation,
) -> bool {
let source_datum = source_geo.and_then(crate::epsg_db::lookup_datum_code_for_crs);
let target_datum = target_geo.and_then(crate::epsg_db::lookup_datum_code_for_crs);
match (source_geo, target_geo) {
(Some(source_code), Some(target_code)) => {
(operation.source_crs_epsg == Some(target_code)
&& operation.target_crs_epsg == Some(source_code))
|| (source_datum.is_some()
&& target_datum.is_some()
&& operation.source_datum_epsg == target_datum
&& operation.target_datum_epsg == source_datum)
}
_ => false,
}
}
fn requires_no_datum_operation(source: &CrsDef, target: &CrsDef) -> bool {
(source.epsg() != 0 && source.epsg() == target.epsg())
|| (source.base_geographic_crs_epsg().is_some()
&& source.base_geographic_crs_epsg() == target.base_geographic_crs_epsg())
|| source.datum().same_datum(target.datum())
|| (source.datum().is_wgs84_compatible() && target.datum().is_wgs84_compatible())
}
fn policy_skip_reason(
source: &CrsDef,
target: &CrsDef,
options: &SelectionOptions,
matched_area: bool,
operation: &CoordinateOperation,
) -> Option<(SkippedOperationReason, String)> {
let datum_shift_required = !requires_no_datum_operation(source, target);
match options.policy {
SelectionPolicy::BestAvailable => None,
SelectionPolicy::RequireGrids => {
if datum_shift_required && !operation.uses_grids() {
Some((
SkippedOperationReason::PolicyFiltered,
"selection policy requires a grid-backed datum operation".into(),
))
} else {
None
}
}
SelectionPolicy::RequireExactAreaMatch => {
if options.area_of_interest.is_some() && !matched_area {
Some((
SkippedOperationReason::AreaOfUseMismatch,
"selection policy requires an exact area-of-use match".into(),
))
} else {
None
}
}
SelectionPolicy::AllowApproximateHelmertFallback => None,
SelectionPolicy::Operation(_) => None,
}
}
fn selection_metadata(
operation: &CoordinateOperation,
direction: OperationStepDirection,
matched_area_of_use: Option<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
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ResolvedAreaOfInterest {
pub(crate) point: Option<Coord>,
pub(crate) bounds: Option<Bounds>,
}
pub(crate) fn resolve_area_of_interest(
source: &CrsDef,
target: &CrsDef,
options: &SelectionOptions,
) -> Result<Option<ResolvedAreaOfInterest>> {
let Some(area) = options.area_of_interest else {
return Ok(None);
};
let point = match area.point {
Some(point) => Some(resolve_area_point(area, point, source, target)?),
None => None,
};
let bounds = match area.bounds {
Some(bounds) => Some(resolve_area_bounds(area, bounds, source, target)?),
None => None,
};
Ok(Some(ResolvedAreaOfInterest { point, bounds }))
}
fn matched_area_of_use(
area: Option<&ResolvedAreaOfInterest>,
operation: &CoordinateOperation,
) -> Option<AreaOfUse> {
let area = area?;
if area.point.is_none() && area.bounds.is_none() {
return None;
}
operation
.areas_of_use
.iter()
.find(|candidate| {
area.point
.map(|value| candidate.contains_point(value))
.unwrap_or(false)
|| area
.bounds
.map(|value| candidate.contains_bounds(value))
.unwrap_or(false)
})
.cloned()
}
fn compare_candidates(
left: &RankedOperationCandidate,
right: &RankedOperationCandidate,
) -> Ordering {
match_kind_rank(right.match_kind)
.cmp(&match_kind_rank(left.match_kind))
.then_with(|| {
right
.matched_area_of_use
.is_some()
.cmp(&left.matched_area_of_use.is_some())
})
.then_with(|| {
let left_accuracy = left
.operation
.accuracy
.map(|accuracy| accuracy.meters)
.unwrap_or(f64::MAX);
let right_accuracy = right
.operation
.accuracy
.map(|accuracy| accuracy.meters)
.unwrap_or(f64::MAX);
left_accuracy
.partial_cmp(&right_accuracy)
.unwrap_or(Ordering::Equal)
})
.then_with(|| left.operation.deprecated.cmp(&right.operation.deprecated))
.then_with(|| right.operation.preferred.cmp(&left.operation.preferred))
}
fn match_kind_rank(kind: OperationMatchKind) -> u8 {
match kind {
OperationMatchKind::Explicit => 4,
OperationMatchKind::ExactSourceTarget => 3,
OperationMatchKind::DerivedGeographic => 2,
OperationMatchKind::DatumCompatible => 1,
OperationMatchKind::ApproximateFallback => 0,
}
}
fn resolve_area_point(
area: AreaOfInterest,
point: Coord,
source: &CrsDef,
target: &CrsDef,
) -> Result<Coord> {
match area.crs {
crate::operation::AreaOfInterestCrs::GeographicDegrees => {
validate_geographic_area_point(point)?;
Ok(point)
}
crate::operation::AreaOfInterestCrs::SourceCrs => geographic_from_crs_point(source, point),
crate::operation::AreaOfInterestCrs::TargetCrs => geographic_from_crs_point(target, point),
}
}
fn resolve_area_bounds(
area: AreaOfInterest,
bounds: Bounds,
source: &CrsDef,
target: &CrsDef,
) -> Result<Bounds> {
validate_area_bounds_shape(bounds)?;
let crs = match area.crs {
crate::operation::AreaOfInterestCrs::GeographicDegrees => {
validate_geographic_area_bounds(bounds)?;
return Ok(bounds);
}
crate::operation::AreaOfInterestCrs::SourceCrs => source,
crate::operation::AreaOfInterestCrs::TargetCrs => target,
};
if matches!(crs, CrsDef::Geographic(_)) {
validate_geographic_area_bounds(bounds)?;
return Ok(bounds);
}
let segments = 8usize;
let mut transformed: Option<Bounds> = None;
for i in 0..=segments {
let t = i as f64 / segments as f64;
let x = bounds.min_x + bounds.width() * t;
let y = bounds.min_y + bounds.height() * t;
for sample in [
Coord::new(x, bounds.min_y),
Coord::new(x, bounds.max_y),
Coord::new(bounds.min_x, y),
Coord::new(bounds.max_x, y),
] {
let point = geographic_from_crs_point(crs, sample)?;
if let Some(accum) = &mut transformed {
accum.expand_to_include(point);
} else {
transformed = Some(Bounds::new(point.x, point.y, point.x, point.y));
}
}
}
Ok(transformed.unwrap_or(bounds))
}
fn geographic_from_crs_point(crs: &CrsDef, point: Coord) -> Result<Coord> {
match crs {
CrsDef::Geographic(_) => {
validate_geographic_area_point(point)?;
Ok(point)
}
CrsDef::Projected(projected) => {
validate_projected(point.x, point.y)?;
let projection = make_projection(&projected.method(), projected.datum())?;
let (lon, lat) = projection.inverse(
projected.linear_unit().to_meters(point.x),
projected.linear_unit().to_meters(point.y),
)?;
let geographic = Coord::new(lon.to_degrees(), lat.to_degrees());
validate_geographic_area_point(geographic)?;
Ok(geographic)
}
}
}
fn validate_area_bounds_shape(bounds: Bounds) -> Result<()> {
if !bounds.is_valid() {
return Err(Error::OutOfRange(
"area-of-interest bounds must be finite and satisfy min <= max".into(),
));
}
Ok(())
}
fn validate_geographic_area_bounds(bounds: Bounds) -> Result<()> {
validate_area_bounds_shape(bounds)?;
for point in [
Coord::new(bounds.min_x, bounds.min_y),
Coord::new(bounds.min_x, bounds.max_y),
Coord::new(bounds.max_x, bounds.min_y),
Coord::new(bounds.max_x, bounds.max_y),
] {
validate_geographic_area_point(point)?;
}
Ok(())
}
fn validate_geographic_area_point(point: Coord) -> Result<()> {
if !point.x.is_finite() || !point.y.is_finite() {
return Err(Error::OutOfRange(
"geographic area-of-interest coordinate must be finite".into(),
));
}
if !(-180.0..=180.0).contains(&point.x) {
return Err(Error::OutOfRange(format!(
"geographic area-of-interest longitude {:.8}° is outside [-180°, 180°]",
point.x
)));
}
if !(-90.0..=90.0).contains(&point.y) {
return Err(Error::OutOfRange(format!(
"geographic area-of-interest latitude {:.8}° is outside [-90°, 90°]",
point.y
)));
}
validate_lon_lat(point.x.to_radians(), point.y.to_radians())
}
fn synthetic_helmert_fallback(source: &CrsDef, target: &CrsDef) -> Option<CoordinateOperation> {
if requires_no_datum_operation(source, target) {
return None;
}
let params = source.datum().approximate_helmert_to(target.datum())?;
Some(CoordinateOperation {
id: None,
name: format!("Approximate {} to {}", source.epsg(), target.epsg()),
source_crs_epsg: source.base_geographic_crs_epsg(),
target_crs_epsg: target.base_geographic_crs_epsg(),
source_datum_epsg: None,
target_datum_epsg: None,
accuracy: None,
areas_of_use: SmallVec::new(),
deprecated: false,
preferred: false,
approximate: true,
method: OperationMethod::Helmert { params },
})
}
fn synthetic_grid_datum_shift(source: &CrsDef, target: &CrsDef) -> Option<CoordinateOperation> {
if requires_no_datum_operation(source, target) {
return None;
}
if !source.datum().uses_grid_shift() && !target.datum().uses_grid_shift() {
return None;
}
if !source.datum().has_known_wgs84_transform() || !target.datum().has_known_wgs84_transform() {
return None;
}
Some(CoordinateOperation {
id: None,
name: format!(
"Grid-backed datum shift {} to {}",
source.epsg(),
target.epsg()
),
source_crs_epsg: source.base_geographic_crs_epsg(),
target_crs_epsg: target.base_geographic_crs_epsg(),
source_datum_epsg: None,
target_datum_epsg: None,
accuracy: None,
areas_of_use: SmallVec::new(),
deprecated: false,
preferred: true,
approximate: false,
method: OperationMethod::DatumShift {
source_to_wgs84: source.datum().to_wgs84.clone(),
target_to_wgs84: target.datum().to_wgs84.clone(),
},
})
}