1use crate::ID;
2use abstutil::{prettyprint_usize, Counter};
3use geom::{Circle, Distance, Time};
4use map_gui::tools::ColorNetwork;
5use map_model::{PathStep, TransitRoute, TransitRouteID, TransitStopID};
6use sim::{AgentID, CarID};
7use widgetry::{Color, ControlState, EventCtx, Key, Line, RewriteColor, Text, TextExt, Widget};
8
9use crate::app::App;
10use crate::info::{header_btns, make_tabs, Details, Tab};
11
12pub fn stop(ctx: &mut EventCtx, app: &App, details: &mut Details, id: TransitStopID) -> Widget {
13 let header = Widget::row(vec![
14 Line("Bus stop").small_heading().into_widget(ctx),
15 header_btns(ctx),
16 ]);
17
18 Widget::custom_col(vec![header, stop_body(ctx, app, details, id).tab_body(ctx)])
19}
20
21fn stop_body(ctx: &mut EventCtx, app: &App, details: &mut Details, id: TransitStopID) -> Widget {
22 let mut rows = vec![];
23
24 let ts = app.primary.map.get_ts(id);
25 let sim = &app.primary.sim;
26
27 rows.push(Line(&ts.name).into_widget(ctx));
28
29 let all_arrivals = &sim.get_analytics().bus_arrivals;
30 for r in app.primary.map.get_routes_serving_stop(id) {
31 let label = format!("{} ({})", r.long_name, r.id);
33 rows.push(
34 ctx.style()
35 .btn_outline
36 .text(format!("Route {}", r.short_name))
37 .build_widget(ctx, &label),
38 );
39 details.hyperlinks.insert(label, Tab::TransitRoute(r.id));
40
41 let arrivals: Vec<(Time, CarID)> = all_arrivals
42 .iter()
43 .filter(|(_, _, route, stop)| r.id == *route && id == *stop)
44 .map(|(t, car, _, _)| (*t, *car))
45 .collect();
46 let mut txt = Text::new();
47 if let Some((t, _)) = arrivals.last() {
48 txt.add_line(Line(format!(" Last bus arrived {} ago", sim.time() - *t)).secondary());
50 } else {
51 txt.add_line(Line(" No arrivals yet").secondary());
52 }
53 rows.push(txt.into_widget(ctx));
54 }
55
56 let mut boardings: Counter<TransitRouteID> = Counter::new();
57 let mut alightings: Counter<TransitRouteID> = Counter::new();
58 if let Some(list) = app.primary.sim.get_analytics().passengers_boarding.get(&id) {
59 for (_, r, _) in list {
60 boardings.inc(*r);
61 }
62 }
63 if let Some(list) = app
64 .primary
65 .sim
66 .get_analytics()
67 .passengers_alighting
68 .get(&id)
69 {
70 for (_, r) in list {
71 alightings.inc(*r);
72 }
73 }
74 let mut txt = Text::new();
75 txt.add_line("Total");
76 txt.append(
77 Line(format!(
78 ": {} boardings, {} alightings",
79 prettyprint_usize(boardings.sum()),
80 prettyprint_usize(alightings.sum())
81 ))
82 .secondary(),
83 );
84 for r in app.primary.map.get_routes_serving_stop(id) {
85 txt.add_line(format!("Route {}", r.short_name));
86 txt.append(
87 Line(format!(
88 ": {} boardings, {} alightings",
89 prettyprint_usize(boardings.get(r.id)),
90 prettyprint_usize(alightings.get(r.id))
91 ))
92 .secondary(),
93 );
94 }
95 rows.push(txt.into_widget(ctx));
96
97 details.draw_extra.zoomed.push(
99 app.cs.bus_body.alpha(0.5),
100 Circle::new(ts.driving_pos.pt(&app.primary.map), Distance::meters(2.5)).to_polygon(),
101 );
102
103 Widget::col(rows)
104}
105
106pub fn bus_status(ctx: &mut EventCtx, app: &App, details: &mut Details, id: CarID) -> Widget {
107 Widget::custom_col(vec![
108 bus_header(ctx, app, details, id, Tab::TransitVehicleStatus(id)),
109 bus_status_body(ctx, app, details, id).tab_body(ctx),
110 ])
111}
112
113fn bus_status_body(ctx: &mut EventCtx, app: &App, details: &mut Details, id: CarID) -> Widget {
114 let mut rows = vec![];
115
116 let route = app
117 .primary
118 .map
119 .get_tr(app.primary.sim.bus_route_id(id).unwrap());
120
121 rows.push(
122 ctx.style()
123 .btn_outline
124 .text(format!("Serves route {}", route.short_name))
125 .build_def(ctx),
126 );
127 details.hyperlinks.insert(
128 format!("Serves route {}", route.short_name),
129 Tab::TransitRoute(route.id),
130 );
131
132 rows.push(
133 Line(format!(
134 "Currently has {} passengers",
135 app.primary.sim.num_transit_passengers(id),
136 ))
137 .into_widget(ctx),
138 );
139
140 Widget::col(rows)
141}
142
143fn bus_header(ctx: &mut EventCtx, app: &App, details: &mut Details, id: CarID, tab: Tab) -> Widget {
144 let route = app.primary.sim.bus_route_id(id).unwrap();
145
146 if let Some(pt) = app
147 .primary
148 .sim
149 .canonical_pt_for_agent(AgentID::Car(id), &app.primary.map)
150 {
151 ctx.canvas.center_on_map_pt(pt);
152 }
153
154 let mut rows = vec![];
155 rows.push(Widget::row(vec![
156 Line(format!(
157 "{} (route {})",
158 id,
159 app.primary.map.get_tr(route).short_name
160 ))
161 .small_heading()
162 .into_widget(ctx),
163 header_btns(ctx),
164 ]));
165 rows.push(make_tabs(
166 ctx,
167 &mut details.hyperlinks,
168 tab,
169 vec![("Status", Tab::TransitVehicleStatus(id))],
170 ));
171
172 Widget::custom_col(rows)
173}
174
175pub fn route(ctx: &mut EventCtx, app: &App, details: &mut Details, id: TransitRouteID) -> Widget {
176 let header = {
177 let map = &app.primary.map;
178 let route = map.get_tr(id);
179
180 Widget::row(vec![
181 Line(format!("Route {}", route.short_name))
182 .small_heading()
183 .into_widget(ctx),
184 header_btns(ctx),
185 ])
186 };
187
188 Widget::custom_col(vec![
189 header,
190 route_body(ctx, app, details, id).tab_body(ctx),
191 ])
192}
193
194fn route_body(ctx: &mut EventCtx, app: &App, details: &mut Details, id: TransitRouteID) -> Widget {
195 let mut rows = vec![];
196
197 let map = &app.primary.map;
198 let route = map.get_tr(id);
199 rows.push(
200 Text::from(&route.long_name)
201 .wrap_to_pct(ctx, 20)
202 .into_widget(ctx),
203 );
204
205 let buses = app.primary.sim.status_of_buses(id, map);
206 let mut bus_locations = Vec::new();
207 if buses.is_empty() {
208 rows.push(format!("No {} running", route.plural_noun()).text_widget(ctx));
209 } else {
210 for (bus, _, _, pt) in buses {
211 rows.push(ctx.style().btn_outline.text(bus.to_string()).build_def(ctx));
212 details
213 .hyperlinks
214 .insert(bus.to_string(), Tab::TransitVehicleStatus(bus));
215 bus_locations.push(pt);
216 }
217 }
218
219 let mut boardings: Counter<TransitStopID> = Counter::new();
220 let mut alightings: Counter<TransitStopID> = Counter::new();
221 let mut waiting: Counter<TransitStopID> = Counter::new();
222 for ts in &route.stops {
223 if let Some(list) = app.primary.sim.get_analytics().passengers_boarding.get(ts) {
224 for (_, r, _) in list {
225 if *r == id {
226 boardings.inc(*ts);
227 }
228 }
229 }
230 if let Some(list) = app.primary.sim.get_analytics().passengers_alighting.get(ts) {
231 for (_, r) in list {
232 if *r == id {
233 alightings.inc(*ts);
234 }
235 }
236 }
237
238 for (_, r, _, _) in app.primary.sim.get_people_waiting_at_stop(*ts) {
239 if *r == id {
240 waiting.inc(*ts);
241 }
242 }
243 }
244
245 rows.push(
246 Text::from_all(vec![
247 Line("Total"),
248 Line(format!(
249 ": {} boardings, {} alightings, {} currently waiting",
250 prettyprint_usize(boardings.sum()),
251 prettyprint_usize(alightings.sum()),
252 prettyprint_usize(waiting.sum())
253 ))
254 .secondary(),
255 ])
256 .into_widget(ctx),
257 );
258
259 rows.push(format!("{} stops", route.stops.len()).text_widget(ctx));
260 {
261 let i = map.get_i(map.get_l(route.start).src_i);
262 let name = format!("Starts at {}", i.name(app.opts.language.as_ref(), map));
263 rows.push(Widget::row(vec![
264 ctx.style()
265 .btn_plain
266 .icon("system/assets/timeline/start_pos.svg")
267 .image_color(RewriteColor::NoOp, ControlState::Default)
268 .build_widget(ctx, &name),
269 name.clone().text_widget(ctx),
270 ]));
271 details.warpers.insert(name, ID::Intersection(i.id));
272 }
273 for (idx, ts) in route.stops.iter().enumerate() {
274 let ts = map.get_ts(*ts);
275 let name = format!("Stop {}: {}", idx + 1, ts.name);
276 rows.push(Widget::row(vec![
277 ctx.style()
278 .btn_plain
279 .icon("system/assets/tools/pin.svg")
280 .build_widget(ctx, &name),
281 Text::from_all(vec![
282 Line(&ts.name),
283 Line(format!(
284 ": {} boardings, {} alightings, {} currently waiting",
285 prettyprint_usize(boardings.get(ts.id)),
286 prettyprint_usize(alightings.get(ts.id)),
287 prettyprint_usize(waiting.get(ts.id))
288 ))
289 .secondary(),
290 ])
291 .into_widget(ctx),
292 ]));
293 details.warpers.insert(name, ID::TransitStop(ts.id));
294 }
295 if let Some(l) = route.end_border {
296 let i = map.get_i(map.get_l(l).dst_i);
297 let name = format!("Ends at {}", i.name(app.opts.language.as_ref(), map));
298 rows.push(Widget::row(vec![
299 ctx.style()
300 .btn_plain
301 .icon("system/assets/timeline/goal_pos.svg")
302 .image_color(RewriteColor::NoOp, ControlState::Default)
303 .build_widget(ctx, &name),
304 name.clone().text_widget(ctx),
305 ]));
306 details.warpers.insert(name, ID::Intersection(i.id));
307 }
308
309 {
311 rows.push(
312 ctx.style()
313 .btn_outline
314 .text("Edit schedule")
315 .hotkey(Key::E)
316 .build_widget(ctx, format!("edit {}", route.id)),
317 );
318 rows.push(describe_schedule(route).into_widget(ctx));
319 }
320
321 {
323 let mut colorer = ColorNetwork::new(app);
324 for path in route.all_paths(map).unwrap() {
325 for step in path.get_steps() {
326 if let PathStep::Lane(l) = step {
327 colorer.add_l(*l, app.cs.unzoomed_bus);
328 }
329 }
330 }
331 details.draw_extra.append(colorer.draw);
332
333 for pt in bus_locations {
334 details.draw_extra.unzoomed.push(
335 Color::BLUE,
336 Circle::new(pt, Distance::meters(20.0)).to_polygon(),
337 );
338 details.draw_extra.zoomed.push(
339 Color::BLUE.alpha(0.5),
340 Circle::new(pt, Distance::meters(5.0)).to_polygon(),
341 );
342 }
343
344 for (idx, ts) in route.stops.iter().enumerate() {
345 let ts = map.get_ts(*ts);
346 details.draw_extra.unzoomed.append(
347 Text::from(format!("{}) {}", idx + 1, ts.name))
348 .bg(app.cs.bus_layer)
349 .render_autocropped(ctx)
350 .centered_on(ts.sidewalk_pos.pt(map)),
351 );
352 details.draw_extra.zoomed.append(
353 Text::from(format!("{}) {}", idx + 1, ts.name))
354 .bg(app.cs.bus_layer)
355 .render_autocropped(ctx)
356 .scale(0.1)
357 .centered_on(ts.sidewalk_pos.pt(map)),
358 );
359 }
360 }
361
362 Widget::col(rows)
363}
364
365fn describe_schedule(route: &TransitRoute) -> Text {
367 let mut txt = Text::new();
368 txt.add_line(format!(
369 "{} {}s run this route daily",
370 route.spawn_times.len(),
371 route.plural_noun()
372 ));
373
374 if false {
375 let mut start = route.spawn_times[0];
377 let mut last = None;
378 let mut dt = None;
379 for t in route.spawn_times.iter().skip(1) {
380 if let Some(l) = last {
381 let new_dt = *t - l;
382 if Some(new_dt) == dt {
383 last = Some(*t);
384 } else {
385 txt.add_line(format!(
386 "Every {} from {} to {}",
387 dt.unwrap(),
388 start.ampm_tostring(),
389 l.ampm_tostring()
390 ));
391 start = l;
392 last = Some(*t);
393 dt = Some(new_dt);
394 }
395 } else {
396 last = Some(*t);
397 dt = Some(*t - start);
398 }
399 }
400 txt.add_line(format!(
402 "Every {} from {} to {}",
403 dt.unwrap(),
404 start.ampm_tostring(),
405 last.unwrap().ampm_tostring()
406 ));
407 } else {
408 for t in &route.spawn_times {
410 txt.add_line(t.ampm_tostring());
411 }
412 }
413 txt
414}