1use oxiproj_projections::ProjParamLookup;
6use oxiproj_transformations::TransParamLookup;
7
8#[derive(Debug, Clone)]
13pub struct ParamList {
14 pub entries: Vec<(String, Option<String>)>,
16}
17
18fn shrink(s: &str) -> String {
34 let mut out = String::with_capacity(s.len());
35 let mut in_string = false;
36 let mut pending_ws = false;
38 let mut last_emitted: Option<char> = None;
40 for ch in s.chars() {
41 if in_string {
42 out.push(ch);
43 if ch == '"' {
44 in_string = false;
45 }
46 last_emitted = Some(ch);
47 continue;
48 }
49 if ch == '"' {
50 if pending_ws && last_emitted.is_some() {
51 out.push(' ');
52 }
53 pending_ws = false;
54 out.push(ch);
55 last_emitted = Some(ch);
56 in_string = true;
57 continue;
58 }
59 if ch.is_whitespace() {
60 pending_ws = true;
61 continue;
62 }
63 if ch == '=' || ch == ',' {
64 pending_ws = false;
66 out.push(ch);
67 last_emitted = Some(ch);
68 continue;
69 }
70 if pending_ws {
73 pending_ws = false;
74 if !matches!(last_emitted, None | Some('=') | Some(',')) {
75 out.push(' ');
76 }
77 }
78 out.push(ch);
79 last_emitted = Some(ch);
80 }
81 out
82}
83
84pub fn parse(s: &str) -> ParamList {
94 let normalized = shrink(s);
95 let mut entries: Vec<(String, Option<String>)> = Vec::new();
96 for raw in normalized.split_ascii_whitespace() {
97 let tok = raw.strip_prefix('+').unwrap_or(raw);
98 if tok.is_empty() {
99 continue;
100 }
101 match tok.split_once('=') {
102 Some((key, value)) => entries.push((key.to_string(), Some(value.to_string()))),
103 None => entries.push((tok.to_string(), None)),
104 }
105 }
106 ParamList { entries }
107}
108
109impl ParamList {
110 pub fn exists(&self, key: &str) -> bool {
112 self.entries.iter().any(|(k, _)| k == key)
113 }
114
115 pub fn get_str(&self, key: &str) -> Option<&str> {
119 for (k, v) in &self.entries {
120 if k == key {
121 return match v {
122 Some(val) => Some(val.as_str()),
123 None => Some(""),
124 };
125 }
126 }
127 None
128 }
129
130 pub fn get_f64(&self, key: &str) -> Option<f64> {
134 for (k, v) in &self.entries {
135 if k == key {
136 let val = v.as_ref()?;
137 if let Ok(parsed) = val.trim().parse::<f64>() {
138 return Some(parsed);
139 }
140 return oxiproj_core::parse_leading_f64(val).map(|(x, _)| x);
141 }
142 }
143 None
144 }
145
146 pub fn get_dms(&self, key: &str) -> Option<f64> {
148 for (k, v) in &self.entries {
149 if k == key {
150 let val = v.as_ref()?;
151 return oxiproj_core::dmstor(val).ok();
152 }
153 }
154 None
155 }
156
157 pub fn get_int(&self, key: &str) -> Option<i64> {
159 for (k, v) in &self.entries {
160 if k == key {
161 let val = v.as_ref()?;
162 return val.trim().parse::<i64>().ok();
163 }
164 }
165 None
166 }
167
168 pub fn get_bool(&self, key: &str) -> bool {
174 for (k, v) in &self.entries {
175 if k == key {
176 return match v {
177 None => true,
178 Some(val) => !matches!(val.as_str(), "f" | "F" | "false"),
179 };
180 }
181 }
182 false
183 }
184}
185
186pub struct ParamView<'a>(pub &'a ParamList);
188
189impl ProjParamLookup for ParamView<'_> {
190 fn get_dms(&self, key: &str) -> Option<f64> {
191 self.0.get_dms(key)
192 }
193 fn get_f64(&self, key: &str) -> Option<f64> {
194 self.0.get_f64(key)
195 }
196 fn get_int(&self, key: &str) -> Option<i64> {
197 self.0.get_int(key)
198 }
199 fn get_str(&self, key: &str) -> Option<&str> {
200 self.0.get_str(key)
201 }
202 fn get_bool(&self, key: &str) -> bool {
203 self.0.get_bool(key)
204 }
205 fn exists(&self, key: &str) -> bool {
206 self.0.exists(key)
207 }
208}
209
210impl TransParamLookup for ParamView<'_> {
211 fn get_dms(&self, key: &str) -> Option<f64> {
212 self.0.get_dms(key)
213 }
214 fn get_f64(&self, key: &str) -> Option<f64> {
215 self.0.get_f64(key)
216 }
217 fn get_int(&self, key: &str) -> Option<i64> {
218 self.0.get_int(key)
219 }
220 fn get_str(&self, key: &str) -> Option<&str> {
221 self.0.get_str(key)
222 }
223 fn get_bool(&self, key: &str) -> bool {
224 self.0.get_bool(key)
225 }
226 fn exists(&self, key: &str) -> bool {
227 self.0.exists(key)
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use oxiproj_core::DEG_TO_RAD;
235
236 #[test]
237 fn parses_basic_merc() {
238 let pl = parse("+proj=merc +lat_ts=45 +ellps=WGS84");
239 assert_eq!(pl.entries.len(), 3);
240 assert_eq!(pl.get_str("proj"), Some("merc"));
241 let lat_ts = pl.get_dms("lat_ts").unwrap();
242 assert!((lat_ts - 45.0 * DEG_TO_RAD).abs() < 1e-12);
243 }
244
245 #[test]
246 fn bare_flag_is_true() {
247 let pl = parse("+south");
248 assert_eq!(pl.entries.len(), 1);
249 assert_eq!(pl.entries[0], ("south".to_string(), None));
250 assert!(pl.get_bool("south"));
251 assert!(!pl.get_bool("missing"));
252 }
253
254 #[test]
255 fn parses_int() {
256 let pl = parse("+zone=32");
257 assert_eq!(pl.get_int("zone"), Some(32));
258 }
259
260 #[test]
261 fn parses_pipeline_tokens() {
262 let pl = parse("+proj=pipeline +step +proj=utm +zone=32 +step +inv");
263 let e = &pl.entries;
264 assert!(e.contains(&("proj".to_string(), Some("pipeline".to_string()))));
265 assert!(e.contains(&("step".to_string(), None)));
266 assert!(e.contains(&("proj".to_string(), Some("utm".to_string()))));
267 assert!(e.contains(&("zone".to_string(), Some("32".to_string()))));
268 assert!(e.contains(&("inv".to_string(), None)));
269 assert_eq!(
271 e.iter().filter(|(k, v)| k == "step" && v.is_none()).count(),
272 2
273 );
274 }
275
276 #[test]
277 fn lon_0_dms() {
278 let pl = parse("+lon_0=9");
279 let v = pl.get_dms("lon_0").unwrap();
280 assert!((v - 9.0 * DEG_TO_RAD).abs() < 1e-12);
281 assert!((v - 0.157079632679).abs() < 1e-9);
282 }
283
284 #[test]
285 fn shrink_collapses_spaces_around_equals() {
286 let pl = parse("+proj=helmert x = 0.01270 dx =-0.0029 s = 0.00195");
289 assert_eq!(pl.get_str("proj"), Some("helmert"));
290 assert_eq!(pl.get_str("x"), Some("0.01270"));
291 assert_eq!(pl.get_str("dx"), Some("-0.0029"));
292 assert_eq!(pl.get_str("s"), Some("0.00195"));
293 }
294
295 #[test]
296 fn shrink_makes_commas_greedy() {
297 let pl = parse("+proj=helmert towgs84 = 1, 2, 3, 4, 5, 6, 7");
300 assert_eq!(pl.get_str("towgs84"), Some("1,2,3,4,5,6,7"));
301 }
302
303 #[test]
304 fn shrink_preserves_scientific_notation_plus() {
305 let pl = parse("+proj=merc +foo=1.23e+08");
307 assert_eq!(pl.get_str("foo"), Some("1.23e+08"));
308 }
309
310 #[test]
311 fn false_values_are_false() {
312 let pl = parse("+over=false +foo=f +bar=t");
313 assert!(!pl.get_bool("over"));
314 assert!(!pl.get_bool("foo"));
315 assert!(pl.get_bool("bar"));
316 }
317}