1use std::collections::{BTreeMap, BTreeSet};
2
3use abstutil::prettyprint_usize;
4use geom::{ArrowCap, Distance, Duration, PolyLine, Polygon, Tessellation, Time};
5use map_gui::options::TrafficSignalStyle;
6use map_gui::render::traffic_signal::draw_signal_stage;
7use map_model::{IntersectionControl, IntersectionID, StageType};
8use sim::AgentType;
9use widgetry::{
10 Color, DrawWithTooltips, EventCtx, FanChart, GeomBatch, Line, PlotOptions, ScatterPlot, Series,
11 Text, Toggle, Widget,
12};
13
14use crate::app::App;
15use crate::common::color_for_agent_type;
16use crate::info::{
17 header_btns, make_tabs, problem_count, throughput, DataOptions, Details, ProblemOptions, Tab,
18};
19
20pub fn info(ctx: &EventCtx, app: &App, details: &mut Details, id: IntersectionID) -> Widget {
21 Widget::custom_col(vec![
22 header(ctx, app, details, id, Tab::IntersectionInfo(id)),
23 info_body(ctx, app, id).tab_body(ctx),
24 ])
25}
26
27fn info_body(ctx: &EventCtx, app: &App, id: IntersectionID) -> Widget {
28 let mut rows = vec![];
29
30 let i = app.primary.map.get_i(id);
31
32 let mut txt = Text::from("Connecting");
33 let mut road_names = BTreeSet::new();
34 for r in &i.roads {
35 road_names.insert(
36 app.primary
37 .map
38 .get_r(*r)
39 .get_name(app.opts.language.as_ref()),
40 );
41 }
42 for r in road_names {
43 txt.add_line(format!(" {}", r));
44 }
45 rows.push(txt.into_widget(ctx));
46
47 if app.opts.dev {
48 rows.push(
49 ctx.style()
50 .btn_outline
51 .text("Open OSM node")
52 .build_widget(ctx, format!("open {}", i.orig_id)),
53 );
54 }
55
56 Widget::col(rows)
57}
58
59pub fn traffic(
60 ctx: &mut EventCtx,
61 app: &App,
62 details: &mut Details,
63 id: IntersectionID,
64 opts: &DataOptions,
65) -> Widget {
66 Widget::custom_col(vec![
67 header(
68 ctx,
69 app,
70 details,
71 id,
72 Tab::IntersectionTraffic(id, opts.clone()),
73 ),
74 traffic_body(ctx, app, id, opts).tab_body(ctx),
75 ])
76}
77
78fn traffic_body(ctx: &mut EventCtx, app: &App, id: IntersectionID, opts: &DataOptions) -> Widget {
79 let mut rows = vec![];
80 let mut txt = Text::new();
81
82 txt.add_line(format!(
83 "Since midnight: {} commuters and vehicles crossed",
84 prettyprint_usize(
85 app.primary
86 .sim
87 .get_analytics()
88 .intersection_thruput
89 .total_for(id)
90 )
91 ));
92 rows.push(txt.into_widget(ctx));
93
94 rows.push(opts.to_controls(ctx, app));
95
96 let time = if opts.show_end_of_day {
97 app.primary.sim.get_end_of_day()
98 } else {
99 app.primary.sim.time()
100 };
101 rows.push(throughput(
102 ctx,
103 app,
104 "Number of commuters and vehicles per hour",
105 move |a| {
106 if a.intersection_thruput.raw.is_empty() {
107 a.intersection_thruput.count_per_hour(id, time)
108 } else {
109 a.intersection_thruput.raw_throughput(time, id)
110 }
111 },
112 opts,
113 ));
114
115 Widget::col(rows)
116}
117
118pub fn delay(
119 ctx: &mut EventCtx,
120 app: &App,
121 details: &mut Details,
122 id: IntersectionID,
123 opts: &DataOptions,
124 fan_chart: bool,
125) -> Widget {
126 Widget::custom_col(vec![
127 header(
128 ctx,
129 app,
130 details,
131 id,
132 Tab::IntersectionDelay(id, opts.clone(), fan_chart),
133 ),
134 delay_body(ctx, app, id, opts, fan_chart).tab_body(ctx),
135 ])
136}
137
138fn delay_body(
139 ctx: &mut EventCtx,
140 app: &App,
141 id: IntersectionID,
142 opts: &DataOptions,
143 fan_chart: bool,
144) -> Widget {
145 let mut rows = vec![];
146 let i = app.primary.map.get_i(id);
147
148 assert!(i.is_traffic_signal());
149 rows.push(opts.to_controls(ctx, app));
150 rows.push(Toggle::choice(
151 ctx,
152 "fan chart / scatter plot",
153 "fan chart",
154 "scatter plot",
155 None,
156 fan_chart,
157 ));
158
159 rows.push(delay_plot(ctx, app, id, opts, fan_chart));
160
161 Widget::col(rows)
162}
163
164pub fn current_demand(
165 ctx: &mut EventCtx,
166 app: &App,
167 details: &mut Details,
168 id: IntersectionID,
169) -> Widget {
170 Widget::custom_col(vec![
171 header(ctx, app, details, id, Tab::IntersectionDemand(id)),
172 current_demand_body(ctx, app, id).tab_body(ctx),
173 ])
174}
175
176fn current_demand_body(ctx: &mut EventCtx, app: &App, id: IntersectionID) -> Widget {
177 let mut rows = vec![];
178 let mut total_demand = 0;
179 let mut demand_per_movement: Vec<(&PolyLine, usize)> = Vec::new();
180 for m in app.primary.map.get_i(id).movements.values() {
181 let demand = app
182 .primary
183 .sim
184 .get_analytics()
185 .demand
186 .get(&m.id)
187 .cloned()
188 .unwrap_or(0);
189 if demand > 0 {
190 total_demand += demand;
191 demand_per_movement.push((&m.geom, demand));
192 }
193 }
194
195 let mut batch = GeomBatch::new();
196 let mut polygon = Tessellation::from(app.primary.map.get_i(id).polygon.clone());
197 let bounds = polygon.get_bounds();
198 let zoom = (0.25 * ctx.canvas.window_width) / bounds.width();
200 polygon.translate(-bounds.min_x, -bounds.min_y);
201 polygon.scale(zoom);
202 batch.push(app.cs.normal_intersection, polygon);
203
204 let mut tooltips = Vec::new();
205 let mut outlines = Vec::new();
206 for (pl, demand) in demand_per_movement {
207 let percent = (demand as f64) / (total_demand as f64);
208 if let Ok(arrow) = pl
209 .make_arrow(percent * Distance::meters(3.0), ArrowCap::Triangle)
210 .translate(-bounds.min_x, -bounds.min_y)
211 .scale(zoom)
212 {
213 outlines.push(arrow.to_outline(Distance::meters(1.0)));
214 batch.push(Color::hex("#A3A3A3"), arrow.clone());
215 tooltips.push((arrow, Text::from(prettyprint_usize(demand)), None));
216 }
217 }
218 batch.extend(Color::WHITE, outlines);
219
220 let mut txt = Text::from(format!(
221 "Active agent demand at {}",
222 app.primary.sim.time().ampm_tostring()
223 ));
224 txt.add_line(
225 Line(format!(
226 "Includes all {} active agents anywhere on the map",
227 prettyprint_usize(total_demand)
228 ))
229 .secondary(),
230 );
231
232 rows.push(
233 Widget::col(vec![
234 txt.into_widget(ctx),
235 DrawWithTooltips::new_widget(
236 ctx,
237 batch,
238 tooltips,
239 Box::new(|arrow| {
240 let mut batch = GeomBatch::from(vec![(Color::hex("#EE702E"), arrow.clone())]);
241 batch.push(Color::WHITE, arrow.to_outline(Distance::meters(1.0)));
242 batch
243 }),
244 ),
245 ])
246 .padding(10)
247 .bg(app.cs.inner_panel_bg)
248 .outline(ctx.style().section_outline),
249 );
250 rows.push(
251 ctx.style()
252 .btn_outline
253 .text("Explore demand across all traffic signals")
254 .build_def(ctx),
255 );
256 if app.opts.dev {
257 rows.push(
258 ctx.style()
259 .btn_outline
260 .text("Where are these agents headed?")
261 .build_widget(ctx, format!("routes across {}", id)),
262 );
263 }
264
265 Widget::col(rows)
266}
267
268pub fn arrivals(
269 ctx: &mut EventCtx,
270 app: &App,
271 details: &mut Details,
272 id: IntersectionID,
273 opts: &DataOptions,
274) -> Widget {
275 Widget::custom_col(vec![
276 header(
277 ctx,
278 app,
279 details,
280 id,
281 Tab::IntersectionArrivals(id, opts.clone()),
282 ),
283 throughput(
284 ctx,
285 app,
286 "Number of in-bound trips from this border",
287 move |_| app.primary.sim.all_arrivals_at_border(id),
288 opts,
289 )
290 .tab_body(ctx),
291 ])
292}
293
294pub fn traffic_signal(
295 ctx: &mut EventCtx,
296 app: &App,
297 details: &mut Details,
298 id: IntersectionID,
299) -> Widget {
300 Widget::custom_col(vec![
301 header(ctx, app, details, id, Tab::IntersectionTrafficSignal(id)),
302 traffic_signal_body(ctx, app, id).tab_body(ctx),
303 ])
304}
305
306fn traffic_signal_body(ctx: &mut EventCtx, app: &App, id: IntersectionID) -> Widget {
307 let mut rows = vec![];
308 let bounds = app.primary.map.get_i(id).polygon.get_bounds();
311 let zoom = 150.0 / bounds.width();
313 let bbox = Polygon::rectangle(zoom * bounds.width(), zoom * bounds.height());
314
315 let signal = app.primary.map.get_traffic_signal(id);
316 {
317 let mut txt = Text::new();
318 txt.add_line(Line(format!("{} stages", signal.stages.len())).small_heading());
319 txt.add_line(format!("Signal offset: {}", signal.offset));
320 {
321 let mut total = Duration::ZERO;
322 for s in &signal.stages {
323 total += s.stage_type.simple_duration();
324 }
325 txt.add_line(format!("One cycle lasts {}", total));
327 }
328 rows.push(txt.into_widget(ctx));
329 }
330
331 for (idx, stage) in signal.stages.iter().enumerate() {
332 rows.push(
333 match stage.stage_type {
334 StageType::Fixed(d) => Line(format!("Stage {}: {}", idx + 1, d)),
335 StageType::Variable(min, delay, additional) => Line(format!(
336 "Stage {}: {}, {}, {} (variable)",
337 idx + 1,
338 min,
339 delay,
340 additional
341 )),
342 }
343 .into_widget(ctx),
344 );
345
346 {
347 let mut orig_batch = GeomBatch::new();
348 draw_signal_stage(
349 ctx.prerender,
350 stage,
351 idx,
352 id,
353 None,
354 &mut orig_batch,
355 app,
356 TrafficSignalStyle::Yuwen,
357 );
358
359 let mut normal = GeomBatch::new();
360 normal.push(Color::BLACK, bbox.clone());
361 normal.append(
362 orig_batch
363 .translate(-bounds.min_x, -bounds.min_y)
364 .scale(zoom),
365 );
366
367 rows.push(normal.into_widget(ctx));
368 }
369 }
370
371 Widget::col(rows)
372}
373
374fn delay_plot(
375 ctx: &EventCtx,
376 app: &App,
377 i: IntersectionID,
378 opts: &DataOptions,
379 fan_chart: bool,
380) -> Widget {
381 let data = if opts.show_before {
382 app.prebaked()
383 } else {
384 app.primary.sim.get_analytics()
385 };
386 let mut by_type: BTreeMap<AgentType, Vec<(Time, Duration)>> = AgentType::all()
387 .into_iter()
388 .map(|t| (t, Vec::new()))
389 .collect();
390 let limit = if opts.show_end_of_day {
391 app.primary.sim.get_end_of_day()
392 } else {
393 app.primary.sim.time()
394 };
395 if let Some(list) = data.intersection_delays.get(&i) {
396 for (_, t, dt, agent_type) in list {
397 if *t > limit {
398 break;
399 }
400 by_type.get_mut(agent_type).unwrap().push((*t, *dt));
401 }
402 }
403 let series: Vec<Series<Time, Duration>> = by_type
404 .into_iter()
405 .map(|(agent_type, pts)| Series {
406 label: agent_type.noun().to_string(),
407 color: color_for_agent_type(app, agent_type),
408 pts,
409 })
410 .collect();
411 let plot_opts = PlotOptions {
412 filterable: true,
413 max_x: Some(limit),
414 max_y: None,
415 disabled: opts.disabled_series(),
416 dims: None,
417 };
418 Widget::col(vec![
419 Line("Delay through intersection")
420 .small_heading()
421 .into_widget(ctx),
422 if fan_chart {
423 FanChart::new_widget(ctx, series, plot_opts, app.opts.units)
424 } else {
425 ScatterPlot::new_widget(ctx, series, plot_opts, app.opts.units)
426 },
427 ])
428 .padding(10)
429 .bg(app.cs.inner_panel_bg)
430 .outline(ctx.style().section_outline)
431}
432
433pub fn problems(
434 ctx: &mut EventCtx,
435 app: &App,
436 details: &mut Details,
437 id: IntersectionID,
438 opts: &ProblemOptions,
439) -> Widget {
440 Widget::custom_col(vec![
441 header(
442 ctx,
443 app,
444 details,
445 id,
446 Tab::IntersectionProblems(id, opts.clone()),
447 ),
448 problems_body(ctx, app, id, opts).tab_body(ctx),
449 ])
450}
451
452fn problems_body(
453 ctx: &mut EventCtx,
454 app: &App,
455 id: IntersectionID,
456 opts: &ProblemOptions,
457) -> Widget {
458 let mut rows = vec![];
459
460 rows.push(opts.to_controls(ctx, app));
461
462 let time = if opts.show_end_of_day {
463 app.primary.sim.get_end_of_day()
464 } else {
465 app.primary.sim.time()
466 };
467 rows.push(problem_count(
468 ctx,
469 app,
470 "Number of problems per 15 minutes",
471 move |a| a.problems_per_intersection(time, id),
472 opts,
473 ));
474
475 Widget::col(rows)
476}
477
478fn header(
479 ctx: &EventCtx,
480 app: &App,
481 details: &mut Details,
482 id: IntersectionID,
483 tab: Tab,
484) -> Widget {
485 let mut rows = vec![];
486
487 let i = app.primary.map.get_i(id);
488
489 let label = if i.is_border() {
490 format!("Border #{}", id.0)
491 } else {
492 match i.control {
493 IntersectionControl::Signed | IntersectionControl::Uncontrolled => {
494 format!("{} (Stop signs)", id)
495 }
496 IntersectionControl::Signalled => format!("{} (Traffic signals)", id),
497 IntersectionControl::Construction => format!("{} (under construction)", id),
498 }
499 };
500 rows.push(Widget::row(vec![
501 Line(label).small_heading().into_widget(ctx),
502 header_btns(ctx),
503 ]));
504
505 rows.push(make_tabs(ctx, &mut details.hyperlinks, tab, {
506 let mut tabs = vec![
507 ("Info", Tab::IntersectionInfo(id)),
508 ("Traffic", Tab::IntersectionTraffic(id, DataOptions::new())),
509 (
510 "Problems",
511 Tab::IntersectionProblems(id, ProblemOptions::new()),
512 ),
513 ];
514 if i.is_traffic_signal() {
515 tabs.push((
516 "Delay",
517 Tab::IntersectionDelay(id, DataOptions::new(), false),
518 ));
519 tabs.push(("Current demand", Tab::IntersectionDemand(id)));
520 tabs.push(("Signal", Tab::IntersectionTrafficSignal(id)));
521 }
522 if i.is_incoming_border() {
523 tabs.push((
524 "Arrivals",
525 Tab::IntersectionArrivals(id, DataOptions::new()),
526 ));
527 }
528 tabs
529 }));
530
531 Widget::custom_col(rows)
532}