game/sandbox/
speed.rs

1use crate::ID;
2use abstutil::prettyprint_usize;
3use geom::{Circle, Distance, Duration, Polygon, Pt2D, Ring, Time};
4use sim::AlertLocation;
5use widgetry::tools::PopupMsg;
6use widgetry::{
7    Choice, Color, ControlState, DrawWithTooltips, EdgeInsets, EventCtx, GeomBatch, GfxCtx,
8    HorizontalAlignment, Key, Line, Outcome, Panel, PanelDims, PersistentSplit, ScreenDims, Text,
9    TextExt, VerticalAlignment, Widget,
10};
11
12use crate::app::{App, Transition};
13use crate::common::Warping;
14use crate::sandbox::time_warp::JumpToTime;
15use crate::sandbox::{GameplayMode, SandboxMode, TimeWarpScreen};
16
17pub struct TimePanel {
18    pub panel: Panel,
19    pub override_height: Option<f64>,
20
21    time: Time,
22    paused: bool,
23    setting: SpeedSetting,
24    // if present, how many trips were completed in the baseline at this point
25    baseline_finished_trips: Option<usize>,
26}
27
28#[derive(Clone, Copy, PartialEq, PartialOrd)]
29pub enum SpeedSetting {
30    /// 1 sim second per real second
31    Realtime,
32    /// 5 sim seconds per real second
33    Fast,
34    /// 30 sim seconds per real second
35    Faster,
36    /// 1 sim hour per real second
37    Fastest,
38}
39
40impl TimePanel {
41    pub fn new(ctx: &mut EventCtx, app: &App) -> TimePanel {
42        let mut time = TimePanel {
43            panel: Panel::empty(ctx),
44            override_height: None,
45            time: app.primary.sim.time(),
46            paused: false,
47            setting: SpeedSetting::Realtime,
48            baseline_finished_trips: None,
49        };
50        time.recreate_panel(ctx, app);
51        time
52    }
53
54    pub fn recreate_panel(&mut self, ctx: &mut EventCtx, app: &App) {
55        let mut row = Vec::new();
56        row.push({
57            let button = ctx
58                .style()
59                .btn_plain
60                .icon("system/assets/speed/triangle.svg")
61                .hotkey(Key::Space);
62
63            Widget::custom_row(vec![if self.paused {
64                button.build_widget(ctx, "play")
65            } else {
66                button
67                    .image_path("system/assets/speed/pause.svg")
68                    .build_widget(ctx, "pause")
69            }])
70            .margin_right(16)
71        });
72
73        row.push(
74            Widget::custom_row(
75                vec![
76                    (SpeedSetting::Realtime, "real-time speed"),
77                    (SpeedSetting::Fast, "5x speed"),
78                    (SpeedSetting::Faster, "30x speed"),
79                    (SpeedSetting::Fastest, "3600x speed"),
80                ]
81                .into_iter()
82                .map(|(s, label)| {
83                    let mut txt = Text::from(Line(label).small());
84                    txt.extend(Text::tooltip(ctx, Key::LeftArrow, "slow down"));
85                    txt.extend(Text::tooltip(ctx, Key::RightArrow, "speed up"));
86
87                    let mut triangle_btn = ctx
88                        .style()
89                        .btn_plain
90                        .btn()
91                        .image_path("system/assets/speed/triangle.svg")
92                        .image_dims(ScreenDims::new(16.0, 26.0))
93                        .tooltip(txt)
94                        .padding(EdgeInsets {
95                            top: 8.0,
96                            bottom: 8.0,
97                            left: 3.0,
98                            right: 3.0,
99                        });
100
101                    if s == SpeedSetting::Realtime {
102                        triangle_btn = triangle_btn.padding_left(10.0);
103                    }
104                    if s == SpeedSetting::Fastest {
105                        triangle_btn = triangle_btn.padding_right(10.0);
106                    }
107
108                    if self.setting < s {
109                        triangle_btn = triangle_btn
110                            .image_color(ctx.style().btn_outline.fg_disabled, ControlState::Default)
111                    }
112
113                    triangle_btn.build_widget(ctx, label)
114                })
115                .collect(),
116            )
117            .margin_right(16),
118        );
119
120        row.push(
121            PersistentSplit::widget(
122                ctx,
123                "step forwards",
124                app.opts.time_increment,
125                Key::M,
126                vec![
127                    Choice::new("+1h", Duration::hours(1)),
128                    Choice::new("+30m", Duration::minutes(30)),
129                    Choice::new("+10m", Duration::minutes(10)),
130                    Choice::new("+0.1s", Duration::seconds(0.1)),
131                ],
132            )
133            .margin_right(16),
134        );
135
136        row.push(
137            ctx.style()
138                .btn_plain
139                .icon("system/assets/speed/jump_to_time.svg")
140                .hotkey(Key::B)
141                .build_widget(ctx, "jump to specific time"),
142        );
143
144        row.push(
145            ctx.style()
146                .btn_plain
147                .icon("system/assets/speed/reset.svg")
148                .hotkey(Key::X)
149                .build_widget(ctx, "reset to midnight"),
150        );
151
152        let mut panel = Panel::new_builder(Widget::col(vec![
153            self.create_time_panel(ctx, app).named("time"),
154            Widget::custom_row(row),
155        ]))
156        .aligned(HorizontalAlignment::Left, VerticalAlignment::Top);
157        if let Some(h) = self.override_height {
158            panel = panel.dims_height(PanelDims::ExactPixels(h));
159        }
160        self.panel = panel.build(ctx);
161    }
162
163    fn trips_completion_bar(&mut self, ctx: &EventCtx, app: &App) -> Widget {
164        let text_color = Color::WHITE;
165        let bar_fg = ctx.style().primary_fg;
166        let bar_bg = bar_fg.tint(0.6).shade(0.2);
167        let cursor_fg = Color::hex("#939393");
168
169        // This is manually tuned
170        let bar_width = 400.0;
171        let bar_height = 27.0;
172
173        let (finished, unfinished) = app.primary.sim.num_trips();
174        let total = finished + unfinished;
175        let ratio = if total > 0 {
176            finished as f64 / total as f64
177        } else {
178            0.0
179        };
180        let finished_width = ratio * bar_width;
181
182        if app.has_prebaked().is_some() {
183            let now = self.time;
184            let mut baseline_finished = self.baseline_finished_trips.unwrap_or(0);
185            for (t, _, _, _) in &app.prebaked().finished_trips[baseline_finished..] {
186                if *t > now {
187                    break;
188                }
189                baseline_finished += 1;
190            }
191            // memoized for perf.
192            // A bit of profiling shows we save about 0.7% of runtime
193            // (using montlake, zoomed out, at max speed)
194            self.baseline_finished_trips = Some(baseline_finished);
195        }
196
197        let baseline_finished_ratio: Option<f64> =
198            self.baseline_finished_trips.and_then(|baseline_finished| {
199                if unfinished + baseline_finished > 0 {
200                    Some(baseline_finished as f64 / (baseline_finished + unfinished) as f64)
201                } else {
202                    None
203                }
204            });
205        let baseline_finished_width: Option<f64> = baseline_finished_ratio
206            .map(|baseline_finished_ratio| baseline_finished_ratio * bar_width);
207
208        let cursor_width = 2.0;
209        let mut progress_bar = GeomBatch::new();
210
211        {
212            // TODO Why is the rounding so hard? The white background is always rounded
213            // at both ends. The moving bar should always be rounded on the left, flat
214            // on the right, except at the very end (for the last 'radius' pixels). And
215            // when the width is too small for the radius, this messes up.
216            progress_bar.push(bar_bg, Polygon::rectangle(bar_width, bar_height));
217            if let Ok(p) = Polygon::maybe_rectangle(finished_width, bar_height) {
218                progress_bar.push(bar_fg, p);
219            }
220
221            if let Some(baseline_finished_width) = baseline_finished_width {
222                if baseline_finished_width > 0.0 {
223                    let baseline_cursor = Polygon::rectangle(cursor_width, bar_height)
224                        .translate(baseline_finished_width, 0.0);
225                    progress_bar.push(cursor_fg, baseline_cursor);
226                }
227            }
228        }
229
230        let text_geom = Text::from(
231            Line(format!("Finished Trips: {}", prettyprint_usize(finished))).fg(text_color),
232        )
233        .render(ctx)
234        .translate(8.0, 0.0);
235        progress_bar.append(text_geom);
236
237        if let Some(baseline_finished_width) = baseline_finished_width {
238            let triangle_width = 9.0;
239            let triangle_height = 9.0;
240
241            // Add a triangle-shaped cursor above the baseline cursor
242            progress_bar = progress_bar.translate(0.0, triangle_height);
243
244            let triangle = Ring::must_new(vec![
245                Pt2D::zero(),
246                Pt2D::new(triangle_width, 0.0),
247                Pt2D::new(triangle_width / 2.0, triangle_height),
248                Pt2D::zero(),
249            ])
250            .into_polygon()
251            .translate(
252                baseline_finished_width - triangle_width / 2.0 + cursor_width / 2.0,
253                0.0,
254            );
255            progress_bar.push(cursor_fg, triangle);
256        }
257
258        let mut tooltip_text = Text::from("Finished Trips");
259        tooltip_text.add_line(format!(
260            "{} ({}% of total)",
261            prettyprint_usize(finished),
262            (ratio * 100.0) as usize
263        ));
264        if let Some(baseline_finished) = self.baseline_finished_trips {
265            // TODO: up/down icons
266            let line = match baseline_finished.cmp(&finished) {
267                std::cmp::Ordering::Greater => {
268                    let difference = baseline_finished - finished;
269                    Line(format!(
270                        "{} less than baseline",
271                        prettyprint_usize(difference)
272                    ))
273                    .fg(ctx.style().text_destructive_color)
274                }
275                std::cmp::Ordering::Less => {
276                    let difference = finished - baseline_finished;
277                    Line(format!(
278                        "{} more than baseline",
279                        prettyprint_usize(difference)
280                    ))
281                    .fg(Color::GREEN)
282                }
283                std::cmp::Ordering::Equal => Line("No change from baseline"),
284            };
285            tooltip_text.add_line(line);
286        }
287
288        let bounds = progress_bar.get_bounds();
289        let bounding_box = Polygon::rectangle(bounds.width(), bounds.height());
290        let tooltip = vec![(bounding_box, tooltip_text, None)];
291        DrawWithTooltips::new_widget(ctx, progress_bar, tooltip, Box::new(|_| GeomBatch::new()))
292    }
293
294    fn create_time_panel(&mut self, ctx: &EventCtx, app: &App) -> Widget {
295        let trips_bar = self.trips_completion_bar(ctx, app);
296
297        // TODO This likely fits better in the top center panel, but no easy way to squeeze it
298        // into the panel for all gameplay modes
299        let record_trips = if let Some(n) = app.primary.sim.num_recorded_trips() {
300            Widget::row(vec![
301                GeomBatch::from(vec![(
302                    Color::RED,
303                    Circle::new(Pt2D::new(0.0, 0.0), Distance::meters(10.0)).to_polygon(),
304                )])
305                .into_widget(ctx)
306                .centered_vert(),
307                format!("{} trips captured", prettyprint_usize(n)).text_widget(ctx),
308                ctx.style()
309                    .btn_solid_primary
310                    .text("Finish Capture")
311                    .build_def(ctx)
312                    .align_right(),
313            ])
314        } else {
315            Widget::nothing()
316        };
317
318        Widget::col(vec![
319            Text::from(Line(self.time.ampm_tostring()).big_monospaced()).into_widget(ctx),
320            trips_bar.margin_above(12),
321            if app.primary.dirty_from_edits {
322                ctx.style()
323                    .btn_plain
324                    .icon("system/assets/tools/warning.svg")
325                    .build_widget(ctx, "see why results are tentative")
326                    .centered_vert()
327                    .align_right()
328            } else {
329                Widget::nothing()
330            },
331            record_trips,
332        ])
333    }
334
335    pub fn event(
336        &mut self,
337        ctx: &mut EventCtx,
338        app: &mut App,
339        maybe_mode: Option<&GameplayMode>,
340    ) -> Option<Transition> {
341        if self.time != app.primary.sim.time() {
342            self.time = app.primary.sim.time();
343            let time = self.create_time_panel(ctx, app);
344            self.panel.replace(ctx, "time", time);
345        }
346
347        match self.panel.event(ctx) {
348            Outcome::Clicked(x) => match x.as_ref() {
349                "real-time speed" => {
350                    self.setting = SpeedSetting::Realtime;
351                    self.recreate_panel(ctx, app);
352                    return None;
353                }
354                "5x speed" => {
355                    self.setting = SpeedSetting::Fast;
356                    self.recreate_panel(ctx, app);
357                    return None;
358                }
359                "30x speed" => {
360                    self.setting = SpeedSetting::Faster;
361                    self.recreate_panel(ctx, app);
362                    return None;
363                }
364                "3600x speed" => {
365                    self.setting = SpeedSetting::Fastest;
366                    self.recreate_panel(ctx, app);
367                    return None;
368                }
369                "play" => {
370                    self.paused = false;
371                    self.recreate_panel(ctx, app);
372                    return None;
373                }
374                "pause" => {
375                    self.pause(ctx, app);
376                }
377                "reset to midnight" => {
378                    if let Some(mode) = maybe_mode {
379                        return Some(Transition::Replace(SandboxMode::simple_new(
380                            app,
381                            mode.clone(),
382                        )));
383                    } else {
384                        return Some(Transition::Push(PopupMsg::new_state(
385                            ctx,
386                            "Error",
387                            vec!["Sorry, you can't go rewind time from this mode."],
388                        )));
389                    }
390                }
391                "jump to specific time" => {
392                    return Some(Transition::Push(JumpToTime::new_state(
393                        ctx,
394                        app,
395                        maybe_mode.cloned(),
396                    )));
397                }
398                "step forwards" => {
399                    let dt = self.panel.persistent_split_value("step forwards");
400                    if dt == Duration::seconds(0.1) {
401                        app.primary
402                            .sim
403                            .tiny_step(&app.primary.map, &mut app.primary.sim_cb);
404                        app.recalculate_current_selection(ctx);
405                        return Some(Transition::KeepWithMouseover);
406                    }
407                    return Some(Transition::Push(TimeWarpScreen::new_state(
408                        ctx,
409                        app,
410                        app.primary.sim.time() + dt,
411                        None,
412                    )));
413                }
414                "see why results are tentative" => {
415                    return Some(Transition::Push(PopupMsg::new_state(
416                        ctx,
417                        "Simulation results not finalized",
418                        vec![
419                            "You edited the map in the middle of the day.",
420                            "Some trips may have been interrupted, and others might have made \
421                            different decisions if they saw the new map from the start.",
422                            "To get final results, reset to midnight and test your proposal over \
423                            a full day.",
424                        ],
425                    )));
426                }
427                "Finish Capture" => {
428                    app.primary.sim.save_recorded_traffic(&app.primary.map);
429                }
430                _ => unreachable!(),
431            },
432            Outcome::Changed(x) => {
433                if x == "step forwards" {
434                    app.opts.time_increment = self.panel.persistent_split_value("step forwards");
435                }
436            }
437            _ => {}
438        }
439
440        if ctx.input.pressed(Key::LeftArrow) {
441            match self.setting {
442                SpeedSetting::Realtime => self.pause(ctx, app),
443                SpeedSetting::Fast => {
444                    self.setting = SpeedSetting::Realtime;
445                    self.recreate_panel(ctx, app);
446                }
447                SpeedSetting::Faster => {
448                    self.setting = SpeedSetting::Fast;
449                    self.recreate_panel(ctx, app);
450                }
451                SpeedSetting::Fastest => {
452                    self.setting = SpeedSetting::Faster;
453                    self.recreate_panel(ctx, app);
454                }
455            }
456        }
457        if ctx.input.pressed(Key::RightArrow) {
458            match self.setting {
459                SpeedSetting::Realtime => {
460                    if self.paused {
461                        self.paused = false;
462                    } else {
463                        self.setting = SpeedSetting::Fast;
464                    }
465                    self.recreate_panel(ctx, app);
466                }
467                SpeedSetting::Fast => {
468                    self.setting = SpeedSetting::Faster;
469                    self.recreate_panel(ctx, app);
470                }
471                SpeedSetting::Faster => {
472                    self.setting = SpeedSetting::Fastest;
473                    self.recreate_panel(ctx, app);
474                }
475                SpeedSetting::Fastest => {}
476            }
477        }
478
479        if !self.paused {
480            if let Some(real_dt) = ctx.input.nonblocking_is_update_event() {
481                ctx.input.use_update_event();
482                let multiplier = match self.setting {
483                    SpeedSetting::Realtime => 1.0,
484                    SpeedSetting::Fast => 5.0,
485                    SpeedSetting::Faster => 30.0,
486                    SpeedSetting::Fastest => 3600.0,
487                };
488                let dt = multiplier * real_dt;
489                // TODO This should match the update frequency in widgetry. Plumb along the deadline
490                // or frequency to here.
491                app.primary.sim.time_limited_step(
492                    &app.primary.map,
493                    dt,
494                    Duration::seconds(0.033),
495                    &mut app.primary.sim_cb,
496                );
497                app.recalculate_current_selection(ctx);
498            }
499        }
500
501        // TODO Need to do this anywhere that steps the sim, like TimeWarpScreen.
502        let alerts = app.primary.sim.clear_alerts();
503        if !alerts.is_empty() {
504            let popup = PopupMsg::new_state(
505                ctx,
506                "Alerts",
507                alerts.iter().map(|(_, _, msg)| msg).collect(),
508            );
509            let maybe_id = match alerts[0].1 {
510                AlertLocation::Nil => None,
511                AlertLocation::Intersection(i) => Some(ID::Intersection(i)),
512                // TODO Open info panel and warp to them
513                AlertLocation::Person(_) => None,
514                AlertLocation::Building(b) => Some(ID::Building(b)),
515            };
516            // TODO Can filter for particular alerts places like this:
517            /*if !alerts[0].2.contains("Turn conflict cycle") {
518                return None;
519            }*/
520            /*if maybe_id != Some(ID::Building(map_model::BuildingID(91))) {
521                return None;
522            }*/
523            self.pause(ctx, app);
524            if let Some(id) = maybe_id {
525                // Just go to the first one, but print all messages
526                return Some(Transition::Multi(vec![
527                    Transition::Push(popup),
528                    Transition::Push(Warping::new_state(
529                        ctx,
530                        app.primary.canonical_point(id).unwrap(),
531                        Some(10.0),
532                        None,
533                        &mut app.primary,
534                    )),
535                ]));
536            } else {
537                return Some(Transition::Push(popup));
538            }
539        }
540
541        None
542    }
543
544    pub fn draw(&self, g: &mut GfxCtx) {
545        self.panel.draw(g);
546    }
547
548    pub fn pause(&mut self, ctx: &mut EventCtx, app: &App) {
549        if !self.paused {
550            self.paused = true;
551            self.recreate_panel(ctx, app);
552        }
553    }
554
555    pub fn resume(&mut self, ctx: &mut EventCtx, app: &App, setting: SpeedSetting) {
556        if self.paused || self.setting != setting {
557            self.paused = false;
558            self.setting = setting;
559            self.recreate_panel(ctx, app);
560        }
561    }
562
563    pub fn is_paused(&self) -> bool {
564        self.paused
565    }
566}