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 baseline_finished_trips: Option<usize>,
26}
27
28#[derive(Clone, Copy, PartialEq, PartialOrd)]
29pub enum SpeedSetting {
30 Realtime,
32 Fast,
34 Faster,
36 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 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 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 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 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 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 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 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 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 AlertLocation::Person(_) => None,
514 AlertLocation::Building(b) => Some(ID::Building(b)),
515 };
516 self.pause(ctx, app);
524 if let Some(id) = maybe_id {
525 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}