1use std::collections::BTreeMap;
2
3use maplit::btreemap;
4
5use crate::ID;
6use geom::{Distance, Duration, Percent, Polygon, Pt2D, UnitFmt};
7use map_model::{Map, Path, PathStep, Traversable};
8use sim::{AgentID, Analytics, PersonID, Problem, TripID, TripInfo, TripPhase, TripPhaseType};
9use synthpop::{TripEndpoint, TripMode};
10use widgetry::{
11 Color, ControlState, DrawWithTooltips, EventCtx, GeomBatch, Line, LinePlot, PlotOptions,
12 RewriteColor, Series, Text, TextExt, Widget,
13};
14
15use crate::app::App;
16use crate::common::color_for_trip_phase;
17use crate::info::{make_table, Details, Tab};
18
19#[derive(Clone)]
20pub struct OpenTrip {
21 pub show_after: bool,
22 cached_routes: Vec<Option<(Polygon, Vec<Polygon>)>>,
24}
25
26impl std::cmp::PartialEq for OpenTrip {
28 fn eq(&self, other: &OpenTrip) -> bool {
29 self.show_after == other.show_after
30 }
31}
32
33impl OpenTrip {
34 pub fn single(id: TripID) -> BTreeMap<TripID, OpenTrip> {
35 btreemap! { id => OpenTrip::new() }
36 }
37
38 pub fn new() -> OpenTrip {
39 OpenTrip {
40 show_after: true,
41 cached_routes: Vec::new(),
42 }
43 }
44}
45
46pub fn ongoing(
47 ctx: &mut EventCtx,
48 app: &App,
49 id: TripID,
50 agent: AgentID,
51 open_trip: &mut OpenTrip,
52 details: &mut Details,
53) -> Widget {
54 let phases = app
55 .primary
56 .sim
57 .get_analytics()
58 .get_trip_phases(id, &app.primary.map);
59 let trip = app.primary.sim.trip_info(id);
60
61 let col_width = Percent::int(7);
62 let props = app.primary.sim.agent_properties(&app.primary.map, agent);
63 let activity = agent.to_type().ongoing_verb();
64 let time_so_far = app.primary.sim.time() - trip.departure;
65
66 let mut col = Vec::new();
67
68 {
69 col.push(Widget::custom_row(vec![
70 Widget::custom_row(vec![Line("Trip time").secondary().into_widget(ctx)])
71 .force_width_window_pct(ctx, col_width),
72 Text::from_all(vec![
73 Line(props.total_time.to_string(&app.opts.units)),
74 Line(format!(
75 " {} / {} this trip",
76 activity,
77 time_so_far.to_string(&app.opts.units)
78 ))
79 .secondary(),
80 ])
81 .into_widget(ctx),
82 ]));
83 }
84 {
85 col.push(Widget::custom_row(vec![
86 Widget::custom_row(vec![Line("Distance").secondary().into_widget(ctx)])
87 .force_width_window_pct(ctx, col_width),
88 Text::from_all(vec![
89 Line(props.dist_crossed.to_string(&app.opts.units)),
90 Line(format!("/{}", props.total_dist.to_string(&app.opts.units))).secondary(),
91 ])
92 .into_widget(ctx),
93 ]));
94 }
95 {
96 col.push(Widget::custom_row(vec![
97 Line("Waiting")
98 .secondary()
99 .into_widget(ctx)
100 .container()
101 .force_width_window_pct(ctx, col_width),
102 Widget::col(vec![
103 format!("{} here", props.waiting_here.to_string(&app.opts.units)).text_widget(ctx),
104 Text::from_all(vec![
105 if props.total_waiting != Duration::ZERO {
106 Line(format!(
107 "{}%",
108 (100.0 * (props.total_waiting / time_so_far)) as usize
109 ))
110 } else {
111 Line("0%")
112 },
113 Line(format!(" total of {} time spent waiting", activity)).secondary(),
114 ])
115 .into_widget(ctx),
116 ]),
117 ]));
118 }
119 col.push(describe_problems(
120 ctx,
121 app.primary.sim.get_analytics(),
122 id,
123 &trip,
124 col_width,
125 ));
126 {
127 col.push(Widget::custom_row(vec![
128 Widget::custom_row(vec![Line("Purpose").secondary().into_widget(ctx)])
129 .force_width_window_pct(ctx, col_width),
130 Line(trip.purpose.to_string()).secondary().into_widget(ctx),
131 ]));
132 }
133
134 col.push(make_trip_details(
135 ctx,
136 app,
137 id,
138 open_trip,
139 details,
140 phases,
141 &app.primary.map,
142 Some(props.dist_crossed.safe_percent(props.total_dist)),
143 ));
144 Widget::col(col)
145}
146
147pub fn future(
148 ctx: &mut EventCtx,
149 app: &App,
150 id: TripID,
151 open_trip: &mut OpenTrip,
152 details: &mut Details,
153) -> Widget {
154 let trip = app.primary.sim.trip_info(id);
155
156 let mut col = Vec::new();
157
158 let now = app.primary.sim.time();
159 if now > trip.departure {
160 col.extend(make_table(
161 ctx,
162 vec![(
163 "Start delayed",
164 (now - trip.departure).to_string(&app.opts.units),
165 )],
166 ));
167 }
168
169 if let Some(estimated_trip_time) = app
170 .has_prebaked()
171 .and_then(|_| app.prebaked().finished_trip_time(id))
172 {
173 col.extend(make_table(
174 ctx,
175 vec![
176 (
177 "Estimated trip time",
178 estimated_trip_time.to_string(&app.opts.units),
179 ),
180 ("Purpose", trip.purpose.to_string()),
181 ],
182 ));
183
184 let unedited_map = app
185 .primary
186 .unedited_map
187 .as_ref()
188 .unwrap_or(&app.primary.map);
189 let phases = app.prebaked().get_trip_phases(id, unedited_map);
190 col.push(make_trip_details(
191 ctx,
192 app,
193 id,
194 open_trip,
195 details,
196 phases,
197 unedited_map,
198 None,
199 ));
200 } else {
201 col.extend(make_table(ctx, vec![("Purpose", trip.purpose.to_string())]));
202
203 col.push(make_trip_details(
204 ctx,
205 app,
206 id,
207 open_trip,
208 details,
209 Vec::new(),
210 &app.primary.map,
211 None,
212 ));
213 }
214
215 Widget::col(col)
216}
217
218pub fn finished(
219 ctx: &mut EventCtx,
220 app: &App,
221 person: PersonID,
222 open_trips: &mut BTreeMap<TripID, OpenTrip>,
223 id: TripID,
224 details: &mut Details,
225) -> Widget {
226 let trip = app.primary.sim.trip_info(id);
227 let (phases, map_for_pathfinding) = if open_trips[&id].show_after {
228 (
229 app.primary
230 .sim
231 .get_analytics()
232 .get_trip_phases(id, &app.primary.map),
233 &app.primary.map,
234 )
235 } else {
236 let unedited_map = app
237 .primary
238 .unedited_map
239 .as_ref()
240 .unwrap_or(&app.primary.map);
241 (
242 app.prebaked().get_trip_phases(id, unedited_map),
243 unedited_map,
244 )
245 };
246
247 let mut col = Vec::new();
248
249 if open_trips[&id].show_after && app.has_prebaked().is_some() {
250 let mut open = open_trips.clone();
251 open.insert(
252 id,
253 OpenTrip {
254 show_after: false,
255 cached_routes: Vec::new(),
256 },
257 );
258 details.hyperlinks.insert(
259 format!("show before changes for {}", id),
260 Tab::PersonTrips(person, open),
261 );
262 col.push(
263 ctx.style()
264 .btn_outline
265 .btn()
266 .label_styled_text(
267 Text::from_all(vec![
268 Line("After / "),
269 Line("Before").secondary(),
270 Line(" "),
271 Line(&app.primary.map.get_edits().edits_name).underlined(),
272 ]),
273 ControlState::Default,
274 )
275 .build_widget(ctx, format!("show before changes for {}", id)),
276 );
277 } else if app.has_prebaked().is_some() {
278 let mut open = open_trips.clone();
279 open.insert(id, OpenTrip::new());
280 details.hyperlinks.insert(
281 format!("show after changes for {}", id),
282 Tab::PersonTrips(person, open),
283 );
284 col.push(
285 ctx.style()
286 .btn_outline
287 .btn()
288 .label_styled_text(
289 Text::from_all(vec![
290 Line("After / ").secondary(),
291 Line("Before"),
292 Line(" "),
293 Line(&app.primary.map.get_edits().edits_name).underlined(),
294 ]),
295 ControlState::Default,
296 )
297 .build_widget(ctx, format!("show after changes for {}", id)),
298 );
299 }
300
301 let col_width = Percent::int(15);
302 {
303 if let Some(end_time) = phases.last().as_ref().and_then(|p| p.end_time) {
304 col.push(Widget::custom_row(vec![
305 Widget::custom_row(vec![Line("Trip time").secondary().into_widget(ctx)])
306 .force_width_window_pct(ctx, col_width),
307 (end_time - trip.departure)
308 .to_string(&app.opts.units)
309 .text_widget(ctx),
310 ]));
311 } else {
312 col.push(Widget::custom_row(vec![
313 Widget::custom_row(vec![Line("Trip time").secondary().into_widget(ctx)])
314 .force_width_window_pct(ctx, col_width),
315 "Trip didn't complete before map changes".text_widget(ctx),
316 ]));
317 }
318
319 let (_, waiting, _) = app.primary.sim.finished_trip_details(id).unwrap();
322 col.push(Widget::custom_row(vec![
323 Widget::custom_row(vec![Line("Total waiting time")
324 .secondary()
325 .into_widget(ctx)])
326 .force_width_window_pct(ctx, col_width),
327 waiting.to_string(&app.opts.units).text_widget(ctx),
328 ]));
329
330 col.push(Widget::custom_row(vec![
331 Widget::custom_row(vec![Line("Purpose").secondary().into_widget(ctx)])
332 .force_width_window_pct(ctx, col_width),
333 Line(trip.purpose.to_string()).secondary().into_widget(ctx),
334 ]));
335 }
336 col.push(describe_problems(
337 ctx,
338 if open_trips[&id].show_after {
339 app.primary.sim.get_analytics()
340 } else {
341 app.prebaked()
342 },
343 id,
344 &trip,
345 col_width,
346 ));
347
348 col.push(make_trip_details(
349 ctx,
350 app,
351 id,
352 open_trips.get_mut(&id).unwrap(),
353 details,
354 phases,
355 map_for_pathfinding,
356 None,
357 ));
358 Widget::col(col)
359}
360
361pub fn cancelled(
362 ctx: &mut EventCtx,
363 app: &App,
364 id: TripID,
365 open_trip: &mut OpenTrip,
366 details: &mut Details,
367) -> Widget {
368 let trip = app.primary.sim.trip_info(id);
369
370 let mut col = vec![Text::from(format!(
371 "Trip cancelled: {}",
372 trip.cancellation_reason.as_ref().unwrap()
373 ))
374 .wrap_to_pct(ctx, 20)
375 .into_widget(ctx)];
376
377 col.extend(make_table(ctx, vec![("Purpose", trip.purpose.to_string())]));
378
379 col.push(make_trip_details(
380 ctx,
381 app,
382 id,
383 open_trip,
384 details,
385 Vec::new(),
386 &app.primary.map,
387 None,
388 ));
389
390 Widget::col(col)
391}
392
393fn describe_problems(
394 ctx: &mut EventCtx,
395 analytics: &Analytics,
396 id: TripID,
397 trip: &TripInfo,
398 col_width: Percent,
399) -> Widget {
400 match trip.mode {
401 TripMode::Walk => {
402 let mut arterial_intersection_crossings = 0;
403 let mut overcrowding = 0;
404 let empty = Vec::new();
405 for (_, problem) in analytics.problems_per_trip.get(&id).unwrap_or(&empty) {
406 match problem {
407 Problem::ArterialIntersectionCrossing(_) => {
408 arterial_intersection_crossings += 1;
409 }
410 Problem::PedestrianOvercrowding(_) => {
411 overcrowding += 1;
412 }
413 _ => {}
414 }
415 }
416 let mut txt = Text::new();
417 txt.add_appended(vec![
418 Line(arterial_intersection_crossings.to_string()),
419 if arterial_intersection_crossings == 1 {
420 Line(" crossing at wide intersections")
421 } else {
422 Line(" crossings at wide intersections")
423 }
424 .secondary(),
425 ]);
426 txt.add_line(Line(format!("{overcrowding} overcrowded sidewalks crossed")).secondary());
427
428 Widget::custom_row(vec![
429 Line("Risk Exposure")
430 .secondary()
431 .into_widget(ctx)
432 .container()
433 .force_width_window_pct(ctx, col_width),
434 txt.into_widget(ctx),
435 ])
436 }
437 TripMode::Bike => {
438 let mut count_complex_intersections = 0;
439 let mut count_overtakes = 0;
440 let empty = Vec::new();
441 for (_, problem) in analytics.problems_per_trip.get(&id).unwrap_or(&empty) {
442 match problem {
443 Problem::ComplexIntersectionCrossing(_) => {
444 count_complex_intersections += 1;
445 }
446 Problem::OvertakeDesired(_) => {
447 count_overtakes += 1;
448 }
449 _ => {}
450 }
451 }
452 let mut txt = Text::new();
453 txt.add_appended(vec![
454 Line(count_complex_intersections.to_string()),
455 if count_complex_intersections == 1 {
456 Line(" crossing at complex intersections")
457 } else {
458 Line(" crossings at complex intersections")
459 }
460 .secondary(),
461 ]);
462 txt.add_appended(vec![
463 Line(count_overtakes.to_string()),
464 if count_overtakes == 1 {
465 Line(" vehicle wanted to over-take")
466 } else {
467 Line(" vehicles wanted to over-take")
468 }
469 .secondary(),
470 ]);
471
472 Widget::custom_row(vec![
473 Line("Risk Exposure")
474 .secondary()
475 .into_widget(ctx)
476 .container()
477 .force_width_window_pct(ctx, col_width),
478 txt.into_widget(ctx),
479 ])
480 }
481 _ => Widget::nothing(),
482 }
483}
484
485fn draw_problems(
486 ctx: &EventCtx,
487 app: &App,
488 analytics: &Analytics,
489 details: &mut Details,
490 id: TripID,
491 map: &Map,
492) {
493 let empty = Vec::new();
494 for (time, problem) in analytics.problems_per_trip.get(&id).unwrap_or(&empty) {
495 match problem {
496 Problem::IntersectionDelay(i, delay) => {
497 let i = map.get_i(*i);
498 let (slow, slower) = if i.is_traffic_signal() {
500 (Duration::seconds(30.0), Duration::minutes(2))
501 } else {
502 (Duration::seconds(5.0), Duration::seconds(30.0))
503 };
504 let (fg_color, bg_color) = if *delay < slow {
505 (Color::WHITE, app.cs.slow_intersection)
506 } else if *delay < slower {
507 (Color::BLACK, app.cs.slower_intersection)
508 } else {
509 (Color::WHITE, app.cs.slowest_intersection)
510 };
511 details.draw_extra.unzoomed.append(
512 Text::from(Line(format!("{}", delay)).fg(fg_color))
513 .bg(bg_color)
514 .render(ctx)
515 .centered_on(i.polygon.center()),
516 );
517 details.draw_extra.zoomed.append(
518 Text::from(Line(format!("{}", delay)).fg(fg_color))
519 .bg(bg_color)
520 .render(ctx)
521 .scale(0.4)
522 .centered_on(i.polygon.center()),
523 );
524 details.tooltips.push((
525 i.polygon.clone(),
526 Text::from(Line(format!("{} delay here", delay))),
527 (id, *time - *delay - Duration::seconds(5.0)),
529 ));
530 }
531 Problem::ComplexIntersectionCrossing(i) => {
532 let i = map.get_i(*i);
533 details.draw_extra.unzoomed.append(
534 GeomBatch::load_svg(ctx, "system/assets/tools/alert.svg")
535 .centered_on(i.polygon.center())
536 .color(RewriteColor::ChangeAlpha(0.8)),
537 );
538 details.draw_extra.zoomed.append(
539 GeomBatch::load_svg(ctx, "system/assets/tools/alert.svg")
540 .scale(0.5)
541 .color(RewriteColor::ChangeAlpha(0.5))
542 .centered_on(i.polygon.center()),
543 );
544 details.tooltips.push((
545 i.polygon.clone(),
546 Text::from_multiline(vec![
547 Line(format!("This intersection has {} legs", i.roads.len())),
548 Line("This has an increased risk of crash or injury for cyclists"),
549 Line("Source: 2020 Seattle DOT Safety Analysis"),
550 ]),
551 (id, *time),
552 ));
553 }
554 Problem::OvertakeDesired(on) => {
555 let pt = on.get_polyline(map).middle();
556 details.draw_extra.unzoomed.append(
557 GeomBatch::load_svg(ctx, "system/assets/tools/alert.svg")
558 .centered_on(pt)
559 .color(RewriteColor::ChangeAlpha(0.8)),
560 );
561 details.draw_extra.zoomed.append(
562 GeomBatch::load_svg(ctx, "system/assets/tools/alert.svg")
563 .scale(0.5)
564 .color(RewriteColor::ChangeAlpha(0.5))
565 .centered_on(pt),
566 );
567 details.tooltips.push((
568 match on {
569 Traversable::Lane(l) => map.get_parent(*l).get_thick_polygon(),
570 Traversable::Turn(t) => map.get_i(t.parent).polygon.clone(),
571 },
572 Text::from("A vehicle wanted to over-take this cyclist near here."),
573 (id, *time),
574 ));
575 }
576 Problem::ArterialIntersectionCrossing(t) => {
577 let t = map.get_t(*t);
578
579 let geom = t.geom.make_polygons(Distance::meters(10.0));
580 details.draw_extra.unzoomed.append(
581 GeomBatch::load_svg(ctx, "system/assets/tools/alert.svg")
582 .centered_on(geom.center())
583 .color(RewriteColor::ChangeAlpha(0.8)),
584 );
585 details.draw_extra.zoomed.append(
586 GeomBatch::load_svg(ctx, "system/assets/tools/alert.svg")
587 .scale(0.5)
588 .color(RewriteColor::ChangeAlpha(0.5))
589 .centered_on(geom.center()),
590 );
591 details.tooltips.push((
592 geom,
593 Text::from_multiline(vec![
594 Line("Arterial intersections have an increased risk of crash or injury for pedestrians"),
595 Line("Source: 2020 Seattle DOT Safety Analysis"),
596 ]),
597 (id, *time)
598 ));
599 }
600 Problem::PedestrianOvercrowding(on) => {
601 let pt = on.get_polyline(map).middle();
602 details.draw_extra.unzoomed.append(
603 GeomBatch::load_svg(ctx, "system/assets/tools/alert.svg")
604 .centered_on(pt)
605 .color(RewriteColor::ChangeAlpha(0.8)),
606 );
607 details.draw_extra.zoomed.append(
608 GeomBatch::load_svg(ctx, "system/assets/tools/alert.svg")
609 .scale(0.5)
610 .color(RewriteColor::ChangeAlpha(0.5))
611 .centered_on(pt),
612 );
613 details.tooltips.push((
614 match on {
615 Traversable::Lane(l) => map.get_parent(*l).get_thick_polygon(),
616 Traversable::Turn(t) => map.get_i(t.parent).polygon.clone(),
617 },
618 Text::from("Too many pedestrians are crowded together here."),
619 (id, *time),
620 ));
621 }
622 }
623 }
624}
625
626fn make_timeline(
628 ctx: &mut EventCtx,
629 app: &App,
630 trip_id: TripID,
631 phases: &[TripPhase],
632 progress_along_path: Option<f64>,
633) -> Widget {
634 let map = &app.primary.map;
635 let sim = &app.primary.sim;
636
637 let total_width = 0.22 * ctx.canvas.window_width;
638 let trip = sim.trip_info(trip_id);
639 let end_time = phases.last().as_ref().and_then(|p| p.end_time);
640 let total_duration_so_far = end_time.unwrap_or_else(|| sim.time()) - trip.departure;
641
642 let mut batch = GeomBatch::new();
646 let mut tooltips = Vec::new();
648 let mut x1 = 0.0;
650 let rectangle_height = 15.0;
651 let icon_height = 30.0;
652 for (idx, p) in phases.iter().enumerate() {
653 let mut tooltip = vec![
654 p.phase_type.describe(map),
655 format!(" Started at {}", p.start_time.ampm_tostring()),
656 ];
657 let phase_duration = if let Some(t2) = p.end_time {
658 let d = t2 - p.start_time;
659 tooltip.push(format!(
660 " Ended at {} (duration: {})",
661 t2.ampm_tostring(),
662 d
663 ));
664 d
665 } else {
666 let d = sim.time() - p.start_time;
667 tooltip.push(format!(
668 " Ongoing (duration so far: {})",
669 d.to_string(&app.opts.units)
670 ));
671 d
672 };
673 let percent_duration = if total_duration_so_far == Duration::ZERO {
674 0.0
675 } else {
676 phase_duration / total_duration_so_far
677 };
678 let percent_duration = percent_duration.max(0.01);
680 tooltip.push(format!(
681 " {}% of trip percentage",
682 (100.0 * percent_duration) as usize
683 ));
684 tooltip.push(format!(
685 " Total delayed time {}",
686 sim.trip_blocked_time(trip_id)
687 ));
688
689 let phase_width = total_width * percent_duration;
690 let rectangle =
691 Polygon::rectangle(phase_width, rectangle_height).translate(x1, icon_height);
692
693 tooltips.push((
694 rectangle.clone(),
695 Text::from_multiline(tooltip.into_iter().map(Line).collect()),
696 None,
697 ));
698
699 batch.push(
700 color_for_trip_phase(app, p.phase_type).alpha(0.7),
701 rectangle,
702 );
703 if idx == phases.len() - 1 {
704 if let Some(p) = progress_along_path {
706 batch.append(
707 GeomBatch::load_svg(ctx, "system/assets/timeline/current_pos.svg").centered_on(
708 Pt2D::new(x1 + p * phase_width, icon_height + (rectangle_height / 2.0)),
709 ),
710 );
711 }
712 }
713 batch.append(
714 GeomBatch::load_svg(
715 ctx.prerender,
716 match p.phase_type {
717 TripPhaseType::Driving => "system/assets/timeline/driving.svg",
718 TripPhaseType::Walking => "system/assets/timeline/walking.svg",
719 TripPhaseType::Biking => "system/assets/timeline/biking.svg",
720 TripPhaseType::Parking => "system/assets/timeline/parking.svg",
721 TripPhaseType::WaitingForBus(_, _) => {
722 "system/assets/timeline/waiting_for_bus.svg"
723 }
724 TripPhaseType::RidingBus(_, _, _) => "system/assets/timeline/riding_bus.svg",
725 TripPhaseType::Cancelled | TripPhaseType::Finished => unreachable!(),
726 TripPhaseType::DelayedStart => "system/assets/timeline/delayed_start.svg",
727 },
728 )
729 .centered_on(Pt2D::new(x1 + phase_width / 2.0, icon_height / 2.0)),
730 );
731
732 x1 += phase_width;
733 }
734
735 DrawWithTooltips::new_widget(ctx, batch, tooltips, Box::new(|_| GeomBatch::new()))
736}
737
738fn make_trip_details(
741 ctx: &mut EventCtx,
742 app: &App,
743 trip_id: TripID,
744 open_trip: &mut OpenTrip,
745 details: &mut Details,
746 phases: Vec<TripPhase>,
747 map_for_pathfinding: &Map,
748 progress_along_path: Option<f64>,
749) -> Widget {
750 let sim = &app.primary.sim;
751 let trip = sim.trip_info(trip_id);
752 let end_time = phases.last().as_ref().and_then(|p| p.end_time);
753
754 let start_btn = {
755 let (id, center, name) = endpoint(&trip.start, app);
756 details
757 .warpers
758 .insert(format!("jump to start of {}", trip_id), id);
759
760 details
761 .draw_extra
762 .unzoomed
763 .append(map_gui::tools::start_marker(ctx, center, 2.0));
764 details
765 .draw_extra
766 .zoomed
767 .append(map_gui::tools::start_marker(ctx, center, 0.5));
768
769 ctx.style()
770 .btn_plain
771 .icon("system/assets/timeline/start_pos.svg")
772 .image_color(RewriteColor::NoOp, ControlState::Default)
773 .tooltip(name)
774 .build_widget(ctx, format!("jump to start of {}", trip_id))
775 };
776
777 let goal_btn = {
778 let (id, center, name) = endpoint(&trip.end, app);
779 details
780 .warpers
781 .insert(format!("jump to goal of {}", trip_id), id);
782
783 details
784 .draw_extra
785 .unzoomed
786 .append(map_gui::tools::goal_marker(ctx, center, 2.0));
787 details
788 .draw_extra
789 .zoomed
790 .append(map_gui::tools::goal_marker(ctx, center, 0.5));
791
792 ctx.style()
793 .btn_plain
794 .icon("system/assets/timeline/goal_pos.svg")
795 .image_color(RewriteColor::NoOp, ControlState::Default)
796 .tooltip(name)
797 .build_widget(ctx, format!("jump to goal of {}", trip_id))
798 };
799
800 let timeline = make_timeline(ctx, app, trip_id, &phases, progress_along_path);
801 let mut elevation = Vec::new();
802 let mut path_impossible = false;
803 for (idx, p) in phases.into_iter().enumerate() {
804 let color = color_for_trip_phase(app, p.phase_type).alpha(0.7);
805 if let Some(path) = &p.path {
806 if ((trip.mode == TripMode::Walk || trip.mode == TripMode::Transit)
808 && p.phase_type == TripPhaseType::Walking)
809 || (trip.mode == TripMode::Bike && p.phase_type == TripPhaseType::Biking)
810 {
811 elevation.push(make_elevation(
812 ctx,
813 color,
814 p.phase_type == TripPhaseType::Walking,
815 path,
816 map_for_pathfinding,
817 app.opts.units,
818 ));
819 }
820
821 if idx == open_trip.cached_routes.len() {
823 if let Some(trace) = path.trace(map_for_pathfinding) {
824 open_trip.cached_routes.push(Some((
825 trace.make_polygons(Distance::meters(10.0)),
826 trace.dashed_lines(
827 Distance::meters(0.75),
828 Distance::meters(1.0),
829 Distance::meters(0.4),
830 ),
831 )));
832 } else {
833 open_trip.cached_routes.push(None);
834 }
835 }
836 if let Some((ref unzoomed, ref zoomed)) = open_trip.cached_routes[idx] {
837 details.draw_extra.unzoomed.push(color, unzoomed.clone());
838 details.draw_extra.zoomed.extend(color, zoomed.clone());
839 }
840 } else if p.has_path_req {
841 path_impossible = true;
842 }
843 if idx == open_trip.cached_routes.len() {
845 open_trip.cached_routes.push(None);
846 }
847 }
848
849 let mut col = vec![
850 Widget::custom_row(vec![
852 start_btn.align_bottom(),
853 timeline,
854 goal_btn.align_bottom(),
855 ])
856 .evenly_spaced(),
857 Widget::row(vec![
858 trip.departure.ampm_tostring().text_widget(ctx),
859 if let Some(t) = end_time {
860 t.ampm_tostring().text_widget(ctx).align_right()
861 } else {
862 Widget::nothing()
863 },
864 ]),
865 Widget::row(vec![
866 if details.can_jump_to_time {
867 details.time_warpers.insert(
868 format!("jump to {}", trip.departure),
869 (trip_id, trip.departure),
870 );
871 ctx.style()
872 .btn_plain
873 .icon("system/assets/speed/jump_to_time.svg")
874 .tooltip({
875 let mut txt = Text::from("This will jump to ");
876 txt.append(Line(trip.departure.ampm_tostring()).fg(Color::hex("#F9EC51")));
877 txt.add_line("The simulation will continue, and your score");
878 txt.add_line("will be calculated at this new time.");
879 txt
880 })
881 .build_widget(ctx, format!("jump to {}", trip.departure))
882 } else {
883 Widget::nothing()
884 },
885 if let Some(t) = end_time {
886 if details.can_jump_to_time {
887 details
888 .time_warpers
889 .insert(format!("jump to {}", t), (trip_id, t));
890 ctx.style()
891 .btn_plain
892 .icon("system/assets/speed/jump_to_time.svg")
893 .tooltip({
894 let mut txt = Text::from("This will jump to ");
895 txt.append(Line(t.ampm_tostring()).fg(Color::hex("#F9EC51")));
896 txt.add_line("The simulation will continue, and your score");
897 txt.add_line("will be calculated at this new time.");
898 txt
899 })
900 .build_widget(ctx, format!("jump to {}", t))
901 .align_right()
902 } else {
903 Widget::nothing()
904 }
905 } else {
906 Widget::nothing()
907 },
908 ]),
909 ];
910 if path_impossible {
911 col.push("Map edits have disconnected the path taken before".text_widget(ctx));
912 }
913 col.extend(elevation);
914
915 let analytics = if app.has_prebaked().is_none() || open_trip.show_after {
916 app.primary.sim.get_analytics()
917 } else {
918 app.prebaked()
919 };
920 draw_problems(ctx, app, analytics, details, trip_id, map_for_pathfinding);
921 Widget::col(col)
922}
923
924fn make_elevation(
925 ctx: &EventCtx,
926 color: Color,
927 walking: bool,
928 path: &Path,
929 map: &Map,
930 unit_fmt: UnitFmt,
931) -> Widget {
932 let mut pts: Vec<(Distance, Distance)> = Vec::new();
933 let mut dist = Distance::ZERO;
934 for step in path.get_steps() {
935 if let PathStep::Turn(t) | PathStep::ContraflowTurn(t) = step {
936 pts.push((dist, map.get_i(t.parent).elevation));
937 }
938 dist += step.as_traversable().get_polyline(map).length();
939 }
940
941 LinePlot::new_widget(
943 ctx,
944 "elevation",
945 vec![Series {
946 label: if walking {
947 "Elevation for walking"
948 } else {
949 "Elevation for biking"
950 }
951 .to_string(),
952 color,
953 pts,
954 }],
955 PlotOptions {
956 max_x: Some(dist.round_up_for_axis()),
957 max_y: Some(map.max_elevation().round_up_for_axis()),
962 ..Default::default()
963 },
964 unit_fmt,
965 )
966}
967
968fn endpoint(endpt: &TripEndpoint, app: &App) -> (ID, Pt2D, String) {
970 match endpt {
971 TripEndpoint::Building(b) => {
972 let bldg = app.primary.map.get_b(*b);
973 (ID::Building(*b), bldg.label_center, bldg.address.clone())
974 }
975 TripEndpoint::Border(i) => {
976 let i = app.primary.map.get_i(*i);
977 (
978 ID::Intersection(i.id),
979 i.polygon.center(),
980 format!(
981 "off map, via {}",
982 i.name(app.opts.language.as_ref(), &app.primary.map)
983 ),
984 )
985 }
986 TripEndpoint::SuddenlyAppear(pos) => (
987 ID::Lane(pos.lane()),
988 pos.pt(&app.primary.map),
989 format!(
990 "suddenly appear {} along",
991 pos.dist_along().to_string(&app.opts.units)
992 ),
993 ),
994 }
995}