game/sandbox/gameplay/
play_scenario.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use maplit::btreeset;
4
5use abstutil::prettyprint_usize;
6use geom::{Duration, Time};
7use map_gui::tools::{checkbox_per_mode, grey_out_map, CityPicker};
8use sim::SlidingWindow;
9use synthpop::{ScenarioModifier, TripMode};
10use widgetry::tools::{ChooseSomething, PopupMsg, URLManager};
11use widgetry::{
12    lctrl, Choice, Color, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, LinePlot, Outcome,
13    Panel, PlotOptions, Series, SimpleState, Slider, Spinner, State, Text, TextExt,
14    VerticalAlignment, Widget,
15};
16
17use crate::app::{App, Transition};
18use crate::edit::EditMode;
19use crate::sandbox::gameplay::freeform::ChangeScenario;
20use crate::sandbox::gameplay::{GameplayMode, GameplayState};
21use crate::sandbox::{Actions, SandboxControls, SandboxMode, TimeWarpScreen};
22
23pub struct PlayScenario {
24    top_right: Panel,
25    scenario_name: String,
26    modifiers: Vec<ScenarioModifier>,
27}
28
29impl PlayScenario {
30    pub fn new_state(
31        ctx: &mut EventCtx,
32        app: &App,
33        name: &str,
34        modifiers: Vec<ScenarioModifier>,
35    ) -> Box<dyn GameplayState> {
36        URLManager::update_url_free_param(
37            // For dynamically generated scenarios like "random" and "home_to_work", this winds up
38            // making up a filename that doesn't actually exist. But if you pass that in, it winds
39            // up working, because we call abstio::parse_scenario_path() on the other side.
40            abstio::path_scenario(app.primary.map.get_name(), name)
41                .strip_prefix(&abstio::path(""))
42                .unwrap()
43                .to_string(),
44        );
45
46        Box::new(PlayScenario {
47            top_right: Panel::empty(ctx),
48            scenario_name: name.to_string(),
49            modifiers,
50        })
51    }
52}
53
54impl GameplayState for PlayScenario {
55    fn event(
56        &mut self,
57        ctx: &mut EventCtx,
58        app: &mut App,
59        _: &mut SandboxControls,
60        _: &mut Actions,
61    ) -> Option<Transition> {
62        // This should really happen in the constructor once, but the old PlayScenario's
63        // on_destroy can wipe this out.
64        app.primary.has_modified_trips = !self.modifiers.is_empty();
65
66        match self.top_right.event(ctx) {
67            Outcome::Clicked(x) => match x.as_ref() {
68                "change map" => {
69                    let scenario = self.scenario_name.clone();
70                    Some(Transition::Push(CityPicker::new_state(
71                        ctx,
72                        app,
73                        Box::new(move |_, app| {
74                            // Try to load a scenario with the same name if it exists
75                            let mode = if abstio::file_exists(abstio::path_scenario(
76                                app.primary.map.get_name(),
77                                &scenario,
78                            )) {
79                                GameplayMode::PlayScenario(
80                                    app.primary.map.get_name().clone(),
81                                    scenario,
82                                    Vec::new(),
83                                )
84                            } else {
85                                GameplayMode::Freeform(app.primary.map.get_name().clone())
86                            };
87                            Transition::Multi(vec![
88                                Transition::Pop,
89                                Transition::Replace(SandboxMode::simple_new(app, mode)),
90                            ])
91                        }),
92                    )))
93                }
94                "change scenario" => Some(Transition::Push(ChangeScenario::new_state(
95                    ctx,
96                    app,
97                    &self.scenario_name,
98                ))),
99                "edit map" => Some(Transition::Push(EditMode::new_state(
100                    ctx,
101                    app,
102                    GameplayMode::PlayScenario(
103                        app.primary.map.get_name().clone(),
104                        self.scenario_name.clone(),
105                        self.modifiers.clone(),
106                    ),
107                ))),
108                "edit traffic patterns" => {
109                    Some(Transition::Push(EditScenarioModifiers::new_state(
110                        ctx,
111                        self.scenario_name.clone(),
112                        self.modifiers.clone(),
113                    )))
114                }
115                "save scenario" => {
116                    let mut s = app.primary.scenario.as_ref().unwrap().clone();
117                    // If the name happens to be random, home_to_work, or census (the 3
118                    // dynamically generated cases), it'll get covered up. So to be safe, rename
119                    // it.
120                    s.scenario_name = format!("saved_{}", s.scenario_name);
121                    s.save();
122                    Some(Transition::Push(PopupMsg::new_state(
123                        ctx,
124                        "Saved",
125                        vec![format!("Scenario '{}' saved", s.scenario_name)],
126                    )))
127                }
128                "When do trips start?" => {
129                    Some(Transition::Push(DepartureSummary::new_state(ctx, app)))
130                }
131                _ => unreachable!(),
132            },
133            _ => None,
134        }
135    }
136
137    fn draw(&self, g: &mut GfxCtx, _: &App) {
138        self.top_right.draw(g);
139    }
140
141    fn on_destroy(&self, app: &mut App) {
142        app.primary.has_modified_trips = false;
143    }
144
145    fn recreate_panels(&mut self, ctx: &mut EventCtx, app: &App) {
146        let mut extra = Vec::new();
147        if self.scenario_name != "empty" {
148            extra.push(Widget::row(vec![
149                ctx.style()
150                    .btn_plain
151                    .icon("system/assets/tools/info.svg")
152                    .build_widget(ctx, "When do trips start?")
153                    .centered_vert(),
154                ctx.style()
155                    .btn_plain
156                    .icon("system/assets/tools/pencil.svg")
157                    .build_widget(ctx, "edit traffic patterns")
158                    .centered_vert(),
159                format!("{} modifications to traffic patterns", self.modifiers.len())
160                    .text_widget(ctx)
161                    .centered_vert(),
162            ]));
163        }
164        if !abstio::file_exists(abstio::path_scenario(
165            app.primary.map.get_name(),
166            &self.scenario_name,
167        )) && app.primary.scenario.is_some()
168        {
169            extra.push(
170                ctx.style()
171                    .btn_plain
172                    .icon("system/assets/tools/save.svg")
173                    .label_text("save scenario")
174                    .build_def(ctx),
175            );
176        }
177
178        let rows = vec![
179            Widget::custom_row(vec![
180                Line("Sandbox")
181                    .small_heading()
182                    .into_widget(ctx)
183                    .margin_right(18),
184                map_gui::tools::change_map_btn(ctx, app).margin_right(8),
185                ctx.style()
186                    .btn_popup_icon_text("system/assets/tools/calendar.svg", &self.scenario_name)
187                    .hotkey(Key::S)
188                    .build_widget(ctx, "change scenario")
189                    .margin_right(8),
190                ctx.style()
191                    .btn_outline
192                    .icon_text("system/assets/tools/pencil.svg", "Edit map")
193                    .hotkey(lctrl(Key::E))
194                    .build_widget(ctx, "edit map")
195                    .margin_right(8),
196            ])
197            .centered(),
198            if extra.is_empty() {
199                Widget::nothing()
200            } else {
201                Widget::row(extra).centered_horiz()
202            },
203        ];
204
205        self.top_right = Panel::new_builder(Widget::col(rows))
206            .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
207            .build(ctx);
208    }
209}
210
211struct EditScenarioModifiers {
212    scenario_name: String,
213    modifiers: Vec<ScenarioModifier>,
214    panel: Panel,
215}
216
217impl EditScenarioModifiers {
218    pub fn new_state(
219        ctx: &mut EventCtx,
220        scenario_name: String,
221        modifiers: Vec<ScenarioModifier>,
222    ) -> Box<dyn State<App>> {
223        let mut rows = vec![
224            Line("Modify traffic patterns")
225                .small_heading()
226                .into_widget(ctx),
227            Text::from(
228                "This scenario determines the exact trips everybody takes, when they leave, where \
229                 they go, and how they choose to get there. You can modify those patterns here. \
230                 The modifications apply in order.",
231            )
232            .wrap_to_pct(ctx, 50)
233            .into_widget(ctx),
234        ];
235        for (idx, m) in modifiers.iter().enumerate() {
236            rows.push(
237                Widget::row(vec![
238                    m.describe().text_widget(ctx).centered_vert(),
239                    ctx.style()
240                        .btn_solid_destructive
241                        .icon("system/assets/tools/trash.svg")
242                        .build_widget(ctx, format!("delete modifier {}", idx + 1))
243                        .align_right(),
244                ])
245                .padding(10)
246                .outline(ctx.style().section_outline),
247            );
248        }
249        rows.push(
250            ctx.style()
251                .btn_outline
252                .text("Change trip mode")
253                .build_def(ctx),
254        );
255        rows.push(
256            ctx.style()
257                .btn_outline
258                .text("Add extra new trips")
259                .build_def(ctx),
260        );
261        rows.push(Widget::row(vec![
262            Spinner::widget(ctx, "repeat_days", (2, 14), 2, 1),
263            ctx.style()
264                .btn_outline
265                .text("Repeat schedule multiple days")
266                .build_def(ctx),
267        ]));
268        rows.push(Widget::row(vec![
269            Spinner::widget(ctx, "repeat_days_noise", (2, 14), 2_usize, 1),
270            ctx.style()
271                .btn_outline
272                .text("Repeat schedule multiple days with +/- 10 minutes of noise")
273                .build_def(ctx),
274        ]));
275        rows.push(Widget::horiz_separator(ctx, 1.0));
276        rows.push(
277            Widget::row(vec![
278                ctx.style()
279                    .btn_solid_primary
280                    .text("Apply")
281                    .hotkey(Key::Enter)
282                    .build_def(ctx),
283                ctx.style()
284                    .btn_solid_destructive
285                    .text("Discard changes")
286                    .hotkey(Key::Escape)
287                    .build_def(ctx),
288            ])
289            .centered(),
290        );
291
292        Box::new(EditScenarioModifiers {
293            scenario_name,
294            modifiers,
295            panel: Panel::new_builder(Widget::col(rows))
296                .exact_size_percent(80, 80)
297                .build(ctx),
298        })
299    }
300}
301
302impl State<App> for EditScenarioModifiers {
303    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
304        if let Outcome::Clicked(x) = self.panel.event(ctx) {
305            match x.as_ref() {
306                "Discard changes" => {
307                    return Transition::Pop;
308                }
309                "Apply" => {
310                    info!("To apply these modifiers in the future:");
311                    info!(
312                        "--scenario_modifiers='{}'",
313                        abstutil::to_json_terse(&self.modifiers)
314                    );
315
316                    return Transition::Multi(vec![
317                        Transition::Pop,
318                        Transition::Replace(SandboxMode::simple_new(
319                            app,
320                            GameplayMode::PlayScenario(
321                                app.primary.map.get_name().clone(),
322                                self.scenario_name.clone(),
323                                self.modifiers.clone(),
324                            ),
325                        )),
326                    ]);
327                }
328                "Change trip mode" => {
329                    return Transition::Push(ChangeMode::new_state(
330                        ctx,
331                        app,
332                        self.scenario_name.clone(),
333                        self.modifiers.clone(),
334                    ));
335                }
336                "Add extra new trips" => {
337                    return Transition::Push(ChooseSomething::new_state(
338                        ctx,
339                        "Which trips do you want to add in?",
340                        // TODO Exclude weekday?
341                        Choice::strings(abstio::list_all_objects(abstio::path_all_scenarios(
342                            app.primary.map.get_name(),
343                        ))),
344                        Box::new(|name, _, _| {
345                            Transition::Multi(vec![
346                                Transition::Pop,
347                                Transition::ConsumeState(Box::new(|state, ctx, _| {
348                                    let mut state =
349                                        state.downcast::<EditScenarioModifiers>().ok().unwrap();
350                                    state.modifiers.push(ScenarioModifier::AddExtraTrips(name));
351                                    vec![EditScenarioModifiers::new_state(
352                                        ctx,
353                                        state.scenario_name,
354                                        state.modifiers,
355                                    )]
356                                })),
357                            ])
358                        }),
359                    ));
360                }
361                "Repeat schedule multiple days" => {
362                    self.modifiers.push(ScenarioModifier::RepeatDays(
363                        self.panel.spinner("repeat_days"),
364                    ));
365                    return Transition::Replace(EditScenarioModifiers::new_state(
366                        ctx,
367                        self.scenario_name.clone(),
368                        self.modifiers.clone(),
369                    ));
370                }
371                "Repeat schedule multiple days with +/- 10 minutes of noise" => {
372                    self.modifiers.push(ScenarioModifier::RepeatDaysNoise {
373                        days: self.panel.spinner("repeat_days_noise"),
374                        departure_time_noise: Duration::minutes(10),
375                    });
376                    return Transition::Replace(EditScenarioModifiers::new_state(
377                        ctx,
378                        self.scenario_name.clone(),
379                        self.modifiers.clone(),
380                    ));
381                }
382                x => {
383                    if let Some(x) = x.strip_prefix("delete modifier ") {
384                        self.modifiers.remove(x.parse::<usize>().unwrap() - 1);
385                        return Transition::Replace(EditScenarioModifiers::new_state(
386                            ctx,
387                            self.scenario_name.clone(),
388                            self.modifiers.clone(),
389                        ));
390                    } else {
391                        unreachable!()
392                    }
393                }
394            }
395        }
396
397        Transition::Keep
398    }
399
400    fn draw(&self, g: &mut GfxCtx, app: &App) {
401        grey_out_map(g, app);
402        self.panel.draw(g);
403    }
404}
405
406struct ChangeMode {
407    panel: Panel,
408    scenario_name: String,
409    modifiers: Vec<ScenarioModifier>,
410    count_trips: CountTrips,
411}
412
413impl ChangeMode {
414    fn new_state(
415        ctx: &mut EventCtx,
416        app: &App,
417        scenario_name: String,
418        modifiers: Vec<ScenarioModifier>,
419    ) -> Box<dyn State<App>> {
420        let mut state = ChangeMode {
421            scenario_name,
422            modifiers,
423            count_trips: CountTrips::new(app),
424            panel: Panel::new_builder(Widget::col(vec![
425                Line("Change trip mode").small_heading().into_widget(ctx),
426                Widget::row(vec![
427                    "Percent of people to modify:"
428                        .text_widget(ctx)
429                        .centered_vert(),
430                    Spinner::widget(ctx, "pct_ppl", (1, 100), 50_usize, 1),
431                ]),
432                "Types of trips to convert:".text_widget(ctx),
433                checkbox_per_mode(ctx, app, &btreeset! { TripMode::Drive }),
434                Widget::row(vec![
435                    "Departing from:".text_widget(ctx),
436                    Slider::area(ctx, 0.25 * ctx.canvas.window_width, 0.0, "depart from"),
437                ]),
438                Widget::row(vec![
439                    "Departing until:".text_widget(ctx),
440                    Slider::area(ctx, 0.25 * ctx.canvas.window_width, 0.3, "depart to"),
441                ]),
442                "Matching trips:".text_widget(ctx).named("count"),
443                Widget::horiz_separator(ctx, 1.0),
444                Widget::row(vec![
445                    "Change to trip type:".text_widget(ctx),
446                    Widget::dropdown(ctx, "to_mode", Some(TripMode::Bike), {
447                        let mut choices = vec![Choice::new("cancel trip", None)];
448                        for m in TripMode::all() {
449                            choices.push(Choice::new(m.ongoing_verb(), Some(m)));
450                        }
451                        choices
452                    }),
453                ]),
454                Widget::row(vec![
455                    ctx.style()
456                        .btn_solid_primary
457                        .text("Apply")
458                        .hotkey(Key::Enter)
459                        .build_def(ctx),
460                    ctx.style()
461                        .btn_solid_destructive
462                        .text("Discard changes")
463                        .hotkey(Key::Escape)
464                        .build_def(ctx),
465                ])
466                .centered(),
467            ]))
468            .exact_size_percent(80, 80)
469            .build(ctx),
470        };
471        state.recalc_count(ctx, app);
472        Box::new(state)
473    }
474
475    fn get_filters(&self, app: &App) -> (BTreeSet<TripMode>, (Time, Time)) {
476        let to_mode = self.panel.dropdown_value::<Option<TripMode>, _>("to_mode");
477        let (p1, p2) = (
478            self.panel.slider("depart from").get_percent(),
479            self.panel.slider("depart to").get_percent(),
480        );
481        let departure_filter = (
482            app.primary.sim.get_end_of_day().percent_of(p1),
483            app.primary.sim.get_end_of_day().percent_of(p2),
484        );
485        let mut from_modes = TripMode::all()
486            .into_iter()
487            .filter(|m| self.panel.is_checked(m.ongoing_verb()))
488            .collect::<BTreeSet<_>>();
489        if let Some(ref m) = to_mode {
490            from_modes.remove(m);
491        }
492        (from_modes, departure_filter)
493    }
494
495    fn recalc_count(&mut self, ctx: &mut EventCtx, app: &App) {
496        let (modes, (t1, t2)) = self.get_filters(app);
497        let mut cnt = 0;
498        for m in modes {
499            cnt += self.count_trips.count(m, t1, t2);
500        }
501        let pct_ppl: usize = self.panel.spinner("pct_ppl");
502        let mut txt = Text::from(format!("Matching trips: {}", prettyprint_usize(cnt)));
503        let adjusted_cnt = ((cnt as f64) * (pct_ppl as f64) / 100.0) as usize;
504        txt.append(
505            Line(format!(
506                " ({}% is {})",
507                pct_ppl,
508                prettyprint_usize(adjusted_cnt)
509            ))
510            .secondary(),
511        );
512        let label = txt.into_widget(ctx);
513        self.panel.replace(ctx, "count", label);
514    }
515}
516
517impl State<App> for ChangeMode {
518    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
519        match self.panel.event(ctx) {
520            Outcome::Clicked(x) => match x.as_ref() {
521                "Discard changes" => Transition::Pop,
522                "Apply" => {
523                    let (from_modes, departure_filter) = self.get_filters(app);
524                    let to_mode = self.panel.dropdown_value::<Option<TripMode>, _>("to_mode");
525                    let pct_ppl = self.panel.spinner("pct_ppl");
526                    if from_modes.is_empty() {
527                        return Transition::Push(PopupMsg::new_state(
528                            ctx,
529                            "Error",
530                            vec!["You have to select at least one mode to convert from"],
531                        ));
532                    }
533                    if departure_filter.0 >= departure_filter.1 {
534                        return Transition::Push(PopupMsg::new_state(
535                            ctx,
536                            "Error",
537                            vec!["Your time range is backwards"],
538                        ));
539                    }
540
541                    let mut mods = self.modifiers.clone();
542                    mods.push(ScenarioModifier::ChangeMode {
543                        to_mode,
544                        pct_ppl,
545                        departure_filter,
546                        from_modes,
547                    });
548                    Transition::Multi(vec![
549                        Transition::Pop,
550                        Transition::Replace(EditScenarioModifiers::new_state(
551                            ctx,
552                            self.scenario_name.clone(),
553                            mods,
554                        )),
555                    ])
556                }
557                _ => unreachable!(),
558            },
559            Outcome::Changed(_) => {
560                self.recalc_count(ctx, app);
561                Transition::Keep
562            }
563            _ => Transition::Keep,
564        }
565    }
566
567    fn draw(&self, g: &mut GfxCtx, app: &App) {
568        grey_out_map(g, app);
569        self.panel.draw(g);
570    }
571}
572
573pub struct DepartureSummary {
574    first_trip: Time,
575}
576
577impl DepartureSummary {
578    pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
579        // Get all departure times, sorted so the sliding window works
580        let mut departure_times: Vec<Time> = app
581            .primary
582            .scenario
583            .as_ref()
584            .unwrap()
585            .people
586            .iter()
587            .flat_map(|person| person.trips.iter().map(|t| t.depart))
588            .collect();
589        departure_times.sort();
590        let first_trip = departure_times
591            .get(0)
592            .cloned()
593            .unwrap_or(Time::START_OF_DAY);
594
595        let mut pts = vec![(Time::START_OF_DAY, 0)];
596        let mut window = SlidingWindow::new(Duration::minutes(15));
597        for time in departure_times {
598            let count = window.add(time);
599            pts.push((time, count));
600        }
601        window.close_off_pts(&mut pts, app.primary.sim.get_end_of_day());
602
603        let panel = Panel::new_builder(Widget::col(vec![
604            Widget::row(vec![
605                Line("Trip departure times")
606                    .small_heading()
607                    .into_widget(ctx),
608                ctx.style().btn_close_widget(ctx),
609            ]),
610            LinePlot::new_widget(
611                ctx,
612                "trip starts",
613                vec![Series {
614                    label: "When do trips start?".to_string(),
615                    color: Color::RED,
616                    pts,
617                }],
618                PlotOptions::fixed(),
619                app.opts.units,
620            )
621            .section(ctx),
622            if first_trip - app.primary.sim.time() > Duration::minutes(15) {
623                ctx.style()
624                    .btn_outline
625                    .text(format!(
626                        "Jump to first trip, at {}",
627                        first_trip.ampm_tostring()
628                    ))
629                    .build_widget(ctx, "Jump to first trip")
630            } else {
631                Widget::nothing()
632            },
633            ctx.style()
634                .btn_outline
635                .text("Commuter patterns")
636                .build_def(ctx),
637        ]))
638        .build(ctx);
639        <dyn SimpleState<_>>::new_state(panel, Box::new(DepartureSummary { first_trip }))
640    }
641}
642
643impl SimpleState<App> for DepartureSummary {
644    fn on_click(
645        &mut self,
646        ctx: &mut EventCtx,
647        app: &mut App,
648        x: &str,
649        _: &mut Panel,
650    ) -> Transition {
651        match x {
652            "close" => Transition::Pop,
653            "Commuter patterns" => Transition::Replace(
654                crate::sandbox::dashboards::CommuterPatterns::new_state(ctx, app),
655            ),
656            "Jump to first trip" => {
657                Transition::Replace(TimeWarpScreen::new_state(ctx, app, self.first_trip, None))
658            }
659            _ => unreachable!(),
660        }
661    }
662}
663
664struct CountTrips {
665    // Times are sorted
666    departures_per_mode: BTreeMap<TripMode, Vec<Time>>,
667}
668
669impl CountTrips {
670    fn new(app: &App) -> CountTrips {
671        let mut departures_per_mode = BTreeMap::new();
672        for m in TripMode::all() {
673            departures_per_mode.insert(m, Vec::new());
674        }
675        for person in &app.primary.scenario.as_ref().unwrap().people {
676            for trip in &person.trips {
677                departures_per_mode
678                    .get_mut(&trip.mode)
679                    .unwrap()
680                    .push(trip.depart);
681            }
682        }
683        for list in departures_per_mode.values_mut() {
684            list.sort();
685        }
686        CountTrips {
687            departures_per_mode,
688        }
689    }
690
691    fn count(&self, mode: TripMode, t1: Time, t2: Time) -> usize {
692        // TODO Could binary search or even have a cursor to remember the position between queries.
693        // Be careful with binary_search_by_key; it might miss multiple trips with the same time.
694        let mut cnt = 0;
695        for t in &self.departures_per_mode[&mode] {
696            if *t >= t1 && *t <= t2 {
697                cnt += 1;
698            }
699            if *t > t2 {
700                break;
701            }
702        }
703        cnt
704    }
705}