game/info/
mod.rs

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    // For drawing the OSD only
51    cached_actions: Vec<Key>,
52}
53
54#[derive(Clone)]
55pub enum Tab {
56    // What trips are open? For finished trips, show the timeline in the current simulation if
57    // true, prebaked if false.
58    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    // The extra bool is for fan chart. TODO Probably scatter plot should own the job of switching
80    // between these?
81    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            // TODO If a parked car becomes in use while the panel is open, should update the
209            // panel better.
210            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        // Avoid an occasionally expensive clone.
235        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
308// TODO Name sucks
309pub struct Details {
310    /// Draw extra things when unzoomed or zoomed.
311    pub draw_extra: ToggleZoomedBuilder,
312    /// Show these tooltips over the map. If the tooltip is clicked, time-warp and open the info
313    /// panel.
314    pub tooltips: Vec<(Polygon, Text, (TripID, Time))>,
315    /// When a button with this label is clicked, open this info panel tab instead.
316    pub hyperlinks: HashMap<String, Tab>,
317    /// When a button with this label is clicked, warp to this ID.
318    pub warpers: HashMap<String, ID>,
319    /// When a button with this label is clicked, time-warp and open the info panel for this trip.
320    pub time_warpers: HashMap<String, (TripID, Time)>,
321    // It's just convenient to plumb this here
322    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        // Highlight something?
423        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            // Different selection styles for different objects.
435            match id {
436                ID::Car(_) | ID::Pedestrian(_) | ID::PedCrowd(_) => {
437                    // Some objects are much wider/taller than others
438                    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                    // Make a circle to cover the object.
451                    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                    // TODO And actually, don't cover up the agent. The Renderable API isn't quite
471                    // conducive to doing this yet.
472                }
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                // TODO Some headings are too wide.. Intersection #xyz (Traffic signals)
493                .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    // (Are we done, optional transition)
505    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        // Let the user click on the map to cancel out this info panel, or click on a tooltip to
512        // time warp.
513        if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
514            // TODO This'll fire left_click elsewhere and conflict; we can't override here
515            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        // Live update?
537        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                    // TODO Most cases use changed_settings, but one doesn't. Detect that
550                    // "sameness" here.
551                    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                    // TODO Messy way of doing this
564                    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                    // This happens when clicking the follow/unfollow button on a trip whose
632                    // agent doesn't exist. Do nothing and just don't crash.
633                    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                // Maybe a non-click action should change the tab. Aka, checkboxes/dropdowns/etc on
642                // a tab.
643                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
677// Internal helper method for InfoPanel::event
678fn 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    // When executed, this assumes the SandboxMode is the top of the stack. It'll
686    // reopen the info panel, then launch the jump-to-time UI.
687    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    // We need to first rewind the simulation
706    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                // TODO not quite...
721                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        // TODO Ahh these colors don't show up differently at all.
744        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
764// Like above, but grouped differently...
765fn 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                // We abuse "disabled" to denote "currently selected"
821                .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    // TODO &str?
844    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    // Slightly weird way to plumb in extra info, but...
855    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
986// TODO Maybe color should be optional, and we'll default to rotating through some options in the
987// Series
988fn 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}