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