tiny start to a new multi-intersection traffic signal editor. just get

the left panel to display some kind of merged view
This commit is contained in:
Dustin Carlino 2020-08-11 13:55:21 -07:00
parent 62828e1ffa
commit c08e244d23
7 changed files with 278 additions and 20 deletions

View File

@ -85,7 +85,7 @@ impl GeomBatch {
} }
/// Compute the bounds of all polygons in this batch. /// Compute the bounds of all polygons in this batch.
fn get_bounds(&self) -> Bounds { pub fn get_bounds(&self) -> Bounds {
let mut bounds = Bounds::new(); let mut bounds = Bounds::new();
for (_, poly) in &self.list { for (_, poly) in &self.list {
bounds.union(poly.get_bounds()); bounds.union(poly.get_bounds());

View File

@ -1,6 +1,7 @@
mod bulk; mod bulk;
mod cluster_traffic_signals; mod cluster_traffic_signals;
mod lanes; mod lanes;
mod new_traffic_signals;
mod routes; mod routes;
mod select; mod select;
mod stop_signs; mod stop_signs;
@ -10,6 +11,7 @@ mod zones;
pub use self::cluster_traffic_signals::ClusterTrafficSignalEditor; pub use self::cluster_traffic_signals::ClusterTrafficSignalEditor;
pub use self::lanes::LaneEditor; pub use self::lanes::LaneEditor;
pub use self::new_traffic_signals::NewTrafficSignalEditor;
pub use self::routes::RouteEditor; pub use self::routes::RouteEditor;
pub use self::stop_signs::StopSignEditor; pub use self::stop_signs::StopSignEditor;
pub use self::traffic_signals::TrafficSignalEditor; pub use self::traffic_signals::TrafficSignalEditor;
@ -641,12 +643,7 @@ pub fn maybe_edit_intersection(
if app.primary.map.maybe_get_traffic_signal(id).is_some() if app.primary.map.maybe_get_traffic_signal(id).is_some()
&& app.per_obj.left_click(ctx, "edit traffic signal") && app.per_obj.left_click(ctx, "edit traffic signal")
{ {
return Some(Box::new(TrafficSignalEditor::new( return Some(TrafficSignalEditor::new(ctx, app, id, mode.clone()));
ctx,
app,
id,
mode.clone(),
)));
} }
if app.primary.map.get_i(id).is_closed() if app.primary.map.get_i(id).is_closed()

View File

@ -0,0 +1,246 @@
use crate::app::App;
use crate::edit::traffic_signals::make_top_panel;
use crate::game::{State, Transition};
use crate::options::TrafficSignalStyle;
use crate::render::draw_signal_phase;
use ezgui::{
hotkey, Btn, Color, Composite, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line,
Outcome, VerticalAlignment, Widget,
};
use geom::{Bounds, Distance, Polygon};
use map_model::{IntersectionID, Phase};
use std::collections::BTreeSet;
pub struct NewTrafficSignalEditor {
side_panel: Composite,
top_panel: Composite,
members: BTreeSet<IntersectionID>,
current_phase: usize,
}
impl NewTrafficSignalEditor {
pub fn new(
ctx: &mut EventCtx,
app: &mut App,
members: BTreeSet<IntersectionID>,
) -> Box<dyn State> {
app.primary.current_selection = None;
Box::new(NewTrafficSignalEditor {
side_panel: make_side_panel(ctx, app, &members, 0),
top_panel: make_top_panel(ctx, app, false, false),
members,
current_phase: 0,
})
}
fn change_phase(&mut self, ctx: &mut EventCtx, app: &App, idx: usize) {
if self.current_phase == idx {
let mut new = make_side_panel(ctx, app, &self.members, self.current_phase);
new.restore(ctx, &self.side_panel);
self.side_panel = new;
} else {
self.current_phase = idx;
self.side_panel = make_side_panel(ctx, app, &self.members, self.current_phase);
// TODO Maybe center of previous member
self.side_panel
.scroll_to_member(ctx, format!("phase {}", idx + 1));
}
}
}
impl State for NewTrafficSignalEditor {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
ctx.canvas_movement();
match self.side_panel.event(ctx) {
Outcome::Clicked(x) => {
if let Some(x) = x.strip_prefix("phase ") {
let idx = x.parse::<usize>().unwrap() - 1;
self.change_phase(ctx, app, idx);
return Transition::Keep;
} else {
unreachable!()
}
}
_ => {}
}
match self.top_panel.event(ctx) {
Outcome::Clicked(x) => match x.as_ref() {
"Finish" => {
return Transition::Pop;
}
// TODO Handle the other things
_ => unreachable!(),
},
_ => {}
}
if self.current_phase != 0 && ctx.input.key_pressed(Key::UpArrow) {
self.change_phase(ctx, app, self.current_phase - 1);
}
// TODO When we enter this state, force all signals to have the same number of phases, so
// we can look up any of them.
let num_phases = self
.members
.iter()
.map(|i| app.primary.map.get_traffic_signal(*i).phases.len())
.max()
.unwrap();
if self.current_phase != num_phases - 1 && ctx.input.key_pressed(Key::DownArrow) {
self.change_phase(ctx, app, self.current_phase + 1);
}
Transition::Keep
}
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.top_panel.draw(g);
self.side_panel.draw(g);
}
}
fn make_side_panel(
ctx: &mut EventCtx,
app: &App,
members: &BTreeSet<IntersectionID>,
selected: usize,
) -> Composite {
let map = &app.primary.map;
let num_phases = members
.iter()
.map(|i| map.get_traffic_signal(*i).phases.len())
.max()
.unwrap();
let mut col = Vec::new();
for idx in 0..num_phases {
// Separator
col.push(
Widget::draw_batch(
ctx,
GeomBatch::from(vec![(
Color::WHITE,
// TODO draw_batch will scale up, but that's inappropriate here, since we're
// depending on window width, which already factors in scale
Polygon::rectangle(0.2 * ctx.canvas.window_width / ctx.get_scale_factor(), 2.0),
)]),
)
.centered_horiz(),
);
let unselected_btn = draw_multiple_signals(ctx, app, members, idx);
let mut selected_btn = unselected_btn.clone();
let bbox = unselected_btn.get_bounds().get_rectangle();
selected_btn.push(Color::RED, bbox.to_outline(Distance::meters(5.0)).unwrap());
let phase_btn = Btn::custom(unselected_btn, selected_btn, bbox).build(
ctx,
format!("phase {}", idx + 1),
None,
);
let phase_col = Widget::col(vec![
Widget::row(vec![
// TODO Print duration
Line(format!("Phase {}", idx + 1)).small_heading().draw(ctx),
Btn::svg_def("system/assets/tools/edit.svg").build(
ctx,
format!("change duration of phase {}", idx + 1),
if selected == idx {
hotkey(Key::X)
} else {
None
},
),
if num_phases > 1 {
Btn::svg_def("system/assets/tools/delete.svg")
.build(ctx, format!("delete phase {}", idx + 1), None)
.align_right()
} else {
Widget::nothing()
},
]),
Widget::row(vec![
phase_btn,
Widget::col(vec![
if idx == 0 {
Btn::text_fg("").inactive(ctx)
} else {
Btn::text_fg("").build(ctx, format!("move up phase {}", idx + 1), None)
},
if idx == num_phases - 1 {
Btn::text_fg("").inactive(ctx)
} else {
Btn::text_fg("").build(ctx, format!("move down phase {}", idx + 1), None)
},
])
.centered_vert()
.align_right(),
]),
])
.padding(10);
if idx == selected {
col.push(phase_col.bg(Color::hex("#2A2A2A")));
} else {
col.push(phase_col);
}
}
Composite::new(Widget::col(col))
.aligned(HorizontalAlignment::Left, VerticalAlignment::Top)
.exact_size_percent(30, 85)
.build(ctx)
}
fn draw_multiple_signals(
ctx: &mut EventCtx,
app: &App,
members: &BTreeSet<IntersectionID>,
idx: usize,
) -> GeomBatch {
let mut batch = GeomBatch::new();
for i in members {
batch.push(
app.cs.normal_intersection,
app.primary.map.get_i(*i).polygon.clone(),
);
draw_signal_phase(
ctx.prerender,
app.primary
.map
.get_traffic_signal(*i)
.phases
.get(idx)
.unwrap_or(&Phase::new()),
*i,
None,
&mut batch,
app,
TrafficSignalStyle::Sidewalks,
);
}
// Transform to a screen-space icon. How much should we scale things down?
batch = batch.autocrop();
let mut zoom: f64 = 1.0;
if true {
// Make the whole thing fit a fixed width
let mut bounds = Bounds::new();
for i in members {
bounds.union(app.primary.map.get_i(*i).polygon.get_bounds());
}
zoom = 300.0 / bounds.width();
} else {
// Don't let any intersection get too small
for i in members {
zoom = zoom.max(150.0 / app.primary.map.get_i(*i).polygon.get_bounds().width());
}
}
batch.scale(zoom)
}

View File

@ -169,12 +169,12 @@ impl State for StopSignEditor {
), ),
}); });
apply_map_edits(ctx, app, edits); apply_map_edits(ctx, app, edits);
return Transition::Replace(Box::new(TrafficSignalEditor::new( return Transition::Replace(TrafficSignalEditor::new(
ctx, ctx,
app, app,
self.id, self.id,
self.mode.clone(), self.mode.clone(),
))); ));
} }
_ => unreachable!(), _ => unreachable!(),
}, },

