start editing route schedules. primitive UI, but getting all the map

edits plumbed through
This commit is contained in:
Dustin Carlino 2020-08-04 14:27:24 -07:00
parent e68df0ed1a
commit 6f3be4463b
9 changed files with 214 additions and 43 deletions

View File

@ -1,6 +1,7 @@
mod bulk;
mod cluster_traffic_signals;
mod lanes;
mod routes;
mod select;
mod stop_signs;
mod traffic_signals;
@ -9,6 +10,7 @@ mod zones;
pub use self::cluster_traffic_signals::ClusterTrafficSignalEditor;
pub use self::lanes::LaneEditor;
pub use self::routes::RouteEditor;
pub use self::stop_signs::StopSignEditor;
pub use self::traffic_signals::TrafficSignalEditor;
pub use self::validate::{
@ -199,29 +201,32 @@ impl State for EditMode {
}
"undo" => {
let mut edits = app.primary.map.get_edits().clone();
let id = cmd_to_id(&edits.commands.pop().unwrap());
let maybe_id = cmd_to_id(&edits.commands.pop().unwrap());
apply_map_edits(ctx, app, edits);
return Transition::Push(Warping::new(
ctx,
id.canonical_point(&app.primary).unwrap(),
Some(10.0),
Some(id),
&mut app.primary,
));
if let Some(id) = maybe_id {
return Transition::Push(Warping::new(
ctx,
id.canonical_point(&app.primary).unwrap(),
Some(10.0),
Some(id),
&mut app.primary,
));
}
}
x => {
let idx = x["most recent change #".len()..].parse::<usize>().unwrap();
let id = cmd_to_id(
if let Some(id) = cmd_to_id(
&app.primary.map.get_edits().commands
[app.primary.map.get_edits().commands.len() - idx],
);
return Transition::Push(Warping::new(
ctx,
id.canonical_point(&app.primary).unwrap(),
Some(10.0),
Some(id),
&mut app.primary,
));
) {
return Transition::Push(Warping::new(
ctx,
id.canonical_point(&app.primary).unwrap(),
Some(10.0),
Some(id),
&mut app.primary,
));
}
}
},
_ => {}
@ -732,12 +737,14 @@ fn make_changelist(ctx: &mut EventCtx, app: &App) -> Composite {
.build(ctx)
}
fn cmd_to_id(cmd: &EditCmd) -> ID {
// TODO Ideally a Tab.
fn cmd_to_id(cmd: &EditCmd) -> Option<ID> {
match cmd {
EditCmd::ChangeLaneType { id, .. } => ID::Lane(*id),
EditCmd::ReverseLane { l, .. } => ID::Lane(*l),
EditCmd::ChangeSpeedLimit { id, .. } => ID::Road(*id),
EditCmd::ChangeIntersection { i, .. } => ID::Intersection(*i),
EditCmd::ChangeAccessRestrictions { id, .. } => ID::Road(*id),
EditCmd::ChangeLaneType { id, .. } => Some(ID::Lane(*id)),
EditCmd::ReverseLane { l, .. } => Some(ID::Lane(*l)),
EditCmd::ChangeSpeedLimit { id, .. } => Some(ID::Road(*id)),
EditCmd::ChangeIntersection { i, .. } => Some(ID::Intersection(*i)),
EditCmd::ChangeAccessRestrictions { id, .. } => Some(ID::Road(*id)),
EditCmd::ChangeRouteSchedule { .. } => None,
}
}

92
game/src/edit/routes.rs Normal file
View File

@ -0,0 +1,92 @@
use crate::app::App;
use crate::edit::apply_map_edits;
use crate::game::{State, Transition};
use ezgui::{
hotkey, Btn, Composite, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Spinner,
TextExt, VerticalAlignment, Widget,
};
use geom::{Duration, Time};
use map_model::{BusRouteID, EditCmd};
pub struct RouteEditor {
composite: Composite,
route: BusRouteID,
}
impl RouteEditor {
pub fn new(ctx: &mut EventCtx, app: &mut App, id: BusRouteID) -> Box<dyn State> {
app.primary.current_selection = None;
let route = app.primary.map.get_br(id);
Box::new(RouteEditor {
composite: Composite::new(Widget::col(vec![
Widget::row(vec![
Line("Route editor").small_heading().draw(ctx),
Btn::plaintext("X")
.build(ctx, "close", hotkey(Key::Escape))
.align_right(),
]),
Line(&route.full_name).draw(ctx),
// TODO This UI needs design, just something to start plumbing the edits
Widget::row(vec![
"Frequency in minutes".draw_text(ctx),
Spinner::new(ctx, (1, 120), 60).named("freq_mins"),
]),
Btn::text_bg2("Apply").build_def(ctx, hotkey(Key::Enter)),
]))
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
.build(ctx),
route: id,
})
}
}
impl State for RouteEditor {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
ctx.canvas_movement();
match self.composite.event(ctx) {
Outcome::Clicked(x) => match x.as_ref() {
"close" => {
return Transition::Pop;
}
"Apply" => {
let freq = Duration::minutes(self.composite.spinner("freq_mins") as usize);
let mut now = Time::START_OF_DAY;
let mut hourly_times = Vec::new();
while now <= Time::START_OF_DAY + Duration::hours(24) {
hourly_times.push(now);
now += freq;
}
let mut edits = app.primary.map.get_edits().clone();
edits.commands.push(EditCmd::ChangeRouteSchedule {
id: self.route,
old: app.primary.map.get_br(self.route).spawn_times.clone(),
new: hourly_times,
});
apply_map_edits(ctx, app, edits);
// TODO Hacks because we don't have an EditMode underneath us yet
app.primary.dirty_from_edits = true;
ctx.loading_screen("apply edits", |_, mut timer| {
app.primary
.map
.recalculate_pathfinding_after_edits(&mut timer);
});
// TODO Ah and actually we need to reset the sim and everything.
return Transition::Pop;
}
_ => unreachable!(),
},
_ => {}
}
Transition::Keep
}
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.composite.draw(g);
}
}

