Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use QuickHull implementation for convex hull #89

Merged
17 commits merged into from
Mar 2, 2017

Conversation

urschrei
Copy link
Member

This supersedes #87, for a number of reasons:

  • It's a lot faster – (O n log (n)) on average
  • It's a far simpler implementation
  • it doesn't require those questionable Ord and Eq traits

There's also a more robust test provided.

Copy link
Contributor

@TeXitoi TeXitoi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

// this is a bit hairy
for point in &to_retain.difference(&to_remove)
.map(|&idx| points[idx])
.collect::<Vec<Point<T>>>() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let iter = points.iter()
    .enumerate()
    .filter(|(idx, _)| ![mix_x_idx, max_x_idx].contains(idx))
    .map(|(_, p)| *p);
for point in iter {

And you don't need to_retain and to_remove (and much less allocations)

hull.push(p_a);
hull.push(p_b);
let mut left_set: Vec<Point<T>> = vec![];
let mut right_set: Vec<Point<T>> = vec![];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These type annotations should not be needed.

/// let res = poly.convex_hull();
/// assert_eq!(res.exterior, correct_hull);
/// ```
fn convex_hull(&self) -> Self where T: Float;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convex_hull must return a Polygon.

fn convex_hull(&self) -> Polygon<T> {
Polygon::new(LineString(quick_hull(&self.exterior.0)), vec![])
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be great if more geometries are supported (I think at MultiPolygon, LineString and MultiLineString in particular).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like those can happen in follow up PRs, but would be good to have support for those other types

if points.len() < 4 {
return points.to_vec();
}
let mut hull: Vec<Point<T>> = vec![];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useless type annotation

}
let mut hull: Vec<Point<T>> = vec![];
let mut min_x_idx = <usize>::min_value();
let mut max_x_idx = <usize>::min_value();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simply use

let mut min_x_idx = 0;
let mut max_x_idx = 0;

let mut max_x_idx = <usize>::min_value();
let mut min_x = Float::max_value();
let mut max_x = Float::min_value();
let to_retain: BTreeSet<usize> = (0..points.len() - 1).collect();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can simply write

let to_retain: BTreeSet<_> = (0..points.len() - 1).collect();'

let mut min_x = Float::max_value();
let mut max_x = Float::min_value();
let to_retain: BTreeSet<usize> = (0..points.len() - 1).collect();
let mut to_remove: BTreeSet<usize> = BTreeSet::new();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useless type annotation

}
// recur
hull_set(p_a, &furthest_point, &mut left_ap, hull);
hull_set(&furthest_point, p_b, &mut left_pb, hull);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the hull.insert calls can have a squared effect because you push at the middle of a Vec (you then must shift n/2 elements). You can rewrite it just using hull.push:

// doing the first part
let mut new_set: Vec<_> = set.iter().filter(|point| point_location(p_a, &furthest_point, point)).collect();
hull_set(p_a, &furthest_point, &mut new_set, hull);

// inserting the current point
hull.push(set[furthest_idx]);

// doing the second part, reusing the memory allocated for the first part
new_set.clear();
new_set.extends(set.iter().filter(|point| point_location(&furthest_point, p_b, point));
hull_set(&furthest_point, p_b, &mut new_set, hull);

and correct the beginning of the function and the insertion in quick_hull accordingly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let mut new_set: Vec<_> = set.iter().filter(|point| point_location(p_a, &furthest_point, point)).collect(); gives me a reference, but I need the Point…

= note: expected type `&mut std::vec::Vec<types::Point<T>>`
= note:    found type `&mut std::vec::Vec<&types::Point<T>>`

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add .cloned() after .iter()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to adapt this and #89 (comment), but it's failing to compile for me in about three different ways – can you send me a PR against https://github.com/urschrei/rust-geo/tree/convex_hull_quick?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// this is a bit hairy
for point in &to_retain.difference(&to_remove)
.map(|&idx| points[idx])
.collect::<Vec<Point<T>>>() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this collect call be removed? Seems like we could just iterate over the Points in the Iterator

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the collect() gives me a trait bound error:

the trait std::iter::Iterator is not implemented for &std::iter::Map<std::collections::btree_set::Difference<'_, usize>, [closure@convexhull.rs:73:14: 73:32 points:_]>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you have to remove the & after the for point in

}
// recur
hull_set(p_a, &furthest_point, &mut left_ap, hull);
hull_set(&furthest_point, p_b, &mut left_pb, hull);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like these calls could be done concurrently? If so, something we can do later in a future PR. Though we don't really depend on any parallelism libraries like rayon yet, hmm.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I've been messing about with Rayon to try to parallelise this. It'll be in a follow-up, if only for discussion, because we're not doing it anywhere yet…

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you must have a very big polygon for parallelism being interesting.

fn convex_hull(&self) -> Polygon<T> {
Polygon::new(LineString(quick_hull(&self.exterior.0)), vec![])
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like those can happen in follow up PRs, but would be good to have support for those other types

@urschrei
Copy link
Member Author

I've covered the nits, but:

  • 6aa0115 has exposed what looks like either an off-by-one or a logic error somewhere; very simple Polygons have their initial point duplicated by the algorithm (see the change at f31f07e), but more complex Polygons such as poly1.rs and poly2.rs don't; the result matches Shapely's (and thus GEOS's), though in a different order.
  • The vector-insert-avoiding change suggested by @TeXitoi is a crucial enhancement, but I can't seem to get all the way there.

@urschrei
Copy link
Member Author

Amazing work, thanks @TeXitoi!
I should be able to impl this for MultiPoint, LineString, and MultiLineString, but we're missing a method to get the exterior ring for a MultiPolygon at the moment.

@TeXitoi
Copy link
Contributor

TeXitoi commented Feb 19, 2017

For multipolygon, just aggregate all the exterior points of the outer polygons in a Vec. You need to give a mutable slice, so creating the Vec is needed.

Copy link
Contributor

@TeXitoi TeXitoi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't really approve as I'm now a contributor, but that's to remove my request changes.

urschrei and others added 2 commits February 20, 2017 12:37
TODO: add some tests for them
- MultiPoint
- LineString
- MultiLineString
- MultiPolygon
{
fn convex_hull(&self) -> Polygon<T> {
let mut aggregated: Vec<Point<T>> = self.0.iter()
.flat_map(|elem| elem.exterior.0.clone())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.flat_map(|elem| elem.exterior.0.iter().cloned()

{
fn convex_hull(&self) -> Polygon<T> {
let mut aggregated: Vec<Point<T>> = self.0.iter()
.flat_map(|elem| elem.0.clone())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Copy link
Member

@frewsxcv frewsxcv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, nice job :)

@frewsxcv
Copy link
Member

frewsxcv commented Mar 1, 2017

@urschrei Anything else here or is this good to merge?

@urschrei
Copy link
Member Author

urschrei commented Mar 1, 2017

Good to go here. Thanks!

@frewsxcv
Copy link
Member

frewsxcv commented Mar 2, 2017

bors r+

ghost pushed a commit that referenced this pull request Mar 2, 2017
89: Use QuickHull implementation for convex hull
@ghost
Copy link

ghost commented Mar 2, 2017

Build succeeded

@ghost ghost merged commit e53a92e into georust:master Mar 2, 2017
@urschrei urschrei deleted the convex_hull_quick branch March 14, 2017 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants