Skip to content
This repository has been archived by the owner on Mar 9, 2023. It is now read-only.

Fix lane count calculation - use Scheme structs to avoid re-reading/re-interpreting tags #111

Closed
wants to merge 49 commits into from

Conversation

BudgieInWA
Copy link
Collaborator

@BudgieInWA BudgieInWA commented Mar 25, 2022

I am submitting this draft PR to get input on the new approach to driving_lane_directions, rust style and tests.yml. This PR is aiming towards #69 #72.

Are we happy with this overall structure for driving_lane_directions?

bf5df9d shows the concise version, and dc5de27 shows how useful tag validation warnings can be added (thinking about #101 and how an editor would like to use tags_to_lanes).

Any feedback on my rust style?

I want to fit into this project, more than anything else, but couldn't help trying out is_some_and and using lots of unwrap_or.

How do the roundtrip tests work?

I couldn't figure it out just yet. Some guidance in the docs about writing and running tests.yml tests would be super awesome. (I figured out cargo test --feature tests, which I'll add to the docs if I get there first.)

Forward and backward lanes are independently guessed to be half of the lanes, but the tags on the website assume that providing `lanes` and `lanes:backward` is enough.
data/tests.yml Outdated Show resolved Hide resolved
Copy link
Collaborator

@droogmic droogmic left a comment

Choose a reason for hiding this comment

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

Looks good, thanks for the work so far.

You are working on issue #72

rust/osm2lanes/src/transform/tags_to_lanes/mod.rs Outdated Show resolved Hide resolved
rust/osm2lanes/src/transform/tags_to_lanes/mod.rs Outdated Show resolved Hide resolved
@droogmic droogmic linked an issue Mar 25, 2022 that may be closed by this pull request
@droogmic
Copy link
Collaborator

but couldn't help trying out is_some_and and using lots of unwrap_or

Looks pretty idiomatic to me, but then again I am not expert.

@droogmic
Copy link
Collaborator

I figured out cargo test --feature tests

This was a bug, it should be fixed now.

You should be able to run all the tests with just cargo test

The roundtrip works by taking the output (only the output), and checking that you can get back to the same spec by going to tags and back again (round trip).

Why not tags -> spec -> tags?
Well: spec to tags is a one to many mapping, so this shouldn't guarantee a roundtrip.

Documentation is very welcome!

Copy link
Contributor

@dabreegster dabreegster left a comment

Choose a reason for hiding this comment

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

Thanks @droogmic for reviewing, feel free to take the lead on this. I may have some time over the weekend to catch up, but other stuff first...

rust/osm2lanes/src/lib.rs Outdated Show resolved Hide resolved
BudgieInWA and others added 4 commits March 26, 2022 18:38
Co-authored-by: droogmic <droogmic@gmail.com>
Incorporate lanes:both_ways and centre_turn_lanes.
Add assumption that oneway=no, lanes=1 means lanes:both_ways=1
@droogmic
Copy link
Collaborator

droogmic commented Mar 27, 2022 via email

Copy link
Collaborator

@droogmic droogmic left a comment

Choose a reason for hiding this comment

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

Looking good so far, surprisingly complex :)

rust/osm2lanes/src/transform/tags_to_lanes/mod.rs Outdated Show resolved Hide resolved
rust/osm2lanes/src/transform/tags_to_lanes/mod.rs Outdated Show resolved Hide resolved
Comment on lines 628 to 629
// Always assume no center turn lane unless tagged, so we already know:
let num_both_ways = tagged_both_ways.unwrap_or(tagged_center_turn_lanes.unwrap_or(0));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is centre_turn_lane=no and lanes:both_ways=1 or centre_turn_lane=yes and lanes:both_ways=0 valid.

rust/osm2lanes/src/transform/tags_to_lanes/mod.rs Outdated Show resolved Hide resolved
}
(Some(l), Some(f), None) => (f, num_both_ways, l - f - num_both_ways),
(Some(l), None, Some(b)) => (l - b - num_both_ways, num_both_ways, b),
(Some(1), None, None) => (0, 1, 0),
Copy link
Collaborator

