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