1#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct GeoPoint {
8 pub lat: f64,
9 pub lon: f64,
10}
11
12impl GeoPoint {
13 pub fn new(lat: f64, lon: f64) -> Self {
14 Self { lat, lon }
15 }
16
17 pub fn from_json(value: &serde_json::Value) -> Option<Self> {
22 match value {
23 serde_json::Value::Object(obj) => {
24 let lat = obj.get("lat").and_then(|v| v.as_f64())?;
25 let lon = obj.get("lon").and_then(|v| v.as_f64())?;
26 Some(Self { lat, lon })
27 }
28 serde_json::Value::Array(arr) if arr.len() == 2 => {
29 let lon = arr[0].as_f64()?;
31 let lat = arr[1].as_f64()?;
32 Some(Self { lat, lon })
33 }
34 serde_json::Value::String(s) => {
35 let parts: Vec<&str> = s.split(',').collect();
36 if parts.len() == 2 {
37 let lat = parts[0].trim().parse().ok()?;
38 let lon = parts[1].trim().parse().ok()?;
39 Some(Self { lat, lon })
40 } else {
41 None
42 }
43 }
44 _ => None,
45 }
46 }
47}
48
49const EARTH_RADIUS_KM: f64 = 6371.0;
51
52pub fn haversine_km(a: GeoPoint, b: GeoPoint) -> f64 {
54 let lat1 = a.lat.to_radians();
55 let lat2 = b.lat.to_radians();
56 let dlat = (b.lat - a.lat).to_radians();
57 let dlon = (b.lon - a.lon).to_radians();
58
59 let h = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
60 let c = 2.0 * h.sqrt().asin();
61
62 EARTH_RADIUS_KM * c
63}
64
65pub fn in_bounding_box(point: GeoPoint, top_left: GeoPoint, bottom_right: GeoPoint) -> bool {
67 point.lat <= top_left.lat
68 && point.lat >= bottom_right.lat
69 && point.lon >= top_left.lon
70 && point.lon <= bottom_right.lon
71}
72
73#[derive(Clone)]
80pub struct GeoPointStore {
81 lats: Vec<f64>,
82 lons: Vec<f64>,
83 lat_order: Vec<u32>,
85}
86
87impl GeoPointStore {
88 pub fn new() -> Self {
89 Self {
90 lats: Vec::new(),
91 lons: Vec::new(),
92 lat_order: Vec::new(),
93 }
94 }
95
96 pub fn add(&mut self, point: GeoPoint) {
97 self.lats.push(point.lat);
98 self.lons.push(point.lon);
99 }
100
101 pub fn add_null(&mut self) {
102 self.lats.push(f64::NAN);
103 self.lons.push(f64::NAN);
104 }
105
106 pub fn len(&self) -> usize {
107 self.lats.len()
108 }
109 pub fn is_empty(&self) -> bool {
110 self.lats.is_empty()
111 }
112
113 pub fn get(&self, doc_id: u32) -> Option<GeoPoint> {
114 let i = doc_id as usize;
115 if i < self.lats.len() && !self.lats[i].is_nan() {
116 Some(GeoPoint::new(self.lats[i], self.lons[i]))
117 } else {
118 None
119 }
120 }
121
122 pub fn to_bytes(&self) -> Vec<u8> {
124 let mut buf = Vec::new();
125 buf.extend_from_slice(&(self.lats.len() as u32).to_le_bytes());
126 for &lat in &self.lats {
127 buf.extend_from_slice(&lat.to_le_bytes());
128 }
129 for &lon in &self.lons {
130 buf.extend_from_slice(&lon.to_le_bytes());
131 }
132 buf
133 }
134
135 pub fn docs_in_lat_range(&self, min_lat: f64, max_lat: f64) -> Vec<u32> {
140 if self.lat_order.is_empty() {
141 return Vec::new();
142 }
143 let start = self
144 .lat_order
145 .partition_point(|&id| self.lats[id as usize] < min_lat);
146 let end = self
147 .lat_order
148 .partition_point(|&id| self.lats[id as usize] <= max_lat);
149 let mut result: Vec<u32> = self.lat_order[start..end].to_vec();
150 result.sort_unstable(); result
152 }
153
154 fn build_lat_order(lats: &[f64]) -> Vec<u32> {
155 let mut order: Vec<u32> = (0..lats.len() as u32)
156 .filter(|&i| !lats[i as usize].is_nan())
157 .collect();
158 order.sort_unstable_by(|&a, &b| lats[a as usize].partial_cmp(&lats[b as usize]).unwrap());
159 order
160 }
161
162 pub fn from_bytes(data: &[u8]) -> Self {
164 let count = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
165 let mut pos = 4;
166 let mut lats = Vec::with_capacity(count);
167 for _ in 0..count {
168 lats.push(f64::from_le_bytes(data[pos..pos + 8].try_into().unwrap()));
169 pos += 8;
170 }
171 let mut lons = Vec::with_capacity(count);
172 for _ in 0..count {
173 lons.push(f64::from_le_bytes(data[pos..pos + 8].try_into().unwrap()));
174 pos += 8;
175 }
176 let lat_order = Self::build_lat_order(&lats);
177 Self {
178 lats,
179 lons,
180 lat_order,
181 }
182 }
183}
184
185impl Default for GeoPointStore {
186 fn default() -> Self {
187 Self::new()
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn haversine_known_distance() {
197 let nyc = GeoPoint::new(40.7128, -74.0060);
199 let london = GeoPoint::new(51.5074, -0.1278);
200 let d = haversine_km(nyc, london);
201 assert!((d - 5570.0).abs() < 50.0, "NYC-London ≈ 5570km, got {d}");
202 }
203
204 #[test]
205 fn haversine_same_point() {
206 let p = GeoPoint::new(0.0, 0.0);
207 assert!(haversine_km(p, p) < 0.001);
208 }
209
210 #[test]
211 fn bounding_box_contains() {
212 let tl = GeoPoint::new(41.0, -74.5);
213 let br = GeoPoint::new(40.0, -73.5);
214 let inside = GeoPoint::new(40.5, -74.0);
215 let outside = GeoPoint::new(42.0, -74.0);
216 assert!(in_bounding_box(inside, tl, br));
217 assert!(!in_bounding_box(outside, tl, br));
218 }
219
220 #[test]
221 fn geo_point_from_json_object() {
222 let v = serde_json::json!({"lat": 40.7, "lon": -73.9});
223 let p = GeoPoint::from_json(&v).unwrap();
224 assert!((p.lat - 40.7).abs() < 0.001);
225 }
226
227 #[test]
228 fn geo_point_from_json_array() {
229 let v = serde_json::json!([-73.9, 40.7]); let p = GeoPoint::from_json(&v).unwrap();
231 assert!((p.lat - 40.7).abs() < 0.001);
232 assert!((p.lon - (-73.9)).abs() < 0.001);
233 }
234
235 #[test]
236 fn geo_point_from_json_string() {
237 let v = serde_json::json!("40.7,-73.9");
238 let p = GeoPoint::from_json(&v).unwrap();
239 assert!((p.lat - 40.7).abs() < 0.001);
240 }
241
242 #[test]
243 fn geo_store_round_trip() {
244 let mut store = GeoPointStore::new();
245 store.add(GeoPoint::new(40.7, -73.9));
246 store.add_null();
247 store.add(GeoPoint::new(51.5, -0.1));
248
249 let bytes = store.to_bytes();
250 let restored = GeoPointStore::from_bytes(&bytes);
251
252 assert_eq!(restored.len(), 3);
253 let p0 = restored.get(0).unwrap();
254 assert!((p0.lat - 40.7).abs() < 0.001);
255 assert!(restored.get(1).is_none()); let p2 = restored.get(2).unwrap();
257 assert!((p2.lat - 51.5).abs() < 0.001);
258 }
259}