Choose a reason for hiding this comment

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

1 lane becomes a centre lane? seems fishy?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It is a bit fishy, but I think it is because describing "how many lanes in each direction does a skinny allyway have" is fishy. I might tag them "lane_markings=no width=5", but we have to represent that in schema somehow.

rust/osm2lanes/src/transform/tags_to_lanes/mod.rs Outdated Show resolved Hide resolved
Comment on lines 597 to 602
fn driving_lane_directions(
tags: &Tags,
locale: &Locale,
oneway: Oneway,
warnings: &mut RoadWarnings,
) -> (usize, usize, usize) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we test this in isolation, it is quite complex now :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree! (if this function ends up keeping all of its responsibilities.)

Does that mean a set of tests in tests.yaml, or rust tests?

Copy link
Contributor

Choose a reason for hiding this comment

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

Rust tests I'd imagine, since the input and output are quite specialized here. The unit tests can be in this file, since it's private

@BudgieInWA
Copy link
Collaborator Author

I am finding more and more out about Tags library and the parsing techniques and good RoadMsg construction, still a little way to go. But, there is a fundamental problem:

The problem with locking in a final guess on the lanes distribution at the start

We cannot assume or guess missing parts of the lanes:{forward,backward}= scheme without parsing everything else. See how 28300bc adds tons of code parsing other schemes. Definitely smelly.

We need a way to narrow down our guess as we go. Random thoughts:

  • Maybe we use Infer like values too keep track of guesses.
  • Maybe in guessing mode, start with the assumption that there is one normal lane in each direction, then add lanes instead of mutating existing lanes. Then at the end we can do a round of assuming (e.g. distribute remaining lanes).
  • Maybe each tagging scheme has its own struct type using Infered values, which remains internally consistent, to help in the code which validates each tag schema, checks for conflicts, and wants to jump between them in order to do these complicated guesses. Then the validated and consistent schemes are used to build the road. Maybe having code that defines the test and guesses for each requirement between schemes is exactly the right amount of code (e.g. lanes:bus:forward=2 requires at least 2 forward lanes, or increases the guesses forward lanes by 2), but it still feels a bit like double handling (the whole point of the Builder pattern is to keep track of the jumping between different inputs, right?)
  • It ties into validating: psv:lanes=designated| requires lanes=2 to parse properly, so it implies lanes=2 (and is an error if lanes!=2)
  • It feels a bit like Logic Programming where different tags are facts that work to collapse the possibility space.

@droogmic
Copy link
Collaborator

droogmic commented Mar 31, 2022

I agree with everything you say, but I also don't really know the best way forward.

but it still feels a bit like double handling (the whole point of the Builder pattern is to keep track of the jumping between different inputs, right?)

correct

Maybe in guessing mode, start with the assumption that there is one normal lane in each direction, then add lanes instead of mutating existing lanes.

I agree, that we should maybe try this instead. The downside is that all the other logic needs to account for this new system, so it will be a painful refactor :(.

A simplified version of the logic you have now should probably still exist, to populate a top level Infer field in roadbuilder that may get invalidated by clearer tags later (like your bus example)?

In any event, it sounds like you understand the problem. Now you just need to decide how hard you make the solution for yourself ;)

@droogmic
Copy link
Collaborator

I added you to the project, so hopefully I dont need to approve the CI anymore

…tructure.

The code is a mess, gesturing generally at the idea, without compiling. I feel like
the whole lot should mean close to what it actually says.
@@ -1054,6 +1055,8 @@

- way_id: 389654080
mapillary: https://www.mapillary.com/app/?pKey=331760328316020
rust:
expect_warnings: true # deprecated centre_turn_lane
Copy link
Collaborator

Choose a reason for hiding this comment

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

good idea to comment like that, need to do that myself

Copy link
Collaborator

@droogmic droogmic 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 agree to this once you look at what I have done and can justify why this is better.

