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
19pub 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 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 } else {
173 unreachable!("unhandled action: {}", action)
174 }
175 }
176 },
177 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
217pub 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 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 .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 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 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 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 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
435fn 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}