ltn/components/
layers.rs

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
15// Partly copied from ungap/layers.s
16
17pub struct Layers {
18    panel: Panel,
19    minimized: bool,
20    // (Mode, max zoom, min zoom, bottom bar position)
21    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    // For the design LTN mode
27    pub autofix_bus_gates: bool,
28    pub autofix_one_ways: bool,
29}
30
31impl Layers {
32    /// Panel won't be initialized, must call `event` first
33    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                // TODO This doesn't seem to do mark_covered_area correctly, so using the scroll
99                // wheel on the spinner just scrolls the canvas
100                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    // Draw after road labels
121    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                        // TODO White = none
221                        "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        // TODO Light/dark buildings? Traffic signals?
288
289        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                    // TODO White = none
307                    "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                // TODO Entry/exit arrows?
344                // TODO Dashed roads are walk/bike
345                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                // TODO Should we invert text color? This gets hard to read
374                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}