use oxiproj_projections::ProjParamLookup;
use oxiproj_transformations::TransParamLookup;
#[derive(Debug, Clone)]
pub struct ParamList {
pub entries: Vec<(String, Option<String>)>,
}
fn shrink(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut in_string = false;
let mut pending_ws = false;
let mut last_emitted: Option<char> = None;
for ch in s.chars() {
if in_string {
out.push(ch);
if ch == '"' {
in_string = false;
}
last_emitted = Some(ch);
continue;
}
if ch == '"' {
if pending_ws && last_emitted.is_some() {
out.push(' ');
}
pending_ws = false;
out.push(ch);
last_emitted = Some(ch);
in_string = true;
continue;
}
if ch.is_whitespace() {
pending_ws = true;
continue;
}
if ch == '=' || ch == ',' {
pending_ws = false;
out.push(ch);
last_emitted = Some(ch);
continue;
}
if pending_ws {
pending_ws = false;
if !matches!(last_emitted, None | Some('=') | Some(',')) {
out.push(' ');
}
}
out.push(ch);
last_emitted = Some(ch);
}
out
}
pub fn parse(s: &str) -> ParamList {
let normalized = shrink(s);
let mut entries: Vec<(String, Option<String>)> = Vec::new();
for raw in normalized.split_ascii_whitespace() {
let tok = raw.strip_prefix('+').unwrap_or(raw);
if tok.is_empty() {
continue;
}
match tok.split_once('=') {
Some((key, value)) => entries.push((key.to_string(), Some(value.to_string()))),
None => entries.push((tok.to_string(), None)),
}
}
ParamList { entries }
}
impl ParamList {
pub fn exists(&self, key: &str) -> bool {
self.entries.iter().any(|(k, _)| k == key)
}
pub fn get_str(&self, key: &str) -> Option<&str> {
for (k, v) in &self.entries {
if k == key {
return match v {
Some(val) => Some(val.as_str()),
None => Some(""),
};
}
}
None
}
pub fn get_f64(&self, key: &str) -> Option<f64> {
for (k, v) in &self.entries {
if k == key {
let val = v.as_ref()?;
if let Ok(parsed) = val.trim().parse::<f64>() {
return Some(parsed);
}
return oxiproj_core::parse_leading_f64(val).map(|(x, _)| x);
}
}
None
}
pub fn get_dms(&self, key: &str) -> Option<f64> {
for (k, v) in &self.entries {
if k == key {
let val = v.as_ref()?;
return oxiproj_core::dmstor(val).ok();
}
}
None
}
pub fn get_int(&self, key: &str) -> Option<i64> {
for (k, v) in &self.entries {
if k == key {
let val = v.as_ref()?;
return val.trim().parse::<i64>().ok();
}
}
None
}
pub fn get_bool(&self, key: &str) -> bool {
for (k, v) in &self.entries {
if k == key {
return match v {
None => true,
Some(val) => !matches!(val.as_str(), "f" | "F" | "false"),
};
}
}
false
}
}
pub struct ParamView<'a>(pub &'a ParamList);
impl ProjParamLookup for ParamView<'_> {
fn get_dms(&self, key: &str) -> Option<f64> {
self.0.get_dms(key)
}
fn get_f64(&self, key: &str) -> Option<f64> {
self.0.get_f64(key)
}
fn get_int(&self, key: &str) -> Option<i64> {
self.0.get_int(key)
}
fn get_str(&self, key: &str) -> Option<&str> {
self.0.get_str(key)
}
fn get_bool(&self, key: &str) -> bool {
self.0.get_bool(key)
}
fn exists(&self, key: &str) -> bool {
self.0.exists(key)
}
}
impl TransParamLookup for ParamView<'_> {
fn get_dms(&self, key: &str) -> Option<f64> {
self.0.get_dms(key)
}
fn get_f64(&self, key: &str) -> Option<f64> {
self.0.get_f64(key)
}
fn get_int(&self, key: &str) -> Option<i64> {
self.0.get_int(key)
}
fn get_str(&self, key: &str) -> Option<&str> {
self.0.get_str(key)
}
fn get_bool(&self, key: &str) -> bool {
self.0.get_bool(key)
}
fn exists(&self, key: &str) -> bool {
self.0.exists(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxiproj_core::DEG_TO_RAD;
#[test]
fn parses_basic_merc() {
let pl = parse("+proj=merc +lat_ts=45 +ellps=WGS84");
assert_eq!(pl.entries.len(), 3);
assert_eq!(pl.get_str("proj"), Some("merc"));
let lat_ts = pl.get_dms("lat_ts").unwrap();
assert!((lat_ts - 45.0 * DEG_TO_RAD).abs() < 1e-12);
}
#[test]
fn bare_flag_is_true() {
let pl = parse("+south");
assert_eq!(pl.entries.len(), 1);
assert_eq!(pl.entries[0], ("south".to_string(), None));
assert!(pl.get_bool("south"));
assert!(!pl.get_bool("missing"));
}
#[test]
fn parses_int() {
let pl = parse("+zone=32");
assert_eq!(pl.get_int("zone"), Some(32));
}
#[test]
fn parses_pipeline_tokens() {
let pl = parse("+proj=pipeline +step +proj=utm +zone=32 +step +inv");
let e = &pl.entries;
assert!(e.contains(&("proj".to_string(), Some("pipeline".to_string()))));
assert!(e.contains(&("step".to_string(), None)));
assert!(e.contains(&("proj".to_string(), Some("utm".to_string()))));
assert!(e.contains(&("zone".to_string(), Some("32".to_string()))));
assert!(e.contains(&("inv".to_string(), None)));
assert_eq!(
e.iter().filter(|(k, v)| k == "step" && v.is_none()).count(),
2
);
}
#[test]
fn lon_0_dms() {
let pl = parse("+lon_0=9");
let v = pl.get_dms("lon_0").unwrap();
assert!((v - 9.0 * DEG_TO_RAD).abs() < 1e-12);
assert!((v - 0.157079632679).abs() < 1e-9);
}
#[test]
fn shrink_collapses_spaces_around_equals() {
let pl = parse("+proj=helmert x = 0.01270 dx =-0.0029 s = 0.00195");
assert_eq!(pl.get_str("proj"), Some("helmert"));
assert_eq!(pl.get_str("x"), Some("0.01270"));
assert_eq!(pl.get_str("dx"), Some("-0.0029"));
assert_eq!(pl.get_str("s"), Some("0.00195"));
}
#[test]
fn shrink_makes_commas_greedy() {
let pl = parse("+proj=helmert towgs84 = 1, 2, 3, 4, 5, 6, 7");
assert_eq!(pl.get_str("towgs84"), Some("1,2,3,4,5,6,7"));
}
#[test]
fn shrink_preserves_scientific_notation_plus() {
let pl = parse("+proj=merc +foo=1.23e+08");
assert_eq!(pl.get_str("foo"), Some("1.23e+08"));
}
#[test]
fn false_values_are_false() {
let pl = parse("+over=false +foo=f +bar=t");
assert!(!pl.get_bool("over"));
assert!(!pl.get_bool("foo"));
assert!(pl.get_bool("bar"));
}
}