oxigdal_algorithms/vector/
centroid.rs1use crate::error::{AlgorithmError, Result};
34use oxigdal_core::vector::{
35 Coordinate, Geometry, GeometryCollection, LineString, MultiLineString, MultiPoint,
36 MultiPolygon, Point, Polygon,
37};
38
39pub fn centroid(geometry: &Geometry) -> Result<Point> {
53 match geometry {
54 Geometry::Point(p) => Ok(p.clone()),
55 Geometry::LineString(ls) => centroid_linestring(ls),
56 Geometry::Polygon(p) => centroid_polygon(p),
57 Geometry::MultiPoint(mp) => centroid_multipoint(mp),
58 Geometry::MultiLineString(mls) => centroid_multilinestring(mls),
59 Geometry::MultiPolygon(mp) => centroid_multipolygon(mp),
60 Geometry::GeometryCollection(gc) => centroid_collection(gc),
61 }
62}
63
64pub fn centroid_point(point: &Point) -> Point {
74 point.clone()
75}
76
77pub fn centroid_linestring(linestring: &LineString) -> Result<Point> {
94 if linestring.coords.is_empty() {
95 return Err(AlgorithmError::EmptyInput {
96 operation: "centroid_linestring",
97 });
98 }
99
100 if linestring.coords.len() == 1 {
101 return Ok(Point::from_coord(linestring.coords[0]));
102 }
103
104 let mut total_length = 0.0;
105 let mut weighted_x = 0.0;
106 let mut weighted_y = 0.0;
107 let mut weighted_z = 0.0;
108 let has_z = linestring.coords[0].has_z();
109
110 for i in 0..linestring.coords.len() - 1 {
111 let p1 = &linestring.coords[i];
112 let p2 = &linestring.coords[i + 1];
113
114 let dx = p2.x - p1.x;
115 let dy = p2.y - p1.y;
116 let dz = if has_z {
117 p2.z.unwrap_or(0.0) - p1.z.unwrap_or(0.0)
118 } else {
119 0.0
120 };
121
122 let length = (dx * dx + dy * dy + dz * dz).sqrt();
123
124 if length > 0.0 {
125 let mid_x = (p1.x + p2.x) / 2.0;
127 let mid_y = (p1.y + p2.y) / 2.0;
128
129 weighted_x += mid_x * length;
130 weighted_y += mid_y * length;
131
132 if has_z {
133 let mid_z = (p1.z.unwrap_or(0.0) + p2.z.unwrap_or(0.0)) / 2.0;
134 weighted_z += mid_z * length;
135 }
136
137 total_length += length;
138 }
139 }
140
141 if total_length == 0.0 {
142 return Ok(Point::from_coord(linestring.coords[0]));
144 }
145
146 let centroid_coord = if has_z {
147 Coordinate::new_3d(
148 weighted_x / total_length,
149 weighted_y / total_length,
150 weighted_z / total_length,
151 )
152 } else {
153 Coordinate::new_2d(weighted_x / total_length, weighted_y / total_length)
154 };
155
156 Ok(Point::from_coord(centroid_coord))
157}
158
159pub fn centroid_polygon(polygon: &Polygon) -> Result<Point> {
176 if polygon.exterior.coords.len() < 3 {
177 return Err(AlgorithmError::InsufficientData {
178 operation: "centroid_polygon",
179 message: "polygon must have at least 3 coordinates".to_string(),
180 });
181 }
182
183 let (ext_area, ext_cx, ext_cy) = ring_centroid(&polygon.exterior.coords)?;
185
186 if ext_area.abs() < f64::EPSILON {
187 return Err(AlgorithmError::GeometryError {
188 message: "polygon has zero area".to_string(),
189 });
190 }
191
192 let mut total_area = ext_area;
193 let mut weighted_x = ext_cx * ext_area;
194 let mut weighted_y = ext_cy * ext_area;
195
196 for hole in &polygon.interiors {
198 let (hole_area, hole_cx, hole_cy) = ring_centroid(&hole.coords)?;
199
200 total_area -= hole_area;
201 weighted_x -= hole_cx * hole_area;
202 weighted_y -= hole_cy * hole_area;
203 }
204
205 if total_area.abs() < f64::EPSILON {
206 return Err(AlgorithmError::GeometryError {
207 message: "polygon has zero effective area after holes".to_string(),
208 });
209 }
210
211 let centroid_coord = Coordinate::new_2d(weighted_x / total_area, weighted_y / total_area);
212
213 Ok(Point::from_coord(centroid_coord))
214}
215
216pub fn centroid_multipoint(multipoint: &MultiPoint) -> Result<Point> {
230 if multipoint.points.is_empty() {
231 return Err(AlgorithmError::EmptyInput {
232 operation: "centroid_multipoint",
233 });
234 }
235
236 let mut sum_x = 0.0;
237 let mut sum_y = 0.0;
238 let mut sum_z = 0.0;
239 let has_z = multipoint.points[0].coord.has_z();
240
241 for point in &multipoint.points {
242 sum_x += point.coord.x;
243 sum_y += point.coord.y;
244
245 if has_z {
246 sum_z += point.coord.z.unwrap_or(0.0);
247 }
248 }
249
250 let n = multipoint.points.len() as f64;
251 let centroid_coord = if has_z {
252 Coordinate::new_3d(sum_x / n, sum_y / n, sum_z / n)
253 } else {
254 Coordinate::new_2d(sum_x / n, sum_y / n)
255 };
256
257 Ok(Point::from_coord(centroid_coord))
258}
259
260pub fn centroid_multilinestring(multilinestring: &MultiLineString) -> Result<Point> {
274 if multilinestring.line_strings.is_empty() {
275 return Err(AlgorithmError::EmptyInput {
276 operation: "centroid_multilinestring",
277 });
278 }
279
280 let mut total_length = 0.0;
281 let mut weighted_x = 0.0;
282 let mut weighted_y = 0.0;
283
284 for linestring in &multilinestring.line_strings {
285 let ls_centroid = centroid_linestring(linestring)?;
286 let length = linestring_length(linestring);
287
288 weighted_x += ls_centroid.coord.x * length;
289 weighted_y += ls_centroid.coord.y * length;
290 total_length += length;
291 }
292
293 if total_length == 0.0 {
294 return Err(AlgorithmError::GeometryError {
295 message: "multilinestring has zero total length".to_string(),
296 });
297 }
298
299 let centroid_coord = Coordinate::new_2d(weighted_x / total_length, weighted_y / total_length);
300
301 Ok(Point::from_coord(centroid_coord))
302}
303
304pub fn centroid_multipolygon(multipolygon: &MultiPolygon) -> Result<Point> {
318 if multipolygon.polygons.is_empty() {
319 return Err(AlgorithmError::EmptyInput {
320 operation: "centroid_multipolygon",
321 });
322 }
323
324 let mut total_area = 0.0;
325 let mut weighted_x = 0.0;
326 let mut weighted_y = 0.0;
327
328 for polygon in &multipolygon.polygons {
329 let poly_centroid = centroid_polygon(polygon)?;
330 let area = polygon_area(polygon);
331
332 weighted_x += poly_centroid.coord.x * area;
333 weighted_y += poly_centroid.coord.y * area;
334 total_area += area;
335 }
336
337 if total_area.abs() < f64::EPSILON {
338 return Err(AlgorithmError::GeometryError {
339 message: "multipolygon has zero total area".to_string(),
340 });
341 }
342
343 let centroid_coord = Coordinate::new_2d(weighted_x / total_area, weighted_y / total_area);
344
345 Ok(Point::from_coord(centroid_coord))
346}
347
348pub fn centroid_collection(collection: &GeometryCollection) -> Result<Point> {
362 if collection.geometries.is_empty() {
363 return Err(AlgorithmError::EmptyInput {
364 operation: "centroid_collection",
365 });
366 }
367
368 let mut total_weight = 0.0;
370 let mut weighted_x = 0.0;
371 let mut weighted_y = 0.0;
372
373 for geometry in &collection.geometries {
374 let geom_centroid = centroid(geometry)?;
375 let weight = geometry_weight(geometry);
376
377 weighted_x += geom_centroid.coord.x * weight;
378 weighted_y += geom_centroid.coord.y * weight;
379 total_weight += weight;
380 }
381
382 if total_weight == 0.0 {
383 return Err(AlgorithmError::GeometryError {
384 message: "collection has zero total weight".to_string(),
385 });
386 }
387
388 let centroid_coord = Coordinate::new_2d(weighted_x / total_weight, weighted_y / total_weight);
389
390 Ok(Point::from_coord(centroid_coord))
391}
392
393fn ring_centroid(coords: &[Coordinate]) -> Result<(f64, f64, f64)> {
395 if coords.len() < 3 {
396 return Err(AlgorithmError::InsufficientData {
397 operation: "ring_centroid",
398 message: "ring must have at least 3 coordinates".to_string(),
399 });
400 }
401
402 let mut area = 0.0;
403 let mut cx = 0.0;
404 let mut cy = 0.0;
405
406 let n = coords.len();
407 for i in 0..n {
408 let j = (i + 1) % n;
409 let cross = coords[i].x * coords[j].y - coords[j].x * coords[i].y;
410
411 area += cross;
412 cx += (coords[i].x + coords[j].x) * cross;
413 cy += (coords[i].y + coords[j].y) * cross;
414 }
415
416 area /= 2.0;
417
418 if area.abs() < f64::EPSILON {
419 let mut sum_x = 0.0;
421 let mut sum_y = 0.0;
422 for coord in coords {
423 sum_x += coord.x;
424 sum_y += coord.y;
425 }
426 let n = coords.len() as f64;
427 return Ok((0.0, sum_x / n, sum_y / n));
428 }
429
430 cx /= 6.0 * area;
431 cy /= 6.0 * area;
432
433 Ok((area, cx, cy))
434}
435
436fn linestring_length(linestring: &LineString) -> f64 {
438 let mut length = 0.0;
439
440 for i in 0..linestring.coords.len().saturating_sub(1) {
441 let p1 = &linestring.coords[i];
442 let p2 = &linestring.coords[i + 1];
443
444 let dx = p2.x - p1.x;
445 let dy = p2.y - p1.y;
446
447 length += (dx * dx + dy * dy).sqrt();
448 }
449
450 length
451}
452
453fn polygon_area(polygon: &Polygon) -> f64 {
455 let mut area = ring_area(&polygon.exterior.coords);
456
457 for hole in &polygon.interiors {
458 area -= ring_area(&hole.coords);
459 }
460
461 area.abs()
462}
463
464fn ring_area(coords: &[Coordinate]) -> f64 {
466 if coords.len() < 3 {
467 return 0.0;
468 }
469
470 let mut area = 0.0;
471 let n = coords.len();
472
473 for i in 0..n {
474 let j = (i + 1) % n;
475 area += coords[i].x * coords[j].y;
476 area -= coords[j].x * coords[i].y;
477 }
478
479 area / 2.0
480}
481
482fn geometry_weight(geometry: &Geometry) -> f64 {
484 match geometry {
485 Geometry::Point(_) => 1.0,
486 Geometry::LineString(ls) => linestring_length(ls).max(1.0),
487 Geometry::Polygon(p) => polygon_area(p).max(1.0),
488 Geometry::MultiPoint(mp) => mp.points.len() as f64,
489 Geometry::MultiLineString(mls) => mls
490 .line_strings
491 .iter()
492 .map(linestring_length)
493 .sum::<f64>()
494 .max(1.0),
495 Geometry::MultiPolygon(mp) => mp.polygons.iter().map(polygon_area).sum::<f64>().max(1.0),
496 Geometry::GeometryCollection(gc) => gc
497 .geometries
498 .iter()
499 .map(geometry_weight)
500 .sum::<f64>()
501 .max(1.0),
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508
509 fn create_square() -> Result<Polygon> {
510 let coords = vec![
511 Coordinate::new_2d(0.0, 0.0),
512 Coordinate::new_2d(4.0, 0.0),
513 Coordinate::new_2d(4.0, 4.0),
514 Coordinate::new_2d(0.0, 4.0),
515 Coordinate::new_2d(0.0, 0.0),
516 ];
517 let exterior = LineString::new(coords).map_err(|e| AlgorithmError::Core(e))?;
518 Polygon::new(exterior, vec![]).map_err(|e| AlgorithmError::Core(e))
519 }
520
521 #[test]
522 fn test_centroid_point() {
523 let point = Point::new(3.0, 5.0);
524 let result = centroid_point(&point);
525
526 assert_eq!(result.coord.x, 3.0);
527 assert_eq!(result.coord.y, 5.0);
528 }
529
530 #[test]
531 fn test_centroid_linestring() {
532 let coords = vec![
533 Coordinate::new_2d(0.0, 0.0),
534 Coordinate::new_2d(4.0, 0.0),
535 Coordinate::new_2d(4.0, 4.0),
536 ];
537 let line = LineString::new(coords);
538 assert!(line.is_ok());
539
540 if let Ok(l) = line {
541 let result = centroid_linestring(&l);
542 assert!(result.is_ok());
543
544 if let Ok(centroid) = result {
545 assert!(centroid.coord.x >= 0.0 && centroid.coord.x <= 4.0);
547 assert!(centroid.coord.y >= 0.0 && centroid.coord.y <= 4.0);
548 }
549 }
550 }
551
552 #[test]
553 fn test_centroid_polygon_square() {
554 let poly = create_square();
555 assert!(poly.is_ok());
556
557 if let Ok(p) = poly {
558 let result = centroid_polygon(&p);
559 assert!(result.is_ok());
560
561 if let Ok(centroid) = result {
562 assert!((centroid.coord.x - 2.0).abs() < 1e-10);
564 assert!((centroid.coord.y - 2.0).abs() < 1e-10);
565 }
566 }
567 }
568
569 #[test]
570 fn test_centroid_polygon_with_hole() {
571 let exterior_coords = vec![
572 Coordinate::new_2d(0.0, 0.0),
573 Coordinate::new_2d(10.0, 0.0),
574 Coordinate::new_2d(10.0, 10.0),
575 Coordinate::new_2d(0.0, 10.0),
576 Coordinate::new_2d(0.0, 0.0),
577 ];
578 let hole_coords = vec![
579 Coordinate::new_2d(2.0, 2.0),
580 Coordinate::new_2d(8.0, 2.0),
581 Coordinate::new_2d(8.0, 8.0),
582 Coordinate::new_2d(2.0, 8.0),
583 Coordinate::new_2d(2.0, 2.0),
584 ];
585
586 let exterior = LineString::new(exterior_coords);
587 let hole = LineString::new(hole_coords);
588
589 assert!(exterior.is_ok() && hole.is_ok());
590
591 if let (Ok(ext), Ok(h)) = (exterior, hole) {
592 let poly = Polygon::new(ext, vec![h]);
593 assert!(poly.is_ok());
594
595 if let Ok(p) = poly {
596 let result = centroid_polygon(&p);
597 assert!(result.is_ok());
598
599 if let Ok(centroid) = result {
600 assert!(centroid.coord.x >= 0.0 && centroid.coord.x <= 10.0);
602 assert!(centroid.coord.y >= 0.0 && centroid.coord.y <= 10.0);
603 }
604 }
605 }
606 }
607
608 #[test]
609 fn test_centroid_multipoint() {
610 let points = vec![
611 Point::new(0.0, 0.0),
612 Point::new(4.0, 0.0),
613 Point::new(4.0, 4.0),
614 Point::new(0.0, 4.0),
615 ];
616 let mp = MultiPoint::new(points);
617
618 let result = centroid_multipoint(&mp);
619 assert!(result.is_ok());
620
621 if let Ok(centroid) = result {
622 assert!((centroid.coord.x - 2.0).abs() < 1e-10);
624 assert!((centroid.coord.y - 2.0).abs() < 1e-10);
625 }
626 }
627
628 #[test]
629 fn test_centroid_empty_multipoint() {
630 let mp = MultiPoint::empty();
631 let result = centroid_multipoint(&mp);
632 assert!(result.is_err());
633 }
634
635 #[test]
636 fn test_linestring_length() {
637 let coords = vec![
638 Coordinate::new_2d(0.0, 0.0),
639 Coordinate::new_2d(3.0, 0.0),
640 Coordinate::new_2d(3.0, 4.0),
641 ];
642 let line = LineString::new(coords);
643 assert!(line.is_ok());
644
645 if let Ok(l) = line {
646 let length = linestring_length(&l);
647 assert!((length - 7.0).abs() < 1e-10); }
649 }
650
651 #[test]
652 fn test_polygon_area() {
653 let poly = create_square();
654 assert!(poly.is_ok());
655
656 if let Ok(p) = poly {
657 let area = polygon_area(&p);
658 assert!((area - 16.0).abs() < 1e-10); }
660 }
661
662 #[test]
663 fn test_ring_centroid() {
664 let coords = vec![
665 Coordinate::new_2d(0.0, 0.0),
666 Coordinate::new_2d(4.0, 0.0),
667 Coordinate::new_2d(4.0, 4.0),
668 Coordinate::new_2d(0.0, 4.0),
669 Coordinate::new_2d(0.0, 0.0),
670 ];
671
672 let result = ring_centroid(&coords);
673 assert!(result.is_ok());
674
675 if let Ok((area, cx, cy)) = result {
676 assert!((area - 16.0).abs() < 1e-10);
677 assert!((cx - 2.0).abs() < 1e-10);
678 assert!((cy - 2.0).abs() < 1e-10);
679 }
680 }
681}