1use maplit::btreeset;
2
3use crate::ID;
4use abstutil::{prettyprint_usize, Counter};
5use geom::{Distance, Time};
6use map_gui::tools::{ColorDiscrete, ColorNetwork};
7use map_model::{AmenityType, Direction, LaneType};
8use sim::AgentType;
9use widgetry::mapspace::ToggleZoomed;
10use widgetry::tools::ColorLegend;
11use widgetry::{Color, EventCtx, GfxCtx, Line, Panel, Text, Widget};
12
13use crate::app::App;
14use crate::layer::{header, Layer, LayerOutcome, PANEL_PLACEMENT};
15
16pub struct BikeActivity {
17 panel: Panel,
18 time: Time,
19 draw: ToggleZoomed,
20 tooltip: Option<Text>,
21}
22
23impl Layer for BikeActivity {
24 fn name(&self) -> Option<&'static str> {
25 Some("cycling activity")
26 }
27 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Option<LayerOutcome> {
28 let mut recalc_tooltip = false;
29 if app.primary.sim.time() != self.time {
30 *self = BikeActivity::new(ctx, app);
31 recalc_tooltip = true;
32 }
33
34 if ctx.canvas.is_unzoomed() {
36 if ctx.redo_mouseover() || recalc_tooltip {
37 self.tooltip = None;
38 if let Some(ID::Road(r)) = app.mouseover_unzoomed_roads_and_intersections(ctx) {
39 let cnt = app
40 .primary
41 .sim
42 .get_analytics()
43 .road_thruput
44 .total_for_with_agent_types(r, btreeset! { AgentType::Bike });
45 if cnt > 0 {
46 self.tooltip = Some(Text::from(prettyprint_usize(cnt)));
47 }
48 }
49 }
50 } else {
51 self.tooltip = None;
52 }
53
54 <dyn Layer>::simple_event(ctx, &mut self.panel)
55 }
56 fn draw(&self, g: &mut GfxCtx, _: &App) {
57 self.panel.draw(g);
58 self.draw.draw(g);
59 if let Some(ref txt) = self.tooltip {
60 g.draw_mouse_tooltip(txt.clone());
61 }
62 }
63 fn draw_minimap(&self, g: &mut GfxCtx) {
64 g.redraw(&self.draw.unzoomed);
65 }
66}
67
68impl BikeActivity {
69 pub fn new(ctx: &mut EventCtx, app: &App) -> BikeActivity {
70 let mut num_lanes = 0;
71 let mut total_dist = Distance::ZERO;
72 let mut on_bike_lanes = Counter::new();
73 let mut off_bike_lanes = Counter::new();
74 let mut intersections_on = Counter::new();
75 let mut intersections_off = Counter::new();
76 for l in app.primary.map.all_lanes() {
78 if l.is_biking() {
79 on_bike_lanes.add(l.id.road, 0);
80 intersections_on.add(l.src_i, 0);
81 intersections_on.add(l.src_i, 0);
82 num_lanes += 1;
83 total_dist += l.length();
84 }
85 }
86
87 for ((r, agent_type, _), count) in &app.primary.sim.get_analytics().road_thruput.counts {
89 if *agent_type == AgentType::Bike {
90 if app
91 .primary
92 .map
93 .get_r(*r)
94 .lanes
95 .iter()
96 .any(|l| l.lane_type == LaneType::Biking)
97 {
98 on_bike_lanes.add(*r, *count);
99 } else {
100 off_bike_lanes.add(*r, *count);
101 }
102 }
103 }
104
105 for ((i, agent_type, _), count) in
107 &app.primary.sim.get_analytics().intersection_thruput.counts
108 {
109 if *agent_type == AgentType::Bike {
110 if app
111 .primary
112 .map
113 .get_i(*i)
114 .roads
115 .iter()
116 .any(|r| on_bike_lanes.get(*r) > 0)
117 {
118 intersections_on.add(*i, *count);
119 } else {
120 intersections_off.add(*i, *count);
121 }
122 }
123 }
124
125 let panel = Panel::new_builder(Widget::col(vec![
126 header(ctx, "Cycling activity"),
127 Text::from_multiline(vec![
128 Line(format!("{} bike lanes", num_lanes)),
129 Line(format!(
130 "total distance of {}",
131 total_dist.to_string(&app.opts.units)
132 )),
133 ])
134 .into_widget(ctx),
135 Line("Throughput on bike lanes").into_widget(ctx),
136 ColorLegend::gradient(
137 ctx,
138 &app.cs.good_to_bad_green,
139 vec!["lowest count", "highest"],
140 ),
141 Line("Throughput on unprotected roads").into_widget(ctx),
142 ColorLegend::gradient(
143 ctx,
144 &app.cs.good_to_bad_red,
145 vec!["lowest count", "highest"],
146 ),
147 ]))
148 .aligned_pair(PANEL_PLACEMENT)
149 .build(ctx);
150
151 let mut colorer = ColorNetwork::new(app);
152 colorer.ranked_roads(on_bike_lanes, &app.cs.good_to_bad_green);
153 colorer.ranked_roads(off_bike_lanes, &app.cs.good_to_bad_red);
154 colorer.ranked_intersections(intersections_on, &app.cs.good_to_bad_green);
155 colorer.ranked_intersections(intersections_off, &app.cs.good_to_bad_red);
156
157 BikeActivity {
158 panel,
159 time: app.primary.sim.time(),
160 draw: colorer.build(ctx),
161 tooltip: None,
162 }
163 }
164}
165
166pub struct Static {
167 panel: Panel,
168 pub draw: ToggleZoomed,
169 name: &'static str,
170}
171
172impl Layer for Static {
173 fn name(&self) -> Option<&'static str> {
174 Some(self.name)
175 }
176 fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Option<LayerOutcome> {
177 <dyn Layer>::simple_event(ctx, &mut self.panel)
178 }
179 fn draw(&self, g: &mut GfxCtx, _: &App) {
180 self.panel.draw(g);
181 self.draw.draw(g);
182 }
183 fn draw_minimap(&self, g: &mut GfxCtx) {
184 g.redraw(&self.draw.unzoomed);
185 }
186}
187
188impl Static {
189 fn new(
190 ctx: &mut EventCtx,
191 colorer: ColorDiscrete,
192 name: &'static str,
193 title: String,
194 extra: Widget,
195 ) -> Static {
196 let (draw, legend) = colorer.build(ctx);
197 let panel = Panel::new_builder(Widget::col(vec![header(ctx, &title), extra, legend]))
198 .aligned_pair(PANEL_PLACEMENT)
199 .build(ctx);
200
201 Static { panel, draw, name }
202 }
203
204 pub fn edits(ctx: &mut EventCtx, app: &App) -> Static {
205 let mut colorer = ColorDiscrete::new(
206 app,
207 vec![("modified road/intersection", app.cs.edits_layer)],
208 );
209
210 let edits = app.primary.map.get_edits();
211 let (lanes, roads) = edits.changed_lanes(&app.primary.map);
212 for l in lanes {
213 colorer.add_l(l, "modified road/intersection");
214 }
215 for r in roads {
216 colorer.add_r(r, "modified road/intersection");
217 }
218 for i in edits.original_intersections.keys() {
219 colorer.add_i(*i, "modified road/intersection");
220 }
221
222 Static::new(
223 ctx,
224 colorer,
225 "map edits",
226 format!("Map edits ({})", edits.edits_name),
227 Text::from_multiline(vec![
228 Line(format!("{} roads changed", edits.original_roads.len())),
229 Line(format!(
230 "{} intersections changed",
231 edits.original_intersections.len()
232 )),
233 ])
234 .into_widget(ctx),
235 )
236 }
237
238 pub fn amenities(ctx: &mut EventCtx, app: &App) -> Static {
239 let food = Color::RED;
240 let school = Color::CYAN;
241 let shopping = Color::PURPLE;
242 let other = Color::GREEN;
243
244 let mut draw = ToggleZoomed::builder();
245 for b in app.primary.map.all_buildings() {
246 if b.amenities.is_empty() {
247 continue;
248 }
249 let mut color = None;
250 for a in &b.amenities {
251 if let Some(t) = AmenityType::categorize(&a.amenity_type) {
252 color = Some(match t {
253 AmenityType::Food => food,
254 AmenityType::School => school,
255 AmenityType::Shopping => shopping,
256 _ => other,
257 });
258 break;
259 }
260 }
261 let color = color.unwrap_or(other);
262 draw.unzoomed.push(color, b.polygon.clone());
263 draw.zoomed.push(color.alpha(0.4), b.polygon.clone());
264 }
265
266 let panel = Panel::new_builder(Widget::col(vec![
267 header(ctx, "Amenities"),
268 ColorLegend::row(ctx, food, AmenityType::Food.to_string()),
269 ColorLegend::row(ctx, school, AmenityType::School.to_string()),
270 ColorLegend::row(ctx, shopping, AmenityType::Shopping.to_string()),
271 ColorLegend::row(ctx, other, "other".to_string()),
272 ]))
273 .aligned_pair(PANEL_PLACEMENT)
274 .build(ctx);
275
276 Static {
277 panel,
278 draw: draw.build(ctx),
279 name: "amenities",
280 }
281 }
282
283 pub fn no_sidewalks(ctx: &mut EventCtx, app: &App) -> Static {
284 let mut colorer = ColorDiscrete::new(app, vec![("no sidewalks", Color::RED)]);
285 for l in app.primary.map.all_lanes() {
286 if l.is_shoulder() && !app.primary.map.get_parent(l.id).is_cycleway() {
287 colorer.add_r(l.id.road, "no sidewalks");
288 }
289 }
290 Static::new(
291 ctx,
292 colorer,
293 "no sidewalks",
294 "No sidewalks".to_string(),
295 Widget::nothing(),
296 )
297 }
298
299 pub fn blackholes(ctx: &mut EventCtx, app: &App) -> Static {
300 let mut colorer = ColorDiscrete::new(
301 app,
302 vec![
303 ("driving blackhole", Color::RED),
304 ("biking blackhole", Color::GREEN),
305 ("driving + biking blackhole", Color::BLUE),
306 ],
307 );
308 for l in app.primary.map.all_lanes() {
309 if l.driving_blackhole && l.biking_blackhole {
310 colorer.add_l(l.id, "driving + biking blackhole");
311 } else if l.driving_blackhole {
312 colorer.add_l(l.id, "driving blackhole");
313 } else if l.biking_blackhole {
314 colorer.add_l(l.id, "biking blackhole");
315 }
316 }
317 Static::new(
318 ctx,
319 colorer,
320 "blackholes",
321 "blackholes".to_string(),
322 Widget::nothing(),
323 )
324 }
325
326 pub fn high_stress(ctx: &mut EventCtx, app: &App) -> Static {
327 let mut colorer = ColorDiscrete::new(app, vec![("high stress", app.cs.edits_layer)]);
328
329 for r in app.primary.map.all_roads() {
330 if r.high_stress_for_bikes(&app.primary.map, Direction::Fwd)
331 || r.high_stress_for_bikes(&app.primary.map, Direction::Back)
332 {
333 colorer.add_r(r.id, "high stress");
334 }
335 }
336
337 Static::new(
338 ctx,
339 colorer,
340 "high stress",
341 "High stress roads for biking".to_string(),
342 Text::from_multiline(vec![
343 Line("High stress defined as:"),
344 Line("- arterial classification"),
345 Line("- no dedicated cycle lane"),
346 ])
347 .into_widget(ctx),
348 )
349 }
350}