game/sandbox/
time_warp.rs

1use anyhow::Result;
2use instant::Instant;
3
4use crate::ID;
5use abstutil::prettyprint_usize;
6use geom::{Duration, Polygon, Pt2D, Ring, Time};
7use map_gui::render::DrawOptions;
8use map_gui::tools::grey_out_map;
9use widgetry::tools::PopupMsg;
10use widgetry::{
11    Choice, DrawBaselayer, EventCtx, GeomBatch, GfxCtx, Key, Line, Outcome, Panel, PanelDims,
12    Slider, State, TabController, Text, Toggle, UpdateType, Widget,
13};
14
15use crate::app::{App, FindDelayedIntersections, ShowEverything, Transition};
16use crate::common::Warping;
17use crate::sandbox::{GameplayMode, SandboxMode};
18
19// TODO Text entry would be great
20pub struct JumpToTime {
21    panel: Panel,
22    target: Time,
23    maybe_mode: Option<GameplayMode>,
24    tabs: TabController,
25}
26
27impl JumpToTime {
28    pub fn new_state(
29        ctx: &mut EventCtx,
30        app: &App,
31        maybe_mode: Option<GameplayMode>,
32    ) -> Box<dyn State<App>> {
33        let target = app.primary.sim.time();
34        let end_of_day = app.primary.sim.get_end_of_day();
35
36        let jump_to_time_btn = ctx
37            .style()
38            .btn_tab
39            .text("Jump to time")
40            .hotkey(Key::T)
41            .tooltip("Jump to time");
42        let jump_to_time_content = {
43            // TODO Auto-fill width?
44            let slider_width = 500.0;
45
46            Widget::col(vec![
47                Line("Jump to what time?").small_heading().into_widget(ctx),
48                if app.has_prebaked().is_some() {
49                    match area_under_curve(
50                        app.prebaked().active_agents(end_of_day),
51                        slider_width,
52                        50.0,
53                    ) {
54                        Ok(polygon) => {
55                            GeomBatch::from(vec![(ctx.style().icon_fg.alpha(0.7), polygon)])
56                                .into_widget(ctx)
57                        }
58
59                        Err(err) => {
60                            warn!("Not drawing area under curve: {err}");
61                            Widget::nothing()
62                        }
63                    }
64                } else {
65                    Widget::nothing()
66                },
67                Slider::area(
68                    ctx,
69                    slider_width,
70                    target.to_percent(end_of_day).min(1.0),
71                    "time slider",
72                ),
73                build_jump_to_time_btn(ctx, target),
74            ])
75        };
76
77        let jump_to_delay_btn = ctx
78            .style()
79            .btn_tab
80            .text("Jump to delay")
81            .hotkey(Key::D)
82            .tooltip("Jump to delay");
83        let jump_to_delay_content = Widget::col(vec![
84            Widget::row(vec![
85                Line("Jump to next").small_heading().into_widget(ctx),
86                Widget::dropdown(
87                    ctx,
88                    "delay",
89                    app.opts.jump_to_delay,
90                    vec![
91                        Choice::new("1", Duration::minutes(1)),
92                        Choice::new("2", Duration::minutes(2)),
93                        Choice::new("5", Duration::minutes(5)),
94                        Choice::new("10", Duration::minutes(10)),
95                    ],
96                ),
97                Line("minute delay").small_heading().into_widget(ctx),
98            ]),
99            build_jump_to_delay_button(ctx, app.opts.jump_to_delay),
100        ]);
101
102        let mut tabs = TabController::new("jump_to_time_tabs");
103        tabs.push_tab(jump_to_time_btn, jump_to_time_content);
104        tabs.push_tab(jump_to_delay_btn, jump_to_delay_content);
105
106        Box::new(JumpToTime {
107            target,
108            maybe_mode,
109            panel: Panel::new_builder(Widget::col(vec![
110                ctx.style().btn_close_widget(ctx),
111                tabs.build_widget(ctx),
112            ]))
113            .dims_width(PanelDims::ExactPixels(640.0))
114            .dims_height(PanelDims::ExactPixels(360.0))
115            .build(ctx),
116            tabs,
117        })
118    }
119}
120
121impl State<App> for JumpToTime {
122    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
123        match self.panel.event(ctx) {
124            Outcome::Clicked(x) => match x.as_ref() {
125                "close" => {
126                    return Transition::Pop;
127                }
128                "jump to time" => {
129                    if self.target < app.primary.sim.time() {
130                        if let Some(mode) = self.maybe_mode.take() {
131                            let target_time = self.target;
132                            return Transition::Replace(SandboxMode::async_new(
133                                app,
134                                mode,
135                                Box::new(move |ctx, app| {
136                                    vec![Transition::Push(TimeWarpScreen::new_state(
137                                        ctx,
138                                        app,
139                                        target_time,
140                                        None,
141                                    ))]
142                                }),
143                            ));
144                        } else {
145                            return Transition::Replace(PopupMsg::new_state(
146                                ctx,
147                                "Error",
148                                vec!["Sorry, you can't go rewind time from this mode."],
149                            ));
150                        }
151                    }
152                    return Transition::Replace(TimeWarpScreen::new_state(
153                        ctx,
154                        app,
155                        self.target,
156                        None,
157                    ));
158                }
159                "jump to delay" => {
160                    let delay = self.panel.dropdown_value("delay");
161                    app.opts.jump_to_delay = delay;
162                    return Transition::Replace(TimeWarpScreen::new_state(
163                        ctx,
164                        app,
165                        app.primary.sim.get_end_of_day(),
166                        Some(delay),
167                    ));
168                }
169                action => {
170                    if self.tabs.handle_action(ctx, action, &mut self.panel) {
171                        // if true, tabs has handled the action
172                    } else {
173                        unreachable!("unhandled action: {}", action)
174                    }
175                }
176            },
177            // TODO check what changed...
178            Outcome::Changed(_) => {
179                if self.tabs.active_tab_idx() == 1 {
180                    self.panel.replace(
181                        ctx,
182                        "jump to delay",
183                        build_jump_to_delay_button(ctx, self.panel.dropdown_value("delay")),
184                    );
185                }
186            }
187            _ => {}
188        }
189
190        if self.tabs.active_tab_idx() == 0 {
191            let target = app
192                .primary
193                .sim
194                .get_end_of_day()
195                .percent_of(self.panel.slider("time slider").get_percent())
196                .round_seconds(600.0);
197            if target != self.target {
198                self.target = target;
199                self.panel
200                    .replace(ctx, "jump to time", build_jump_to_time_btn(ctx, target));
201            }
202        }
203
204        if self.panel.clicked_outside(ctx) {
205            return Transition::Pop;
206        }
207
208        Transition::Keep
209    }
210
211    fn draw(&self, g: &mut GfxCtx, app: &App) {
212        grey_out_map(g, app);
213        self.panel.draw(g);
214    }
215}
216
217// Display a nicer screen for jumping forwards in time, allowing cancellation.
218pub struct TimeWarpScreen {
219    target: Time,
220    wall_time_started: Instant,
221    sim_time_started: geom::Time,
222    halt_upon_delay: Option<Duration>,
223    panel: Panel,
224}
225
226impl TimeWarpScreen {
227    pub fn new_state(
228        ctx: &mut EventCtx,
229        app: &mut App,
230        target: Time,
231        mut halt_upon_delay: Option<Duration>,
232    ) -> Box<dyn State<App>> {
233        if let Some(halt_limit) = halt_upon_delay {
234            if app.primary.sim_cb.is_none() {
235                app.primary.sim_cb = Some(Box::new(FindDelayedIntersections {
236                    halt_limit,
237                    report_limit: halt_limit,
238                    currently_delayed: Vec::new(),
239                }));
240                // TODO Can we get away with less frequently? Not sure about all the edge cases
241                app.primary.sim.set_periodic_callback(Duration::minutes(1));
242            } else {
243                halt_upon_delay = None;
244            }
245        }
246
247        Box::new(TimeWarpScreen {
248            target,
249            wall_time_started: Instant::now(),
250            sim_time_started: app.primary.sim.time(),
251            halt_upon_delay,
252            panel: Panel::new_builder(
253                Widget::col(vec![
254                    Widget::placeholder(ctx, "text"),
255                    Toggle::checkbox(
256                        ctx,
257                        "skip drawing (for faster simulations)",
258                        Key::Space,
259                        app.opts.dont_draw_time_warp,
260                    )
261                    .named("don't draw"),
262                    ctx.style()
263                        .btn_outline
264                        .text("stop now")
265                        .hotkey(Key::Escape)
266                        .build_def(ctx)
267                        .centered_horiz(),
268                ])
269                // hardcoded width avoids jiggle due to text updates
270                .force_width(700.0),
271            )
272            .build(ctx),
273        })
274    }
275}
276
277impl State<App> for TimeWarpScreen {
278    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
279        if ctx.input.nonblocking_is_update_event().is_some() {
280            ctx.input.use_update_event();
281            app.primary.sim.time_limited_step(
282                &app.primary.map,
283                self.target - app.primary.sim.time(),
284                Duration::seconds(0.033),
285                &mut app.primary.sim_cb,
286            );
287            #[allow(clippy::never_loop)]
288            for (t, maybe_i, alert) in app.primary.sim.clear_alerts() {
289                // TODO Just the first :(
290                return Transition::Replace(PopupMsg::new_state(
291                    ctx,
292                    "Alert",
293                    vec![format!("At {}, near {:?}, {}", t, maybe_i, alert)],
294                ));
295            }
296            if let Some(ref mut cb) = app.primary.sim_cb {
297                let di = cb.downcast_mut::<FindDelayedIntersections>().unwrap();
298                if let Some((i, t)) = di.currently_delayed.get(0) {
299                    if app.primary.sim.time() - *t > di.halt_limit {
300                        let id = ID::Intersection(*i);
301                        app.primary.layer =
302                            Some(Box::new(crate::layer::traffic::TrafficJams::new(ctx, app)));
303                        return Transition::Replace(Warping::new_state(
304                            ctx,
305                            app.primary.canonical_point(id.clone()).unwrap(),
306                            Some(10.0),
307                            Some(id),
308                            &mut app.primary,
309                        ));
310                    }
311                }
312            }
313
314            let now = app.primary.sim.time();
315            let (finished_after, _) = app.primary.sim.num_trips();
316            let finished_before = if app.has_prebaked().is_some() {
317                let mut cnt = 0;
318                for (t, _, _, _) in &app.prebaked().finished_trips {
319                    if *t > now {
320                        break;
321                    }
322                    cnt += 1;
323                }
324                Some(cnt)
325            } else {
326                None
327            };
328
329            let elapsed_sim_time = now - self.sim_time_started;
330            let elapsed_wall_time = Duration::realtime_elapsed(self.wall_time_started);
331            let txt = Text::from_multiline(vec![
332                // I'm covered in shame for not doing this from the start.
333                Line("Let's do the time warp again!").small_heading(),
334                Line(format!(
335                    "{} / {}",
336                    now.ampm_tostring(),
337                    self.target.ampm_tostring()
338                )),
339                Line(format!(
340                    "Speed: {}x",
341                    prettyprint_usize((elapsed_sim_time / elapsed_wall_time) as usize)
342                )),
343                if let Some(n) = finished_before {
344                    // TODO Underline
345                    Line(format!(
346                        "Finished trips: {} ({} compared to before \"{}\")",
347                        prettyprint_usize(finished_after),
348                        compare_count(finished_after, n),
349                        app.primary.map.get_edits().edits_name,
350                    ))
351                } else {
352                    Line(format!(
353                        "Finished trips: {}",
354                        prettyprint_usize(finished_after)
355                    ))
356                },
357            ]);
358
359            self.panel.replace(ctx, "text", txt.into_widget(ctx));
360        }
361        // >= because of the case of resetting to midnight. GameplayMode::initialize takes a tiny
362        // step past midnight after spawning things, so that agents initially appear on the map.
363        if app.primary.sim.time() >= self.target {
364            return Transition::Pop;
365        }
366
367        match self.panel.event(ctx) {
368            Outcome::Changed(_) => {
369                app.opts.dont_draw_time_warp = self.panel.is_checked("don't draw");
370            }
371            Outcome::Clicked(x) => match x.as_ref() {
372                "stop now" => {
373                    return Transition::Pop;
374                }
375                _ => unreachable!(),
376            },
377            _ => {}
378        }
379        if self.panel.clicked_outside(ctx) {
380            return Transition::Pop;
381        }
382
383        ctx.request_update(UpdateType::Game);
384        Transition::Keep
385    }
386
387    fn draw_baselayer(&self) -> DrawBaselayer {
388        DrawBaselayer::Custom
389    }
390
391    fn draw(&self, g: &mut GfxCtx, app: &App) {
392        if app.opts.dont_draw_time_warp {
393            g.clear(app.cs.inner_panel_bg);
394        } else {
395            app.draw(g, DrawOptions::new(), &ShowEverything::new());
396            grey_out_map(g, app);
397        }
398
399        self.panel.draw(g);
400    }
401
402    fn on_destroy(&mut self, _: &mut EventCtx, app: &mut App) {
403        if self.halt_upon_delay.is_some() {
404            assert!(app.primary.sim_cb.is_some());
405            app.primary.sim_cb = None;
406            app.primary.sim.unset_periodic_callback();
407        }
408    }
409}
410
411fn area_under_curve(raw: Vec<(Time, usize)>, width: f64, height: f64) -> Result<Polygon> {
412    assert!(!raw.is_empty());
413    let min_x = Time::START_OF_DAY;
414    let min_y = 0;
415    let max_x = raw.last().unwrap().0;
416    let max_y = raw.iter().max_by_key(|(_, cnt)| *cnt).unwrap().1;
417
418    let mut pts = Vec::new();
419    for (t, cnt) in raw {
420        pts.push(lttb::DataPoint::new(
421            width * (t - min_x) / (max_x - min_x),
422            height * (1.0 - (((cnt - min_y) as f64) / ((max_y - min_y) as f64))),
423        ));
424    }
425    let mut downsampled = Vec::new();
426    for pt in lttb::lttb(pts, 100) {
427        downsampled.push(Pt2D::new(pt.x, pt.y));
428    }
429    downsampled.push(Pt2D::new(width, height));
430    downsampled.push(downsampled[0]);
431
432    Ring::deduping_new(downsampled).map(|ring| ring.into_polygon())
433}
434
435// TODO Maybe color, put in helpers
436fn compare_count(after: usize, before: usize) -> String {
437    match after.cmp(&before) {
438        std::cmp::Ordering::Equal => "+0".to_string(),
439        std::cmp::Ordering::Greater => {
440            format!("+{}", prettyprint_usize(after - before))
441        }
442        std::cmp::Ordering::Less => {
443            format!("-{}", prettyprint_usize(before - after))
444        }
445    }
446}
447
448fn build_jump_to_time_btn(ctx: &EventCtx, target: Time) -> Widget {
449    ctx.style()
450        .btn_solid_primary
451        .text(format!("Jump to {}", target.ampm_tostring()))
452        .hotkey(Key::Enter)
453        .build_widget(ctx, "jump to time")
454        .centered_horiz()
455        .margin_above(16)
456}
457
458fn build_jump_to_delay_button(ctx: &EventCtx, delay: Duration) -> Widget {
459    ctx.style()
460        .btn_solid_primary
461        .text(format!("Jump to next {} delay", delay))
462        .hotkey(Key::Enter)
463        .build_widget(ctx, "jump to delay")
464        .centered_horiz()
465        .margin_above(16)
466}