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::{Common, Sort, SortOrder, common_opts, exists_q, wrap};
9use crate::query::{AsQuery, 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    pub fn at(path: impl Into<String>) -> Self {
45        Self {
46            path: path.into(),
47            _scope: PhantomData,
48        }
49    }
50
51    /// Points within `distance` (e.g. `"12km"`, `"5mi"`) of `center`. Returns a
52    /// [`GeoDistanceQuery`] builder for `distance_type` / `validation_method`
53    /// plus `boost` / `name`.
54    pub fn within(&self, distance: impl Into<String>, center: GeoPoint) -> GeoDistanceQuery<S> {
55        GeoDistanceQuery {
56            path: self.path.clone(),
57            distance: distance.into(),
58            center,
59            opts: Map::new(),
60            common: Common::default(),
61            _scope: PhantomData,
62        }
63    }
64
65    /// Points inside the axis-aligned box with the given corners.
66    pub fn in_bounding_box(&self, top_left: GeoPoint, bottom_right: GeoPoint) -> Query<S> {
67        let mut corners = Map::new();
68        corners.insert("top_left".to_string(), top_left.to_value());
69        corners.insert("bottom_right".to_string(), bottom_right.to_value());
70        let mut body = Map::new();
71        body.insert(self.path.clone(), Value::Object(corners));
72        wrap_object("geo_bounding_box", body)
73    }
74
75    /// Points inside the polygon described by `points` (three or more vertices).
76    pub fn in_polygon(&self, points: impl IntoIterator<Item = GeoPoint>) -> Query<S> {
77        let vertices = points.into_iter().map(GeoPoint::to_value).collect();
78        let mut inner = Map::new();
79        inner.insert("points".to_string(), Value::Array(vertices));
80        let mut body = Map::new();
81        body.insert(self.path.clone(), Value::Object(inner));
82        wrap_object("geo_polygon", body)
83    }
84
85    /// The field has a value.
86    pub fn exists(&self) -> Query<S> {
87        exists_q(&self.path)
88    }
89
90    /// Sort by distance from `center`, measured in `unit` (e.g. `"km"`).
91    pub fn distance_sort(
92        &self,
93        center: GeoPoint,
94        order: SortOrder,
95        unit: impl Into<String>,
96    ) -> Sort {
97        let mut body = Map::new();
98        body.insert(self.path.clone(), center.to_value());
99        body.insert(
100            "order".to_string(),
101            Value::String(order.as_str().to_string()),
102        );
103        body.insert("unit".to_string(), Value::String(unit.into()));
104        Sort::from_parts("_geo_distance".to_string(), body)
105    }
106}
107
108/// `{ "<name>": { <body> } }` as a scope-`S` query.
109fn wrap_object<S>(name: &str, body: Map<String, Value>) -> Query<S> {
110    wrap(name, body)
111}
112
113/// A `geo_distance` clause: points within a radius of a center, with the
114/// `distance_type` / `validation_method` options plus `boost` / `name`.
115#[derive(Debug, Clone)]
116pub struct GeoDistanceQuery<S = Root> {
117    path: String,
118    distance: String,
119    center: GeoPoint,
120    opts: Map<String, Value>,
121    common: Common,
122    _scope: PhantomData<fn() -> S>,
123}
124
125impl<S> GeoDistanceQuery<S> {
126    /// How distance is computed: `"arc"` (default) or `"plane"` (faster, less
127    /// accurate over long spans).
128    #[must_use]
129    pub fn distance_type(mut self, distance_type: impl Into<String>) -> Self {
130        self.opts.insert(
131            "distance_type".to_string(),
132            Value::String(distance_type.into()),
133        );
134        self
135    }
136
137    /// How malformed coordinates are handled: `"STRICT"` (default),
138    /// `"COERCE"`, or `"IGNORE_MALFORMED"`.
139    #[must_use]
140    pub fn validation_method(mut self, validation_method: impl Into<String>) -> Self {
141        self.opts.insert(
142            "validation_method".to_string(),
143            Value::String(validation_method.into()),
144        );
145        self
146    }
147
148    common_opts!(common);
149}
150
151impl<S> AsQuery<S> for GeoDistanceQuery<S> {
152    fn into_query(self) -> Option<Query<S>> {
153        let mut body = self.opts;
154        body.insert("distance".to_string(), Value::String(self.distance));
155        body.insert(self.path, self.center.to_value());
156        self.common.write(&mut body);
157        Some(wrap("geo_distance", body))
158    }
159}