use crate::Error;
use std::collections::BTreeMap;
pub trait Tokenize {
fn remove_comments(&self) -> String;
fn handle_prefix_modifiers(&self) -> String;
fn split_into_steps(&self) -> Vec<String>;
fn split_into_parameters(&self) -> BTreeMap<String, String>;
fn normalize(&self) -> String;
fn is_pipeline(&self) -> bool;
fn is_resource_name(&self) -> bool;
fn operator_name(&self) -> String;
}
impl<T> Tokenize for T
where
T: AsRef<str>,
{
fn remove_comments(&self) -> String {
let all = self
.as_ref()
.trim()
.replace("\r\n", "\n") .replace('\r', "\n") .replace("\n:", "\n") .to_string();
let mut trimmed = String::new();
for line in all.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let line: Vec<&str> = line.trim().split('#').collect();
if line[0].starts_with('#') {
continue;
}
trimmed += " ";
trimmed += line[0].trim();
}
trimmed
}
fn handle_prefix_modifiers(&self) -> String {
if self.is_pipeline() {
return self.as_ref().to_string();
}
let mut elements: Vec<_> = self.as_ref().split_whitespace().collect();
if elements.is_empty() {
return "".to_string();
}
let modifiers = ["inv", "omit_fwd", "omit_inv"];
while modifiers.contains(&elements[0]) {
elements.rotate_left(1);
}
elements.join(" ")
}
fn split_into_steps(&self) -> Vec<String> {
self.remove_comments()
.normalize()
.split('|')
.filter(|x| !x.trim().is_empty())
.map(|x| x.handle_prefix_modifiers())
.collect()
}
fn split_into_parameters(&self) -> BTreeMap<String, String> {
let step = self.as_ref().normalize().handle_prefix_modifiers();
let elements: Vec<_> = step.split_whitespace().collect();
let mut params = BTreeMap::new();
if step.is_pipeline() {
params.insert(String::from("_name"), step);
return params;
}
for element in elements {
let mut parts: Vec<&str> = element.trim().split('=').collect();
parts.push("true");
assert!(parts.len() > 1);
if params.is_empty() && parts.len() == 2 {
params.insert(String::from("_name"), String::from(parts[0]));
continue;
}
params.insert(String::from(parts[0]), String::from(parts[1]));
}
params
}
fn normalize(&self) -> String {
self.as_ref()
.trim()
.trim_matches(':')
.replace("\n:", "\n")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.replace("= ", "=")
.replace(": ", ":")
.replace(", ", ",")
.replace("| ", "|")
.replace("> ", ">")
.replace("< ", "<")
.replace(" =", "=")
.replace(" :", ":")
.replace(" ,", ",")
.replace(" |", "|")
.replace(" >", ">")
.replace(" <", "<")
.replace('>', "|omit_inv ")
.replace('<', "|omit_fwd ")
.replace("₀=", "_0=")
.replace("₁=", "_1=")
.replace("₂=", "_2=")
.replace("₃=", "_3=")
.replace("₄=", "_4=")
.replace("₅=", "_5=")
.replace("₆=", "_6=")
.replace("₇=", "_7=")
.replace("₈=", "_8=")
.replace("₉=", "_9=")
.replace("$ ", "$") .split_whitespace()
.collect::<Vec<_>>()
.join(" ")
}
fn is_pipeline(&self) -> bool {
self.as_ref().contains('|') || self.as_ref().contains('<') || self.as_ref().contains('>')
}
fn is_resource_name(&self) -> bool {
self.operator_name().contains(':')
}
fn operator_name(&self) -> String {
if self.is_pipeline() {
return "".to_string();
}
self.split_into_parameters()
.get("_name")
.unwrap_or(&"".to_string())
.to_string()
}
}
pub fn parse_proj(definition: &str) -> Result<String, Error> {
if definition.contains('|') | !definition.contains("proj") {
return Ok(definition.to_string());
}
let all = definition
.replace("\r\n", "\n")
.replace('\r', "\n")
.replace(" +", " ")
.replace("\n+", " ")
.trim()
.trim_start_matches('+')
.to_string();
let mut trimmed = String::new();
for line in all.lines() {
let line = line.trim();
let line: Vec<&str> = line.trim().split('#').collect();
if line[0].starts_with('#') {
continue;
}
trimmed += " ";
trimmed += line[0].trim();
}
trimmed = " ".to_string() + &trimmed.normalize() + " ";
let steps: Vec<String> = trimmed
.split(" step ")
.filter(|x| !x.trim().trim_start_matches("step ").is_empty())
.map(|x| x.trim().trim_start_matches("step ").to_string())
.collect();
let mut geodesy_steps = Vec::new();
let mut pipeline_globals = "".to_string();
let mut pipeline_is_inverted = false;
for (step_index, step) in steps.iter().enumerate() {
let mut elements: Vec<_> = step.split_whitespace().map(|x| x.to_string()).collect();
for (i, element) in elements.iter().enumerate() {
if element.starts_with("init=") {
return Err(Error::Unsupported(
"parse_proj does not support PROJ init clauses: ".to_string() + step,
));
}
if element.starts_with("proj=") {
elements.swap(i, 0);
elements[0] = elements[0][5..].to_string();
if elements[0] == "pipeline" {
if step_index != 0 {
return Err(Error::Unsupported(
"PROJ does not support nested pipelines: ".to_string() + &trimmed,
));
}
elements.remove(0);
if elements.contains(&"inv".to_string()) {
pipeline_is_inverted = true;
}
let pipeline_globals_elements: Vec<String> = elements
.join(" ")
.trim()
.to_string()
.split_whitespace()
.filter(|x| x.trim() != "inv")
.map(|x| x.trim().to_string())
.collect();
pipeline_globals = pipeline_globals_elements.join(" ").trim().to_string();
elements.clear();
}
break;
}
}
tidy_proj(&mut elements)?;
let mut geodesy_step = elements.join(" ").trim().to_string();
if !geodesy_step.is_empty() {
if !pipeline_globals.is_empty() {
elements.insert(1, pipeline_globals.clone());
}
let step_is_inverted = elements.contains(&"inv".to_string());
elements = elements
.iter()
.filter(|x| x.as_str() != "inv")
.map(|x| match x.as_str() {
"omit_fwd" => "omit_inv",
"omit_inv" => "omit_fwd",
_ => x,
})
.map(|x| x.to_string())
.collect();
if step_is_inverted != pipeline_is_inverted {
elements.insert(1, "inv".to_string());
}
geodesy_step = elements.join(" ").trim().to_string();
if pipeline_is_inverted {
geodesy_steps.insert(0, geodesy_step);
} else {
geodesy_steps.push(geodesy_step);
}
}
}
Ok(geodesy_steps.join(" | ").trim().to_string())
}
fn tidy_proj(elements: &mut Vec<String>) -> Result<(), Error> {
let mut ellps_def: [Option<usize>; 3] = [None; 3];
for (i, element) in elements.iter().enumerate() {
if element.starts_with("ellps=") {
ellps_def[0] = Some(i);
}
if element.starts_with("a=") {
ellps_def[1] = Some(i);
}
if element.starts_with("rf=") {
ellps_def[2] = Some(i);
}
}
if let [None, Some(a_idx), Some(rf_idx)] = ellps_def {
let a = elements[a_idx][2..].to_string();
let rf = elements[rf_idx][3..].to_string();
elements.push(format!("ellps={a},{rf}").to_string());
if a_idx > rf_idx {
elements.remove(a_idx);
elements.remove(rf_idx);
} else {
elements.remove(rf_idx);
elements.remove(a_idx);
}
}
for (i, element) in elements.iter().enumerate() {
if let Some(stripped) = element.strip_prefix("k=") {
elements[i] = "k_0=".to_string() + stripped;
break;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
#[test]
fn token() -> Result<(), Error> {
assert_eq!("foo bar $ baz = bonk".normalize(), "foo bar $baz=bonk");
assert_eq!(
"foo | bar baz = bonk, bonk , bonk x₁=42 y₁ = 7".normalize(),
"foo|bar baz=bonk,bonk,bonk x_1=42 y_1=7"
);
assert_eq!(
" : foo>bar <baz = bonk,\n: bonk , bonk<zap".normalize(),
"foo|omit_inv bar|omit_fwd baz=bonk,bonk,bonk|omit_fwd zap"
);
let all = "foo>bar |baz bonk=bonk, bonk , bonk x₁=42 <zap".split_into_steps();
assert_eq!(all.len(), 4);
assert_eq!(all[0], "foo");
assert_eq!(all[1], "bar omit_inv");
assert_eq!(all[2], "baz bonk=bonk,bonk,bonk x_1=42");
assert_eq!(all[3], "zap omit_fwd");
let args = "foo bar baz=bonk".split_into_parameters();
assert_eq!(args["_name"], "foo");
assert_eq!(args["bar"], "true");
assert_eq!(args["baz"], "bonk");
assert_eq!("foo bar baz=bonk".operator_name(), "foo");
assert_eq!("inv foo bar baz=bonk".operator_name(), "foo");
assert!("foo | bar".is_pipeline());
assert!("foo > bar".is_pipeline());
assert!("foo < bar".is_pipeline());
assert!("foo:bar".is_resource_name());
let args = "foo x₁=42".split_into_parameters();
assert_eq!(args["_name"], "foo");
assert_eq!(args["x_1"], "42");
assert_eq!("foo bar baz= $bonk".operator_name(), "foo");
Ok(())
}
#[test]
fn proj() -> Result<(), Error> {
assert_eq!(
parse_proj("+a = 1 +proj =foo b= 2 ")?,
"foo a=1 b=2"
);
assert_eq!(
parse_proj("+a = 1 +proj =foo + b= 2 ")?,
"foo a=1 b=2"
);
assert_eq!(parse_proj(" proj=")?, "");
assert_eq!(
parse_proj("proj=pipeline +foo=bar +step proj=utm zone=32")?,
"utm foo=bar zone=32"
);
assert_eq!(
parse_proj(
"proj=pipeline +foo = bar ellps=GRS80 step proj=cart step proj=helmert s=3 step proj=cart ellps=intl"
)?,
"cart foo=bar ellps=GRS80 | helmert foo=bar ellps=GRS80 s=3 | cart foo=bar ellps=GRS80 ellps=intl"
);
assert_eq!(
parse_proj("proj=utm zone=32 step proj=utm inv zone=32")?,
"utm zone=32 | utm inv zone=32"
);
assert_eq!(
parse_proj(
" +step proj = step step=quickstep step step proj=utm inv zone=32 step proj=stepwise step proj=quickstep"
)?,
"step step=quickstep | utm inv zone=32 | stepwise | quickstep"
);
assert_eq!(
parse_proj(
"inv ellps=intl proj=pipeline ugly=syntax +step inv proj=utm zone=32 step proj=utm zone=33"
)?,
"utm inv ellps=intl ugly=syntax zone=33 | utm ellps=intl ugly=syntax zone=32"
);
assert_eq!(
parse_proj(
"proj=pipeline inv +step omit_fwd inv proj=utm zone=32 step omit_inv proj=utm zone=33"
)?,
"utm inv omit_fwd zone=33 | utm omit_inv zone=32"
);
assert!(matches!(
parse_proj("proj=pipeline step proj=pipeline"),
Err(Error::Unsupported(_))
));
assert!(matches!(
parse_proj("pipeline step init=another_pipeline step proj=noop"),
Err(Error::Unsupported(_))
));
let mut ctx = Minimal::default();
let op = ctx.op("helmert x=1 x=2")?;
let mut operands = crate::test_data::coor2d();
assert_eq!(2, ctx.apply(op, Fwd, &mut operands)?);
assert_eq!(operands[0][0], 57.0);
assert_eq!(operands[1][0], 61.0);
Ok(())
}
#[test]
fn tidy_proj() -> Result<(), Error> {
assert_eq!(
parse_proj(
"+proj=pipeline +step +inv +proj=tmerc +a=6378249.145 +rf=293.465 +step +proj=step2"
)?,
"tmerc inv ellps=6378249.145,293.465 | step2"
);
assert_eq!(parse_proj("+proj=tmerc +ellps=GRS80")?, "tmerc ellps=GRS80");
assert_eq!(
parse_proj("+proj=tmerc +ellps=GRS80 +a=1")?,
"tmerc ellps=GRS80 a=1"
);
assert_eq!(parse_proj("+proj=tmerc +k=1.5")?, "tmerc k_0=1.5");
Ok(())
}
}