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 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 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 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 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}