use crate::pj::Pj;
pub(crate) fn optimize_pipeline(steps: Vec<Pj>) -> Vec<Pj> {
let steps = remove_complete_noops(steps);
cancel_axisswap_pairs(steps)
}
fn remove_complete_noops(steps: Vec<Pj>) -> Vec<Pj> {
steps
.into_iter()
.filter(|s| !(s.omit_fwd && s.omit_inv))
.collect()
}
fn cancel_axisswap_pairs(steps: Vec<Pj>) -> Vec<Pj> {
let mut result: Vec<Pj> = Vec::with_capacity(steps.len());
let mut iter = steps.into_iter().peekable();
while let Some(current) = iter.next() {
let cancels = iter
.peek()
.map(|next| {
current.op_name == "axisswap"
&& next.op_name == "axisswap"
&& axisswap_steps_cancel(¤t, next)
})
.unwrap_or(false);
if cancels {
let _ = iter.next();
} else {
result.push(current);
}
}
result
}
fn invert_perm(p: [i8; 4]) -> [i8; 4] {
let mut inv = [0i8; 4];
for (i, &val) in p.iter().enumerate() {
let axis = (val.unsigned_abs() as usize).saturating_sub(1);
inv[axis] = val.signum() * (i as i8 + 1);
}
inv
}
fn effective_perm(order: [i8; 4], inverted: bool) -> [i8; 4] {
if inverted {
invert_perm(order)
} else {
order
}
}
fn axisswap_steps_cancel(a: &Pj, b: &Pj) -> bool {
if a.omit_fwd || a.omit_inv || b.omit_fwd || b.omit_inv {
return false;
}
let (order_a, order_b) = match (a.axisswap_order, b.axisswap_order) {
(Some(x), Some(y)) => (x, y),
_ => return false,
};
let eff_a = effective_perm(order_a, a.inverted);
let eff_b = effective_perm(order_b, b.inverted);
eff_b == invert_perm(eff_a)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pj::Pj;
use oxiproj_core::{Ellipsoid, IoUnits};
#[derive(Debug)]
struct IdOp;
impl oxiproj_core::Operation for IdOp {
fn forward_2d(&self, lp: oxiproj_core::Lp) -> oxiproj_core::ProjResult<oxiproj_core::Xy> {
Ok(oxiproj_core::Xy::new(lp.lam, lp.phi))
}
fn inverse_2d(&self, xy: oxiproj_core::Xy) -> oxiproj_core::ProjResult<oxiproj_core::Lp> {
Ok(oxiproj_core::Lp::new(xy.x, xy.y))
}
}
fn make_pj(op_name: &str, omit_fwd: bool, omit_inv: bool) -> Pj {
let ellipsoid = Ellipsoid::named("WGS84").expect("WGS84");
Pj {
operation: Box::new(IdOp),
ellipsoid,
factors_es: ellipsoid.es,
lam0: 0.0,
phi0: 0.0,
x0: 0.0,
y0: 0.0,
z0: 0.0,
k0: 1.0,
to_meter: 1.0,
fr_meter: 1.0,
vto_meter: 1.0,
vfr_meter: 1.0,
from_greenwich: 0.0,
over: false,
geoc: false,
lon_wrap_center: None,
is_latlong: false,
left: IoUnits::Whatever,
right: IoUnits::Whatever,
inverted: false,
bypass_prepare_finalize: true,
omit_fwd,
omit_inv,
ad_proj: None,
op_name: op_name.to_string(),
axisswap_order: None,
}
}
fn make_axisswap(order: [i8; 4], inverted: bool) -> Pj {
let mut pj = make_pj("axisswap", false, false);
pj.axisswap_order = Some(order);
pj.inverted = inverted;
pj
}
#[test]
fn noop_removal_both_omit() {
let steps = vec![
make_pj("noop", false, false),
make_pj("noop", true, true), make_pj("noop", false, false),
];
let result = optimize_pipeline(steps);
assert_eq!(
result.len(),
2,
"middle step with omit_fwd+omit_inv must be removed"
);
}
#[test]
fn noop_removal_only_fwd_kept() {
let steps = vec![make_pj("noop", true, false)];
let result = optimize_pipeline(steps);
assert_eq!(result.len(), 1, "omit_fwd-only step must be kept");
}
#[test]
fn noop_removal_only_inv_kept() {
let steps = vec![make_pj("noop", false, true)];
let result = optimize_pipeline(steps);
assert_eq!(result.len(), 1, "omit_inv-only step must be kept");
}
#[test]
fn axisswap_involution_pair_cancelled() {
let steps = vec![
make_axisswap([2, 1, 3, 4], false),
make_axisswap([2, 1, 3, 4], false),
];
let result = optimize_pipeline(steps);
assert_eq!(result.len(), 0, "two `+order=2,1` steps must cancel");
}
#[test]
fn axisswap_negate_involution_pair_cancelled() {
let steps = vec![
make_axisswap([1, -2, 3, 4], false),
make_axisswap([1, -2, 3, 4], false),
];
let result = optimize_pipeline(steps);
assert_eq!(result.len(), 0, "two `+order=1,-2` steps must cancel");
}
#[test]
fn axisswap_fwd_inv_pair_cancelled() {
let steps = vec![
make_axisswap([2, 3, 1, 4], false),
make_axisswap([2, 3, 1, 4], true),
];
let result = optimize_pipeline(steps);
assert_eq!(result.len(), 0, "a permutation and its inverse must cancel");
}
#[test]
fn axisswap_three_cycle_twice_not_cancelled() {
let steps = vec![
make_axisswap([2, 3, 1, 4], false),
make_axisswap([2, 3, 1, 4], false),
];
let result = optimize_pipeline(steps);
assert_eq!(
result.len(),
2,
"two equal 3-cycles do NOT cancel (net 3,1,2)"
);
}
#[test]
fn axisswap_rotation_twice_not_cancelled() {
let steps = vec![
make_axisswap([-2, 1, 3, 4], false),
make_axisswap([-2, 1, 3, 4], false),
];
let result = optimize_pipeline(steps);
assert_eq!(result.len(), 2, "two `+order=-2,1` rotations do NOT cancel");
}
#[test]
fn axisswap_swap_then_negate_not_cancelled() {
let steps = vec![
make_axisswap([2, 1, 3, 4], false),
make_axisswap([1, -2, 3, 4], false),
];
let result = optimize_pipeline(steps);
assert_eq!(
result.len(),
2,
"distinct non-inverse permutations must NOT cancel"
);
}
#[test]
fn axisswap_with_omit_not_cancelled() {
let mut a = make_axisswap([2, 1, 3, 4], false);
a.omit_fwd = true; let steps = vec![a, make_axisswap([2, 1, 3, 4], false)];
let result = optimize_pipeline(steps);
assert_eq!(
result.len(),
2,
"axisswap with selective omit must not cancel"
);
}
#[test]
fn axisswap_unknown_order_not_cancelled() {
let steps = vec![
make_pj("axisswap", false, false),
make_pj("axisswap", false, false),
];
let result = optimize_pipeline(steps);
assert_eq!(
result.len(),
2,
"axisswap steps without a known permutation must not cancel"
);
}
#[test]
fn axisswap_singleton_kept() {
let steps = vec![make_axisswap([2, 1, 3, 4], false)];
let result = optimize_pipeline(steps);
assert_eq!(result.len(), 1, "single axisswap must be kept");
}
#[test]
fn invert_perm_round_trips() {
for p in [
[1, 2, 3, 4],
[2, 1, 3, 4],
[1, -2, 3, 4],
[2, 3, 1, 4],
[-2, 1, 3, 4],
[4, 3, 2, 1],
[-1, -2, -3, -4],
] {
assert_eq!(
invert_perm(invert_perm(p)),
p,
"invert twice restores {p:?}"
);
}
}
#[test]
fn empty_input_stays_empty() {
let result = optimize_pipeline(vec![]);
assert!(result.is_empty());
}
}