1use serde::{Deserialize, Serialize};
11
12#[derive(
15 Debug,
16 Clone,
17 Copy,
18 PartialEq,
19 Serialize,
20 Deserialize,
21 zerompk::ToMessagePack,
22 zerompk::FromMessagePack,
23)]
24pub struct Coord {
25 pub lng: f64,
26 pub lat: f64,
27}
28
29impl Coord {
30 pub fn new(lng: f64, lat: f64) -> Self {
31 Self { lng, lat }
32 }
33}
34
35#[derive(
37 Debug,
38 Clone,
39 PartialEq,
40 Serialize,
41 Deserialize,
42 zerompk::ToMessagePack,
43 zerompk::FromMessagePack,
44)]
45#[serde(tag = "type")]
46#[non_exhaustive]
47pub enum Geometry {
48 Point {
49 coordinates: [f64; 2],
50 },
51 LineString {
52 coordinates: Vec<[f64; 2]>,
53 },
54 Polygon {
55 coordinates: Vec<Vec<[f64; 2]>>,
56 },
57 MultiPoint {
58 coordinates: Vec<[f64; 2]>,
59 },
60 MultiLineString {
61 coordinates: Vec<Vec<[f64; 2]>>,
62 },
63 MultiPolygon {
64 coordinates: Vec<Vec<Vec<[f64; 2]>>>,
65 },
66 GeometryCollection {
67 geometries: Vec<Geometry>,
68 },
69}
70
71impl Geometry {
72 pub fn point(lng: f64, lat: f64) -> Self {
74 Geometry::Point {
75 coordinates: [lng, lat],
76 }
77 }
78
79 pub fn line_string(coords: Vec<[f64; 2]>) -> Self {
81 Geometry::LineString {
82 coordinates: coords,
83 }
84 }
85
86 pub fn polygon(rings: Vec<Vec<[f64; 2]>>) -> Self {
91 Geometry::Polygon { coordinates: rings }
92 }
93
94 pub fn geometry_type(&self) -> &'static str {
96 match self {
97 Geometry::Point { .. } => "Point",
98 Geometry::LineString { .. } => "LineString",
99 Geometry::Polygon { .. } => "Polygon",
100 Geometry::MultiPoint { .. } => "MultiPoint",
101 Geometry::MultiLineString { .. } => "MultiLineString",
102 Geometry::MultiPolygon { .. } => "MultiPolygon",
103 Geometry::GeometryCollection { .. } => "GeometryCollection",
104 }
105 }
106
107 pub fn centroid(&self) -> Option<[f64; 2]> {
109 match self {
110 Geometry::Point { coordinates } => Some(*coordinates),
111 Geometry::LineString { coordinates } => {
112 if coordinates.is_empty() {
113 return None;
114 }
115 let n = coordinates.len() as f64;
116 let sum_lng: f64 = coordinates.iter().map(|c| c[0]).sum();
117 let sum_lat: f64 = coordinates.iter().map(|c| c[1]).sum();
118 Some([sum_lng / n, sum_lat / n])
119 }
120 Geometry::Polygon { coordinates } => {
121 coordinates.first().and_then(|ring| {
123 if ring.is_empty() {
124 return None;
125 }
126 let n = ring.len() as f64;
127 let sum_lng: f64 = ring.iter().map(|c| c[0]).sum();
128 let sum_lat: f64 = ring.iter().map(|c| c[1]).sum();
129 Some([sum_lng / n, sum_lat / n])
130 })
131 }
132 Geometry::MultiPoint { coordinates } => {
133 if coordinates.is_empty() {
134 return None;
135 }
136 let n = coordinates.len() as f64;
137 let sum_lng: f64 = coordinates.iter().map(|c| c[0]).sum();
138 let sum_lat: f64 = coordinates.iter().map(|c| c[1]).sum();
139 Some([sum_lng / n, sum_lat / n])
140 }
141 _ => None,
142 }
143 }
144}
145
146const EARTH_RADIUS_M: f64 = 6_371_000.0;
149
150pub fn haversine_distance(lng1: f64, lat1: f64, lng2: f64, lat2: f64) -> f64 {
154 let lat1_r = lat1.to_radians();
155 let lat2_r = lat2.to_radians();
156 let dlat = (lat2 - lat1).to_radians();
157 let dlng = (lng2 - lng1).to_radians();
158
159 let a = (dlat / 2.0).sin().powi(2) + lat1_r.cos() * lat2_r.cos() * (dlng / 2.0).sin().powi(2);
160 let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
161 EARTH_RADIUS_M * c
162}
163
164pub fn haversine_bearing(lng1: f64, lat1: f64, lng2: f64, lat2: f64) -> f64 {
166 let lat1_r = lat1.to_radians();
167 let lat2_r = lat2.to_radians();
168 let dlng = (lng2 - lng1).to_radians();
169
170 let y = dlng.sin() * lat2_r.cos();
171 let x = lat1_r.cos() * lat2_r.sin() - lat1_r.sin() * lat2_r.cos() * dlng.cos();
172 let bearing = y.atan2(x).to_degrees();
173 (bearing + 360.0) % 360.0
174}
175
176pub fn polygon_area(ring: &[[f64; 2]]) -> f64 {
181 if ring.len() < 3 {
182 return 0.0;
183 }
184 let mut sum = 0.0;
185 let n = ring.len();
186 for i in 0..n {
187 let j = (i + 1) % n;
188 let lat_avg = ((ring[i][1] + ring[j][1]) / 2.0).to_radians();
190 let x1 = ring[i][0].to_radians() * EARTH_RADIUS_M * lat_avg.cos();
191 let y1 = ring[i][1].to_radians() * EARTH_RADIUS_M;
192 let x2 = ring[j][0].to_radians() * EARTH_RADIUS_M * lat_avg.cos();
193 let y2 = ring[j][1].to_radians() * EARTH_RADIUS_M;
194 sum += x1 * y2 - x2 * y1;
195 }
196 (sum / 2.0).abs()
197}
198
199pub fn point_in_polygon(lng: f64, lat: f64, ring: &[[f64; 2]]) -> bool {
201 let mut inside = false;
202 let n = ring.len();
203 let mut j = n.wrapping_sub(1);
204 for i in 0..n {
205 let yi = ring[i][1];
206 let yj = ring[j][1];
207 if ((yi > lat) != (yj > lat))
208 && (lng < (ring[j][0] - ring[i][0]) * (lat - yi) / (yj - yi) + ring[i][0])
209 {
210 inside = !inside;
211 }
212 j = i;
213 }
214 inside
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn point_creation() {
223 let p = Geometry::point(-73.9857, 40.7484);
224 assert_eq!(p.geometry_type(), "Point");
225 if let Geometry::Point { coordinates } = &p {
226 assert!((coordinates[0] - (-73.9857)).abs() < 1e-6);
227 assert!((coordinates[1] - 40.7484).abs() < 1e-6);
228 }
229 }
230
231 #[test]
232 fn haversine_nyc_to_london() {
233 let d = haversine_distance(-74.006, 40.7128, -0.1278, 51.5074);
235 assert!((d - 5_570_000.0).abs() < 50_000.0, "got {d}m");
237 }
238
239 #[test]
240 fn haversine_same_point() {
241 let d = haversine_distance(0.0, 0.0, 0.0, 0.0);
242 assert!(d.abs() < 1e-6);
243 }
244
245 #[test]
246 fn bearing_north() {
247 let b = haversine_bearing(0.0, 0.0, 0.0, 1.0);
248 assert!((b - 0.0).abs() < 1.0, "expected ~0, got {b}");
249 }
250
251 #[test]
252 fn bearing_east() {
253 let b = haversine_bearing(0.0, 0.0, 1.0, 0.0);
254 assert!((b - 90.0).abs() < 1.0, "expected ~90, got {b}");
255 }
256
257 #[test]
258 fn polygon_area_simple() {
259 let ring = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]];
261 let area = polygon_area(&ring);
262 let area_km2 = area / 1e6;
263 assert!(
264 (area_km2 - 12321.0).abs() < 500.0,
265 "expected ~12321 km², got {area_km2}"
266 );
267 }
268
269 #[test]
270 fn point_in_polygon_inside() {
271 let ring = vec![
272 [0.0, 0.0],
273 [10.0, 0.0],
274 [10.0, 10.0],
275 [0.0, 10.0],
276 [0.0, 0.0],
277 ];
278 assert!(point_in_polygon(5.0, 5.0, &ring));
279 assert!(!point_in_polygon(15.0, 5.0, &ring));
280 }
281
282 #[test]
283 fn centroid_point() {
284 let p = Geometry::point(10.0, 20.0);
285 assert_eq!(p.centroid(), Some([10.0, 20.0]));
286 }
287
288 #[test]
289 fn centroid_linestring() {
290 let ls = Geometry::line_string(vec![[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]]);
291 let c = ls.centroid().unwrap();
292 assert!((c[0] - 6.6667).abs() < 0.01);
293 assert!((c[1] - 3.3333).abs() < 0.01);
294 }
295
296 #[test]
297 fn geojson_serialize() {
298 let p = Geometry::point(1.0, 2.0);
299 let json = sonic_rs::to_string(&p).unwrap();
300 assert!(json.contains("\"type\":\"Point\""));
301 assert!(json.contains("\"coordinates\":[1.0,2.0]"));
302 }
303
304 #[test]
305 fn geojson_roundtrip() {
306 let original = Geometry::polygon(vec![vec![
307 [0.0, 0.0],
308 [1.0, 0.0],
309 [1.0, 1.0],
310 [0.0, 1.0],
311 [0.0, 0.0],
312 ]]);
313 let json = sonic_rs::to_string(&original).unwrap();
314 let parsed: Geometry = sonic_rs::from_str(&json).unwrap();
315 assert_eq!(original, parsed);
316 }
317}