Skip to main content

flusso_query/handles/
geo.rs

1//! Geographic field handles: a [`GeoPoint`] and the [`Geo`] field with distance,
2//! bounding-box, and polygon queries plus sort-by-distance.
3
4use std::marker::PhantomData;
5
6use serde_json::{Map, Value};
7
8use super::{Sort, SortOrder, exists_q};
9use crate::query::{Query, Root};
10
11/// A geographic point — latitude/longitude in degrees.
12#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
13pub struct GeoPoint {
14    /// Latitude in degrees.
15    pub lat: f64,
16    /// Longitude in degrees.
17    pub lon: f64,
18}
19
20impl GeoPoint {
21    /// A point at `lat`/`lon` degrees.
22    pub fn new(lat: f64, lon: f64) -> Self {
23        Self { lat, lon }
24    }
25
26    /// `{ "lat": …, "lon": … }`.
27    fn to_value(self) -> Value {
28        let mut point = Map::new();
29        point.insert("lat".to_string(), Value::from(self.lat));
30        point.insert("lon".to_string(), Value::from(self.lon));
31        Value::Object(point)
32    }
33}
34
35/// A `geo_point` field — distance, bounding-box, and polygon queries, plus
36/// sort-by-distance.
37#[derive(Debug, Clone)]
38pub struct Geo<S = Root> {
39    path: String,
40    _scope: PhantomData<fn() -> S>,
41}
42
43impl<S> Geo<S> {
44    /// Build a handle for the field at `path`.
45    pub fn at(path: impl Into<String>) -> Self {
46        Self {
47            path: path.into(),
48            _scope: PhantomData,
49        }
50    }
51
52    /// Points within `distance` (e.g. `"12km"`, `"5mi"`) of `center`.
53    pub fn within(&self, distance: impl Into<String>, center: GeoPoint) -> Query<S> {
54        let mut body = Map::new();
55        body.insert("distance".to_string(), Value::String(distance.into()));
56        body.insert(self.path.clone(), center.to_value());
57        wrap_object("geo_distance", body)
58    }
59
60    /// Points inside the axis-aligned box with the given corners.
61    pub fn in_bounding_box(&self, top_left: GeoPoint, bottom_right: GeoPoint) -> Query<S> {
62        let mut corners = Map::new();
63        corners.insert("top_left".to_string(), top_left.to_value());
64        corners.insert("bottom_right".to_string(), bottom_right.to_value());
65        let mut body = Map::new();
66        body.insert(self.path.clone(), Value::Object(corners));
67        wrap_object("geo_bounding_box", body)
68    }
69
70    /// Points inside the polygon described by `points` (three or more vertices).
71    pub fn in_polygon(&self, points: impl IntoIterator<Item = GeoPoint>) -> Query<S> {
72        let vertices = points.into_iter().map(GeoPoint::to_value).collect();
73        let mut inner = Map::new();
74        inner.insert("points".to_string(), Value::Array(vertices));
75        let mut body = Map::new();
76        body.insert(self.path.clone(), Value::Object(inner));
77        wrap_object("geo_polygon", body)
78    }
79
80    /// The field has a value.
81    pub fn exists(&self) -> Query<S> {
82        exists_q(&self.path)
83    }
84
85    /// Sort by distance from `center`, measured in `unit` (e.g. `"km"`).
86    pub fn distance_sort(
87        &self,
88        center: GeoPoint,
89        order: SortOrder,
90        unit: impl Into<String>,
91    ) -> Sort {
92        let mut body = Map::new();
93        body.insert(self.path.clone(), center.to_value());
94        body.insert(
95            "order".to_string(),
96            Value::String(order.as_str().to_string()),
97        );
98        body.insert("unit".to_string(), Value::String(unit.into()));
99        let mut outer = Map::new();
100        outer.insert("_geo_distance".to_string(), Value::Object(body));
101        Sort::raw(Value::Object(outer))
102    }
103}
104
105/// `{ "<name>": { <body> } }` as a scope-`S` query.
106fn wrap_object<S>(name: &str, body: Map<String, Value>) -> Query<S> {
107    let mut outer = Map::new();
108    outer.insert(name.to_string(), Value::Object(body));
109    Query::leaf(Value::Object(outer))
110}