View File

@ -3,7 +3,7 @@ use crate::common::ColorNetwork;
use crate::helpers::ID;
use crate::info::{header_btns, make_tabs, Details, Tab};
use abstutil::{prettyprint_usize, Counter};
use ezgui::{Btn, Color, EventCtx, Line, RewriteColor, Text, TextExt, Widget};
use ezgui::{hotkey, Btn, Color, EventCtx, Key, Line, RewriteColor, Text, TextExt, Widget};
use geom::{Circle, Distance, Time};
use map_model::{BusRoute, BusRouteID, BusStopID, PathStep};
use sim::{AgentID, CarID};
@ -279,6 +279,11 @@ pub fn route(ctx: &mut EventCtx, app: &App, details: &mut Details, id: BusRouteI
// TODO Soon it'll be time to split into tabs
{
rows.push(Btn::text_fg("Edit schedule").build(
ctx,
format!("edit {}", route.id),
hotkey(Key::E),
));
rows.push(describe_schedule(route).draw(ctx));
}

View File

@ -9,6 +9,7 @@ mod trip;
use crate::app::App;
use crate::common::Warping;
use crate::edit::RouteEditor;
use crate::game::Transition;
use crate::helpers::{color_for_agent_type, copy_to_clipboard, hotkey_btn, ID};
use crate::sandbox::{SandboxMode, TimeWarpScreen};
@ -553,6 +554,16 @@ impl InfoPanel {
));
}
return (false, None);
} else if let Some(x) = action.strip_prefix("edit BusRoute #") {
// TODO Push EditMode too for consistency, but how to get at GameplayMode?
return (
false,
Some(Transition::Push(RouteEditor::new(
ctx,
app,
BusRouteID(x.parse::<usize>().unwrap()),
))),
);
} else {
let mut close_panel = true;
let t =

View File

@ -174,6 +174,7 @@ impl GameplayMode {
}
_ => {}
},
EditCmd::ChangeRouteSchedule { .. } => {}
}
}
true

