diff --git a/Cargo.lock b/Cargo.lock index a27d5665da..82fa1b7e82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -311,6 +311,15 @@ dependencies = [ "jobserver", ] +[[package]] +name = "celes" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7030e9a123aeed72ba38cb8d36b70ba973e19f7a52ce96c92d03bbc4f47310" +dependencies = [ + "serde", +] + [[package]] name = "cesu8" version = "1.1.0" @@ -2041,6 +2050,19 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" +[[package]] +name = "locale-codes" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1f4cc9d2da40c19763d5dd398baf388f2cc1473ca3d53c578c75e9ad0402324" +dependencies = [ + "lazy_static", + "log", + "regex", + "serde", + "serde_json", +] + [[package]] name = "lock_api" version = "0.4.4" @@ -2674,6 +2696,17 @@ dependencies = [ "serde", ] +[[package]] +name = "osm2lanes" +version = "0.1.0" +source = "git+https://github.com/a-b-street/osm2lanes#df0e7bb5d7f9f327b815894aaa7e0d69de410691" +dependencies = [ + "celes", + "locale-codes", + "log", + "serde", +] + [[package]] name = "osm_viewer" version = "0.1.0" @@ -3094,6 +3127,7 @@ dependencies = [ "geom", "kml", "log", + "osm2lanes", "petgraph", "serde", "serde_json", diff --git a/abstutil/src/collections.rs b/abstutil/src/collections.rs index bf3538ddd6..3c7aff7096 100644 --- a/abstutil/src/collections.rs +++ b/abstutil/src/collections.rs @@ -283,6 +283,9 @@ impl Tags { pub fn contains_key(&self, k: &str) -> bool { self.0.contains_key(k) } + pub fn has_any(&self, keys: Vec<&str>) -> bool { + keys.into_iter().any(|key| self.contains_key(key)) + } pub fn is(&self, k: &str, v: &str) -> bool { self.0.get(k) == Some(&v.to_string()) diff --git a/convert_osm/src/extract.rs b/convert_osm/src/extract.rs index f98c55f9b0..68afae4298 100644 --- a/convert_osm/src/extract.rs +++ b/convert_osm/src/extract.rs @@ -7,7 +7,7 @@ use abstutil::{Tags, Timer}; use geom::{Distance, FindClosest, HashablePt2D, Polygon, Pt2D, Ring}; use kml::{ExtraShape, ExtraShapes}; use raw_map::{ - osm, Amenity, AreaType, Direction, DrivingSide, NamePerLanguage, RawArea, RawBuilding, RawMap, + osm, Amenity, AreaType, Direction, NamePerLanguage, RawArea, RawBuilding, RawMap, RawParkingLot, RawRoad, RestrictionType, }; @@ -445,6 +445,7 @@ fn is_road(tags: &mut Tags, opts: &Options, name: &MapName) -> bool { } // It's a road! Now fill in some possibly missing data. + // TODO Stop doing this entirely? // If there's no parking data in OSM already, then assume no parking and mark that it's // inferred. @@ -457,51 +458,9 @@ fn is_road(tags: &mut Tags, opts: &Options, name: &MapName) -> bool { tags.insert(osm::PARKING_BOTH, "no_parking"); tags.insert(osm::INFERRED_PARKING, "true"); } - - // If there's no sidewalk data in OSM already, then make an assumption and mark that - // it's inferred. - if !tags.contains_key(osm::SIDEWALK) && opts.map_config.inferred_sidewalks { - tags.insert(osm::INFERRED_SIDEWALKS, "true"); - - if tags.contains_key("sidewalk:left") || tags.contains_key("sidewalk:right") { - // Attempt to mangle - // https://wiki.openstreetmap.org/wiki/Key:sidewalk#Separately_mapped_sidewalks_on_only_one_side - // into left/right/both. We have to make assumptions for missing values. - let right = !tags.is("sidewalk:right", "no"); - let left = !tags.is("sidewalk:left", "no"); - let value = match (right, left) { - (true, true) => "both", - (true, false) => "right", - (false, true) => "left", - (false, false) => "none", - }; - tags.insert(osm::SIDEWALK, value); - } else if tags.is_any(osm::HIGHWAY, vec!["motorway", "motorway_link"]) - || tags.is_any("junction", vec!["intersection", "roundabout"]) - || tags.is("foot", "no") - || tags.is(osm::HIGHWAY, "service") - // TODO For now, not attempting shared walking/biking paths. - || tags.is_any(osm::HIGHWAY, vec!["cycleway", "pedestrian", "track"]) - { - tags.insert(osm::SIDEWALK, "none"); - } else if tags.is("oneway", "yes") { - if opts.map_config.driving_side == DrivingSide::Right { - tags.insert(osm::SIDEWALK, "right"); - } else { - tags.insert(osm::SIDEWALK, "left"); - } - if tags.is_any(osm::HIGHWAY, vec!["residential", "living_street"]) - && !tags.is("dual_carriageway", "yes") - { - tags.insert(osm::SIDEWALK, "both"); - } - // Hack for Geneva, which maps sidewalks as separate ways - if name.city == CityName::new("ch", "geneva") { - tags.insert(osm::SIDEWALK, "both"); - } - } else { - tags.insert(osm::SIDEWALK, "both"); - } + // Hack for Geneva, which maps sidewalks as separate ways + if !tags.contains_key(osm::SIDEWALK) && name.city == CityName::new("ch", "geneva") { + tags.insert(osm::SIDEWALK, "both"); } true diff --git a/raw_map/Cargo.toml b/raw_map/Cargo.toml index 64ba0aa743..e9f890370e 100644 --- a/raw_map/Cargo.toml +++ b/raw_map/Cargo.toml @@ -13,6 +13,7 @@ geojson = { version = "0.22.0", features = ["geo-types"] } geom = { path = "../geom" } kml = { path = "../kml" } log = "0.4.14" +osm2lanes = { git = "https://github.com/a-b-street/osm2lanes" } petgraph = { version = "0.6.0" } serde = "1.0.123" serde_json = "1.0.61" diff --git a/raw_map/src/lane_specs.rs b/raw_map/src/lane_specs.rs index 084f90fd5c..064e431bf6 100644 --- a/raw_map/src/lane_specs.rs +++ b/raw_map/src/lane_specs.rs @@ -1,583 +1,307 @@ -/// Purely from OSM tags, determine the lanes that a road segment has. -use std::iter; +use osm2lanes::road::Designated; +use osm2lanes::tag::TagsWrite; use abstutil::Tags; use geom::Distance; -use crate::{osm, BufferType, Direction, DrivingSide, LaneSpec, LaneType, MapConfig}; +use crate::{osm, Direction, DrivingSide, LaneSpec, LaneType, MapConfig}; -pub fn get_lane_specs_ltr(tags: &Tags, cfg: &MapConfig) -> Vec { - let fwd = |lt: LaneType| LaneSpec { - lt, - dir: Direction::Fwd, - width: LaneSpec::typical_lane_widths(lt, tags)[0].0, - }; - let back = |lt: LaneType| LaneSpec { - lt, - dir: Direction::Back, - width: LaneSpec::typical_lane_widths(lt, tags)[0].0, - }; - - // Easy special cases first. - if tags.is_any("railway", vec!["light_rail", "rail"]) { - return vec![fwd(LaneType::LightRail)]; - } - if tags.is(osm::HIGHWAY, "steps") { - return vec![fwd(LaneType::Sidewalk)]; +pub fn get_lane_specs_ltr(orig_tags: &Tags, cfg: &MapConfig) -> Vec { + // Special cases first + if orig_tags.is_any("railway", vec!["light_rail", "rail"]) { + return vec![LaneSpec { + lt: LaneType::LightRail, + dir: Direction::Fwd, + width: LaneSpec::typical_lane_widths(LaneType::LightRail, orig_tags)[0].0, + }]; } - // Eventually, we should have some kind of special LaneType for shared walking/cycling paths of - // different kinds. Until then, model by making bike lanes and a shoulder for walking. - if tags.is_any( - osm::HIGHWAY, - vec!["cycleway", "footway", "path", "pedestrian", "track"], - ) { - // If it just allows foot traffic, simply make it a sidewalk. For most of the above highway - // types, assume bikes are allowed, except for footways, where they must be explicitly - // allowed. - if tags.is("bicycle", "no") - || (tags.is(osm::HIGHWAY, "footway") - && !tags.is_any("bicycle", vec!["designated", "yes", "dismount"])) - { - return vec![fwd(LaneType::Sidewalk)]; - } - // Otherwise, there'll always be a bike lane. - - let mut fwd_side = vec![fwd(LaneType::Biking)]; - let mut back_side = if tags.is("oneway", "yes") { - vec![] - } else { - vec![back(LaneType::Biking)] - }; - if !tags.is("foot", "no") { - fwd_side.push(fwd(LaneType::Shoulder)); - if !back_side.is_empty() { - back_side.push(back(LaneType::Shoulder)); - } - } - return LaneSpec::assemble_ltr(fwd_side, back_side, cfg.driving_side); - } + let tags = transform_tags(orig_tags, cfg); + let locale = osm2lanes::locale::Config::new() + .driving_side(match cfg.driving_side { + DrivingSide::Right => osm2lanes::locale::DrivingSide::Right, + DrivingSide::Left => osm2lanes::locale::DrivingSide::Left, + }) + .build(); + let mut config = osm2lanes::transform::TagsToLanesConfig::default(); + config.error_on_warnings = false; + config.include_separators = true; - // TODO Reversible roads should be handled differently? - let oneway = - tags.is_any("oneway", vec!["yes", "reversible"]) || tags.is("junction", "roundabout"); + match osm2lanes::transform::tags_to_lanes(&tags, &locale, &config) { + Ok(output) => { + let mut result = output + .road + .lanes + .into_iter() + .map(|lane| transform_lane(lane, &locale)) + .flatten() + .collect::>(); - // How many driving lanes in each direction? - let num_driving_fwd = if let Some(n) = tags - .get("lanes:forward") - .and_then(|num| num.parse::().ok()) - { - n - } else if let Some(n) = tags.get("lanes").and_then(|num| num.parse::().ok()) { - if oneway { - n - } else if n % 2 == 0 { - n / 2 - } else { - // usize division rounds down - (n / 2) + 1 - } - } else { - 1 - }; - let num_driving_back = if let Some(n) = tags - .get("lanes:backward") - .and_then(|num| num.parse::().ok()) - { - n - } else if let Some(n) = tags.get("lanes").and_then(|num| num.parse::().ok()) { - let base = n - num_driving_fwd; - if oneway { - base - } else { - // lanes=1 but not oneway... what is this supposed to mean? - base.max(1) - } - } else if oneway { - 0 - } else { - 1 - }; + // No shoulders on unwalkable roads + if orig_tags.is_any( + crate::osm::HIGHWAY, + vec!["motorway", "motorway_link", "construction"], + ) || orig_tags.is("foot", "no") + || orig_tags.is("access", "no") + || orig_tags.is("motorroad", "yes") + { + result.retain(|lane| lane.lt != LaneType::Shoulder); + } - #[allow(clippy::if_same_then_else)] // better readability - let driving_lane = - if tags.is("access", "no") && (tags.is("bus", "yes") || tags.is("psv", "yes")) { - // Sup West Seattle - LaneType::Bus - } else if tags - .get("motor_vehicle:conditional") - .map(|x| x.starts_with("no")) - .unwrap_or(false) - && tags.is("bus", "yes") - { - // Example: 3rd Ave in downtown Seattle - LaneType::Bus - } else if tags.is("access", "no") || tags.is("highway", "construction") { - LaneType::Construction - } else { - LaneType::Driving - }; + if output.road.highway.is_construction() { + // Remove sidewalks and make everything else a construction lane + result.retain(|lane| !lane.lt.is_walkable()); + for lane in &mut result { + lane.lt = LaneType::Construction; + } + } - // These are ordered from the road center, going outwards. Most of the members of fwd_side will - // have Direction::Fwd, but there can be exceptions with two-way cycletracks. - let mut fwd_side: Vec = iter::repeat_with(|| fwd(driving_lane)) - .take(num_driving_fwd) - .collect(); - let mut back_side: Vec = iter::repeat_with(|| back(driving_lane)) - .take(num_driving_back) - .collect(); - // TODO Fix upstream. https://wiki.openstreetmap.org/wiki/Key:centre_turn_lane - if tags.is("lanes:both_ways", "1") || tags.is("centre_turn_lane", "yes") { - fwd_side.insert(0, fwd(LaneType::SharedLeftTurn)); - } + // If there's no driving lane, ignore any assumptions about parking + // (https://www.openstreetmap.org/way/6449188 is an example) + if result.iter().all(|lane| lane.lt != LaneType::Driving) { + result.retain(|lane| lane.lt != LaneType::Parking); + } - if driving_lane == LaneType::Construction { - return LaneSpec::assemble_ltr(fwd_side, back_side, cfg.driving_side); - } + // Use our own widths for the moment + for lane in &mut result { + lane.width = LaneSpec::typical_lane_widths(lane.lt, &orig_tags)[0].0; + } - let fwd_bus_spec = if let Some(s) = tags.get("bus:lanes:forward") { - s - } else if let Some(s) = tags.get("psv:lanes:forward") { - s - } else if oneway { - if let Some(s) = tags.get("bus:lanes") { - s - } else if let Some(s) = tags.get("psv:lanes") { - s - } else { - "" - } - } else { - "" - }; - if !fwd_bus_spec.is_empty() { - let parts: Vec<&str> = fwd_bus_spec.split('|').collect(); - let offset = if fwd_side[0].lt == LaneType::SharedLeftTurn { - 1 - } else { - 0 - }; - if parts.len() == fwd_side.len() - offset { - for (idx, part) in parts.into_iter().enumerate() { - if part == "designated" { - fwd_side[idx + offset].lt = LaneType::Bus; - } + if let Some(x) = orig_tags + .get("sidewalk:left:width") + .and_then(|num| num.parse::().ok()) + { + // TODO Make sure this is a sidewalk! + result[0].width = Distance::meters(x); } - } - } - if let Some(spec) = tags - .get("bus:lanes:backward") - .or_else(|| tags.get("psv:lanes:backward")) - { - let parts: Vec<&str> = spec.split('|').collect(); - if parts.len() == back_side.len() { - for (idx, part) in parts.into_iter().enumerate() { - if part == "designated" { - back_side[idx].lt = LaneType::Bus; - } + if let Some(x) = orig_tags + .get("sidewalk:right:width") + .and_then(|num| num.parse::().ok()) + { + result.last_mut().unwrap().width = Distance::meters(x); } - } - } - if tags.is_any("cycleway", vec!["lane", "track"]) { - fwd_side.push(fwd(LaneType::Biking)); - if !back_side.is_empty() { - back_side.push(back(LaneType::Biking)); - } - } else if tags.is_any("cycleway:both", vec!["lane", "track"]) { - fwd_side.push(fwd(LaneType::Biking)); - back_side.push(back(LaneType::Biking)); - } else { - // Note here that we look at driving_side frequently, to match up left/right with fwd/back. - // If we're driving on the right, then right=fwd. Driving on the left, then right=back. - // - // TODO Can we express this more simply by referring to a left_side and right_side here? - if tags.is_any("cycleway:right", vec!["lane", "track"]) { - if cfg.driving_side == DrivingSide::Right { - if tags.is("cycleway:right:oneway", "no") || tags.is("oneway:bicycle", "no") { - fwd_side.push(back(LaneType::Biking)); + // Fix direction on outer lanes + for (idx, lane) in result.iter_mut().enumerate() { + if lane.lt == LaneType::Sidewalk || lane.lt == LaneType::Shoulder { + if idx == 0 { + lane.dir = if cfg.driving_side == DrivingSide::Right { + Direction::Back + } else { + Direction::Fwd + }; + } else { + // Assume last + lane.dir = if cfg.driving_side == DrivingSide::Right { + Direction::Fwd + } else { + Direction::Back + }; + } } - fwd_side.push(fwd(LaneType::Biking)); - } else { - if tags.is("cycleway:right:oneway", "no") || tags.is("oneway:bicycle", "no") { - back_side.push(fwd(LaneType::Biking)); - } - back_side.push(back(LaneType::Biking)); - } - } - if tags.is("cycleway:left", "opposite_lane") || tags.is("cycleway", "opposite_lane") { - if cfg.driving_side == DrivingSide::Right { - back_side.push(back(LaneType::Biking)); - } else { - fwd_side.push(fwd(LaneType::Biking)); } + + result } - if tags.is_any("cycleway:left", vec!["lane", "opposite_track", "track"]) { - if cfg.driving_side == DrivingSide::Right { - if tags.is("cycleway:left:oneway", "no") || tags.is("oneway:bicycle", "no") { - back_side.push(fwd(LaneType::Biking)); - back_side.push(back(LaneType::Biking)); - } else if oneway { - fwd_side.insert(0, fwd(LaneType::Biking)); - } else { - back_side.push(back(LaneType::Biking)); - } - } else { - // TODO This should mimic the logic for right-handed driving, but I need test cases - // first to do this sanely - if tags.is("cycleway:left:oneway", "no") || tags.is("oneway:bicycle", "no") { - fwd_side.push(back(LaneType::Biking)); - } - fwd_side.push(fwd(LaneType::Biking)); - } + Err(err) => { + error!("osm2lanes broke on {:?}: {}", orig_tags, err); + vec![LaneSpec { + lt: LaneType::Driving, + dir: Direction::Fwd, + width: Distance::meters(1.0), + }] } } +} - // My brain hurts. How does the above combinatorial explosion play with - // https://wiki.openstreetmap.org/wiki/Proposed_features/cycleway:separation? Let's take the - // "post-processing" approach. - // TODO Not attempting left-handed driving yet. - // TODO A two-way cycletrack on one side of a one-way road will almost definitely break this. - if let Some(buffer) = tags - .get("cycleway:right:separation:left") - .and_then(osm_separation_type) - { - // TODO These shouldn't fail, but snapping is imperfect... like around - // https://www.openstreetmap.org/way/486283205 - if let Some(idx) = fwd_side.iter().position(|x| x.lt == LaneType::Biking) { - fwd_side.insert(idx, fwd(LaneType::Buffer(buffer))); - } - } - if let Some(buffer) = tags - .get("cycleway:left:separation:left") - .and_then(osm_separation_type) - { - if let Some(idx) = back_side.iter().position(|x| x.lt == LaneType::Biking) { - back_side.insert(idx, back(LaneType::Buffer(buffer))); - } +fn transform_tags(tags: &Tags, cfg: &MapConfig) -> osm2lanes::tag::Tags { + let mut tags = tags.clone(); + + // Patch around some common issues + if tags.is(osm::SIDEWALK, "none") { + tags.insert(osm::SIDEWALK, "no"); } - if let Some(buffer) = tags - .get("cycleway:left:separation:right") - .and_then(osm_separation_type) - { - // This is assuming a one-way road. That's why we're not looking at back_side. - if let Some(idx) = fwd_side.iter().position(|x| x.lt == LaneType::Biking) { - fwd_side.insert(idx + 1, fwd(LaneType::Buffer(buffer))); - } + if tags.is("oneway", "reversible") { + tags.insert("oneway", "yes"); } - - if driving_lane == LaneType::Driving { - let has_parking = vec!["parallel", "diagonal", "perpendicular"]; - let parking_lane_fwd = tags.is_any(osm::PARKING_RIGHT, has_parking.clone()) - || tags.is_any(osm::PARKING_BOTH, has_parking.clone()); - let parking_lane_back = tags.is_any(osm::PARKING_LEFT, has_parking.clone()) - || tags.is_any(osm::PARKING_BOTH, has_parking); - if parking_lane_fwd { - fwd_side.push(fwd(LaneType::Parking)); - } - if parking_lane_back { - back_side.push(back(LaneType::Parking)); - } + if tags.is("highway", "living_street") { + tags.insert("highway", "residential"); } - if tags.is(osm::SIDEWALK, "both") { - fwd_side.push(fwd(LaneType::Sidewalk)); - back_side.push(back(LaneType::Sidewalk)); - } else if tags.is(osm::SIDEWALK, "separate") && cfg.inferred_sidewalks { - // TODO Need to snap separate sidewalks to ways. Until then, just do this. - fwd_side.push(fwd(LaneType::Sidewalk)); - if !back_side.is_empty() { - back_side.push(back(LaneType::Sidewalk)); - } - } else if tags.is(osm::SIDEWALK, "right") { - if cfg.driving_side == DrivingSide::Right { - fwd_side.push(fwd(LaneType::Sidewalk)); - } else { - back_side.push(back(LaneType::Sidewalk)); - } - } else if tags.is(osm::SIDEWALK, "left") { - if cfg.driving_side == DrivingSide::Right { - back_side.push(back(LaneType::Sidewalk)); + if tags.is(osm::SIDEWALK, "separate") && cfg.inferred_sidewalks { + // Make blind guesses + let value = if tags.is("oneway", "yes") { + if cfg.driving_side == DrivingSide::Right { + "right" + } else { + "left" + } } else { - fwd_side.push(fwd(LaneType::Sidewalk)); - } + "both" + }; + tags.insert(osm::SIDEWALK, value); } - // Playing fast-and-loose here (and not checking the lane being modified is a sidewalk) because - // of imminent cutover to osm2lanes, where this will be done way more carefully - if let Some(x) = tags - .get("sidewalk:left:width") - .and_then(|num| num.parse::().ok()) - { - if cfg.driving_side == DrivingSide::Right { - back_side.last_mut().unwrap().width = Distance::meters(x); + // If there's no sidewalk data in OSM already, then make an assumption and mark that it's + // inferred. + if !tags.contains_key(osm::SIDEWALK) && cfg.inferred_sidewalks { + tags.insert(osm::INFERRED_SIDEWALKS, "true"); + + if tags.contains_key("sidewalk:left") || tags.contains_key("sidewalk:right") { + // Attempt to mangle + // https://wiki.openstreetmap.org/wiki/Key:sidewalk#Separately_mapped_sidewalks_on_only_one_side + // into left/right/both. We have to make assumptions for missing values. + let right = !tags.is("sidewalk:right", "no"); + let left = !tags.is("sidewalk:left", "no"); + let value = match (right, left) { + (true, true) => "both", + (true, false) => "right", + (false, true) => "left", + (false, false) => "no", + }; + tags.insert(osm::SIDEWALK, value); + // Remove conflicting values + tags.remove("sidewalk:right"); + tags.remove("sidewalk:left"); + } else if tags.is_any(osm::HIGHWAY, vec!["motorway", "motorway_link"]) + || tags.is_any("junction", vec!["intersection", "roundabout"]) + || tags.is("foot", "no") + || tags.is(osm::HIGHWAY, "service") + // TODO For now, not attempting shared walking/biking paths. + || tags.is_any(osm::HIGHWAY, vec!["cycleway", "pedestrian", "track"]) + { + tags.insert(osm::SIDEWALK, "no"); + } else if tags.is("oneway", "yes") { + if cfg.driving_side == DrivingSide::Right { + tags.insert(osm::SIDEWALK, "right"); + } else { + tags.insert(osm::SIDEWALK, "left"); + } + if tags.is_any(osm::HIGHWAY, vec!["residential", "living_street"]) + && !tags.is("dual_carriageway", "yes") + { + tags.insert(osm::SIDEWALK, "both"); + } } else { - fwd_side.last_mut().unwrap().width = Distance::meters(x); + tags.insert(osm::SIDEWALK, "both"); } } - if let Some(x) = tags - .get("sidewalk:right:width") - .and_then(|num| num.parse::().ok()) + + // Multiple bus schemas + if tags.has_any(vec!["bus:lanes:forward", "bus:lanes:backward"]) + && tags.has_any(vec!["lanes:bus:forward", "lanes:bus:backward"]) { - if cfg.driving_side == DrivingSide::Right { - fwd_side.last_mut().unwrap().width = Distance::meters(x); - } else { - back_side.last_mut().unwrap().width = Distance::meters(x); - } + // Arbitrarily pick one! + tags.remove("lanes:bus:forward"); + tags.remove("lanes:bus:backward"); } - let mut need_fwd_shoulder = fwd_side - .last() - .map(|spec| spec.lt != LaneType::Sidewalk) - .unwrap_or(true); - let mut need_back_shoulder = back_side - .last() - .map(|spec| spec.lt != LaneType::Sidewalk) - .unwrap_or(true); - if tags.is_any( - osm::HIGHWAY, - vec!["motorway", "motorway_link", "construction"], - ) || tags.is("foot", "no") - || tags.is("access", "no") - || tags.is("motorroad", "yes") + // Nothing supports the concept of contraflow cycling without an explicit lane yet, so just + // ignore this + if tags.is("cycleway", "opposite") + && tags.is("oneway", "yes") + && !tags.is("oneway:bicycle", "no") { - need_fwd_shoulder = false; - need_back_shoulder = false; - } - // If it's a one-way, fine to not have sidewalks on both sides. - if tags.is("oneway", "yes") { - need_back_shoulder = false; + tags.remove("cycleway"); } - // For living streets in Krakow, there aren't separate footways. People can walk in the street. - // For now, model that by putting shoulders. - if cfg.inferred_sidewalks || tags.is(osm::HIGHWAY, "living_street") { - if need_fwd_shoulder { - fwd_side.push(fwd(LaneType::Shoulder)); - } - if need_back_shoulder { - back_side.push(back(LaneType::Shoulder)); - } + // Bidirectional 1 lane roads not modelled yet + if tags.is("lanes", "1") && !tags.is("oneway", "yes") { + tags.insert("lanes", "2"); } - LaneSpec::assemble_ltr(fwd_side, back_side, cfg.driving_side) -} - -// See https://wiki.openstreetmap.org/wiki/Proposed_features/cycleway:separation#Typical_values. -// Lots of these mappings are pretty wacky right now. We need more BufferTypes. -#[allow(clippy::ptr_arg)] // Can't chain with `tags.get("foo").and_then` otherwise -fn osm_separation_type(x: &String) -> Option { - match x.as_ref() { - "bollard" | "vertical_panel" => Some(BufferType::FlexPosts), - "kerb" | "separation_kerb" => Some(BufferType::Curb), - "grass_verge" | "planter" | "tree_row" => Some(BufferType::Planters), - "guard_rail" | "jersey_barrier" | "railing" => Some(BufferType::JerseyBarrier), - // TODO Make sure there's a parking lane on that side... also mapped? Any flex posts in - // between? - "parking_lane" => None, - "barred_area" | "dashed_line" | "solid_line" => Some(BufferType::Stripes), - _ => None, + let mut result = osm2lanes::tag::Tags::default(); + for (k, v) in tags.inner() { + result.checked_insert(k.to_string(), v).unwrap(); } + result } -#[cfg(test)] -mod tests { - use super::*; +fn transform_lane( + lane: osm2lanes::road::Lane, + locale: &osm2lanes::locale::Locale, +) -> Vec { + use osm2lanes::road::Lane; - fn tags(kv: Vec<&str>) -> Tags { - let mut tags = Tags::empty(); - for pair in kv { - let parts = pair.split('=').collect::>(); - tags.insert(parts[0], parts[1]); - } - tags - } - - #[test] - fn test_osm_to_specs() { - let mut ok = true; - for (url, input, driving_side, expected_lt, expected_dir) in vec![ - ( - "https://www.openstreetmap.org/way/428294122", - vec![ - "lanes=2", - "oneway=yes", - "sidewalk=both", - "cycleway:left=lane", - ], - DrivingSide::Right, - "sbdds", - "v^^^^", - ), - ( - "https://www.openstreetmap.org/way/8591383", - vec![ - "lanes=1", - "oneway=yes", - "sidewalk=both", - "cycleway:left=track", - "oneway:bicycle=no", - ], - DrivingSide::Right, - "sbbds", - "vv^^^", - ), - ( - // A slight variation of the above, using cycleway:left:oneway=no, which should be - // equivalent - "https://www.openstreetmap.org/way/8591383", - vec![ - "lanes=1", - "oneway=yes", - "sidewalk=both", - "cycleway:left=track", - "cycleway:left:oneway=no", - ], - DrivingSide::Right, - "sbbds", - "vv^^^", - ), - ( - "https://www.openstreetmap.org/way/353690151", - vec![ - "lanes=4", - "sidewalk=both", - "parking:lane:both=parallel", - "cycleway:right=track", - "cycleway:right:oneway=no", - ], - DrivingSide::Right, - "spddddbbps", - "vvvv^^v^^^", - ), - ( - "https://www.openstreetmap.org/way/389654080", - vec![ - "lanes=2", - "sidewalk=both", - "parking:lane:left=parallel", - "parking:lane:right=no_stopping", - "centre_turn_lane=yes", - "cycleway:right=track", - "cycleway:right:oneway=no", - ], - DrivingSide::Right, - "spdCdbbs", - "vvv^^v^^", - ), - ( - "https://www.openstreetmap.org/way/369623526", - vec![ - "lanes=1", - "oneway=yes", - "sidewalk=both", - "parking:lane:right=diagonal", - "cycleway:left=opposite_track", - "oneway:bicycle=no", - ], - DrivingSide::Right, - "sbbdps", - "vv^^^^", - ), - ( - "https://www.openstreetmap.org/way/534549104", - vec![ - "lanes=2", - "oneway=yes", - "sidewalk=both", - "cycleway:right=track", - "cycleway:right:oneway=no", - "oneway:bicycle=no", - ], - DrivingSide::Right, - "sddbbs", - "v^^v^^", - ), - ( - "https://www.openstreetmap.org/way/777565028", - vec!["highway=residential", "oneway=no", "sidewalk=both"], - DrivingSide::Left, - "sdds", - "^^vv", - ), - ( - "https://www.openstreetmap.org/way/224637155", - vec!["lanes=2", "oneway=yes", "sidewalk=left"], - DrivingSide::Left, - "sdd", - "^^^", - ), - ( - "https://www.openstreetmap.org/way/4188078", - vec![ - "lanes=2", - "cycleway:left=lane", - "oneway=yes", - "sidewalk=left", - ], - DrivingSide::Left, - "sbdd", - "^^^^", - ), - ( - "https://www.openstreetmap.org/way/49207928", - vec!["cycleway:right=lane", "sidewalk=both"], - DrivingSide::Left, - "sddbs", - "^^vvv", - ), - // How should an odd number of lanes forward/backwards be split without any clues? - ( - "https://www.openstreetmap.org/way/898731283", - vec!["lanes=3", "sidewalk=both"], - DrivingSide::Left, - "sddds", - "^^^vv", - ), - ( - // I didn't look for a real example of this - "https://www.openstreetmap.org/way/898731283", - vec!["lanes=5", "sidewalk=none"], - DrivingSide::Right, - "SdddddS", - "vvv^^^^", - ), - ( - "https://www.openstreetmap.org/way/335668924", - vec!["lanes=1", "sidewalk=none"], - DrivingSide::Right, - "SddS", - "vv^^", - ), - ] { - let cfg = MapConfig { - driving_side, - bikes_can_use_bus_lanes: true, - inferred_sidewalks: true, - street_parking_spot_length: geom::Distance::meters(8.0), - turn_on_red: true, + let mut lt; + let dir; + match lane { + Lane::Travel { + direction, + designated, + .. + } => { + lt = match designated { + Designated::Foot => LaneType::Sidewalk, + Designated::Motor => LaneType::Driving, + Designated::Bicycle => LaneType::Biking, + Designated::Bus => LaneType::Bus, }; - let actual = get_lane_specs_ltr(&tags(input.clone()), &cfg); - let actual_lt: String = actual.iter().map(|s| s.lt.to_char()).collect(); - let actual_dir: String = actual - .iter() - .map(|s| if s.dir == Direction::Fwd { '^' } else { 'v' }) - .collect(); - if actual_lt != expected_lt || actual_dir != expected_dir { - ok = false; - println!("For input (example from {}):", url); - for kv in input { - println!(" {}", kv); + match direction { + Some(direction) => match direction { + osm2lanes::road::Direction::Forward => { + dir = Direction::Fwd; + } + osm2lanes::road::Direction::Backward => { + dir = Direction::Back; + } + osm2lanes::road::Direction::Both => { + match designated { + Designated::Motor => { + lt = LaneType::SharedLeftTurn; + dir = Direction::Fwd; + } + Designated::Bicycle => { + // Rewrite one bidirectional cycletrack into two lanes + let width = Distance::meters(lane.width(locale).val()); + // TODO driving side and also side of the road??? + let (dir1, dir2) = (Direction::Back, Direction::Fwd); + return vec![ + LaneSpec { + lt: LaneType::Biking, + dir: dir1, + width, + }, + LaneSpec { + lt: LaneType::Biking, + dir: dir2, + width, + }, + ]; + } + x => todo!("dir=both, designated={:?}", x), + } + } + }, + // Fix later + None => { + dir = Direction::Fwd; } - println!("Got:"); - println!(" {}", actual_lt); - println!(" {}", actual_dir); - println!("Expected:"); - println!(" {}", expected_lt); - println!(" {}", expected_dir); - println!(); + }; + } + Lane::Shoulder { .. } => { + lt = LaneType::Shoulder; + // Fix later + dir = Direction::Fwd; + } + Lane::Separator { .. } => { + // TODO Barriers + return Vec::new(); + } + Lane::Parking { + direction, + designated: Designated::Motor, + .. + } => { + lt = LaneType::Parking; + dir = match direction { + osm2lanes::road::Direction::Forward => Direction::Fwd, + osm2lanes::road::Direction::Backward => Direction::Back, + osm2lanes::road::Direction::Both => todo!("dir = both for parking"), } } - assert!(ok); + _ => todo!("handle {:?}", lane), } + let width = Distance::meters(lane.width(locale).val()); + vec![LaneSpec { lt, dir, width }] } diff --git a/raw_map/src/types.rs b/raw_map/src/types.rs index 91a96991fe..0ece6cb34d 100644 --- a/raw_map/src/types.rs +++ b/raw_map/src/types.rs @@ -535,25 +535,4 @@ impl LaneSpec { LaneType::Buffer(BufferType::Curb) => vec![(Distance::meters(0.5), "default")], } } - - /// Put a list of forward and backward lanes into left-to-right order, depending on the driving - /// side. Both input lists should be ordered from the center of the road going outwards. - pub(crate) fn assemble_ltr( - mut fwd_side: Vec, - mut back_side: Vec, - driving_side: DrivingSide, - ) -> Vec { - match driving_side { - DrivingSide::Right => { - back_side.reverse(); - back_side.extend(fwd_side); - back_side - } - DrivingSide::Left => { - fwd_side.reverse(); - fwd_side.extend(back_side); - fwd_side - } - } - } }