mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-01 02:33:54 +03:00
move more UIs away from wizards. editing signal offset broke, because
the perma format doesn't encode offset yet
This commit is contained in:
parent
ec756b5167
commit
f61881f6fa
@ -465,9 +465,12 @@ impl Choice<String> {
|
||||
Choice::new(label.to_string(), label.to_string())
|
||||
}
|
||||
|
||||
pub fn strings(list: Vec<String>) -> Vec<Choice<String>> {
|
||||
pub fn strings<I: Into<String>>(list: Vec<I>) -> Vec<Choice<String>> {
|
||||
list.into_iter()
|
||||
.map(|x| Choice::new(x.clone(), x))
|
||||
.map(|x| {
|
||||
let x = x.into();
|
||||
Choice::new(x.clone(), x)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,14 @@ pub mod shared_row;
|
||||
|
||||
use crate::app::{App, ShowLayers, ShowObject};
|
||||
use crate::common::{tool_panel, CommonState, ContextualActions};
|
||||
use crate::game::{msg, ChooseSomething, DrawBaselayer, State, Transition, WizardState};
|
||||
use crate::game::{msg, ChooseSomething, DrawBaselayer, PromptInput, State, Transition};
|
||||
use crate::helpers::ID;
|
||||
use crate::options::OptionsPanel;
|
||||
use crate::render::{calculate_corners, DrawOptions};
|
||||
use abstutil::{Parallelism, Tags, Timer};
|
||||
use ezgui::{
|
||||
hotkey, lctrl, Btn, Checkbox, Choice, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx,
|
||||
HorizontalAlignment, Key, Line, Outcome, Text, UpdateType, VerticalAlignment, Widget, Wizard,
|
||||
HorizontalAlignment, Key, Line, Outcome, Text, UpdateType, VerticalAlignment, Widget,
|
||||
};
|
||||
use geom::{Distance, Pt2D};
|
||||
use map_model::{osm, ControlTrafficSignal, NORMAL_LANE_THICKNESS};
|
||||
@ -211,7 +211,11 @@ impl State for DebugMode {
|
||||
self.reset_info(ctx);
|
||||
}
|
||||
"search OSM metadata" => {
|
||||
return Transition::Push(WizardState::new(Box::new(search_osm)));
|
||||
return Transition::Push(PromptInput::new(
|
||||
ctx,
|
||||
"Search for what?",
|
||||
Box::new(search_osm),
|
||||
));
|
||||
}
|
||||
"clear OSM search results" => {
|
||||
self.search_results = None;
|
||||
@ -363,8 +367,7 @@ impl ShowObject for DebugMode {
|
||||
}
|
||||
}
|
||||
|
||||
fn search_osm(wiz: &mut Wizard, ctx: &mut EventCtx, app: &mut App) -> Option<Transition> {
|
||||
let filter = wiz.wrap(ctx).input_string("Search for what?")?;
|
||||
fn search_osm(filter: String, ctx: &mut EventCtx, app: &mut App) -> Transition {
|
||||
let mut num_matches = 0;
|
||||
let mut batch = GeomBatch::new();
|
||||
|
||||
@ -398,11 +401,11 @@ fn search_osm(wiz: &mut Wizard, ctx: &mut EventCtx, app: &mut App) -> Option<Tra
|
||||
draw: batch.upload(ctx),
|
||||
};
|
||||
|
||||
Some(Transition::PopWithData(Box::new(|state, ctx, _| {
|
||||
Transition::PopWithData(Box::new(|state, ctx, _| {
|
||||
let mut mode = state.downcast_mut::<DebugMode>().unwrap();
|
||||
mode.search_results = Some(results);
|
||||
mode.reset_info(ctx);
|
||||
})))
|
||||
}))
|
||||
}
|
||||
|
||||
struct SearchResults {
|
||||
|
@ -1,11 +1,11 @@
|
||||
use crate::app::{App, ShowEverything};
|
||||
use crate::common::{CityPicker, ColorLegend};
|
||||
use crate::game::{msg, State, Transition, WizardState};
|
||||
use crate::game::{msg, State, Transition};
|
||||
use crate::helpers::{nice_map_name, open_browser, ID};
|
||||
use abstutil::{prettyprint_usize, Tags, Timer};
|
||||
use ezgui::{
|
||||
hotkey, Btn, Checkbox, Choice, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx,
|
||||
HorizontalAlignment, Key, Line, Outcome, Text, TextExt, VerticalAlignment, Widget,
|
||||
HorizontalAlignment, Key, Line, Menu, Outcome, Text, TextExt, VerticalAlignment, Widget,
|
||||
};
|
||||
use geom::{Distance, FindClosest, PolyLine, Polygon};
|
||||
use map_model::{osm, RoadID};
|
||||
@ -20,7 +20,6 @@ pub struct ParkingMapper {
|
||||
draw_layer: Drawable,
|
||||
show: Show,
|
||||
selected: Option<(HashSet<RoadID>, Drawable)>,
|
||||
hide_layer: bool,
|
||||
|
||||
data: BTreeMap<i64, Value>,
|
||||
}
|
||||
@ -182,75 +181,9 @@ impl ParkingMapper {
|
||||
.aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
|
||||
.build(ctx),
|
||||
selected: None,
|
||||
hide_layer: false,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
fn make_wizard(&self, ctx: &mut EventCtx, app: &mut App) -> Box<dyn State> {
|
||||
let show = self.show;
|
||||
let osm_way_id = app
|
||||
.primary
|
||||
.map
|
||||
.get_r(*self.selected.as_ref().unwrap().0.iter().next().unwrap())
|
||||
.orig_id
|
||||
.osm_way_id;
|
||||
let data = self.data.clone();
|
||||
|
||||
let mut state = WizardState::new(Box::new(move |wiz, ctx, app| {
|
||||
let mut wizard = wiz.wrap(ctx);
|
||||
let (_, value) = wizard.choose("What kind of parking does this road have?", || {
|
||||
vec![
|
||||
Choice::new("none -- no stopping or parking", Value::NoStopping),
|
||||
Choice::new("both sides", Value::BothSides),
|
||||
Choice::new("just on the green side", Value::RightOnly),
|
||||
Choice::new("just on the blue side", Value::LeftOnly),
|
||||
Choice::new(
|
||||
"it changes at some point along the road",
|
||||
Value::Complicated,
|
||||
),
|
||||
Choice::new("loading zone on one or both sides", Value::Complicated),
|
||||
]
|
||||
})?;
|
||||
if value == Value::Complicated {
|
||||
wizard.acknowledge("Complicated road", || {
|
||||
vec![
|
||||
"You'll have to manually split the way in ID or JOSM and apply the \
|
||||
appropriate parking tags to each section.",
|
||||
]
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut new_data = data.clone();
|
||||
new_data.insert(osm_way_id, value);
|
||||
Some(Transition::PopThenReplace(ParkingMapper::make(
|
||||
ctx, app, show, new_data,
|
||||
)))
|
||||
}));
|
||||
state.downcast_mut::<WizardState>().unwrap().custom_pop = Some(Transition::PopThenReplace(
|
||||
ParkingMapper::make(ctx, app, self.show, self.data.clone()),
|
||||
));
|
||||
|
||||
let mut batch = GeomBatch::new();
|
||||
let map = &app.primary.map;
|
||||
let thickness = Distance::meters(2.0);
|
||||
for id in &self.selected.as_ref().unwrap().0 {
|
||||
let r = map.get_r(*id);
|
||||
batch.push(
|
||||
Color::GREEN,
|
||||
map.right_shift(r.center_pts.clone(), r.get_half_width(map))
|
||||
.make_polygons(thickness),
|
||||
);
|
||||
batch.push(
|
||||
Color::BLUE,
|
||||
map.left_shift(r.center_pts.clone(), r.get_half_width(map))
|
||||
.make_polygons(thickness),
|
||||
);
|
||||
}
|
||||
state.downcast_mut::<WizardState>().unwrap().also_draw =
|
||||
Some((ctx.upload(batch.clone()), ctx.upload(batch)));
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
impl State for ParkingMapper {
|
||||
@ -340,8 +273,13 @@ impl State for ParkingMapper {
|
||||
}
|
||||
}
|
||||
if self.selected.is_some() && app.per_obj.left_click(ctx, "map parking") {
|
||||
self.hide_layer = true;
|
||||
return Transition::Push(self.make_wizard(ctx, app));
|
||||
return Transition::Push(ChangeWay::new(
|
||||
ctx,
|
||||
app,
|
||||
&self.selected.as_ref().unwrap().0,
|
||||
self.show,
|
||||
self.data.clone(),
|
||||
));
|
||||
}
|
||||
if self.selected.is_some() && ctx.input.key_pressed(Key::N) {
|
||||
let osm_way_id = app
|
||||
@ -437,9 +375,7 @@ impl State for ParkingMapper {
|
||||
}
|
||||
|
||||
fn draw(&self, g: &mut GfxCtx, _: &App) {
|
||||
if !self.hide_layer {
|
||||
g.redraw(&self.draw_layer);
|
||||
}
|
||||
g.redraw(&self.draw_layer);
|
||||
if let Some((_, ref roads)) = self.selected {
|
||||
g.redraw(roads);
|
||||
}
|
||||
@ -447,6 +383,125 @@ impl State for ParkingMapper {
|
||||
}
|
||||
}
|
||||
|
||||
struct ChangeWay {
|
||||
composite: Composite,
|
||||
draw: Drawable,
|
||||
osm_way_id: i64,
|
||||
data: BTreeMap<i64, Value>,
|
||||
show: Show,
|
||||
}
|
||||
|
||||
impl ChangeWay {
|
||||
fn new(
|
||||
ctx: &mut EventCtx,
|
||||
app: &App,
|
||||
selected: &HashSet<RoadID>,
|
||||
show: Show,
|
||||
data: BTreeMap<i64, Value>,
|
||||
) -> Box<dyn State> {
|
||||
let map = &app.primary.map;
|
||||
let osm_way_id = map
|
||||
.get_r(*selected.iter().next().unwrap())
|
||||
.orig_id
|
||||
.osm_way_id;
|
||||
|
||||
let mut batch = GeomBatch::new();
|
||||
let thickness = Distance::meters(2.0);
|
||||
for id in selected {
|
||||
let r = map.get_r(*id);
|
||||
batch.push(
|
||||
Color::GREEN,
|
||||
map.right_shift(r.center_pts.clone(), r.get_half_width(map))
|
||||
.make_polygons(thickness),
|
||||
);
|
||||
batch.push(
|
||||
Color::BLUE,
|
||||
map.left_shift(r.center_pts.clone(), r.get_half_width(map))
|
||||
.make_polygons(thickness),
|
||||
);
|
||||
}
|
||||
|
||||
Box::new(ChangeWay {
|
||||
composite: Composite::new(Widget::col(vec![
|
||||
Widget::row(vec![
|
||||
Line("What kind of parking does this road have?")
|
||||
.small_heading()
|
||||
.draw(ctx),
|
||||
Btn::plaintext("X")
|
||||
.build(ctx, "close", hotkey(Key::Escape))
|
||||
.align_right(),
|
||||
]),
|
||||
Menu::new(
|
||||
ctx,
|
||||
vec![
|
||||
Choice::new("none -- no stopping or parking", Value::NoStopping),
|
||||
Choice::new("both sides", Value::BothSides),
|
||||
Choice::new("just on the green side", Value::RightOnly),
|
||||
Choice::new("just on the blue side", Value::LeftOnly),
|
||||
Choice::new(
|
||||
"it changes at some point along the road",
|
||||
Value::Complicated,
|
||||
),
|
||||
Choice::new("loading zone on one or both sides", Value::Complicated),
|
||||
],
|
||||
)
|
||||
.named("menu"),
|
||||
]))
|
||||
.build(ctx),
|
||||
draw: ctx.upload(batch),
|
||||
osm_way_id,
|
||||
data,
|
||||
show,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl State for ChangeWay {
|
||||
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" => Transition::Pop,
|
||||
_ => {
|
||||
let value = self
|
||||
.composite
|
||||
.menu::<Value>("menu")
|
||||
.current_choice()
|
||||
.clone();
|
||||
if value == Value::Complicated {
|
||||
Transition::Replace(msg(
|
||||
"Complicated road",
|
||||
vec![
|
||||
"You'll have to manually split the way in ID or JOSM and apply \
|
||||
the appropriate parking tags to each section.",
|
||||
],
|
||||
))
|
||||
} else {
|
||||
self.data.insert(self.osm_way_id, value);
|
||||
Transition::PopThenReplace(ParkingMapper::make(
|
||||
ctx,
|
||||
app,
|
||||
self.show,
|
||||
self.data.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
if ctx.normal_left_click() && ctx.canvas.get_cursor_in_screen_space().is_none() {
|
||||
return Transition::Pop;
|
||||
}
|
||||
Transition::Keep
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&self, g: &mut GfxCtx, _: &App) {
|
||||
g.redraw(&self.draw);
|
||||
self.composite.draw(g);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn generate_osmc(_: &BTreeMap<i64, Value>, _: bool, _: &mut Timer) -> Result<(), Box<dyn Error>> {
|
||||
Err("Woops, mapping mode isn't supported on the web yet"
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::app::{App, ShowEverything};
|
||||
use crate::common::CommonState;
|
||||
use crate::game::{ChooseSomething, DrawBaselayer, State, Transition, WizardState};
|
||||
use crate::game::{ChooseSomething, DrawBaselayer, PromptInput, State, Transition};
|
||||
use crate::render::DrawOptions;
|
||||
use ezgui::{
|
||||
hotkey, lctrl, Btn, Choice, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx,
|
||||
@ -180,16 +180,19 @@ impl State for StoryMapEditor {
|
||||
}
|
||||
"save" => {
|
||||
if self.story.name == "new story" {
|
||||
return Transition::Push(WizardState::new(Box::new(|wiz, ctx, _| {
|
||||
let name = wiz.wrap(ctx).input_string("Name this story map")?;
|
||||
Some(Transition::PopWithData(Box::new(move |state, ctx, app| {
|
||||
let editor = state.downcast_mut::<StoryMapEditor>().unwrap();
|
||||
editor.story.name = name;
|
||||
editor.story.save(app);
|
||||
editor.dirty = false;
|
||||
editor.redo_panel(ctx);
|
||||
})))
|
||||
})));
|
||||
return Transition::Push(PromptInput::new(
|
||||
ctx,
|
||||
"Name this story map",
|
||||
Box::new(|name, _, _| {
|
||||
Transition::PopWithData(Box::new(move |state, ctx, app| {
|
||||
let editor = state.downcast_mut::<StoryMapEditor>().unwrap();
|
||||
editor.story.name = name;
|
||||
editor.story.save(app);
|
||||
editor.dirty = false;
|
||||
editor.redo_panel(ctx);
|
||||
}))
|
||||
}),
|
||||
));
|
||||
} else {
|
||||
self.story.save(app);
|
||||
self.dirty = false;
|
||||
|
@ -1,15 +1,15 @@
|
||||
use crate::app::{App, ShowEverything};
|
||||
use crate::common::CommonState;
|
||||
use crate::edit::{apply_map_edits, check_sidewalk_connectivity, StopSignEditor};
|
||||
use crate::game::{msg, ChooseSomething, DrawBaselayer, State, Transition, WizardState};
|
||||
use crate::game::{msg, ChooseSomething, DrawBaselayer, State, Transition};
|
||||
use crate::render::{
|
||||
draw_signal_phase, make_signal_diagram, DrawOptions, DrawTurnGroup, BIG_ARROW_THICKNESS,
|
||||
};
|
||||
use crate::sandbox::{spawn_agents_around, GameplayMode, SpeedControls, TimePanel};
|
||||
use abstutil::Timer;
|
||||
use ezgui::{
|
||||
hotkey, lctrl, Btn, Choice, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx,
|
||||
HorizontalAlignment, Key, Line, Outcome, RewriteColor, Text, TextExt, UpdateType,
|
||||
hotkey, lctrl, Btn, Checkbox, Choice, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx,
|
||||
HorizontalAlignment, Key, Line, Outcome, RewriteColor, Spinner, Text, TextExt, UpdateType,
|
||||
VerticalAlignment, Widget,
|
||||
};
|
||||
use geom::{ArrowCap, Distance, Duration, Polygon};
|
||||
@ -108,12 +108,24 @@ impl State for TrafficSignalEditor {
|
||||
Outcome::Clicked(x) => match x.as_ref() {
|
||||
"Edit entire signal" => {
|
||||
return Transition::Push(edit_entire_signal(
|
||||
ctx,
|
||||
app,
|
||||
self.i,
|
||||
self.mode.clone(),
|
||||
self.command_stack.get(0).cloned(),
|
||||
));
|
||||
}
|
||||
"Change signal offset" => {
|
||||
let mut new_signal = orig_signal.clone();
|
||||
new_signal.offset = Duration::seconds(self.composite.spinner("offset") as f64);
|
||||
|
||||
self.command_stack.push(orig_signal.clone());
|
||||
self.redo_stack.clear();
|
||||
self.top_panel = make_top_panel(ctx, app, true, false);
|
||||
app.primary.map.incremental_edit_traffic_signal(new_signal);
|
||||
self.change_phase(self.current_phase, ctx, app);
|
||||
return Transition::Keep;
|
||||
}
|
||||
"Add new phase" => {
|
||||
let mut new_signal = orig_signal.clone();
|
||||
new_signal.phases.push(Phase::new());
|
||||
@ -128,7 +140,7 @@ impl State for TrafficSignalEditor {
|
||||
x => {
|
||||
if let Some(x) = x.strip_prefix("change duration of phase ") {
|
||||
let idx = x.parse::<usize>().unwrap() - 1;
|
||||
return Transition::Push(change_duration(app, self.i, idx));
|
||||
return Transition::Push(ChangeDuration::new(ctx, app, self.i, idx));
|
||||
}
|
||||
if let Some(x) = x.strip_prefix("delete phase ") {
|
||||
let idx = x.parse::<usize>().unwrap() - 1;
|
||||
@ -469,6 +481,7 @@ pub fn make_top_panel(ctx: &mut EventCtx, app: &App, can_undo: bool, can_redo: b
|
||||
}
|
||||
|
||||
fn edit_entire_signal(
|
||||
ctx: &mut EventCtx,
|
||||
app: &App,
|
||||
i: IntersectionID,
|
||||
mode: GameplayMode,
|
||||
@ -479,64 +492,62 @@ fn edit_entire_signal(
|
||||
.map
|
||||
.get_turns_in_intersection(i)
|
||||
.any(|t| t.between_sidewalks());
|
||||
let current_offset = app.primary.map.get_traffic_signal(i).offset;
|
||||
|
||||
WizardState::new(Box::new(move |wiz, ctx, app| {
|
||||
let use_template = "use template";
|
||||
let all_walk = "add an all-walk phase at the end";
|
||||
let stop_sign = "convert to stop signs";
|
||||
let close = "close intersection for construction";
|
||||
let offset = "edit signal offset";
|
||||
let reset = "reset to default";
|
||||
let use_template = "use template";
|
||||
let all_walk = "add an all-walk phase at the end";
|
||||
let stop_sign = "convert to stop signs";
|
||||
let close = "close intersection for construction";
|
||||
let reset = "reset to default";
|
||||
|
||||
let mut choices = vec![use_template];
|
||||
if has_sidewalks {
|
||||
choices.push(all_walk);
|
||||
}
|
||||
// TODO Conflating stop signs and construction here
|
||||
if mode.can_edit_stop_signs() {
|
||||
choices.push(stop_sign);
|
||||
choices.push(close);
|
||||
}
|
||||
choices.push(offset);
|
||||
choices.push(reset);
|
||||
let mut choices = vec![use_template];
|
||||
if has_sidewalks {
|
||||
choices.push(all_walk);
|
||||
}
|
||||
// TODO Conflating stop signs and construction here
|
||||
if mode.can_edit_stop_signs() {
|
||||
choices.push(stop_sign);
|
||||
choices.push(close);
|
||||
}
|
||||
choices.push(reset);
|
||||
|
||||
let mut wizard = wiz.wrap(ctx);
|
||||
match wizard.choose_string("", move || choices.clone())?.as_str() {
|
||||
x if x == use_template => {
|
||||
let (_, new_signal) =
|
||||
wizard.choose("Use which preset for this intersection?", || {
|
||||
Choice::from(ControlTrafficSignal::get_possible_policies(
|
||||
&app.primary.map,
|
||||
i,
|
||||
&mut Timer::throwaway(),
|
||||
))
|
||||
})?;
|
||||
Some(Transition::PopWithData(Box::new(move |state, ctx, app| {
|
||||
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
|
||||
editor
|
||||
.command_stack
|
||||
.push(app.primary.map.get_traffic_signal(editor.i).clone());
|
||||
editor.redo_stack.clear();
|
||||
editor.top_panel = make_top_panel(ctx, app, true, false);
|
||||
app.primary.map.incremental_edit_traffic_signal(new_signal);
|
||||
editor.change_phase(0, ctx, app);
|
||||
})))
|
||||
}
|
||||
x if x == all_walk => {
|
||||
Some(Transition::PopWithData(Box::new(move |state, ctx, app| {
|
||||
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
|
||||
let orig_signal = app.primary.map.get_traffic_signal(editor.i);
|
||||
let mut new_signal = orig_signal.clone();
|
||||
if new_signal.convert_to_ped_scramble() {
|
||||
editor.command_stack.push(orig_signal.clone());
|
||||
ChooseSomething::new(
|
||||
ctx,
|
||||
"What do you want to change?",
|
||||
Choice::strings(choices),
|
||||
Box::new(move |x, ctx, app| match x.as_str() {
|
||||
x if x == use_template => Transition::Replace(ChooseSomething::new(
|
||||
ctx,
|
||||
"Use which preset for this intersection?",
|
||||
Choice::from(ControlTrafficSignal::get_possible_policies(
|
||||
&app.primary.map,
|
||||
i,
|
||||
&mut Timer::throwaway(),
|
||||
)),
|
||||
Box::new(move |new_signal, _, _| {
|
||||
Transition::PopWithData(Box::new(move |state, ctx, app| {
|
||||
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
|
||||
editor
|
||||
.command_stack
|
||||
.push(app.primary.map.get_traffic_signal(editor.i).clone());
|
||||
editor.redo_stack.clear();
|
||||
editor.top_panel = make_top_panel(ctx, app, true, false);
|
||||
app.primary.map.incremental_edit_traffic_signal(new_signal);
|
||||
editor.change_phase(0, ctx, app);
|
||||
}
|
||||
})))
|
||||
}
|
||||
}))
|
||||
}),
|
||||
)),
|
||||
x if x == all_walk => Transition::PopWithData(Box::new(move |state, ctx, app| {
|
||||
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
|
||||
let orig_signal = app.primary.map.get_traffic_signal(editor.i);
|
||||
let mut new_signal = orig_signal.clone();
|
||||
if new_signal.convert_to_ped_scramble() {
|
||||
editor.command_stack.push(orig_signal.clone());
|
||||
editor.redo_stack.clear();
|
||||
editor.top_panel = make_top_panel(ctx, app, true, false);
|
||||
app.primary.map.incremental_edit_traffic_signal(new_signal);
|
||||
editor.change_phase(0, ctx, app);
|
||||
}
|
||||
})),
|
||||
x if x == stop_sign => {
|
||||
// First restore the original signal
|
||||
if let Some(ref orig) = orig_signal {
|
||||
@ -552,12 +563,7 @@ fn edit_entire_signal(
|
||||
new: EditIntersection::StopSign(ControlStopSign::new(&app.primary.map, i)),
|
||||
});
|
||||
apply_map_edits(ctx, app, edits);
|
||||
Some(Transition::PopThenReplace(Box::new(StopSignEditor::new(
|
||||
ctx,
|
||||
app,
|
||||
i,
|
||||
mode.clone(),
|
||||
))))
|
||||
Transition::PopThenReplace(Box::new(StopSignEditor::new(ctx, app, i, mode.clone())))
|
||||
}
|
||||
x if x == close => {
|
||||
// First restore the original signal
|
||||
@ -573,33 +579,17 @@ fn edit_entire_signal(
|
||||
new: EditIntersection::Closed,
|
||||
};
|
||||
if let Some(err) = check_sidewalk_connectivity(ctx, app, cmd.clone()) {
|
||||
Some(Transition::Replace(err))
|
||||
Transition::Replace(err)
|
||||
} else {
|
||||
let mut edits = app.primary.map.get_edits().clone();
|
||||
edits.commands.push(cmd);
|
||||
apply_map_edits(ctx, app, edits);
|
||||
|
||||
Some(Transition::PopTwice)
|
||||
Transition::PopTwice
|
||||
}
|
||||
}
|
||||
x if x == offset => {
|
||||
let new_duration = wizard.input_usize_prefilled(
|
||||
"What should the offset of this traffic signal be (seconds)?",
|
||||
format!("{}", current_offset.inner_seconds() as usize),
|
||||
)?;
|
||||
Some(Transition::PopWithData(Box::new(move |state, ctx, app| {
|
||||
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
|
||||
let mut signal = app.primary.map.get_traffic_signal(editor.i).clone();
|
||||
editor.command_stack.push(signal.clone());
|
||||
editor.redo_stack.clear();
|
||||
editor.top_panel = make_top_panel(ctx, app, true, false);
|
||||
signal.offset = Duration::seconds(new_duration as f64);
|
||||
app.primary.map.incremental_edit_traffic_signal(signal);
|
||||
editor.change_phase(editor.current_phase, ctx, app);
|
||||
})))
|
||||
}
|
||||
x if x == reset => {
|
||||
Some(Transition::PopWithData(Box::new(move |state, ctx, app| {
|
||||
Transition::PopWithData(Box::new(move |state, ctx, app| {
|
||||
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
|
||||
let orig_signal = app.primary.map.get_traffic_signal(editor.i);
|
||||
let new_signal = ControlTrafficSignal::get_possible_policies(
|
||||
@ -617,59 +607,109 @@ fn edit_entire_signal(
|
||||
editor.current_phase = 0;
|
||||
editor.composite =
|
||||
make_signal_diagram(ctx, app, editor.i, editor.current_phase, true);
|
||||
})))
|
||||
}))
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn change_duration(app: &App, i: IntersectionID, idx: usize) -> Box<dyn State> {
|
||||
let current_type = app.primary.map.get_traffic_signal(i).phases[idx]
|
||||
.phase_type
|
||||
.clone();
|
||||
struct ChangeDuration {
|
||||
composite: Composite,
|
||||
idx: usize,
|
||||
}
|
||||
|
||||
// TODO This UI shouldn't be a wizard
|
||||
WizardState::new(Box::new(move |wiz, ctx, _| {
|
||||
let mut wizard = wiz.wrap(ctx);
|
||||
let new_duration = Duration::seconds(wizard.input_something(
|
||||
"How long should this phase be (seconds)?",
|
||||
Some(format!(
|
||||
"{}",
|
||||
current_type.simple_duration().inner_seconds() as usize
|
||||
)),
|
||||
Box::new(|line| {
|
||||
line.parse::<usize>()
|
||||
.ok()
|
||||
.and_then(|n| if n != 0 { Some(n) } else { None })
|
||||
}),
|
||||
)? as f64);
|
||||
let fixed = format!("Fixed: always {}", new_duration);
|
||||
let adaptive = format!(
|
||||
"Adaptive: some multiple of {}, based on current demand",
|
||||
new_duration
|
||||
);
|
||||
let choice = wizard.choose_string("How should this phase be timed?", move || {
|
||||
vec![fixed.clone(), adaptive.clone()]
|
||||
})?;
|
||||
let new_type = if choice.starts_with("Fixed") {
|
||||
PhaseType::Fixed(new_duration)
|
||||
} else {
|
||||
PhaseType::Adaptive(new_duration)
|
||||
};
|
||||
Some(Transition::PopWithData(Box::new(move |state, ctx, app| {
|
||||
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
|
||||
let orig_signal = app.primary.map.get_traffic_signal(editor.i);
|
||||
impl ChangeDuration {
|
||||
fn new(ctx: &mut EventCtx, app: &App, i: IntersectionID, idx: usize) -> Box<dyn State> {
|
||||
let current = app.primary.map.get_traffic_signal(i).phases[idx]
|
||||
.phase_type
|
||||
.clone();
|
||||
|
||||
let mut new_signal = orig_signal.clone();
|
||||
new_signal.phases[idx].phase_type = new_type;
|
||||
editor.command_stack.push(orig_signal.clone());
|
||||
editor.redo_stack.clear();
|
||||
editor.top_panel = make_top_panel(ctx, app, true, false);
|
||||
app.primary.map.incremental_edit_traffic_signal(new_signal);
|
||||
editor.change_phase(idx, ctx, app);
|
||||
})))
|
||||
}))
|
||||
Box::new(ChangeDuration {
|
||||
composite: Composite::new(Widget::col(vec![
|
||||
Widget::row(vec![
|
||||
Line("How long should this phase last?")
|
||||
.small_heading()
|
||||
.draw(ctx),
|
||||
Btn::plaintext("X")
|
||||
.build(ctx, "close", hotkey(Key::Escape))
|
||||
.align_right(),
|
||||
]),
|
||||
Widget::row(vec![
|
||||
"Seconds:".draw_text(ctx),
|
||||
Spinner::new(
|
||||
ctx,
|
||||
(5, 300),
|
||||
current.simple_duration().inner_seconds() as isize,
|
||||
)
|
||||
.named("duration"),
|
||||
]),
|
||||
Widget::row(vec![
|
||||
"Type:".draw_text(ctx),
|
||||
Checkbox::toggle(
|
||||
ctx,
|
||||
"phase type",
|
||||
"fixed",
|
||||
"adaptive",
|
||||
None,
|
||||
match current {
|
||||
PhaseType::Fixed(_) => true,
|
||||
PhaseType::Adaptive(_) => false,
|
||||
},
|
||||
),
|
||||
]),
|
||||
Btn::text_bg2("Apply").build_def(ctx, hotkey(Key::Enter)),
|
||||
]))
|
||||
.build(ctx),
|
||||
idx,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl State for ChangeDuration {
|
||||
fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition {
|
||||
match self.composite.event(ctx) {
|
||||
Outcome::Clicked(x) => match x.as_ref() {
|
||||
"close" => Transition::Pop,
|
||||
"Apply" => {
|
||||
let dt = Duration::seconds(self.composite.spinner("duration") as f64);
|
||||
let new_type = if self.composite.is_checked("phase type") {
|
||||
PhaseType::Fixed(dt)
|
||||
} else {
|
||||
PhaseType::Adaptive(dt)
|
||||
};
|
||||
let idx = self.idx;
|
||||
return Transition::PopWithData(Box::new(move |state, ctx, app| {
|
||||
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
|
||||
let orig_signal = app.primary.map.get_traffic_signal(editor.i);
|
||||
|
||||
let mut new_signal = orig_signal.clone();
|
||||
new_signal.phases[idx].phase_type = new_type;
|
||||
editor.command_stack.push(orig_signal.clone());
|
||||
editor.redo_stack.clear();
|
||||
editor.top_panel = make_top_panel(ctx, app, true, false);
|
||||
app.primary.map.incremental_edit_traffic_signal(new_signal);
|
||||
editor.change_phase(idx, ctx, app);
|
||||
}));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
_ => {
|
||||
if ctx.normal_left_click() && ctx.canvas.get_cursor_in_screen_space().is_none() {
|
||||
return Transition::Pop;
|
||||
}
|
||||
Transition::Keep
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_baselayer(&self) -> DrawBaselayer {
|
||||
DrawBaselayer::PreviousState
|
||||
}
|
||||
|
||||
fn draw(&self, g: &mut GfxCtx, _: &App) {
|
||||
self.composite.draw(g);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_for_missing_groups(
|
||||
|
@ -439,3 +439,61 @@ impl<T: 'static + Clone> State for ChooseSomething<T> {
|
||||
self.composite.draw(g);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PromptInput {
|
||||
composite: Composite,
|
||||
cb: Box<dyn Fn(String, &mut EventCtx, &mut App) -> Transition>,
|
||||
}
|
||||
|
||||
impl PromptInput {
|
||||
pub fn new(
|
||||
ctx: &mut EventCtx,
|
||||
query: &str,
|
||||
cb: Box<dyn Fn(String, &mut EventCtx, &mut App) -> Transition>,
|
||||
) -> Box<dyn State> {
|
||||
Box::new(PromptInput {
|
||||
composite: Composite::new(Widget::col(vec![
|
||||
Widget::row(vec![
|
||||
Line(query).small_heading().draw(ctx),
|
||||
Btn::plaintext("X")
|
||||
.build(ctx, "close", hotkey(Key::Escape))
|
||||
.align_right(),
|
||||
]),
|
||||
Widget::text_entry(ctx, String::new(), true).named("input"),
|
||||
Btn::text_fg("confirm").build_def(ctx, hotkey(Key::Enter)),
|
||||
]))
|
||||
.build(ctx),
|
||||
cb,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl State for PromptInput {
|
||||
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
|
||||
match self.composite.event(ctx) {
|
||||
Outcome::Clicked(x) => match x.as_ref() {
|
||||
"close" => Transition::Pop,
|
||||
"confirm" => {
|
||||
let data = self.composite.text_box("input");
|
||||
(self.cb)(data, ctx, app)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
_ => {
|
||||
if ctx.normal_left_click() && ctx.canvas.get_cursor_in_screen_space().is_none() {
|
||||
return Transition::Pop;
|
||||
}
|
||||
Transition::Keep
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_baselayer(&self) -> DrawBaselayer {
|
||||
DrawBaselayer::PreviousState
|
||||
}
|
||||
|
||||
fn draw(&self, g: &mut GfxCtx, app: &App) {
|
||||
State::grey_out_map(g, app);
|
||||
self.composite.draw(g);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use crate::render::intersection::make_crosswalk;
|
||||
use crate::render::{DrawTurnGroup, BIG_ARROW_THICKNESS};
|
||||
use ezgui::{
|
||||
hotkey, Btn, Color, Composite, EventCtx, GeomBatch, HorizontalAlignment, Key, Line, Prerender,
|
||||
RewriteColor, Text, TextExt, VerticalAlignment, Widget,
|
||||
RewriteColor, Spinner, Text, TextExt, VerticalAlignment, Widget,
|
||||
};
|
||||
use geom::{Angle, ArrowCap, Circle, Distance, Duration, Line, PolyLine, Polygon, Pt2D};
|
||||
use map_model::{IntersectionID, Phase, PhaseType, TurnPriority, SIDEWALK_THICKNESS};
|
||||
@ -455,6 +455,10 @@ pub fn make_signal_diagram(
|
||||
);
|
||||
|
||||
col.push(Btn::text_fg("Add new phase").build_def(ctx, None));
|
||||
col.push(Widget::row(vec![
|
||||
Spinner::new(ctx, (0, 300), signal.offset.inner_seconds() as isize).named("offset"),
|
||||
Btn::text_fg("Change signal offset").build_def(ctx, None),
|
||||
]));
|
||||
}
|
||||
|
||||
Composite::new(Widget::col(col))
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::app::App;
|
||||
use crate::common::CityPicker;
|
||||
use crate::edit::EditMode;
|
||||
use crate::game::{msg, State, Transition, WizardState};
|
||||
use crate::game::{msg, State, Transition};
|
||||
use crate::helpers::{checkbox_per_mode, nice_map_name};
|
||||
use crate::sandbox::gameplay::freeform::make_change_traffic;
|
||||
use crate::sandbox::gameplay::{GameplayMode, GameplayState};
|
||||
@ -191,9 +191,14 @@ impl EditScenarioModifiers {
|
||||
.outline(2.0, Color::WHITE),
|
||||
);
|
||||
}
|
||||
rows.push(Btn::text_bg2("Change trip mode").build_def(ctx, None));
|
||||
rows.push(Widget::row(vec![
|
||||
Btn::text_bg2("New modification").build_def(ctx, None),
|
||||
Btn::text_bg2("Change trip mode").build_def(ctx, None),
|
||||
Spinner::new(ctx, (2, 14), 2).named("repeat_days"),
|
||||
Btn::text_bg2("Repeat schedule multiple days").build_def(ctx, None),
|
||||
]));
|
||||
rows.push(Widget::row(vec![
|
||||
Spinner::new(ctx, (1, 100), 1).named("cancel_pct"),
|
||||
Btn::text_bg2("Cancel all trips for some percent of people").build_def(ctx, None),
|
||||
]));
|
||||
rows.push(
|
||||
Widget::row(vec![
|
||||
@ -231,12 +236,6 @@ impl State for EditScenarioModifiers {
|
||||
),
|
||||
)));
|
||||
}
|
||||
"New modification" => {
|
||||
return Transition::Push(new_modifier(
|
||||
self.scenario_name.clone(),
|
||||
self.modifiers.clone(),
|
||||
));
|
||||
}
|
||||
"Change trip mode" => {
|
||||
return Transition::Push(ChangeMode::new(
|
||||
ctx,
|
||||
@ -245,6 +244,26 @@ impl State for EditScenarioModifiers {
|
||||
self.modifiers.clone(),
|
||||
));
|
||||
}
|
||||
"Repeat schedule multiple days" => {
|
||||
self.modifiers.push(ScenarioModifier::RepeatDays(
|
||||
self.composite.spinner("repeat_days") as usize,
|
||||
));
|
||||
return Transition::Replace(EditScenarioModifiers::new(
|
||||
ctx,
|
||||
self.scenario_name.clone(),
|
||||
self.modifiers.clone(),
|
||||
));
|
||||
}
|
||||
"Cancel all trips for some percent of people" => {
|
||||
self.modifiers.push(ScenarioModifier::CancelPeople(
|
||||
self.composite.spinner("cancel_pct") as usize,
|
||||
));
|
||||
return Transition::Replace(EditScenarioModifiers::new(
|
||||
ctx,
|
||||
self.scenario_name.clone(),
|
||||
self.modifiers.clone(),
|
||||
));
|
||||
}
|
||||
x => {
|
||||
if let Some(x) = x.strip_prefix("delete modifier ") {
|
||||
self.modifiers.remove(x.parse::<usize>().unwrap() - 1);
|
||||
@ -270,34 +289,6 @@ impl State for EditScenarioModifiers {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Wizard isn't the right UI for this
|
||||
fn new_modifier(scenario_name: String, modifiers: Vec<ScenarioModifier>) -> Box<dyn State> {
|
||||
WizardState::new(Box::new(move |wiz, ctx, _| {
|
||||
let mut wizard = wiz.wrap(ctx);
|
||||
let new_mod = match wizard
|
||||
.choose_string("", || {
|
||||
vec!["repeat days", "cancel all trips for some people"]
|
||||
})?
|
||||
.as_str()
|
||||
{
|
||||
x if x == "repeat days" => ScenarioModifier::RepeatDays(
|
||||
wizard.input_usize("Repeat everyone's schedule how many days?")?,
|
||||
),
|
||||
x if x == "cancel all trips for some people" => ScenarioModifier::CancelPeople(
|
||||
wizard.input_percent("What percent of people should cancel trips? (0 to 100)")?,
|
||||
),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let mut mods = modifiers.clone();
|
||||
mods.push(new_mod);
|
||||
Some(Transition::PopThenReplace(EditScenarioModifiers::new(
|
||||
ctx,
|
||||
scenario_name.clone(),
|
||||
mods,
|
||||
)))
|
||||
}))
|
||||
}
|
||||
|
||||
struct ChangeMode {
|
||||
composite: Composite,
|
||||
scenario_name: String,
|
||||
|
Loading…
Reference in New Issue
Block a user