View File

@ -1,11 +1,11 @@
use crate::raw::{OriginalIntersection, OriginalRoad};
use crate::{
connectivity, ControlStopSign, ControlTrafficSignal, IntersectionID, IntersectionType, LaneID,
LaneType, Map, PathConstraints, RoadID, TurnID, Zone,
connectivity, BusRoute, BusRouteID, ControlStopSign, ControlTrafficSignal, IntersectionID,
IntersectionType, LaneID, LaneType, Map, PathConstraints, RoadID, TurnID, Zone,
};
use abstutil::{deserialize_btreemap, retain_btreemap, retain_btreeset, serialize_btreemap, Timer};
use enumset::EnumSet;
use geom::Speed;
use geom::{Speed, Time};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
@ -20,6 +20,7 @@ pub struct MapEdits {
pub original_intersections: BTreeMap<IntersectionID, EditIntersection>,
pub changed_speed_limits: BTreeSet<RoadID>,
pub changed_access_restrictions: BTreeSet<RoadID>,
pub changed_routes: BTreeSet<BusRouteID>,
// Edits without these are player generated.
pub proposal_description: Vec<String>,
@ -64,6 +65,11 @@ pub enum EditCmd {
new_allow_through_traffic: EnumSet<PathConstraints>,
old_allow_through_traffic: EnumSet<PathConstraints>,
},
ChangeRouteSchedule {
id: BusRouteID,
old: Vec<Time>,
new: Vec<Time>,
},
}
pub struct EditEffects {
@ -87,6 +93,7 @@ impl MapEdits {
original_intersections: BTreeMap::new(),
changed_speed_limits: BTreeSet::new(),
changed_access_restrictions: BTreeSet::new(),
changed_routes: BTreeSet::new(),
}
}
@ -116,6 +123,7 @@ impl MapEdits {
let mut orig_intersections: BTreeMap<IntersectionID, EditIntersection> = BTreeMap::new();
let mut changed_speed_limits = BTreeSet::new();
let mut changed_access_restrictions = BTreeSet::new();
let mut changed_routes = BTreeSet::new();
for cmd in &self.commands {
match cmd {
@ -142,6 +150,9 @@ impl MapEdits {
EditCmd::ChangeAccessRestrictions { id, .. } => {
changed_access_restrictions.insert(*id);
}
EditCmd::ChangeRouteSchedule { id, .. } => {
changed_routes.insert(*id);
}
}
}
@ -156,12 +167,16 @@ impl MapEdits {
let r = map.get_r(*r);
r.access_restrictions_from_osm() != r.allow_through_traffic
});
retain_btreeset(&mut changed_routes, |br| {
map.get_br(*br).spawn_times != BusRoute::default_spawn_times()
});
self.original_lts = orig_lts;
self.reversed_lanes = reversed_lanes;
self.original_intersections = orig_intersections;
self.changed_speed_limits = changed_speed_limits;
self.changed_access_restrictions = changed_access_restrictions;
self.changed_routes = changed_routes;
}
// Assumes update_derived has been called.
@ -285,6 +300,11 @@ enum PermanentEditCmd {
new_allow_through_traffic: EnumSet<PathConstraints>,
old_allow_through_traffic: EnumSet<PathConstraints>,
},
ChangeRouteSchedule {
id: (BusRouteID, String),
old: Vec<Time>,
new: Vec<Time>,
},
}
impl PermanentMapEdits {
@ -332,6 +352,13 @@ impl PermanentMapEdits {
new_allow_through_traffic: *new_allow_through_traffic,
old_allow_through_traffic: *old_allow_through_traffic,
},
EditCmd::ChangeRouteSchedule { id, old, new } => {
PermanentEditCmd::ChangeRouteSchedule {
id: (*id, map.get_br(*id).full_name.clone()),
old: old.clone(),
new: new.clone(),
}
}
})
.collect(),
}
@ -398,6 +425,12 @@ impl PermanentMapEdits {
old_allow_through_traffic,
})
}
PermanentEditCmd::ChangeRouteSchedule { id, old, new } => {
let id = map
.find_br(id.0, &id.1)
.ok_or(format!("can't find {} with full name {}", id.0, id.1))?;
Ok(EditCmd::ChangeRouteSchedule { id, old, new })
}
})
.collect::<Result<Vec<EditCmd>, String>>()?,
@ -406,6 +439,7 @@ impl PermanentMapEdits {
original_intersections: BTreeMap::new(),
changed_speed_limits: BTreeSet::new(),
changed_access_restrictions: BTreeSet::new(),
changed_routes: BTreeSet::new(),
};
edits.update_derived(map);
Ok(edits)
@ -508,6 +542,7 @@ impl EditCmd {
EditCmd::ChangeAccessRestrictions { id, .. } => {
format!("access restrictions for {}", id)
}
EditCmd::ChangeRouteSchedule { id, .. } => format!("reschedule route #{}", id.0),
}
}
@ -632,6 +667,10 @@ impl EditCmd {
effects.changed_intersections.insert(r.dst_i);
true
}
EditCmd::ChangeRouteSchedule { id, new, .. } => {
map.bus_routes[id.0].spawn_times = new.clone();
true
}
}
}
@ -686,6 +725,12 @@ impl EditCmd {
new_allow_through_traffic: *old_allow_through_traffic,
}
.apply(effects, map, timer),
EditCmd::ChangeRouteSchedule { id, old, new } => EditCmd::ChangeRouteSchedule {
id: *id,
old: new.clone(),
new: old.clone(),
}
.apply(effects, map, timer),
}
}
}

