Skip to content

Commit 271207e

Browse files
committed
Robust Winding Order
Compute Winding Order using robust predicates. + derive `PartialOrd` for `Coordinate` + add `is_closed` utility to `LineString` + add `robust-0.2.2` crate + add `RobustWindingOrder` trait + impl `RobustWindingOrder` for `LineString<T: Float>` + add tests (copied from `WindingOrder`)
1 parent f6a5b0a commit 271207e

File tree

5 files changed

+189
-5
lines changed

5 files changed

+189
-5
lines changed

geo-types/src/coordinate.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use approx::{AbsDiffEq, RelativeEq, UlpsEq};
99
/// as an envelope, a precision model, and spatial reference system
1010
/// information), a `Coordinate` only contains ordinate values and accessor
1111
/// methods.
12-
#[derive(Eq, PartialEq, Clone, Copy, Debug, Hash)]
12+
#[derive(Eq, PartialEq, PartialOrd, Clone, Copy, Debug, Hash)]
1313
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1414
pub struct Coordinate<T>
1515
where

geo-types/src/line_string.rs

+25-4
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,8 @@ impl<T: CoordinateType> LineString<T> {
160160
/// and the value of the first coordinate does not equal the value of the last coordinate, then
161161
/// a new coordinate is added to the end with the value of the first coordinate.
162162
pub(crate) fn close(&mut self) {
163-
if let (Some(first), Some(last)) = (self.0.first().copied(), self.0.last().copied()) {
164-
if first != last {
165-
self.0.push(first);
166-
}
163+
if !self.is_closed() && !self.0.is_empty() {
164+
self.0.push(self.0[0]);
167165
}
168166
}
169167

@@ -181,6 +179,29 @@ impl<T: CoordinateType> LineString<T> {
181179
pub fn num_coords(&self) -> usize {
182180
self.0.len()
183181
}
182+
183+
/// Checks if the linestring is closed; i.e. the first
184+
/// and last points have the same coords.
185+
///
186+
/// # Examples
187+
///
188+
/// ```
189+
/// use geo_types::LineString;
190+
///
191+
/// let mut coords = vec![(0., 0.), (5., 0.), (0., 0.)];
192+
/// let line_string: LineString<f32> = coords.into_iter().collect();
193+
/// assert!(line_string.is_closed());
194+
/// ```
195+
pub fn is_closed(&self) -> bool {
196+
// LineString with less than 2 elements can't be closed
197+
if self.0.len() < 2 {
198+
false
199+
} else if self.0.first().unwrap() != self.0.last().unwrap() {
200+
false
201+
} else {
202+
true
203+
}
204+
}
184205
}
185206

186207
/// Turn a `Vec` of `Point`-like objects into a `LineString`.

geo/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ proj = { version = "0.20.3", optional = true }
2424

2525
geo-types = { version = "0.6.0", path = "../geo-types", features = ["rstar"] }
2626

27+
robust = { version = "0.2.2" }
28+
2729
[features]
2830
default = []
2931
use-proj = ["proj"]

geo/src/algorithm/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ pub mod vincenty_distance;
5959
pub mod vincenty_length;
6060
/// Calculate and work with the winding order of `Linestring`s.
6161
pub mod winding_order;
62+
/// Calculate the winding order of `LineString`s using robust predicates.
63+
pub mod robust_winding_order;
6264
/// Locate a point along a `Line` or `LineString`.
6365
pub mod line_locate_point;
6466
/// Interpolate a point along a `Line` or `LineString`.
+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
use super::winding_order::*;
2+
use crate::{Coordinate, CoordinateType, LineString};
3+
use num_traits::Float;
4+
5+
pub trait RobustWinding<T>: Winding<T>
6+
where
7+
T: CoordinateType,
8+
{
9+
/// Return the winding order of this object
10+
fn robust_winding_order(&self) -> Option<WindingOrder>;
11+
}
12+
13+
/// Compute index of the lexicographically least point.
14+
/// Should only be called on a non-empty slice.
15+
fn lexicographically_least_index<T: Copy + PartialOrd>(pts: &[T]) -> usize {
16+
assert!(pts.len() > 0);
17+
18+
let mut min: Option<(usize, T)> = None;
19+
for (i, pt) in pts.iter().enumerate() {
20+
if let Some((_, min_pt)) = min {
21+
if pt < &min_pt {
22+
min = Some( (i, *pt) )
23+
}
24+
} else {
25+
min = Some( (i, *pt) )
26+
}
27+
}
28+
29+
min.unwrap().0
30+
}
31+
32+
impl<T: CoordinateType + Float> RobustWinding<T> for LineString<T> {
33+
fn robust_winding_order(&self) -> Option<WindingOrder> {
34+
// If linestring has at most 2 points, it is either
35+
// not closed, or is the same point. Either way, the
36+
// WindingOrder is unspecified.
37+
if self.num_coords() < 3 { return None; }
38+
39+
// Open linestrings do not have a winding order.
40+
if !self.is_closed() { return None; }
41+
42+
let i = lexicographically_least_index(&self.0);
43+
let mut next = i;
44+
while self.0[next] == self.0[i] {
45+
next += 1;
46+
if next >= self.num_coords() { next = 0; }
47+
}
48+
49+
let mut prev = i;
50+
while self.0[prev] == self.0[i] {
51+
if prev == 0 {
52+
prev = self.num_coords() - 1;
53+
} else {
54+
prev -= 1;
55+
}
56+
}
57+
58+
use robust::{Coord, orient2d};
59+
use num_traits::NumCast;
60+
let orientation = orient2d(
61+
Coord {
62+
x: <f64 as NumCast>::from( self.0[prev].x ).unwrap(),
63+
y: <f64 as NumCast>::from( self.0[prev].y ).unwrap(),
64+
},
65+
Coord {
66+
x: <f64 as NumCast>::from( self.0[i].x ).unwrap(),
67+
y: <f64 as NumCast>::from( self.0[i].y ).unwrap(),
68+
},
69+
Coord {
70+
x: <f64 as NumCast>::from( self.0[next].x ).unwrap(),
71+
y: <f64 as NumCast>::from( self.0[next].y ).unwrap(),
72+
},
73+
);
74+
75+
if orientation < 0. {
76+
Some(WindingOrder::Clockwise)
77+
} else if orientation > 0. {
78+
Some(WindingOrder::CounterClockwise)
79+
} else {
80+
None
81+
}
82+
83+
}
84+
}
85+
86+
#[cfg(test)]
87+
mod test {
88+
use super::*;
89+
use crate::Point;
90+
91+
#[test]
92+
fn robust_winding_order() {
93+
// 3 points forming a triangle
94+
let a = Point::new(0., 0.);
95+
let b = Point::new(2., 0.);
96+
let c = Point::new(1., 2.);
97+
98+
// That triangle, but in clockwise ordering
99+
let cw_line = LineString::from(vec![a.0, c.0, b.0, a.0]);
100+
// That triangle, but in counterclockwise ordering
101+
let ccw_line = LineString::from(vec![a.0, b.0, c.0, a.0]);
102+
103+
// Verify open linestrings return None
104+
assert!(LineString::from(vec![a.0, b.0, c.0])
105+
.robust_winding_order()
106+
.is_none());
107+
108+
assert_eq!(cw_line.robust_winding_order(), Some(WindingOrder::Clockwise));
109+
assert_eq!(cw_line.is_cw(), true);
110+
assert_eq!(cw_line.is_ccw(), false);
111+
assert_eq!(
112+
ccw_line.robust_winding_order(),
113+
Some(WindingOrder::CounterClockwise)
114+
);
115+
assert_eq!(ccw_line.is_cw(), false);
116+
assert_eq!(ccw_line.is_ccw(), true);
117+
118+
let cw_points1: Vec<_> = cw_line.points_cw().collect();
119+
assert_eq!(cw_points1.len(), 4);
120+
assert_eq!(cw_points1[0], a);
121+
assert_eq!(cw_points1[1], c);
122+
assert_eq!(cw_points1[2], b);
123+
assert_eq!(cw_points1[3], a);
124+
125+
let ccw_points1: Vec<_> = cw_line.points_ccw().collect();
126+
assert_eq!(ccw_points1.len(), 4);
127+
assert_eq!(ccw_points1[0], a);
128+
assert_eq!(ccw_points1[1], b);
129+
assert_eq!(ccw_points1[2], c);
130+
assert_eq!(ccw_points1[3], a);
131+
132+
assert_ne!(cw_points1, ccw_points1);
133+
134+
let cw_points2: Vec<_> = ccw_line.points_cw().collect();
135+
let ccw_points2: Vec<_> = ccw_line.points_ccw().collect();
136+
137+
// cw_line and ccw_line are wound differently, but the ordered winding iterator should have
138+
// make them similar
139+
assert_eq!(cw_points2, cw_points2);
140+
assert_eq!(ccw_points2, ccw_points2);
141+
142+
// test make_clockwise_winding
143+
let mut new_line1 = ccw_line.clone();
144+
new_line1.make_cw_winding();
145+
assert_eq!(new_line1.robust_winding_order(), Some(WindingOrder::Clockwise));
146+
assert_eq!(new_line1, cw_line);
147+
assert_ne!(new_line1, ccw_line);
148+
149+
// test make_counterclockwise_winding
150+
let mut new_line2 = cw_line.clone();
151+
new_line2.make_ccw_winding();
152+
assert_eq!(
153+
new_line2.robust_winding_order(),
154+
Some(WindingOrder::CounterClockwise)
155+
);
156+
assert_ne!(new_line2, cw_line);
157+
assert_eq!(new_line2, ccw_line);
158+
}
159+
}

0 commit comments

Comments
 (0)