ltn/pages/design_ltn/
modals.rs

1use std::collections::BTreeSet;
2
3use geom::{Distance, Polygon};
4use map_gui::tools::grey_out_map;
5use map_model::{FilterType, RoadFilter, RoadID};
6use osm2streets::{Direction, LaneSpec};
7use widgetry::{
8    Color, ControlState, DrawBaselayer, EventCtx, GeomBatch, GfxCtx, Key, Line, Outcome, Panel,
9    RewriteColor, State, Text, Texture, Toggle, Widget,
10};
11
12use crate::{redraw_all_icons, render, App, Transition};
13
14pub struct ResolveOneWayAndFilter {
15    panel: Panel,
16    roads: Vec<(RoadID, Distance)>,
17}
18
19impl ResolveOneWayAndFilter {
20    pub fn new_state(ctx: &mut EventCtx, roads: Vec<(RoadID, Distance)>) -> Box<dyn State<App>> {
21        let mut txt = Text::new();
22        txt.add_line(Line("Warning").small_heading());
23        txt.add_line("A modal filter cannot be placed on a one-way street.");
24        txt.add_line("");
25        txt.add_line("You can make the street two-way first, then place a filter.");
26
27        let panel = Panel::new_builder(Widget::col(vec![
28            txt.into_widget(ctx),
29            Toggle::checkbox(ctx, "Don't show this warning again", None, true),
30            ctx.style().btn_solid_primary.text("OK").build_def(ctx),
31        ]))
32        .build(ctx);
33
34        Box::new(Self { panel, roads })
35    }
36}
37
38impl State<App> for ResolveOneWayAndFilter {
39    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
40        if let Outcome::Clicked(_) = self.panel.event(ctx) {
41            // OK is the only choice
42            app.session.layers.autofix_one_ways =
43                self.panel.is_checked("Don't show this warning again");
44
45            fix_oneway_and_add_filter(ctx, app, &self.roads);
46
47            return Transition::Multi(vec![Transition::Pop, Transition::Recreate]);
48        }
49        Transition::Keep
50    }
51
52    fn draw_baselayer(&self) -> DrawBaselayer {
53        DrawBaselayer::PreviousState
54    }
55
56    fn draw(&self, g: &mut GfxCtx, app: &App) {
57        grey_out_map(g, app);
58        self.panel.draw(g);
59    }
60}
61
62pub fn fix_oneway_and_add_filter(ctx: &mut EventCtx, app: &mut App, roads: &[(RoadID, Distance)]) {
63    let driving_side = app.per_map.map.get_config().driving_side;
64    let mut edits = app.per_map.map.get_edits().clone();
65    for (r, dist) in roads {
66        edits
67            .commands
68            .push(app.per_map.map.edit_road_cmd(*r, |new| {
69                LaneSpec::toggle_road_direction(&mut new.lanes_ltr, driving_side);
70                // Maybe we just flipped a one-way forwards to a one-way backwards. So one more
71                // time to make it two-way
72                if LaneSpec::oneway_for_driving(&new.lanes_ltr) == Some(Direction::Back) {
73                    LaneSpec::toggle_road_direction(&mut new.lanes_ltr, driving_side);
74                }
75                new.modal_filter = Some(RoadFilter::new(*dist, app.session.filter_type));
76            }));
77    }
78    app.apply_edits(edits);
79    redraw_all_icons(ctx, app);
80}
81
82pub struct ResolveBusGate {
83    panel: Panel,
84    roads: Vec<(RoadID, Distance)>,
85}
86
87impl ResolveBusGate {
88    pub fn new_state(
89        ctx: &mut EventCtx,
90        app: &mut App,
91        roads: Vec<(RoadID, Distance)>,
92    ) -> Box<dyn State<App>> {
93        // TODO This'll mess up the placement, but we don't have easy access to the bottom panel
94        // here
95        app.session.layers.show_bus_routes(ctx, &app.cs, None);
96
97        let mut txt = Text::new();
98        txt.add_line(Line("Warning").small_heading());
99        txt.add_line("The following bus routes cross this road. Adding a regular modal filter would block them.");
100        txt.add_line("");
101
102        let mut routes = BTreeSet::new();
103        for (r, _) in &roads {
104            routes.extend(app.per_map.map.get_bus_routes_on_road(*r));
105        }
106        for route in routes {
107            txt.add_line(format!("- {route}"));
108        }
109
110        txt.add_line("");
111        txt.add_line("You can use a bus gate instead.");
112
113        let panel = Panel::new_builder(Widget::col(vec![
114            txt.into_widget(ctx),
115            Toggle::checkbox(ctx, "Don't show this warning again", None, true),
116            ctx.style().btn_solid_primary.text("OK").build_def(ctx),
117        ]))
118        .build(ctx);
119
120        Box::new(Self { panel, roads })
121    }
122}
123
124impl State<App> for ResolveBusGate {
125    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
126        if let Outcome::Clicked(_) = self.panel.event(ctx) {
127            // OK is the only choice
128            app.session.layers.autofix_bus_gates =
129                self.panel.is_checked("Don't show this warning again");
130            // Force the panel to show the new checkbox state
131            app.session.layers.show_bus_routes(ctx, &app.cs, None);
132
133            let mut edits = app.per_map.map.get_edits().clone();
134            for (r, dist) in self.roads.drain(..) {
135                edits.commands.push(app.per_map.map.edit_road_cmd(r, |new| {
136                    new.modal_filter = Some(RoadFilter::new(dist, FilterType::BusGate));
137                }));
138            }
139            app.apply_edits(edits);
140            redraw_all_icons(ctx, app);
141
142            return Transition::Multi(vec![Transition::Pop, Transition::Recreate]);
143        }
144        Transition::Keep
145    }
146
147    fn draw_baselayer(&self) -> DrawBaselayer {
148        DrawBaselayer::PreviousState
149    }
150
151    fn draw(&self, g: &mut GfxCtx, app: &App) {
152        grey_out_map(g, app);
153        self.panel.draw(g);
154    }
155}
156
157pub struct ChangeFilterType {
158    panel: Panel,
159}
160
161impl ChangeFilterType {
162    pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
163        let filter = |ft: FilterType, hotkey: Key, name: &str| {
164            ctx.style()
165                .btn_solid_primary
166                .icon_text(render::filter_svg_path(ft), name)
167                .image_color(
168                    RewriteColor::Change(render::filter_hide_color(ft), Color::CLEAR),
169                    ControlState::Default,
170                )
171                .image_color(
172                    RewriteColor::Change(render::filter_hide_color(ft), Color::CLEAR),
173                    ControlState::Disabled,
174                )
175                .disabled(app.session.filter_type == ft)
176                .hotkey(hotkey)
177                .build_def(ctx)
178        };
179
180        let panel = Panel::new_builder(Widget::col(vec![
181            Widget::row(vec![
182                Line("Choose a modal filter to place on streets")
183                    .small_heading()
184                    .into_widget(ctx),
185                ctx.style().btn_close_widget(ctx),
186            ]),
187            Widget::row(vec![
188                Widget::col(vec![
189                    filter(
190                        FilterType::WalkCycleOnly,
191                        Key::Num1,
192                        "Walking/cycling only",
193                    ),
194                    filter(FilterType::NoEntry, Key::Num2, "No entry"),
195                    filter(FilterType::BusGate, Key::Num3, "Bus gate"),
196                    filter(FilterType::SchoolStreet, Key::Num4, "School street"),
197                ]),
198                Widget::vertical_separator(ctx),
199                Widget::col(vec![
200                    GeomBatch::from(vec![
201                        (match app.session.filter_type {
202                            FilterType::WalkCycleOnly => Texture(1),
203                            FilterType::NoEntry => Texture(2),
204                            FilterType::BusGate => Texture(3),
205                            FilterType::SchoolStreet => Texture(4),
206                            // The rectangle size must match the base image, otherwise it'll be
207                            // repeated (tiled) or cropped -- not scaled.
208                        }, Polygon::rectangle(crate::SPRITE_WIDTH as f64, crate::SPRITE_HEIGHT as f64))
209                    ]).into_widget(ctx),
210                    // TODO Ambulances, etc
211                    Text::from(Line(match app.session.filter_type {
212                        FilterType::WalkCycleOnly => "A physical barrier that only allows people walking, cycling, and rolling to pass. Often planters or bollards. Larger vehicles cannot enter.",
213                        FilterType::NoEntry => "An alternative sign to indicate vehicles are not allowed to enter the street. Only people walking, cycling, and rolling may pass through.",
214                        FilterType::BusGate => "A bus gate sign and traffic cameras are installed to allow buses, pedestrians, and cyclists to pass. There is no physical barrier.",
215                        FilterType::SchoolStreet => "A closure during school hours only. The barrier usually allows teachers and staff to access the school.",
216                    })).wrap_to_pixels(ctx, crate::SPRITE_WIDTH as f64).into_widget(ctx),
217                ]),
218            ]),
219            ctx.style().btn_solid_primary.text("OK").hotkey(Key::Enter).build_def(ctx).centered_horiz(),
220        ]))
221        .build(ctx);
222        Box::new(Self { panel })
223    }
224}
225
226impl State<App> for ChangeFilterType {
227    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
228        if let Outcome::Clicked(x) = self.panel.event(ctx) {
229            return match x.as_ref() {
230                "No entry" => {
231                    app.session.filter_type = FilterType::NoEntry;
232                    Transition::Replace(Self::new_state(ctx, app))
233                }
234                "Walking/cycling only" => {
235                    app.session.filter_type = FilterType::WalkCycleOnly;
236                    Transition::Replace(Self::new_state(ctx, app))
237                }
238                "Bus gate" => {
239                    app.session.filter_type = FilterType::BusGate;
240                    Transition::Replace(Self::new_state(ctx, app))
241                }
242                "School street" => {
243                    app.session.filter_type = FilterType::SchoolStreet;
244                    Transition::Replace(Self::new_state(ctx, app))
245                }
246                "close" | "OK" => Transition::Multi(vec![Transition::Pop, Transition::Recreate]),
247                _ => unreachable!(),
248            };
249        }
250
251        Transition::Keep
252    }
253
254    fn draw_baselayer(&self) -> DrawBaselayer {
255        DrawBaselayer::PreviousState
256    }
257
258    fn draw(&self, g: &mut GfxCtx, app: &App) {
259        grey_out_map(g, app);
260        self.panel.draw(g);
261    }
262}