game/edit/traffic_signals/
edits.rs

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    // TODO Conflating stop signs and construction here
214    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                        // TODO This menu for a single intersection is a strange place to import for all
402                        // intersections, but I'm not sure where else it should go. Also, this will
403                        // blindly overwrite changes for all intersections and quit the current editor.
404                        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}