View File

@ -20,10 +20,10 @@ use std::collections::BTreeSet;
// TODO Warn if there are empty phases or if some turn is completely absent from the signal. // TODO Warn if there are empty phases or if some turn is completely absent from the signal.
pub struct TrafficSignalEditor { pub struct TrafficSignalEditor {
pub i: IntersectionID, i: IntersectionID,
current_phase: usize, current_phase: usize,
composite: Composite, composite: Composite,
pub top_panel: Composite, top_panel: Composite,
mode: GameplayMode, mode: GameplayMode,
groups: Vec<DrawTurnGroup>, groups: Vec<DrawTurnGroup>,
@ -31,8 +31,8 @@ pub struct TrafficSignalEditor {
group_selected: Option<(TurnGroupID, Option<TurnPriority>)>, group_selected: Option<(TurnGroupID, Option<TurnPriority>)>,
// The first ControlTrafficSignal is the original // The first ControlTrafficSignal is the original
pub command_stack: Vec<ControlTrafficSignal>, command_stack: Vec<ControlTrafficSignal>,
pub redo_stack: Vec<ControlTrafficSignal>, redo_stack: Vec<ControlTrafficSignal>,
fade_irrelevant: Drawable, fade_irrelevant: Drawable,
} }
@ -43,7 +43,7 @@ impl TrafficSignalEditor {
app: &mut App, app: &mut App,
id: IntersectionID, id: IntersectionID,
mode: GameplayMode, mode: GameplayMode,
) -> TrafficSignalEditor { ) -> Box<dyn State> {
app.primary.current_selection = None; app.primary.current_selection = None;
let map = &app.primary.map; let map = &app.primary.map;
@ -57,7 +57,7 @@ impl TrafficSignalEditor {
vec![Polygon::convex_hull(holes).into_ring()], vec![Polygon::convex_hull(holes).into_ring()],
); );
TrafficSignalEditor { Box::new(TrafficSignalEditor {
i: id, i: id,
current_phase: 0, current_phase: 0,
composite: make_signal_diagram(ctx, app, id, 0), composite: make_signal_diagram(ctx, app, id, 0),
@ -68,7 +68,7 @@ impl TrafficSignalEditor {
command_stack: Vec::new(), command_stack: Vec::new(),
redo_stack: Vec::new(), redo_stack: Vec::new(),
fade_irrelevant: GeomBatch::from(vec![(app.cs.fade_map_dark, fade_area)]).upload(ctx), fade_irrelevant: GeomBatch::from(vec![(app.cs.fade_map_dark, fade_area)]).upload(ctx),
} })
} }
fn change_phase(&mut self, idx: usize, ctx: &mut EventCtx, app: &App) { fn change_phase(&mut self, idx: usize, ctx: &mut EventCtx, app: &App) {

View File

@ -442,7 +442,7 @@ impl ContextualActions for Actions {
match (id, action.as_ref()) { match (id, action.as_ref()) {
(ID::Intersection(i), "edit traffic signal") => Transition::PushTwice( (ID::Intersection(i), "edit traffic signal") => Transition::PushTwice(
EditMode::new(ctx, app, self.gameplay.clone()), EditMode::new(ctx, app, self.gameplay.clone()),
Box::new(TrafficSignalEditor::new(ctx, app, i, self.gameplay.clone())), TrafficSignalEditor::new(ctx, app, i, self.gameplay.clone()),
), ),
(ID::Intersection(i), "edit stop sign") => Transition::PushTwice( (ID::Intersection(i), "edit stop sign") => Transition::PushTwice(
EditMode::new(ctx, app, self.gameplay.clone()), EditMode::new(ctx, app, self.gameplay.clone()),

View File

@ -1,6 +1,6 @@
use crate::app::{App, ShowEverything}; use crate::app::{App, ShowEverything};
use crate::common::CommonState; use crate::common::CommonState;
use crate::edit::ClusterTrafficSignalEditor; use crate::edit::{ClusterTrafficSignalEditor, NewTrafficSignalEditor};
use crate::game::{DrawBaselayer, PopupMsg, State, Transition}; use crate::game::{DrawBaselayer, PopupMsg, State, Transition};
use crate::helpers::ID; use crate::helpers::ID;
use crate::render::{DrawOptions, BIG_ARROW_THICKNESS}; use crate::render::{DrawOptions, BIG_ARROW_THICKNESS};
@ -39,7 +39,8 @@ impl UberTurnPicker {
.align_right(), .align_right(),
]), ]),
Btn::text_fg("View uber-turns").build_def(ctx, hotkey(Key::Enter)), Btn::text_fg("View uber-turns").build_def(ctx, hotkey(Key::Enter)),
Btn::text_fg("Edit").build_def(ctx, hotkey(Key::E)), Btn::text_fg("Edit (old attempt)").build_def(ctx, None),
Btn::text_fg("Edit (new attempt)").build_def(ctx, hotkey(Key::E)),
Btn::text_fg("Detect all clusters").build_def(ctx, hotkey(Key::D)), Btn::text_fg("Detect all clusters").build_def(ctx, hotkey(Key::D)),
])) ]))
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top) .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
@ -85,7 +86,7 @@ impl State for UberTurnPicker {
true, true,
)); ));
} }
"Edit" => { "Edit (old attempt)" => {
if self.members.len() < 2 { if self.members.len() < 2 {
return Transition::Push(PopupMsg::new( return Transition::Push(PopupMsg::new(
ctx, ctx,
@ -99,6 +100,20 @@ impl State for UberTurnPicker {
&IntersectionCluster::new(self.members.clone(), &app.primary.map).0, &IntersectionCluster::new(self.members.clone(), &app.primary.map).0,
)); ));
} }
"Edit (new attempt)" => {
if self.members.len() < 2 {
return Transition::Push(PopupMsg::new(
ctx,
"Error",
vec!["Select at least two intersections"],
));
}
return Transition::Replace(NewTrafficSignalEditor::new(
ctx,
app,
self.members.clone(),
));
}
"Detect all clusters" => { "Detect all clusters" => {
self.members.clear(); self.members.clear();
for ic in IntersectionCluster::find_all(&app.primary.map) { for ic in IntersectionCluster::find_all(&app.primary.map) {