-
Notifications
You must be signed in to change notification settings - Fork 202
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #84 from urschrei/visvalingam
Add Visvalingam-Whyatt line-simplification algorithm
- Loading branch information
Showing
5 changed files
with
1,216 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
use std::cmp::Ordering; | ||
use std::collections::BinaryHeap; | ||
use num::Float; | ||
use types::{Point, LineString}; | ||
|
||
// A helper struct for `visvalingam`, defined out here because | ||
// #[deriving] doesn't work inside functions. | ||
#[derive(PartialEq, Debug)] | ||
struct VScore<T> | ||
where T: Float | ||
{ | ||
area: T, | ||
current: usize, | ||
left: usize, | ||
right: usize, | ||
} | ||
|
||
// These impls give us a min-heap | ||
impl<T> Ord for VScore<T> | ||
where T: Float | ||
{ | ||
fn cmp(&self, other: &VScore<T>) -> Ordering { | ||
other.area.partial_cmp(&self.area).unwrap() | ||
} | ||
} | ||
|
||
impl<T> PartialOrd for VScore<T> | ||
where T: Float | ||
{ | ||
fn partial_cmp(&self, other: &VScore<T>) -> Option<Ordering> { | ||
Some(self.cmp(other)) | ||
} | ||
} | ||
|
||
impl<T> Eq for VScore<T> where T: Float {} | ||
|
||
/// Simplify a line using the [Visvalingam-Whyatt](http://www.tandfonline.com/doi/abs/10.1179/000870493786962263) algorithm | ||
/// | ||
/// epsilon is the minimum triangle area | ||
// The paper states that: | ||
// If [the new triangle's] calculated area is less than that of the last point to be | ||
// eliminated, use the latter's area instead. | ||
// (This ensures that the current point cannot be eliminated | ||
// without eliminating previously eliminated points) | ||
// (Visvalingam and Whyatt 2013, p47) | ||
// However, this does *not* apply if you're using a user-defined epsilon; | ||
// It's OK to remove triangles with areas below the epsilon, | ||
// then recalculate the new triangle area and push it onto the heap | ||
pub fn visvalingam<T>(orig: &[Point<T>], epsilon: &T) -> Vec<Point<T>> | ||
where T: Float | ||
{ | ||
// No need to continue without at least three points | ||
if orig.len() < 3 || orig.is_empty() { | ||
return orig.to_vec(); | ||
} | ||
|
||
let max = orig.len(); | ||
|
||
// Adjacent retained points. Simulating the points in a | ||
// linked list with indices into `orig`. Big number (larger than or equal to | ||
// `max`) means no next element, and (0, 0) means deleted element. | ||
let mut adjacent: Vec<(_)> = (0..orig.len()) | ||
.map(|i| { | ||
if i == 0 { | ||
(-1_i32, 1_i32) | ||
} else { | ||
((i - 1) as i32, (i + 1) as i32) | ||
} | ||
}) | ||
.collect(); | ||
|
||
// Store all the triangles in a minimum priority queue, based on their area. | ||
// Invalid triangles are *not* removed if / when points | ||
// are removed; they're handled by skipping them as | ||
// necessary in the main loop. (This is handled by recording the | ||
// state in the VScore) | ||
let mut pq = BinaryHeap::new(); | ||
// Compute the initial triangles, i.e. take all consecutive groups | ||
// of 3 points and form triangles from them | ||
for (i, win) in orig.windows(3).enumerate() { | ||
pq.push(VScore { | ||
area: area(win.first().unwrap(), &win[1], win.last().unwrap()), | ||
current: i + 1, | ||
left: i, | ||
right: i + 2, | ||
}); | ||
} | ||
// While there are still points for which the associated triangle | ||
// has an area below the epsilon | ||
loop { | ||
let smallest = match pq.pop() { | ||
// We've exhausted all the possible triangles, so leave the main loop | ||
None => break, | ||
// This triangle's area is above epsilon, so skip it | ||
Some(ref x) if x.area > *epsilon => continue, | ||
// This triangle's area is below epsilon: eliminate the associated point | ||
Some(s) => s, | ||
}; | ||
let (left, right) = adjacent[smallest.current]; | ||
// A point in this triangle has been removed since this VScore | ||
// was created, so skip it | ||
if left as i32 != smallest.left as i32 || right as i32 != smallest.right as i32 { | ||
continue; | ||
} | ||
// We've got a valid triangle, and its area is smaller than epsilon, so | ||
// remove it from the simulated "linked list" | ||
let (ll, _) = adjacent[left as usize]; | ||
let (_, rr) = adjacent[right as usize]; | ||
adjacent[left as usize] = (ll, right); | ||
adjacent[right as usize] = (left, rr); | ||
adjacent[smallest.current as usize] = (0, 0); | ||
|
||
// Now recompute the triangle area, using left and right adjacent points | ||
let choices = [(ll, left, right), (left, right, rr)]; | ||
for &(ai, current_point, bi) in &choices { | ||
if ai as usize >= max || bi as usize >= max { | ||
// Out of bounds, i.e. we're on one edge | ||
continue; | ||
} | ||
let new_left = Point::new(orig[ai as usize].x(), orig[ai as usize].y()); | ||
let new_current = Point::new(orig[current_point as usize].x(), | ||
orig[current_point as usize].y()); | ||
let new_right = Point::new(orig[bi as usize].x(), orig[bi as usize].y()); | ||
pq.push(VScore { | ||
area: area(&new_left, &new_current, &new_right), | ||
current: current_point as usize, | ||
left: ai as usize, | ||
right: bi as usize, | ||
}); | ||
} | ||
} | ||
// Filter out the points that have been deleted, returning remaining points | ||
orig.iter() | ||
.zip(adjacent.iter()) | ||
.filter_map(|(tup, adj)| { if *adj != (0, 0) { Some(*tup) } else { None } }) | ||
.collect::<Vec<Point<T>>>() | ||
} | ||
|
||
// Area of a triangle given three vertices | ||
fn area<T>(p1: &Point<T>, p2: &Point<T>, p3: &Point<T>) -> T | ||
where T: Float | ||
{ | ||
((p1.x() - p3.x()) * (p2.y() - p3.y()) - (p2.x() - p3.x()) * (p1.y() - p3.y())).abs() / | ||
(T::one() + T::one()) | ||
} | ||
|
||
pub trait SimplifyVW<T, Epsilon = T> { | ||
/// Returns the simplified representation of a LineString, using the [Visvalingam-Whyatt](http://www.tandfonline.com/doi/abs/10.1179/000870493786962263) algorithm | ||
/// | ||
/// See [here](https://bost.ocks.org/mike/simplify/) for a graphical explanation | ||
/// | ||
/// ``` | ||
/// use geo::{Point, LineString}; | ||
/// use geo::algorithm::simplifyvw::{SimplifyVW}; | ||
/// | ||
/// let mut vec = Vec::new(); | ||
/// vec.push(Point::new(5.0, 2.0)); | ||
/// vec.push(Point::new(3.0, 8.0)); | ||
/// vec.push(Point::new(6.0, 20.0)); | ||
/// vec.push(Point::new(7.0, 25.0)); | ||
/// vec.push(Point::new(10.0, 10.0)); | ||
/// let linestring = LineString(vec); | ||
/// let mut compare = Vec::new(); | ||
/// compare.push(Point::new(5.0, 2.0)); | ||
/// compare.push(Point::new(7.0, 25.0)); | ||
/// compare.push(Point::new(10.0, 10.0)); | ||
/// let ls_compare = LineString(compare); | ||
/// let simplified = linestring.simplifyvw(&30.0); | ||
/// assert_eq!(simplified, ls_compare) | ||
/// ``` | ||
fn simplifyvw(&self, epsilon: &T) -> Self where T: Float; | ||
} | ||
|
||
impl<T> SimplifyVW<T> for LineString<T> | ||
where T: Float | ||
{ | ||
fn simplifyvw(&self, epsilon: &T) -> LineString<T> { | ||
LineString(visvalingam(&self.0, epsilon)) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use types::Point; | ||
use super::visvalingam; | ||
|
||
#[test] | ||
fn visvalingam_test() { | ||
// this is the PostGIS example | ||
let points = vec![(5.0, 2.0), (3.0, 8.0), (6.0, 20.0), (7.0, 25.0), (10.0, 10.0)]; | ||
let points_ls: Vec<_> = points.iter().map(|e| Point::new(e.0, e.1)).collect(); | ||
|
||
let correct = vec![(5.0, 2.0), (7.0, 25.0), (10.0, 10.0)]; | ||
let correct_ls: Vec<_> = correct.iter().map(|e| Point::new(e.0, e.1)).collect(); | ||
|
||
let simplified = visvalingam(&points_ls, &30.); | ||
assert_eq!(simplified, correct_ls); | ||
} | ||
#[test] | ||
fn visvalingam_test_long() { | ||
// simplify a longer LineString | ||
let points = include!("../vw_orig.rs"); | ||
let points_ls: Vec<_> = points.iter().map(|e| Point::new(e[0], e[1])).collect(); | ||
let correct = include!("../vw_simplified.rs"); | ||
let correct_ls: Vec<_> = correct.iter().map(|e| Point::new(e[0], e[1])).collect(); | ||
let simplified = visvalingam(&points_ls, &0.0005); | ||
assert_eq!(simplified, correct_ls); | ||
} | ||
#[test] | ||
fn visvalingam_test_empty_linestring() { | ||
let vec = Vec::new(); | ||
let compare = Vec::new(); | ||
let simplified = visvalingam(&vec, &1.0); | ||
assert_eq!(simplified, compare); | ||
} | ||
#[test] | ||
fn visvalingam_test_two_point_linestring() { | ||
let mut vec = Vec::new(); | ||
vec.push(Point::new(0.0, 0.0)); | ||
vec.push(Point::new(27.8, 0.1)); | ||
let mut compare = Vec::new(); | ||
compare.push(Point::new(0.0, 0.0)); | ||
compare.push(Point::new(27.8, 0.1)); | ||
let simplified = visvalingam(&vec, &1.0); | ||
assert_eq!(simplified, compare); | ||
} | ||
} |
Oops, something went wrong.