View File

@ -4,7 +4,7 @@ use crate::{
BusRoute, BusRouteID, BusStop, BusStopID, LaneID, LaneType, Map, PathConstraints, Position,
};
use abstutil::Timer;
use geom::{Distance, Duration, HashablePt2D, Time};
use geom::{Distance, HashablePt2D};
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::error::Error;
@ -136,7 +136,7 @@ fn make_route(
route_type,
start,
end_border,
spawn_times: default_spawn_times(),
spawn_times: BusRoute::default_spawn_times(),
};
// Make sure the route is connected
@ -329,16 +329,3 @@ fn pick_start_lane(
first_stop.lane()
))
}
fn default_spawn_times() -> Vec<Time> {
// Hourly spawning from midnight to 7, then every 30 minutes till 7, then hourly again
let mut times = Vec::new();
for i in 0..24 {
let hour = Time::START_OF_DAY + Duration::hours(i);
times.push(hour);
if i >= 7 && i <= 19 {
times.push(hour + Duration::minutes(30));
}
}
times
}

View File

@ -585,6 +585,16 @@ impl Map {
None
}
// TODO Should store the OSM relation ID instead.
pub fn find_br(&self, id: BusRouteID, full_name: &str) -> Option<BusRouteID> {
for br in self.all_bus_routes() {
if br.id == id && br.full_name == full_name {
return Some(br.id);
}
}
None
}
pub fn right_shift(&self, pl: PolyLine, width: Distance) -> PolyLine {
self.config.driving_side.right_shift(pl, width)
}

View File

@ -1,6 +1,6 @@
use crate::{LaneID, Map, PathConstraints, PathRequest, Position};
use abstutil::{deserialize_usize, serialize_usize};
use geom::Time;
use geom::{Duration, Time};
use serde::{Deserialize, Serialize};
use std::fmt;
@ -90,4 +90,17 @@ impl BusRoute {
"trains"
}
}
pub fn default_spawn_times() -> Vec<Time> {
// Hourly spawning from midnight to 7, then every 30 minutes till 7, then hourly again
let mut times = Vec::new();
for i in 0..24 {
let hour = Time::START_OF_DAY + Duration::hours(i);
times.push(hour);
if i >= 7 && i <= 19 {
times.push(hour + Duration::minutes(30));
}
}
times
}
}