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 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 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 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 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 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 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 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 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}