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
18pub 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 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 "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 "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 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 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 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 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 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 ])
356 }
357
358 fn highlight_road_type(&mut self, ctx: &mut EventCtx, app: &App, name: &str) {
359 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 _ => Color::GREEN,
399 };
400 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}