1use nodedb_types::geometry::Geometry;
13
14const EARTH_RADIUS_M: f64 = 6_371_000.0;
16
17pub fn st_area(geom: &Geometry) -> f64 {
23 match geom {
24 Geometry::Polygon { coordinates } => polygon_area(coordinates),
25 Geometry::MultiPolygon { coordinates } => {
26 coordinates.iter().map(|rings| polygon_area(rings)).sum()
27 }
28 Geometry::GeometryCollection { geometries } => geometries.iter().map(st_area).sum(),
29 Geometry::Point { .. }
30 | Geometry::LineString { .. }
31 | Geometry::MultiPoint { .. }
32 | Geometry::MultiLineString { .. } => 0.0,
33 _ => 0.0,
37 }
38}
39
40fn polygon_area(rings: &[Vec<[f64; 2]>]) -> f64 {
42 let mut iter = rings.iter();
43 let Some(exterior) = iter.next() else {
44 return 0.0;
45 };
46 let holes: f64 = iter.map(|ring| ring_area(ring)).sum();
47 (ring_area(exterior) - holes).max(0.0)
48}
49
50fn ring_area(ring: &[[f64; 2]]) -> f64 {
57 if ring.len() < 3 {
58 return 0.0;
59 }
60 let mut total = 0.0;
61 for i in 0..ring.len() {
62 let a = ring[i];
63 let b = ring[(i + 1) % ring.len()];
64 let mut dlng = (b[0] - a[0]).to_radians();
65 if dlng > std::f64::consts::PI {
66 dlng -= std::f64::consts::TAU;
67 } else if dlng < -std::f64::consts::PI {
68 dlng += std::f64::consts::TAU;
69 }
70 total += dlng * (2.0 + a[1].to_radians().sin() + b[1].to_radians().sin());
71 }
72 (total * EARTH_RADIUS_M * EARTH_RADIUS_M / 2.0).abs()
73}
74
75pub fn st_centroid(geom: &Geometry) -> Option<Geometry> {
83 let [lng, lat] = centroid_coords(geom)?;
84 Some(Geometry::point(lng, lat))
85}
86
87#[derive(Default, Clone, Copy)]
89struct Weighted {
90 lng: f64,
91 lat: f64,
92 weight: f64,
93}
94
95impl Weighted {
96 fn add(&mut self, point: [f64; 2], weight: f64) {
97 self.lng += point[0] * weight;
98 self.lat += point[1] * weight;
99 self.weight += weight;
100 }
101
102 fn merge(&mut self, other: Self) {
103 self.lng += other.lng;
104 self.lat += other.lat;
105 self.weight += other.weight;
106 }
107
108 fn resolve(self) -> Option<[f64; 2]> {
109 if self.weight > 0.0 {
110 Some([self.lng / self.weight, self.lat / self.weight])
111 } else {
112 None
113 }
114 }
115}
116
117fn centroid_coords(geom: &Geometry) -> Option<[f64; 2]> {
118 areal_centroid(geom)
119 .and_then(Weighted::resolve)
120 .or_else(|| linear_centroid(geom).and_then(Weighted::resolve))
121 .or_else(|| punctual_centroid(geom).and_then(Weighted::resolve))
122}
123
124fn areal_centroid(geom: &Geometry) -> Option<Weighted> {
127 match geom {
128 Geometry::Polygon { coordinates } => Some(polygon_centroid(coordinates)),
129 Geometry::MultiPolygon { coordinates } => {
130 let mut acc = Weighted::default();
131 for rings in coordinates {
132 acc.merge(polygon_centroid(rings));
133 }
134 Some(acc)
135 }
136 Geometry::GeometryCollection { geometries } => collect(geometries, areal_centroid),
137 Geometry::Point { .. }
138 | Geometry::LineString { .. }
139 | Geometry::MultiPoint { .. }
140 | Geometry::MultiLineString { .. } => None,
141 _ => None,
144 }
145}
146
147fn linear_centroid(geom: &Geometry) -> Option<Weighted> {
149 match geom {
150 Geometry::LineString { coordinates } => Some(line_centroid(coordinates)),
151 Geometry::MultiLineString { coordinates } => {
152 let mut acc = Weighted::default();
153 for line in coordinates {
154 acc.merge(line_centroid(line));
155 }
156 Some(acc)
157 }
158 Geometry::GeometryCollection { geometries } => collect(geometries, linear_centroid),
159 Geometry::Point { .. }
160 | Geometry::MultiPoint { .. }
161 | Geometry::Polygon { .. }
162 | Geometry::MultiPolygon { .. } => None,
163 _ => None,
166 }
167}
168
169fn punctual_centroid(geom: &Geometry) -> Option<Weighted> {
172 let mut acc = Weighted::default();
173 for point in vertices(geom) {
174 acc.add(point, 1.0);
175 }
176 Some(acc)
177}
178
179fn collect(geometries: &[Geometry], f: fn(&Geometry) -> Option<Weighted>) -> Option<Weighted> {
182 let mut acc = Weighted::default();
183 let mut any = false;
184 for member in geometries {
185 if let Some(part) = f(member) {
186 acc.merge(part);
187 any = true;
188 }
189 }
190 any.then_some(acc)
191}
192
193fn polygon_centroid(rings: &[Vec<[f64; 2]>]) -> Weighted {
196 let mut acc = Weighted::default();
197 let mut iter = rings.iter();
198 let Some(exterior) = iter.next() else {
199 return acc;
200 };
201 if let Some(point) = ring_centroid(exterior) {
202 acc.add(point, ring_area(exterior));
203 }
204 for hole in iter {
205 if let Some(point) = ring_centroid(hole) {
206 acc.add(point, -ring_area(hole));
207 }
208 }
209 if acc.weight <= 0.0 {
213 return Weighted::default();
214 }
215 acc
216}
217
218fn ring_centroid(ring: &[[f64; 2]]) -> Option<[f64; 2]> {
221 if ring.is_empty() {
222 return None;
223 }
224 let mut twice_area = 0.0;
225 let mut lng = 0.0;
226 let mut lat = 0.0;
227 for i in 0..ring.len() {
228 let a = ring[i];
229 let b = ring[(i + 1) % ring.len()];
230 let cross = a[0] * b[1] - b[0] * a[1];
231 twice_area += cross;
232 lng += (a[0] + b[0]) * cross;
233 lat += (a[1] + b[1]) * cross;
234 }
235 if twice_area.abs() < f64::EPSILON {
236 let n = ring.len() as f64;
237 return Some([
238 ring.iter().map(|c| c[0]).sum::<f64>() / n,
239 ring.iter().map(|c| c[1]).sum::<f64>() / n,
240 ]);
241 }
242 Some([lng / (3.0 * twice_area), lat / (3.0 * twice_area)])
243}
244
245fn line_centroid(coords: &[[f64; 2]]) -> Weighted {
248 let mut acc = Weighted::default();
249 for pair in coords.windows(2) {
250 let (a, b) = (pair[0], pair[1]);
251 let length = nodedb_types::geometry::haversine_distance(a[0], a[1], b[0], b[1]);
252 acc.add([(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0], length);
253 }
254 acc
255}
256
257fn vertices(geom: &Geometry) -> Vec<[f64; 2]> {
259 match geom {
260 Geometry::Point { coordinates } => vec![*coordinates],
261 Geometry::LineString { coordinates } | Geometry::MultiPoint { coordinates } => {
262 coordinates.clone()
263 }
264 Geometry::Polygon { coordinates } | Geometry::MultiLineString { coordinates } => {
265 coordinates.iter().flatten().copied().collect()
266 }
267 Geometry::MultiPolygon { coordinates } => {
268 coordinates.iter().flatten().flatten().copied().collect()
269 }
270 Geometry::GeometryCollection { geometries } => {
271 geometries.iter().flat_map(vertices).collect()
272 }
273 _ => Vec::new(),
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
288 fn area_of_unit_degree_cell_at_equator() {
289 let poly = Geometry::polygon(vec![vec![
290 [0.0, 0.0],
291 [1.0, 0.0],
292 [1.0, 1.0],
293 [0.0, 1.0],
294 [0.0, 0.0],
295 ]]);
296 let area = st_area(&poly);
297 let expected = 1.236e10;
298 assert!(
299 (area - expected).abs() / expected < 0.01,
300 "expected ~{expected} m², got {area}"
301 );
302 }
303
304 #[test]
306 fn area_is_orientation_independent() {
307 let ccw = Geometry::polygon(vec![vec![
308 [0.0, 0.0],
309 [1.0, 0.0],
310 [1.0, 1.0],
311 [0.0, 1.0],
312 [0.0, 0.0],
313 ]]);
314 let cw = Geometry::polygon(vec![vec![
315 [0.0, 0.0],
316 [0.0, 1.0],
317 [1.0, 1.0],
318 [1.0, 0.0],
319 [0.0, 0.0],
320 ]]);
321 assert!((st_area(&ccw) - st_area(&cw)).abs() < 1.0);
322 }
323
324 #[test]
325 fn area_subtracts_holes() {
326 let solid = Geometry::polygon(vec![vec![
327 [0.0, 0.0],
328 [4.0, 0.0],
329 [4.0, 4.0],
330 [0.0, 4.0],
331 [0.0, 0.0],
332 ]]);
333 let with_hole = Geometry::polygon(vec![
334 vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0], [0.0, 0.0]],
335 vec![[1.0, 1.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0], [1.0, 1.0]],
336 ]);
337 assert!(
338 st_area(&with_hole) < st_area(&solid),
339 "hole must reduce the measured area"
340 );
341 }
342
343 #[test]
344 fn area_of_non_areal_geometry_is_zero() {
345 let line = Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]]);
346 assert_eq!(st_area(&line), 0.0);
347 assert_eq!(st_area(&Geometry::point(1.0, 2.0)), 0.0);
348 }
349
350 #[test]
351 fn centroid_of_point_is_itself() {
352 let p = Geometry::point(10.0, 20.0);
353 assert_eq!(st_centroid(&p), Some(Geometry::point(10.0, 20.0)));
354 }
355
356 #[test]
357 fn centroid_of_square_is_its_center() {
358 let square = Geometry::polygon(vec![vec![
359 [0.0, 0.0],
360 [2.0, 0.0],
361 [2.0, 2.0],
362 [0.0, 2.0],
363 [0.0, 0.0],
364 ]]);
365 let Some(Geometry::Point { coordinates }) = st_centroid(&square) else {
366 panic!("expected a Point centroid");
367 };
368 assert!((coordinates[0] - 1.0).abs() < 1e-9);
369 assert!((coordinates[1] - 1.0).abs() < 1e-9);
370 }
371
372 #[test]
375 fn centroid_is_area_weighted_not_vertex_mean() {
376 let l_shape = Geometry::polygon(vec![vec![
377 [0.0, 0.0],
378 [4.0, 0.0],
379 [4.0, 1.0],
380 [1.0, 1.0],
381 [1.0, 4.0],
382 [0.0, 4.0],
383 [0.0, 0.0],
384 ]]);
385 let Some(Geometry::Point { coordinates }) = st_centroid(&l_shape) else {
386 panic!("expected a Point centroid");
387 };
388 let expected = 9.5 / 7.0;
392 assert!(
393 (coordinates[0] - expected).abs() < 0.01 && (coordinates[1] - expected).abs() < 0.01,
394 "expected ~({expected}, {expected}), got {coordinates:?}"
395 );
396 }
397
398 #[test]
401 fn centroid_of_collection_prefers_areal_components() {
402 let collection = Geometry::GeometryCollection {
403 geometries: vec![
404 Geometry::polygon(vec![vec![
405 [0.0, 0.0],
406 [2.0, 0.0],
407 [2.0, 2.0],
408 [0.0, 2.0],
409 [0.0, 0.0],
410 ]]),
411 Geometry::point(100.0, 100.0),
412 ],
413 };
414 let Some(Geometry::Point { coordinates }) = st_centroid(&collection) else {
415 panic!("expected a Point centroid");
416 };
417 assert!(
418 (coordinates[0] - 1.0).abs() < 1e-6,
419 "stray point must not pull the centroid, got {coordinates:?}"
420 );
421 }
422
423 #[test]
426 fn centroid_of_degenerate_polygon_falls_back() {
427 let collapsed =
428 Geometry::polygon(vec![vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [0.0, 0.0]]]);
429 let centroid = st_centroid(&collapsed);
430 assert!(
431 centroid.is_some(),
432 "a collinear ring must still yield a centroid"
433 );
434 }
435
436 #[test]
437 fn centroid_of_linestring_is_length_weighted() {
438 let line = Geometry::line_string(vec![[0.0, 0.0], [10.0, 0.0], [11.0, 0.0]]);
441 let Some(Geometry::Point { coordinates }) = st_centroid(&line) else {
442 panic!("expected a Point centroid");
443 };
444 assert!(
445 coordinates[0] < 6.0,
446 "length weighting must bias toward the long segment, got {coordinates:?}"
447 );
448 }
449
450 #[test]
451 fn centroid_of_empty_geometry_is_none() {
452 let empty = Geometry::GeometryCollection { geometries: vec![] };
453 assert_eq!(st_centroid(&empty), None);
454 }
455}