1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
2
3pub use trip::OpenTrip;
4
5use crate::ID;
6use geom::{Circle, Distance, Polygon, Time};
7use map_model::{
8 AreaID, BuildingID, IntersectionID, LaneID, ParkingLotID, TransitRouteID, TransitStopID,
9};
10use sim::{
11 AgentID, AgentType, Analytics, CarID, ParkingSpot, PedestrianID, PersonID, PersonState,
12 ProblemType, TripID, VehicleType,
13};
14use widgetry::mapspace::{ToggleZoomed, ToggleZoomedBuilder};
15use widgetry::tools::open_browser;
16use widgetry::{
17 Color, EventCtx, GfxCtx, Key, Line, LinePlot, Outcome, Panel, PlotOptions, Series, Text,
18 TextExt, Toggle, Widget,
19};
20
21use crate::app::{App, Transition};
22use crate::common::{color_for_agent_type, Warping};
23use crate::debug::path_counter::PathCounter;
24use crate::edit::{EditMode, RouteEditor};
25use crate::layer::PANEL_PLACEMENT;
26use crate::sandbox::{dashboards, GameplayMode, SandboxMode, TimeWarpScreen};
27
28mod building;
29mod debug;
30mod intersection;
31mod lane;
32mod parking_lot;
33mod person;
34mod transit;
35mod trip;
36
37pub struct InfoPanel {
38 tab: Tab,
39 time: Time,
40 is_paused: bool,
41 panel: Panel,
42
43 draw_extra: ToggleZoomed,
44 tooltips: Vec<(Polygon, Text, (TripID, Time))>,
45
46 hyperlinks: HashMap<String, Tab>,
47 warpers: HashMap<String, ID>,
48 time_warpers: HashMap<String, (TripID, Time)>,
49
50 cached_actions: Vec<Key>,
52}
53
54#[derive(Clone)]
55pub enum Tab {
56 PersonTrips(PersonID, BTreeMap<TripID, OpenTrip>),
59 PersonBio(PersonID),
60 PersonSchedule(PersonID),
61
62 TransitVehicleStatus(CarID),
63 TransitStop(TransitStopID),
64 TransitRoute(TransitRouteID),
65
66 ParkedCar(CarID),
67
68 BldgInfo(BuildingID),
69 BldgPeople(BuildingID),
70
71 ParkingLot(ParkingLotID),
72
73 Crowd(Vec<PedestrianID>),
74
75 Area(AreaID),
76
77 IntersectionInfo(IntersectionID),
78 IntersectionTraffic(IntersectionID, DataOptions),
79 IntersectionDelay(IntersectionID, DataOptions, bool),
82 IntersectionDemand(IntersectionID),
83 IntersectionArrivals(IntersectionID, DataOptions),
84 IntersectionTrafficSignal(IntersectionID),
85 IntersectionProblems(IntersectionID, ProblemOptions),
86
87 LaneInfo(LaneID),
88 LaneDebug(LaneID),
89 LaneTraffic(LaneID, DataOptions),
90 LaneProblems(LaneID, ProblemOptions),
91}
92
93impl Tab {
94 pub fn from_id(app: &App, id: ID) -> Tab {
95 match id {
96 ID::Road(_) => unreachable!(),
97 ID::Lane(l) => match app.session.info_panel_tab["lane"] {
98 "info" => Tab::LaneInfo(l),
99 "debug" => Tab::LaneDebug(l),
100 "traffic" => Tab::LaneTraffic(l, DataOptions::new()),
101 "problems" => Tab::LaneProblems(l, ProblemOptions::new()),
102 _ => unreachable!(),
103 },
104 ID::Intersection(i) => match app.session.info_panel_tab["intersection"] {
105 "info" => Tab::IntersectionInfo(i),
106 "traffic" => Tab::IntersectionTraffic(i, DataOptions::new()),
107 "delay" => {
108 if app.primary.map.get_i(i).is_traffic_signal() {
109 Tab::IntersectionDelay(i, DataOptions::new(), false)
110 } else {
111 Tab::IntersectionInfo(i)
112 }
113 }
114 "demand" => {
115 if app.primary.map.get_i(i).is_traffic_signal() {
116 Tab::IntersectionDemand(i)
117 } else {
118 Tab::IntersectionInfo(i)
119 }
120 }
121 "arrivals" => {
122 if app.primary.map.get_i(i).is_incoming_border() {
123 Tab::IntersectionArrivals(i, DataOptions::new())
124 } else {
125 Tab::IntersectionInfo(i)
126 }
127 }
128 "traffic signal" => {
129 if app.primary.map.get_i(i).is_traffic_signal() {
130 Tab::IntersectionTrafficSignal(i)
131 } else {
132 Tab::IntersectionInfo(i)
133 }
134 }
135 "problems" => Tab::IntersectionProblems(i, ProblemOptions::new()),
136 _ => unreachable!(),
137 },
138 ID::Building(b) => match app.session.info_panel_tab["bldg"] {
139 "info" => Tab::BldgInfo(b),
140 "people" => Tab::BldgPeople(b),
141 _ => unreachable!(),
142 },
143 ID::ParkingLot(b) => Tab::ParkingLot(b),
144 ID::Car(c) => {
145 if let Some(p) = app.primary.sim.agent_to_person(AgentID::Car(c)) {
146 match app.session.info_panel_tab["person"] {
147 "trips" => Tab::PersonTrips(
148 p,
149 OpenTrip::single(
150 app.primary.sim.agent_to_trip(AgentID::Car(c)).unwrap(),
151 ),
152 ),
153 "bio" => Tab::PersonBio(p),
154 "schedule" => Tab::PersonSchedule(p),
155 _ => unreachable!(),
156 }
157 } else if c.vehicle_type == VehicleType::Bus || c.vehicle_type == VehicleType::Train
158 {
159 match app.session.info_panel_tab["bus"] {
160 "status" => Tab::TransitVehicleStatus(c),
161 _ => unreachable!(),
162 }
163 } else {
164 Tab::ParkedCar(c)
165 }
166 }
167 ID::Pedestrian(p) => {
168 let person = app
169 .primary
170 .sim
171 .agent_to_person(AgentID::Pedestrian(p))
172 .unwrap();
173 match app.session.info_panel_tab["person"] {
174 "trips" => Tab::PersonTrips(
175 person,
176 OpenTrip::single(
177 app.primary
178 .sim
179 .agent_to_trip(AgentID::Pedestrian(p))
180 .unwrap(),
181 ),
182 ),
183 "bio" => Tab::PersonBio(person),
184 "schedule" => Tab::PersonSchedule(person),
185 _ => unreachable!(),
186 }
187 }
188 ID::PedCrowd(members) => Tab::Crowd(members),
189 ID::TransitStop(bs) => Tab::TransitStop(bs),
190 ID::Area(a) => Tab::Area(a),
191 }
192 }
193
194 fn to_id(&self, app: &App) -> Option<ID> {
195 match self {
196 Tab::PersonTrips(p, _) | Tab::PersonBio(p) | Tab::PersonSchedule(p) => {
197 match app.primary.sim.get_person(*p).state {
198 PersonState::Inside(b) => Some(ID::Building(b)),
199 PersonState::Trip(t) => {
200 app.primary.sim.trip_to_agent(t).ok().map(ID::from_agent)
201 }
202 _ => None,
203 }
204 }
205 Tab::TransitVehicleStatus(c) => Some(ID::Car(*c)),
206 Tab::TransitStop(bs) => Some(ID::TransitStop(*bs)),
207 Tab::TransitRoute(_) => None,
208 Tab::ParkedCar(c) => match app.primary.sim.lookup_parked_car(*c)?.spot {
211 ParkingSpot::Onstreet(_, _) => Some(ID::Car(*c)),
212 ParkingSpot::Offstreet(b, _) => Some(ID::Building(b)),
213 ParkingSpot::Lot(_, _) => Some(ID::Car(*c)),
214 },
215 Tab::BldgInfo(b) | Tab::BldgPeople(b) => Some(ID::Building(*b)),
216 Tab::ParkingLot(pl) => Some(ID::ParkingLot(*pl)),
217 Tab::Crowd(members) => Some(ID::PedCrowd(members.clone())),
218 Tab::Area(a) => Some(ID::Area(*a)),
219 Tab::IntersectionInfo(i)
220 | Tab::IntersectionTraffic(i, _)
221 | Tab::IntersectionDelay(i, _, _)
222 | Tab::IntersectionDemand(i)
223 | Tab::IntersectionArrivals(i, _)
224 | Tab::IntersectionTrafficSignal(i)
225 | Tab::IntersectionProblems(i, _) => Some(ID::Intersection(*i)),
226 Tab::LaneInfo(l)
227 | Tab::LaneDebug(l)
228 | Tab::LaneTraffic(l, _)
229 | Tab::LaneProblems(l, _) => Some(ID::Lane(*l)),
230 }
231 }
232
233 fn changed_settings(&self, c: &Panel) -> Option<Tab> {
234 match self {
236 Tab::IntersectionTraffic(_, _)
237 | Tab::IntersectionDelay(_, _, _)
238 | Tab::IntersectionArrivals(_, _)
239 | Tab::IntersectionProblems(_, _)
240 | Tab::LaneTraffic(_, _) => {}
241 Tab::LaneProblems(_, _) => {}
242 _ => {
243 return None;
244 }
245 }
246
247 let mut new_tab = self.clone();
248 match new_tab {
249 Tab::IntersectionTraffic(_, ref mut opts)
250 | Tab::IntersectionArrivals(_, ref mut opts)
251 | Tab::LaneTraffic(_, ref mut opts) => {
252 let new_opts = DataOptions::from_controls(c);
253 if *opts == new_opts {
254 return None;
255 }
256 *opts = new_opts;
257 }
258 Tab::IntersectionDelay(_, ref mut opts, ref mut fan_chart) => {
259 let new_opts = DataOptions::from_controls(c);
260 let new_fan_chart = c.is_checked("fan chart / scatter plot");
261 if *opts == new_opts && *fan_chart == new_fan_chart {
262 return None;
263 }
264 *opts = new_opts;
265 *fan_chart = new_fan_chart;
266 }
267 Tab::IntersectionProblems(_, ref mut opts) | Tab::LaneProblems(_, ref mut opts) => {
268 let new_opts = ProblemOptions::from_controls(c);
269 if *opts == new_opts {
270 return None;
271 }
272 *opts = new_opts;
273 }
274 _ => unreachable!(),
275 }
276 Some(new_tab)
277 }
278
279 fn variant(&self) -> (&'static str, &'static str) {
280 match self {
281 Tab::PersonTrips(_, _) => ("person", "trips"),
282 Tab::PersonBio(_) => ("person", "bio"),
283 Tab::PersonSchedule(_) => ("person", "schedule"),
284 Tab::TransitVehicleStatus(_) => ("bus", "status"),
285 Tab::TransitStop(_) => ("bus stop", "info"),
286 Tab::TransitRoute(_) => ("bus route", "info"),
287 Tab::ParkedCar(_) => ("parked car", "info"),
288 Tab::BldgInfo(_) => ("bldg", "info"),
289 Tab::BldgPeople(_) => ("bldg", "people"),
290 Tab::ParkingLot(_) => ("parking lot", "info"),
291 Tab::Crowd(_) => ("crowd", "info"),
292 Tab::Area(_) => ("area", "info"),
293 Tab::IntersectionInfo(_) => ("intersection", "info"),
294 Tab::IntersectionTraffic(_, _) => ("intersection", "traffic"),
295 Tab::IntersectionDelay(_, _, _) => ("intersection", "delay"),
296 Tab::IntersectionDemand(_) => ("intersection", "demand"),
297 Tab::IntersectionArrivals(_, _) => ("intersection", "arrivals"),
298 Tab::IntersectionTrafficSignal(_) => ("intersection", "traffic signal"),
299 Tab::IntersectionProblems(_, _) => ("intersection", "problems"),
300 Tab::LaneInfo(_) => ("lane", "info"),
301 Tab::LaneDebug(_) => ("lane", "debug"),
302 Tab::LaneTraffic(_, _) => ("lane", "traffic"),
303 Tab::LaneProblems(_, _) => ("lane", "problems"),
304 }
305 }
306}
307
308pub struct Details {
310 pub draw_extra: ToggleZoomedBuilder,
312 pub tooltips: Vec<(Polygon, Text, (TripID, Time))>,
315 pub hyperlinks: HashMap<String, Tab>,
317 pub warpers: HashMap<String, ID>,
319 pub time_warpers: HashMap<String, (TripID, Time)>,
321 pub can_jump_to_time: bool,
323}
324
325impl InfoPanel {
326 pub fn new(
327 ctx: &mut EventCtx,
328 app: &mut App,
329 mut tab: Tab,
330 ctx_actions: &mut dyn ContextualActions,
331 ) -> InfoPanel {
332 let (k, v) = tab.variant();
333 app.session.info_panel_tab.insert(k, v);
334
335 let mut details = Details {
336 draw_extra: ToggleZoomed::builder(),
337 tooltips: Vec::new(),
338 hyperlinks: HashMap::new(),
339 warpers: HashMap::new(),
340 time_warpers: HashMap::new(),
341 can_jump_to_time: ctx_actions.gameplay_mode().can_jump_to_time(),
342 };
343
344 let (header_and_tabs, main_tab) = match tab {
345 Tab::PersonTrips(p, ref mut open) => (
346 person::trips(ctx, app, &mut details, p, open, ctx_actions.is_paused()),
347 true,
348 ),
349 Tab::PersonBio(p) => (
350 person::bio(ctx, app, &mut details, p, ctx_actions.is_paused()),
351 false,
352 ),
353 Tab::PersonSchedule(p) => (
354 person::schedule(ctx, app, &mut details, p, ctx_actions.is_paused()),
355 false,
356 ),
357 Tab::TransitVehicleStatus(c) => (transit::bus_status(ctx, app, &mut details, c), true),
358 Tab::TransitStop(bs) => (transit::stop(ctx, app, &mut details, bs), true),
359 Tab::TransitRoute(br) => (transit::route(ctx, app, &mut details, br), true),
360 Tab::ParkedCar(c) => (
361 person::parked_car(ctx, app, &mut details, c, ctx_actions.is_paused()),
362 true,
363 ),
364 Tab::BldgInfo(b) => (building::info(ctx, app, &mut details, b), true),
365 Tab::BldgPeople(b) => (building::people(ctx, app, &mut details, b), false),
366 Tab::ParkingLot(pl) => (parking_lot::info(ctx, app, &mut details, pl), true),
367 Tab::Crowd(ref members) => (person::crowd(ctx, app, &mut details, members), true),
368 Tab::Area(a) => (debug::area(ctx, app, &mut details, a), true),
369 Tab::IntersectionInfo(i) => (intersection::info(ctx, app, &mut details, i), true),
370 Tab::IntersectionTraffic(i, ref opts) => (
371 intersection::traffic(ctx, app, &mut details, i, opts),
372 false,
373 ),
374 Tab::IntersectionDelay(i, ref opts, fan_chart) => (
375 intersection::delay(ctx, app, &mut details, i, opts, fan_chart),
376 false,
377 ),
378 Tab::IntersectionDemand(i) => (
379 intersection::current_demand(ctx, app, &mut details, i),
380 false,
381 ),
382 Tab::IntersectionArrivals(i, ref opts) => (
383 intersection::arrivals(ctx, app, &mut details, i, opts),
384 false,
385 ),
386 Tab::IntersectionTrafficSignal(i) => (
387 intersection::traffic_signal(ctx, app, &mut details, i),
388 false,
389 ),
390 Tab::IntersectionProblems(i, ref opts) => (
391 intersection::problems(ctx, app, &mut details, i, opts),
392 false,
393 ),
394 Tab::LaneInfo(l) => (lane::info(ctx, app, &mut details, l), true),
395 Tab::LaneDebug(l) => (lane::debug(ctx, app, &mut details, l), false),
396 Tab::LaneTraffic(l, ref opts) => {
397 (lane::traffic(ctx, app, &mut details, l, opts), false)
398 }
399 Tab::LaneProblems(l, ref opts) => {
400 (lane::problems(ctx, app, &mut details, l, opts), false)
401 }
402 };
403
404 let mut col = vec![header_and_tabs];
405 let maybe_id = tab.to_id(app);
406 let mut cached_actions = Vec::new();
407 if main_tab {
408 if let Some(id) = maybe_id.clone() {
409 for (key, label) in ctx_actions.actions(app, id) {
410 cached_actions.push(key);
411 let button = ctx
412 .style()
413 .btn_outline
414 .text(&label)
415 .hotkey(key)
416 .build_widget(ctx, label);
417 col.push(button);
418 }
419 }
420 }
421
422 if let Some((id, outline)) = maybe_id.and_then(|id| {
424 app.primary
425 .get_obj_outline(
426 ctx,
427 id.clone(),
428 &app.cs,
429 &app.primary.map,
430 &mut app.primary.agents.borrow_mut(),
431 )
432 .map(|outline| (id, outline))
433 }) {
434 match id {
436 ID::Car(_) | ID::Pedestrian(_) | ID::PedCrowd(_) => {
437 let multiplier = match id {
439 ID::Car(c) => {
440 if c.vehicle_type == VehicleType::Bike {
441 3.0
442 } else {
443 0.75
444 }
445 }
446 ID::Pedestrian(_) => 3.0,
447 ID::PedCrowd(_) => 0.75,
448 _ => unreachable!(),
449 };
450 let bounds = outline.get_bounds();
452 let radius = multiplier * Distance::meters(bounds.width().max(bounds.height()));
453 details.draw_extra.unzoomed.push(
454 app.cs.current_object.alpha(0.5),
455 Circle::new(bounds.center(), radius).to_polygon(),
456 );
457 match Circle::new(bounds.center(), radius).to_outline(Distance::meters(0.3)) {
458 Ok(poly) => {
459 details
460 .draw_extra
461 .unzoomed
462 .push(app.cs.current_object, poly.clone());
463 details.draw_extra.zoomed.push(app.cs.current_object, poly);
464 }
465 Err(err) => {
466 warn!("No outline for {:?}: {}", id, err);
467 }
468 }
469
470 }
473 _ => {
474 details
475 .draw_extra
476 .unzoomed
477 .push(app.cs.perma_selected_object, outline.clone());
478 details
479 .draw_extra
480 .zoomed
481 .push(app.cs.perma_selected_object, outline);
482 }
483 }
484 }
485
486 InfoPanel {
487 tab,
488 time: app.primary.sim.time(),
489 is_paused: ctx_actions.is_paused(),
490 panel: Panel::new_builder(Widget::col(col).bg(app.cs.panel_bg).padding(16))
491 .aligned_pair(PANEL_PLACEMENT)
492 .exact_size_percent(30, 60)
494 .build_custom(ctx),
495 draw_extra: details.draw_extra.build(ctx),
496 tooltips: details.tooltips,
497 hyperlinks: details.hyperlinks,
498 warpers: details.warpers,
499 time_warpers: details.time_warpers,
500 cached_actions,
501 }
502 }
503
504 pub fn event(
506 &mut self,
507 ctx: &mut EventCtx,
508 app: &mut App,
509 ctx_actions: &mut dyn ContextualActions,
510 ) -> (bool, Option<Transition>) {
511 if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
514 if app.primary.current_selection.is_none() {
516 let mut found_tooltip = false;
517 if let Some((_, _, (trip, time))) = self
518 .tooltips
519 .iter()
520 .find(|(poly, _, _)| poly.contains_pt(pt))
521 {
522 found_tooltip = true;
523 if app
524 .per_obj
525 .left_click(ctx, &format!("warp here at {}", time))
526 {
527 return do_time_warp(ctx_actions, app, *trip, *time);
528 }
529 }
530 if !found_tooltip && app.per_obj.left_click(ctx, "stop showing info") {
531 return (true, None);
532 }
533 }
534 }
535
536 if app.primary.sim.time() != self.time || ctx_actions.is_paused() != self.is_paused {
538 let mut new = InfoPanel::new(ctx, app, self.tab.clone(), ctx_actions);
539 new.panel.restore(ctx, &self.panel);
540 *self = new;
541 return (false, None);
542 }
543
544 let maybe_id = self.tab.to_id(app);
545 match self.panel.event(ctx) {
546 Outcome::Clicked(action) => {
547 if let Some(new_tab) = self.hyperlinks.get(&action).cloned() {
548 let mut new = InfoPanel::new(ctx, app, new_tab, ctx_actions);
549 if let (Tab::PersonTrips(p1, _), Tab::PersonTrips(p2, _)) =
552 (&self.tab, &new.tab)
553 {
554 if p1 == p2 {
555 new.panel.restore(ctx, &self.panel);
556 }
557 }
558 *self = new;
559 (false, None)
560 } else if action == "close" {
561 (true, None)
562 } else if action == "jump to object" {
563 if let Some(id) = self.tab.to_id(app) {
565 (
566 false,
567 Some(Transition::Push(Warping::new_state(
568 ctx,
569 app.primary.canonical_point(id.clone()).unwrap(),
570 Some(10.0),
571 Some(id),
572 &mut app.primary,
573 ))),
574 )
575 } else {
576 (false, None)
577 }
578 } else if let Some(id) = self.warpers.get(&action) {
579 (
580 false,
581 Some(Transition::Push(Warping::new_state(
582 ctx,
583 app.primary.canonical_point(id.clone()).unwrap(),
584 Some(10.0),
585 None,
586 &mut app.primary,
587 ))),
588 )
589 } else if let Some((trip, time)) = self.time_warpers.get(&action) {
590 do_time_warp(ctx_actions, app, *trip, *time)
591 } else if let Some(url) = action.strip_prefix("open ") {
592 open_browser(url);
593 (false, None)
594 } else if let Some(x) = action.strip_prefix("edit TransitRoute #") {
595 (
596 false,
597 Some(Transition::Multi(vec![
598 Transition::Push(EditMode::new_state(
599 ctx,
600 app,
601 ctx_actions.gameplay_mode(),
602 )),
603 Transition::Push(RouteEditor::new_state(
604 ctx,
605 app,
606 TransitRouteID(x.parse::<usize>().unwrap()),
607 )),
608 ])),
609 )
610 } else if action == "Explore demand across all traffic signals" {
611 (
612 false,
613 Some(Transition::Push(
614 dashboards::TrafficSignalDemand::new_state(ctx, app),
615 )),
616 )
617 } else if let Some(x) = action.strip_prefix("routes across Intersection #") {
618 (
619 false,
620 Some(Transition::Push(PathCounter::demand_across_intersection(
621 ctx,
622 app,
623 IntersectionID(x.parse::<usize>().unwrap()),
624 ))),
625 )
626 } else if let Some(id) = maybe_id {
627 let mut close_panel = true;
628 let t = ctx_actions.execute(ctx, app, id, action, &mut close_panel);
629 (close_panel, Some(t))
630 } else {
631 error!(
634 "Can't do {} on this tab, because it doesn't map to an ID",
635 action
636 );
637 (false, None)
638 }
639 }
640 _ => {
641 if let Some(new_tab) = self.tab.changed_settings(&self.panel) {
644 let mut new = InfoPanel::new(ctx, app, new_tab, ctx_actions);
645 new.panel.restore(ctx, &self.panel);
646 *self = new;
647 return (false, None);
648 }
649
650 (false, None)
651 }
652 }
653 }
654
655 pub fn draw(&self, g: &mut GfxCtx, _: &App) {
656 self.panel.draw(g);
657 self.draw_extra.draw(g);
658 if let Some(pt) = g.canvas.get_cursor_in_map_space() {
659 for (poly, txt, _) in &self.tooltips {
660 if poly.contains_pt(pt) {
661 g.draw_mouse_tooltip(txt.clone());
662 break;
663 }
664 }
665 }
666 }
667
668 pub fn active_keys(&self) -> &Vec<Key> {
669 &self.cached_actions
670 }
671
672 pub fn active_id(&self, app: &App) -> Option<ID> {
673 self.tab.to_id(app)
674 }
675}
676
677fn do_time_warp(
679 ctx_actions: &mut dyn ContextualActions,
680 app: &mut App,
681 trip: TripID,
682 time: Time,
683) -> (bool, Option<Transition>) {
684 let person = app.primary.sim.trip_to_person(trip).unwrap();
685 let jump_to_time = Transition::ConsumeState(Box::new(move |state, ctx, app| {
688 let mut sandbox = state.downcast::<SandboxMode>().ok().unwrap();
689
690 let mut actions = sandbox.contextual_actions();
691 sandbox.controls.common.as_mut().unwrap().launch_info_panel(
692 ctx,
693 app,
694 Tab::PersonTrips(person, OpenTrip::single(trip)),
695 &mut actions,
696 );
697
698 vec![sandbox, TimeWarpScreen::new_state(ctx, app, time, None)]
699 }));
700
701 if time >= app.primary.sim.time() {
702 return (false, Some(jump_to_time));
703 }
704
705 let rewind_sim = Transition::Replace(SandboxMode::async_new(
707 app,
708 ctx_actions.gameplay_mode(),
709 Box::new(move |_, _| vec![jump_to_time]),
710 ));
711
712 (false, Some(rewind_sim))
713}
714
715fn make_table<I: Into<String>>(ctx: &EventCtx, rows: Vec<(I, String)>) -> Vec<Widget> {
716 rows.into_iter()
717 .map(|(k, v)| {
718 Widget::row(vec![
719 Line(k).secondary().into_widget(ctx),
720 v.text_widget(ctx).centered_vert().align_right(),
722 ])
723 })
724 .collect()
725}
726
727fn throughput<F: Fn(&Analytics) -> Vec<(AgentType, Vec<(Time, usize)>)>>(
728 ctx: &EventCtx,
729 app: &App,
730 title: &str,
731 get_data: F,
732 opts: &DataOptions,
733) -> Widget {
734 let mut series = get_data(app.primary.sim.get_analytics())
735 .into_iter()
736 .map(|(agent_type, pts)| Series {
737 label: agent_type.noun().to_string(),
738 color: color_for_agent_type(app, agent_type),
739 pts,
740 })
741 .collect::<Vec<_>>();
742 if opts.show_before {
743 for (agent_type, pts) in get_data(app.prebaked()) {
745 series.push(Series {
746 label: agent_type.noun().to_string(),
747 color: color_for_agent_type(app, agent_type).alpha(0.3),
748 pts,
749 });
750 }
751 }
752
753 let mut plot_opts = PlotOptions::filterable();
754 plot_opts.disabled = opts.disabled_series();
755 Widget::col(vec![
756 Line(title).small_heading().into_widget(ctx),
757 LinePlot::new_widget(ctx, title, series, plot_opts, app.opts.units),
758 ])
759 .padding(10)
760 .bg(app.cs.inner_panel_bg)
761 .outline(ctx.style().section_outline)
762}
763
764fn problem_count<F: Fn(&Analytics) -> Vec<(ProblemType, Vec<(Time, usize)>)>>(
766 ctx: &EventCtx,
767 app: &App,
768 title: &str,
769 get_data: F,
770 opts: &ProblemOptions,
771) -> Widget {
772 let mut series = get_data(app.primary.sim.get_analytics())
773 .into_iter()
774 .map(|(problem_type, pts)| Series {
775 label: problem_type.name().to_string(),
776 color: color_for_problem_type(app, problem_type),
777 pts,
778 })
779 .collect::<Vec<_>>();
780 if opts.show_before {
781 for (problem_type, pts) in get_data(app.prebaked()) {
782 series.push(Series {
783 label: problem_type.name().to_string(),
784 color: color_for_problem_type(app, problem_type).alpha(0.3),
785 pts,
786 });
787 }
788 }
789
790 let mut plot_opts = PlotOptions::filterable();
791 plot_opts.disabled = opts.disabled_series();
792 Widget::col(vec![
793 Line(title).small_heading().into_widget(ctx),
794 LinePlot::new_widget(ctx, title, series, plot_opts, app.opts.units),
795 ])
796 .padding(10)
797 .bg(app.cs.inner_panel_bg)
798 .outline(ctx.style().section_outline)
799}
800
801fn make_tabs(
802 ctx: &EventCtx,
803 hyperlinks: &mut HashMap<String, Tab>,
804 current_tab: Tab,
805 tabs: Vec<(&str, Tab)>,
806) -> Widget {
807 use widgetry::DEFAULT_CORNER_RADIUS;
808 let mut row = Vec::new();
809 for (name, link) in tabs {
810 row.push(
811 ctx.style()
812 .btn_tab
813 .text(name)
814 .corner_rounding(geom::CornerRadii {
815 top_left: DEFAULT_CORNER_RADIUS,
816 top_right: DEFAULT_CORNER_RADIUS,
817 bottom_left: 0.0,
818 bottom_right: 0.0,
819 })
820 .disabled(current_tab.variant() == link.variant())
822 .build_def(ctx),
823 );
824 hyperlinks.insert(name.to_string(), link);
825 }
826
827 Widget::row(row).margin_above(16)
828}
829
830fn header_btns(ctx: &EventCtx) -> Widget {
831 Widget::row(vec![
832 ctx.style()
833 .btn_plain
834 .icon("system/assets/tools/location.svg")
835 .hotkey(Key::J)
836 .build_widget(ctx, "jump to object"),
837 ctx.style().btn_close_widget(ctx),
838 ])
839 .align_right()
840}
841
842pub trait ContextualActions {
843 fn actions(&self, app: &App, id: ID) -> Vec<(Key, String)>;
845 fn execute(
846 &mut self,
847 ctx: &mut EventCtx,
848 app: &mut App,
849 id: ID,
850 action: String,
851 close_panel: &mut bool,
852 ) -> Transition;
853
854 fn is_paused(&self) -> bool;
856 fn gameplay_mode(&self) -> GameplayMode;
857}
858
859#[derive(Clone, PartialEq)]
860pub struct DataOptions {
861 pub show_before: bool,
862 pub show_end_of_day: bool,
863 disabled_types: BTreeSet<AgentType>,
864}
865
866impl DataOptions {
867 pub fn new() -> DataOptions {
868 DataOptions {
869 show_before: false,
870 show_end_of_day: false,
871 disabled_types: BTreeSet::new(),
872 }
873 }
874
875 pub fn to_controls(&self, ctx: &mut EventCtx, app: &App) -> Widget {
876 if app.has_prebaked().is_none() {
877 return Widget::nothing();
878 }
879 Widget::row(vec![
880 Toggle::custom_checkbox(
881 ctx,
882 "Show before changes",
883 vec![
884 Line("Show before "),
885 Line(&app.primary.map.get_edits().edits_name).underlined(),
886 ],
887 None,
888 self.show_before,
889 ),
890 if self.show_before {
891 Toggle::switch(ctx, "Show full day", None, self.show_end_of_day)
892 } else {
893 Widget::nothing()
894 },
895 ])
896 .evenly_spaced()
897 }
898
899 pub fn from_controls(c: &Panel) -> DataOptions {
900 let show_before = c.maybe_is_checked("Show before changes").unwrap_or(false);
901 let mut disabled_types = BTreeSet::new();
902 for a in AgentType::all() {
903 let label = a.noun();
904 if !c.maybe_is_checked(label).unwrap_or(true) {
905 disabled_types.insert(a);
906 }
907 }
908 DataOptions {
909 show_before,
910 show_end_of_day: show_before && c.maybe_is_checked("Show full day").unwrap_or(false),
911 disabled_types,
912 }
913 }
914
915 pub fn disabled_series(&self) -> HashSet<String> {
916 self.disabled_types
917 .iter()
918 .map(|a| a.noun().to_string())
919 .collect()
920 }
921}
922
923#[derive(Clone, PartialEq)]
924pub struct ProblemOptions {
925 pub show_before: bool,
926 pub show_end_of_day: bool,
927 disabled_types: HashSet<ProblemType>,
928}
929
930impl ProblemOptions {
931 pub fn new() -> Self {
932 Self {
933 show_before: false,
934 show_end_of_day: false,
935 disabled_types: HashSet::new(),
936 }
937 }
938
939 pub fn to_controls(&self, ctx: &mut EventCtx, app: &App) -> Widget {
940 if app.has_prebaked().is_none() {
941 return Widget::nothing();
942 }
943 Widget::row(vec![
944 Toggle::custom_checkbox(
945 ctx,
946 "Show before changes",
947 vec![
948 Line("Show before "),
949 Line(&app.primary.map.get_edits().edits_name).underlined(),
950 ],
951 None,
952 self.show_before,
953 ),
954 if self.show_before {
955 Toggle::switch(ctx, "Show full day", None, self.show_end_of_day)
956 } else {
957 Widget::nothing()
958 },
959 ])
960 .evenly_spaced()
961 }
962
963 pub fn from_controls(c: &Panel) -> ProblemOptions {
964 let show_before = c.maybe_is_checked("Show before changes").unwrap_or(false);
965 let mut disabled_types = HashSet::new();
966 for pt in ProblemType::all() {
967 if !c.maybe_is_checked(pt.name()).unwrap_or(true) {
968 disabled_types.insert(pt);
969 }
970 }
971 ProblemOptions {
972 show_before,
973 show_end_of_day: show_before && c.maybe_is_checked("Show full day").unwrap_or(false),
974 disabled_types,
975 }
976 }
977
978 pub fn disabled_series(&self) -> HashSet<String> {
979 self.disabled_types
980 .iter()
981 .map(|pt| pt.name().to_string())
982 .collect()
983 }
984}
985
986fn color_for_problem_type(app: &App, problem_type: ProblemType) -> Color {
989 for (idx, pt) in ProblemType::all().into_iter().enumerate() {
990 if problem_type == pt {
991 return app.cs.rotating_color_plot(idx);
992 }
993 }
994 unreachable!()
995}