From 6f3be4463b664c8fb440222f540c76e867195e70 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Tue, 4 Aug 2020 14:27:24 -0700 Subject: [PATCH] start editing route schedules. primitive UI, but getting all the map edits plumbed through --- game/src/edit/mod.rs | 53 ++++++++++-------- game/src/edit/routes.rs | 92 +++++++++++++++++++++++++++++++ game/src/info/bus.rs | 7 ++- game/src/info/mod.rs | 11 ++++ game/src/sandbox/gameplay/mod.rs | 1 + map_model/src/edits.rs | 51 ++++++++++++++++- map_model/src/make/transit.rs | 17 +----- map_model/src/map.rs | 10 ++++ map_model/src/objects/bus_stop.rs | 15 ++++- 9 files changed, 214 insertions(+), 43 deletions(-) create mode 100644 game/src/edit/routes.rs diff --git a/game/src/edit/mod.rs b/game/src/edit/mod.rs index bac1f2a825..acd02a68b5 100644 --- a/game/src/edit/mod.rs +++ b/game/src/edit/mod.rs @@ -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::().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 { 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, } } diff --git a/game/src/edit/routes.rs b/game/src/edit/routes.rs new file mode 100644 index 0000000000..9b2480964e --- /dev/null +++ b/game/src/edit/routes.rs @@ -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 { + 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); + } +} diff --git a/game/src/info/bus.rs b/game/src/info/bus.rs index 8fca89789a..e530d56076 100644 --- a/game/src/info/bus.rs +++ b/game/src/info/bus.rs @@ -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)); } diff --git a/game/src/info/mod.rs b/game/src/info/mod.rs index 44a63b94e1..2fa2e9bcfb 100644 --- a/game/src/info/mod.rs +++ b/game/src/info/mod.rs @@ -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::().unwrap()), + ))), + ); } else { let mut close_panel = true; let t = diff --git a/game/src/sandbox/gameplay/mod.rs b/game/src/sandbox/gameplay/mod.rs index e17b6ae4b5..cb6884a412 100644 --- a/game/src/sandbox/gameplay/mod.rs +++ b/game/src/sandbox/gameplay/mod.rs @@ -174,6 +174,7 @@ impl GameplayMode { } _ => {} }, + EditCmd::ChangeRouteSchedule { .. } => {} } } true diff --git a/map_model/src/edits.rs b/map_model/src/edits.rs index 6371637158..e853f29500 100644 --- a/map_model/src/edits.rs +++ b/map_model/src/edits.rs @@ -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, pub changed_speed_limits: BTreeSet, pub changed_access_restrictions: BTreeSet, + pub changed_routes: BTreeSet, // Edits without these are player generated. pub proposal_description: Vec, @@ -64,6 +65,11 @@ pub enum EditCmd { new_allow_through_traffic: EnumSet, old_allow_through_traffic: EnumSet, }, + ChangeRouteSchedule { + id: BusRouteID, + old: Vec