1#![doc = include_str!("../README.md")]
2
3pub mod surface;
4use std::collections::BTreeMap;
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use video_analysis_core::{DetectError, Result};
9
10pub type Properties = BTreeMap<String, Value>;
12
13pub type Position = [f64; 2];
15
16const GEOMETRY_EPSILON: f64 = 1e-12;
17
18fn invalid_argument(message: impl Into<String>) -> DetectError {
19 DetectError::InvalidArgument(message.into())
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
23pub struct Coordinate {
25 pub lon: f64,
27 pub lat: f64,
29}
30
31impl Coordinate {
32 pub fn new(lon: f64, lat: f64) -> Result<Self> {
34 let coordinate = Self { lon, lat };
35 coordinate.validate()?;
36 Ok(coordinate)
37 }
38
39 pub fn from_position(position: Position) -> Result<Self> {
41 Self::new(position[0], position[1])
42 }
43
44 pub fn as_position(self) -> Position {
46 [self.lon, self.lat]
47 }
48
49 pub fn validate(self) -> Result<()> {
51 if !self.lon.is_finite() || !self.lat.is_finite() {
52 return Err(invalid_argument("coordinate values must be finite"));
53 }
54 Ok(())
55 }
56
57 pub fn validate_geographic(self) -> Result<()> {
59 self.validate()?;
60 if !(-180.0..=180.0).contains(&self.lon) {
61 return Err(invalid_argument(
62 "coordinate longitude must be between -180 and 180",
63 ));
64 }
65 if !(-90.0..=90.0).contains(&self.lat) {
66 return Err(invalid_argument(
67 "coordinate latitude must be between -90 and 90",
68 ));
69 }
70 Ok(())
71 }
72}
73
74impl From<Coordinate> for Position {
75 fn from(value: Coordinate) -> Self {
76 value.as_position()
77 }
78}
79
80impl TryFrom<Position> for Coordinate {
81 type Error = DetectError;
82
83 fn try_from(value: Position) -> Result<Self> {
84 Self::from_position(value)
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
89pub struct BBox {
91 pub min_lon: f64,
93 pub min_lat: f64,
95 pub max_lon: f64,
97 pub max_lat: f64,
99}
100
101impl BBox {
102 pub fn new(values: [f64; 4]) -> Result<Self> {
104 let bbox = Self {
105 min_lon: values[0],
106 min_lat: values[1],
107 max_lon: values[2],
108 max_lat: values[3],
109 };
110 bbox.validate()?;
111 Ok(bbox)
112 }
113
114 pub fn as_array(self) -> [f64; 4] {
116 [self.min_lon, self.min_lat, self.max_lon, self.max_lat]
117 }
118
119 pub fn validate(self) -> Result<()> {
121 if !self.min_lon.is_finite()
122 || !self.min_lat.is_finite()
123 || !self.max_lon.is_finite()
124 || !self.max_lat.is_finite()
125 {
126 return Err(invalid_argument("bbox values must be finite"));
127 }
128 if self.min_lon > self.max_lon {
129 return Err(invalid_argument("bbox min_lon must be <= max_lon"));
130 }
131 if self.min_lat > self.max_lat {
132 return Err(invalid_argument("bbox min_lat must be <= max_lat"));
133 }
134 Ok(())
135 }
136
137 pub fn validate_geographic(self) -> Result<()> {
139 self.validate()?;
140 Coordinate::new(self.min_lon, self.min_lat)?.validate_geographic()?;
141 Coordinate::new(self.max_lon, self.max_lat)?.validate_geographic()?;
142 Ok(())
143 }
144
145 pub fn contains(self, coordinate: Coordinate) -> bool {
147 coordinate.lon >= self.min_lon
148 && coordinate.lon <= self.max_lon
149 && coordinate.lat >= self.min_lat
150 && coordinate.lat <= self.max_lat
151 }
152
153 pub fn intersects_coordinates(self, coordinates: &[Coordinate]) -> bool {
155 coordinates
156 .iter()
157 .copied()
158 .any(|coordinate| self.contains(coordinate))
159 }
160
161 pub fn intersects_geometry(self, geometry: &Geometry) -> bool {
163 geometry_intersects_bbox(geometry, self)
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
168#[serde(tag = "type")]
169pub enum Geometry {
171 Point {
173 coordinates: Position,
175 },
176 MultiPoint {
178 coordinates: Vec<Position>,
180 },
181 LineString {
183 coordinates: Vec<Position>,
185 },
186 MultiLineString {
188 coordinates: Vec<Vec<Position>>,
190 },
191 Polygon {
193 coordinates: Vec<Vec<Position>>,
195 },
196 MultiPolygon {
198 coordinates: Vec<Vec<Vec<Position>>>,
200 },
201 GeometryCollection {
203 geometries: Vec<Geometry>,
205 },
206}
207
208impl Geometry {
209 pub fn validate(&self) -> Result<()> {
211 match self {
212 Self::Point { coordinates } => {
213 Coordinate::from_position(*coordinates)?;
214 }
215 Self::MultiPoint { coordinates } => {
216 validate_positions(coordinates, "multipoint coordinates")?;
217 }
218 Self::LineString { coordinates } => {
219 validate_line_positions(coordinates, "linestring coordinates")?;
220 }
221 Self::MultiLineString { coordinates } => {
222 for line in coordinates {
223 validate_line_positions(line, "multilinestring coordinates")?;
224 }
225 }
226 Self::Polygon { coordinates } => {
227 validate_polygon_positions(coordinates, "polygon coordinates")?;
228 }
229 Self::MultiPolygon { coordinates } => {
230 for polygon in coordinates {
231 validate_polygon_positions(polygon, "multipolygon coordinates")?;
232 }
233 }
234 Self::GeometryCollection { geometries } => {
235 for geometry in geometries {
236 geometry.validate()?;
237 }
238 }
239 }
240 Ok(())
241 }
242}
243
244#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245pub struct GeoFeature {
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub id: Option<String>,
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub bbox: Option<BBox>,
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub geometry: Option<Geometry>,
256 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
258 pub properties: Properties,
259}
260
261impl GeoFeature {
262 pub fn new(geometry: Option<Geometry>) -> Self {
264 Self {
265 id: None,
266 bbox: None,
267 geometry,
268 properties: Properties::new(),
269 }
270 }
271
272 pub fn with_id(mut self, id: impl Into<String>) -> Self {
274 self.id = Some(id.into());
275 self
276 }
277
278 pub fn with_bbox(mut self, bbox: BBox) -> Result<Self> {
280 bbox.validate()?;
281 self.bbox = Some(bbox);
282 Ok(self)
283 }
284
285 pub fn insert_property(&mut self, key: impl Into<String>, value: impl Into<Value>) {
287 self.properties.insert(key.into(), value.into());
288 }
289
290 pub fn validate(&self) -> Result<()> {
292 if self.id.as_ref().is_some_and(String::is_empty) {
293 return Err(invalid_argument("feature id must not be empty"));
294 }
295 if let Some(bbox) = self.bbox {
296 bbox.validate()?;
297 }
298 if let Some(geometry) = &self.geometry {
299 geometry.validate()?;
300 }
301 Ok(())
302 }
303}
304
305#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
306pub struct GeoFeatureCollection {
308 #[serde(skip_serializing_if = "Option::is_none")]
310 pub bbox: Option<BBox>,
311 pub features: Vec<GeoFeature>,
313}
314
315impl GeoFeatureCollection {
316 pub fn new(features: impl Into<Vec<GeoFeature>>) -> Self {
318 Self {
319 bbox: None,
320 features: features.into(),
321 }
322 }
323
324 pub fn push(&mut self, feature: GeoFeature) {
326 self.features.push(feature);
327 }
328
329 pub fn filter_intersecting_bbox(&self, bbox: BBox) -> Self {
331 Self {
332 bbox: self.bbox,
333 features: self
334 .features
335 .iter()
336 .filter(|feature| {
337 feature
338 .geometry
339 .as_ref()
340 .is_some_and(|geometry| bbox.intersects_geometry(geometry))
341 })
342 .cloned()
343 .collect(),
344 }
345 }
346
347 pub fn validate(&self) -> Result<()> {
349 if let Some(bbox) = self.bbox {
350 bbox.validate()?;
351 }
352 for feature in &self.features {
353 feature.validate()?;
354 }
355 Ok(())
356 }
357}
358
359pub fn point(coordinate: Coordinate) -> Geometry {
361 Geometry::Point {
362 coordinates: coordinate.as_position(),
363 }
364}
365
366pub fn line_string(coordinates: &[Coordinate]) -> Result<Geometry> {
368 let coordinates = coordinates_to_positions(coordinates)?;
369 validate_line_positions(&coordinates, "linestring coordinates")?;
370 Ok(Geometry::LineString { coordinates })
371}
372
373pub fn polygon_or_multipolygon(polygons: Vec<Vec<Vec<Coordinate>>>) -> Option<Geometry> {
375 let mut output: Vec<Vec<Vec<Position>>> = polygons
376 .into_iter()
377 .map(|polygon| {
378 polygon
379 .into_iter()
380 .map(|ring| ring.into_iter().map(Coordinate::as_position).collect())
381 .collect()
382 })
383 .collect();
384
385 match output.len() {
386 0 => None,
387 1 => Some(Geometry::Polygon {
388 coordinates: output.remove(0),
389 }),
390 _ => Some(Geometry::MultiPolygon {
391 coordinates: output,
392 }),
393 }
394}
395
396pub fn assemble_multipolygon(
398 outer_segments: Vec<Vec<Coordinate>>,
399 inner_segments: Vec<Vec<Coordinate>>,
400) -> Result<Geometry> {
401 let mut outer_rings = stitch_rings(outer_segments)
402 .ok_or_else(|| invalid_argument("outer ring segments could not be stitched"))?;
403 let mut inner_rings = stitch_rings(inner_segments)
404 .ok_or_else(|| invalid_argument("inner ring segments could not be stitched"))?;
405
406 if outer_rings.is_empty() {
407 return Err(invalid_argument(
408 "multipolygon requires at least one outer ring",
409 ));
410 }
411
412 for ring in &mut outer_rings {
413 normalize_ring_orientation(ring, true);
414 }
415 for ring in &mut inner_rings {
416 normalize_ring_orientation(ring, false);
417 }
418
419 let mut polygons: Vec<Vec<Vec<Coordinate>>> =
420 outer_rings.into_iter().map(|outer| vec![outer]).collect();
421
422 for inner in inner_rings {
423 let Some(point) = inner.first().copied() else {
424 return Err(invalid_argument("inner ring must not be empty"));
425 };
426 let Some((target_index, _)) = polygons
427 .iter()
428 .enumerate()
429 .filter_map(|(index, polygon)| {
430 let outer = &polygon[0];
431 if point_in_ring(point, outer) {
432 Some((index, ring_area(outer).abs()))
433 } else {
434 None
435 }
436 })
437 .min_by(|(_, left), (_, right)| left.total_cmp(right))
438 else {
439 return Err(invalid_argument(
440 "inner ring is not contained by an outer ring",
441 ));
442 };
443 polygons[target_index].push(inner);
444 }
445
446 polygon_or_multipolygon(polygons)
447 .ok_or_else(|| invalid_argument("multipolygon requires at least one polygon"))
448}
449
450pub fn stitch_rings(mut segments: Vec<Vec<Coordinate>>) -> Option<Vec<Vec<Coordinate>>> {
452 let mut rings = Vec::new();
453
454 while !segments.is_empty() {
455 let mut ring = segments.remove(0);
456 if ring.len() < 2 {
457 return None;
458 }
459
460 loop {
461 if is_valid_closed_ring(&ring) {
462 rings.push(ring);
463 break;
464 }
465
466 let (index, action) = find_connecting_segment(&ring, &segments)?;
467 let segment = segments.remove(index);
468 apply_segment(&mut ring, segment, action);
469 }
470 }
471
472 Some(rings)
473}
474
475pub fn is_valid_closed_ring(ring: &[Coordinate]) -> bool {
477 ring.len() >= 4 && ring.first() == ring.last()
478}
479
480pub fn ring_area(ring: &[Coordinate]) -> f64 {
482 if ring.len() < 4 {
483 return 0.0;
484 }
485 ring.windows(2)
486 .map(|window| {
487 let a = window[0];
488 let b = window[1];
489 (a.lon * b.lat) - (b.lon * a.lat)
490 })
491 .sum::<f64>()
492 / 2.0
493}
494
495pub fn normalize_ring_orientation(ring: &mut [Coordinate], counter_clockwise: bool) {
497 let is_counter_clockwise = ring_area(ring) > 0.0;
498 if is_counter_clockwise != counter_clockwise {
499 ring.reverse();
500 }
501}
502
503pub fn point_in_ring(point: Coordinate, ring: &[Coordinate]) -> bool {
505 if ring.len() < 4 {
506 return false;
507 }
508
509 let mut inside = false;
510 let mut previous = ring[ring.len() - 1];
511 for current in ring.iter().copied() {
512 let intersects = ((current.lat > point.lat) != (previous.lat > point.lat))
513 && (point.lon
514 < (previous.lon - current.lon) * (point.lat - current.lat)
515 / (previous.lat - current.lat)
516 + current.lon);
517 if intersects {
518 inside = !inside;
519 }
520 previous = current;
521 }
522 inside
523}
524
525pub fn geometry_intersects_bbox(geometry: &Geometry, bbox: BBox) -> bool {
527 match geometry {
528 Geometry::Point { coordinates } => Coordinate::from_position(*coordinates)
529 .map(|coordinate| bbox.contains(coordinate))
530 .unwrap_or(false),
531 Geometry::MultiPoint { coordinates } => point_positions_intersect_bbox(coordinates, bbox),
532 Geometry::LineString { coordinates } => line_positions_intersect_bbox(coordinates, bbox),
533 Geometry::MultiLineString { coordinates } => coordinates
534 .iter()
535 .any(|line| line_positions_intersect_bbox(line, bbox)),
536 Geometry::Polygon { coordinates } => polygon_positions_intersect_bbox(coordinates, bbox),
537 Geometry::MultiPolygon { coordinates } => coordinates
538 .iter()
539 .any(|polygon| polygon_positions_intersect_bbox(polygon, bbox)),
540 Geometry::GeometryCollection { geometries } => geometries
541 .iter()
542 .any(|geometry| geometry_intersects_bbox(geometry, bbox)),
543 }
544}
545
546fn line_positions_intersect_bbox(coordinates: &[Position], bbox: BBox) -> bool {
547 if positions_intersect_bbox(coordinates, bbox) {
548 return true;
549 }
550 positions_to_coordinates(coordinates)
551 .map(|coordinates| line_segments_intersect_bbox(&coordinates, bbox))
552 .unwrap_or(false)
553}
554
555fn polygon_positions_intersect_bbox(polygon: &[Vec<Position>], bbox: BBox) -> bool {
556 let rings = polygon
557 .iter()
558 .map(|ring| positions_to_coordinates(ring))
559 .collect::<Result<Vec<_>>>();
560 let Ok(rings) = rings else {
561 return false;
562 };
563 if rings.iter().any(|ring| {
564 ring.iter()
565 .copied()
566 .any(|coordinate| bbox.contains(coordinate))
567 || line_segments_intersect_bbox(ring, bbox)
568 }) {
569 return true;
570 }
571 let Some(exterior) = rings.first() else {
572 return false;
573 };
574 bbox_corners(bbox).iter().copied().any(|corner| {
575 point_in_ring(corner, exterior)
576 && !rings
577 .iter()
578 .skip(1)
579 .any(|interior| point_in_ring(corner, interior))
580 })
581}
582
583fn line_segments_intersect_bbox(coordinates: &[Coordinate], bbox: BBox) -> bool {
584 coordinates
585 .windows(2)
586 .any(|segment| segment_intersects_bbox(segment[0], segment[1], bbox))
587}
588
589fn segment_intersects_bbox(start: Coordinate, end: Coordinate, bbox: BBox) -> bool {
590 if bbox.contains(start) || bbox.contains(end) {
591 return true;
592 }
593 if start.lon.max(end.lon) < bbox.min_lon
594 || start.lon.min(end.lon) > bbox.max_lon
595 || start.lat.max(end.lat) < bbox.min_lat
596 || start.lat.min(end.lat) > bbox.max_lat
597 {
598 return false;
599 }
600 let corners = bbox_corners(bbox);
601 let edges = [
602 (corners[0], corners[1]),
603 (corners[1], corners[2]),
604 (corners[2], corners[3]),
605 (corners[3], corners[0]),
606 ];
607 edges
608 .iter()
609 .any(|(edge_start, edge_end)| segments_intersect(start, end, *edge_start, *edge_end))
610}
611
612fn bbox_corners(bbox: BBox) -> [Coordinate; 4] {
613 [
614 Coordinate {
615 lon: bbox.min_lon,
616 lat: bbox.min_lat,
617 },
618 Coordinate {
619 lon: bbox.max_lon,
620 lat: bbox.min_lat,
621 },
622 Coordinate {
623 lon: bbox.max_lon,
624 lat: bbox.max_lat,
625 },
626 Coordinate {
627 lon: bbox.min_lon,
628 lat: bbox.max_lat,
629 },
630 ]
631}
632
633fn segments_intersect(
634 first_start: Coordinate,
635 first_end: Coordinate,
636 second_start: Coordinate,
637 second_end: Coordinate,
638) -> bool {
639 let o1 = orientation_sign(first_start, first_end, second_start);
640 let o2 = orientation_sign(first_start, first_end, second_end);
641 let o3 = orientation_sign(second_start, second_end, first_start);
642 let o4 = orientation_sign(second_start, second_end, first_end);
643
644 if o1 == 0 && coordinate_on_segment(second_start, first_start, first_end) {
645 return true;
646 }
647 if o2 == 0 && coordinate_on_segment(second_end, first_start, first_end) {
648 return true;
649 }
650 if o3 == 0 && coordinate_on_segment(first_start, second_start, second_end) {
651 return true;
652 }
653 if o4 == 0 && coordinate_on_segment(first_end, second_start, second_end) {
654 return true;
655 }
656
657 o1 != o2 && o3 != o4
658}
659
660fn orientation_sign(a: Coordinate, b: Coordinate, c: Coordinate) -> i8 {
661 let orientation = (b.lon - a.lon) * (c.lat - a.lat) - (b.lat - a.lat) * (c.lon - a.lon);
662 if orientation.abs() <= GEOMETRY_EPSILON {
663 0
664 } else if orientation > 0.0 {
665 1
666 } else {
667 -1
668 }
669}
670
671fn coordinate_on_segment(point: Coordinate, start: Coordinate, end: Coordinate) -> bool {
672 orientation_sign(start, end, point) == 0
673 && point.lon >= start.lon.min(end.lon) - GEOMETRY_EPSILON
674 && point.lon <= start.lon.max(end.lon) + GEOMETRY_EPSILON
675 && point.lat >= start.lat.min(end.lat) - GEOMETRY_EPSILON
676 && point.lat <= start.lat.max(end.lat) + GEOMETRY_EPSILON
677}
678
679pub fn map_geometry_coordinates<F>(geometry: &Geometry, mut transform: F) -> Result<Geometry>
681where
682 F: FnMut(Coordinate) -> Result<Coordinate>,
683{
684 map_geometry_coordinates_inner(geometry, &mut transform)
685}
686
687pub fn map_feature_coordinates<F>(feature: &GeoFeature, transform: F) -> Result<GeoFeature>
689where
690 F: FnMut(Coordinate) -> Result<Coordinate>,
691{
692 let geometry = match &feature.geometry {
693 Some(geometry) => Some(map_geometry_coordinates(geometry, transform)?),
694 None => None,
695 };
696
697 Ok(GeoFeature {
698 id: feature.id.clone(),
699 bbox: feature.bbox,
700 geometry,
701 properties: feature.properties.clone(),
702 })
703}
704
705pub fn translate_geometry(geometry: &Geometry, delta_lon: f64, delta_lat: f64) -> Result<Geometry> {
707 if !delta_lon.is_finite() || !delta_lat.is_finite() {
708 return Err(invalid_argument("translation delta values must be finite"));
709 }
710 map_geometry_coordinates(geometry, |coordinate| {
711 Coordinate::new(coordinate.lon + delta_lon, coordinate.lat + delta_lat)
712 })
713}
714
715pub fn simplify_geometry(geometry: &Geometry, tolerance: f64) -> Result<Geometry> {
717 validate_tolerance(tolerance)?;
718 match geometry {
719 Geometry::Point { .. } | Geometry::MultiPoint { .. } => Ok(geometry.clone()),
720 Geometry::LineString { coordinates } => Ok(Geometry::LineString {
721 coordinates: coordinates_to_positions(&simplify_line(
722 &positions_to_coordinates(coordinates)?,
723 tolerance,
724 )?)?,
725 }),
726 Geometry::MultiLineString { coordinates } => Ok(Geometry::MultiLineString {
727 coordinates: coordinates
728 .iter()
729 .map(|line| {
730 let simplified = simplify_line(&positions_to_coordinates(line)?, tolerance)?;
731 coordinates_to_positions(&simplified)
732 })
733 .collect::<Result<Vec<_>>>()?,
734 }),
735 Geometry::Polygon { coordinates } => Ok(Geometry::Polygon {
736 coordinates: simplify_polygon_positions(coordinates, tolerance)?,
737 }),
738 Geometry::MultiPolygon { coordinates } => Ok(Geometry::MultiPolygon {
739 coordinates: coordinates
740 .iter()
741 .map(|polygon| simplify_polygon_positions(polygon, tolerance))
742 .collect::<Result<Vec<_>>>()?,
743 }),
744 Geometry::GeometryCollection { geometries } => Ok(Geometry::GeometryCollection {
745 geometries: geometries
746 .iter()
747 .map(|geometry| simplify_geometry(geometry, tolerance))
748 .collect::<Result<Vec<_>>>()?,
749 }),
750 }
751}
752
753pub fn simplify_line(coordinates: &[Coordinate], tolerance: f64) -> Result<Vec<Coordinate>> {
755 validate_tolerance(tolerance)?;
756 validate_coordinate_slice(coordinates, 2, "line coordinates")?;
757
758 if coordinates.len() <= 2 || tolerance == 0.0 {
759 return Ok(coordinates.to_vec());
760 }
761
762 let mut keep = vec![false; coordinates.len()];
763 keep[0] = true;
764 keep[coordinates.len() - 1] = true;
765 simplify_line_range(coordinates, 0, coordinates.len() - 1, tolerance, &mut keep);
766
767 Ok(coordinates
768 .iter()
769 .copied()
770 .zip(keep)
771 .filter_map(|(coordinate, keep)| keep.then_some(coordinate))
772 .collect())
773}
774
775pub fn simplify_ring(ring: &[Coordinate], tolerance: f64) -> Result<Vec<Coordinate>> {
777 validate_tolerance(tolerance)?;
778 validate_ring(ring, "ring coordinates")?;
779
780 if ring.len() <= 5 || tolerance == 0.0 {
781 return Ok(ring.to_vec());
782 }
783
784 let mut open_ring = ring[..ring.len() - 1].to_vec();
785 let first = open_ring[0];
786 open_ring.push(first);
787 let mut simplified = simplify_line(&open_ring, tolerance)?;
788 simplified.pop();
789
790 if simplified.len() < 3 {
791 return Ok(ring.to_vec());
792 }
793
794 if simplified.last().copied() != Some(first) {
795 simplified.push(first);
796 }
797 Ok(simplified)
798}
799
800fn validate_positions(coordinates: &[Position], label: &str) -> Result<()> {
801 for coordinate in coordinates {
802 Coordinate::from_position(*coordinate)
803 .map_err(|_| invalid_argument(format!("{label} must be finite")))?;
804 }
805 Ok(())
806}
807
808fn validate_line_positions(coordinates: &[Position], label: &str) -> Result<()> {
809 if coordinates.len() < 2 {
810 return Err(invalid_argument(format!(
811 "{label} must contain at least two positions"
812 )));
813 }
814 validate_positions(coordinates, label)
815}
816
817fn validate_polygon_positions(coordinates: &[Vec<Position>], label: &str) -> Result<()> {
818 if coordinates.is_empty() {
819 return Err(invalid_argument(format!(
820 "{label} must contain at least one ring"
821 )));
822 }
823 for ring in coordinates {
824 let ring = positions_to_coordinates(ring)?;
825 validate_ring(&ring, label)?;
826 }
827 Ok(())
828}
829
830fn validate_coordinate_slice(
831 coordinates: &[Coordinate],
832 minimum: usize,
833 label: &str,
834) -> Result<()> {
835 if coordinates.len() < minimum {
836 return Err(invalid_argument(format!(
837 "{label} must contain at least {minimum} positions"
838 )));
839 }
840 for coordinate in coordinates {
841 coordinate.validate()?;
842 }
843 Ok(())
844}
845
846fn validate_ring(ring: &[Coordinate], label: &str) -> Result<()> {
847 validate_coordinate_slice(ring, 4, label)?;
848 if ring.first() != ring.last() {
849 return Err(invalid_argument(format!("{label} must be closed")));
850 }
851 Ok(())
852}
853
854fn validate_tolerance(tolerance: f64) -> Result<()> {
855 if tolerance < 0.0 || !tolerance.is_finite() {
856 return Err(invalid_argument(
857 "simplification tolerance must be finite and non-negative",
858 ));
859 }
860 Ok(())
861}
862
863fn positions_to_coordinates(positions: &[Position]) -> Result<Vec<Coordinate>> {
864 positions
865 .iter()
866 .copied()
867 .map(Coordinate::from_position)
868 .collect()
869}
870
871fn coordinates_to_positions(coordinates: &[Coordinate]) -> Result<Vec<Position>> {
872 coordinates
873 .iter()
874 .map(|coordinate| {
875 coordinate.validate()?;
876 Ok(coordinate.as_position())
877 })
878 .collect()
879}
880
881fn point_positions_intersect_bbox(coordinates: &[Position], bbox: BBox) -> bool {
882 coordinates
883 .iter()
884 .copied()
885 .filter_map(|position| Coordinate::from_position(position).ok())
886 .any(|coordinate| bbox.contains(coordinate))
887}
888
889fn positions_intersect_bbox(coordinates: &[Position], bbox: BBox) -> bool {
890 let coordinates = coordinates
891 .iter()
892 .copied()
893 .filter_map(|position| Coordinate::from_position(position).ok())
894 .collect::<Vec<_>>();
895 coordinates
896 .iter()
897 .copied()
898 .any(|coordinate| bbox.contains(coordinate))
899 || line_segments_intersect_bbox(&coordinates, bbox)
900}
901
902fn map_geometry_coordinates_inner(
903 geometry: &Geometry,
904 transform: &mut dyn FnMut(Coordinate) -> Result<Coordinate>,
905) -> Result<Geometry> {
906 match geometry {
907 Geometry::Point { coordinates } => Ok(Geometry::Point {
908 coordinates: transform(Coordinate::from_position(*coordinates)?)?.as_position(),
909 }),
910 Geometry::MultiPoint { coordinates } => Ok(Geometry::MultiPoint {
911 coordinates: map_positions(coordinates, transform)?,
912 }),
913 Geometry::LineString { coordinates } => Ok(Geometry::LineString {
914 coordinates: map_positions(coordinates, transform)?,
915 }),
916 Geometry::MultiLineString { coordinates } => Ok(Geometry::MultiLineString {
917 coordinates: coordinates
918 .iter()
919 .map(|line| map_positions(line, transform))
920 .collect::<Result<Vec<_>>>()?,
921 }),
922 Geometry::Polygon { coordinates } => Ok(Geometry::Polygon {
923 coordinates: coordinates
924 .iter()
925 .map(|ring| map_positions(ring, transform))
926 .collect::<Result<Vec<_>>>()?,
927 }),
928 Geometry::MultiPolygon { coordinates } => Ok(Geometry::MultiPolygon {
929 coordinates: coordinates
930 .iter()
931 .map(|polygon| {
932 polygon
933 .iter()
934 .map(|ring| map_positions(ring, transform))
935 .collect::<Result<Vec<_>>>()
936 })
937 .collect::<Result<Vec<_>>>()?,
938 }),
939 Geometry::GeometryCollection { geometries } => Ok(Geometry::GeometryCollection {
940 geometries: geometries
941 .iter()
942 .map(|geometry| map_geometry_coordinates_inner(geometry, transform))
943 .collect::<Result<Vec<_>>>()?,
944 }),
945 }
946}
947
948fn map_positions(
949 positions: &[Position],
950 transform: &mut dyn FnMut(Coordinate) -> Result<Coordinate>,
951) -> Result<Vec<Position>> {
952 positions
953 .iter()
954 .copied()
955 .map(|position| {
956 transform(Coordinate::from_position(position)?).map(Coordinate::as_position)
957 })
958 .collect()
959}
960
961fn simplify_polygon_positions(
962 polygon: &[Vec<Position>],
963 tolerance: f64,
964) -> Result<Vec<Vec<Position>>> {
965 polygon
966 .iter()
967 .map(|ring| {
968 let simplified = simplify_ring(&positions_to_coordinates(ring)?, tolerance)?;
969 coordinates_to_positions(&simplified)
970 })
971 .collect()
972}
973
974fn simplify_line_range(
975 coordinates: &[Coordinate],
976 start: usize,
977 end: usize,
978 tolerance: f64,
979 keep: &mut [bool],
980) {
981 if end <= start + 1 {
982 return;
983 }
984
985 let mut farthest_index = start + 1;
986 let mut farthest_distance = 0.0;
987 for index in start + 1..end {
988 let distance =
989 perpendicular_distance(coordinates[index], coordinates[start], coordinates[end]);
990 if distance > farthest_distance {
991 farthest_distance = distance;
992 farthest_index = index;
993 }
994 }
995
996 if farthest_distance > tolerance {
997 keep[farthest_index] = true;
998 simplify_line_range(coordinates, start, farthest_index, tolerance, keep);
999 simplify_line_range(coordinates, farthest_index, end, tolerance, keep);
1000 }
1001}
1002
1003fn perpendicular_distance(point: Coordinate, start: Coordinate, end: Coordinate) -> f64 {
1004 let dx = end.lon - start.lon;
1005 let dy = end.lat - start.lat;
1006 if dx == 0.0 && dy == 0.0 {
1007 return (point.lon - start.lon).hypot(point.lat - start.lat);
1008 }
1009 ((dy * point.lon - dx * point.lat + end.lon * start.lat - end.lat * start.lon).abs())
1010 / dx.hypot(dy)
1011}
1012
1013#[derive(Debug, Clone, Copy)]
1014enum StitchAction {
1015 AppendForward,
1016 AppendReverse,
1017 PrependForward,
1018 PrependReverse,
1019}
1020
1021fn find_connecting_segment(
1022 ring: &[Coordinate],
1023 segments: &[Vec<Coordinate>],
1024) -> Option<(usize, StitchAction)> {
1025 let first = *ring.first()?;
1026 let last = *ring.last()?;
1027 segments.iter().enumerate().find_map(|(index, segment)| {
1028 let segment_first = *segment.first()?;
1029 let segment_last = *segment.last()?;
1030 if last == segment_first {
1031 Some((index, StitchAction::AppendForward))
1032 } else if last == segment_last {
1033 Some((index, StitchAction::AppendReverse))
1034 } else if first == segment_last {
1035 Some((index, StitchAction::PrependForward))
1036 } else if first == segment_first {
1037 Some((index, StitchAction::PrependReverse))
1038 } else {
1039 None
1040 }
1041 })
1042}
1043
1044fn apply_segment(ring: &mut Vec<Coordinate>, mut segment: Vec<Coordinate>, action: StitchAction) {
1045 match action {
1046 StitchAction::AppendForward => ring.extend(segment.into_iter().skip(1)),
1047 StitchAction::AppendReverse => {
1048 segment.reverse();
1049 ring.extend(segment.into_iter().skip(1));
1050 }
1051 StitchAction::PrependForward => {
1052 segment.pop();
1053 segment.extend(ring.iter().copied());
1054 *ring = segment;
1055 }
1056 StitchAction::PrependReverse => {
1057 segment.reverse();
1058 segment.pop();
1059 segment.extend(ring.iter().copied());
1060 *ring = segment;
1061 }
1062 }
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067 use super::*;
1068
1069 fn coord(lon: f64, lat: f64) -> Coordinate {
1070 Coordinate::new(lon, lat).unwrap()
1071 }
1072
1073 #[test]
1074 fn bbox_contains_coordinate() {
1075 let bbox = BBox::new([8.5, 48.8, 9.3, 49.2]).unwrap();
1076
1077 assert!(bbox.contains(coord(8.7, 48.9)));
1078 assert!(!bbox.contains(coord(10.0, 48.9)));
1079 }
1080
1081 #[test]
1082 fn bbox_intersects_crossing_line_segments() {
1083 let bbox = BBox::new([0.0, 0.0, 1.0, 1.0]).unwrap();
1084 let geometry = Geometry::LineString {
1085 coordinates: vec![[-1.0, 0.5], [2.0, 0.5]],
1086 };
1087
1088 assert!(bbox.intersects_geometry(&geometry));
1089 }
1090
1091 #[test]
1092 fn segment_intersection_handles_crossing_touching_overlap_and_separated() {
1093 assert!(segments_intersect(
1094 coord(0.0, 0.0),
1095 coord(2.0, 2.0),
1096 coord(0.0, 2.0),
1097 coord(2.0, 0.0),
1098 ));
1099 assert!(segments_intersect(
1100 coord(0.0, 0.0),
1101 coord(1.0, 1.0),
1102 coord(1.0, 1.0),
1103 coord(2.0, 0.0),
1104 ));
1105 assert!(segments_intersect(
1106 coord(0.0, 0.0),
1107 coord(2.0, 0.0),
1108 coord(1.0, 0.0),
1109 coord(3.0, 0.0),
1110 ));
1111 assert!(!segments_intersect(
1112 coord(0.0, 0.0),
1113 coord(1.0, 0.0),
1114 coord(2.0, 0.0),
1115 coord(3.0, 0.0),
1116 ));
1117 }
1118
1119 #[test]
1120 fn coordinate_on_segment_handles_endpoint_interior_and_off_segment() {
1121 let start = coord(0.0, 0.0);
1122 let end = coord(2.0, 2.0);
1123
1124 assert!(coordinate_on_segment(start, start, end));
1125 assert!(coordinate_on_segment(coord(1.0, 1.0), start, end));
1126 assert!(!coordinate_on_segment(coord(1.0, 1.1), start, end));
1127 assert!(!coordinate_on_segment(coord(3.0, 3.0), start, end));
1128 }
1129
1130 #[test]
1131 fn positions_intersect_bbox_detects_crossing_without_inside_vertices() {
1132 let bbox = BBox::new([0.0, 0.0, 1.0, 1.0]).unwrap();
1133
1134 assert!(positions_intersect_bbox(&[[-1.0, 0.5], [2.0, 0.5]], bbox));
1135 }
1136
1137 #[test]
1138 fn bbox_intersects_polygon_that_contains_viewport() {
1139 let bbox = BBox::new([0.0, 0.0, 1.0, 1.0]).unwrap();
1140 let geometry = Geometry::Polygon {
1141 coordinates: vec![vec![
1142 [-1.0, -1.0],
1143 [2.0, -1.0],
1144 [2.0, 2.0],
1145 [-1.0, 2.0],
1146 [-1.0, -1.0],
1147 ]],
1148 };
1149
1150 assert!(bbox.intersects_geometry(&geometry));
1151 }
1152
1153 #[test]
1154 fn bbox_inside_polygon_hole_does_not_intersect() {
1155 let bbox = BBox::new([0.25, 0.25, 0.75, 0.75]).unwrap();
1156 let geometry = Geometry::Polygon {
1157 coordinates: vec![
1158 vec![
1159 [-1.0, -1.0],
1160 [2.0, -1.0],
1161 [2.0, 2.0],
1162 [-1.0, 2.0],
1163 [-1.0, -1.0],
1164 ],
1165 vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]],
1166 ],
1167 };
1168
1169 assert!(!bbox.intersects_geometry(&geometry));
1170 }
1171
1172 #[test]
1173 fn normalizes_ring_orientation() {
1174 let mut ring = vec![
1175 coord(0.0, 0.0),
1176 coord(0.0, 1.0),
1177 coord(1.0, 1.0),
1178 coord(1.0, 0.0),
1179 coord(0.0, 0.0),
1180 ];
1181
1182 normalize_ring_orientation(&mut ring, true);
1183 assert!(ring_area(&ring) > 0.0);
1184 normalize_ring_orientation(&mut ring, false);
1185 assert!(ring_area(&ring) < 0.0);
1186 }
1187
1188 #[test]
1189 fn point_in_ring_detects_inside_and_outside() {
1190 let ring = vec![
1191 coord(0.0, 0.0),
1192 coord(1.0, 0.0),
1193 coord(1.0, 1.0),
1194 coord(0.0, 1.0),
1195 coord(0.0, 0.0),
1196 ];
1197
1198 assert!(point_in_ring(coord(0.5, 0.5), &ring));
1199 assert!(!point_in_ring(coord(2.0, 0.5), &ring));
1200 }
1201
1202 #[test]
1203 fn stitches_reversed_segments_into_ring() {
1204 let segments = vec![
1205 vec![coord(0.0, 0.0), coord(1.0, 0.0), coord(1.0, 1.0)],
1206 vec![coord(0.0, 0.0), coord(0.0, 1.0), coord(1.0, 1.0)],
1207 ];
1208
1209 let rings = stitch_rings(segments).unwrap();
1210
1211 assert_eq!(rings.len(), 1);
1212 assert!(is_valid_closed_ring(&rings[0]));
1213 }
1214
1215 #[test]
1216 fn assemble_multipolygon_assigns_inner_ring() {
1217 let outer = vec![
1218 coord(0.0, 0.0),
1219 coord(4.0, 0.0),
1220 coord(4.0, 4.0),
1221 coord(0.0, 4.0),
1222 coord(0.0, 0.0),
1223 ];
1224 let inner = vec![
1225 coord(1.0, 1.0),
1226 coord(2.0, 1.0),
1227 coord(2.0, 2.0),
1228 coord(1.0, 2.0),
1229 coord(1.0, 1.0),
1230 ];
1231
1232 let geometry = assemble_multipolygon(vec![outer], vec![inner]).unwrap();
1233
1234 let Geometry::Polygon { coordinates } = geometry else {
1235 panic!("expected polygon");
1236 };
1237 assert_eq!(coordinates.len(), 2);
1238 }
1239
1240 #[test]
1241 fn maps_geometry_coordinates() {
1242 let geometry = point(coord(1.0, 2.0));
1243
1244 let translated = translate_geometry(&geometry, 3.0, 4.0).unwrap();
1245
1246 assert_eq!(
1247 translated,
1248 Geometry::Point {
1249 coordinates: [4.0, 6.0]
1250 }
1251 );
1252 }
1253
1254 #[test]
1255 fn simplifies_line_coordinates() {
1256 let line = vec![
1257 coord(0.0, 0.0),
1258 coord(1.0, 0.01),
1259 coord(2.0, -0.01),
1260 coord(3.0, 0.0),
1261 ];
1262
1263 let simplified = simplify_line(&line, 0.1).unwrap();
1264
1265 assert_eq!(simplified, vec![coord(0.0, 0.0), coord(3.0, 0.0)]);
1266 }
1267
1268 #[test]
1269 fn simplify_ring_preserves_valid_closure() {
1270 let ring = vec![
1271 coord(0.0, 0.0),
1272 coord(1.0, 0.01),
1273 coord(2.0, 0.0),
1274 coord(2.0, 2.0),
1275 coord(0.0, 2.0),
1276 coord(0.0, 0.0),
1277 ];
1278
1279 let simplified = simplify_ring(&ring, 0.1).unwrap();
1280
1281 assert!(is_valid_closed_ring(&simplified));
1282 assert_eq!(simplified.first(), simplified.last());
1283 }
1284
1285 #[test]
1286 fn feature_properties_are_generic_json_values() {
1287 let mut feature = GeoFeature::new(Some(point(coord(8.7, 48.9)))).with_id("node/123");
1288 feature.insert_property("name", "Test");
1289
1290 assert_eq!(feature.id.as_deref(), Some("node/123"));
1291 assert_eq!(feature.properties["name"], Value::from("Test"));
1292 }
1293}