uni_query/query/
spatial.rs1use anyhow::{Result, anyhow};
5use std::collections::HashMap;
6use uni_common::Value;
7
8const EARTH_RADIUS_KM: f64 = 6371.0;
9
10pub fn eval_spatial_function(name: &str, args: &[Value]) -> Result<Value> {
11 match name {
12 "POINT" => eval_point(args),
13 "DISTANCE" => eval_distance(args),
14 "POINT.WITHINBBOX" => eval_within_bbox(args),
15 _ => Err(anyhow!("Unknown spatial function: {}", name)),
16 }
17}
18
19fn eval_point(args: &[Value]) -> Result<Value> {
20 if args.len() != 1 {
21 return Err(anyhow!("point() requires exactly 1 map argument"));
22 }
23
24 let map = args[0]
25 .as_object()
26 .ok_or_else(|| anyhow!("point() requires a map argument"))?;
27
28 if map.contains_key("latitude") && map.contains_key("longitude") {
30 let lat = map["latitude"]
31 .as_f64()
32 .ok_or_else(|| anyhow!("latitude must be a number"))?;
33 let lon = map["longitude"]
34 .as_f64()
35 .ok_or_else(|| anyhow!("longitude must be a number"))?;
36
37 if !(-90.0..=90.0).contains(&lat) {
38 return Err(anyhow!("latitude must be between -90 and 90"));
39 }
40 if !(-180.0..=180.0).contains(&lon) {
41 return Err(anyhow!("longitude must be between -180 and 180"));
42 }
43
44 return Ok(Value::Map(HashMap::from([
45 ("type".to_string(), Value::String("Point".into())),
46 ("crs".to_string(), Value::String("WGS84".into())),
47 ("latitude".to_string(), Value::Float(lat)),
48 ("longitude".to_string(), Value::Float(lon)),
49 ])));
50 }
51
52 if map.contains_key("x") && map.contains_key("y") {
54 let x = map["x"]
55 .as_f64()
56 .ok_or_else(|| anyhow!("x must be a number"))?;
57 let y = map["y"]
58 .as_f64()
59 .ok_or_else(|| anyhow!("y must be a number"))?;
60 let z = map.get("z").and_then(|v| v.as_f64());
61
62 let crs = if z.is_some() {
63 "Cartesian-3D"
64 } else {
65 "Cartesian"
66 };
67
68 let z_value = z.map_or(Value::Null, Value::Float);
69
70 return Ok(Value::Map(HashMap::from([
71 ("type".to_string(), Value::String("Point".into())),
72 ("crs".to_string(), Value::String(crs.into())),
73 ("x".to_string(), Value::Float(x)),
74 ("y".to_string(), Value::Float(y)),
75 ("z".to_string(), z_value),
76 ])));
77 }
78
79 Err(anyhow!(
80 "point() requires either {{latitude, longitude}} or {{x, y}}"
81 ))
82}
83
84fn eval_distance(args: &[Value]) -> Result<Value> {
85 if args.len() != 2 {
86 return Err(anyhow!("distance() requires exactly 2 point arguments"));
87 }
88
89 let p1 = args[0]
90 .as_object()
91 .ok_or_else(|| anyhow!("First argument must be a point"))?;
92 let p2 = args[1]
93 .as_object()
94 .ok_or_else(|| anyhow!("Second argument must be a point"))?;
95
96 let crs1 = p1.get("crs").and_then(|v| v.as_str()).unwrap_or("");
97 let crs2 = p2.get("crs").and_then(|v| v.as_str()).unwrap_or("");
98
99 if crs1 != crs2 {
100 return Err(anyhow!(
101 "Cannot compute distance between points with different CRS"
102 ));
103 }
104
105 match crs1 {
106 "WGS84" => {
107 let (lat1, lon1) = get_geo_coords(p1, "First point")?;
108 let (lat2, lon2) = get_geo_coords(p2, "Second point")?;
109 let (lat1, lon1) = (lat1.to_radians(), lon1.to_radians());
110 let (lat2, lon2) = (lat2.to_radians(), lon2.to_radians());
111
112 let dlat = lat2 - lat1;
113 let dlon = lon2 - lon1;
114
115 let a =
116 (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
117 let c = 2.0 * a.sqrt().asin();
118
119 Ok(Value::Float(EARTH_RADIUS_KM * c * 1000.0)) }
121 "Cartesian" => {
122 let (x1, y1) = get_cartesian_coords(p1, "First point")?;
123 let (x2, y2) = get_cartesian_coords(p2, "Second point")?;
124
125 Ok(Value::Float(((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()))
126 }
127 "Cartesian-3D" => {
128 let (x1, y1) = get_cartesian_coords(p1, "First point")?;
129 let (x2, y2) = get_cartesian_coords(p2, "Second point")?;
130 let z1 = p1.get("z").and_then(|v| v.as_f64()).unwrap_or(0.0);
131 let z2 = p2.get("z").and_then(|v| v.as_f64()).unwrap_or(0.0);
132
133 Ok(Value::Float(
134 ((x2 - x1).powi(2) + (y2 - y1).powi(2) + (z2 - z1).powi(2)).sqrt(),
135 ))
136 }
137 _ => Err(anyhow!("Unknown coordinate reference system: {}", crs1)),
138 }
139}
140
141fn get_geo_coords(point: &HashMap<String, Value>, name: &str) -> Result<(f64, f64)> {
143 let lat = point
144 .get("latitude")
145 .and_then(|v| v.as_f64())
146 .ok_or_else(|| anyhow!("{} latitude must be a number", name))?;
147 let lon = point
148 .get("longitude")
149 .and_then(|v| v.as_f64())
150 .ok_or_else(|| anyhow!("{} longitude must be a number", name))?;
151 Ok((lat, lon))
152}
153
154fn get_cartesian_coords(point: &HashMap<String, Value>, name: &str) -> Result<(f64, f64)> {
156 let x = point
157 .get("x")
158 .and_then(|v| v.as_f64())
159 .ok_or_else(|| anyhow!("{} x must be a number", name))?;
160 let y = point
161 .get("y")
162 .and_then(|v| v.as_f64())
163 .ok_or_else(|| anyhow!("{} y must be a number", name))?;
164 Ok((x, y))
165}
166
167fn eval_within_bbox(args: &[Value]) -> Result<Value> {
170 if args.len() != 3 {
171 return Err(anyhow!(
172 "point.withinBBox() requires 3 arguments: point, lowerLeft, upperRight"
173 ));
174 }
175
176 let point = args[0]
177 .as_object()
178 .ok_or_else(|| anyhow!("First argument must be a point"))?;
179 let lower_left = args[1]
180 .as_object()
181 .ok_or_else(|| anyhow!("Second argument must be a point (lower-left corner)"))?;
182 let upper_right = args[2]
183 .as_object()
184 .ok_or_else(|| anyhow!("Third argument must be a point (upper-right corner)"))?;
185
186 if point.contains_key("latitude") {
188 let (lat, lon) = get_geo_coords(point, "Point")?;
189 let (min_lat, min_lon) = get_geo_coords(lower_left, "Lower-left")?;
190 let (max_lat, max_lon) = get_geo_coords(upper_right, "Upper-right")?;
191
192 return Ok(Value::Bool(
193 (min_lat..=max_lat).contains(&lat) && (min_lon..=max_lon).contains(&lon),
194 ));
195 }
196
197 if point.contains_key("x") {
199 let (x, y) = get_cartesian_coords(point, "Point")?;
200 let (min_x, min_y) = get_cartesian_coords(lower_left, "Lower-left")?;
201 let (max_x, max_y) = get_cartesian_coords(upper_right, "Upper-right")?;
202
203 return Ok(Value::Bool(
204 (min_x..=max_x).contains(&x) && (min_y..=max_y).contains(&y),
205 ));
206 }
207
208 Err(anyhow!(
209 "Point must have either latitude/longitude or x/y coordinates"
210 ))
211}