use std::cell::RefCell;
use std::marker::PhantomData;
use crate::{TransBuild, TransParams};
use oxiproj_core::{Coord, IoUnits, Operation, ProjError, ProjResult};
type StackFrame = [Vec<f64>; 4];
fn empty_frame() -> StackFrame {
[Vec::new(), Vec::new(), Vec::new(), Vec::new()]
}
thread_local! {
static COORD_STACK_FRAMES: RefCell<Vec<StackFrame>> = RefCell::new(vec![empty_frame()]);
}
#[must_use = "the pipeline stack scope is only active while this guard is alive; \
binding it to `_` drops it immediately and closes the scope"]
#[derive(Debug)]
pub struct PipelineStackScope {
_marker: PhantomData<*const ()>,
}
pub fn enter_pipeline_scope() -> PipelineStackScope {
COORD_STACK_FRAMES.with(|cell| {
cell.borrow_mut().push(empty_frame());
});
PipelineStackScope {
_marker: PhantomData,
}
}
impl Drop for PipelineStackScope {
fn drop(&mut self) {
COORD_STACK_FRAMES.with(|cell| {
let mut frames = cell.borrow_mut();
if frames.len() > 1 {
frames.pop();
}
});
}
}
fn do_push(c: Coord, which: [bool; 4]) -> ProjResult<Coord> {
let v = c.v();
COORD_STACK_FRAMES.with(|cell| {
let mut frames = cell.borrow_mut();
if let Some(frame) = frames.last_mut() {
for i in 0..4 {
if which[i] {
frame[i].push(v[i]);
}
}
}
});
Ok(c)
}
fn do_pop(c: Coord, which: [bool; 4]) -> ProjResult<Coord> {
let mut v = c.v();
COORD_STACK_FRAMES.with(|cell| {
let mut frames = cell.borrow_mut();
if let Some(frame) = frames.last_mut() {
for i in 0..4 {
if which[i] {
if let Some(val) = frame[i].pop() {
v[i] = val;
}
}
}
}
});
Ok(Coord::new(v[0], v[1], v[2], v[3]))
}
#[derive(Debug)]
struct PushOp {
which: [bool; 4],
}
#[derive(Debug)]
struct PopOp {
which: [bool; 4],
}
impl Operation for PushOp {
fn forward_4d(&self, c: Coord) -> ProjResult<Coord> {
do_push(c, self.which)
}
fn inverse_4d(&self, c: Coord) -> ProjResult<Coord> {
do_pop(c, self.which)
}
fn has_inverse(&self) -> bool {
true
}
}
impl Operation for PopOp {
fn forward_4d(&self, c: Coord) -> ProjResult<Coord> {
do_pop(c, self.which)
}
fn inverse_4d(&self, c: Coord) -> ProjResult<Coord> {
do_push(c, self.which)
}
fn has_inverse(&self) -> bool {
true
}
}
fn parse_which(p: &TransParams) -> [bool; 4] {
let v1 = p.params.exists("v_1");
let v2 = p.params.exists("v_2");
let v3 = p.params.exists("v_3");
let v4 = p.params.exists("v_4");
if !v1 && !v2 && !v3 && !v4 {
[true, true, true, true]
} else {
[v1, v2, v3, v4]
}
}
pub fn new_push(p: &TransParams) -> ProjResult<TransBuild> {
let _ = ProjError::InvalidOp; Ok(TransBuild::new(
Box::new(PushOp {
which: parse_which(p),
}),
IoUnits::Whatever,
IoUnits::Whatever,
))
}
pub fn new_pop(p: &TransParams) -> ProjResult<TransBuild> {
Ok(TransBuild::new(
Box::new(PopOp {
which: parse_which(p),
}),
IoUnits::Whatever,
IoUnits::Whatever,
))
}
#[cfg(test)]
mod tests {
use super::*;
use oxiproj_core::{Coord, Ellipsoid};
struct NoParams;
impl crate::TransParamLookup for NoParams {
fn get_dms(&self, _key: &str) -> Option<f64> {
None
}
fn get_f64(&self, _key: &str) -> Option<f64> {
None
}
fn get_int(&self, _key: &str) -> Option<i64> {
None
}
fn get_str(&self, _key: &str) -> Option<&str> {
None
}
fn get_bool(&self, _key: &str) -> bool {
false
}
fn exists(&self, _key: &str) -> bool {
false
}
}
struct V1V2Params;
impl crate::TransParamLookup for V1V2Params {
fn get_dms(&self, _key: &str) -> Option<f64> {
None
}
fn get_f64(&self, _key: &str) -> Option<f64> {
None
}
fn get_int(&self, _key: &str) -> Option<i64> {
None
}
fn get_str(&self, _key: &str) -> Option<&str> {
None
}
fn get_bool(&self, _key: &str) -> bool {
false
}
fn exists(&self, key: &str) -> bool {
key == "v_1" || key == "v_2"
}
}
fn wgs84() -> Ellipsoid {
Ellipsoid::named("WGS84").expect("WGS84 ellipsoid must be available")
}
fn clear_stacks() {
COORD_STACK_FRAMES.with(|cell| {
*cell.borrow_mut() = vec![empty_frame()];
});
}
fn frame_count() -> usize {
COORD_STACK_FRAMES.with(|cell| cell.borrow().len())
}
#[test]
fn push_all_then_pop_all_round_trips() -> ProjResult<()> {
clear_stacks();
let push = PushOp {
which: [true, true, true, true],
};
let pop = PopOp {
which: [true, true, true, true],
};
let original = Coord::new(1.0, 2.0, 3.0, 4.0);
let after_push = push.forward_4d(original)?;
assert_eq!(after_push.v(), [1.0, 2.0, 3.0, 4.0]);
let modified = Coord::new(9.0, 8.0, 7.0, 6.0);
let restored = pop.forward_4d(modified)?;
assert_eq!(restored.v(), [1.0, 2.0, 3.0, 4.0]);
Ok(())
}
#[test]
fn push_v1_v2_only_preserves_v3_v4() -> ProjResult<()> {
clear_stacks();
let push = PushOp {
which: [true, true, false, false],
};
let pop = PopOp {
which: [true, true, false, false],
};
let original = Coord::new(10.0, 20.0, 30.0, 40.0);
push.forward_4d(original)?;
let modified = Coord::new(99.0, 88.0, 77.0, 66.0);
let restored = pop.forward_4d(modified)?;
assert_eq!(restored.v()[0], 10.0);
assert_eq!(restored.v()[1], 20.0);
assert_eq!(restored.v()[2], 77.0);
assert_eq!(restored.v()[3], 66.0);
Ok(())
}
#[test]
fn push_inverse_acts_as_pop() -> ProjResult<()> {
clear_stacks();
let push = PushOp {
which: [true, true, true, true],
};
let saved = Coord::new(5.0, 6.0, 7.0, 8.0);
push.forward_4d(saved)?;
let modified = Coord::new(0.0, 0.0, 0.0, 0.0);
let restored = push.inverse_4d(modified)?;
assert_eq!(restored.v(), [5.0, 6.0, 7.0, 8.0]);
Ok(())
}
#[test]
fn pop_inverse_acts_as_push() -> ProjResult<()> {
clear_stacks();
let pop = PopOp {
which: [true, true, true, true],
};
let saved = Coord::new(11.0, 22.0, 33.0, 44.0);
pop.inverse_4d(saved)?;
let modified = Coord::new(0.0, 0.0, 0.0, 0.0);
let restored = pop.forward_4d(modified)?;
assert_eq!(restored.v(), [11.0, 22.0, 33.0, 44.0]);
Ok(())
}
#[test]
fn no_params_defaults_to_all_components() {
let which = parse_which(&TransParams {
ellipsoid: &wgs84(),
params: &NoParams,
registry: None,
});
assert_eq!(which, [true, true, true, true]);
}
#[test]
fn v1_v2_params_selects_first_two() {
let which = parse_which(&TransParams {
ellipsoid: &wgs84(),
params: &V1V2Params,
registry: None,
});
assert_eq!(which, [true, true, false, false]);
}
#[test]
fn new_push_builds_successfully() {
let ell = wgs84();
let p = TransParams {
ellipsoid: &ell,
params: &NoParams,
registry: None,
};
assert!(new_push(&p).is_ok());
}
#[test]
fn new_pop_builds_successfully() {
let ell = wgs84();
let p = TransParams {
ellipsoid: &ell,
params: &NoParams,
registry: None,
};
assert!(new_pop(&p).is_ok());
}
#[test]
fn stack_underflow_leaves_component_unchanged() -> ProjResult<()> {
clear_stacks();
let pop = PopOp {
which: [true, false, false, false],
};
let c = Coord::new(99.0, 1.0, 2.0, 3.0);
let result = pop.forward_4d(c)?;
assert_eq!(result.v()[0], 99.0); Ok(())
}
#[test]
fn push_and_pop_have_inverse() {
let push = PushOp { which: [true; 4] };
let pop = PopOp { which: [true; 4] };
assert!(push.has_inverse());
assert!(pop.has_inverse());
}
#[test]
fn enter_pipeline_scope_opens_and_closes_a_frame() {
clear_stacks();
assert_eq!(frame_count(), 1);
{
let _scope = enter_pipeline_scope();
assert_eq!(frame_count(), 2);
{
let _nested = enter_pipeline_scope();
assert_eq!(frame_count(), 3);
}
assert_eq!(frame_count(), 2);
}
assert_eq!(frame_count(), 1);
}
#[test]
fn cross_pipeline_scopes_do_not_contaminate() -> ProjResult<()> {
clear_stacks();
let push = PushOp { which: [true; 4] };
let pop = PopOp { which: [true; 4] };
{
let _scope_a = enter_pipeline_scope();
let point_a = Coord::new(111.0, 222.0, 333.0, 444.0);
push.forward_4d(point_a)?;
}
let restored_b = {
let _scope_b = enter_pipeline_scope();
let point_b = Coord::new(1.0, 2.0, 3.0, 4.0);
push.forward_4d(point_b)?;
pop.forward_4d(Coord::new(9.0, 9.0, 9.0, 9.0))?
};
assert_eq!(
restored_b.v(),
[1.0, 2.0, 3.0, 4.0],
"pipeline B must recover its own pushed point, not pipeline A's orphaned one"
);
clear_stacks();
let untouched = Coord::new(7.0, 8.0, 9.0, 10.0);
let after_pop = pop.forward_4d(untouched)?;
assert_eq!(after_pop.v(), untouched.v());
Ok(())
}
#[test]
fn nested_pipeline_scope_isolates_from_outer() -> ProjResult<()> {
clear_stacks();
let push = PushOp { which: [true; 4] };
let pop = PopOp { which: [true; 4] };
let _outer = enter_pipeline_scope();
let outer_point = Coord::new(1000.0, 2000.0, 3000.0, 4000.0);
push.forward_4d(outer_point)?;
{
let _inner = enter_pipeline_scope();
let inner_point = Coord::new(1.0, 1.0, 1.0, 1.0);
push.forward_4d(inner_point)?;
let inner_restored = pop.forward_4d(Coord::new(0.0, 0.0, 0.0, 0.0))?;
assert_eq!(
inner_restored.v(),
[1.0, 1.0, 1.0, 1.0],
"nested pipeline must recover its own value"
);
}
let outer_restored = pop.forward_4d(Coord::new(0.0, 0.0, 0.0, 0.0))?;
assert_eq!(
outer_restored.v(),
[1000.0, 2000.0, 3000.0, 4000.0],
"outer pipeline's value must survive a nested pipeline's own push/pop"
);
Ok(())
}
#[test]
fn error_path_orphaned_push_is_discarded_on_scope_drop() -> ProjResult<()> {
clear_stacks();
let push = PushOp { which: [true; 4] };
let pop = PopOp { which: [true; 4] };
{
let _aborted_scope = enter_pipeline_scope();
push.forward_4d(Coord::new(42.0, 42.0, 42.0, 42.0))?;
}
let fresh_scope = enter_pipeline_scope();
let fresh_point = Coord::new(5.0, 6.0, 7.0, 8.0);
push.forward_4d(fresh_point)?;
let restored = pop.forward_4d(Coord::new(0.0, 0.0, 0.0, 0.0))?;
assert_eq!(restored.v(), [5.0, 6.0, 7.0, 8.0]);
drop(fresh_scope);
Ok(())
}
}