game/info/
lane.rs

1use abstutil::prettyprint_usize;
2use map_model::{LaneID, PathConstraints};
3use widgetry::{EventCtx, Line, LinePlot, PlotOptions, Series, Text, TextExt, Widget};
4
5use crate::app::App;
6use crate::info::{
7    header_btns, make_table, make_tabs, problem_count, throughput, DataOptions, Details,
8    ProblemOptions, Tab,
9};
10
11pub fn info(ctx: &EventCtx, app: &App, details: &mut Details, id: LaneID) -> Widget {
12    Widget::custom_col(vec![
13        header(ctx, app, details, id, Tab::LaneInfo(id)),
14        info_body(ctx, app, id).tab_body(ctx),
15    ])
16}
17
18fn info_body(ctx: &EventCtx, app: &App, id: LaneID) -> Widget {
19    let mut rows = vec![];
20
21    let map = &app.primary.map;
22    let l = map.get_l(id);
23    let r = map.get_r(id.road);
24
25    let mut kv = Vec::new();
26
27    if !l.is_walkable() {
28        kv.push(("Type", l.lane_type.describe().to_string()));
29    }
30    if r.is_private() {
31        let mut ban = Vec::new();
32        for p in PathConstraints::all() {
33            if !r.access_restrictions.allow_through_traffic.contains(p) {
34                ban.push(format!("{:?}", p).to_ascii_lowercase());
35            }
36        }
37        if !ban.is_empty() {
38            kv.push(("No through-traffic for", ban.join(", ")));
39        }
40    }
41
42    if l.is_parking() {
43        kv.push((
44            "Parking",
45            format!(
46                "{} / {} spots available",
47                app.primary.sim.get_free_onstreet_spots(l.id).len(),
48                l.number_parking_spots(app.primary.map.get_config())
49            ),
50        ));
51    } else {
52        kv.push(("Speed limit", r.speed_limit.to_string(&app.opts.units)));
53    }
54
55    kv.push(("Length", l.length().to_string(&app.opts.units)));
56
57    rows.extend(make_table(ctx, kv));
58
59    if l.is_parking() {
60        let capacity = l.number_parking_spots(app.primary.map.get_config());
61        let mut series = vec![Series {
62            label: format!("After \"{}\"", app.primary.map.get_edits().edits_name),
63            color: app.cs.after_changes,
64            pts: app.primary.sim.get_analytics().parking_lane_availability(
65                app.primary.sim.time(),
66                l.id,
67                capacity,
68            ),
69        }];
70        if app.has_prebaked().is_some() {
71            series.push(Series {
72                label: format!("Before \"{}\"", app.primary.map.get_edits().edits_name),
73                color: app.cs.before_changes.alpha(0.5),
74                pts: app.prebaked().parking_lane_availability(
75                    app.primary.sim.get_end_of_day(),
76                    l.id,
77                    capacity,
78                ),
79            });
80        }
81        let section = Widget::col(vec![
82            Line("Parking spots available")
83                .small_heading()
84                .into_widget(ctx),
85            LinePlot::new_widget(
86                ctx,
87                "parking availability",
88                series,
89                PlotOptions {
90                    max_y: Some(capacity),
91                    ..Default::default()
92                },
93                app.opts.units,
94            ),
95        ])
96        .padding(10)
97        .bg(app.cs.inner_panel_bg)
98        .outline(ctx.style().section_outline);
99        rows.push(section);
100    }
101
102    Widget::col(rows)
103}
104
105pub fn debug(ctx: &EventCtx, app: &App, details: &mut Details, id: LaneID) -> Widget {
106    Widget::custom_col(vec![
107        header(ctx, app, details, id, Tab::LaneDebug(id)),
108        debug_body(ctx, app, id).tab_body(ctx),
109    ])
110}
111
112fn debug_body(ctx: &EventCtx, app: &App, id: LaneID) -> Widget {
113    let mut rows = vec![];
114
115    let map = &app.primary.map;
116    let l = map.get_l(id);
117    let r = map.get_r(id.road);
118
119    let mut kv = vec![("Parent".to_string(), r.id.to_string())];
120
121    if l.lane_type.is_for_moving_vehicles() {
122        kv.push((
123            "Driving blackhole".to_string(),
124            l.driving_blackhole.to_string(),
125        ));
126        kv.push((
127            "Biking blackhole".to_string(),
128            l.biking_blackhole.to_string(),
129        ));
130    }
131
132    if let Some(types) = l.get_lane_level_turn_restrictions(r, false) {
133        kv.push((
134            "Turn restrictions".to_string(),
135            format!("{:?}", types.into_iter().collect::<Vec<_>>()),
136        ));
137    }
138    for (restriction, to) in &r.turn_restrictions {
139        kv.push((
140            format!("Restriction from this road to {}", to),
141            format!("{:?}", restriction),
142        ));
143    }
144
145    // TODO Simplify and expose everywhere after there's better data
146    kv.push((
147        "Elevation change".to_string(),
148        format!(
149            "{} to {}",
150            map.get_i(l.src_i).elevation,
151            map.get_i(l.dst_i).elevation
152        ),
153    ));
154    kv.push((
155        "Incline / grade".to_string(),
156        format!("{:.1}%", r.percent_incline * 100.0),
157    ));
158    kv.push((
159        "Elevation details".to_string(),
160        format!(
161            "{} over {}",
162            map.get_i(l.dst_i).elevation - map.get_i(l.src_i).elevation,
163            l.length()
164        ),
165    ));
166    kv.push((
167        "Dir, side, offset".to_string(),
168        format!(
169            "{}, {:?}, {}",
170            l.dir,
171            l.get_nearest_side_of_road(map).side,
172            l.id.offset
173        ),
174    ));
175    if let Some((reserved, total)) = app.primary.sim.debug_queue_lengths(l.id) {
176        kv.push((
177            "Queue (reserved, total) length".to_string(),
178            format!("{}, {}", reserved, total),
179        ));
180    }
181
182    rows.extend(make_table(ctx, kv));
183
184    rows.push(
185        ctx.style()
186            .btn_outline
187            .text("Open OSM way")
188            .build_widget(ctx, format!("open {}", r.orig_id.osm_way_id)),
189    );
190
191    let mut txt = Text::from("");
192    txt.add_line("Raw OpenStreetMap data");
193    rows.push(txt.into_widget(ctx));
194
195    rows.extend(make_table(
196        ctx,
197        r.osm_tags
198            .inner()
199            .iter()
200            .map(|(k, v)| (k, v.to_string()))
201            .collect(),
202    ));
203
204    Widget::col(rows)
205}
206
207pub fn traffic(
208    ctx: &mut EventCtx,
209    app: &App,
210    details: &mut Details,
211    id: LaneID,
212    opts: &DataOptions,
213) -> Widget {
214    Widget::custom_col(vec![
215        header(ctx, app, details, id, Tab::LaneTraffic(id, opts.clone())),
216        traffic_body(ctx, app, id, opts).tab_body(ctx),
217    ])
218}
219
220fn traffic_body(ctx: &mut EventCtx, app: &App, id: LaneID, opts: &DataOptions) -> Widget {
221    let mut rows = vec![];
222
223    let r = id.road;
224
225    // Since this applies to the entire road, ignore lane type.
226    let mut txt = Text::from("Traffic over entire road, not just this lane");
227    txt.add_line(format!(
228        "Since midnight: {} commuters and vehicles crossed",
229        prettyprint_usize(app.primary.sim.get_analytics().road_thruput.total_for(r))
230    ));
231    rows.push(txt.into_widget(ctx));
232
233    rows.push(opts.to_controls(ctx, app));
234
235    let time = if opts.show_end_of_day {
236        app.primary.sim.get_end_of_day()
237    } else {
238        app.primary.sim.time()
239    };
240    // TODO This conflates commuters and vehicles, so we should maybe split it into different plots.
241    rows.push(throughput(
242        ctx,
243        app,
244        "Number of commuters and vehicles per hour",
245        move |a| {
246            if a.road_thruput.raw.is_empty() {
247                a.road_thruput.count_per_hour(r, time)
248            } else {
249                a.road_thruput.raw_throughput(time, r)
250            }
251        },
252        opts,
253    ));
254
255    Widget::col(rows)
256}
257
258pub fn problems(
259    ctx: &mut EventCtx,
260    app: &App,
261    details: &mut Details,
262    id: LaneID,
263    opts: &ProblemOptions,
264) -> Widget {
265    Widget::custom_col(vec![
266        header(ctx, app, details, id, Tab::LaneProblems(id, opts.clone())),
267        problems_body(ctx, app, id, opts).tab_body(ctx),
268    ])
269}
270
271fn problems_body(ctx: &mut EventCtx, app: &App, id: LaneID, opts: &ProblemOptions) -> Widget {
272    let mut rows = vec![];
273
274    rows.push(opts.to_controls(ctx, app));
275
276    let time = if opts.show_end_of_day {
277        app.primary.sim.get_end_of_day()
278    } else {
279        app.primary.sim.time()
280    };
281    rows.push(problem_count(
282        ctx,
283        app,
284        "Number of problems per 15 minutes",
285        move |a| a.problems_per_lane(time, id),
286        opts,
287    ));
288
289    Widget::col(rows)
290}
291
292fn header(ctx: &EventCtx, app: &App, details: &mut Details, id: LaneID, tab: Tab) -> Widget {
293    let mut rows = vec![];
294
295    let map = &app.primary.map;
296    let l = map.get_l(id);
297    let r = map.get_r(id.road);
298
299    let label = if l.is_shoulder() {
300        "Shoulder"
301    } else if l.is_sidewalk() {
302        "Sidewalk"
303    } else {
304        "Lane"
305    };
306
307    // Navbar
308    rows.push(Widget::row(vec![
309        Line(format!("{} #{}", label, id.encode_u32()))
310            .small_heading()
311            .into_widget(ctx),
312        header_btns(ctx),
313    ]));
314
315    // subtitle
316    rows.push(format!("@ {}", r.get_name(app.opts.language.as_ref())).text_widget(ctx));
317
318    // tabs
319    let mut tabs = vec![("Info", Tab::LaneInfo(id))];
320    if !l.is_parking() {
321        tabs.push(("Traffic", Tab::LaneTraffic(id, DataOptions::new())));
322        tabs.push(("Problems", Tab::LaneProblems(id, ProblemOptions::new())));
323    }
324    if app.opts.dev {
325        tabs.push(("Debug", Tab::LaneDebug(id)));
326    }
327    rows.push(make_tabs(ctx, &mut details.hyperlinks, tab, tabs));
328
329    Widget::custom_col(rows)
330}