use super::datum::{self, Op, Point, Step};
use super::def::Def;
use super::epsg;
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, OnceLock};
#[derive(Debug, Clone)]
pub(crate) struct Leg {
pub op: Arc<Op>,
pub reverse: bool,
}
#[derive(Debug)]
struct Chain {
legs: [Leg; 2],
rank: Op,
}
#[derive(Debug)]
enum Kind {
Same,
List(Vec<Leg>),
Chains { chains: Vec<Chain>, ballpark: Option<Op> },
Hub,
}
type Boxes = Vec<Vec<[f64; 4]>>;
#[derive(Debug)]
pub(crate) struct Route {
kind: Kind,
boxes: OnceLock<Boxes>,
hub_boxes: OnceLock<(Boxes, Boxes)>,
dst_ops: Vec<Arc<Op>>,
}
fn legs(ops: &[Arc<Op>], reverse: bool) -> Vec<Leg> {
ops.iter().map(|op| Leg { op: op.clone(), reverse }).collect()
}
fn intersects(a: &[f64; 4], b: &[f64; 4]) -> bool {
let [w, s, e, n] = *a;
let [ow, os, oe, on] = *b;
if n < os || s > on {
return false;
}
if w == -180.0 && e == 180.0 && ow > oe {
return true;
}
if ow == -180.0 && oe == 180.0 && w > e {
return true;
}
if w <= e {
if ow <= oe {
return w.max(ow) < e.min(oe);
}
if ow > 180.0 || oe < -180.0 {
return false;
}
return intersects(a, &[ow, os, 180.0, on]) || intersects(a, &[-180.0, os, oe, on]);
}
if ow <= oe {
return intersects(b, a);
}
true
}
fn contains(a: &[f64; 4], b: &[f64; 4]) -> bool {
let [w, s, e, n] = *a;
let [ow, os, oe, on] = *b;
if !(s <= os && n >= on) {
return false;
}
if w == -180.0 && e == 180.0 {
return ow != oe;
}
if ow == -180.0 && oe == 180.0 {
return false;
}
if w < e {
return ow < oe && w <= ow && e >= oe;
}
if ow < oe { ow >= w || oe <= e } else { w <= ow && e >= oe }
}
fn intersection(a: &[f64; 4], b: &[f64; 4]) -> Option<[f64; 4]> {
let [w, s, e, n] = *a;
let [ow, os, oe, on] = *b;
if n < os || s > on {
return None;
}
let (s2, n2) = (s.max(os), n.min(on));
if w == -180.0 && e == 180.0 && ow > oe {
return Some([ow, s2, oe, n2]);
}
if ow == -180.0 && oe == 180.0 && w > e {
return Some([w, s2, e, n2]);
}
if w <= e {
if ow <= oe {
let (rw, re) = (w.max(ow), e.min(oe));
return (rw < re).then_some([rw, s2, re, n2]);
}
if ow > 180.0 || oe < -180.0 {
return None;
}
return match (intersection(a, &[ow, os, 180.0, on]), intersection(a, &[-180.0, os, oe, on])) {
(Some(x), Some(y)) => Some(if x[2] - x[0] > y[2] - y[0] { x } else { y }),
(x, y) => x.or(y),
};
}
if ow <= oe {
return intersection(b, a);
}
Some([w.max(ow), s2, e.min(oe), n2])
}
fn pseudo_area(a: &[f64; 4]) -> f64 {
let [w, s, mut e, n] = *a;
if w > e {
e += 360.0;
}
(e - w) * (n.to_radians().sin() - s.to_radians().sin())
}
fn area_of_interest(src: &Def, dst: &Def) -> Option<[f64; 4]> {
smaller(src.area(), dst.area())
}
fn smaller(a: Option<[f64; 4]>, b: Option<[f64; 4]>) -> Option<[f64; 4]> {
match (a, b) {
(Some(a), Some(b)) => Some(if pseudo_area(&a) < pseudo_area(&b) { a } else { b }),
(a, b) => a.or(b),
}
}
fn relevant(op: &Op, src: &Def, dst: &Def, aoi: Option<&[f64; 4]>) -> bool {
op.ballpark || [src.area().as_ref(), dst.area().as_ref(), aoi].into_iter().flatten().all(|a| intersects(&op.bounds, a))
}
impl Route {
pub fn new(src: &Def, dst: &Def) -> Self {
let aoi = area_of_interest(src, dst);
let keep = |op: &Op| relevant(op, src, dst, aoi.as_ref());
let concatenated = !src.is_geographic() || !dst.is_geographic();
let list = |ops: &[Arc<Op>], reverse: bool| -> Kind {
Kind::List(
legs(ops, reverse)
.into_iter()
.filter(|l| keep(&l.op))
.map(|l| {
let flat = concatenated && (l.op.chain_accuracy != l.op.accuracy || l.op.chain_bounds != l.op.bounds);
if flat { Leg { op: Arc::new(l.op.as_concatenated()), reverse: l.reverse } } else { l }
})
.collect(),
)
};
let kind = if src.same_datum(dst) {
Kind::Same
} else if dst.is_wgs84() {
list(src.ops(), false)
} else if src.is_wgs84() {
list(dst.ops(), true)
} else {
match (src.geog(), dst.geog()) {
(Some(a), Some(b)) => match epsg::pair(a, b) {
Some((ops, reverse)) if ops.iter().any(|o| !o.ballpark && keep(o)) => {
let mut kind = list(&ops, reverse);
let area = |code: u32| epsg::get(code).and_then(|e| e.area);
if let Kind::List(l) = &mut kind
&& aoi != smaller(area(a), area(b))
{
let key = |leg: &Leg| {
let name = step_names(&leg.op, leg.reverse).join(" + ");
SortKey::new(&leg.op, name, pipeline_steps(&[(&leg.op, leg.reverse)]), aoi.as_ref())
};
let mut keyed: Vec<(SortKey, Leg)> = l.drain(..).map(|leg| (key(&leg), leg)).collect();
keyed.sort_by(|x, y| x.0.cmp(&y.0));
l.extend(keyed.into_iter().map(|(_, leg)| leg));
}
kind
}
_ => chains(a, b, src, dst, aoi.as_ref()),
},
_ => Kind::Hub,
}
};
Self { kind, boxes: OnceLock::new(), hub_boxes: OnceLock::new(), dst_ops: dst.ops().to_vec() }
}
pub fn accuracy_at(&self, src: &Def, x: f64, y: f64, p: Point) -> f64 {
let (dlon, dlat) = src.source_degrees(x, y).unwrap_or((p.lon.to_degrees(), p.lat.to_degrees()));
let wrapped = if (-180.0..=180.0).contains(&dlon) { dlon } else { (dlon + 180.0).rem_euclid(360.0) - 180.0 };
let inside = |op: &Op, boxes: Option<&Vec<[f64; 4]>>| match boxes {
Some(b) if !b.is_empty() => b.iter().any(|b| x >= b[0] && x <= b[2] && y >= b[1] && y <= b[3]),
_ => op.contains(dlon, dlat) || op.contains(wrapped, dlat),
};
let first_without_grids = |ops: &[&Op]| ops.iter().find(|o| !o.needs_grids()).map_or(-1.0, |o| o.accuracy);
match &self.kind {
Kind::Same => 0.0,
Kind::List(list) if list.is_empty() => -1.0,
Kind::List(list) => {
let boxes = self.boxes.get_or_init(|| list.iter().map(|l| src.areas(&l.op)).collect());
let ops: Vec<&Op> = list.iter().map(|l| l.op.as_ref()).collect();
datum::pick_index(&ops, &[], |op, i| inside(op, boxes.get(i)))
.map_or_else(|| first_without_grids(&ops), |i| ops[i].accuracy)
}
Kind::Chains { chains, .. } if chains.is_empty() => -1.0,
Kind::Chains { chains, ballpark } => {
if chains.len() == 1 && ballpark.is_none() {
return chains[0].rank.accuracy;
}
let boxes = self.boxes.get_or_init(|| chains.iter().map(|c| src.areas(&c.rank)).collect());
let ranks: Vec<&Op> = chains.iter().map(|c| &c.rank).chain(ballpark.as_ref()).collect();
match datum::pick_index(&ranks, &[], |op, i| op.ballpark || inside(op, boxes.get(i))) {
Some(i) => ranks[i].accuracy,
None => first_without_grids(&ranks),
}
}
Kind::Hub => -1.0,
}
}
pub fn apply(&self, src: &Def, x: f64, y: f64, p: Point, three_d: bool) -> Option<Point> {
let (dlon, dlat) = src.source_degrees(x, y).unwrap_or((p.lon.to_degrees(), p.lat.to_degrees()));
let wrapped = if (-180.0..=180.0).contains(&dlon) { dlon } else { (dlon + 180.0).rem_euclid(360.0) - 180.0 };
let inside = |op: &Op, boxes: Option<&Vec<[f64; 4]>>| match boxes {
Some(b) if !b.is_empty() => b.iter().any(|b| x >= b[0] && x <= b[2] && y >= b[1] && y <= b[3]),
_ => op.contains(dlon, dlat) || op.contains(wrapped, dlat),
};
match &self.kind {
Kind::Same => Some(p),
Kind::List(list) if list.is_empty() => Some(p),
Kind::List(list) => {
let boxes = self.boxes.get_or_init(|| list.iter().map(|l| src.areas(&l.op)).collect());
let ops: Vec<&Op> = list.iter().map(|l| l.op.as_ref()).collect();
datum::transform_with(&ops, |op, i| inside(op, boxes.get(i)), |i| datum::run_op(&list[i].op, p, list[i].reverse, three_d))
}
Kind::Chains { chains, .. } if chains.is_empty() => Some(p),
Kind::Chains { chains, ballpark } => {
let run = |c: &Chain| {
let [l1, l2] = &c.legs;
let r = datum::run(&l1.op.steps, p, l1.reverse, true).and_then(|q| datum::run(&l2.op.steps, q, l2.reverse, true))?;
Some(if three_d || !c.rank.keeps_height { r } else { Point { h: p.h, ..r } })
};
if chains.len() == 1 && ballpark.is_none() {
return run(&chains[0]);
}
let boxes = self.boxes.get_or_init(|| chains.iter().map(|c| src.areas(&c.rank)).collect());
let ranks: Vec<&Op> = chains.iter().map(|c| &c.rank).chain(ballpark.as_ref()).collect();
let mut excluded = Vec::new();
for _ in 0..3 {
let Some(i) = datum::pick_index(&ranks, &excluded, |op, i| op.ballpark || inside(op, boxes.get(i))) else { break };
let Some(chain) = chains.get(i) else { return Some(p) };
if let Some(r) = run(chain) {
return Some(r);
}
excluded.push(i);
}
if ballpark.is_some() {
return Some(p);
}
let first = chains.iter().find(|c| !c.rank.needs_grids())?;
run(first)
}
Kind::Hub => {
let (sb, db) = self.hub_boxes.get_or_init(|| {
let areas = |ops: &[Arc<Op>]| ops.iter().map(|o| src.areas(o)).collect();
(areas(src_ops(src)), areas(&self.dst_ops))
});
let single = |op: &Op| !op.ballpark && !op.concatenated;
let s = datum::pick_in(src_ops(src), |op, i| single(op) && inside(op, sb.get(i)));
let d = datum::pick_in(&self.dst_ops, |op, i| single(op) && inside(op, db.get(i)));
match (s, d) {
(Some(s), Some(d)) => {
let r = datum::run(&s.steps, p, false, true).and_then(|q| datum::run(&d.steps, q, true, true))?;
Some(if three_d || !(s.keeps_height && d.keeps_height) { r } else { Point { h: p.h, ..r } })
}
_ => Some(p),
}
}
}
}
}
fn src_ops(src: &Def) -> &[Arc<Op>] {
src.ops()
}
struct Found {
legs: [(usize, bool); 2],
conversions: [Option<String>; 3],
}
impl Found {
fn reversed(self) -> Self {
let [(i1, r1), (i2, r2)] = self.legs;
let back = |c: Option<String>| c.map(|n| if n.starts_with("axis order change") { n } else { inverse_name(&n) });
let [c0, c1, c2] = self.conversions;
Found { legs: [(i2, !r2), (i1, !r1)], conversions: [back(c2), back(c1), back(c0)] }
}
}
fn conversion_name(from: &epsg::Geodetic, to: &epsg::Geodetic) -> String {
use epsg::GeodeticKind::*;
let qualifier = |g: &epsg::Geodetic| match g.kind {
Geographic2D => " (geog2D)",
Geographic3D => " (geog3D)",
Geocentric => " (geocentric)",
};
match (from.kind, to.kind) {
(Geographic2D, Geographic2D) => return "axis order change (2D)".to_string(),
(Geographic3D, Geographic3D) => return "axis order change (geographic3D horizontal)".to_string(),
_ => {}
}
let (q1, q2) = if from.name == to.name { (qualifier(from), qualifier(to)) } else { ("", "") };
let kind = if from.kind == Geocentric || to.kind == Geocentric { "Conversion" } else { NULL_OFFSET };
format!("{kind} from {}{q1} to {}{q2}", from.name, to.name)
}
const NULL_OFFSET: &str = "Null geographic offset";
fn chains(a: u32, b: u32, src: &Def, dst: &Def, aoi: Option<&[f64; 4]>) -> Kind {
let reg = epsg::registry();
let (Some(ga), Some(gb)) = (reg.geodetic.get(&a), reg.geodetic.get(&b)) else {
return Kind::Chains { chains: Vec::new(), ballpark: Some(ballpark_op(src, dst)) };
};
let extents: Vec<[f64; 4]> = [src.area(), dst.area()].into_iter().flatten().filter(|e| *e != [-180.0, -90.0, 180.0, 90.0]).collect();
let reverse = !dst.is_geographic();
let (ga, gb) = if reverse { (gb, ga) } else { (ga, gb) };
let mut found = crs_code_chains(reg, ga, gb, &extents);
let ensemble_only = !found.is_empty() && found.iter().all(|f| f.legs.iter().any(|&(i, _)| reg.ops[i].ensemble));
let time_only = !found.is_empty()
&& !src.is_dynamic()
&& !dst.is_dynamic()
&& found.iter().all(|f| f.legs.iter().any(|&(i, _)| reg.ops[i].time_dependent));
let small = too_small(reg, &found, src.area(), dst.area());
if found.is_empty() || ensemble_only || time_only || small {
let known: Vec<String> = if small { found.iter().map(|f| found_name(reg, f)).collect() } else { Vec::new() };
for f in datum_chains(reg, ga, gb, &extents) {
if !small || !known.contains(&found_name(reg, &f)) {
found.push(f);
}
}
}
if reverse {
found = found.into_iter().map(Found::reversed).collect();
}
let mut out: Vec<(SortKey, Chain)> = Vec::new();
for f in found {
let [(i1, r1), (i2, r2)] = f.legs;
let (o1, o2) = (®.ops[i1].op, ®.ops[i2].op);
let Some(bounds) = intersection(&o1.chain_bounds, &o2.chain_bounds) else { continue };
if aoi.is_some_and(|a| !intersects(&bounds, a)) {
continue;
}
let named = if contains(&o1.chain_bounds, &o2.chain_bounds) {
Some(o2)
} else if contains(&o2.chain_bounds, &o1.chain_bounds) {
Some(o1)
} else {
None
};
let (a1, a2) = (o1.chain_accuracy, o2.chain_accuracy);
let accuracy = if a1 >= 0.0 && a2 >= 0.0 { a1 + a2 } else { -1.0 };
let name = found_name(reg, &f);
let rank = Op {
bounds,
accuracy,
chain_accuracy: accuracy,
chain_bounds: bounds,
offshore: named.is_some_and(|o| o.offshore),
unknown_area: named.is_none_or(|o| o.unknown_area),
ballpark: false,
concatenated: true,
between_3d: o1.between_3d,
geocentric: o1.geocentric || o2.geocentric,
priority: o1.priority || o2.priority || datum::is_priority(&name),
keeps_height: o1.keeps_height && o2.keeps_height,
steps: o1.steps.iter().chain(&o2.steps).cloned().collect(),
name,
};
let legs = [Leg { op: o1.clone(), reverse: r1 }, Leg { op: o2.clone(), reverse: r2 }];
let key = SortKey::new(&rank, rank.name.clone(), pipeline_steps(&[(o1, r1), (o2, r2)]), aoi);
out.push((key, Chain { legs, rank }));
}
out.sort_by(|x, y| x.0.cmp(&y.0));
let ballpark = !aoi.is_some_and(|a| out.iter().any(|(_, c)| !c.rank.needs_grids() && contains(&c.rank.bounds, a)));
let chains = remove_uninteresting(out.into_iter().map(|(_, c)| c).collect());
Kind::Chains { chains, ballpark: ballpark.then(|| ballpark_op(src, dst)) }
}
fn remove_uninteresting(chains: Vec<Chain>) -> Vec<Chain> {
let count =
|c: &Chain| c.rank.name.split(" + ").filter(|n| !n.starts_with("Conversion from ") && !n.starts_with("axis order change")).count();
let mut kept: Vec<Chain> = Vec::new();
for c in chains {
if let Some(last) = kept.last() {
let (accuracy, last_accuracy) = (c.rank.accuracy, last.rank.accuracy);
if ((accuracy >= last_accuracy && last_accuracy >= 0.0) || (accuracy < 0.0 && last_accuracy >= 0.0))
&& c.rank.bounds == last.rank.bounds
&& count(&c) > count(last)
{
continue;
}
}
kept.push(c);
}
let mut out: Vec<Chain> = Vec::new();
for c in kept {
let same = |o: &Chain| {
o.rank.bounds == c.rank.bounds && o.legs.iter().zip(&c.legs).all(|(x, y)| x.reverse == y.reverse && x.op.steps == y.op.steps)
};
if !out.iter().any(same) {
out.push(c);
}
}
out
}
fn found_name(reg: &epsg::Registry, f: &Found) -> String {
let [(i1, r1), (i2, r2)] = f.legs;
let mut names: Vec<String> = Vec::new();
let mut push = |more: Vec<String>| {
for n in more {
if names.last().is_some_and(|last| inverse_name(last) == n) {
names.pop();
} else {
names.push(n);
}
}
};
push(f.conversions[0].iter().cloned().collect());
push(step_names(®.ops[i1].op, r1));
push(f.conversions[1].iter().cloned().collect());
push(step_names(®.ops[i2].op, r2));
push(f.conversions[2].iter().cloned().collect());
names.join(" + ")
}
fn too_small(reg: &epsg::Registry, found: &[Found], e1: Option<[f64; 4]>, e2: Option<[f64; 4]>) -> bool {
if found.is_empty() || found.len() >= 10 {
return false;
}
let area = |b: Option<[f64; 4]>| b.map_or(0.0, |b| pseudo_area(&b));
let target = match (e1, e2) {
(Some(x), Some(y)) => area(extent_intersection(&x, &y)),
(x, y) => area(x.or(y)),
};
if target <= 0.0 {
return false;
}
!found.iter().any(|f| {
let [(i1, _), (i2, _)] = f.legs;
let Some(op) = intersection(®.ops[i1].op.chain_bounds, ®.ops[i2].op.chain_bounds) else { return false };
let covered = match (e1, e2) {
(Some(x), Some(y)) => {
let (p, q) = (extent_intersection(&op, &x), extent_intersection(&op, &y));
area(p) + area(q) - p.zip(q).map_or(0.0, |(p, q)| area(extent_intersection(&p, &q)))
}
(x, y) => x.or(y).map_or(pseudo_area(&op), |e| area(extent_intersection(&op, &e))),
};
covered > 0.5 * target
})
}
fn extent_intersection(a: &[f64; 4], b: &[f64; 4]) -> Option<[f64; 4]> {
if contains(a, b) {
return Some(*b);
}
if contains(b, a) {
return Some(*a);
}
intersection(a, b)
}
fn rejected_pivot(from: &str, to: &str, pivot: &str) -> bool {
let year = |name: &str, prefix: &str| {
let digits: String = name.strip_prefix(prefix)?.chars().take_while(char::is_ascii_digit).collect();
let v: i32 = digits.parse().ok()?;
(v > 0).then_some(if (80..=99).contains(&v) { v + 1900 } else { v })
};
if from.starts_with("ETRF")
&& to.starts_with("ETRF")
&& let (Some(s), Some(t), Some(p)) = (year(from, "ETRF"), year(to, "ETRF"), year(pivot, "ITRF"))
&& (p < s.min(t) || p > s.max(t))
{
return true;
}
from.starts_with("NAD83(CSRS)") && to.starts_with("NAD83(CSRS)") && matches!(pivot, "NAD83" | "WGS 84")
}
fn irrelevant(reg: &epsg::Registry, i: usize, a: u32, b: u32) -> bool {
reg.ops[i].via.iter().any(|&c| c == a || c == b)
}
fn superseded(reg: &epsg::Registry, ids: impl Iterator<Item = usize> + Clone, any: bool) -> HashSet<usize> {
let keys: HashSet<&str> = ids.clone().map(|i| reg.ops[i].key.as_str()).collect();
let found = |r: &String| keys.contains(r.as_str());
ids.filter(|&i| {
let r = ®.ops[i].replaced_by;
!r.is_empty() && if any { r.iter().any(found) } else { r.iter().all(found) }
})
.collect()
}
fn crs_code_chains(reg: &epsg::Registry, a: &epsg::Geodetic, b: &epsg::Geodetic, extents: &[[f64; 4]]) -> Vec<Found> {
let candidates = |g: &epsg::Geodetic| -> Vec<&epsg::Geodetic> {
reg.by_datum.get(&g.datum).into_iter().flatten().filter_map(|c| reg.geodetic.get(c)).collect()
};
let (ca, cb) = (candidates(a), candidates(b));
let mut found: Vec<Found> = Vec::new();
let proj_only = |found: &[Found]| found.iter().all(|f| f.legs.iter().any(|&(i, _)| reg.ops[i].key.starts_with("PROJ:")));
for same_kind in [true, false] {
let pairs = ca
.iter()
.flat_map(|x| cb.iter().map(move |y| (*x, *y)))
.filter(|(x, y)| x.name == a.name && y.name == b.name && (x.kind == y.kind) == same_kind)
.chain(ca.iter().flat_map(|x| cb.iter().map(move |y| (*x, *y))).filter(|(x, y)| x.name != a.name || y.name != b.name));
for (x, y) in pairs {
let more = crs_code_pair(reg, x, y, extents);
if more.is_empty() {
continue;
}
let framing = |from: &epsg::Geodetic, to: &epsg::Geodetic| {
(from.code != to.code).then(|| conversion_name(from, to)).filter(|n| !n.starts_with(NULL_OFFSET))
};
let (first, last) = (framing(a, x), framing(y, b));
found.extend(more.into_iter().map(|legs| Found { legs, conversions: [first.clone(), None, last.clone()] }));
if !proj_only(&found) {
return found;
}
}
}
found
}
fn crs_code_pair(reg: &epsg::Registry, x: &epsg::Geodetic, y: &epsg::Geodetic, extents: &[[f64; 4]]) -> Vec<[(usize, bool); 2]> {
use epsg::GeodeticKind::*;
let geographic = x.kind != Geocentric && y.kind != Geocentric;
let oldest = if geographic { epsg::datum_date(x.datum).zip(epsg::datum_date(y.datum)).map(|(p, q)| p.min(q)) } else { None };
let fits = |i: usize| extents.iter().all(|e| intersects(®.ops[i].op.bounds, e));
let (Some(from_x), Some(from_y)) = (reg.at.get(&x.code), reg.at.get(&y.code)) else { return Vec::new() };
let other = |i: usize, code: u32| {
let r = ®.ops[i];
if r.source == code { (r.target, false) } else { (r.source, true) }
};
let mut out = Vec::new();
for (rev1, rev2) in [(false, false), (false, true), (true, false), (true, true)] {
let mut rows: Vec<(usize, usize, u32)> = Vec::new();
for &i in from_x {
let (pivot, r1) = other(i, x.code);
if r1 != rev1 || !fits(i) {
continue;
}
let Some(p) = reg.geodetic.get(&pivot) else { continue };
if geographic && (p.kind == Geocentric || oldest.is_some_and(|oldest| epsg::datum_date(p.datum).is_some_and(|d| d < oldest))) {
continue;
}
for &j in from_y {
let (back, r2) = other(j, y.code);
if back != pivot || r2 == rev2 || i == j || !fits(j) || !intersects(®.ops[i].op.bounds, ®.ops[j].op.bounds) {
continue;
}
rows.push((i, j, pivot));
}
}
let gone1 = superseded(reg, rows.iter().map(|r| r.0), false);
let gone2 = superseded(reg, rows.iter().map(|r| r.1), false);
for (i, j, pivot) in rows {
let name = reg.geodetic.get(&pivot).map_or("", |p| p.name.as_str());
if gone1.contains(&i)
|| gone2.contains(&j)
|| !reg.ops[i].op.available()
|| !reg.ops[j].op.available()
|| irrelevant(reg, i, x.code, y.code)
|| irrelevant(reg, j, x.code, y.code)
|| rejected_pivot(&x.name, &y.name, name)
{
continue;
}
out.push([(i, rev1), (j, rev2)]);
}
}
out
}
fn datum_chains(reg: &epsg::Registry, a: &epsg::Geodetic, b: &epsg::Geodetic, extents: &[[f64; 4]]) -> Vec<Found> {
let date = epsg::datum_date;
let csrs = a.name.starts_with("NAD83(CSRS)") && b.name.starts_with("NAD83(CSRS)");
let fits = |i: usize| extents.iter().all(|e| intersects(®.ops[i].op.bounds, e));
let side = |datum: u32| -> HashMap<u32, Vec<(usize, bool)>> {
let mut by: HashMap<u32, Vec<(usize, bool)>> = HashMap::new();
for code in reg.by_datum.get(&datum).into_iter().flatten() {
for &i in reg.at.get(code).into_iter().flatten() {
let r = ®.ops[i];
let (other, reverse) = if r.source == *code { (r.target, false) } else { (r.source, true) };
let Some(o) = reg.geodetic.get(&other) else { continue };
if o.datum == datum || (csrs && matches!(o.name.as_str(), "NAD83" | "WGS 84")) || !fits(i) {
continue;
}
let list = by.entry(o.datum).or_default();
if !list.contains(&(i, reverse)) {
list.push((i, reverse));
}
}
}
by
};
let (from_a, from_b) = (side(a.datum), side(b.datum));
let mut pairs: Vec<((usize, bool), (usize, bool))> = Vec::new();
let mut pivots: Vec<&u32> = from_a.keys().collect();
pivots.sort_unstable();
for pivot in pivots {
let Some(second) = from_b.get(pivot) else { continue };
for &(i, r1) in &from_a[pivot] {
if irrelevant(reg, i, a.code, b.code) {
continue;
}
for &(j, r2) in second {
if intersects(®.ops[i].op.bounds, ®.ops[j].op.bounds) && !irrelevant(reg, j, a.code, b.code) {
pairs.push(((i, r1), (j, !r2)));
}
}
}
}
let gone = superseded(reg, pairs.iter().flat_map(|(x, y)| [x.0, y.0]), true);
let oldest = match (date(a.datum), date(b.datum)) {
(s, Some(t)) if s.is_none_or(|s| t < s) => Some(t),
(s, _) => s,
};
let ancient = |code: u32| {
reg.geodetic.get(&code).and_then(|g| date(g.datum)).is_some_and(|d| d < "1900-01-01") && oldest.is_some_and(|o| o > "1900-01-01")
};
let mut out = Vec::new();
for ((i, r1), (j, r2)) in pairs {
let (o1, o2) = (®.ops[i], ®.ops[j]);
if gone.contains(&i)
|| gone.contains(&j)
|| [o1.source, o1.target, o2.source, o2.target].into_iter().any(ancient)
|| !o1.op.available()
|| !o2.op.available()
{
continue;
}
let ends = |r: &epsg::Registered, reverse: bool| if reverse { (r.target, r.source) } else { (r.source, r.target) };
let ((s1, t1), (s2, t2)) = (ends(o1, r1), ends(o2, r2));
let g = |code: u32| reg.geodetic.get(&code);
let conversion = |from: u32, to: u32| match (g(from), g(to)) {
(Some(f), Some(t)) if from != to => Some(conversion_name(f, t)),
_ => None,
};
out.push(Found { legs: [(i, r1), (j, r2)], conversions: [conversion(a.code, s1), conversion(t1, s2), conversion(t2, b.code)] });
}
out
}
fn ballpark_op(src: &Def, dst: &Def) -> Op {
let accuracy = if src.rotation_only_to(dst) { 0.0 } else { -1.0 };
Op { accuracy, chain_accuracy: accuracy, ballpark: true, ..Op::world(Vec::new()) }
}
struct SortKey {
unknown: bool,
unknown_without_grids: bool,
area: f64,
accuracy: f64,
grids: bool,
steps: usize,
pipeline_steps: usize,
name: String,
}
impl SortKey {
fn new(op: &Op, name: String, pipeline_steps: usize, aoi: Option<&[f64; 4]>) -> Self {
let area = aoi.map_or(Some(op.bounds), |a| intersection(&op.bounds, a)).map_or(0.0, |b| pseudo_area(&b));
let (unknown, grids) = (op.accuracy < 0.0, op.needs_grids());
Self {
unknown,
unknown_without_grids: unknown && !grids,
area,
accuracy: if unknown { 0.0 } else { op.accuracy },
grids,
steps: name.split(" + ").count(),
pipeline_steps,
name,
}
}
fn cmp(&self, other: &Self) -> Ordering {
self.unknown
.cmp(&other.unknown)
.then(self.unknown_without_grids.cmp(&other.unknown_without_grids))
.then(other.area.total_cmp(&self.area))
.then(self.accuracy.total_cmp(&other.accuracy))
.then(self.grids.cmp(&other.grids))
.then(self.steps.cmp(&other.steps))
.then(self.pipeline_steps.cmp(&other.pipeline_steps))
.then(self.name.len().cmp(&other.name.len()))
.then_with(|| other.name.cmp(&self.name))
}
}
fn pipeline_steps(legs: &[(&Op, bool)]) -> usize {
enum Item<'a> {
Step(&'a Step, bool),
Translation([f64; 3]),
}
let cart = |item: &Item| match item {
Item::Step(Step::Cart { ell, inv }, inverted) => Some((*ell, *inv != *inverted)),
_ => None,
};
let translation = |item: &Item| match item {
Item::Translation(t) => Some(*t),
Item::Step(Step::Helmert { t, r, scale, pivot, inv, rates: None }, inverted)
if *scale == 1.0 && *pivot == [0.0; 3] && *r == [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] =>
{
let sign = if inv != inverted { -1.0 } else { 1.0 };
Some(t.map(|v| v * sign))
}
_ => None,
};
let mut run: Vec<Item> = Vec::new();
for &(op, reverse) in legs {
let steps: Vec<(&Step, bool)> =
if reverse { op.steps.iter().rev().map(|s| (s, true)).collect() } else { op.steps.iter().map(|s| (s, false)).collect() };
for (step, inverted) in steps {
let item = Item::Step(step, inverted);
if let (Some(last), Some((ell, false))) = (run.last(), cart(&item))
&& cart(last) == Some((ell, true))
{
run.pop();
continue;
}
if let (Some(a), Some(b)) = (run.last().and_then(translation), translation(&item)) {
run.pop();
let sum = [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
if sum != [0.0; 3] {
run.push(Item::Translation(sum));
}
continue;
}
run.push(item);
}
}
let geocentric = run.iter().any(|item| cart(item).is_some());
run.len() + if geocentric { 2 } else { 0 }
}
fn inverse_name(name: &str) -> String {
if let Some(forward) = name.strip_prefix("Inverse of ") {
return forward.to_string();
}
for kind in ["Conversion", "Null geographic offset", "Null geocentric translation"] {
if let Some((from, to)) = name.strip_prefix(kind).and_then(|n| n.strip_prefix(" from ")).and_then(|n| n.split_once(" to ")) {
return format!("{kind} from {to} to {from}");
}
}
format!("Inverse of {name}")
}
fn step_names(op: &Op, reverse: bool) -> Vec<String> {
let names: Vec<&str> = op.name.split(" + ").filter(|n| !n.is_empty()).collect();
if reverse { names.into_iter().rev().map(inverse_name).collect() } else { names.into_iter().map(str::to_string).collect() }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crs::{SpatialRef, Transformer};
fn moved(src: u32, dst: u32, x: f64, y: f64) -> (f64, f64) {
let (s, d) = (SpatialRef::from_epsg(src).unwrap(), SpatialRef::from_epsg(dst).unwrap());
Transformer::new(&s, &d).unwrap().transform(x, y).unwrap()
}
#[test]
fn areas_across_the_antimeridian() {
let russia = [18.92, 39.87, -168.97, 85.19];
let azores = [-31.34, 39.3, -31.02, 39.77];
assert_eq!(intersection(&russia, &azores), None);
assert!(!intersects(&russia, &azores));
assert_eq!(intersection(&russia, &[170.0, 50.0, -170.0, 60.0]), Some([170.0, 50.0, -170.0, 60.0]));
assert_eq!(intersection(&[-180.0, -90.0, 180.0, 90.0], &russia), Some(russia));
assert_eq!(intersection(&[100.0, 40.0, 120.0, 50.0], &russia), Some([100.0, 40.0, 120.0, 50.0]));
assert!(contains(&russia, &[170.0, 50.0, -170.0, 60.0]) && contains(&russia, &[30.0, 50.0, 40.0, 60.0]));
assert!(!contains(&russia, &azores) && !contains(&azores, &russia));
assert!(contains(&[-180.0, -90.0, 180.0, 90.0], &azores));
}
#[test]
fn chains_outside_their_areas_as_proj() {
let (x, y) = moved(4182, 4740, -31.016816380974838, 39.56501297818613);
assert!((x + 31.021097205869058).abs() < 1e-7 && (y - 39.566344404860935).abs() < 1e-7, "{x} {y}");
let (x, y) = moved(2999, 9053, 298971.18861002335, 8694000.007133322);
assert!((x - 43.16443339530448).abs() < 1e-7 && (y + 11.811498334670878).abs() < 1e-7, "{x} {y}");
}
#[test]
fn chain_names_as_proj() {
assert_eq!(inverse_name("NTF to WGS 84 (1)"), "Inverse of NTF to WGS 84 (1)");
assert_eq!(inverse_name("Inverse of WGS 72BE to WGS 84 (1)"), "WGS 72BE to WGS 84 (1)");
assert_eq!(inverse_name("Conversion from A (geog2D) to A (geocentric)"), "Conversion from A (geocentric) to A (geog2D)");
let reg = epsg::registry();
let name = |a: u32, b: u32| conversion_name(®.geodetic[&a], ®.geodetic[&b]);
assert_eq!(name(7844, 7842), "Conversion from GDA2020 (geog2D) to GDA2020 (geocentric)");
assert_eq!(name(4258, 4937), "Null geographic offset from ETRS89 (geog2D) to ETRS89 (geog3D)");
assert_eq!(name(10310, 10312), "axis order change (2D)");
}
#[test]
fn chains_ordered_as_proj() {
let close = |(x, y): (f64, f64), (gx, gy): (f64, f64), tol: f64| (x - gx).abs() < tol && (y - gy).abs() < tol;
let p = moved(10459, 10606, 19600.97347104833, 592323.3389500268);
assert!(close(p, (113.95807615479848, -27.180067590263132), 1e-8), "{p:?}");
let p = moved(27573, 32431, 412711.90358792024, 3155755.5255349036);
assert!(close(p, (259245.25512248135, 4840436.820252412), 1e-3), "{p:?}");
let p = moved(28351, 4324, 625263.796562132, 8842164.96869988);
assert!(close(p, (124.144357767899, -10.472091951341795), 1e-8), "{p:?}");
let p = moved(2392, 4124, 2500642.641281675, 7632767.385064217);
assert!(close(p, (24.016658156077746, 68.77924926097009), 1e-7), "{p:?}");
}
}