-
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
Parse placement
tags and shift roads accordingly
#139
Changes from all commits
128aa44
79eef65
c1911fd
94147c7
1adb096
ce540b1
717ad95
b8fee5b
bfcdf55
8d20e2d
5f59cc7
eb0d3dd
2e28f38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
mod classic; | ||
mod osm2lanes; | ||
mod placement; | ||
#[cfg(test)] | ||
mod tests; | ||
|
||
|
@@ -87,6 +88,47 @@ impl LaneType { | |
} | ||
} | ||
|
||
/// Determines if the lane is a travel lane that is represented in OSM `*:lanes` tags. | ||
/// Note that the `lanes` tag counts car driving lanes, excluding bike lanes, whereas the | ||
/// `:lanes` suffix specifies that each lane, including bike lanes, should have a value between | ||
/// `|`s. This function identifies the latter kind. | ||
pub fn is_tagged_by_lanes_suffix(&self) -> bool { | ||
match self { | ||
LaneType::Driving => true, | ||
LaneType::Biking => true, // FIXME depends on lane vs track | ||
LaneType::Bus => true, | ||
LaneType::Parking => false, | ||
LaneType::Sidewalk => false, | ||
LaneType::Shoulder => false, | ||
LaneType::SharedLeftTurn => true, | ||
LaneType::Construction => true, | ||
LaneType::LightRail => false, | ||
LaneType::Buffer(_) => false, | ||
LaneType::Footway => false, | ||
LaneType::SharedUse => false, | ||
} | ||
} | ||
|
||
/// Determines if the lane is part of the roadway, the contiguous sealed surface that OSM | ||
/// mappers consider the "road". | ||
pub fn is_roadway(&self) -> bool { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function (and The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding to Rephrasing... if we start to refactor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Absolutely, I'm enjoying the standard set by supporting A/B Street at each iteration.
This is an opportunity to see what a useful osm2lanes API would look like, so we can carve osm2lanes down into the support for this lib. I think it's ok that the types (aka vocabulary) let you say semantically nonsense things like "pedestrian parking". What if someone tagged it as park of crowd/queue management at some arena or something? We shouldn't crash. Parsing tags into fields seems simpler than trying to classify into an enum of distinct types right at the beginning. If parking can be tagged with access restrictions, then it's up to the renderer to scratch its head when it tries to find lane markings for foot parking. |
||
match self { | ||
LaneType::Driving => true, | ||
LaneType::Biking => true, // FIXME depends on lane vs track | ||
LaneType::Bus => true, | ||
LaneType::Parking => true, // FIXME depends on on-street vs street-side | ||
LaneType::Sidewalk => false, | ||
LaneType::Shoulder => true, | ||
LaneType::SharedLeftTurn => true, | ||
LaneType::Construction => true, | ||
LaneType::LightRail => true, // FIXME only for trams | ||
LaneType::Buffer(BufferType::Curb) => false, | ||
LaneType::Buffer(_) => true, | ||
LaneType::Footway => false, | ||
LaneType::SharedUse => false, | ||
} | ||
} | ||
|
||
pub fn is_walkable(self) -> bool { | ||
matches!( | ||
self, | ||
|
@@ -355,3 +397,86 @@ impl fmt::Display for Direction { | |
} | ||
} | ||
} | ||
|
||
/// Refers to a lane by its left-to-right position among all lanes in that direction. Backward | ||
/// lanes are counted left-to-right from the backwards direction. | ||
/// | ||
/// e.g. The left-most forward lane is `LtrLaneNum::Forward(1)` and the backward lane furthest to | ||
/// the road-right is `LtrLaneNum::Backward(1)`, because of the backward perspective. | ||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] | ||
pub enum LtrLaneNum { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Before I read the below comment, I was confused what this meant. Always counting from the left, its the 0th, 1st, 2nd, etc lane that's forwards or backwards, regardless of contraflow. What about There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pretty much that (but starting at 1). Contraflow lanes are not mentioned anywhere, but a strict reading of "the forward lanes in LtR order" can be followed by mappers and consumers equally well. Does Like tagging This is where i need to summarise the idea in comments! And link to the wiki quotes. |
||
Forward(usize), | ||
Backward(usize), | ||
} | ||
|
||
impl LtrLaneNum { | ||
pub fn direction(&self) -> Direction { | ||
match self { | ||
Self::Forward(_) => Direction::Fwd, | ||
Self::Backward(_) => Direction::Back, | ||
} | ||
} | ||
|
||
pub fn number(&self) -> usize { | ||
match self { | ||
Self::Forward(num) | Self::Backward(num) => *num, | ||
} | ||
} | ||
|
||
/// Converts to the same numbered lane in the opposite direction. | ||
pub fn reverse(&self) -> Self { | ||
use LtrLaneNum::*; | ||
match self { | ||
Forward(n) => Backward(*n), | ||
Backward(n) => Forward(*n), | ||
} | ||
} | ||
} | ||
|
||
/// Identifies a position within the width of a roadway. Lanes are identified by their left-to-right | ||
/// position, as per the OSM convention. | ||
Comment on lines
+436
to
+437
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Early on I was tempted to represent this using |
||
/// | ||
/// Most commonly seen as a value of the placement tag, e.g. | ||
/// `placement=right_of:1` means that the OSM way is drawn along the right edge of lane 1. | ||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] | ||
pub enum RoadPosition { | ||
/// The center of the carriageway width, ignoring lanes. The default placement of OSM ways. | ||
Center, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems awkward that this type uses "center" and "middle" to mean the same thing, but "center" matches with the convention in this repo, and "middle" matches the placement tag. Opinion? |
||
/// The center of the full width of a `Road`, including verges and footpaths. | ||
FullWidthCenter, | ||
/// The center of the separation between both directions of traffic, i.e. the dividing line, | ||
/// median, or shared turning lane. For a oneway road, this is the "inside" edge of the road, | ||
/// i.e. the right side of LHT and the left side of RHT. | ||
Separation, | ||
/// On the left edge of the named lane (from the direction of the named lane). | ||
LeftOf(LtrLaneNum), | ||
/// In the middle of the named lane. | ||
MiddleOf(LtrLaneNum), | ||
/// On the right edge of the named lane (from the direction of the named lane). | ||
RightOf(LtrLaneNum), | ||
Comment on lines
+451
to
+456
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The other way to do this could be |
||
} | ||
|
||
impl RoadPosition { | ||
/// Converts to the same placement interpreted from the other direction. That is, only the | ||
/// wrapped LtrLaneNum is reversed. | ||
pub fn reverse(self) -> Self { | ||
use RoadPosition::*; | ||
match self { | ||
Center | FullWidthCenter | Separation => self, | ||
LeftOf(n) => LeftOf(n.reverse()), | ||
MiddleOf(n) => MiddleOf(n.reverse()), | ||
RightOf(n) => RightOf(n.reverse()), | ||
} | ||
} | ||
} | ||
|
||
/// Describes the placement of a line (such as the OSM Way) along a road. | ||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] | ||
pub enum Placement { | ||
/// Along the specified position down the entire length. | ||
Consistent(RoadPosition), | ||
/// Varying linearly from a specified position at the start, to a different one at the end. | ||
Varying(RoadPosition, RoadPosition), | ||
/// Varying linearly from some unspecified position at the start, to a different one at the end. | ||
Transition, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
use abstutil::Tags; | ||
use anyhow::Result; | ||
|
||
use super::{LtrLaneNum, Placement, RoadPosition}; | ||
|
||
use Placement::*; | ||
use RoadPosition::*; | ||
|
||
impl RoadPosition { | ||
/// Tries to parse a road position from an osm tag value as per the `placement` scheme. | ||
/// See https://wiki.openstreetmap.org/wiki/Proposed_features/placement#Tagging | ||
/// | ||
/// The direction is treated as forward, use `reverse()` on the result if the context is backward. | ||
pub fn parse(value: &str) -> Result<Self> { | ||
match value { | ||
"" => Ok(Center), | ||
"separation" => Ok(Separation), | ||
_ => { | ||
if let Some((kind, lane_str)) = value.split_once(':') { | ||
if let Ok(lane) = lane_str.parse::<usize>() { | ||
match kind { | ||
"left_of" => Ok(LeftOf(LtrLaneNum::Forward(lane))), | ||
"middle_of" => Ok(MiddleOf(LtrLaneNum::Forward(lane))), | ||
"right_of" => Ok(RightOf(LtrLaneNum::Forward(lane))), | ||
_ => bail!("unknown lane position specifier: {kind}"), | ||
} | ||
} else { | ||
bail!("bad lane number: {lane_str}") | ||
} | ||
} else { | ||
bail!("unknown placement value: {value}") | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
impl Placement { | ||
/// Tries to parse a placement from a set of OSM tags according to the `placement` scheme. | ||
/// See https://wiki.openstreetmap.org/wiki/Proposed_features/placement#Tagging | ||
/// | ||
/// Limitations: | ||
/// - Doesn't validate tag combos, just returns the first interpretation it finds. | ||
/// - Doesn't allow `:end` and `:start` to mix `:forward` and `:back`. Maybe it should? | ||
pub fn parse(tags: &Tags) -> Result<Self> { | ||
if let Some(transition_or_pos) = tags.get("placement") { | ||
if transition_or_pos == "transition" { | ||
Ok(Transition) | ||
} else { | ||
Ok(Consistent(RoadPosition::parse(transition_or_pos.as_str())?)) | ||
} | ||
} else if tags.has_any(vec!["placement:start", "placement:end"]) { | ||
Ok(Varying( | ||
RoadPosition::parse(tags.get("placement:start").map_or("", |s| s.as_str()))?, | ||
RoadPosition::parse(tags.get("placement:end").map_or("", |s| s.as_str()))?, | ||
)) | ||
} else if let Some(pos) = tags.get("placement:forward") { | ||
Ok(Consistent(RoadPosition::parse(pos.as_str())?)) | ||
} else if tags.has_any(vec!["placement:forward:start", "placement:forward:end"]) { | ||
Ok(Varying( | ||
RoadPosition::parse( | ||
tags.get("placement:forward:start") | ||
.map_or("", |s| s.as_str()), | ||
)?, | ||
RoadPosition::parse(tags.get("placement:forward:end").map_or("", |s| s.as_str()))?, | ||
)) | ||
} else if let Some(backwards_pos) = tags.get("placement:backward") { | ||
Ok(Consistent( | ||
RoadPosition::parse(backwards_pos.as_str())?.reverse(), | ||
)) | ||
} else if tags.has_any(vec!["placement:backward:start", "placement:backward:end"]) { | ||
Ok(Varying( | ||
RoadPosition::parse( | ||
tags.get("placement:backward:start") | ||
.map_or("", |s| s.as_str()), | ||
)? | ||
.reverse(), | ||
RoadPosition::parse( | ||
tags.get("placement:backward:end") | ||
.map_or("", |s| s.as_str()), | ||
)? | ||
.reverse(), | ||
)) | ||
} else { | ||
Ok(Consistent(Center)) // The default when not tagged. | ||
} | ||
} | ||
} | ||
|
||
#[cfg(tests)] | ||
mod tests { | ||
use super::*; | ||
use std::collections::BTreeMap; | ||
use LtrLaneNum::*; | ||
|
||
#[test] | ||
fn road_position_parses() { | ||
assert_eq!(RoadPosition::parse("").unwrap(), Center); | ||
assert_eq!(RoadPosition::parse("separation").unwrap(), Separation); | ||
assert_eq!( | ||
RoadPosition::parse("left_of:1").unwrap(), | ||
LeftOf(Forward(1)) | ||
); | ||
assert_eq!( | ||
RoadPosition::parse("middle_of:1").unwrap(), | ||
MiddleOf(Forward(1)) | ||
); | ||
assert_eq!( | ||
RoadPosition::parse("right_of:1").unwrap(), | ||
RightOf(Forward(1)) | ||
); | ||
} | ||
|
||
#[test] | ||
fn placement_parses() { | ||
assert_eq!( | ||
Placement::parse(&Tags::new(BTreeMap::from([( | ||
"placement".into(), | ||
"transition".into() | ||
)]))) | ||
.unwrap(), | ||
Transition | ||
); | ||
|
||
assert_eq!( | ||
Placement::parse(&Tags::new(BTreeMap::from([( | ||
"placement".into(), | ||
"right_of:1".into() | ||
)]))) | ||
.unwrap(), | ||
Consistent(RightOf(Forward(1))) | ||
); | ||
|
||
assert_eq!( | ||
Placement::parse(&Tags::new(BTreeMap::from([( | ||
"placement:forward".into(), | ||
"right_of:1".into() | ||
)]))) | ||
.unwrap(), | ||
Consistent(RightOf(Forward(1))) | ||
); | ||
|
||
assert_eq!( | ||
Placement::parse(&Tags::new(BTreeMap::from([( | ||
"placement:backward".into(), | ||
"right_of:1".into() | ||
)]))) | ||
.unwrap(), | ||
Consistent(RightOf(Backward(1))) | ||
); | ||
|
||
assert_eq!( | ||
Placement::parse(&Tags::new(BTreeMap::from([ | ||
("placement:start".into(), "right_of:1".into()), | ||
("placement:end".into(), "right_of:2".into()) | ||
]))) | ||
.unwrap(), | ||
Varying(RightOf(Forward(1)), RightOf(Forward(2))) | ||
); | ||
|
||
assert_eq!( | ||
Placement::parse(&Tags::new(BTreeMap::from([ | ||
("placement:backward:start".into(), "right_of:1".into()), | ||
("placement:backward:end".into(), "right_of:2".into()) | ||
]))) | ||
.unwrap(), | ||
Varying(RightOf(Backward(1)), RightOf(Backward(2))) | ||
); | ||
} | ||
} |
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 don't know if you were aware of this, it's not very well presented on the wiki and has big implications in
osm2lanes
parsing.