-
Notifications
You must be signed in to change notification settings - Fork 12
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
Start calculating Movements and Conflicts #96
Conversation
a27d99f
to
0dc5a0b
Compare
still faulty due to road sort order and missing travel direction checks. We should be able to see on and off ramps, sausage links, etc. pretty clearly in the result when its working.
0dc5a0b
to
b5110af
Compare
…ections cross or not
The movements become incorrect (and lead to crashes) when the number of roads in an intersection changes. The movements should just be recalculated (which might be easy, but I haven't reasoned it out yet).
osm2streets/src/render.rs
Outdated
for (a, b) in intersection.movements.iter() { | ||
if *a != *b { | ||
pairs.push(( | ||
Line::must_new(road_points[*a], road_points[*b]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, this can fail if the points are too close together... I find it surprising that a line type can't even represent lines segments of length zero... I guess that would mean that it has no direction, and so a whole host of operations on it would need to be fallible...
I'll use the constructor that returns a result instead.
@dabreegster I finally found the spare energy to get this functioning at a draft level. Movements are drawn crudely as arrows between the Road endpoints on the new "Debug movements" layer, and some merges vs diverges are detected. Still to do:
|
Awesome! I'm having a look now. Do you want high- or low-level feedback now, or after those TODOs done? |
Any high level thoughts now. I'll request a review when I'm ready for it to go in. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
… classifications I have spot checked the results, which look largely correct. I did find a false-positive crossing detection with a one way merging into a bidi road in northgate_dual_carriageway, which I have edited into the output, so the test still fails.
complexity and conflict_level are nonsense for bike intersections
# Conflicts: # osm2streets/src/initial.rs # osm2streets/src/lib.rs
…indexes The indexes we had were indexing into a filtered road list. Road ids seems better anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dabreegster I'm ready for you to take a look at the implementation, and consider merging this in.
The complexity
and conflict_level
fields themselves might end up not actually being used in any calculations / rendering decisions, but they are useful now for debugging and for the tests. It's more likely to be the Major/Minor movement distinction, which I'm starting to work through in my head atm.
|
||
for (i, intersection) in &self.intersections { | ||
// Find the points where the arrows should (leave, enter) the roads. | ||
let road_points: BTreeMap<_, _> = intersection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to implement a method on Road
that could calculate these offset endpoints (for a given end and direction). Because a Road
doesn't know it's own DrivingSide
or dividing line (!!), it's non-trivial, so I'm happy with this currently.
I want to figure out how a Road
should figure out it's own driving side, dividing line and HalfRoad
s pretty soon. Maybe annotating contraflow lanes within LaneSpec
would make it easy enough to calculate on demand from lanes_lrt
, or maybe the driving side and dividing lane should be stored on the Road
as part of the OSM parsing process (where we have the most context).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree it's annoying how often DrivingSide
has to be plumbed around. Just so I understand, by diving line, you mean the point where the direction of travel changes (or the left/right edge on one-way roads)? Looking at pairs of lanes_ltr
and direction should work for that, right? And that process exposes cases where there isn't one obvious dividing lane (bidi cycletracks, shared center turn lanes)
Maybe annotating contraflow lanes within LaneSpec
What do you mean by this? They have Direction
already
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that is what I mean by "dividing line". If contraflow lanes were annotated as "contraflow" from the beginning, then we wouldn't get confused when trying to find the dividing line. Knowing the driving side might be enough to come up with a consistent definition / detection algorithm though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm open to trying either way. This is sounding like a good thing to unit test individually -- it's probably just a few weird edge cases I'm worrying about
osm2streets/src/lib.rs
Outdated
@@ -86,21 +87,36 @@ impl StreetNetwork { | |||
for i in [id.i1, id.i2] { | |||
self.intersections.get_mut(&i).unwrap().roads.push(id); | |||
self.sort_roads(i); | |||
// Recalculate movements and complexity. | |||
let (complexity, conflict_level, movements) = | |||
crate::transform::classify_intersections::guess_complexity(self, &i); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sort of thing challenges my understanding of what should be a "transformation step" and what is an inherent part of the data types. Or at least which transformations are responsible for calculating data (that therefore need to be rerun when later transformations want to change things, such as merging intersections).
The "calculate change first, then commit them all" pattern that is forced upon us by the borrow checker in our current transformation implementations might actually be a better idea than calling this directly whenever a road is added. That is, maybe the responsibility for recalculating the movements etc. should be higher up than insert_road
. At the level of merge_intersections(a, b)
or higher.
Anyway, I've got a lot to understand yet about how the transformations fit together...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sort of thing challenges my understanding of what should be a "transformation step" and what is an inherent part of the data types
I don't have clear ideas here either. :( I'm going to try to draw the flow chart of how everything gets created / set currently. The big hammer is to treat this as a big dataflow-type problem and consider automatically recalculating derived state when something changes. Or not do that automatically, but at least have a principled way to organize / reason about this. The problem is that changes percolate to related objects. If you do anything to a road, you need to recalculate geometry for two intersections. That calculation might trim back other roads one hop away. If there are transformations that look at trimmed road length, would we want to always go trigger them again? It gets confusing very quickly...
maybe the responsibility for recalculating the movements etc. should be higher up than insert_road. At the level of merge_intersections(a, b) or higher.
I like this way of thinking. Sometimes a very low-level, "surgical" set of mutations can happen. Or we use this "medium level" of abstraction. But another transformation may just want to say "merge short road" and automatically recalculate a bunch of derived state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or not do that automatically, but at least have a principled way to organize / reason about this [big dataflow-type problem].
This seems the likely best approach to me. At least for the code that takes the raw input to the naively "filled out" street representation.
My idea is that when you call StreetNetwork::new(osm_stuff)
you might expect to get back Road
s and Intersection
s that are complete, but blindly representing the raw data. There would be a handful of low-level conditions that are met ("all the geometry connects up properly to each other..."), and the other higher level conditions haven't been addressed yet ("... but some of it overlaps"). These are the bits of code that don't feel like "transformations" to me, more like "hydrating" the abstract raw descriptions ("this centerline with this placement and these lanes") into a useful concrete representation ("these lanes covering these areas").
Because the raw data is criminally under-specified we are expected to infer a bunch of stuff; under-represented so we want to guess/fill in a bunch of stuff; and imprecise so we want to correct a bunch of stuff. For example, if all intersection-internal ways were tagged junction=intersection
(or whatever the tag is) then we wouldn't need a transformation to merge intersections together, we'd simply collect all the connected intersection nodes together before creating the Intersection
.
These inferences, guesses and corrections are easier to make with the hydrated representation, so it makes sense to hydrate the raw data, calculate any inferences, propagate them back into the raw data then hydrate again. This is how I see "transformations" fitting in conceptually.
Actually modifying the osm data as the result of transformations sounds like a painful idea, but it feels like there's something there: StreetNetwork
/Road
/Intersection
are like primitives, with some source-of-truth data (placement adjusted centerlines, connectivity, stop line positions) and some straightforwardly derived data (geometry, etc.). If StreetNetwork
provided a rich enough set of operations (like collapsing a road (merging intersections), collapsing an intersection (merging roads), zipping/snapping adjacent roads, adding turn restrictions, moving centerlines/stop line positions, that sort of thing) then it could be used to manually draw beautifully accurate street networks (or a horrible mess). The problem of getting a beautiful street network out of raw osm data then becomes one of naively translating the osm spec into StreetNetwork
primitives, then doing bunch of tweaking operations on them using osm-specific inferences/assumptions.
The StreetNetwork
primitive operations could be efficiently implemented, maybe even in a way that allows batching (perform a bunch of operations and keep track of what is invalidated, then recalculate all the invalidated data). Then we could reason about the order of transformations, if they need repeating, even of multiple transformations can be committed at the same time.
I'm gonna let that marinate in my brain for a little while and look to talk with you about it later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just finished writing up #117 and this sounds so much better. The dataflow "framework" doesn't have to make the entire codebase complex; this batching executor could isolate it. I really agree at least two of the current transformations are something much more fundamental / less optional
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine merging this in and making smaller incremental steps. We have enough ideas for refactors and new abstractions, and rebasing a long-lived PR gets tough. As long as nothing external using osm2streets currently gets affected, I prefer checking in experiments and leaving the results unused or flagging them off
Awesome leap forward!
osm2streets/src/lib.rs
Outdated
@@ -86,21 +87,36 @@ impl StreetNetwork { | |||
for i in [id.i1, id.i2] { | |||
self.intersections.get_mut(&i).unwrap().roads.push(id); | |||
self.sort_roads(i); | |||
// Recalculate movements and complexity. | |||
let (complexity, conflict_level, movements) = | |||
crate::transform::classify_intersections::guess_complexity(self, &i); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sort of thing challenges my understanding of what should be a "transformation step" and what is an inherent part of the data types
I don't have clear ideas here either. :( I'm going to try to draw the flow chart of how everything gets created / set currently. The big hammer is to treat this as a big dataflow-type problem and consider automatically recalculating derived state when something changes. Or not do that automatically, but at least have a principled way to organize / reason about this. The problem is that changes percolate to related objects. If you do anything to a road, you need to recalculate geometry for two intersections. That calculation might trim back other roads one hop away. If there are transformations that look at trimmed road length, would we want to always go trigger them again? It gets confusing very quickly...
maybe the responsibility for recalculating the movements etc. should be higher up than insert_road. At the level of merge_intersections(a, b) or higher.
I like this way of thinking. Sometimes a very low-level, "surgical" set of mutations can happen. Or we use this "medium level" of abstraction. But another transformation may just want to say "merge short road" and automatically recalculate a bunch of derived state.
// detect if they diverge, merge, or cross. | ||
// assert!(connections is sorted) so small_con large_con makes sense. | ||
let mut each_con = connections.iter(); | ||
while let Some(small_con) = each_con.next() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just so I understand: conflict is symmetric, so we only need to calculate "half the matrix" of pairs, right? What do you mean by sorted / small and large connections? Literally the tuples of movement indices, ie, (1, 3) < (2, 1)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, spot on. I'll clarify this in the "every pair of connections" comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact, the logic doesn't depend on the sort order of small_con
/large_con
(it may have at one point). We also aren't doing anything with the full set of conflicts, so I'll omit storing them all here.
…ted logic and document it
still faulty due to road sort order and missing travel direction checks.
See #67. We should be able to see on and off ramps, sausage links, etc. pretty clearly in the result when its working.