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
108pub fn from_geojson_str(s: &str) -> Option<Geometry> {
120 sonic_rs::from_str(s).ok()
121}
122
123const EARTH_RADIUS_M: f64 = 6_371_000.0;
126
127pub fn haversine_distance(lng1: f64, lat1: f64, lng2: f64, lat2: f64) -> f64 {
131 let lat1_r = lat1.to_radians();
132 let lat2_r = lat2.to_radians();
133 let dlat = (lat2 - lat1).to_radians();
134 let dlng = (lng2 - lng1).to_radians();
135
136 let a = (dlat / 2.0).sin().powi(2) + lat1_r.cos() * lat2_r.cos() * (dlng / 2.0).sin().powi(2);
137 let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
138 EARTH_RADIUS_M * c
139}
140
141pub fn haversine_bearing(lng1: f64, lat1: f64, lng2: f64, lat2: f64) -> f64 {
143 let lat1_r = lat1.to_radians();
144 let lat2_r = lat2.to_radians();
145 let dlng = (lng2 - lng1).to_radians();
146
147 let y = dlng.sin() * lat2_r.cos();
148 let x = lat1_r.cos() * lat2_r.sin() - lat1_r.sin() * lat2_r.cos() * dlng.cos();
149 let bearing = y.atan2(x).to_degrees();
150 (bearing + 360.0) % 360.0
151}
152
153pub fn point_in_polygon(lng: f64, lat: f64, ring: &[[f64; 2]]) -> bool {
155 let mut inside = false;
156 let n = ring.len();
157 let mut j = n.wrapping_sub(1);
158 for i in 0..n {
159 let yi = ring[i][1];
160 let yj = ring[j][1];
161 if ((yi > lat) != (yj > lat))
162 && (lng < (ring[j][0] - ring[i][0]) * (lat - yi) / (yj - yi) + ring[i][0])
163 {
164 inside = !inside;
165 }
166 j = i;
167 }
168 inside
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn point_creation() {
177 let p = Geometry::point(-73.9857, 40.7484);
178 assert_eq!(p.geometry_type(), "Point");
179 if let Geometry::Point { coordinates } = &p {
180 assert!((coordinates[0] - (-73.9857)).abs() < 1e-6);
181 assert!((coordinates[1] - 40.7484).abs() < 1e-6);
182 }
183 }
184
185 #[test]
186 fn haversine_nyc_to_london() {
187 let d = haversine_distance(-74.006, 40.7128, -0.1278, 51.5074);
189 assert!((d - 5_570_000.0).abs() < 50_000.0, "got {d}m");
191 }
192
193 #[test]
194 fn haversine_same_point() {
195 let d = haversine_distance(0.0, 0.0, 0.0, 0.0);
196 assert!(d.abs() < 1e-6);
197 }
198
199 #[test]
200 fn bearing_north() {
201 let b = haversine_bearing(0.0, 0.0, 0.0, 1.0);
202 assert!((b - 0.0).abs() < 1.0, "expected ~0, got {b}");
203 }
204
205 #[test]
206 fn bearing_east() {
207 let b = haversine_bearing(0.0, 0.0, 1.0, 0.0);
208 assert!((b - 90.0).abs() < 1.0, "expected ~90, got {b}");
209 }
210
211 #[test]
212 fn point_in_polygon_inside() {
213 let ring = vec![
214 [0.0, 0.0],
215 [10.0, 0.0],
216 [10.0, 10.0],
217 [0.0, 10.0],
218 [0.0, 0.0],
219 ];
220 assert!(point_in_polygon(5.0, 5.0, &ring));
221 assert!(!point_in_polygon(15.0, 5.0, &ring));
222 }
223
224 #[test]
225 fn geojson_serialize() {
226 let p = Geometry::point(1.0, 2.0);
227 let json = sonic_rs::to_string(&p).unwrap();
228 assert!(json.contains("\"type\":\"Point\""));
229 assert!(json.contains("\"coordinates\":[1.0,2.0]"));
230 }
231
232 #[test]
233 fn geojson_roundtrip() {
234 let original = Geometry::polygon(vec![vec![
235 [0.0, 0.0],
236 [1.0, 0.0],
237 [1.0, 1.0],
238 [0.0, 1.0],
239 [0.0, 0.0],
240 ]]);
241 let json = sonic_rs::to_string(&original).unwrap();
242 let parsed: Geometry = sonic_rs::from_str(&json).unwrap();
243 assert_eq!(original, parsed);
244 }
245}