use crate::area_of_use::AreaOfUse;
use crate::transform::BoundingBox;
use core::cmp::Ordering;
#[derive(Debug, Clone)]
pub struct CandidateOperation {
pub name: String,
pub accuracy_m: f64,
pub area_of_use: Option<AreaOfUse>,
pub source_id: Option<String>,
}
#[derive(Debug, Clone)]
pub struct OperationRanking {
pub candidate_idx: usize,
pub effective_accuracy_m: f64,
pub coverage_fraction: f64,
pub score: f64,
}
pub fn select_best_operation(
candidates: &[CandidateOperation],
query_bbox: Option<&BoundingBox>,
) -> Option<usize> {
let ranked = rank_operations(candidates, query_bbox);
ranked.into_iter().next().map(|r| r.candidate_idx)
}
pub fn rank_operations(
candidates: &[CandidateOperation],
query_bbox: Option<&BoundingBox>,
) -> Vec<OperationRanking> {
match query_bbox {
Some(bbox) => rank_with_bbox(candidates, bbox),
None => rank_accuracy_only(candidates),
}
}
pub fn area_coverage_fraction(candidate_aou: &AreaOfUse, query: &BoundingBox) -> f64 {
let west = query.min_x;
let south = query.min_y;
let east = query.max_x;
let north = query.max_y;
let query_area = (east - west) * (north - south);
if query_area <= 0.0 {
let cx = (west + east) / 2.0;
let cy = (south + north) / 2.0;
if point_in_aou(candidate_aou, cx, cy) {
1.0
} else {
0.0
}
} else {
let inter_w = west.max(candidate_aou.west);
let inter_s = south.max(candidate_aou.south);
let inter_e = east.min(candidate_aou.east);
let inter_n = north.min(candidate_aou.north);
let inter_area = (inter_e - inter_w).max(0.0) * (inter_n - inter_s).max(0.0);
(inter_area / query_area).min(1.0)
}
}
pub fn operation_score(accuracy_m: f64, coverage: f64) -> f64 {
if accuracy_m == 0.0 {
return f64::INFINITY;
}
let cov_cubed = coverage * coverage * coverage;
let denom = accuracy_m.max(1e-9_f64);
(cov_cubed / denom).ln_1p()
}
fn rank_with_bbox(candidates: &[CandidateOperation], bbox: &BoundingBox) -> Vec<OperationRanking> {
let mut rankings: Vec<OperationRanking> = candidates
.iter()
.enumerate()
.filter_map(|(idx, candidate)| {
let coverage = match &candidate.area_of_use {
None => 1.0_f64, Some(aou) => area_coverage_fraction(aou, bbox),
};
if coverage < 0.01 {
return None;
}
let score = operation_score(candidate.accuracy_m, coverage);
Some(OperationRanking {
candidate_idx: idx,
effective_accuracy_m: candidate.accuracy_m,
coverage_fraction: coverage,
score,
})
})
.collect();
rankings.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(Ordering::Equal));
rankings
}
fn rank_accuracy_only(candidates: &[CandidateOperation]) -> Vec<OperationRanking> {
let mut rankings: Vec<OperationRanking> = candidates
.iter()
.enumerate()
.map(|(idx, candidate)| {
let score = operation_score(candidate.accuracy_m, 1.0);
OperationRanking {
candidate_idx: idx,
effective_accuracy_m: candidate.accuracy_m,
coverage_fraction: 1.0,
score,
}
})
.collect();
rankings.sort_by(|a, b| {
let score_ord = b.score.partial_cmp(&a.score).unwrap_or(Ordering::Equal);
if score_ord != Ordering::Equal {
return score_ord;
}
a.effective_accuracy_m
.partial_cmp(&b.effective_accuracy_m)
.unwrap_or(Ordering::Equal)
});
rankings
}
fn point_in_aou(aou: &AreaOfUse, lon: f64, lat: f64) -> bool {
aou.contains(lon, lat)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transform::BoundingBox;
fn global_aou() -> AreaOfUse {
AreaOfUse::new(-180.0, -90.0, 180.0, 90.0, "World")
}
fn europe_aou() -> AreaOfUse {
AreaOfUse::new(-10.0, 35.0, 40.0, 72.0, "Europe")
}
fn north_america_aou() -> AreaOfUse {
AreaOfUse::new(-170.0, 15.0, -50.0, 85.0, "North America")
}
fn make_candidate(name: &str, accuracy_m: f64, aou: Option<AreaOfUse>) -> CandidateOperation {
CandidateOperation {
name: name.to_string(),
accuracy_m,
area_of_use: aou,
source_id: None,
}
}
#[test]
fn test_operation_score_higher_for_better_accuracy() {
let score_good = operation_score(0.5, 1.0);
let score_bad = operation_score(5.0, 1.0);
assert!(
score_good > score_bad,
"0.5 m accuracy should score higher than 5.0 m: {score_good} vs {score_bad}"
);
}
#[test]
fn test_operation_score_higher_for_better_coverage() {
let score_high = operation_score(1.0, 0.9);
let score_low = operation_score(1.0, 0.5);
assert!(
score_high > score_low,
"0.9 coverage should score higher than 0.5: {score_high} vs {score_low}"
);
}
#[test]
fn test_operation_score_identity_is_infinite() {
let score = operation_score(0.0, 1.0);
assert_eq!(
score,
f64::INFINITY,
"identity accuracy (0.0) must yield INFINITY"
);
}
#[test]
fn test_select_best_single_candidate() {
let candidates = vec![make_candidate("only", 1.0, Some(global_aou()))];
let result = select_best_operation(&candidates, None);
assert_eq!(result, Some(0));
}
#[test]
fn test_select_best_empty_returns_none() {
let candidates: Vec<CandidateOperation> = vec![];
let result = select_best_operation(&candidates, None);
assert_eq!(result, None);
}
#[test]
fn test_select_best_chooses_higher_accuracy_when_coverage_equal() {
let candidates = vec![
make_candidate("precise", 0.5, Some(global_aou())), make_candidate("coarse", 5.0, Some(global_aou())), ];
let result = select_best_operation(&candidates, None);
assert_eq!(
result,
Some(0),
"0.5 m candidate (idx 0) should win over 5.0 m (idx 1)"
);
}
#[test]
fn test_select_best_chooses_better_coverage_when_accuracy_equal() {
let query = BoundingBox::new(0.0, 45.0, 10.0, 55.0).expect("valid bbox");
let candidates = vec![
make_candidate("eu_full", 1.0, Some(europe_aou())), make_candidate("na", 1.0, Some(north_america_aou())), ];
let result = select_best_operation(&candidates, Some(&query));
assert_eq!(
result,
Some(0),
"European operation should win for European query"
);
}
#[test]
fn test_select_best_filters_low_coverage_candidates() {
let query = BoundingBox::new(5.0, 48.0, 15.0, 55.0).expect("valid bbox");
let asia_aou = AreaOfUse::new(70.0, 10.0, 150.0, 60.0, "East Asia");
let candidates = vec![
make_candidate("asia_op", 0.1, Some(asia_aou)), make_candidate("eu_op", 1.0, Some(europe_aou())), ];
let result = select_best_operation(&candidates, Some(&query));
assert_eq!(
result,
Some(1),
"Asia operation should be filtered; EU operation (idx 1) should win"
);
}
#[test]
fn test_select_best_no_query_bbox_uses_accuracy_only() {
let japan_aou = AreaOfUse::new(120.0, 20.0, 150.0, 50.0, "Japan");
let candidates = vec![
make_candidate("japan_precise", 0.3, Some(japan_aou)), make_candidate("global_rough", 10.0, None), ];
let result = select_best_operation(&candidates, None);
assert_eq!(
result,
Some(0),
"without bbox, accuracy determines winner: japan_precise (0.3 m) should win"
);
}
#[test]
fn test_rank_operations_returns_sorted_descending() {
let candidates = vec![
make_candidate("coarse_global", 50.0, None), make_candidate("fine_global", 0.5, None), make_candidate("medium_global", 10.0, None), ];
let rankings = rank_operations(&candidates, None);
assert_eq!(rankings.len(), 3, "all 3 candidates should be ranked");
for window in rankings.windows(2) {
let a = &window[0];
let b = &window[1];
assert!(
a.score >= b.score,
"rankings must be sorted descending by score: {} vs {}",
a.score,
b.score
);
}
}
#[test]
fn test_area_coverage_fraction_full_overlap_returns_one() {
let aou = AreaOfUse::new(-20.0, 30.0, 50.0, 80.0, "Large region");
let query = BoundingBox::new(0.0, 45.0, 10.0, 55.0).expect("valid bbox");
let frac = area_coverage_fraction(&aou, &query);
assert!(
(frac - 1.0).abs() < 1e-9,
"query fully inside AOU should yield 1.0, got {frac}"
);
}
#[test]
fn test_area_coverage_fraction_no_overlap_returns_zero() {
let aou = europe_aou();
let query = BoundingBox::new(100.0, 20.0, 140.0, 50.0).expect("valid bbox");
let frac = area_coverage_fraction(&aou, &query);
assert!(
frac < 1e-9,
"disjoint rectangles should yield 0.0, got {frac}"
);
}
#[test]
fn test_area_coverage_fraction_partial_overlap() {
let aou = AreaOfUse::new(10.0, 45.0, 40.0, 72.0, "Partial AOU");
let query = BoundingBox::new(0.0, 45.0, 20.0, 55.0).expect("valid bbox");
let frac = area_coverage_fraction(&aou, &query);
assert!(
(frac - 0.5).abs() < 1e-9,
"half-overlapping bboxes should yield ~0.5, got {frac}"
);
}
}