Let the user make edits to switch between marked/unmarked crosswalks!

This commit is contained in:
Dustin Carlino 2022-04-26 16:35:16 +01:00
parent 96d7010582
commit dff4e17b28
12 changed files with 410 additions and 140 deletions

View File

@ -0,0 +1,106 @@
use geom::Distance;
use map_model::{EditCmd, IntersectionID, TurnID, TurnType};
use widgetry::mapspace::{ObjectID, World, WorldOutcome};
use widgetry::{
Color, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel, State, TextExt,
VerticalAlignment, Widget,
};
use crate::app::App;
use crate::app::Transition;
use crate::edit::apply_map_edits;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct ID(TurnID);
impl ObjectID for ID {}
pub struct CrosswalkEditor {
id: IntersectionID,
world: World<ID>,
panel: Panel,
}
impl CrosswalkEditor {
pub fn new_state(ctx: &mut EventCtx, app: &mut App, id: IntersectionID) -> Box<dyn State<App>> {
app.primary.current_selection = None;
let map = &app.primary.map;
let mut world = World::bounded(map.get_bounds());
for turn in &map.get_i(id).turns {
if turn.turn_type.pedestrian_crossing() {
let width = Distance::meters(3.0);
let hitbox = if let Some(line) = turn.crosswalk_line() {
line.make_polygons(width)
} else {
turn.geom.make_polygons(width)
};
world
.add(ID(turn.id))
.hitbox(hitbox)
.draw_color(Color::RED.alpha(0.5))
.hover_alpha(0.3)
.clickable()
.build(ctx);
}
}
Box::new(Self {
id,
world,
panel: Panel::new_builder(Widget::col(vec![
Line("Crosswalks editor").small_heading().into_widget(ctx),
"Click a crosswalk to toggle it between marked and unmarked".text_widget(ctx),
Line("Pedestrians can cross using both, but have priority over vehicles at marked zebra crossings").secondary().into_widget(ctx),
ctx.style()
.btn_solid_primary
.text("Finish")
.hotkey(Key::Escape)
.build_def(ctx),
]))
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
.build(ctx),
})
}
}
impl State<App> for CrosswalkEditor {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
if let WorldOutcome::ClickedObject(ID(turn)) = self.world.event(ctx) {
let mut edits = app.primary.map.get_edits().clone();
let old = app.primary.map.get_i_crosswalks_edit(self.id);
let mut new = old.clone();
new.0.insert(
turn,
if old.0[&turn] == TurnType::Crosswalk {
TurnType::UnmarkedCrossing
} else {
TurnType::Crosswalk
},
);
edits.commands.push(EditCmd::ChangeCrosswalks {
i: self.id,
old,
new,
});
apply_map_edits(ctx, app, edits);
return Transition::Replace(Self::new_state(ctx, app, self.id));
}
if let Outcome::Clicked(ref x) = self.panel.event(ctx) {
match x.as_ref() {
"Finish" => {
return Transition::Pop;
}
_ => unreachable!(),
}
}
Transition::Keep
}
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.world.draw(g);
}
}

View File

@ -24,6 +24,7 @@ use crate::common::{tool_panel, CommonState, Warping};
use crate::debug::DebugMode; use crate::debug::DebugMode;
use crate::sandbox::{GameplayMode, SandboxMode, TimeWarpScreen}; use crate::sandbox::{GameplayMode, SandboxMode, TimeWarpScreen};
mod crosswalks;
mod heuristics; mod heuristics;
mod multiple_roads; mod multiple_roads;
mod roads; mod roads;
@ -904,6 +905,7 @@ fn cmd_to_id(cmd: &EditCmd) -> Option<ID> {
match cmd { match cmd {
EditCmd::ChangeRoad { r, .. } => Some(ID::Road(*r)), EditCmd::ChangeRoad { r, .. } => Some(ID::Road(*r)),
EditCmd::ChangeIntersection { i, .. } => Some(ID::Intersection(*i)), EditCmd::ChangeIntersection { i, .. } => Some(ID::Intersection(*i)),
EditCmd::ChangeCrosswalks { i, .. } => Some(ID::Intersection(*i)),
EditCmd::ChangeRouteSchedule { .. } => None, EditCmd::ChangeRouteSchedule { .. } => None,
} }
} }

View File

