use crate::error::{Error, Result};
#[cfg(not(feature = "std"))]
use alloc::collections::BTreeMap as HashMap;
#[cfg(not(feature = "std"))]
use alloc::format;
#[cfg(not(feature = "std"))]
use alloc::string::{String, ToString};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[cfg(feature = "std")]
use std::collections::HashMap;
pub struct WktParser {
input: String,
position: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct WktNode {
pub node_type: String,
pub value: Option<String>,
pub children: Vec<WktNode>,
pub parameters: HashMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AxisDirection {
North,
South,
East,
West,
Up,
Down,
Other,
}
impl AxisDirection {
pub fn from_keyword(s: &str) -> Self {
match s.to_uppercase().as_str() {
"NORTH" => Self::North,
"SOUTH" => Self::South,
"EAST" => Self::East,
"WEST" => Self::West,
"UP" => Self::Up,
"DOWN" => Self::Down,
_ => Self::Other,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AxisInfo {
pub name: String,
pub direction: AxisDirection,
}
impl WktParser {
pub fn new<S: Into<String>>(input: S) -> Self {
Self {
input: input.into(),
position: 0,
}
}
pub fn parse(&mut self) -> Result<WktNode> {
self.skip_whitespace();
self.parse_node()
}
fn parse_node(&mut self) -> Result<WktNode> {
let node_type = self.parse_identifier()?;
self.skip_whitespace();
if !self.expect_char('[')? {
return Err(Error::wkt_parse_error(
self.position,
format!("Expected '[' after {}", node_type),
));
}
self.skip_whitespace();
let value = if self.peek_char() == Some('"') {
Some(self.parse_string()?)
} else {
None
};
let mut children = Vec::new();
let mut parameters = HashMap::new();
let mut first_item = value.is_none();
loop {
self.skip_whitespace();
if self.peek_char() == Some(']') {
self.advance();
break;
}
if !first_item {
if !self.expect_char(',')? {
return Err(Error::wkt_parse_error(
self.position,
"Expected ',' or ']'".to_string(),
));
}
self.skip_whitespace();
}
first_item = false;
let saved_pos = self.position;
if self.is_identifier_start() {
let _ident_result = self.parse_identifier();
if _ident_result.is_ok() {
self.skip_whitespace();
if self.peek_char() == Some('[') {
self.position = saved_pos;
children.push(self.parse_node()?);
} else {
self.position = saved_pos;
let (key, value) = self.parse_parameter()?;
parameters.insert(key, value);
}
} else {
self.position = saved_pos;
let (key, value) = self.parse_parameter()?;
parameters.insert(key, value);
}
} else {
let (key, value) = self.parse_parameter()?;
parameters.insert(key, value);
}
}
Ok(WktNode {
node_type,
value,
children,
parameters,
})
}
fn parse_identifier(&mut self) -> Result<String> {
let mut ident = String::new();
while let Some(ch) = self.peek_char() {
if ch.is_ascii_alphanumeric() || ch == '_' {
ident.push(ch);
self.advance();
} else {
break;
}
}
if ident.is_empty() {
Err(Error::wkt_parse_error(
self.position,
"Expected identifier".to_string(),
))
} else {
Ok(ident)
}
}
fn parse_string(&mut self) -> Result<String> {
if !self.expect_char('"')? {
return Err(Error::wkt_parse_error(
self.position,
"Expected '\"'".to_string(),
));
}
let mut value = String::new();
loop {
match self.peek_char() {
Some('"') => {
self.advance();
break;
}
Some('\\') => {
self.advance();
if let Some(ch) = self.peek_char() {
value.push(ch);
self.advance();
} else {
return Err(Error::wkt_parse_error(
self.position,
"Unexpected end of string".to_string(),
));
}
}
Some(ch) => {
value.push(ch);
self.advance();
}
None => {
return Err(Error::wkt_parse_error(
self.position,
"Unterminated string".to_string(),
));
}
}
}
Ok(value)
}
fn parse_parameter(&mut self) -> Result<(String, String)> {
let saved_pos = self.position;
if let Ok(ident) = self.parse_identifier() {
self.skip_whitespace();
if self.peek_char() == Some('=') {
self.advance();
self.skip_whitespace();
let value = self.parse_value()?;
return Ok((ident, value));
}
}
self.position = saved_pos;
let value = self.parse_value()?;
Ok((format!("param_{}", self.position), value))
}
fn parse_value(&mut self) -> Result<String> {
self.skip_whitespace();
if self.peek_char() == Some('"') {
self.parse_string()
} else {
self.parse_number()
}
}
fn parse_number(&mut self) -> Result<String> {
let mut number = String::new();
if self.peek_char() == Some('-') {
number.push('-');
self.advance();
}
while let Some(ch) = self.peek_char() {
if ch.is_ascii_digit() || ch == '.' || ch == 'e' || ch == 'E' || ch == '+' || ch == '-'
{
number.push(ch);
self.advance();
} else {
break;
}
}
if number.is_empty() || number == "-" {
Err(Error::wkt_parse_error(
self.position,
"Expected number".to_string(),
))
} else {
Ok(number)
}
}
fn is_identifier_start(&self) -> bool {
matches!(self.peek_char(), Some(ch) if ch.is_ascii_alphabetic() || ch == '_')
}
fn expect_char(&mut self, expected: char) -> Result<bool> {
if self.peek_char() == Some(expected) {
self.advance();
Ok(true)
} else {
Ok(false)
}
}
fn peek_char(&self) -> Option<char> {
self.input.chars().nth(self.position)
}
fn advance(&mut self) {
if self.position < self.input.len() {
self.position += 1;
}
}
fn skip_whitespace(&mut self) {
while let Some(ch) = self.peek_char() {
if ch.is_whitespace() {
self.advance();
} else {
break;
}
}
}
}
impl WktNode {
pub fn find_child(&self, node_type: &str) -> Option<&WktNode> {
self.children
.iter()
.find(|child| child.node_type == node_type)
}
pub fn find_children(&self, node_type: &str) -> Vec<&WktNode> {
self.children
.iter()
.filter(|child| child.node_type == node_type)
.collect()
}
pub fn get_parameter(&self, key: &str) -> Option<&str> {
self.parameters.get(key).map(|s| s.as_str())
}
pub fn find_child_any(&self, node_types: &[&str]) -> Option<&WktNode> {
self.children
.iter()
.find(|child| node_types.iter().any(|t| child.node_type == *t))
}
pub fn to_string_repr(&self) -> String {
let mut result = self.node_type.clone();
result.push('[');
if let Some(value) = &self.value {
result.push('"');
result.push_str(value);
result.push('"');
if !self.children.is_empty() || !self.parameters.is_empty() {
result.push(',');
}
}
for (i, child) in self.children.iter().enumerate() {
if i > 0 || self.value.is_some() {
result.push(',');
}
result.push_str(&child.to_string_repr());
}
for (i, (key, value)) in self.parameters.iter().enumerate() {
if i > 0 || !self.children.is_empty() || self.value.is_some() {
result.push(',');
}
result.push_str(key);
result.push('=');
result.push_str(value);
}
result.push(']');
result
}
}
pub fn parse_wkt<S: Into<String>>(wkt: S) -> Result<WktNode> {
let mut parser = WktParser::new(wkt);
parser.parse()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WktVersion {
Wkt1,
Wkt2,
Unknown,
}
#[derive(Debug, Clone)]
pub struct WktError {
pub message: String,
pub position: Option<usize>,
}
impl WktError {
pub fn new(message: impl Into<String>, position: Option<usize>) -> Self {
Self {
message: message.into(),
position,
}
}
}
#[cfg(feature = "std")]
impl std::fmt::Display for WktError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.position {
Some(pos) => write!(f, "WKT parse error at position {}: {}", pos, self.message),
None => write!(f, "WKT parse error: {}", self.message),
}
}
}
#[cfg(not(feature = "std"))]
impl core::fmt::Display for WktError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self.position {
Some(pos) => write!(f, "WKT parse error at position {}: {}", pos, self.message),
None => write!(f, "WKT parse error: {}", self.message),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for WktError {}
impl WktParser {
pub fn detect_version(wkt: &str) -> WktVersion {
let upper = wkt.to_uppercase();
if upper.contains("PROJCRS")
|| upper.contains("GEOGCRS")
|| upper.contains("GEODCRS")
|| upper.contains("ENGCRS")
|| upper.contains("VERTCRS")
|| upper.contains("COMPOUNDCRS")
{
return WktVersion::Wkt2;
}
if upper.contains("PROJCS")
|| upper.contains("GEOGCS")
|| upper.contains("GEOCCS")
|| upper.contains("VERT_CS")
|| upper.contains("COMPD_CS")
{
return WktVersion::Wkt1;
}
WktVersion::Unknown
}
pub fn extract_name(wkt: &str) -> Option<String> {
let bracket_pos = wkt.find('[')?;
let after_bracket = &wkt[bracket_pos + 1..];
let quote_start = after_bracket.find('"')?;
let after_quote = &after_bracket[quote_start + 1..];
let quote_end = after_quote.find('"')?;
Some(after_quote[..quote_end].to_string())
}
pub fn extract_epsg(wkt: &str) -> Option<i32> {
if let Some(idx) = wkt.find("AUTHORITY[\"EPSG\",\"") {
let after = &wkt[idx + "AUTHORITY[\"EPSG\",\"".len()..];
let end = after.find('"')?;
let code_str = &after[..end];
return code_str.parse::<i32>().ok();
}
if let Some(idx) = wkt.find("ID[\"EPSG\",") {
let after = &wkt[idx + "ID[\"EPSG\",".len()..];
let after = after.trim_start();
let after = after.trim_start_matches('"');
let end = after.find(|c: char| !c.is_ascii_digit())?;
let code_str = &after[..end];
return code_str.parse::<i32>().ok();
}
None
}
pub fn extract_unit(wkt: &str) -> Option<(String, f64)> {
let search_terms = ["UNIT[\"", "LENGTHUNIT[\"", "ANGLEUNIT[\""];
for term in &search_terms {
if let Some(idx) = wkt.find(term) {
let after = &wkt[idx + term.len()..];
let name_end = after.find('"')?;
let unit_name = after[..name_end].to_string();
let rest = &after[name_end + 1..];
let comma_pos = rest.find(',')?;
let rest_after_comma = rest[comma_pos + 1..].trim_start();
let num_end = rest_after_comma
.find([']', ','])
.unwrap_or(rest_after_comma.len());
let factor_str = rest_after_comma[..num_end].trim();
if let Ok(factor) = factor_str.parse::<f64>() {
return Some((unit_name, factor));
}
}
}
None
}
pub fn extract_axes(wkt: &str) -> Vec<AxisInfo> {
let mut axes = Vec::new();
let mut search_from = 0;
while let Some(idx) = wkt[search_from..].find("AXIS[\"") {
let abs_idx = search_from + idx;
let after = &wkt[abs_idx + "AXIS[\"".len()..];
let name_end = match after.find('"') {
Some(e) => e,
None => break,
};
let axis_name = after[..name_end].to_string();
let rest = &after[name_end + 1..];
let comma_pos = match rest.find(',') {
Some(p) => p,
None => break,
};
let after_comma = rest[comma_pos + 1..].trim_start();
let dir_end = after_comma.find([']', ',']).unwrap_or(after_comma.len());
let direction_str = after_comma[..dir_end].trim();
let direction = AxisDirection::from_keyword(direction_str);
axes.push(AxisInfo {
name: axis_name,
direction,
});
search_from = abs_idx + "AXIS[\"".len() + name_end + 1;
}
axes
}
pub fn extract_ellipsoid_name(wkt: &str) -> Option<String> {
for keyword in &["ELLIPSOID[\"", "SPHEROID[\""] {
if let Some(idx) = wkt.find(keyword) {
let after = &wkt[idx + keyword.len()..];
let end = after.find('"')?;
return Some(after[..end].to_string());
}
}
None
}
#[cfg(feature = "std")]
pub fn parse_crs(
wkt: &str,
) -> core::result::Result<crate::crs_registry::CrsDefinition, WktError> {
use crate::crs_registry::{AreaOfUse, CrsDefinition, CrsType, CrsUnit};
if wkt.trim().is_empty() {
return Err(WktError::new("WKT string is empty", Some(0)));
}
let name = match Self::extract_name(wkt) {
Some(n) => n,
None => return Err(WktError::new("Could not extract CRS name from WKT", None)),
};
let epsg_code = Self::extract_epsg(wkt);
let upper = wkt.trim_start().to_uppercase();
let crs_type = if upper.starts_with("PROJCRS") || upper.starts_with("PROJCS") {
CrsType::Projected
} else if upper.starts_with("GEOGCRS") || upper.starts_with("GEOGCS") {
CrsType::Geographic2D
} else if upper.starts_with("GEOCCS") || upper.starts_with("GEODCRS") {
CrsType::Geocentric
} else if upper.starts_with("VERT_CS") || upper.starts_with("VERTCRS") {
CrsType::Vertical
} else if upper.starts_with("COMPD_CS") || upper.starts_with("COMPOUNDCRS") {
CrsType::Compound
} else {
CrsType::Geographic2D };
let datum = extract_datum_name(wkt).unwrap_or_default();
let unit = match Self::extract_unit(wkt) {
Some((_, factor)) if (factor - 1.0).abs() < f64::EPSILON => CrsUnit::Metre,
Some((ref name_str, _)) if name_str.to_lowercase().contains("degree") => {
CrsUnit::Degree
}
Some((ref name_str, _)) if name_str.to_lowercase().contains("foot") => {
CrsUnit::FootIntl
}
_ => match crs_type {
CrsType::Projected => CrsUnit::Metre,
_ => CrsUnit::Degree,
},
};
Ok(CrsDefinition {
epsg_code,
name: name.clone(),
crs_type,
datum,
unit,
proj_string: None,
wkt_name: Some(name),
area_of_use: None::<AreaOfUse>,
deprecated: false,
})
}
}
fn extract_datum_name(wkt: &str) -> Option<String> {
let idx = wkt.find("DATUM[\"").or_else(|| wkt.find("DATUM [\""))?;
let after = &wkt[idx..];
let bracket_pos = after.find('[')?;
let after_bracket = &after[bracket_pos + 1..];
let quote_start = after_bracket.find('"')?;
let after_quote = &after_bracket[quote_start + 1..];
let quote_end = after_quote.find('"')?;
Some(after_quote[..quote_end].to_string())
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_geogcs() {
let wkt = r#"GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]]]"#;
let result = parse_wkt(wkt);
assert!(result.is_ok());
let node = result.expect("should parse");
assert_eq!(node.node_type, "GEOGCS");
assert_eq!(node.value, Some("WGS 84".to_string()));
assert!(node.find_child("DATUM").is_some());
}
#[test]
fn test_parse_projcs() {
let wkt = r#"PROJCS["WGS 84 / UTM zone 33N",GEOGCS["WGS 84",DATUM["WGS_1984"]]]"#;
let result = parse_wkt(wkt);
assert!(result.is_ok());
let node = result.expect("should parse");
assert_eq!(node.node_type, "PROJCS");
assert_eq!(node.value, Some("WGS 84 / UTM zone 33N".to_string()));
assert!(node.find_child("GEOGCS").is_some());
}
#[test]
fn test_parse_with_parameters() {
let wkt = r#"SPHEROID["WGS 84",6378137,298.257223563]"#;
let result = parse_wkt(wkt);
assert!(result.is_ok());
let node = result.expect("should parse");
assert_eq!(node.node_type, "SPHEROID");
assert_eq!(node.value, Some("WGS 84".to_string()));
}
#[test]
fn test_parse_nested() {
let wkt = r#"DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]]"#;
let result = parse_wkt(wkt);
assert!(result.is_ok());
let node = result.expect("should parse");
assert_eq!(node.node_type, "DATUM");
assert_eq!(node.value, Some("WGS_1984".to_string()));
let spheroid = node.find_child("SPHEROID");
assert!(spheroid.is_some());
let spheroid = spheroid.expect("should have spheroid");
assert_eq!(spheroid.value, Some("WGS 84".to_string()));
}
#[test]
fn test_parse_invalid_wkt() {
let result = parse_wkt(r#"GEOGCS["WGS 84""#);
assert!(result.is_err());
let result = parse_wkt(r#"GEOGCS"WGS 84"]"#);
assert!(result.is_err());
let result = parse_wkt("");
assert!(result.is_err());
}
#[test]
fn test_find_child() {
let wkt = r#"GEOGCS["WGS 84",DATUM["WGS_1984"],PRIMEM["Greenwich",0]]"#;
let node = parse_wkt(wkt).expect("should parse");
assert!(node.find_child("DATUM").is_some());
assert!(node.find_child("PRIMEM").is_some());
assert!(node.find_child("NONEXISTENT").is_none());
}
#[test]
fn test_find_children() {
let wkt = r#"COMPD_CS["name",GEOGCS["WGS 84"],VERT_CS["height"]]"#;
let node = parse_wkt(wkt).expect("should parse");
let geogcs = node.find_children("GEOGCS");
assert_eq!(geogcs.len(), 1);
let vert_cs = node.find_children("VERT_CS");
assert_eq!(vert_cs.len(), 1);
}
#[test]
fn test_node_to_string() {
let wkt = r#"SPHEROID["WGS 84",6378137,298.257223563]"#;
let node = parse_wkt(wkt).expect("should parse");
let result = node.to_string_repr();
assert!(result.contains("SPHEROID"));
assert!(result.contains("WGS 84"));
}
#[test]
fn test_parse_wkt2_geogcrs() {
let wkt = r#"GEOGCRS["WGS 84",DATUM["World Geodetic System 1984",ELLIPSOID["WGS 84",6378137,298.257223563]],ID["EPSG",4326]]"#;
let node = parse_wkt(wkt).expect("should parse WKT2 GEOGCRS");
assert_eq!(node.node_type, "GEOGCRS");
assert_eq!(node.value, Some("WGS 84".to_string()));
assert!(node.find_child("DATUM").is_some());
assert!(node.find_child("ID").is_some());
let datum = node.find_child("DATUM").expect("should have DATUM");
let ellipsoid = datum.find_child("ELLIPSOID");
assert!(ellipsoid.is_some());
let ellipsoid = ellipsoid.expect("should have ELLIPSOID");
assert_eq!(ellipsoid.value, Some("WGS 84".to_string()));
}
#[test]
fn test_parse_wkt2_projcrs() {
let wkt = r#"PROJCRS["WGS 84 / UTM zone 33N",BASEGEOGCRS["WGS 84",DATUM["World Geodetic System 1984",ELLIPSOID["WGS 84",6378137,298.257223563]]],ID["EPSG",32633]]"#;
let node = parse_wkt(wkt).expect("should parse WKT2 PROJCRS");
assert_eq!(node.node_type, "PROJCRS");
assert_eq!(node.value, Some("WGS 84 / UTM zone 33N".to_string()));
assert!(node.find_child("BASEGEOGCRS").is_some());
}
#[test]
fn test_ellipsoid_keyword_parsed() {
let wkt = r#"ELLIPSOID["WGS 84",6378137,298.257223563]"#;
let node = parse_wkt(wkt).expect("should parse ELLIPSOID node");
assert_eq!(node.node_type, "ELLIPSOID");
assert_eq!(node.value, Some("WGS 84".to_string()));
}
#[test]
fn test_find_child_any_spheroid_or_ellipsoid() {
let wkt1 = r#"DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]]"#;
let node1 = parse_wkt(wkt1).expect("parse wkt1");
let found1 = node1.find_child_any(&["SPHEROID", "ELLIPSOID"]);
assert!(found1.is_some());
assert_eq!(found1.expect("found").node_type, "SPHEROID");
let wkt2 =
r#"DATUM["World Geodetic System 1984",ELLIPSOID["WGS 84",6378137,298.257223563]]"#;
let node2 = parse_wkt(wkt2).expect("parse wkt2");
let found2 = node2.find_child_any(&["SPHEROID", "ELLIPSOID"]);
assert!(found2.is_some());
assert_eq!(found2.expect("found").node_type, "ELLIPSOID");
}
#[test]
fn test_extract_ellipsoid_name_wkt1() {
let wkt = r#"GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]]]"#;
let name = WktParser::extract_ellipsoid_name(wkt);
assert_eq!(name, Some("WGS 84".to_string()));
}
#[test]
fn test_extract_ellipsoid_name_wkt2() {
let wkt =
r#"GEOGCRS["WGS 84",DATUM["WGS 1984",ELLIPSOID["WGS 84",6378137,298.257223563]]]"#;
let name = WktParser::extract_ellipsoid_name(wkt);
assert_eq!(name, Some("WGS 84".to_string()));
}
#[test]
fn test_extract_axes() {
let wkt = r#"GEOGCRS["WGS 84",AXIS["Latitude",NORTH],AXIS["Longitude",EAST]]"#;
let axes = WktParser::extract_axes(wkt);
assert_eq!(axes.len(), 2);
assert_eq!(axes[0].name, "Latitude");
assert_eq!(axes[0].direction, AxisDirection::North);
assert_eq!(axes[1].name, "Longitude");
assert_eq!(axes[1].direction, AxisDirection::East);
}
#[test]
fn test_extract_axes_projected() {
let wkt = r#"PROJCRS["UTM 33N",AXIS["Easting",EAST],AXIS["Northing",NORTH]]"#;
let axes = WktParser::extract_axes(wkt);
assert_eq!(axes.len(), 2);
assert_eq!(axes[0].name, "Easting");
assert_eq!(axes[0].direction, AxisDirection::East);
assert_eq!(axes[1].name, "Northing");
assert_eq!(axes[1].direction, AxisDirection::North);
}
#[test]
fn test_extract_axes_with_up() {
let wkt = r#"VERTCRS["Height",AXIS["Height",UP]]"#;
let axes = WktParser::extract_axes(wkt);
assert_eq!(axes.len(), 1);
assert_eq!(axes[0].name, "Height");
assert_eq!(axes[0].direction, AxisDirection::Up);
}
#[test]
fn test_extract_axes_empty() {
let wkt = r#"GEOGCS["WGS 84",DATUM["WGS_1984"]]"#;
let axes = WktParser::extract_axes(wkt);
assert!(axes.is_empty());
}
#[test]
fn test_detect_version_wkt2() {
let wkt = r#"GEOGCRS["WGS 84",DATUM["WGS 1984"]]"#;
assert_eq!(WktParser::detect_version(wkt), WktVersion::Wkt2);
let wkt2 = r#"PROJCRS["UTM",BASEGEOGCRS["WGS 84"]]"#;
assert_eq!(WktParser::detect_version(wkt2), WktVersion::Wkt2);
}
#[test]
fn test_extract_epsg_wkt2() {
let wkt = r#"GEOGCRS["WGS 84",DATUM["WGS 1984"],ID["EPSG",4326]]"#;
let epsg = WktParser::extract_epsg(wkt);
assert_eq!(epsg, Some(4326));
}
}