game/info/
intersection.rs

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    // Pick a zoom so that we fit a fixed width in pixels
199    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    // Slightly inaccurate -- the turn rendering may slightly exceed the intersection polygon --
309    // but this is close enough.
310    let bounds = app.primary.map.get_i(id).polygon.get_bounds();
311    // Pick a zoom so that we fit a fixed width in pixels
312    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            // TODO Say "normally" or something?
326            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}