@ -50,29 +50,37 @@ impl StopSignEditor {
let panel = Panel::new_builder(Widget::col(vec![ let panel = Panel::new_builder(Widget::col(vec![
Line("Stop sign editor").small_heading().into_widget(ctx), Line("Stop sign editor").small_heading().into_widget(ctx),
ctx.style() Widget::row(vec![
.btn_outline ctx.style()
.text("reset to default") .btn_solid_primary
.hotkey(Key::R) .text("Finish")
.disabled( .hotkey(Key::Escape)
&ControlStopSign::new(&app.primary.map, id) .build_def(ctx),
== app.primary.map.get_stop_sign(id), ctx.style()
) .btn_outline
.build_def(ctx), .text("reset to default")
ctx.style() .hotkey(Key::R)
.btn_outline .disabled(
.text("close intersection for construction") &ControlStopSign::new(&app.primary.map, id)
.hotkey(Key::C) == app.primary.map.get_stop_sign(id),
.build_def(ctx), )
ctx.style() .build_def(ctx),
.btn_outline ctx.style()
.text("convert to traffic signal") .btn_outline
.build_def(ctx), .text("Change crosswalks")
ctx.style() .hotkey(Key::C)
.btn_solid_primary .build_def(ctx),
.text("Finish") ]),
.hotkey(Key::Escape) Widget::row(vec![
.build_def(ctx), ctx.style()
.btn_outline
.text("close intersection for construction")
.build_def(ctx),
ctx.style()
.btn_outline
.text("convert to traffic signal")
.build_def(ctx),
]),
])) ]))
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top) .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
.build(ctx); .build(ctx);
@ -154,6 +162,9 @@ impl SimpleState<App> for StopSignEditor {
self.mode.clone(), self.mode.clone(),
)) ))
} }
"Change crosswalks" => Transition::Replace(
super::crosswalks::CrosswalkEditor::new_state(ctx, app, self.id),
),
_ => unreachable!(), _ => unreachable!(),
} }
} }

View File

@ -355,6 +355,14 @@ impl State<App> for TrafficSignalEditor {
); );
} }
} }
"Change crosswalks" => {
// TODO Probably need to follow everything Cancel does
return Transition::Replace(super::crosswalks::CrosswalkEditor::new_state(
ctx,
app,
*self.members.iter().next().unwrap(),
));
}
"Preview" => { "Preview" => {
// Might have to do this first! // Might have to do this first!
app.primary app.primary
@ -535,37 +543,29 @@ impl State<App> for TrafficSignalEditor {
} }
fn make_top_panel(ctx: &mut EventCtx, app: &App, can_undo: bool, can_redo: bool) -> Panel { fn make_top_panel(ctx: &mut EventCtx, app: &App, can_undo: bool, can_redo: bool) -> Panel {
let row = vec![ let mut second_row = vec![ctx
ctx.style() .style()
.btn_solid_primary .btn_outline
.text("Finish") .text("Change crosswalks")
.hotkey(Key::Enter) .hotkey(Key::C)
.build_def(ctx), .build_def(ctx)];
ctx.style() if app.opts.dev {
.btn_outline second_row.push(
.text("Preview") ctx.style()
.hotkey(lctrl(Key::P)) .btn_outline
.build_def(ctx), .text("Export")
ctx.style() .tooltip(Text::from_multiline(vec![
.btn_plain Line("This will create a JSON file in traffic_signal_data/.").small(),
.icon("system/assets/tools/undo.svg") Line(
.disabled(!can_undo) "Contribute this to map how this traffic signal is currently timed in \
.hotkey(lctrl(Key::Z)) real life.",
.build_widget(ctx, "undo"), )
ctx.style() .small(),
.btn_plain ]))
.icon("system/assets/tools/redo.svg") .build_def(ctx),
.disabled(!can_redo) );
// TODO ctrl+shift+Z! }
.hotkey(lctrl(Key::Y))
.build_widget(ctx, "redo"),
ctx.style()
.btn_plain_destructive
.text("Cancel")
.hotkey(Key::Escape)
.build_def(ctx)
.align_right(),
];
Panel::new_builder(Widget::col(vec![ Panel::new_builder(Widget::col(vec![
Widget::row(vec![ Widget::row(vec![
Line("Traffic signal editor") Line("Traffic signal editor")
@ -578,23 +578,38 @@ fn make_top_panel(ctx: &mut EventCtx, app: &App, can_undo: bool, can_redo: bool)
.hotkey(Key::M) .hotkey(Key::M)
.build_widget(ctx, "Edit multiple signals"), .build_widget(ctx, "Edit multiple signals"),
]), ]),
Widget::row(row), Widget::row(vec![
if app.opts.dev { ctx.style()
.btn_solid_primary
.text("Finish")
.hotkey(Key::Enter)
.build_def(ctx),
ctx.style() ctx.style()
.btn_outline .btn_outline
.text("Export") .text("Preview")
.tooltip(Text::from_multiline(vec![ .hotkey(lctrl(Key::P))
Line("This will create a JSON file in traffic_signal_data/.").small(), .build_def(ctx),
Line( ctx.style()
"Contribute this to map how this traffic signal is currently timed in \ .btn_plain
real life.", .icon("system/assets/tools/undo.svg")
) .disabled(!can_undo)
.small(), .hotkey(lctrl(Key::Z))
])) .build_widget(ctx, "undo"),
ctx.style()
.btn_plain
.icon("system/assets/tools/redo.svg")
.disabled(!can_redo)
// TODO ctrl+shift+Z!
.hotkey(lctrl(Key::Y))
.build_widget(ctx, "redo"),
ctx.style()
.btn_plain_destructive
.text("Cancel")
.hotkey(Key::Escape)
.build_def(ctx) .build_def(ctx)
} else { .align_right(),
Widget::nothing() ]),
}, Widget::row(second_row),
])) ]))
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top) .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
.build(ctx) .build(ctx)

View File

@ -191,6 +191,12 @@ impl GameplayMode {
} }
_ => {} _ => {}
}, },
EditCmd::ChangeCrosswalks { .. } => {
// TODO Another hack to see if we can only edit signal timing
if !self.can_edit_stop_signs() {
return false;
}
}
EditCmd::ChangeRouteSchedule { .. } => {} EditCmd::ChangeRouteSchedule { .. } => {}
} }
} }

