diff --git a/Cargo.lock b/Cargo.lock index e8cfa4ad40..f54b392277 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3763,7 +3763,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" name = "sumo" version = "0.1.0" dependencies = [ + "abstio", "abstutil", + "anyhow", "geom", "map_model", "quick-xml", diff --git a/map_model/src/make/mod.rs b/map_model/src/make/mod.rs index 6d5f10ec4f..38bd80fd7a 100644 --- a/map_model/src/make/mod.rs +++ b/map_model/src/make/mod.rs @@ -3,7 +3,8 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; -use abstutil::{MapName, Parallelism, Tags, Timer}; +use abstio::MapName; +use abstutil::{Parallelism, Tags, Timer}; use geom::{Bounds, Distance, FindClosest, GPSBounds, HashablePt2D, Speed, EPSILON_DIST}; use crate::pathfind::Pathfinder; diff --git a/sumo/Cargo.toml b/sumo/Cargo.toml index 4e13bdcae3..8ccb7e7223 100644 --- a/sumo/Cargo.toml +++ b/sumo/Cargo.toml @@ -5,7 +5,9 @@ authors = ["Dustin Carlino "] edition = "2018" [dependencies] +abstio = { path = "../abstio" } abstutil = { path = "../abstutil" } +anyhow = "1.0.37" geom = { path = "../geom" } map_model = { path = "../map_model" } quick-xml = { version = "0.20.0", features=["serialize"] } diff --git a/sumo/README.md b/sumo/README.md index 6e0703e0ad..34ffc295ce 100644 --- a/sumo/README.md +++ b/sumo/README.md @@ -15,7 +15,7 @@ and [SUMO](https://www.eclipse.org/sumo/). Some of the ideas: A quick SUMO primer. To convert an OSM file into a SUMO network: -`netconvert --osm-files data/input/seattle/osm/montlake.osm --output.street-names -o montlake.net.xml` +`netconvert --osm-files data/input/seattle/osm/montlake.osm --output.street-names --keep-edges.components 1 -o montlake.net.xml` To convert the network into an ABST map: diff --git a/sumo/src/lib.rs b/sumo/src/lib.rs index 317e9539d7..1831269134 100644 --- a/sumo/src/lib.rs +++ b/sumo/src/lib.rs @@ -1,10 +1,13 @@ //! This crate provides a Rust interface to different parts of the [SUMO](https://www.eclipse.org/sumo/) traffic simulator. +#[macro_use] +extern crate anyhow; + use std::collections::BTreeMap; use geom::{Distance, PolyLine, Polygon, Pt2D, Speed}; -pub use self::raw::{Connection, Direction, EdgeID, LaneID, NodeID}; +pub use self::raw::{Connection, Direction, EdgeID, InternalLaneID, LaneID, NodeID}; mod normalize; mod raw; @@ -44,21 +47,22 @@ pub struct Lane { pub length: Distance, pub width: Distance, pub center_line: PolyLine, - pub allow: Vec, + pub allow: Vec, } +/// See https://sumo.dlr.de/docs/Networks/SUMO_Road_Networks.html#internal_edges pub struct InternalEdge { pub id: EdgeID, pub lanes: Vec, } pub struct InternalLane { - pub id: LaneID, + pub id: InternalLaneID, pub index: usize, pub speed: Speed, pub length: Distance, pub center_line: Option, - pub allow: Vec, + pub allow: Vec, } pub struct Junction { @@ -66,6 +70,16 @@ pub struct Junction { pub junction_type: String, pub pt: Pt2D, pub incoming_lanes: Vec, - pub internal_lanes: Vec, + pub internal_lanes: Vec, pub shape: Polygon, } + +#[derive(PartialEq)] +pub enum VehicleClass { + Pedestrian, + Bicycle, + RailUrban, + // TODO Use all values from + // https://sumo.dlr.de/docs/Definition_of_Vehicles,_Vehicle_Types,_and_Routes.html#abstract_vehicle_class + Other(String), +} diff --git a/sumo/src/main.rs b/sumo/src/main.rs index af956134d8..37db0a9a32 100644 --- a/sumo/src/main.rs +++ b/sumo/src/main.rs @@ -2,27 +2,31 @@ use std::collections::{BTreeMap, BTreeSet}; -use abstutil::{CmdArgs, MapName, Tags, Timer}; +use anyhow::{bail, Result}; + +use abstio::MapName; +use abstutil::{CmdArgs, Tags, Timer}; use geom::{Distance, PolyLine}; use map_model::{ osm, raw, AccessRestrictions, Intersection, IntersectionID, IntersectionType, Lane, LaneID, LaneType, Map, Road, RoadID, Turn, TurnID, TurnType, }; -use sumo::{Direction, Network, NodeID}; +use sumo::{Direction, InternalLaneID, Network, NodeID, VehicleClass}; -fn main() { +fn main() -> Result<()> { let mut timer = Timer::new("convert SUMO network"); let mut args = CmdArgs::new(); let input = args.required_free(); args.done(); let network = Network::load(&input, &mut timer).unwrap(); - let map = convert(&input, network); + let map = convert(&input, network)?; map.save(); + Ok(()) } -fn convert(orig_path: &str, network: Network) -> Map { +fn convert(orig_path: &str, network: Network) -> Result { let mut intersections = Vec::new(); let mut ids_intersections: BTreeMap = BTreeMap::new(); for (_, junction) in network.junctions { @@ -33,10 +37,12 @@ fn convert(orig_path: &str, network: Network) -> Map { turns: BTreeSet::new(), elevation: Distance::ZERO, intersection_type: IntersectionType::StopSign, + // TODO Temporary ID. We could consider squeezing SUMO IDs into this scheme. orig_id: osm::NodeID(123), incoming_lanes: Vec::new(), outgoing_lanes: Vec::new(), roads: BTreeSet::new(), + merged: false, }); ids_intersections.insert(junction.id, id); } @@ -59,11 +65,11 @@ fn convert(orig_path: &str, network: Network) -> Map { for lane in &edge.lanes { let lane_id = LaneID(lanes.len()); ids_lanes.insert(lane.id.clone(), lane_id); - let lane_type = if lane.allow == vec!["pedestrian".to_string()] { + let lane_type = if lane.allow == vec![VehicleClass::Pedestrian] { LaneType::Sidewalk - } else if lane.allow == vec!["bicycle".to_string()] { + } else if lane.allow == vec![VehicleClass::Bicycle] { LaneType::Biking - } else if lane.allow == vec!["rail_urban".to_string()] { + } else if lane.allow == vec![VehicleClass::RailUrban] { LaneType::LightRail } else { LaneType::Driving @@ -100,9 +106,10 @@ fn convert(orig_path: &str, network: Network) -> Map { } let parts: Vec<&str> = edge.edge_type.split(".").collect(); // "highway.footway" - if parts.len() == 2 { - tags.insert(parts[0].to_string(), parts[1].to_string()); + if parts.len() != 2 { + bail!("Unknown edge_type {}", edge.edge_type); } + tags.insert(parts[0].to_string(), parts[1].to_string()); let mut lanes_ltr = lanes_rtl; lanes_ltr.reverse(); @@ -111,6 +118,7 @@ fn convert(orig_path: &str, network: Network) -> Map { osm_tags: Tags::new(tags), turn_restrictions: Vec::new(), complicated_turn_restrictions: Vec::new(), + // TODO Temporary ID. We could consider squeezing SUMO IDs into this scheme. orig_id: raw::OriginalRoad::new(123, (456, 789)), speed_limit, access_restrictions: AccessRestrictions::new(), @@ -124,14 +132,17 @@ fn convert(orig_path: &str, network: Network) -> Map { dst_i, }); } else { - // TODO Should we check that the attributes are the same for both directions? + // There's an existing road with the forward direction lanes. We're currently + // processing the reverse direction of that road, so lanes_rtl oriented in the forwards + // direction is already left-to-right. let mut lanes_ltr = lanes_rtl; lanes_ltr.extend(roads[road_id.0].lanes_ltr.clone()); + // TODO Should we check that the attributes are the same for both directions? roads[road_id.0].lanes_ltr = lanes_ltr; } } - let mut internal_lane_geometry: BTreeMap = BTreeMap::new(); + let mut internal_lane_geometry: BTreeMap = BTreeMap::new(); for (_, edge) in network.internal_edges { for lane in edge.lanes { if let Some(pl) = lane.center_line { @@ -180,7 +191,7 @@ fn convert(orig_path: &str, network: Network) -> Map { } } - Map::import_minimal( + Ok(Map::import_minimal( // Double basename because "foo.net.xml" just becomes "foo.net" MapName::new("sumo", &abstutil::basename(abstutil::basename(orig_path))), network.location.converted_boundary, @@ -189,5 +200,5 @@ fn convert(orig_path: &str, network: Network) -> Map { roads, lanes, turns, - ) + )) } diff --git a/sumo/src/normalize.rs b/sumo/src/normalize.rs index 6e72b8f579..4bf76f931b 100644 --- a/sumo/src/normalize.rs +++ b/sumo/src/normalize.rs @@ -1,16 +1,19 @@ //! Transforms a `raw::Network` into a `Network` that's easier to reason about. use std::collections::BTreeMap; -use std::error::Error; + +use anyhow::Result; use abstutil::Timer; use geom::{Distance, PolyLine, Pt2D, Ring}; -use crate::{raw, Edge, InternalEdge, InternalLane, Junction, Lane, Network}; +use crate::{ + raw, Edge, InternalEdge, InternalLane, InternalLaneID, Junction, Lane, LaneID, Network, +}; impl Network { /// Reads a .net.xml file and return the normalized SUMO network. - pub fn load(path: &str, timer: &mut Timer) -> Result> { + pub fn load(path: &str, timer: &mut Timer) -> Result { let raw = raw::Network::parse(path, timer)?; timer.start("normalize"); let network = Network::from_raw(raw); @@ -52,7 +55,7 @@ impl Network { let mut lanes = Vec::new(); for lane in edge.lanes { lanes.push(InternalLane { - id: lane.id, + id: InternalLaneID(lane.id), index: lane.index, speed: lane.speed, length: lane.length, @@ -87,7 +90,7 @@ impl Network { let mut lanes = Vec::new(); for lane in edge.lanes { lanes.push(Lane { - id: lane.id, + id: LaneID(lane.id), index: lane.index, speed: lane.speed, length: lane.length, @@ -117,6 +120,7 @@ impl Network { network } + /// Normalize coordinates to map-space, with Y increasing down. fn fix_coordinates(&mut self) { // I tried netconvert's --flip-y-axis option, but it makes all of the y coordinates // extremely negative. diff --git a/sumo/src/raw.rs b/sumo/src/raw.rs index d869232be4..f643bdb726 100644 --- a/sumo/src/raw.rs +++ b/sumo/src/raw.rs @@ -2,13 +2,13 @@ //! subset of the structures and fields defined at //! are produced. -use std::error::Error; - use abstutil::Timer; use serde::Deserialize; use geom::{Bounds, Distance, GPSBounds, PolyLine, Polygon, Pt2D, Ring, Speed}; +use crate::VehicleClass; + #[derive(Deserialize)] pub struct Network { pub location: Location, @@ -31,9 +31,9 @@ pub struct Location { } impl Network { - pub fn parse(path: &str, timer: &mut Timer) -> Result> { + pub fn parse(path: &str, timer: &mut Timer) -> anyhow::Result { timer.start(format!("read {}", path)); - let bytes = abstutil::slurp_file(path)?; + let bytes = abstio::slurp_file(path)?; let raw_string = std::str::from_utf8(&bytes)?; let network = quick_xml::de::from_str(raw_string)?; timer.stop(format!("read {}", path)); @@ -46,7 +46,9 @@ pub struct EdgeID(pub String); #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize)] pub struct NodeID(String); #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize)] -pub struct LaneID(String); +pub struct LaneID(pub String); +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize)] +pub struct InternalLaneID(pub String); #[derive(Deserialize)] pub struct Type { @@ -54,10 +56,10 @@ pub struct Type { pub priority: usize, pub speed: Speed, pub width: Option, - #[serde(deserialize_with = "parse_list", default)] - pub allow: Vec, - #[serde(deserialize_with = "parse_list", default)] - pub disallow: Vec, + #[serde(deserialize_with = "parse_list_vehicles", default)] + pub allow: Vec, + #[serde(deserialize_with = "parse_list_vehicles", default)] + pub disallow: Vec, } #[derive(Deserialize)] @@ -109,17 +111,18 @@ impl std::default::Default for SpreadType { #[derive(Deserialize)] pub struct Lane { - pub id: LaneID, + /// This could be a LaneID or an InternalLaneID. It'll be distinguished during normalization. + pub id: String, pub index: usize, pub speed: Speed, pub length: Distance, pub width: Option, #[serde(deserialize_with = "parse_pl")] - pub shape: Result, - #[serde(deserialize_with = "parse_list", default)] - pub allow: Vec, - #[serde(deserialize_with = "parse_list", default)] - pub disallow: Vec, + pub shape: anyhow::Result, + #[serde(deserialize_with = "parse_list_vehicles", default)] + pub allow: Vec, + #[serde(deserialize_with = "parse_list_vehicles", default)] + pub disallow: Vec, } #[derive(Deserialize)] @@ -131,8 +134,12 @@ pub struct Junction { pub y: f64, #[serde(rename = "incLanes", deserialize_with = "parse_list_lanes", default)] pub incoming_lanes: Vec, - #[serde(rename = "intLanes", deserialize_with = "parse_list_lanes", default)] - pub internal_lanes: Vec, + #[serde( + rename = "intLanes", + deserialize_with = "parse_list_internal_lanes", + default + )] + pub internal_lanes: Vec, #[serde(deserialize_with = "parse_polygon", default)] pub shape: Option, } @@ -150,7 +157,7 @@ pub struct Connection { pub to: EdgeID, #[serde(rename = "toLane")] pub to_lane: usize, - pub via: Option, + pub via: Option, pub dir: Direction, } impl Connection { @@ -217,10 +224,20 @@ fn parse_gps_bounds<'de, D: serde::Deserializer<'de>>(d: D) -> Result>(d: D) -> Result, D::Error> { +fn parse_list_vehicles<'de, D: serde::Deserializer<'de>>( + d: D, +) -> Result, D::Error> { let raw = ::deserialize(d)?; - let parts: Vec = raw.split(" ").map(|x| x.to_string()).collect(); - Ok(parts) + let mut vehicles = Vec::new(); + for x in raw.split(" ") { + vehicles.push(match x { + "pedestrian" => VehicleClass::Pedestrian, + "bicycle" => VehicleClass::Bicycle, + "rail_urban" => VehicleClass::RailUrban, + other => VehicleClass::Other(other.to_string()), + }); + } + Ok(vehicles) } fn parse_list_lanes<'de, D: serde::Deserializer<'de>>(d: D) -> Result, D::Error> { @@ -229,6 +246,17 @@ fn parse_list_lanes<'de, D: serde::Deserializer<'de>>(d: D) -> Result>( + d: D, +) -> Result, D::Error> { + let raw = ::deserialize(d)?; + let parts: Vec = raw + .split(" ") + .map(|x| InternalLaneID(x.to_string())) + .collect(); + Ok(parts) +} + fn parse_pts<'de, D: serde::Deserializer<'de>>(d: D) -> Result, D::Error> { let raw = ::deserialize(d)?; let mut pts = Vec::new(); @@ -238,7 +266,7 @@ fn parse_pts<'de, D: serde::Deserializer<'de>>(d: D) -> Result, D::Err Ok(pts) } -fn parse_pl<'de, D: serde::Deserializer<'de>>(d: D) -> Result, D::Error> { +fn parse_pl<'de, D: serde::Deserializer<'de>>(d: D) -> Result, D::Error> { let pts = parse_pts(d)?; Ok(PolyLine::new(pts)) } @@ -259,14 +287,14 @@ fn parse_polygon<'de, D: serde::Deserializer<'de>>(d: D) -> Result Result> { +fn parse_pt(pt: &str) -> anyhow::Result { let mut parts = Vec::new(); for x in pt.split(",") { parts.push(x.parse::()?); } // Ignore the Z coordinate if it's there if parts.len() != 2 && parts.len() != 3 { - return Err("not 2 or 3 parts".into()); + bail!("not 2 or 3 parts"); } Ok(Pt2D::new(parts[0], parts[1])) }