1use nodedb_types::geometry::Geometry;
12
13pub fn geometry_to_wkt(geom: &Geometry) -> String {
15 match geom {
16 Geometry::Point { coordinates } => {
17 format!("POINT({} {})", coordinates[0], coordinates[1])
18 }
19 Geometry::LineString { coordinates } => {
20 format!("LINESTRING({})", coords_to_wkt(coordinates))
21 }
22 Geometry::Polygon { coordinates } => {
23 let rings: Vec<String> = coordinates
24 .iter()
25 .map(|ring| format!("({})", coords_to_wkt(ring)))
26 .collect();
27 format!("POLYGON({})", rings.join(", "))
28 }
29 Geometry::MultiPoint { coordinates } => {
30 let pts: Vec<String> = coordinates
31 .iter()
32 .map(|c| format!("({} {})", c[0], c[1]))
33 .collect();
34 format!("MULTIPOINT({})", pts.join(", "))
35 }
36 Geometry::MultiLineString { coordinates } => {
37 let lines: Vec<String> = coordinates
38 .iter()
39 .map(|ls| format!("({})", coords_to_wkt(ls)))
40 .collect();
41 format!("MULTILINESTRING({})", lines.join(", "))
42 }
43 Geometry::MultiPolygon { coordinates } => {
44 let polys: Vec<String> = coordinates
45 .iter()
46 .map(|poly| {
47 let rings: Vec<String> = poly
48 .iter()
49 .map(|ring| format!("({})", coords_to_wkt(ring)))
50 .collect();
51 format!("({})", rings.join(", "))
52 })
53 .collect();
54 format!("MULTIPOLYGON({})", polys.join(", "))
55 }
56 Geometry::GeometryCollection { geometries } => {
57 let geoms: Vec<String> = geometries.iter().map(geometry_to_wkt).collect();
58 format!("GEOMETRYCOLLECTION({})", geoms.join(", "))
59 }
60 }
61}
62
63pub fn geometry_from_wkt(input: &str) -> Option<Geometry> {
67 let s = input.trim();
68 if let Some(rest) = strip_prefix_ci(s, "GEOMETRYCOLLECTION") {
69 parse_geometry_collection(rest.trim())
70 } else if let Some(rest) = strip_prefix_ci(s, "MULTIPOLYGON") {
71 parse_multipolygon(rest.trim())
72 } else if let Some(rest) = strip_prefix_ci(s, "MULTILINESTRING") {
73 parse_multilinestring(rest.trim())
74 } else if let Some(rest) = strip_prefix_ci(s, "MULTIPOINT") {
75 parse_multipoint(rest.trim())
76 } else if let Some(rest) = strip_prefix_ci(s, "POLYGON") {
77 parse_polygon(rest.trim())
78 } else if let Some(rest) = strip_prefix_ci(s, "LINESTRING") {
79 parse_linestring(rest.trim())
80 } else if let Some(rest) = strip_prefix_ci(s, "POINT") {
81 parse_point(rest.trim())
82 } else {
83 None
84 }
85}
86
87fn coords_to_wkt(coords: &[[f64; 2]]) -> String {
90 coords
91 .iter()
92 .map(|c| format!("{} {}", c[0], c[1]))
93 .collect::<Vec<_>>()
94 .join(", ")
95}
96
97fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
101 if s.len() >= prefix.len() && s[..prefix.len()].eq_ignore_ascii_case(prefix) {
102 Some(&s[prefix.len()..])
103 } else {
104 None
105 }
106}
107
108fn strip_parens(s: &str) -> Option<&str> {
110 let s = s.trim();
111 if s.starts_with('(') && s.ends_with(')') {
112 Some(&s[1..s.len() - 1])
113 } else {
114 None
115 }
116}
117
118fn parse_coord(s: &str) -> Option<[f64; 2]> {
120 let s = s.trim();
121 let mut parts = s.split_whitespace();
122 let lng: f64 = parts.next()?.parse().ok()?;
123 let lat: f64 = parts.next()?.parse().ok()?;
124 Some([lng, lat])
125}
126
127fn parse_coord_list(s: &str) -> Option<Vec<[f64; 2]>> {
129 s.split(',').map(parse_coord).collect()
130}
131
132fn parse_point(s: &str) -> Option<Geometry> {
133 let inner = strip_parens(s)?;
134 let coord = parse_coord(inner)?;
135 Some(Geometry::Point { coordinates: coord })
136}
137
138fn parse_linestring(s: &str) -> Option<Geometry> {
139 let inner = strip_parens(s)?;
140 let coords = parse_coord_list(inner)?;
141 Some(Geometry::LineString {
142 coordinates: coords,
143 })
144}
145
146fn parse_polygon(s: &str) -> Option<Geometry> {
147 let inner = strip_parens(s)?;
148 let rings = split_top_level_parens(inner)?;
149 let ring_coords: Option<Vec<Vec<[f64; 2]>>> = rings
150 .iter()
151 .map(|r| parse_coord_list(strip_parens(r.trim())?))
152 .collect();
153 Some(Geometry::Polygon {
154 coordinates: ring_coords?,
155 })
156}
157
158fn parse_multipoint(s: &str) -> Option<Geometry> {
159 let inner = strip_parens(s)?;
160 let coords = if inner.contains('(') {
162 let parts = split_top_level_parens(inner)?;
163 parts
164 .iter()
165 .map(|p| parse_coord(strip_parens(p.trim())?))
166 .collect::<Option<Vec<_>>>()?
167 } else {
168 parse_coord_list(inner)?
169 };
170 Some(Geometry::MultiPoint {
171 coordinates: coords,
172 })
173}
174
175fn parse_multilinestring(s: &str) -> Option<Geometry> {
176 let inner = strip_parens(s)?;
177 let parts = split_top_level_parens(inner)?;
178 let lines: Option<Vec<Vec<[f64; 2]>>> = parts
179 .iter()
180 .map(|p| parse_coord_list(strip_parens(p.trim())?))
181 .collect();
182 Some(Geometry::MultiLineString {
183 coordinates: lines?,
184 })
185}
186
187fn parse_multipolygon(s: &str) -> Option<Geometry> {
188 let inner = strip_parens(s)?;
189 let poly_parts = split_top_level_parens(inner)?;
190 let polys: Option<Vec<Vec<Vec<[f64; 2]>>>> = poly_parts
191 .iter()
192 .map(|p| {
193 let rings_str = strip_parens(p.trim())?;
194 let ring_parts = split_top_level_parens(rings_str)?;
195 ring_parts
196 .iter()
197 .map(|r| parse_coord_list(strip_parens(r.trim())?))
198 .collect::<Option<Vec<_>>>()
199 })
200 .collect();
201 Some(Geometry::MultiPolygon {
202 coordinates: polys?,
203 })
204}
205
206fn parse_geometry_collection(s: &str) -> Option<Geometry> {
207 let inner = strip_parens(s)?;
208 let parts = split_top_level_items(inner);
210 let geoms: Option<Vec<Geometry>> = parts.iter().map(|p| geometry_from_wkt(p.trim())).collect();
211 Some(Geometry::GeometryCollection { geometries: geoms? })
212}
213
214fn split_top_level_parens(s: &str) -> Option<Vec<String>> {
217 let mut parts = Vec::new();
218 let mut depth = 0;
219 let mut start = 0;
220
221 for (i, ch) in s.char_indices() {
222 match ch {
223 '(' => depth += 1,
224 ')' => depth -= 1,
225 ',' if depth == 0 => {
226 parts.push(s[start..i].to_string());
227 start = i + 1;
228 }
229 _ => {}
230 }
231 }
232 if start < s.len() {
233 parts.push(s[start..].to_string());
234 }
235 if parts.is_empty() { None } else { Some(parts) }
236}
237
238fn split_top_level_items(s: &str) -> Vec<String> {
240 let mut parts = Vec::new();
241 let mut depth = 0;
242 let mut start = 0;
243
244 for (i, ch) in s.char_indices() {
245 match ch {
246 '(' => depth += 1,
247 ')' => depth -= 1,
248 ',' if depth == 0 => {
249 parts.push(s[start..i].to_string());
250 start = i + 1;
251 }
252 _ => {}
253 }
254 }
255 if start < s.len() {
256 parts.push(s[start..].to_string());
257 }
258 parts
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn point_roundtrip() {
267 let geom = Geometry::point(-73.9857, 40.7484);
268 let wkt = geometry_to_wkt(&geom);
269 assert!(wkt.starts_with("POINT("));
270 let parsed = geometry_from_wkt(&wkt).unwrap();
271 assert_eq!(geom, parsed);
272 }
273
274 #[test]
275 fn linestring_roundtrip() {
276 let geom = Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0], [2.0, 0.0]]);
277 let wkt = geometry_to_wkt(&geom);
278 let parsed = geometry_from_wkt(&wkt).unwrap();
279 assert_eq!(geom, parsed);
280 }
281
282 #[test]
283 fn polygon_roundtrip() {
284 let geom = Geometry::polygon(vec![vec![
285 [0.0, 0.0],
286 [10.0, 0.0],
287 [10.0, 10.0],
288 [0.0, 10.0],
289 [0.0, 0.0],
290 ]]);
291 let wkt = geometry_to_wkt(&geom);
292 assert!(wkt.starts_with("POLYGON("));
293 let parsed = geometry_from_wkt(&wkt).unwrap();
294 assert_eq!(geom, parsed);
295 }
296
297 #[test]
298 fn polygon_with_hole_roundtrip() {
299 let geom = Geometry::polygon(vec![
300 vec![
301 [0.0, 0.0],
302 [10.0, 0.0],
303 [10.0, 10.0],
304 [0.0, 10.0],
305 [0.0, 0.0],
306 ],
307 vec![[2.0, 2.0], [8.0, 2.0], [8.0, 8.0], [2.0, 8.0], [2.0, 2.0]],
308 ]);
309 let wkt = geometry_to_wkt(&geom);
310 let parsed = geometry_from_wkt(&wkt).unwrap();
311 assert_eq!(geom, parsed);
312 }
313
314 #[test]
315 fn multipoint_roundtrip() {
316 let geom = Geometry::MultiPoint {
317 coordinates: vec![[1.0, 2.0], [3.0, 4.0]],
318 };
319 let wkt = geometry_to_wkt(&geom);
320 let parsed = geometry_from_wkt(&wkt).unwrap();
321 assert_eq!(geom, parsed);
322 }
323
324 #[test]
325 fn multilinestring_roundtrip() {
326 let geom = Geometry::MultiLineString {
327 coordinates: vec![vec![[0.0, 0.0], [1.0, 1.0]], vec![[2.0, 2.0], [3.0, 3.0]]],
328 };
329 let wkt = geometry_to_wkt(&geom);
330 let parsed = geometry_from_wkt(&wkt).unwrap();
331 assert_eq!(geom, parsed);
332 }
333
334 #[test]
335 fn multipolygon_roundtrip() {
336 let geom = Geometry::MultiPolygon {
337 coordinates: vec![
338 vec![vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 0.0]]],
339 vec![vec![[5.0, 5.0], [6.0, 5.0], [6.0, 6.0], [5.0, 5.0]]],
340 ],
341 };
342 let wkt = geometry_to_wkt(&geom);
343 let parsed = geometry_from_wkt(&wkt).unwrap();
344 assert_eq!(geom, parsed);
345 }
346
347 #[test]
348 fn geometry_collection_roundtrip() {
349 let geom = Geometry::GeometryCollection {
350 geometries: vec![
351 Geometry::point(1.0, 2.0),
352 Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]]),
353 ],
354 };
355 let wkt = geometry_to_wkt(&geom);
356 assert!(wkt.starts_with("GEOMETRYCOLLECTION("));
357 let parsed = geometry_from_wkt(&wkt).unwrap();
358 assert_eq!(geom, parsed);
359 }
360
361 #[test]
362 fn case_insensitive_parse() {
363 let parsed = geometry_from_wkt("point(5 10)").unwrap();
364 assert_eq!(parsed, Geometry::point(5.0, 10.0));
365 }
366
367 #[test]
368 fn invalid_wkt_returns_none() {
369 assert!(geometry_from_wkt("").is_none());
370 assert!(geometry_from_wkt("GARBAGE(1 2)").is_none());
371 assert!(geometry_from_wkt("POINT(abc def)").is_none());
372 }
373}