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