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

Isochrone PBF output and multipolygon support #4575

Merged
merged 30 commits into from
Feb 19, 2024

Conversation

chrstnbwnkl
Copy link
Member

@chrstnbwnkl chrstnbwnkl commented Feb 8, 2024

Issue

This PR builds on #4543 but takes it step further by grouping contour rings into multipolygons by means of pont in polygon checks.

Tasklist

  • Add tests
  • Update the docs with any new request parameters or changes to behavior described
  • Update the changelog

repeated Geometry geometries = 2; // if polygon first one is outer rest are inners, though this is a problem when we allow multi location isochrones
}

message Interval {
Copy link
Member Author

@chrstnbwnkl chrstnbwnkl Feb 8, 2024

Choose a reason for hiding this comment

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

Rather than just having a repeated Contour field that each holds a ring (or linestring), there are multiple intervals (one for each requested contour), each of which holds several contours (i.e. linestrings or polygons). If linestrings were requested, there'll be just one geometry; if polygons were requested the geometry field holds a number of rings, where the first is the exterior ring and the remaning are inner rings.

Especially useful for multi-location, but also for separated contours (e.g. when a ferry edge is part of the expansion)

case Options_Format_pbf:
return serializeIsochronePbf(request, intervals, contours);
case Options_Format_json:
if (polygons)
Copy link
Member Author

Choose a reason for hiding this comment

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

This still needs to change, probably should keep the current serialization as the default and only use the new method when explicitly requested by the user.

ASSERT_EQ(actual_poly_size, expected_poly_size);
for (uint32_t j = 0; j < actual_poly_size; ++j) {

// we only compare exterior rings
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 noticed we never compare full geometry, as we leave out the holes. maybe check those as well?

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's do that

// iterate over outer rings and for each inner ring check if the inner ring is within the outer ring
for (const auto inner : inner_ptrs) {
// construct bbox
AABB2<PointLL> inner_bbox(std::vector(inner->rbegin(), inner->rend()));
Copy link
Member Author

Choose a reason for hiding this comment

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

AABB2 constructor expects vector, not list. there's probably a better way to solve this 😄

@nilsnolde
Copy link
Member

nilsnolde commented Feb 12, 2024

One bigger question to be discussed: would this warrant a request param to enable proper multipolygons and keep the "legacy"/current behavior by default?

IMO our current GeoJSON polygon support is broken and this is actually fixing that bug. Apart from the fact that it's contours and not polygons and it's a crutch to begin with. But if we offer this we should do it as correctly as we can.

Most libs seem to be forgiving enough to render them half ok-ish even now (with multiple outer rings per polygon feature..). Also I'd guess 99% of consumers use some library's GeoJSON implementation, feeding in just the full thing and getting lucky rendering it properly, so nothing would change for them. As @chrstnbwnkl found out, our own web app at https://github.com/gis-ops/valhalla-app needs a bit of work to handle this, but that's just because we weren't doing using leaflet properly for GeoJSON in the first place (using L.polygon() instead of L.GeoJSON()).

@chrstnbwnkl chrstnbwnkl marked this pull request as ready for review February 12, 2024 15:34
return results;
}

// iterate over outer rings and for each inner ring check if the inner ring is within the exterior
Copy link
Member

@kevinkreiser kevinkreiser Feb 12, 2024

Choose a reason for hiding this comment

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

the check can simply take a single point from the inner and do point in polygon on a given outer. i would consider using the simplified winding number algorithm as proposed in the graphics gems book under "An Incremental Angle Point in Polygon Test". here's a sample implementation of that: https://jeremytammik.github.io/tbc/a/0491_point_in_poly.htm

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the resource! I gave it a shot, and placed the implentation in midgard/util.cc, maybe there's a better place to put it though

kevinkreiser
kevinkreiser previously approved these changes Feb 14, 2024
Copy link
Member

@kevinkreiser kevinkreiser left a comment

Choose a reason for hiding this comment

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

looks excellent pbf support and multipolygon with corrected inner outer relationships is awesome

@chrstnbwnkl
Copy link
Member Author

chrstnbwnkl commented Feb 15, 2024

So there's probably some floating point issue in the ARM build again that impacts the contouring. Shape tolerance in the isochrone test is exceeded by a bit, but I'm hesitant to increase it again, since I just did that not too long ago in #4463.

@nilsnolde
Copy link
Member

I don’t think we can do too much here and have to deal with it as it comes

Copy link
Member

@nilsnolde nilsnolde left a comment

Choose a reason for hiding this comment

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

good stuff:) just a few nits here and there from my side

Comment on lines +73 to +75
* ~~Generating outer contours or contours with interior holes for regions that cannot be accessed within the specified time.~~
* ~~Options to control the minimum size of interior holes.~~
* Removing self intersections from polygonal contours.
* ~~Removing self intersections from polygonal contours.~~
Copy link
Member

Choose a reason for hiding this comment

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

🥳

// if the user requested linestrings, we'll give them linestrings
if (!polygons || contours.size() < 2) {
std::for_each(contours.begin(), contours.end(),
[&results](const contour_t& c) { results.push_back({&c}); });
Copy link
Member

Choose a reason for hiding this comment

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

probably we can reserve here with results.reserve(contours.size()?

Copy link
Member Author

@chrstnbwnkl chrstnbwnkl Feb 15, 2024

Choose a reason for hiding this comment

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

They're nested vectors, of which we don't know the size before runtime, so not sure this makes any difference

Copy link
Member

Choose a reason for hiding this comment

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

You’re right, didn’t see the nesting


// iterate over outer rings and for each inner ring check if the inner ring is within the exterior
// ring
for (const auto inner : inner_ptrs) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
for (const auto inner : inner_ptrs) {
for (const auto* inner : inner_ptrs) {

it's easy to get confused if we hide stuff in auto

bool found_exterior = false;
// go over exterior rings from smallest to largest
for (size_t i = results.size(); i > 0; --i) {
contour_t ext = *results[i - 1][0];
Copy link
Member

Choose a reason for hiding this comment

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

if you don't want the pointer, rather take a ref to the underlying value or you do a copy

Suggested change
contour_t ext = *results[i - 1][0];
const contour_t& ext = *results[i - 1][0];


template bool point_in_poly<valhalla::midgard::PointLL, std::list<valhalla::midgard::PointLL>>(
const valhalla::midgard::PointLL&,
std::list<valhalla::midgard::PointLL>&);
Copy link
Member

Choose a reason for hiding this comment

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

the polygon is not const?

Copy link
Member Author

Choose a reason for hiding this comment

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

whoops, updated that

Comment on lines +77 to +79
if (!found_exterior) {
LOG_WARN("No exterior ring contour found for inner contour.");
}
Copy link
Member

Choose a reason for hiding this comment

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

did you observe this? probably could happen in the contouring algo?

Copy link
Member Author

Choose a reason for hiding this comment

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

Haven't observed this yet, even after excessive playing around (with different graph extracts even)

for (const auto inner : inner_ptrs) {

// get the first point of the ring (could be any though)
PointLL inner_pt = inner->front();
Copy link
Member

Choose a reason for hiding this comment

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

could also take a (const) reference here

@kevinkreiser kevinkreiser merged commit 812386f into valhalla:master Feb 19, 2024
7 checks passed
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