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

Avoid generating self-intersecting isochrone polygons that would otherwise result from ::Generalize() #3026

Merged
merged 22 commits into from
Apr 28, 2021

Conversation

ktatterso
Copy link
Contributor

@ktatterso ktatterso commented Apr 21, 2021

Issue

When generating isochrones, customers can specify a numeric value, generalize or gen_factor, which simplifies the returned polygon. This is particularly useful for reducing the size of the polygon, and hence, the returned payload.

The polyline/polygon simplification method employed by Valhalla is the Douglas-Peucker algorithm. On its own, it has no facilities that would prevent it from generating a self-intersecting polygon during its simplification step. Based on my studies, using a gen_factor=55, auto costing, and a 30 min contour - the probability of us returning a self-intersecting polygon is quite high, I'd estimate 80-90% (urban areas).

This is problematic for customers/users who take these isochrone polygons and input them to other GIS systems for analysis - only to find their analysis tools don't work with self-intersecting polygons.

My solution to avoid generating self-intersections all boils down to this question: will simplifying this polyline result in a self-intersection? To determine the answer to that question poses a spatial problem: are there any polygon points in the "vicinity" of the polyline that would cause a self-intersection with the polyline I might simplify?

To answer the question "are there any points near this polyline I'm about to simplify?" I have created the PointTileIndex class. This class can discretize the lat/lon space into whatever width is desired - in this case, gen_factor determines the width. The PointTileIndex constructor will "bin" all the given points into the index. Then, within the Douglas-Peucker algorithm, we can use the PointTileIndex to answer that very question: "what points are near this polyline?"

Armed with a set of "points near my polyline", we can very accurately ask, "will any of these nearby points cause a self-intersection if I simplify?" Geometrically, you can think of line simplification as the geometric collapse of many triangles. The triangles in question are formed by the start position, the end position, and each point along the polyline. Hence, if a stray polygon point is found in any of the triangles we're about to collapse, we would cause a self-intersection if we simplified.

Example 1

Location: 47.412426,9.334023
Costing: Auto
Contour minutes: 30

Generalize: 0 (no self-intersection)

image

Generalize: 55 (without fix, self-intersects)

image

Generalize: 55 (after fix, no self-intersection)

image

Example 2

Location: 34.061657,-118.268494
Costing: Auto
Contour minutes: 30

Generalize: 0 (no self-intersection)

image

Generalize: 55 (without fix, self-intersects)

image

Generalize: 55 (after fix, no self-intersection)

image

Performance

The ::tile() method is O(n). Asking for nearby points is k*O(1) where k is the number of nearby tiles. Fortunately, most segments considered for simplification aren't very long, so k is small.

For example, the 30-min auto isochrone (gen_factor=55) for Orlando previously took 560 ms (not including serialization). The ::Generalize() step took 6 ms of that 560 ms. After my changes, ::Generalize() takes 22 ms, increasing the total time to 578 ms.

Thoughts & other ideas

I tried using Boost's boost::geometry::buffer() method with a distance_strategy=0.0, thinking if I gave it a self-intersecting polygon it would fix it. Unfortunately, it did not. Given a self-intersecting polygon it would return an empty result. In contrast, it worked fine for non-self-intersecting polygons.

I also wondered about other routines that might heal/repair/fix self-intersecting polygons. In the end, I decided that this approach would keep with the spirit of the original algorithm, only avoiding self-intersections as needed.

Tasklist

  • Add tests
  • Add #fixes with the issue number that this PR addresses
  • Update the docs with any new request parameters or changes to behavior described
  • Update the changelog

@gknisely
Copy link
Member

@ktatterso Can we get some before and after screen shots of the results.

Copy link
Contributor

@merkispavel merkispavel left a comment

Choose a reason for hiding this comment

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

The first round of review

test/isochrone.cc Outdated Show resolved Hide resolved
valhalla/midgard/polyline2.h Outdated Show resolved Hide resolved
valhalla/midgard/point_tile_index.h Outdated Show resolved Hide resolved
src/midgard/point_tile_index.cc Outdated Show resolved Hide resolved
src/midgard/polyline2.cc Outdated Show resolved Hide resolved
valhalla/midgard/point_tile_index.h Show resolved Hide resolved
src/midgard/point_tile_index.cc Outdated Show resolved Hide resolved
src/midgard/point_tile_index.cc Outdated Show resolved Hide resolved
src/midgard/polyline2.cc Outdated Show resolved Hide resolved
src/midgard/polyline2.cc Outdated Show resolved Hide resolved
src/midgard/polyline2.cc Outdated Show resolved Hide resolved
@ktatterso
Copy link
Contributor Author

@ktatterso Can we get some before and after screen shots of the results.

I added some examples. thanks.

*/
class PointTileIndex {
// This guy can tell us which tile a points belongs to.
std::unique_ptr<Tiles<PointLL>> tiles;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Switched to using the Tiles class for creating a gridded space over the given points-of-interest. Works well and performs equal to my previous solution.

merkispavel
merkispavel previously approved these changes Apr 28, 2021
Copy link
Contributor

@merkispavel merkispavel left a comment

Choose a reason for hiding this comment

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

LGTM

@@ -6,6 +6,7 @@
#include <valhalla/midgard/point2.h>
#include <valhalla/midgard/pointll.h>

#include <list>
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: unused

Comment on lines +22 to +23
for (auto iter = polyline.begin(); iter != polyline.end(); iter++) {
const PointLL& p = *iter;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit:

Suggested change
for (auto iter = polyline.begin(); iter != polyline.end(); iter++) {
const PointLL& p = *iter;
for (const auto& p : polyline) {

Comment on lines +24 to +31
if (p.lat() < min_lat)
min_lat = p.lat();
if (p.lat() > max_lat)
max_lat = p.lat();
if (p.lng() < min_lng)
min_lng = p.lng();
if (p.lng() > max_lng)
max_lng = p.lng();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit

Suggested change
if (p.lat() < min_lat)
min_lat = p.lat();
if (p.lat() > max_lat)
max_lat = p.lat();
if (p.lng() < min_lng)
min_lng = p.lng();
if (p.lng() > max_lng)
max_lng = p.lng();
min_lat = std::min(min_lat, p.lat());
max_lat = std::min(max_lat, p.lat());
min_lng = std::min(min_lng, p.lng());
max_lng = std::max(max_lng, p.lng());

@ktatterso ktatterso requested a review from merkispavel April 28, 2021 19:36
@@ -1,9 +1,11 @@
file(GLOB headers ${VALHALLA_SOURCE_DIR}/valhalla/midgard/*.h)

set_source_files_properties(
if ((UNIX OR APPLE) AND ENABLE_SINGLE_FILES_WERROR)
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

@merkispavel merkispavel left a comment

Choose a reason for hiding this comment

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

Re-approve

@ktatterso ktatterso merged commit 3af3781 into master Apr 28, 2021
@ktatterso ktatterso deleted the intersecting-polys branch April 28, 2021 22:28
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.

4 participants