View File

@ -502,7 +502,7 @@ pub fn make_crosswalk(batch: &mut GeomBatch, turn: &Turn, map: &Map, cs: &ColorS
// crosswalk line itself. Center the lines inside these two boundaries. // crosswalk line itself. Center the lines inside these two boundaries.
let boundary = width; let boundary = width;
let tile_every = width * 0.6; let tile_every = width * 0.6;
let line = if let Some(l) = crosswalk_line(turn) { let line = if let Some(l) = turn.crosswalk_line() {
l l
} else { } else {
return; return;
@ -596,7 +596,7 @@ fn make_unmarked_crossing(batch: &mut GeomBatch, turn: &Turn, map: &Map, cs: &Co
let color = cs.general_road_marking.alpha(0.5); let color = cs.general_road_marking.alpha(0.5);
let band_width = Distance::meters(0.1); let band_width = Distance::meters(0.1);
let total_width = map.get_l(turn.id.src).width; let total_width = map.get_l(turn.id.src).width;
if let Some(line) = crosswalk_line(turn) { if let Some(line) = turn.crosswalk_line() {
if let Ok(slice) = line.slice(total_width, line.length() - total_width) { if let Ok(slice) = line.slice(total_width, line.length() - total_width) {
batch.push( batch.push(
color, color,
@ -614,20 +614,6 @@ fn make_unmarked_crossing(batch: &mut GeomBatch, turn: &Turn, map: &Map, cs: &Co
} }
} }
// The geometry of crosswalks will first cross part of a sidewalk corner, then actually enter the
// road. Extract the piece that's in the road.
fn crosswalk_line(turn: &Turn) -> Option<Line> {
let pts = turn.geom.points();
if pts.len() < 3 {
warn!(
"Not rendering crosswalk for {}; its geometry was squished earlier",
turn.id
);
return None;
}
Line::new(pts[1], pts[2]).ok()
}
// TODO copied from DrawLane // TODO copied from DrawLane
fn perp_line(l: Line, length: Distance) -> Line { fn perp_line(l: Line, length: Distance) -> Line {
let pt1 = l.shift_right(length / 2.0).pt1(); let pt1 = l.shift_right(length / 2.0).pt1();

View File

@ -16,7 +16,8 @@ use crate::make::{match_points_to_lanes, snap_driveway, trim_path};
use crate::{ use crate::{
connectivity, AccessRestrictions, BuildingID, ControlStopSign, ControlTrafficSignal, Direction, connectivity, AccessRestrictions, BuildingID, ControlStopSign, ControlTrafficSignal, Direction,
IntersectionID, IntersectionType, LaneID, LaneSpec, LaneType, Map, MapConfig, Movement, IntersectionID, IntersectionType, LaneID, LaneSpec, LaneType, Map, MapConfig, Movement,
ParkingLotID, PathConstraints, Pathfinder, Road, RoadID, TransitRouteID, TurnID, Zone, ParkingLotID, PathConstraints, Pathfinder, Road, RoadID, TransitRouteID, TurnID, TurnType,
Zone,
}; };
mod compat; mod compat;
@ -38,6 +39,7 @@ pub struct MapEdits {
/// Derived from commands, kept up to date by update_derived /// Derived from commands, kept up to date by update_derived
pub changed_roads: BTreeSet<RoadID>, pub changed_roads: BTreeSet<RoadID>,
pub original_intersections: BTreeMap<IntersectionID, EditIntersection>, pub original_intersections: BTreeMap<IntersectionID, EditIntersection>,
pub original_crosswalks: BTreeMap<IntersectionID, EditCrosswalks>,
pub changed_routes: BTreeSet<TransitRouteID>, pub changed_routes: BTreeSet<TransitRouteID>,
/// Some edits are included in the game by default, in data/system/proposals, as "community /// Some edits are included in the game by default, in data/system/proposals, as "community
@ -62,6 +64,11 @@ pub struct EditRoad {
pub access_restrictions: AccessRestrictions, pub access_restrictions: AccessRestrictions,
} }
/// This must contain all crossing turns at one intersection, each mapped either to Crosswalk or
/// UnmarkedCrossing
#[derive(Debug, Clone, PartialEq)]
pub struct EditCrosswalks(pub BTreeMap<TurnID, TurnType>);
impl EditRoad { impl EditRoad {
pub fn get_orig_from_osm(r: &Road, cfg: &MapConfig) -> EditRoad { pub fn get_orig_from_osm(r: &Road, cfg: &MapConfig) -> EditRoad {
EditRoad { EditRoad {
@ -189,12 +196,18 @@ pub enum EditCmd {
old: Vec<Time>, old: Vec<Time>,
new: Vec<Time>, new: Vec<Time>,
}, },
ChangeCrosswalks {
i: IntersectionID,
old: EditCrosswalks,
new: EditCrosswalks,
},
} }
pub struct EditEffects { pub struct EditEffects {
pub changed_roads: BTreeSet<RoadID>, pub changed_roads: BTreeSet<RoadID>,
pub deleted_lanes: BTreeSet<LaneID>, pub deleted_lanes: BTreeSet<LaneID>,
pub changed_intersections: BTreeSet<IntersectionID>, pub changed_intersections: BTreeSet<IntersectionID>,
// TODO Will we need modified turns?
pub added_turns: BTreeSet<TurnID>, pub added_turns: BTreeSet<TurnID>,
pub deleted_turns: BTreeSet<TurnID>, pub deleted_turns: BTreeSet<TurnID>,
pub changed_parking_lots: BTreeSet<ParkingLotID>, pub changed_parking_lots: BTreeSet<ParkingLotID>,
@ -212,6 +225,7 @@ impl MapEdits {
changed_roads: BTreeSet::new(), changed_roads: BTreeSet::new(),
original_intersections: BTreeMap::new(), original_intersections: BTreeMap::new(),
original_crosswalks: BTreeMap::new(),
changed_routes: BTreeSet::new(), changed_routes: BTreeSet::new(),
} }
} }
@ -271,6 +285,7 @@ impl MapEdits {
fn update_derived(&mut self, map: &Map) { fn update_derived(&mut self, map: &Map) {
self.changed_roads.clear(); self.changed_roads.clear();
self.original_intersections.clear(); self.original_intersections.clear();
self.original_crosswalks.clear();
self.changed_routes.clear(); self.changed_routes.clear();
for cmd in &self.commands { for cmd in &self.commands {
@ -283,6 +298,11 @@ impl MapEdits {
self.original_intersections.insert(*i, old.clone()); self.original_intersections.insert(*i, old.clone());
} }
} }
EditCmd::ChangeCrosswalks { i, ref old, .. } => {
if !self.original_crosswalks.contains_key(i) {
self.original_crosswalks.insert(*i, old.clone());
}
}
EditCmd::ChangeRouteSchedule { id, .. } => { EditCmd::ChangeRouteSchedule { id, .. } => {
self.changed_routes.insert(*id); self.changed_routes.insert(*id);
} }
@ -294,6 +314,8 @@ impl MapEdits {
}); });
self.original_intersections self.original_intersections
.retain(|i, orig| map.get_i_edit(*i) != orig.clone()); .retain(|i, orig| map.get_i_edit(*i) != orig.clone());
self.original_crosswalks
.retain(|i, orig| map.get_i_crosswalks_edit(*i) != orig.clone());
self.changed_routes.retain(|br| { self.changed_routes.retain(|br| {
let r = map.get_tr(*br); let r = map.get_tr(*br);
r.spawn_times != r.orig_spawn_times r.spawn_times != r.orig_spawn_times
@ -316,6 +338,13 @@ impl MapEdits {
new: map.get_i_edit(*i), new: map.get_i_edit(*i),
}); });
} }
for (i, old) in &self.original_crosswalks {
self.commands.push(EditCmd::ChangeCrosswalks {
i: *i,
old: old.clone(),
new: map.get_i_crosswalks_edit(*i),
});
}
for r in &self.changed_routes { for r in &self.changed_routes {
let r = map.get_tr(*r); let r = map.get_tr(*r);
self.commands.push(EditCmd::ChangeRouteSchedule { self.commands.push(EditCmd::ChangeRouteSchedule {
@ -393,6 +422,7 @@ impl EditCmd {
EditIntersection::TrafficSignal(_) => format!("traffic signal #{}", i.0), EditIntersection::TrafficSignal(_) => format!("traffic signal #{}", i.0),
EditIntersection::Closed => format!("close {}", i), EditIntersection::Closed => format!("close {}", i),
}, },
EditCmd::ChangeCrosswalks { i, .. } => format!("crosswalks at {}", i),
EditCmd::ChangeRouteSchedule { id, .. } => { EditCmd::ChangeRouteSchedule { id, .. } => {
format!("reschedule route {}", map.get_tr(*id).short_name) format!("reschedule route {}", map.get_tr(*id).short_name)
} }
@ -469,6 +499,15 @@ impl EditCmd {
recalculate_turns(*i, map, effects); recalculate_turns(*i, map, effects);
} }
} }
EditCmd::ChangeCrosswalks { i, ref new, .. } => {
if map.get_i_crosswalks_edit(*i) == new.clone() {
return;
}
effects.changed_intersections.insert(*i);
for (turn, turn_type) in &new.0 {
map.mut_turn(*turn).turn_type = *turn_type;
}
}
EditCmd::ChangeRouteSchedule { id, new, .. } => { EditCmd::ChangeRouteSchedule { id, new, .. } => {
map.transit_routes[id.0].spawn_times = new.clone(); map.transit_routes[id.0].spawn_times = new.clone();
} }
@ -487,6 +526,11 @@ impl EditCmd {
old: new, old: new,
new: old, new: old,
}, },
EditCmd::ChangeCrosswalks { i, old, new } => EditCmd::ChangeCrosswalks {
i,
old: new,
new: old,
},
EditCmd::ChangeRouteSchedule { id, old, new } => EditCmd::ChangeRouteSchedule { EditCmd::ChangeRouteSchedule { id, old, new } => EditCmd::ChangeRouteSchedule {
id, id,
old: new, old: new,
@ -794,6 +838,16 @@ impl Map {
} }
} }
pub fn get_i_crosswalks_edit(&self, i: IntersectionID) -> EditCrosswalks {
let mut turns = BTreeMap::new();
for turn in &self.get_i(i).turns {
if turn.turn_type.pedestrian_crossing() {
turns.insert(turn.id, turn.turn_type);
}
}
EditCrosswalks(turns)
}
pub fn save_edits(&self) { pub fn save_edits(&self) {
// Don't overwrite the current edits with the compressed first. Otherwise, undo/redo order // Don't overwrite the current edits with the compressed first. Otherwise, undo/redo order
// in the UI gets messed up. // in the UI gets messed up.

View File

@ -7,9 +7,9 @@ use abstio::MapName;
use abstutil::{deserialize_btreemap, serialize_btreemap}; use abstutil::{deserialize_btreemap, serialize_btreemap};
use geom::Time; use geom::Time;
use crate::edits::{EditCmd, EditIntersection, EditRoad, MapEdits}; use crate::edits::{EditCmd, EditCrosswalks, EditIntersection, EditRoad, MapEdits};
use crate::raw::OriginalRoad; use crate::raw::OriginalRoad;
use crate::{osm, ControlStopSign, IntersectionID, Map}; use crate::{osm, ControlStopSign, IntersectionID, Map, MovementID, TurnType};
/// MapEdits are converted to this before serializing. Referencing things like LaneID in a Map won't /// MapEdits are converted to this before serializing. Referencing things like LaneID in a Map won't
/// work if the basemap is rebuilt from new OSM data, so instead we use stabler OSM IDs that're less /// work if the basemap is rebuilt from new OSM data, so instead we use stabler OSM IDs that're less
@ -44,6 +44,15 @@ pub enum PermanentEditIntersection {
Closed, Closed,
} }
#[derive(Serialize, Deserialize, Clone)]
pub struct PermanentEditCrosswalks {
#[serde(
serialize_with = "serialize_btreemap",
deserialize_with = "deserialize_btreemap"
)]
turns: BTreeMap<traffic_signal_data::Turn, TurnType>,
}
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub enum PermanentEditCmd { pub enum PermanentEditCmd {
@ -57,6 +66,11 @@ pub enum PermanentEditCmd {
new: PermanentEditIntersection, new: PermanentEditIntersection,
old: PermanentEditIntersection, old: PermanentEditIntersection,
}, },
ChangeCrosswalks {
i: osm::NodeID,
new: PermanentEditCrosswalks,
old: PermanentEditCrosswalks,
},
ChangeRouteSchedule { ChangeRouteSchedule {
gtfs_id: String, gtfs_id: String,
old: Vec<Time>, old: Vec<Time>,
@ -77,6 +91,11 @@ impl EditCmd {
new: new.to_permanent(map), new: new.to_permanent(map),
old: old.to_permanent(map), old: old.to_permanent(map),
}, },
EditCmd::ChangeCrosswalks { i, new, old } => PermanentEditCmd::ChangeCrosswalks {
i: map.get_i(*i).orig_id,
new: new.to_permanent(map),
old: old.to_permanent(map),
},
EditCmd::ChangeRouteSchedule { id, old, new } => { EditCmd::ChangeRouteSchedule { id, old, new } => {
PermanentEditCmd::ChangeRouteSchedule { PermanentEditCmd::ChangeRouteSchedule {
gtfs_id: map.get_tr(*id).gtfs_id.clone(), gtfs_id: map.get_tr(*id).gtfs_id.clone(),
@ -118,6 +137,18 @@ impl PermanentEditCmd {
.with_context(|| format!("old ChangeIntersection of {} invalid", i))?, .with_context(|| format!("old ChangeIntersection of {} invalid", i))?,
}) })
} }
PermanentEditCmd::ChangeCrosswalks { i, new, old } => {
let id = map.find_i_by_osm_id(i)?;
Ok(EditCmd::ChangeCrosswalks {
i: id,
new: new
.with_permanent(id, map)
.with_context(|| format!("new ChangeCrosswalks of {} invalid", i))?,
old: old
.with_permanent(id, map)
.with_context(|| format!("old ChangeCrosswalks of {} invalid", i))?,
})
}
PermanentEditCmd::ChangeRouteSchedule { gtfs_id, old, new } => { PermanentEditCmd::ChangeRouteSchedule { gtfs_id, old, new } => {
let id = map let id = map
.find_tr_by_gtfs(&gtfs_id) .find_tr_by_gtfs(&gtfs_id)
@ -161,6 +192,7 @@ impl PermanentMapEdits {
changed_roads: BTreeSet::new(), changed_roads: BTreeSet::new(),
original_intersections: BTreeMap::new(), original_intersections: BTreeMap::new(),
original_crosswalks: BTreeMap::new(),
changed_routes: BTreeSet::new(), changed_routes: BTreeSet::new(),
}; };
edits.update_derived(map); edits.update_derived(map);
@ -189,6 +221,7 @@ impl PermanentMapEdits {
changed_roads: BTreeSet::new(), changed_roads: BTreeSet::new(),
original_intersections: BTreeMap::new(), original_intersections: BTreeMap::new(),
original_crosswalks: BTreeMap::new(),
changed_routes: BTreeSet::new(), changed_routes: BTreeSet::new(),
}; };
edits.update_derived(map); edits.update_derived(map);
@ -257,3 +290,40 @@ impl PermanentEditIntersection {
} }
} }
} }
impl EditCrosswalks {
fn to_permanent(&self, map: &Map) -> PermanentEditCrosswalks {
PermanentEditCrosswalks {
turns: self
.0
.iter()
.map(|(id, turn_type)| (id.to_movement(map).to_permanent(map), *turn_type))
.collect(),
}
}
}
impl PermanentEditCrosswalks {
fn with_permanent(self, i: IntersectionID, map: &Map) -> Result<EditCrosswalks> {
let mut turns = BTreeMap::new();
for (id, turn_type) in self.turns {
let movement = MovementID::from_permanent(id, map)?;
// Find all TurnIDs that map to this MovementID
let mut turn_ids = Vec::new();
for turn in &map.get_i(i).turns {
if turn.id.to_movement(map) == movement {
turn_ids.push(turn.id);
}
}
if turn_ids.len() != 1 {
bail!(
"{:?} didn't map to exactly 1 crossing turn: {:?}",
movement,
turn_ids
);
}
turns.insert(turn_ids.pop().unwrap(), turn_type);
}
Ok(EditCrosswalks(turns))
}
}

View File

@ -260,6 +260,14 @@ impl Map {
pub(crate) fn mut_road(&mut self, id: RoadID) -> &mut Road { pub(crate) fn mut_road(&mut self, id: RoadID) -> &mut Road {
&mut self.roads[id.0] &mut self.roads[id.0]
} }
pub(crate) fn mut_turn(&mut self, id: TurnID) -> &mut Turn {
for turn in &mut self.intersections[id.parent.0].turns {
if turn.id == id {
return turn;
}
}
panic!("Couldn't find {id}");
}
pub fn get_i(&self, id: IntersectionID) -> &Intersection { pub fn get_i(&self, id: IntersectionID) -> &Intersection {
&self.intersections[id.0] &self.intersections[id.0]

View File

@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
use abstutil::MultiMap; use abstutil::MultiMap;
use geom::{Angle, Distance, PolyLine, Pt2D}; use geom::{Angle, Distance, PolyLine, Pt2D};
use raw_map::{osm, OriginalRoad};
use crate::{DirectedRoadID, Direction, IntersectionID, Map, TurnID, TurnType}; use crate::{DirectedRoadID, Direction, IntersectionID, Map, TurnID, TurnType};
@ -210,3 +211,50 @@ fn movement_geom(
} }
PolyLine::deduping_new(pts) PolyLine::deduping_new(pts)
} }
impl MovementID {
pub fn to_permanent(&self, map: &Map) -> traffic_signal_data::Turn {
let from = map.get_r(self.from.road).orig_id;
let to = map.get_r(self.to.road).orig_id;
traffic_signal_data::Turn {
from: traffic_signal_data::DirectedRoad {
osm_way_id: from.osm_way_id.0,
osm_node1: from.i1.0,
osm_node2: from.i2.0,
is_forwards: self.from.dir == Direction::Fwd,
},
to: traffic_signal_data::DirectedRoad {
osm_way_id: to.osm_way_id.0,
osm_node1: to.i1.0,
osm_node2: to.i2.0,
is_forwards: self.to.dir == Direction::Fwd,
},
intersection_osm_node_id: map.get_i(self.parent).orig_id.0,
is_crosswalk: self.crosswalk,
}
}
pub fn from_permanent(id: traffic_signal_data::Turn, map: &Map) -> Result<MovementID> {
Ok(MovementID {
from: find_r(id.from, map)?,
to: find_r(id.to, map)?,
parent: map.find_i_by_osm_id(osm::NodeID(id.intersection_osm_node_id))?,
crosswalk: id.is_crosswalk,
})
}
}
fn find_r(id: traffic_signal_data::DirectedRoad, map: &Map) -> Result<DirectedRoadID> {
Ok(DirectedRoadID {
road: map.find_r_by_osm_id(OriginalRoad::new(
id.osm_way_id,
(id.osm_node1, id.osm_node2),
))?,
dir: if id.is_forwards {
Direction::Fwd
} else {
Direction::Back
},
})
}

View File

@ -6,10 +6,8 @@ use serde::{Deserialize, Serialize};
use geom::{Distance, Duration, Speed}; use geom::{Distance, Duration, Speed};
use crate::make::traffic_signals::get_possible_policies; use crate::make::traffic_signals::get_possible_policies;
use crate::raw::OriginalRoad;
use crate::{ use crate::{
osm, DirectedRoadID, Direction, Intersection, IntersectionID, Map, Movement, MovementID, Intersection, IntersectionID, Map, Movement, MovementID, RoadID, TurnID, TurnPriority,
RoadID, TurnID, TurnPriority,
}; };
// The pace to use for crosswalk pace in m/s // The pace to use for crosswalk pace in m/s
@ -391,12 +389,12 @@ impl ControlTrafficSignal {
protected_turns: s protected_turns: s
.protected_movements .protected_movements
.iter() .iter()
.map(|t| export_movement(t, map)) .map(|mvmnt| mvmnt.to_permanent(map))
.collect(), .collect(),
permitted_turns: s permitted_turns: s
.yield_movements .yield_movements
.iter() .iter()
.map(|t| export_movement(t, map)) .map(|mvmnt| mvmnt.to_permanent(map))
.collect(), .collect(),
stage_type: match s.stage_type { stage_type: match s.stage_type {
StageType::Fixed(d) => { StageType::Fixed(d) => {
@ -429,7 +427,7 @@ impl ControlTrafficSignal {
let mut errors = Vec::new(); let mut errors = Vec::new();
let mut protected_movements = BTreeSet::new(); let mut protected_movements = BTreeSet::new();
for t in s.protected_turns { for t in s.protected_turns {
match import_movement(t, map) { match MovementID::from_permanent(t, map) {
Ok(mvmnt) => { Ok(mvmnt) => {
protected_movements.insert(mvmnt); protected_movements.insert(mvmnt);
} }
@ -440,7 +438,7 @@ impl ControlTrafficSignal {
} }
let mut permitted_movements = BTreeSet::new(); let mut permitted_movements = BTreeSet::new();
for t in s.permitted_turns { for t in s.permitted_turns {
match import_movement(t, map) { match MovementID::from_permanent(t, map) {
Ok(mvmnt) => { Ok(mvmnt) => {
permitted_movements.insert(mvmnt); permitted_movements.insert(mvmnt);
} }
@ -479,48 +477,3 @@ impl ControlTrafficSignal {
Ok(ts) Ok(ts)
} }
} }
fn export_movement(id: &MovementID, map: &Map) -> traffic_signal_data::Turn {
let from = map.get_r(id.from.road).orig_id;
let to = map.get_r(id.to.road).orig_id;
traffic_signal_data::Turn {
from: traffic_signal_data::DirectedRoad {
osm_way_id: from.osm_way_id.0,
osm_node1: from.i1.0,
osm_node2: from.i2.0,
is_forwards: id.from.dir == Direction::Fwd,
},
to: traffic_signal_data::DirectedRoad {
osm_way_id: to.osm_way_id.0,
osm_node1: to.i1.0,
osm_node2: to.i2.0,
is_forwards: id.to.dir == Direction::Fwd,
},
intersection_osm_node_id: map.get_i(id.parent).orig_id.0,
is_crosswalk: id.crosswalk,
}
}
fn import_movement(id: traffic_signal_data::Turn, map: &Map) -> Result<MovementID> {
Ok(MovementID {
from: find_r(id.from, map)?,
to: find_r(id.to, map)?,
parent: map.find_i_by_osm_id(osm::NodeID(id.intersection_osm_node_id))?,
crosswalk: id.is_crosswalk,
})
}
fn find_r(id: traffic_signal_data::DirectedRoad, map: &Map) -> Result<DirectedRoadID> {
Ok(DirectedRoadID {
road: map.find_r_by_osm_id(OriginalRoad::new(
id.osm_way_id,
(id.osm_node1, id.osm_node2),
))?,
dir: if id.is_forwards {
Direction::Fwd
} else {
Direction::Back
},
})
}

View File

@ -2,7 +2,7 @@ use std::fmt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use geom::{Angle, PolyLine}; use geom::{Angle, Line, PolyLine};
use crate::raw::RestrictionType; use crate::raw::RestrictionType;
use crate::{ use crate::{
@ -282,6 +282,17 @@ impl Turn {
}, },
}) })
} }
/// Only appropriat for pedestrian crossings. The geometry of crosswalks will first cross part
/// of a sidewalk corner, then actually enter the road. Extract the piece that's in the road.
pub fn crosswalk_line(&self) -> Option<Line> {
let pts = self.geom.points();
if pts.len() < 3 {
warn!("Crosswalk {} was squished earlier", self.id);
return None;
}
Line::new(pts[1], pts[2]).ok()
}
} }
impl TurnID { impl TurnID {