game/ungap/
layers.rs

1use std::collections::HashMap;
2
3use geom::Distance;
4use map_gui::tools::{DrawRoadLabels, Navigator};
5use map_model::osm::RoadRank;
6use map_model::LaneType;
7use widgetry::tools::PopupMsg;
8use widgetry::{
9    ButtonBuilder, Color, ControlState, Drawable, EdgeInsets, EventCtx, GeomBatch, GfxCtx,
10    HorizontalAlignment, Image, Key, Line, Outcome, Panel, ScreenPt, Text, Toggle,
11    VerticalAlignment, Widget,
12};
13
14use crate::app::{App, Transition};
15use crate::ungap::bike_network;
16use crate::ungap::bike_network::DrawNetworkLayer;
17
18/// A bottom-right panel for managing a bunch of toggleable layers in the "ungap the map" tool.
19pub struct Layers {
20    panel: Panel,
21    minimized: bool,
22    bike_network: Option<DrawNetworkLayer>,
23    labels: Option<DrawRoadLabels>,
24    elevation: bool,
25    steep_streets: Option<Drawable>,
26    // TODO Once widgetry buttons can take custom enums, that'd be perfect here
27    road_types: HashMap<String, Drawable>,
28    fade_map: Drawable,
29
30    zoom_enabled_cache_key: (bool, bool),
31    map_edit_key: usize,
32}
33
34impl Layers {
35    pub fn new(ctx: &mut EventCtx, app: &App) -> Layers {
36        let mut l = Layers {
37            panel: Panel::empty(ctx),
38            minimized: true,
39            bike_network: Some(DrawNetworkLayer::new(ctx, app)),
40            labels: Some(DrawRoadLabels::only_major_roads()),
41            elevation: false,
42            steep_streets: None,
43            road_types: HashMap::new(),
44            fade_map: GeomBatch::from(vec![(
45                Color::BLACK.alpha(0.4),
46                app.primary.map.get_boundary_polygon().clone(),
47            )])
48            .upload(ctx),
49            zoom_enabled_cache_key: zoom_enabled_cache_key(ctx),
50            map_edit_key: usize::MAX,
51        };
52
53        l.update_panel(ctx, app);
54        l
55    }
56
57    pub fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Option<Transition> {
58        let key = app.primary.map.get_edits_change_key();
59        if self.map_edit_key != key {
60            self.map_edit_key = key;
61            if self.bike_network.is_some() {
62                self.bike_network = Some(DrawNetworkLayer::new(ctx, app));
63            }
64            self.road_types.clear();
65        }
66
67        if ctx.redo_mouseover() && self.elevation && !self.minimized {
68            let mut label = Text::new().into_widget(ctx);
69
70            if ctx.canvas.is_unzoomed() {
71                if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
72                    if let Some((elevation, _)) = app
73                        .session
74                        .elevation_contours
75                        .value()
76                        .unwrap()
77                        .0
78                        .closest_pt(pt, Distance::meters(300.0))
79                    {
80                        label =
81                            Line(format!("{} ft", elevation.to_feet().round())).into_widget(ctx);
82                    }
83                }
84            }
85            self.panel.replace(ctx, "current elevation", label);
86        }
87
88        match self.panel.event(ctx) {
89            Outcome::Clicked(x) => {
90                return Some(Transition::Push(match x.as_ref() {
91                    // TODO Add physical picture examples
92                    "highway" => PopupMsg::new_state(ctx, "Highways", vec!["Unless there's a separate trail (like on the 520 or I90 bridge), highways aren't accessible to biking"]),
93                    "major street" => PopupMsg::new_state(ctx, "Major streets", vec!["Arterials have more traffic, but are often where businesses are located"]),
94                    "minor street" => PopupMsg::new_state(ctx, "Minor streets", vec!["Local streets have a low volume of traffic and are usually comfortable for biking, even without dedicated infrastructure"]),
95                    "trail" => PopupMsg::new_state(ctx, "Trails", vec!["Trails like the Burke Gilman are usually well-separated from vehicle traffic. The space is usually shared between people walking, cycling, and rolling."]),
96                    "protected bike lane" => PopupMsg::new_state(ctx, "Protected bike lanes", vec!["Bike lanes separated from vehicle traffic by physical barriers or a few feet of striping"]),
97                    "painted bike lane" => PopupMsg::new_state(ctx, "Painted bike lanes", vec!["Bike lanes without any separation from vehicle traffic. Often uncomfortably close to the \"door zone\" of parked cars."]),
98                    "greenway" => PopupMsg::new_state(ctx, "Stay Healthy Streets and neighborhood greenways", vec!["Residential streets with additional signage and light barriers. These are intended to be low traffic, dedicated for people walking and biking."]),
99                    // TODO Add URLs
100                    "about the elevation data" => PopupMsg::new_state(ctx, "About the elevation data", vec!["Biking uphill next to traffic without any dedicated space isn't fun.", "Biking downhill next to traffic, especially in the door-zone of parked cars, and especially on Seattle's bumpy roads... is downright terrifying.", "", "Note the elevation data is incorrect near bridges.", "Thanks to King County LIDAR and Ordnance Survey for the data, and Eldan Goldenberg for processing it."]),
101                   "zoom map out" => {
102                        ctx.canvas.center_zoom(-8.0);
103                        self.update_panel(ctx, app);
104                        return Some(Transition::Keep);
105                    },
106                    "zoom map in" => {
107                        ctx.canvas.center_zoom(8.0);
108                        self.update_panel(ctx, app);
109                        return Some(Transition::Keep);
110                    },
111                    "search" => {
112                        Navigator::new_state(ctx, app)
113                    }
114                    "hide panel" => {
115                        self.minimized = true;
116                        self.update_panel(ctx, app);
117                        return Some(Transition::Keep);
118                    }
119                    "show panel" => {
120                        self.minimized = false;
121                        self.update_panel(ctx, app);
122                        return Some(Transition::Keep);
123                    }
124                    _ => unreachable!(),
125            }));
126            }
127            Outcome::Changed(x) => match x.as_ref() {
128                "bike network" => {
129                    if self.panel.is_checked("bike network") {
130                        self.bike_network = Some(DrawNetworkLayer::new(ctx, app));
131                    } else {
132                        self.bike_network = None;
133                    }
134                    self.update_panel(ctx, app);
135                }
136                "road labels" => {
137                    if self.panel.is_checked("road labels") {
138                        self.labels = Some(DrawRoadLabels::only_major_roads());
139                    } else {
140                        self.labels = None;
141                    }
142                }
143                "elevation" => {
144                    self.elevation = self.panel.is_checked("elevation");
145                    self.update_panel(ctx, app);
146                    if self.elevation {
147                        let name = app.primary.map.get_name().clone();
148                        if app.session.elevation_contours.key() != Some(name.clone()) {
149                            let mut low = Distance::ZERO;
150                            let mut high = Distance::ZERO;
151                            for i in app.primary.map.all_intersections() {
152                                low = low.min(i.elevation);
153                                high = high.max(i.elevation);
154                            }
155                            // TODO Maybe also draw the uphill arrows on the steepest streets?
156                            let value = crate::layer::elevation::ElevationContours::make_contours(
157                                ctx, app, low, high,
158                            );
159                            app.session.elevation_contours.set(name, value);
160                        }
161                    }
162                }
163                "steep streets" => {
164                    if self.panel.is_checked("steep streets") {
165                        let (mut colorer, _, _) =
166                            crate::layer::elevation::SteepStreets::make_colorer(ctx, app);
167                        // The Colorer fades the map as the very first thing in the batch, but we
168                        // don't want to do that twice.
169                        // TODO Can't use no_fading without complicating make_colorer...
170                        colorer.draw.unzoomed.shift();
171                        self.steep_streets = Some(colorer.draw.unzoomed.upload(ctx));
172                    } else {
173                        self.steep_streets = None;
174                    }
175                    self.update_panel(ctx, app);
176                }
177                _ => unreachable!(),
178            },
179            _ => {}
180        }
181        if let Some(name) = self.panel.currently_hovering().cloned() {
182            self.highlight_road_type(ctx, app, &name);
183        }
184
185        if self.zoom_enabled_cache_key != zoom_enabled_cache_key(ctx) {
186            // appropriately disable/enable zoom buttons in case user scroll-zoomed
187            self.update_panel(ctx, app);
188            self.zoom_enabled_cache_key = zoom_enabled_cache_key(ctx);
189        }
190
191        None
192    }
193
194    pub fn draw(&self, g: &mut GfxCtx, app: &App) {
195        self.panel.draw(g);
196        if g.canvas.is_unzoomed() {
197            g.redraw(&self.fade_map);
198
199            let mut draw_bike_layer = true;
200
201            if let Some(name) = self.panel.currently_hovering() {
202                if let Some(draw) = self.road_types.get(name) {
203                    g.redraw(draw);
204                }
205                if name == "trail"
206                    || name == "protected bike lane"
207                    || name == "painted bike lane"
208                    || name == "greenway"
209                {
210                    draw_bike_layer = false;
211                }
212            }
213            if draw_bike_layer {
214                if let Some(ref n) = self.bike_network {
215                    n.draw(g);
216                }
217            }
218
219            if let Some(ref l) = self.labels {
220                l.draw(g, app);
221            }
222
223            if self.elevation {
224                if let Some((_, ref draw)) = app.session.elevation_contours.value() {
225                    draw.draw(g);
226                }
227            }
228            if let Some(ref draw) = self.steep_streets {
229                g.redraw(draw);
230            }
231        }
232    }
233
234    pub fn layer_icon_pos(&self) -> ScreenPt {
235        if self.minimized {
236            self.panel.center_of("show panel")
237        } else {
238            self.panel.center_of("layer icon")
239        }
240    }
241
242    pub fn show_panel(&mut self, ctx: &mut EventCtx, app: &App) {
243        self.minimized = false;
244        self.update_panel(ctx, app);
245    }
246
247    fn update_panel(&mut self, ctx: &mut EventCtx, app: &App) {
248        self.panel = Panel::new_builder(Widget::col(vec![
249            make_zoom_controls(ctx).align_right().padding_right(16),
250            self.make_legend(ctx, app)
251                .padding(16)
252                .bg(ctx.style().panel_bg),
253        ]))
254        .aligned(HorizontalAlignment::Right, VerticalAlignment::Bottom)
255        .build_custom(ctx);
256    }
257
258    fn make_legend(&self, ctx: &mut EventCtx, app: &App) -> Widget {
259        if self.minimized {
260            return ctx
261                .style()
262                .btn_plain
263                .icon("system/assets/tools/layers.svg")
264                .hotkey(Key::L)
265                .build_widget(ctx, "show panel");
266        }
267
268        Widget::col(vec![
269            Widget::row(vec![
270                Image::from_path("system/assets/tools/layers.svg")
271                    .dims(30.0)
272                    .into_widget(ctx)
273                    .centered_vert()
274                    .named("layer icon"),
275                Widget::custom_row(vec![
276                    // TODO Looks too close to access restrictions
277                    legend_btn(app.cs.unzoomed_highway, "highway").build_def(ctx),
278                    legend_btn(app.cs.unzoomed_arterial, "major street").build_def(ctx),
279                    legend_btn(app.cs.unzoomed_residential, "minor street").build_def(ctx),
280                ]),
281                ctx.style()
282                    .btn_plain
283                    .icon("system/assets/tools/search.svg")
284                    .hotkey(Key::K)
285                    .build_widget(ctx, "search"),
286                ctx.style()
287                    .btn_plain
288                    .icon("system/assets/tools/minimize.svg")
289                    .hotkey(Key::L)
290                    .build_widget(ctx, "hide panel")
291                    .align_right(),
292            ]),
293            Widget::custom_row({
294                let mut row = vec![Toggle::checkbox(
295                    ctx,
296                    "bike network",
297                    Key::B,
298                    self.bike_network.is_some(),
299                )];
300                if self.bike_network.is_some() {
301                    row.push(legend_btn(*bike_network::DEDICATED_TRAIL, "trail").build_def(ctx));
302                    row.push(
303                        legend_btn(*bike_network::PROTECTED_BIKE_LANE, "protected bike lane")
304                            .build_def(ctx),
305                    );
306                    row.push(
307                        legend_btn(*bike_network::PAINTED_BIKE_LANE, "painted bike lane")
308                            .build_def(ctx),
309                    );
310                    row.push(legend_btn(*bike_network::GREENWAY, "greenway").build_def(ctx));
311                }
312                row
313            }),
314            // TODO Distinguish door-zone bike lanes?
315            // TODO Call out bike turning boxes?
316            // TODO Call out bike signals?
317            Toggle::checkbox(ctx, "road labels", None, self.labels.is_some()),
318            Widget::row(vec![
319                Toggle::checkbox(ctx, "elevation", Key::E, self.elevation),
320                ctx.style()
321                    .btn_plain
322                    .icon("system/assets/tools/info.svg")
323                    .build_widget(ctx, "about the elevation data")
324                    .centered_vert(),
325                Text::new()
326                    .into_widget(ctx)
327                    .named("current elevation")
328                    .centered_vert(),
329            ]),
330            Widget::row({
331                let mut row = vec![Toggle::checkbox(
332                    ctx,
333                    "steep streets",
334                    Key::S,
335                    self.steep_streets.is_some(),
336                )];
337                if self.steep_streets.is_some() {
338                    let (categories, uphill_legend) =
339                        crate::layer::elevation::SteepStreets::make_legend(ctx);
340                    let mut legend: Vec<Widget> = categories
341                        .into_iter()
342                        .map(|(label, color)| {
343                            legend_btn(color, label)
344                                .label_color(Color::WHITE, ControlState::Default)
345                                .disabled(true)
346                                .build_def(ctx)
347                        })
348                        .collect();
349                    legend.push(uphill_legend);
350                    row.push(Widget::custom_row(legend));
351                }
352                row
353            }),
354            // TODO Probably a collisions layer
355        ])
356    }
357
358    fn highlight_road_type(&mut self, ctx: &mut EventCtx, app: &App, name: &str) {
359        // TODO Button enums would rock
360        if name == "bike network"
361            || name == "road labels"
362            || name == "elevation"
363            || name == "steep streets"
364            || name.starts_with("about ")
365        {
366            return;
367        }
368        if self.road_types.contains_key(name) {
369            return;
370        }
371
372        let mut batch = GeomBatch::new();
373        for r in app.primary.map.all_roads() {
374            let rank = r.get_rank();
375            let mut bike_lane = false;
376            let mut buffer = false;
377            for l in &r.lanes {
378                if l.lane_type == LaneType::Biking {
379                    bike_lane = true;
380                } else if matches!(l.lane_type, LaneType::Buffer(_)) {
381                    buffer = true;
382                }
383            }
384
385            let show = (name == "highway" && rank == RoadRank::Highway)
386                || (name == "major street" && rank == RoadRank::Arterial)
387                || (name == "minor street" && rank == RoadRank::Local)
388                || (name == "trail" && r.is_cycleway())
389                || (name == "protected bike lane" && bike_lane && buffer)
390                || (name == "painted bike lane" && bike_lane && !buffer)
391                || (name == "greenway" && bike_network::is_greenway(r));
392            if show {
393                let color = match name {
394                    "highway" => app.cs.unzoomed_highway,
395                    "major street" => app.cs.unzoomed_arterial,
396                    "minor street" => app.cs.unzoomed_residential,
397                    // Some of the bike layers are too faded, so always use a louder green.
398                    _ => Color::GREEN,
399                };
400                // TODO If it's a bike element, should probably thicken for the unzoomed scale...
401                // the maximum amount?
402                batch.push(color, r.get_thick_polygon());
403            }
404        }
405
406        self.road_types.insert(name.to_string(), ctx.upload(batch));
407    }
408}
409
410fn make_zoom_controls(ctx: &mut EventCtx) -> Widget {
411    let builder = ctx
412        .style()
413        .btn_floating
414        .btn()
415        .image_dims(30.0)
416        .outline((1.0, ctx.style().btn_plain.fg), ControlState::Default)
417        .padding(12.0);
418
419    Widget::custom_col(vec![
420        builder
421            .clone()
422            .image_path("system/assets/speed/plus.svg")
423            .corner_rounding(geom::CornerRadii {
424                top_left: 16.0,
425                top_right: 16.0,
426                bottom_right: 0.0,
427                bottom_left: 0.0,
428            })
429            .disabled(ctx.canvas.is_max_zoom())
430            .build_widget(ctx, "zoom map in"),
431        builder
432            .image_path("system/assets/speed/minus.svg")
433            .image_dims(30.0)
434            .padding(12.0)
435            .corner_rounding(geom::CornerRadii {
436                top_left: 0.0,
437                top_right: 0.0,
438                bottom_right: 16.0,
439                bottom_left: 16.0,
440            })
441            .disabled(ctx.canvas.is_min_zoom())
442            .build_widget(ctx, "zoom map out"),
443    ])
444}
445
446fn legend_btn(color: Color, label: &str) -> ButtonBuilder {
447    ButtonBuilder::new()
448        .label_text(label)
449        .bg_color(color, ControlState::Default)
450        .bg_color(color.alpha(0.6), ControlState::Hovered)
451        .padding(EdgeInsets {
452            top: 10.0,
453            bottom: 10.0,
454            left: 20.0,
455            right: 20.0,
456        })
457        .corner_rounding(0.0)
458}
459
460fn zoom_enabled_cache_key(ctx: &EventCtx) -> (bool, bool) {
461    (ctx.canvas.is_max_zoom(), ctx.canvas.is_min_zoom())
462}