1use std::collections::HashSet;
4
5use geohash::Coord;
6use geoutils::Location;
7use serde_json::{Value, json};
8
9use crate::functions::{Function, custom_error, number_value};
10use crate::interpreter::SearchResult;
11use crate::registry::register_if_enabled;
12use crate::{Context, Runtime, arg, defn};
13
14defn!(
19 GeoDistanceFn,
20 vec![arg!(number), arg!(number), arg!(number), arg!(number)],
21 None
22);
23
24impl Function for GeoDistanceFn {
25 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
26 self.signature.validate(args, ctx)?;
27 let lat1 = args[0].as_f64().unwrap();
28 let lon1 = args[1].as_f64().unwrap();
29 let lat2 = args[2].as_f64().unwrap();
30 let lon2 = args[3].as_f64().unwrap();
31
32 let loc1 = Location::new(lat1, lon1);
33 let loc2 = Location::new(lat2, lon2);
34
35 let distance = loc1.haversine_distance_to(&loc2);
36 Ok(number_value(distance.meters()))
37 }
38}
39
40defn!(
45 GeoDistanceKmFn,
46 vec![arg!(number), arg!(number), arg!(number), arg!(number)],
47 None
48);
49
50impl Function for GeoDistanceKmFn {
51 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
52 self.signature.validate(args, ctx)?;
53 let lat1 = args[0].as_f64().unwrap();
54 let lon1 = args[1].as_f64().unwrap();
55 let lat2 = args[2].as_f64().unwrap();
56 let lon2 = args[3].as_f64().unwrap();
57
58 let loc1 = Location::new(lat1, lon1);
59 let loc2 = Location::new(lat2, lon2);
60
61 let distance = loc1.haversine_distance_to(&loc2);
62 Ok(number_value(distance.meters() / 1000.0))
63 }
64}
65
66defn!(
71 GeoDistanceMilesFn,
72 vec![arg!(number), arg!(number), arg!(number), arg!(number)],
73 None
74);
75
76impl Function for GeoDistanceMilesFn {
77 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
78 self.signature.validate(args, ctx)?;
79 let lat1 = args[0].as_f64().unwrap();
80 let lon1 = args[1].as_f64().unwrap();
81 let lat2 = args[2].as_f64().unwrap();
82 let lon2 = args[3].as_f64().unwrap();
83
84 let loc1 = Location::new(lat1, lon1);
85 let loc2 = Location::new(lat2, lon2);
86
87 const METERS_TO_MILES: f64 = 0.000621371;
89
90 let distance = loc1.haversine_distance_to(&loc2);
91 Ok(number_value(distance.meters() * METERS_TO_MILES))
92 }
93}
94
95defn!(
100 GeoBearingFn,
101 vec![arg!(number), arg!(number), arg!(number), arg!(number)],
102 None
103);
104
105impl Function for GeoBearingFn {
106 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
107 self.signature.validate(args, ctx)?;
108 let lat1 = args[0].as_f64().unwrap();
109 let lon1 = args[1].as_f64().unwrap();
110 let lat2 = args[2].as_f64().unwrap();
111 let lon2 = args[3].as_f64().unwrap();
112
113 let lat1_rad = lat1.to_radians();
115 let lat2_rad = lat2.to_radians();
116 let delta_lon = (lon2 - lon1).to_radians();
117
118 let x = delta_lon.sin() * lat2_rad.cos();
119 let y = lat1_rad.cos() * lat2_rad.sin() - lat1_rad.sin() * lat2_rad.cos() * delta_lon.cos();
120
121 let bearing_rad = x.atan2(y);
122 let mut bearing = bearing_rad.to_degrees();
123
124 if bearing < 0.0 {
126 bearing += 360.0;
127 }
128
129 Ok(number_value(bearing))
130 }
131}
132
133defn!(GeoBoundingBoxFn, vec![arg!(array)], None);
138
139impl Function for GeoBoundingBoxFn {
140 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
141 self.signature.validate(args, ctx)?;
142 let points = args[0].as_array().unwrap();
143
144 if points.is_empty() {
145 return Err(custom_error(
146 ctx,
147 "geo_bounding_box requires a non-empty array of [lat, lon] points",
148 ));
149 }
150
151 let mut min_lat = f64::MAX;
152 let mut max_lat = f64::MIN;
153 let mut min_lon = f64::MAX;
154 let mut max_lon = f64::MIN;
155
156 for (i, point) in points.iter().enumerate() {
157 let arr = point.as_array().ok_or_else(|| {
158 custom_error(
159 ctx,
160 &format!("geo_bounding_box: element {i} is not an array"),
161 )
162 })?;
163 if arr.len() < 2 {
164 return Err(custom_error(
165 ctx,
166 &format!(
167 "geo_bounding_box: element {i} must have at least 2 elements [lat, lon]"
168 ),
169 ));
170 }
171 let lat = arr[0].as_f64().ok_or_else(|| {
172 custom_error(
173 ctx,
174 &format!("geo_bounding_box: element {i} lat is not a number"),
175 )
176 })?;
177 let lon = arr[1].as_f64().ok_or_else(|| {
178 custom_error(
179 ctx,
180 &format!("geo_bounding_box: element {i} lon is not a number"),
181 )
182 })?;
183
184 if lat < min_lat {
185 min_lat = lat;
186 }
187 if lat > max_lat {
188 max_lat = lat;
189 }
190 if lon < min_lon {
191 min_lon = lon;
192 }
193 if lon > max_lon {
194 max_lon = lon;
195 }
196 }
197
198 Ok(json!({
199 "min_lat": min_lat,
200 "max_lat": max_lat,
201 "min_lon": min_lon,
202 "max_lon": max_lon,
203 }))
204 }
205}
206
207defn!(
212 GeoInBboxFn,
213 vec![
214 arg!(number),
215 arg!(number),
216 arg!(number),
217 arg!(number),
218 arg!(number),
219 arg!(number)
220 ],
221 None
222);
223
224impl Function for GeoInBboxFn {
225 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
226 self.signature.validate(args, ctx)?;
227 let lat = args[0].as_f64().unwrap();
228 let lon = args[1].as_f64().unwrap();
229 let min_lat = args[2].as_f64().unwrap();
230 let max_lat = args[3].as_f64().unwrap();
231 let min_lon = args[4].as_f64().unwrap();
232 let max_lon = args[5].as_f64().unwrap();
233
234 let inside = lat >= min_lat && lat <= max_lat && lon >= min_lon && lon <= max_lon;
235 Ok(Value::Bool(inside))
236 }
237}
238
239defn!(
244 GeoInRadiusFn,
245 vec![
246 arg!(number),
247 arg!(number),
248 arg!(number),
249 arg!(number),
250 arg!(number)
251 ],
252 None
253);
254
255impl Function for GeoInRadiusFn {
256 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
257 self.signature.validate(args, ctx)?;
258 let lat = args[0].as_f64().unwrap();
259 let lon = args[1].as_f64().unwrap();
260 let center_lat = args[2].as_f64().unwrap();
261 let center_lon = args[3].as_f64().unwrap();
262 let radius_km = args[4].as_f64().unwrap();
263
264 let point = Location::new(lat, lon);
265 let center = Location::new(center_lat, center_lon);
266 let distance_m = point.haversine_distance_to(¢er).meters();
267
268 Ok(Value::Bool(distance_m <= radius_km * 1000.0))
269 }
270}
271
272defn!(GeoMidpointFn, vec![arg!(array)], None);
277
278impl Function for GeoMidpointFn {
279 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
280 self.signature.validate(args, ctx)?;
281 let points = args[0].as_array().unwrap();
282
283 if points.is_empty() {
284 return Err(custom_error(
285 ctx,
286 "geo_midpoint requires a non-empty array of [lat, lon] points",
287 ));
288 }
289
290 let mut x = 0.0_f64;
292 let mut y = 0.0_f64;
293 let mut z = 0.0_f64;
294
295 for (i, point) in points.iter().enumerate() {
296 let arr = point.as_array().ok_or_else(|| {
297 custom_error(ctx, &format!("geo_midpoint: element {i} is not an array"))
298 })?;
299 if arr.len() < 2 {
300 return Err(custom_error(
301 ctx,
302 &format!("geo_midpoint: element {i} must have at least 2 elements [lat, lon]"),
303 ));
304 }
305 let lat = arr[0].as_f64().ok_or_else(|| {
306 custom_error(
307 ctx,
308 &format!("geo_midpoint: element {i} lat is not a number"),
309 )
310 })?;
311 let lon = arr[1].as_f64().ok_or_else(|| {
312 custom_error(
313 ctx,
314 &format!("geo_midpoint: element {i} lon is not a number"),
315 )
316 })?;
317
318 let lat_rad = lat.to_radians();
319 let lon_rad = lon.to_radians();
320
321 x += lat_rad.cos() * lon_rad.cos();
322 y += lat_rad.cos() * lon_rad.sin();
323 z += lat_rad.sin();
324 }
325
326 let n = points.len() as f64;
327 x /= n;
328 y /= n;
329 z /= n;
330
331 let lon_mid = y.atan2(x).to_degrees();
332 let hyp = (x * x + y * y).sqrt();
333 let lat_mid = z.atan2(hyp).to_degrees();
334
335 Ok(json!([lat_mid, lon_mid]))
336 }
337}
338
339defn!(
344 GeohashEncodeFn,
345 vec![arg!(number), arg!(number)],
346 Some(arg!(number))
347);
348
349impl Function for GeohashEncodeFn {
350 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
351 self.signature.validate(args, ctx)?;
352 let lat = args[0].as_f64().unwrap();
353 let lon = args[1].as_f64().unwrap();
354 let precision = if args.len() > 2 {
355 args[2].as_f64().unwrap() as usize
356 } else {
357 12
358 };
359
360 let hash = geohash::encode(Coord { x: lon, y: lat }, precision)
361 .map_err(|e| custom_error(ctx, &format!("geohash_encode: {e}")))?;
362 Ok(Value::String(hash))
363 }
364}
365
366defn!(GeohashDecodeFn, vec![arg!(string)], None);
371
372impl Function for GeohashDecodeFn {
373 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
374 self.signature.validate(args, ctx)?;
375 let hash = args[0].as_str().unwrap();
376
377 let (coord, _, _) = geohash::decode(hash)
378 .map_err(|e| custom_error(ctx, &format!("geohash_decode: {e}")))?;
379
380 Ok(json!({
381 "lat": coord.y,
382 "lon": coord.x,
383 }))
384 }
385}
386
387pub fn register_filtered(runtime: &mut Runtime, enabled: &HashSet<&str>) {
389 register_if_enabled(
390 runtime,
391 "geo_distance",
392 enabled,
393 Box::new(GeoDistanceFn::new()),
394 );
395 register_if_enabled(
396 runtime,
397 "geo_distance_km",
398 enabled,
399 Box::new(GeoDistanceKmFn::new()),
400 );
401 register_if_enabled(
402 runtime,
403 "geo_distance_miles",
404 enabled,
405 Box::new(GeoDistanceMilesFn::new()),
406 );
407 register_if_enabled(
408 runtime,
409 "geo_bearing",
410 enabled,
411 Box::new(GeoBearingFn::new()),
412 );
413 register_if_enabled(
414 runtime,
415 "geo_bounding_box",
416 enabled,
417 Box::new(GeoBoundingBoxFn::new()),
418 );
419 register_if_enabled(
420 runtime,
421 "geo_in_bbox",
422 enabled,
423 Box::new(GeoInBboxFn::new()),
424 );
425 register_if_enabled(
426 runtime,
427 "geo_in_radius",
428 enabled,
429 Box::new(GeoInRadiusFn::new()),
430 );
431 register_if_enabled(
432 runtime,
433 "geo_midpoint",
434 enabled,
435 Box::new(GeoMidpointFn::new()),
436 );
437 register_if_enabled(
438 runtime,
439 "geohash_encode",
440 enabled,
441 Box::new(GeohashEncodeFn::new()),
442 );
443 register_if_enabled(
444 runtime,
445 "geohash_decode",
446 enabled,
447 Box::new(GeohashDecodeFn::new()),
448 );
449}
450
451#[cfg(test)]
452mod tests {
453 use crate::Runtime;
454 use serde_json::json;
455
456 fn setup_runtime() -> Runtime {
457 Runtime::builder()
458 .with_standard()
459 .with_all_extensions()
460 .build()
461 }
462
463 #[test]
464 fn test_geo_distance() {
465 let runtime = setup_runtime();
466 let data = json!({"nyc": [40.7128, -74.0060], "la": [34.0522, -118.2437]});
468 let expr = runtime
469 .compile("geo_distance(nyc[0], nyc[1], la[0], la[1])")
470 .unwrap();
471 let result = expr.search(&data).unwrap();
472 let meters = result.as_f64().unwrap();
473 assert!(meters > 3900000.0 && meters < 4000000.0);
475 }
476
477 #[test]
478 fn test_geo_distance_km() {
479 let runtime = setup_runtime();
480 let data = json!({"nyc": [40.7128, -74.0060], "la": [34.0522, -118.2437]});
481 let expr = runtime
482 .compile("geo_distance_km(nyc[0], nyc[1], la[0], la[1])")
483 .unwrap();
484 let result = expr.search(&data).unwrap();
485 let km = result.as_f64().unwrap();
486 assert!(km > 3900.0 && km < 4000.0);
488 }
489
490 #[test]
491 fn test_geo_distance_miles() {
492 let runtime = setup_runtime();
493 let data = json!({"nyc": [40.7128, -74.0060], "la": [34.0522, -118.2437]});
494 let expr = runtime
495 .compile("geo_distance_miles(nyc[0], nyc[1], la[0], la[1])")
496 .unwrap();
497 let result = expr.search(&data).unwrap();
498 let miles = result.as_f64().unwrap();
499 assert!(miles > 2400.0 && miles < 2500.0);
501 }
502
503 #[test]
504 fn test_geo_bearing() {
505 let runtime = setup_runtime();
506 let data = json!({"nyc": [40.7128, -74.0060], "la": [34.0522, -118.2437]});
508 let expr = runtime
509 .compile("geo_bearing(nyc[0], nyc[1], la[0], la[1])")
510 .unwrap();
511 let result = expr.search(&data).unwrap();
512 let bearing = result.as_f64().unwrap();
513 assert!(bearing > 260.0 && bearing < 290.0);
515 }
516
517 #[test]
518 fn test_geo_distance_same_point() {
519 let runtime = setup_runtime();
520 let data = json!([40.7128, -74.0060]);
521 let expr = runtime
522 .compile("geo_distance(@[0], @[1], @[0], @[1])")
523 .unwrap();
524 let result = expr.search(&data).unwrap();
525 let meters = result.as_f64().unwrap();
526 assert!(meters < 1.0); }
528
529 #[test]
530 fn test_geo_midpoint_two_points() {
531 let runtime = setup_runtime();
532 let data = json!([[0, 0], [0, 90]]);
534 let expr = runtime.compile("geo_midpoint(@)").unwrap();
535 let result = expr.search(&data).unwrap();
536 let arr = result.as_array().unwrap();
537 let lat = arr[0].as_f64().unwrap();
538 let lon = arr[1].as_f64().unwrap();
539 assert!(lat.abs() < 0.01, "lat should be ~0, got {lat}");
540 assert!((lon - 45.0).abs() < 0.01, "lon should be ~45, got {lon}");
541 }
542
543 #[test]
544 fn test_geo_midpoint_same_point() {
545 let runtime = setup_runtime();
546 let data = json!([[40.7128, -74.0060], [40.7128, -74.0060]]);
547 let expr = runtime.compile("geo_midpoint(@)").unwrap();
548 let result = expr.search(&data).unwrap();
549 let arr = result.as_array().unwrap();
550 let lat = arr[0].as_f64().unwrap();
551 let lon = arr[1].as_f64().unwrap();
552 assert!((lat - 40.7128).abs() < 0.001);
553 assert!((lon - (-74.0060)).abs() < 0.001);
554 }
555
556 #[test]
557 fn test_geo_bounding_box() {
558 let runtime = setup_runtime();
559 let data = json!([
560 [40.7128, -74.0060],
561 [34.0522, -118.2437],
562 [37.7749, -122.4194]
563 ]);
564 let expr = runtime.compile("geo_bounding_box(@)").unwrap();
565 let result = expr.search(&data).unwrap();
566 let obj = result.as_object().unwrap();
567 assert!((obj["min_lat"].as_f64().unwrap() - 34.0522).abs() < 0.001);
568 assert!((obj["max_lat"].as_f64().unwrap() - 40.7128).abs() < 0.001);
569 assert!((obj["min_lon"].as_f64().unwrap() - (-122.4194)).abs() < 0.001);
570 assert!((obj["max_lon"].as_f64().unwrap() - (-74.0060)).abs() < 0.001);
571 }
572
573 #[test]
574 fn test_geo_bounding_box_single_point() {
575 let runtime = setup_runtime();
576 let data = json!([[40.7128, -74.0060]]);
577 let expr = runtime.compile("geo_bounding_box(@)").unwrap();
578 let result = expr.search(&data).unwrap();
579 let obj = result.as_object().unwrap();
580 assert!((obj["min_lat"].as_f64().unwrap() - 40.7128).abs() < 0.001);
581 assert!((obj["max_lat"].as_f64().unwrap() - 40.7128).abs() < 0.001);
582 }
583
584 #[test]
585 fn test_geo_in_radius_inside() {
586 let runtime = setup_runtime();
587 let data = json!(null);
589 let expr = runtime
590 .compile("geo_in_radius(`40.7580`, `-73.9855`, `40.7484`, `-73.9857`, `2`)")
591 .unwrap();
592 let result = expr.search(&data).unwrap();
593 assert_eq!(result, json!(true));
594 }
595
596 #[test]
597 fn test_geo_in_radius_outside() {
598 let runtime = setup_runtime();
599 let data = json!(null);
601 let expr = runtime
602 .compile("geo_in_radius(`34.0522`, `-118.2437`, `40.7128`, `-74.0060`, `100`)")
603 .unwrap();
604 let result = expr.search(&data).unwrap();
605 assert_eq!(result, json!(false));
606 }
607
608 #[test]
609 fn test_geo_in_bbox_inside() {
610 let runtime = setup_runtime();
611 let data = json!(null);
612 let expr = runtime
613 .compile("geo_in_bbox(`40.0`, `-75.0`, `39.0`, `41.0`, `-76.0`, `-74.0`)")
614 .unwrap();
615 let result = expr.search(&data).unwrap();
616 assert_eq!(result, json!(true));
617 }
618
619 #[test]
620 fn test_geo_in_bbox_outside() {
621 let runtime = setup_runtime();
622 let data = json!(null);
623 let expr = runtime
624 .compile("geo_in_bbox(`42.0`, `-75.0`, `39.0`, `41.0`, `-76.0`, `-74.0`)")
625 .unwrap();
626 let result = expr.search(&data).unwrap();
627 assert_eq!(result, json!(false));
628 }
629
630 #[test]
631 fn test_geo_in_bbox_boundary() {
632 let runtime = setup_runtime();
633 let data = json!(null);
634 let expr = runtime
636 .compile("geo_in_bbox(`39.0`, `-76.0`, `39.0`, `41.0`, `-76.0`, `-74.0`)")
637 .unwrap();
638 let result = expr.search(&data).unwrap();
639 assert_eq!(result, json!(true));
640 }
641
642 #[test]
643 fn test_geohash_encode_default_precision() {
644 let runtime = setup_runtime();
645 let data = json!(null);
646 let expr = runtime
647 .compile("geohash_encode(`40.7128`, `-74.0060`)")
648 .unwrap();
649 let result = expr.search(&data).unwrap();
650 let hash = result.as_str().unwrap();
651 assert_eq!(hash.len(), 12);
652 assert!(hash.starts_with("dr5r"));
653 }
654
655 #[test]
656 fn test_geohash_encode_custom_precision() {
657 let runtime = setup_runtime();
658 let data = json!(null);
659 let expr = runtime
660 .compile("geohash_encode(`40.7128`, `-74.0060`, `5`)")
661 .unwrap();
662 let result = expr.search(&data).unwrap();
663 let hash = result.as_str().unwrap();
664 assert_eq!(hash.len(), 5);
665 }
666
667 #[test]
668 fn test_geohash_decode() {
669 let runtime = setup_runtime();
670 let data = json!(null);
671 let expr = runtime.compile("geohash_decode('dr5ru7')").unwrap();
672 let result = expr.search(&data).unwrap();
673 let obj = result.as_object().unwrap();
674 let lat = obj["lat"].as_f64().unwrap();
675 let lon = obj["lon"].as_f64().unwrap();
676 assert!(lat > 40.0 && lat < 41.0, "lat should be ~40.7, got {lat}");
678 assert!(lon > -75.0 && lon < -73.0, "lon should be ~-74, got {lon}");
679 }
680
681 #[test]
682 fn test_geohash_roundtrip() {
683 let runtime = setup_runtime();
684 let data = json!(null);
685 let encode_expr = runtime
687 .compile("geohash_encode(`48.8566`, `2.3522`, `8`)")
688 .unwrap();
689 let hash_val = encode_expr.search(&data).unwrap();
690 let hash = hash_val.as_str().unwrap();
691
692 let decode_expr_str = format!("geohash_decode('{hash}')");
693 let decode_expr = runtime.compile(&decode_expr_str).unwrap();
694 let result = decode_expr.search(&data).unwrap();
695 let obj = result.as_object().unwrap();
696 let lat = obj["lat"].as_f64().unwrap();
697 let lon = obj["lon"].as_f64().unwrap();
698 assert!(
699 (lat - 48.8566).abs() < 0.01,
700 "lat roundtrip: expected ~48.8566, got {lat}"
701 );
702 assert!(
703 (lon - 2.3522).abs() < 0.01,
704 "lon roundtrip: expected ~2.3522, got {lon}"
705 );
706 }
707}