1use crate::value_ops::{to_value_number, value_to_f64};
7use nodedb_types::Value;
8
9pub 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 "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 "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 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 "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
308fn 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
318fn 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 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}