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 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 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 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 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 rows.push(format!("@ {}", r.get_name(app.opts.language.as_ref())).text_widget(ctx));
317
318 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}