1use geom::Duration;
2use map_gui::tools::FilePicker;
3use map_model::{
4 ControlStopSign, ControlTrafficSignal, EditIntersectionControl, IntersectionID, StageType,
5};
6use widgetry::tools::{ChooseSomething, PopupMsg};
7use widgetry::{
8 Choice, DrawBaselayer, EventCtx, Key, Line, Panel, SimpleState, Spinner, State, Text, TextExt,
9 Widget,
10};
11
12use crate::app::{App, Transition};
13use crate::edit::traffic_signals::{BundleEdits, TrafficSignalEditor};
14use crate::edit::{apply_map_edits, check_sidewalk_connectivity, StopSignEditor};
15use crate::sandbox::GameplayMode;
16
17pub struct ChangeDuration {
18 idx: usize,
19}
20
21impl ChangeDuration {
22 pub fn new_state(
23 ctx: &mut EventCtx,
24 app: &App,
25 signal: &ControlTrafficSignal,
26 idx: usize,
27 ) -> Box<dyn State<App>> {
28 let i = app.primary.map.get_i(signal.id);
29 let panel = Panel::new_builder(Widget::col(vec![
30 Widget::row(vec![
31 Line("How long should this stage last?")
32 .small_heading()
33 .into_widget(ctx),
34 ctx.style().btn_close_widget(ctx),
35 ]),
36 Widget::row(vec![
37 "Duration:".text_widget(ctx).centered_vert(),
38 Spinner::widget(
39 ctx,
40 "duration",
41 (signal.get_min_crossing_time(idx, i), Duration::minutes(5)),
42 signal.stages[idx].stage_type.simple_duration(),
43 Duration::seconds(1.0),
44 ),
45 ]),
46 Line("Minimum time is set by the time required for crosswalk")
47 .secondary()
48 .into_widget(ctx),
49 Widget::col(vec![
50 Text::from_all(match signal.stages[idx].stage_type {
51 StageType::Fixed(_) => vec![
52 Line("Fixed timing").small_heading(),
53 Line(" (Adjust both values below to enable variable timing)"),
54 ],
55 StageType::Variable(_, _, _) => vec![
56 Line("Variable timing").small_heading(),
57 Line(" (Set either values below to 0 to use fixed timing."),
58 ],
59 })
60 .into_widget(ctx)
61 .named("timing type"),
62 Widget::row(vec![
63 "How much additional time can this stage last?"
64 .text_widget(ctx)
65 .centered_vert(),
66 Spinner::widget(
67 ctx,
68 "additional",
69 (Duration::ZERO, Duration::minutes(5)),
70 match signal.stages[idx].stage_type {
71 StageType::Fixed(_) => Duration::ZERO,
72 StageType::Variable(_, _, additional) => additional,
73 },
74 Duration::seconds(1.0),
75 ),
76 ]),
77 Widget::row(vec![
78 "How long with no demand before the stage ends?"
79 .text_widget(ctx)
80 .centered_vert(),
81 Spinner::widget(
82 ctx,
83 "delay",
84 (Duration::ZERO, Duration::seconds(300.0)),
85 match signal.stages[idx].stage_type {
86 StageType::Fixed(_) => Duration::ZERO,
87 StageType::Variable(_, delay, _) => delay,
88 },
89 Duration::seconds(1.0),
90 ),
91 ]),
92 ])
93 .padding(10)
94 .bg(app.cs.inner_panel_bg)
95 .outline(ctx.style().section_outline),
96 ctx.style()
97 .btn_solid_primary
98 .text("Apply")
99 .hotkey(Key::Enter)
100 .build_def(ctx),
101 ]))
102 .build(ctx);
103 <dyn SimpleState<_>>::new_state(panel, Box::new(ChangeDuration { idx }))
104 }
105}
106
107impl SimpleState<App> for ChangeDuration {
108 fn on_click(
109 &mut self,
110 _: &mut EventCtx,
111 _: &mut App,
112 x: &str,
113 panel: &mut Panel,
114 ) -> Transition {
115 match x {
116 "close" => Transition::Pop,
117 "Apply" => {
118 let dt = panel.spinner("duration");
119 let delay = panel.spinner("delay");
120 let additional = panel.spinner("additional");
121 let new_type = if delay == Duration::ZERO || additional == Duration::ZERO {
122 StageType::Fixed(dt)
123 } else {
124 StageType::Variable(dt, delay, additional)
125 };
126 let idx = self.idx;
127 Transition::Multi(vec![
128 Transition::Pop,
129 Transition::ModifyState(Box::new(move |state, ctx, app| {
130 let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
131 editor.add_new_edit(ctx, app, idx, |ts| {
132 ts.stages[idx].stage_type = new_type.clone();
133 });
134 })),
135 ])
136 }
137 _ => unreachable!(),
138 }
139 }
140
141 fn panel_changed(
142 &mut self,
143 ctx: &mut EventCtx,
144 _: &mut App,
145 panel: &mut Panel,
146 ) -> Option<Transition> {
147 let new_label = Text::from_all(
148 if panel.spinner::<Duration>("delay") == Duration::ZERO
149 || panel.spinner::<Duration>("additional") == Duration::ZERO
150 {
151 vec![
152 Line("Fixed timing").small_heading(),
153 Line(" (Adjust both values below to enable variable timing)"),
154 ]
155 } else {
156 vec![
157 Line("Variable timing").small_heading(),
158 Line(" (Set either values below to 0 to use fixed timing."),
159 ]
160 },
161 )
162 .into_widget(ctx);
163 panel.replace(ctx, "timing type", new_label);
164 None
165 }
166
167 fn other_event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition {
168 if ctx.normal_left_click() && ctx.canvas.get_cursor_in_screen_space().is_none() {
169 return Transition::Pop;
170 }
171 Transition::Keep
172 }
173
174 fn draw_baselayer(&self) -> DrawBaselayer {
175 DrawBaselayer::PreviousState
176 }
177}
178
179pub fn edit_entire_signal(
180 ctx: &mut EventCtx,
181 app: &App,
182 i: IntersectionID,
183 mode: GameplayMode,
184 original: BundleEdits,
185) -> Box<dyn State<App>> {
186 let has_sidewalks = app
187 .primary
188 .map
189 .get_i(i)
190 .turns
191 .iter()
192 .any(|t| t.between_sidewalks());
193
194 let use_template = "use template";
195 let all_walk = "add an all-walk stage at the end";
196 let major_minor_timing = "use timing pattern for a major/minor intersection";
197 let stop_sign = "convert to stop signs";
198 let close = "close intersection for construction";
199 let reset = "reset to default";
200 let gmns_picker = "import from a new GMNS timing.csv";
201 let gmns_existing = app
202 .session
203 .last_gmns_timing_csv
204 .as_ref()
205 .map(|(path, _)| format!("import from GMNS {}", path));
206 let gmns_all = "import all traffic signals from a new GMNS timing.csv";
207
208 let mut choices = vec![use_template.to_string()];
209 if has_sidewalks {
210 choices.push(all_walk.to_string());
211 }
212 choices.push(major_minor_timing.to_string());
213 if mode.can_edit_stop_signs() {
215 choices.push(stop_sign.to_string());
216 choices.push(close.to_string());
217 }
218 choices.push(reset.to_string());
219 choices.push(gmns_picker.to_string());
220 if let Some(x) = gmns_existing.clone() {
221 choices.push(x);
222 }
223 choices.push(gmns_all.to_string());
224
225 ChooseSomething::new_state(
226 ctx,
227 "What do you want to change?",
228 Choice::strings(choices),
229 Box::new(move |x, ctx, app| match x.as_str() {
230 x if x == use_template => Transition::Replace(ChooseSomething::new_state(
231 ctx,
232 "Use which preset for this intersection?",
233 Choice::from(ControlTrafficSignal::get_possible_policies(
234 &app.primary.map,
235 i,
236 )),
237 Box::new(move |new_signal, _, _| {
238 Transition::Multi(vec![
239 Transition::Pop,
240 Transition::ModifyState(Box::new(move |state, ctx, app| {
241 let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
242 editor.add_new_edit(ctx, app, 0, |ts| {
243 *ts = new_signal.clone();
244 });
245 })),
246 ])
247 }),
248 )),
249 x if x == all_walk => Transition::Multi(vec![
250 Transition::Pop,
251 Transition::ModifyState(Box::new(move |state, ctx, app| {
252 let mut new_signal = app.primary.map.get_traffic_signal(i).clone();
253 if new_signal.convert_to_ped_scramble(app.primary.map.get_i(i)) {
254 let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
255 editor.add_new_edit(ctx, app, 0, |ts| {
256 *ts = new_signal.clone();
257 });
258 }
259 })),
260 ]),
261 x if x == major_minor_timing => Transition::Replace(ChooseSomething::new_state(
262 ctx,
263 "Use what timing split?",
264 vec![
265 Choice::new(
266 "120s cycle: 96s major roads, 24s minor roads",
267 (Duration::seconds(96.0), Duration::seconds(24.0)),
268 ),
269 Choice::new(
270 "60s cycle: 36s major roads, 24s minor roads",
271 (Duration::seconds(36.0), Duration::seconds(24.0)),
272 ),
273 ],
274 Box::new(move |timing, ctx, app| {
275 let mut new_signal = app.primary.map.get_traffic_signal(i).clone();
276 match new_signal.adjust_major_minor_timing(timing.0, timing.1, &app.primary.map)
277 {
278 Ok(()) => Transition::Multi(vec![
279 Transition::Pop,
280 Transition::ModifyState(Box::new(move |state, ctx, app| {
281 let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
282 editor.add_new_edit(ctx, app, 0, |ts| {
283 *ts = new_signal.clone();
284 });
285 })),
286 ]),
287 Err(err) => Transition::Replace(PopupMsg::new_state(
288 ctx,
289 "Error",
290 vec![err.to_string()],
291 )),
292 }
293 }),
294 )),
295 x if x == stop_sign => {
296 original.apply(app);
297
298 let mut edits = app.primary.map.get_edits().clone();
299 edits
300 .commands
301 .push(app.primary.map.edit_intersection_cmd(i, |new| {
302 new.control = EditIntersectionControl::StopSign(ControlStopSign::new(
303 &app.primary.map,
304 i,
305 ));
306 }));
307 apply_map_edits(ctx, app, edits);
308 Transition::Multi(vec![
309 Transition::Pop,
310 Transition::Replace(StopSignEditor::new_state(ctx, app, i, mode)),
311 ])
312 }
313 x if x == close => {
314 original.apply(app);
315
316 let cmd = app.primary.map.edit_intersection_cmd(i, |new| {
317 new.control = EditIntersectionControl::Closed;
318 });
319 if let Some(err) = check_sidewalk_connectivity(ctx, app, cmd.clone()) {
320 Transition::Replace(err)
321 } else {
322 let mut edits = app.primary.map.get_edits().clone();
323 edits.commands.push(cmd);
324 apply_map_edits(ctx, app, edits);
325
326 Transition::Multi(vec![Transition::Pop, Transition::Pop])
327 }
328 }
329 x if x == reset => Transition::Multi(vec![
330 Transition::Pop,
331 Transition::ModifyState(Box::new(move |state, ctx, app| {
332 let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
333 let new_signal =
334 ControlTrafficSignal::get_possible_policies(&app.primary.map, i)
335 .remove(0)
336 .1;
337 editor.add_new_edit(ctx, app, 0, |ts| {
338 *ts = new_signal.clone();
339 });
340 })),
341 ]),
342 x if x == gmns_picker => Transition::Replace(FilePicker::new_state(
343 ctx,
344 None,
345 Box::new(move |ctx, app, maybe_file| {
346 if let Ok(Some((path, bytes))) = maybe_file {
347 app.session.last_gmns_timing_csv = Some((path.clone(), bytes.clone()));
348 match crate::edit::traffic_signals::gmns::import(
349 &app.primary.map,
350 i,
351 &bytes,
352 ) {
353 Ok(new_signal) => Transition::Multi(vec![
354 Transition::Pop,
355 Transition::ModifyState(Box::new(move |state, ctx, app| {
356 let editor =
357 state.downcast_mut::<TrafficSignalEditor>().unwrap();
358 editor.add_new_edit(ctx, app, 0, |ts| {
359 *ts = new_signal.clone();
360 });
361 })),
362 ]),
363 Err(err) => Transition::Replace(PopupMsg::new_state(
364 ctx,
365 "Error",
366 vec![err.to_string()],
367 )),
368 }
369 } else {
370 Transition::Pop
371 }
372 }),
373 )),
374 x if Some(x.to_string()) == gmns_existing => {
375 match crate::edit::traffic_signals::gmns::import(
376 &app.primary.map,
377 i,
378 &app.session.last_gmns_timing_csv.as_ref().unwrap().1,
379 ) {
380 Ok(new_signal) => Transition::Multi(vec![
381 Transition::Pop,
382 Transition::ModifyState(Box::new(move |state, ctx, app| {
383 let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
384 editor.add_new_edit(ctx, app, 0, |ts| {
385 *ts = new_signal.clone();
386 });
387 })),
388 ]),
389 Err(err) => Transition::Replace(PopupMsg::new_state(
390 ctx,
391 "Error",
392 vec![err.to_string()],
393 )),
394 }
395 }
396 x if x == gmns_all => Transition::Replace(FilePicker::new_state(
397 ctx,
398 None,
399 Box::new(move |ctx, app, maybe_file| {
400 if let Ok(Some((path, bytes))) = maybe_file {
401 Transition::Multi(vec![
405 Transition::Pop,
406 Transition::Pop,
407 Transition::Push(crate::edit::traffic_signals::gmns::import_all(
408 ctx, app, &path, bytes,
409 )),
410 ])
411 } else {
412 Transition::Pop
413 }
414 }),
415 )),
416 _ => unreachable!(),
417 }),
418 )
419}