For example:
How do we extend this to include https://taginfo.openstreetmap.org/keys/taxi:lanes?
it feels a lot of the lane handling is hardcoded to bus and only bus?

sidewalk: "no"
shoulder: "no"
lanes: "3"
lanes:forward: "2"
Copy link
Collaborator

Choose a reason for hiding this comment

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

can this not be inferred from bus:lanes:forward
what happens when this is removed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It could, but lanes:forward should be tagged in OSM anyway. Inferring it is being a bit fancy and I'm not trying to solve the entirety of lane tagging in this PR.

Copy link
Collaborator

Choose a reason for hiding this comment

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

but lanes:forward should be tagged in OSM anyway

it should but it often isn't, we shouldn't depend on perfect OSM tagging.
we can leave it in to simplify the implementation for now, but please add a comment describing why this data is redundant so that we can try to remove it later?

impl ToString for DrivingSide {
fn to_string(&self) -> String {
match self {
Self::Right => String::from("right"),
Copy link
Collaborator

Choose a reason for hiding this comment

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

not sure if important,

but I avoided implementing this to avoid confusing right hand drive with right hand travel.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I am confused about the difference, so maybe it is important! Is right hand drive when the driver gets in the right hand door of the car? I suppose this should be Into<TagKey> instead of ToString or something?

@@ -11,6 +11,8 @@ pub use key::TagKey;
mod osm;
pub use osm::{Highway, HighwayType, Lifecycle, HIGHWAY, LIFECYCLE};

use crate::transform::{RoadWarnings, TagsToLanesMsg};
Copy link
Collaborator

Choose a reason for hiding this comment

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

it feels strange including these in this mod.

I would expect tag to eventually become its own crate or similar, with no dependencies on the osm2lanes transformation logic.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed, get_parsed is a convenience function for parsing a bunch of tags at the same time, and should probably live in transform somewhere. It was suggested that it be moved in here, but maybe it should be a function, not a method: let l: Option<usize> = get_parsed_tag(tags, "lanes", &warnings); or a method on RoadWarnings: let l: Option<usize> = warnings.parse_tag(tags, "lanes")?

&self,
key: &K,
warnings: &mut RoadWarnings,
) -> Option<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Result<Option<T>, String>

where the Err is the value that cannot be parsed?

or

Result<Option<T>, E>

where E comes from the underlying FromStr

Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe Option<Result> makes more sense?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It would be Result<T, E> (which would be converted to an Option with ok() most of the time, because the error is already added to the warnings).


const LANES: TagKey = TagKey::from("lanes");
#[derive(Debug)]
pub struct LanesScheme {
Copy link
Collaborator

Choose a reason for hiding this comment

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

the name of the mod and this struct are unclear for me

in my PR I named this Counts, as it isn't really defining a scheme?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I didn't know what to name the mod. The struct is named LanesScheme because it represents the lanes:* tags (which together make up what I was calling "scheme").

rust/osm2lanes/src/transform/tags_to_lanes/lane.rs Outdated Show resolved Hide resolved

const CENTRE_TURN_LANE: TagKey = TagKey::from("centre_turn_lane");
pub struct CentreTurnLaneScheme {
pub present: Infer<bool>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

what does "present" mean in this context?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It refers to the presence of such a lane: "is a centre turn lane present on this road?".

Is something like struct CentreTurnLaneScheme(Infer<bool>) possible? I haven't used that language feature before.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yes, that would be the correct construct.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Comment on lines +29 to +32
pub struct BuswayScheme {
pub forward_side_direction: Infer<Option<Direction>>,
pub backward_side_direction: Infer<Option<Direction>>,
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think I found a case of a Backward in forward_side_direction or Forward in backward_side_direction
nor would I expect it.

what would go wrong with BuswayScheme(Infer<(bool, bool)>) to cover the existence of a forward and/or backward lane?

Copy link
Collaborator

Choose a reason for hiding this comment

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

second: this would be nice to add this as a standalone intermediate refactor PR, to make reviewing easier.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think I found a case of a Backward in forward_side_direction or Forward in backward_side_direction
nor would I expect it.

Given this, I agree that BuswayScheme(Infer<(bool, bool)>) is better.

@BudgieInWA
Copy link
Collaborator Author

I can agree to this once you look at what I have done and can justify why this is better.

How do we extend this to include taginfo.openstreetmap.org/keys/taxi:lanes?
it feels a lot of the lane handling is hardcoded to bus and only bus?

My intention is to achieve a couple of things:

  1. Separate tag parsing from road building, by introducing Scheme structs that represent the valid states described by smalls sets of tags. BuswayScheme is a good example: the busway tags are a complicated way of describing "is there a bus lane on the forward side, and is there a bus lane on the backward side", so we parse all those tags into BuswayScheme(Infer<(bool, bool)>) and never have to look up a busway tag value again.
  2. Fix the guessing of the total number of lanes, by inverting the order that the tags are looked at.
  3. Move towards tackling the ordering of looking at different tags at the top level tags_to_lanes. a.k.a. express the direction of dependencies between tags in one place. I have only taken a step in that direction, creating BuswayScheme, CentreTurnLaneScheme, and LanesScheme (which take other Scheme arguments to show dependencies), and constructing them in the right order at the top level.

These ideas would be further helped by:

  • Implementing a Scheme for every tag or set of tags that we parse (even Oneway has way more complexity that we don't support yet).
  • Bringing the guessing logic out of the Scheme constructors, and into road builder. Maybe Scheme constructors should not Infer::Guess at all, and only return Direct, Calculated (for internally calculated values, like lanes=lanes:forward + lanes:backward) and Default values. At the top level, or as a set of RoadBuilder methods, follow an ordered procedure for building up the road using the Schemes, but no tags.
    • This would allow us to remove all dependencies from LanesScheme::new, (except maybe oneway) and figure out the guesses in a context which has access to a RoadBuilder with some lanes already added and the parsed LanesScheme (like Mitigation for more than one bus lanes scheme used #136 (comment)). That step would treat Direct values in LanesScheme differently to Default values.

@droogmic
Copy link
Collaborator

Ok, but then I think we might be trying to do too much in one PR (which is where the difficulty is coming from).

Either we fix the lane counting, or we fix the scheme parsing. Doing both at the same time is blocking quick progress :)

@BudgieInWA BudgieInWA changed the title Fix lane count calculation Fix lane count calculation - use Scheme structs to avoid re-reading/re-interpreting tags Apr 23, 2022
@droogmic
Copy link
Collaborator

kotlin is probably failing in the tests.yml file, so I suggest bisecting the changes you made there until kotlin is happy?

@droogmic droogmic closed this Apr 23, 2022
@droogmic droogmic reopened this Apr 23, 2022
@droogmic
Copy link
Collaborator

my bad, the button for close is very close to the one with comment 😆

Comment on lines +1225 to +1226
lanes: 4
lanes:backward: 1
Copy link
Collaborator

Choose a reason for hiding this comment

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

these are interpreted as integers, when they should be strings, that causes the kotlin tests to fail

@dabreegster
Copy link
Contributor

Thanks for splitting into smaller PRs and pushing them through. Just checking in, was closing this PR intentional, or a side effect of the commit message there? Are any of the ideas in this PR still not merged in by one of the smaller PRs?

@droogmic
Copy link
Collaborator

A side-effect. I removed a lot of stuff from this PR that were not needed to make the tests pass

@BudgieInWA will probably want to go through the final status to reintroduce some of the less critical ideas he had (I removed the Infer::Guessed variant for example), preferably as a TDD when they become needed.

@BudgieInWA
Copy link
Collaborator Author

A side-effect. I removed a lot of stuff from this PR that were not needed to make the tests pass

@BudgieInWA will probably want to go through the final status to reintroduce some of the less critical ideas he had (I removed the Infer::Guessed variant for example), preferably as a TDD when they become needed.

This is a great outcome. Getting these ideas our into the open is probably the appropriate amount of progress for now.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Fix Lane Logic on Website
3 participants