game/sandbox/dashboards/
misc.rs

1use abstutil::{prettyprint_usize, Counter};
2use geom::Time;
3use map_model::TransitRouteID;
4use widgetry::{
5    Autocomplete, EventCtx, GfxCtx, Image, Line, LinePlot, Outcome, Panel, PlotOptions, Series,
6    State, TextExt, Widget,
7};
8
9use crate::app::{App, Transition};
10use crate::info::Tab;
11use crate::sandbox::dashboards::DashTab;
12use crate::sandbox::SandboxMode;
13
14pub struct ActiveTraffic {
15    panel: Panel,
16}
17
18impl ActiveTraffic {
19    pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
20        // TODO Downsampling in the middle of the day and comparing to the downsampled entire day
21        // doesn't work. For the same simulation, by end of day, the plots will be identical, but
22        // until then, they'll differ. See https://github.com/a-b-street/abstreet/issues/85 for
23        // more details on the "downsampling subset" problem.
24        let mut active_agents = vec![Series {
25            label: format!("After \"{}\"", app.primary.map.get_edits().edits_name),
26            color: app.cs.after_changes,
27            pts: downsample(
28                app.primary
29                    .sim
30                    .get_analytics()
31                    .active_agents(app.primary.sim.time()),
32            ),
33        }];
34        if app.has_prebaked().is_some() {
35            active_agents.push(Series {
36                label: format!("Before \"{}\"", app.primary.map.get_edits().edits_name),
37                color: app.cs.before_changes.alpha(0.5),
38                pts: downsample(
39                    app.prebaked()
40                        .active_agents(app.primary.sim.get_end_of_day()),
41                ),
42            });
43        }
44
45        Box::new(ActiveTraffic {
46            panel: Panel::new_builder(Widget::col(vec![
47                DashTab::ActiveTraffic.picker(ctx, app),
48                LinePlot::new_widget(
49                    ctx,
50                    "active traffic",
51                    active_agents,
52                    PlotOptions::fixed(),
53                    app.opts.units,
54                )
55                .section(ctx),
56            ]))
57            .exact_size_percent(90, 90)
58            .build(ctx),
59        })
60    }
61}
62
63impl State<App> for ActiveTraffic {
64    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
65        match self.panel.event(ctx) {
66            Outcome::Clicked(x) => match x.as_ref() {
67                "close" => Transition::Pop,
68                _ => unreachable!(),
69            },
70            Outcome::Changed(_) => DashTab::ActiveTraffic
71                .transition(ctx, app, &self.panel)
72                .unwrap(),
73            _ => Transition::Keep,
74        }
75    }
76
77    fn draw(&self, g: &mut GfxCtx, _app: &App) {
78        self.panel.draw(g);
79    }
80}
81
82fn downsample(raw: Vec<(Time, usize)>) -> Vec<(Time, usize)> {
83    if raw.is_empty() {
84        return raw;
85    }
86
87    let min_x = Time::START_OF_DAY;
88    let min_y = 0;
89    let max_x = raw.last().unwrap().0;
90    let max_y = raw.iter().max_by_key(|(_, cnt)| *cnt).unwrap().1;
91
92    let mut pts = Vec::new();
93    for (t, cnt) in raw {
94        pts.push(lttb::DataPoint::new(
95            (t - min_x) / (max_x - min_x),
96            ((cnt - min_y) as f64) / ((max_y - min_y) as f64),
97        ));
98    }
99    let mut downsampled = Vec::new();
100    for pt in lttb::lttb(pts, 100) {
101        downsampled.push((
102            max_x.percent_of(pt.x),
103            min_y + (pt.y * (max_y - min_y) as f64) as usize,
104        ));
105    }
106    downsampled
107}
108
109pub struct TransitRoutes {
110    panel: Panel,
111}
112
113impl TransitRoutes {
114    pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
115        // Count totals per route
116        let mut boardings = Counter::new();
117        for list in app.primary.sim.get_analytics().passengers_boarding.values() {
118            for (_, r, _) in list {
119                boardings.inc(*r);
120            }
121        }
122        let mut alightings = Counter::new();
123        for list in app
124            .primary
125            .sim
126            .get_analytics()
127            .passengers_alighting
128            .values()
129        {
130            for (_, r) in list {
131                alightings.inc(*r);
132            }
133        }
134        let mut waiting = Counter::new();
135        for ts in app.primary.map.all_transit_stops().keys() {
136            for (_, r, _, _) in app.primary.sim.get_people_waiting_at_stop(*ts) {
137                waiting.inc(*r);
138            }
139        }
140
141        // Sort descending by count, but ascending by name. Hence the funny negation.
142        let mut routes: Vec<(isize, isize, isize, String, TransitRouteID)> = Vec::new();
143        for r in app.primary.map.all_transit_routes() {
144            routes.push((
145                -(boardings.get(r.id) as isize),
146                -(alightings.get(r.id) as isize),
147                -(waiting.get(r.id) as isize),
148                r.long_name.clone(),
149                r.id,
150            ));
151        }
152        routes.sort();
153
154        let col = vec![
155            DashTab::TransitRoutes.picker(ctx, app),
156            Line(format!("{} Transit routes", routes.len()))
157                .small_heading()
158                .into_widget(ctx),
159            Widget::row(vec![
160                Image::from_path("system/assets/tools/search.svg").into_widget(ctx),
161                Autocomplete::new_widget(
162                    ctx,
163                    routes
164                        .iter()
165                        .map(|(_, _, _, r, id)| (r.clone(), *id))
166                        .collect(),
167                    10,
168                )
169                .named("search"),
170            ])
171            .padding(8),
172            // TODO Maybe a table instead
173            Widget::col(
174                routes
175                    .into_iter()
176                    .map(|(boardings, alightings, waiting, name, id)| {
177                        Widget::row(vec![
178                            ctx.style()
179                                .btn_outline
180                                .text(name)
181                                .build_widget(ctx, id.to_string()),
182                            format!(
183                                "{} boardings, {} alightings, {} currently waiting",
184                                prettyprint_usize(-boardings as usize),
185                                prettyprint_usize(-alightings as usize),
186                                prettyprint_usize(-waiting as usize)
187                            )
188                            .text_widget(ctx),
189                        ])
190                    })
191                    .collect(),
192            ),
193        ];
194
195        Box::new(TransitRoutes {
196            panel: Panel::new_builder(Widget::col(col))
197                .exact_size_percent(90, 90)
198                .build(ctx),
199        })
200    }
201}
202
203impl State<App> for TransitRoutes {
204    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
205        let route = match self.panel.event(ctx) {
206            Outcome::Clicked(x) => {
207                if let Some(x) = x.strip_prefix("TransitRoute #") {
208                    TransitRouteID(x.parse::<usize>().unwrap())
209                } else if x == "close" {
210                    return Transition::Pop;
211                } else {
212                    unreachable!()
213                }
214            }
215            Outcome::Changed(_) => {
216                if let Some(t) = DashTab::TransitRoutes.transition(ctx, app, &self.panel) {
217                    return t;
218                } else {
219                    return Transition::Keep;
220                }
221            }
222            _ => {
223                if let Some(routes) = self.panel.autocomplete_done("search") {
224                    if !routes.is_empty() {
225                        routes[0]
226                    } else {
227                        return Transition::Keep;
228                    }
229                } else {
230                    return Transition::Keep;
231                }
232            }
233        };
234
235        Transition::Multi(vec![
236            Transition::Pop,
237            Transition::ModifyState(Box::new(move |state, ctx, app| {
238                let sandbox = state.downcast_mut::<SandboxMode>().unwrap();
239                let mut actions = sandbox.contextual_actions();
240                sandbox.controls.common.as_mut().unwrap().launch_info_panel(
241                    ctx,
242                    app,
243                    Tab::TransitRoute(route),
244                    &mut actions,
245                )
246            })),
247        ])
248    }
249
250    fn draw(&self, g: &mut GfxCtx, _app: &App) {
251        self.panel.draw(g);
252    }
253}