game/info/
transit.rs

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        // Full names can overlap, so include the ID
32        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            // TODO Button to jump to the bus
49            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    // Draw where the bus/train stops
98    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    // TODO Soon it'll be time to split into tabs
310    {
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    // Draw the route, label stops, and show location of buses
322    {
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
365// TODO Unit test
366fn 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        // Compress the times
376        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        // Handle end
401        txt.add_line(format!(
402            "Every {} from {} to {}",
403            dt.unwrap(),
404            start.ampm_tostring(),
405            last.unwrap().ampm_tostring()
406        ));
407    } else {
408        // Just list the times
409        for t in &route.spawn_times {
410            txt.add_line(t.ampm_tostring());
411        }
412    }
413    txt
414}