1use geom::Polygon;
2use map_gui::colors::ColorScheme;
3use map_model::{CrossingType, FilterType};
4use widgetry::tools::ColorLegend;
5use widgetry::{
6 ButtonBuilder, Color, ControlState, EdgeInsets, EventCtx, GeomBatch, GfxCtx,
7 HorizontalAlignment, Image, Key, Line, Outcome, Panel, RoundedF64, Spinner, TextExt, Toggle,
8 VerticalAlignment, Widget,
9};
10
11use crate::components::Mode;
12use crate::render::{colors, filter_svg_path};
13use crate::{pages, App, Transition};
14
15pub struct Layers {
18 panel: Panel,
19 minimized: bool,
20 panel_cache_key: (Mode, bool, bool, Option<f64>),
22 show_bus_routes: bool,
23 show_turn_restrictions: bool,
24 pub show_crossing_time: bool,
25
26 pub autofix_bus_gates: bool,
28 pub autofix_one_ways: bool,
29}
30
31impl Layers {
32 pub fn new(ctx: &mut EventCtx) -> Layers {
34 Self {
35 panel: Panel::empty(ctx),
36 minimized: true,
37 panel_cache_key: (Mode::Impact, false, false, None),
38 show_bus_routes: false,
39 show_turn_restrictions: true,
40 show_crossing_time: false,
41
42 autofix_bus_gates: false,
43 autofix_one_ways: false,
44 }
45 }
46
47 pub fn event(
48 &mut self,
49 ctx: &mut EventCtx,
50 cs: &ColorScheme,
51 mode: Mode,
52 bottom_panel: Option<&Panel>,
53 ) -> Option<Transition> {
54 match self.panel.event(ctx) {
55 Outcome::Clicked(x) => {
56 match x.as_ref() {
57 "zoom map out" => {
58 ctx.canvas.center_zoom(-8.0);
59 }
60 "zoom map in" => {
61 ctx.canvas.center_zoom(8.0);
62 }
63 "hide layers" => {
64 self.minimized = true;
65 }
66 "show layers" => {
67 self.minimized = false;
68 }
69 _ => unreachable!(),
70 }
71 self.update_panel(ctx, cs, bottom_panel);
72 return Some(Transition::Keep);
73 }
74 Outcome::Changed(x) => {
75 if x == "show bus routes" {
76 self.show_bus_routes = self.panel.is_checked(&x);
77 self.update_panel(ctx, cs, bottom_panel);
78 return Some(Transition::Keep);
79 } else if x == "show turn restrictions" {
80 self.show_turn_restrictions = self.panel.is_checked(&x);
81 self.update_panel(ctx, cs, bottom_panel);
82 return Some(Transition::Keep);
83 } else if x == "show time to nearest crossing" {
84 self.show_crossing_time = self.panel.is_checked(&x);
85 self.update_panel(ctx, cs, bottom_panel);
86 return Some(Transition::Keep);
87 } else if x == "Use bus gates when needed" {
88 self.autofix_bus_gates = self.panel.is_checked(&x);
89 self.update_panel(ctx, cs, bottom_panel);
90 return Some(Transition::Keep);
91 } else if x == "Fix one-way streets when needed" {
92 self.autofix_one_ways = self.panel.is_checked(&x);
93 self.update_panel(ctx, cs, bottom_panel);
94 return Some(Transition::Keep);
95 }
96
97 ctx.set_scale_factor(self.panel.spinner::<RoundedF64>("scale_factor").0);
98 self.update_panel(ctx, cs, bottom_panel);
101 return Some(Transition::Recreate);
102 }
103 _ => {}
104 }
105
106 let cache_key = (
107 mode,
108 ctx.canvas.is_max_zoom(),
109 ctx.canvas.is_min_zoom(),
110 bottom_panel.map(|p| p.panel_rect().y1),
111 );
112 if self.panel_cache_key != cache_key {
113 self.panel_cache_key = cache_key;
114 self.update_panel(ctx, cs, bottom_panel);
115 }
116
117 None
118 }
119
120 pub fn draw(&self, g: &mut GfxCtx, app: &App) {
122 self.panel.draw(g);
123 if self.show_bus_routes {
124 g.redraw(&app.per_map.draw_bus_routes);
125 }
126 if self.show_turn_restrictions {
127 g.redraw(&app.per_map.draw_turn_restrictions);
128 }
129 }
130
131 pub fn show_bus_routes(
132 &mut self,
133 ctx: &mut EventCtx,
134 cs: &ColorScheme,
135 bottom_panel: Option<&Panel>,
136 ) {
137 self.minimized = false;
138 self.show_bus_routes = true;
139 self.update_panel(ctx, cs, bottom_panel);
140 }
141
142 pub fn show_panel(
143 &mut self,
144 ctx: &mut EventCtx,
145 cs: &ColorScheme,
146 bottom_panel: Option<&Panel>,
147 ) {
148 self.minimized = false;
149 self.update_panel(ctx, cs, bottom_panel);
150 }
151
152 fn update_panel(&mut self, ctx: &mut EventCtx, cs: &ColorScheme, bottom_panel: Option<&Panel>) {
153 let mut builder = Panel::new_builder(
154 Widget::col(vec![
155 make_zoom_controls(ctx).align_right(),
156 self.make_legend(ctx, cs).bg(ctx.style().panel_bg),
157 ])
158 .padding_right(16),
159 )
160 .aligned(HorizontalAlignment::Right, VerticalAlignment::Bottom);
161 if let Some(bottom_panel) = bottom_panel {
162 let buffer = 5.0;
163 builder = builder.aligned(
164 HorizontalAlignment::Right,
165 VerticalAlignment::Above(bottom_panel.panel_rect().y1 - buffer),
166 );
167 }
168 self.panel = builder.build_custom(ctx);
169 }
170
171 fn make_legend(&self, ctx: &mut EventCtx, cs: &ColorScheme) -> Widget {
172 if self.minimized {
173 return ctx
174 .style()
175 .btn_plain
176 .icon("system/assets/tools/layers.svg")
177 .hotkey(Key::L)
178 .build_widget(ctx, "show layers")
179 .centered_horiz();
180 }
181
182 Widget::col(vec![
183 Widget::row(vec![
184 Image::from_path("system/assets/tools/layers.svg")
185 .dims(30.0)
186 .into_widget(ctx)
187 .centered_vert()
188 .named("layer icon"),
189 ctx.style()
190 .btn_plain
191 .icon("system/assets/tools/minimize.svg")
192 .hotkey(Key::L)
193 .build_widget(ctx, "hide layers")
194 .align_right(),
195 ]),
196 self.panel_cache_key.0.legend(ctx, cs, self),
197 {
198 let checkbox = Toggle::checkbox(ctx, "show bus routes", None, self.show_bus_routes);
199 if self.show_bus_routes {
200 checkbox.outline((1.0, *colors::BUS_ROUTE))
201 } else {
202 checkbox
203 }
204 },
205 Toggle::checkbox(
206 ctx,
207 "show turn restrictions",
208 None,
209 self.show_turn_restrictions,
210 ),
211 if self.panel_cache_key.0 == Mode::Crossings {
212 Widget::col(vec![
213 Toggle::checkbox(
214 ctx,
215 "show time to nearest crossing",
216 None,
217 self.show_crossing_time,
218 ),
219 Widget::row(vec![
220 "Time:".text_widget(ctx),
222 ColorLegend::gradient_with_width(
223 ctx,
224 &cs.good_to_bad_red,
225 vec!["< 1 min", "> 5 mins"],
226 150.0,
227 ),
228 ])
229 .hide(!self.show_crossing_time),
230 ])
231 } else {
232 Widget::nothing()
233 },
234 Widget::row(vec![
235 "Adjust the size of text:".text_widget(ctx).centered_vert(),
236 Spinner::f64_widget(
237 ctx,
238 "scale_factor",
239 (0.5, 2.5),
240 ctx.prerender.get_scale_factor(),
241 0.1,
242 ),
243 ]),
244 ])
245 .padding(16)
246 }
247}
248
249fn make_zoom_controls(ctx: &mut EventCtx) -> Widget {
250 let builder = ctx
251 .style()
252 .btn_floating
253 .btn()
254 .image_dims(30.0)
255 .outline((1.0, ctx.style().btn_plain.fg), ControlState::Default)
256 .padding(12.0);
257
258 Widget::custom_col(vec![
259 builder
260 .clone()
261 .image_path("system/assets/speed/plus.svg")
262 .corner_rounding(geom::CornerRadii {
263 top_left: 16.0,
264 top_right: 16.0,
265 bottom_right: 0.0,
266 bottom_left: 0.0,
267 })
268 .disabled(ctx.canvas.is_max_zoom())
269 .build_widget(ctx, "zoom map in"),
270 builder
271 .image_path("system/assets/speed/minus.svg")
272 .image_dims(30.0)
273 .padding(12.0)
274 .corner_rounding(geom::CornerRadii {
275 top_left: 0.0,
276 top_right: 0.0,
277 bottom_right: 16.0,
278 bottom_left: 16.0,
279 })
280 .disabled(ctx.canvas.is_min_zoom())
281 .build_widget(ctx, "zoom map out"),
282 ])
283}
284
285impl Mode {
286 fn legend(&self, ctx: &mut EventCtx, cs: &ColorScheme, layers: &Layers) -> Widget {
287 Widget::col(match self {
290 Mode::PickArea => vec![
291 entry_tooltip(
292 ctx,
293 Color::BLACK,
294 "main road",
295 "Classified as non-local, designed for through-traffic",
296 ),
297 entry_tooltip(
298 ctx,
299 Color::YELLOW.alpha(0.2),
300 "neighbourhood",
301 "Analyze through-traffic here",
302 ),
303 ],
304 Mode::ModifyNeighbourhood => vec![
305 Widget::row(vec![
306 "Shortcuts:".text_widget(ctx),
308 ColorLegend::gradient_with_width(
309 ctx,
310 &cs.good_to_bad_red,
311 vec!["low", "high"],
312 150.0,
313 ),
314 ]),
315 Widget::row(vec!["Cells:".text_widget(ctx), color_grid(ctx)]),
316 Widget::row(vec![
317 "Modal filters:".text_widget(ctx),
318 Image::from_path(filter_svg_path(FilterType::WalkCycleOnly))
319 .untinted()
320 .dims(30.0)
321 .into_widget(ctx),
322 Image::from_path(filter_svg_path(FilterType::NoEntry))
323 .untinted()
324 .dims(30.0)
325 .into_widget(ctx),
326 Image::from_path(filter_svg_path(FilterType::BusGate))
327 .untinted()
328 .dims(30.0)
329 .into_widget(ctx),
330 Image::from_path(filter_svg_path(FilterType::SchoolStreet))
331 .untinted()
332 .dims(30.0)
333 .into_widget(ctx),
334 ]),
335 Line("Faded filters exist already").small().into_widget(ctx),
336 Widget::row(vec![
337 "Private road:".text_widget(ctx),
338 Image::from_path("system/assets/map/private_road.svg")
339 .untinted()
340 .dims(30.0)
341 .into_widget(ctx),
342 ]),
343 Toggle::checkbox(
346 ctx,
347 "Use bus gates when needed",
348 None,
349 layers.autofix_bus_gates,
350 ),
351 Toggle::checkbox(
352 ctx,
353 "Fix one-way streets when needed",
354 None,
355 layers.autofix_one_ways,
356 ),
357 ],
358 Mode::SelectBoundary => vec![],
359 Mode::FreehandBoundary => vec![],
360 Mode::PerResidentImpact => vec![],
361 Mode::RoutePlanner => vec![
362 entry(
363 ctx,
364 *colors::PLAN_ROUTE_BEFORE,
365 "driving route before changes",
366 ),
367 entry(
368 ctx,
369 *colors::PLAN_ROUTE_AFTER,
370 "driving route after changes",
371 ),
372 entry(ctx, *colors::PLAN_ROUTE_BIKE, "cycling route"),
373 entry(ctx, *colors::PLAN_ROUTE_WALK, "walking route"),
375 ],
376 Mode::Crossings => vec![
377 Widget::row(vec![
378 Image::from_path(pages::Crossings::svg_path(CrossingType::Unsignalized))
379 .untinted()
380 .dims(30.0)
381 .into_widget(ctx),
382 "Unsignalized crossing".text_widget(ctx),
383 ]),
384 Widget::row(vec![
385 Image::from_path(pages::Crossings::svg_path(CrossingType::Signalized))
386 .untinted()
387 .dims(30.0)
388 .into_widget(ctx),
389 "Signalized crossing".text_widget(ctx),
390 ]),
391 entry(ctx, *colors::IMPERMEABLE, "impermeable (no crossings)"),
392 entry(ctx, *colors::SEMI_PERMEABLE, "semi-permeable (1 crossing)"),
393 entry(ctx, *colors::POROUS, "porous (≥2 crossings)"),
394 ],
395 Mode::Impact => vec![
396 map_gui::tools::compare_counts::CompareCounts::relative_scale()
397 .make_legend(ctx, vec!["less", "same", "more"]),
398 ],
399 Mode::CycleNetwork => vec![
400 entry(
401 ctx,
402 *colors::NETWORK_SEGREGATED_LANE,
403 "segregated cycle lane",
404 ),
405 entry(ctx, *colors::NETWORK_QUIET_STREET, "quiet local street"),
406 entry(
407 ctx,
408 *colors::NETWORK_PAINTED_LANE,
409 "painted cycle lane or shared bus lane",
410 ),
411 entry(
412 ctx,
413 *colors::NETWORK_THROUGH_TRAFFIC_STREET,
414 "local street with cut-through traffic",
415 ),
416 ],
417 Mode::Census => vec![],
418 })
419 }
420}
421
422fn entry_builder<'a, 'c>(color: Color, label: &'static str) -> ButtonBuilder<'a, 'c> {
423 let mut btn = ButtonBuilder::new()
424 .label_text(label)
425 .bg_color(color, ControlState::Disabled)
426 .disabled(true)
427 .padding(EdgeInsets {
428 top: 10.0,
429 bottom: 10.0,
430 left: 20.0,
431 right: 20.0,
432 })
433 .corner_rounding(0.0);
434 if color == Color::BLACK {
435 btn = btn.label_color(Color::WHITE, ControlState::Disabled);
436 }
437 btn
438}
439
440fn entry(ctx: &EventCtx, color: Color, label: &'static str) -> Widget {
441 entry_builder(color, label).build_def(ctx)
442}
443
444pub fn legend_entry(ctx: &EventCtx, color: Color, label: &'static str) -> Widget {
445 entry(ctx, color, label)
446}
447
448fn entry_tooltip(
449 ctx: &mut EventCtx,
450 color: Color,
451 label: &'static str,
452 tooltip: &'static str,
453) -> Widget {
454 entry_builder(color, label)
455 .disabled_tooltip(tooltip)
456 .build_def(ctx)
457}
458
459fn color_grid(ctx: &mut EventCtx) -> Widget {
460 let size = 16.0;
461 let columns = 3;
462 let mut batch = GeomBatch::new();
463
464 for (i, color) in colors::CELLS.iter().enumerate() {
465 let row = (i / columns) as f64;
466 let column = (i % columns) as f64;
467 batch.push(
468 *color,
469 Polygon::rectangle(size, size).translate(size * column, size * row),
470 );
471 }
472
473 batch.into_widget(ctx)
474}