game/layer/
map.rs

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        // Show a tooltip with count, only when unzoomed
35        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        // Make sure all bikes lanes show up no matter what
77        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        // Show throughput, broken down by bike lanes or not
88        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        // Use intersection data too, but bin as on bike lanes or not based on connecting roads
106        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}