Skip to main content

nodedb_query/
geo_functions.rs

1//! Geospatial SQL function evaluation.
2//!
3//! All ST_* predicates, geometry operations, and geo_* utility functions.
4//! Called from `functions::eval_function` for any geo-prefixed or ST_-prefixed name.
5
6use crate::value_ops::{to_value_number, value_to_f64};
7use nodedb_types::Value;
8
9/// Try to evaluate a geo/spatial function. Returns `Some(result)` if the
10/// function name matched, `None` if unrecognized (caller falls through).
11pub fn eval_geo_function(name: &str, args: &[Value]) -> Option<Value> {
12    let result = match name {
13        "geo_distance" | "haversine_distance" => {
14            let lng1 = num_arg(args, 0).unwrap_or(0.0);
15            let lat1 = num_arg(args, 1).unwrap_or(0.0);
16            let lng2 = num_arg(args, 2).unwrap_or(0.0);
17            let lat2 = num_arg(args, 3).unwrap_or(0.0);
18            to_value_number(nodedb_types::geometry::haversine_distance(
19                lng1, lat1, lng2, lat2,
20            ))
21        }
22        "geo_bearing" | "haversine_bearing" => {
23            let lng1 = num_arg(args, 0).unwrap_or(0.0);
24            let lat1 = num_arg(args, 1).unwrap_or(0.0);
25            let lng2 = num_arg(args, 2).unwrap_or(0.0);
26            let lat2 = num_arg(args, 3).unwrap_or(0.0);
27            to_value_number(nodedb_types::geometry::haversine_bearing(
28                lng1, lat1, lng2, lat2,
29            ))
30        }
31        "geo_point" => {
32            let lng = num_arg(args, 0).unwrap_or(0.0);
33            let lat = num_arg(args, 1).unwrap_or(0.0);
34            Value::Geometry(nodedb_types::geometry::Geometry::point(lng, lat))
35        }
36        "geo_geohash" => {
37            let lng = num_arg(args, 0).unwrap_or(0.0);
38            let lat = num_arg(args, 1).unwrap_or(0.0);
39            let precision = num_arg(args, 2).unwrap_or(6.0) as u8;
40            Value::String(nodedb_spatial::geohash_encode(lng, lat, precision))
41        }
42        "geo_geohash_decode" => {
43            let hash = str_arg(args, 0).unwrap_or_default();
44            match nodedb_spatial::geohash_decode(&hash) {
45                Some(bb) => {
46                    let mut map = std::collections::HashMap::new();
47                    map.insert("min_lng".to_string(), Value::Float(bb.min_lng));
48                    map.insert("min_lat".to_string(), Value::Float(bb.min_lat));
49                    map.insert("max_lng".to_string(), Value::Float(bb.max_lng));
50                    map.insert("max_lat".to_string(), Value::Float(bb.max_lat));
51                    Value::Object(map)
52                }
53                None => Value::Null,
54            }
55        }
56        "geo_geohash_neighbors" => {
57            let hash = str_arg(args, 0).unwrap_or_default();
58            let neighbors = nodedb_spatial::geohash_neighbors(&hash);
59            let arr: Vec<Value> = neighbors
60                .into_iter()
61                .map(|(dir, h)| {
62                    let mut map = std::collections::HashMap::new();
63                    map.insert("direction".to_string(), Value::String(format!("{dir:?}")));
64                    map.insert("hash".to_string(), Value::String(h));
65                    Value::Object(map)
66                })
67                .collect();
68            Value::Array(arr)
69        }
70
71        // ── Spatial predicates (ST_*) ──
72        "st_contains" => geo_predicate_2(args, nodedb_spatial::st_contains),
73        "st_intersects" => geo_predicate_2(args, nodedb_spatial::st_intersects),
74        "st_within" => geo_predicate_2(args, nodedb_spatial::st_within),
75        "st_disjoint" => geo_predicate_2(args, nodedb_spatial::st_disjoint),
76        "st_dwithin" => {
77            let (Some(a), Some(b)) = (geom_arg(args, 0), geom_arg(args, 1)) else {
78                return Some(Value::Null);
79            };
80            let dist = num_arg(args, 2).unwrap_or(0.0);
81            Value::Bool(nodedb_spatial::st_dwithin(&a, &b, dist))
82        }
83        "st_distance" => {
84            let (Some(a), Some(b)) = (geom_arg(args, 0), geom_arg(args, 1)) else {
85                return Some(Value::Null);
86            };
87            to_value_number(nodedb_spatial::st_distance(&a, &b))
88        }
89        "st_buffer" => {
90            let Some(geom) = geom_arg(args, 0) else {
91                return Some(Value::Null);
92            };
93            let dist = num_arg(args, 1).unwrap_or(0.0);
94            let segs = num_arg(args, 2).unwrap_or(32.0) as usize;
95            Value::Geometry(nodedb_spatial::st_buffer(&geom, dist, segs))
96        }
97        "st_envelope" => {
98            let Some(geom) = geom_arg(args, 0) else {
99                return Some(Value::Null);
100            };
101            Value::Geometry(nodedb_spatial::st_envelope(&geom))
102        }
103        "st_union" => {
104            let (Some(a), Some(b)) = (geom_arg(args, 0), geom_arg(args, 1)) else {
105                return Some(Value::Null);
106            };
107            Value::Geometry(nodedb_spatial::st_union(&a, &b))
108        }
109
110        // ── Extended geo functions ──
111        "geo_length" => {
112            let Some(geom) = geom_arg(args, 0) else {
113                return Some(Value::Null);
114            };
115            to_value_number(geo_linestring_length(&geom))
116        }
117        "geo_perimeter" => {
118            let Some(geom) = geom_arg(args, 0) else {
119                return Some(Value::Null);
120            };
121            to_value_number(geo_polygon_perimeter(&geom))
122        }
123        "geo_line" => {
124            let coords: Vec<[f64; 2]> = args
125                .iter()
126                .filter_map(|v| {
127                    let geom = v.as_geometry()?;
128                    if let nodedb_types::geometry::Geometry::Point { coordinates } = geom {
129                        Some(*coordinates)
130                    } else {
131                        None
132                    }
133                })
134                .collect();
135            if coords.len() < 2 {
136                Value::Null
137            } else {
138                Value::Geometry(nodedb_types::geometry::Geometry::line_string(coords))
139            }
140        }
141        "geo_polygon" => {
142            // Args are arrays of coordinate pairs. Convert from Value::Array.
143            let rings: Vec<Vec<[f64; 2]>> = args
144                .iter()
145                .filter_map(|v| {
146                    let arr = v.as_array()?;
147                    let coords: Vec<[f64; 2]> = arr
148                        .iter()
149                        .filter_map(|pt| {
150                            let inner = pt.as_array()?;
151                            if inner.len() >= 2 {
152                                Some([inner[0].as_f64()?, inner[1].as_f64()?])
153                            } else {
154                                None
155                            }
156                        })
157                        .collect();
158                    if coords.is_empty() {
159                        None
160                    } else {
161                        Some(coords)
162                    }
163                })
164                .collect();
165            if rings.is_empty() {
166                Value::Null
167            } else {
168                Value::Geometry(nodedb_types::geometry::Geometry::polygon(rings))
169            }
170        }
171        "geo_circle" => {
172            let lng = num_arg(args, 0).unwrap_or(0.0);
173            let lat = num_arg(args, 1).unwrap_or(0.0);
174            let radius = num_arg(args, 2).unwrap_or(0.0);
175            let segs = num_arg(args, 3).unwrap_or(32.0) as usize;
176            let circle = nodedb_spatial::st_buffer(
177                &nodedb_types::geometry::Geometry::point(lng, lat),
178                radius,
179                segs,
180            );
181            Value::Geometry(circle)
182        }
183        "geo_bbox" => {
184            let min_lng = num_arg(args, 0).unwrap_or(0.0);
185            let min_lat = num_arg(args, 1).unwrap_or(0.0);
186            let max_lng = num_arg(args, 2).unwrap_or(0.0);
187            let max_lat = num_arg(args, 3).unwrap_or(0.0);
188            Value::Geometry(nodedb_types::geometry::Geometry::polygon(vec![vec![
189                [min_lng, min_lat],
190                [max_lng, min_lat],
191                [max_lng, max_lat],
192                [min_lng, max_lat],
193                [min_lng, min_lat],
194            ]]))
195        }
196        "geo_as_geojson" => {
197            let Some(geom) = geom_arg(args, 0) else {
198                return Some(Value::Null);
199            };
200            match sonic_rs::to_string(&geom) {
201                Ok(s) => Value::String(s),
202                Err(_) => Value::Null,
203            }
204        }
205        "geo_from_geojson" => {
206            let s = str_arg(args, 0).unwrap_or_default();
207            match sonic_rs::from_str::<nodedb_types::geometry::Geometry>(&s) {
208                Ok(g) => Value::Geometry(g),
209                Err(_) => Value::Null,
210            }
211        }
212        "geo_as_wkt" => {
213            let Some(geom) = geom_arg(args, 0) else {
214                return Some(Value::Null);
215            };
216            Value::String(nodedb_spatial::geometry_to_wkt(&geom))
217        }
218        "geo_from_wkt" => {
219            let s = str_arg(args, 0).unwrap_or_default();
220            match nodedb_spatial::geometry_from_wkt(&s) {
221                Some(g) => Value::Geometry(g),
222                None => Value::Null,
223            }
224        }
225        "geo_x" => {
226            let Some(geom) = geom_arg(args, 0) else {
227                return Some(Value::Null);
228            };
229            if let nodedb_types::geometry::Geometry::Point { coordinates } = geom {
230                to_value_number(coordinates[0])
231            } else {
232                Value::Null
233            }
234        }
235        "geo_y" => {
236            let Some(geom) = geom_arg(args, 0) else {
237                return Some(Value::Null);
238            };
239            if let nodedb_types::geometry::Geometry::Point { coordinates } = geom {
240                to_value_number(coordinates[1])
241            } else {
242                Value::Null
243            }
244        }
245        "geo_num_points" => {
246            let Some(geom) = geom_arg(args, 0) else {
247                return Some(Value::Null);
248            };
249            Value::Integer(count_points(&geom) as i64)
250        }
251        "geo_type" => {
252            let Some(geom) = geom_arg(args, 0) else {
253                return Some(Value::Null);
254            };
255            Value::String(geom.geometry_type().to_string())
256        }
257        "geo_is_valid" => {
258            let Some(geom) = geom_arg(args, 0) else {
259                return Some(Value::Null);
260            };
261            Value::Bool(nodedb_spatial::is_valid(&geom))
262        }
263
264        // ── H3 hexagonal index ──
265        "geo_h3" => {
266            let lng = num_arg(args, 0).unwrap_or(0.0);
267            let lat = num_arg(args, 1).unwrap_or(0.0);
268            let resolution = num_arg(args, 2).unwrap_or(7.0) as u8;
269            match nodedb_spatial::h3::h3_encode_string(lng, lat, resolution) {
270                Some(hex) => Value::String(hex),
271                None => Value::Null,
272            }
273        }
274        "geo_h3_to_boundary" => {
275            let h3_str = str_arg(args, 0).unwrap_or_default();
276            let h3_idx = u64::from_str_radix(&h3_str, 16).unwrap_or(0);
277            if !nodedb_spatial::h3::h3_is_valid(h3_idx) {
278                return Some(Value::Null);
279            }
280            match nodedb_spatial::h3::h3_to_boundary(h3_idx) {
281                Some(geom) => Value::Geometry(geom),
282                None => Value::Null,
283            }
284        }
285        "geo_h3_resolution" => {
286            let h3_str = str_arg(args, 0).unwrap_or_default();
287            let h3_idx = u64::from_str_radix(&h3_str, 16).unwrap_or(0);
288            if !nodedb_spatial::h3::h3_is_valid(h3_idx) {
289                return Some(Value::Null);
290            }
291            match nodedb_spatial::h3::h3_resolution(h3_idx) {
292                Some(r) => Value::Integer(r as i64),
293                None => Value::Null,
294            }
295        }
296        "st_intersection" => {
297            let (Some(a), Some(b)) = (geom_arg(args, 0), geom_arg(args, 1)) else {
298                return Some(Value::Null);
299            };
300            Value::Geometry(nodedb_spatial::st_intersection(&a, &b))
301        }
302
303        _ => return None,
304    };
305    Some(result)
306}
307
308// ── Helpers ──
309
310fn str_arg(args: &[Value], idx: usize) -> Option<String> {
311    args.get(idx)?.as_str().map(|s| s.to_string())
312}
313
314fn num_arg(args: &[Value], idx: usize) -> Option<f64> {
315    args.get(idx).and_then(|v| value_to_f64(v, true))
316}
317
318/// Extract a geometry argument. Supports `Value::Geometry` directly,
319/// or falls back to JSON deserialization for `Value::Object` (GeoJSON).
320fn geom_arg(args: &[Value], idx: usize) -> Option<nodedb_types::geometry::Geometry> {
321    match args.get(idx)? {
322        Value::Geometry(g) => Some(g.clone()),
323        other => {
324            // Fallback: convert Value → JSON → Geometry for GeoJSON objects.
325            let json = serde_json::Value::from(other.clone());
326            serde_json::from_value(json).ok()
327        }
328    }
329}
330
331fn geo_predicate_2(
332    args: &[Value],
333    f: fn(&nodedb_types::geometry::Geometry, &nodedb_types::geometry::Geometry) -> bool,
334) -> Value {
335    let (Some(a), Some(b)) = (geom_arg(args, 0), geom_arg(args, 1)) else {
336        return Value::Null;
337    };
338    Value::Bool(f(&a, &b))
339}
340
341fn geo_linestring_length(geom: &nodedb_types::geometry::Geometry) -> f64 {
342    let coords = match geom {
343        nodedb_types::geometry::Geometry::LineString { coordinates } => coordinates,
344        _ => return 0.0,
345    };
346    let mut total = 0.0;
347    for i in 0..coords.len().saturating_sub(1) {
348        total += nodedb_types::geometry::haversine_distance(
349            coords[i][0],
350            coords[i][1],
351            coords[i + 1][0],
352            coords[i + 1][1],
353        );
354    }
355    total
356}
357
358fn geo_polygon_perimeter(geom: &nodedb_types::geometry::Geometry) -> f64 {
359    let rings = match geom {
360        nodedb_types::geometry::Geometry::Polygon { coordinates } => coordinates,
361        _ => return 0.0,
362    };
363    let Some(exterior) = rings.first() else {
364        return 0.0;
365    };
366    let mut total = 0.0;
367    for i in 0..exterior.len().saturating_sub(1) {
368        total += nodedb_types::geometry::haversine_distance(
369            exterior[i][0],
370            exterior[i][1],
371            exterior[i + 1][0],
372            exterior[i + 1][1],
373        );
374    }
375    total
376}
377
378fn count_points(geom: &nodedb_types::geometry::Geometry) -> usize {
379    use nodedb_types::geometry::Geometry;
380    match geom {
381        Geometry::Point { .. } => 1,
382        Geometry::LineString { coordinates } => coordinates.len(),
383        Geometry::Polygon { coordinates } => coordinates.iter().map(|r| r.len()).sum(),
384        Geometry::MultiPoint { coordinates } => coordinates.len(),
385        Geometry::MultiLineString { coordinates } => coordinates.iter().map(|ls| ls.len()).sum(),
386        Geometry::MultiPolygon { coordinates } => coordinates
387            .iter()
388            .flat_map(|poly| poly.iter())
389            .map(|ring| ring.len())
390            .sum(),
391        Geometry::GeometryCollection { geometries } => geometries.iter().map(count_points).sum(),
392    }
393}