mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-29 01:13:53 +03:00
start editing route schedules. primitive UI, but getting all the map
edits plumbed through
This commit is contained in:
parent
e68df0ed1a
commit
6f3be4463b
@ -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
92
game/src/edit/routes.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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 =
|
||||
|
@ -174,6 +174,7 @@ impl GameplayMode {
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
EditCmd::ChangeRouteSchedule { .. } => {}
|
||||
}
|
||||
}
|
||||
true
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user