game/edit/
multiple_roads.rs

1//! After a single road has been edited, these states let the changes be copied to all similar road
2//! segments. Note that only lane configuration is copied, not speed limit or access restrictions.
3
4use std::collections::HashSet;
5
6use crate::ID;
7use geom::Distance;
8use map_model::{EditRoad, MapEdits, RoadID};
9use widgetry::tools::PopupMsg;
10use widgetry::{
11    Color, Drawable, EventCtx, Fill, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line, Outcome,
12    Panel, State, Text, TextExt, Texture, VerticalAlignment, Widget,
13};
14
15use crate::app::App;
16use crate::app::Transition;
17use crate::common::Warping;
18use crate::edit::apply_map_edits;
19
20pub struct SelectSegments {
21    new_state: EditRoad,
22    candidates: HashSet<RoadID>,
23    base_road: RoadID,
24    base_edits: MapEdits,
25
26    current: HashSet<RoadID>,
27    draw: Drawable,
28    panel: Panel,
29    selected: Option<RoadID>,
30}
31
32impl SelectSegments {
33    pub fn new_state(
34        ctx: &mut EventCtx,
35        app: &mut App,
36        base_road: RoadID,
37        orig_state: EditRoad,
38        new_state: EditRoad,
39        base_edits: MapEdits,
40    ) -> Box<dyn State<App>> {
41        app.primary.current_selection = None;
42
43        // Find all road segments matching the original state and name. base_road has already
44        // changed to new_state, so no need to exclude it.
45        let map = &app.primary.map;
46        let base_name = map.get_r(base_road).get_name(None);
47        let mut candidates = HashSet::new();
48        for r in map.all_roads() {
49            if map.get_r_edit(r.id).lanes_ltr == orig_state.lanes_ltr
50                && r.get_name(None) == base_name
51            {
52                candidates.insert(r.id);
53            }
54        }
55
56        if candidates.is_empty() {
57            return PopupMsg::new_state(
58                ctx,
59                "Error",
60                vec!["No other roads resemble the one you changed"],
61            );
62        }
63
64        let current = candidates.clone();
65        let mut state = SelectSegments {
66            new_state,
67            candidates,
68            base_road,
69            base_edits,
70
71            current,
72            draw: Drawable::empty(ctx),
73            panel: Panel::empty(ctx),
74            selected: None,
75        };
76        state.recalculate(ctx, app);
77        Box::new(state)
78    }
79
80    fn recalculate(&mut self, ctx: &mut EventCtx, app: &App) {
81        // Update the drawn view
82        let mut batch = GeomBatch::new();
83        let map = &app.primary.map;
84        let color = Color::CYAN;
85        // Point out the road we're using as the template
86        batch.push(
87            color.alpha(0.9),
88            map.get_r(self.base_road)
89                .get_thick_polygon()
90                .to_outline(Distance::meters(3.0)),
91        );
92        for r in &self.candidates {
93            let alpha = if self.current.contains(r) { 0.9 } else { 0.5 };
94            batch.push(
95                Fill::ColoredTexture(Color::CYAN.alpha(alpha), Texture::CROSS_HATCH),
96                map.get_r(*r).get_thick_polygon(),
97            );
98        }
99        self.draw = ctx.upload(batch);
100
101        // Update the panel
102        self.panel = Panel::new_builder(Widget::col(vec![
103            Line("Apply changes to similar roads")
104                .small_heading()
105                .into_widget(ctx),
106            Widget::row(vec![
107                format!(
108                    "{} / {} roads similar to",
109                    self.current.len(),
110                    self.candidates.len(),
111                )
112                .text_widget(ctx)
113                .centered_vert(),
114                ctx.style()
115                    .btn_plain
116                    .icon_text(
117                        "system/assets/tools/location.svg",
118                        format!("#{}", self.base_road.0),
119                    )
120                    .build_widget(ctx, "jump to changed road"),
121                "are selected".text_widget(ctx).centered_vert(),
122            ]),
123            // TODO Explain that this is only for lane configuration, NOT speed limit
124            Widget::row(vec![
125                "Click to select/unselect".text_widget(ctx).centered_vert(),
126                ctx.style()
127                    .btn_plain
128                    .text("Select all")
129                    .disabled(self.current.len() == self.candidates.len())
130                    .build_def(ctx),
131                ctx.style()
132                    .btn_plain
133                    .text("Unselect all")
134                    .disabled(self.current.is_empty())
135                    .build_def(ctx),
136            ]),
137            Widget::row(vec![
138                ctx.style()
139                    .btn_solid_primary
140                    .text(format!("Apply changes to {} roads", self.current.len()))
141                    .hotkey(Key::Enter)
142                    .build_widget(ctx, "Apply"),
143                ctx.style()
144                    .btn_plain
145                    .text("Cancel")
146                    .hotkey(Key::Escape)
147                    .build_def(ctx),
148            ]),
149        ]))
150        .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
151        .build(ctx);
152    }
153}
154
155impl State<App> for SelectSegments {
156    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
157        if let Outcome::Clicked(x) = self.panel.event(ctx) {
158            match x.as_ref() {
159                "Apply" => {
160                    let mut edits = std::mem::take(&mut self.base_edits);
161                    for r in &self.current {
162                        edits
163                            .commands
164                            .push(app.primary.map.edit_road_cmd(*r, |new| {
165                                new.lanes_ltr = self.new_state.lanes_ltr.clone();
166                            }));
167                    }
168                    apply_map_edits(ctx, app, edits);
169                    app.primary.current_selection = None;
170                    return Transition::Multi(vec![
171                        Transition::Pop,
172                        Transition::Replace(PopupMsg::new_state(
173                            ctx,
174                            "Success",
175                            vec![format!(
176                                "Changed {} other roads to match",
177                                self.current.len()
178                            )],
179                        )),
180                    ]);
181                }
182                "Select all" => {
183                    self.current = self.candidates.clone();
184                    self.recalculate(ctx, app);
185                }
186                "Unselect all" => {
187                    self.current.clear();
188                    self.recalculate(ctx, app);
189                }
190                "Cancel" => {
191                    return Transition::Pop;
192                }
193                "jump to changed road" => {
194                    return Transition::Push(Warping::new_state(
195                        ctx,
196                        app.primary
197                            .canonical_point(ID::Road(self.base_road))
198                            .unwrap(),
199                        Some(10.0),
200                        Some(ID::Road(self.base_road)),
201                        &mut app.primary,
202                    ));
203                }
204                _ => unreachable!(),
205            }
206        }
207
208        if ctx.redo_mouseover() {
209            self.selected = None;
210            ctx.show_cursor();
211            if let Some(r) = match app.mouseover_unzoomed_roads_and_intersections(ctx) {
212                Some(ID::Road(r)) => Some(r),
213                Some(ID::Lane(l)) => Some(l.road),
214                _ => None,
215            } {
216                if self.candidates.contains(&r) {
217                    self.selected = Some(r);
218                    ctx.hide_cursor();
219                }
220                if r == self.base_road {
221                    self.selected = Some(r);
222                }
223            }
224        }
225
226        ctx.canvas_movement();
227
228        if let Some(r) = self.selected {
229            if r != self.base_road {
230                if self.current.contains(&r) && app.per_obj.left_click(ctx, "exclude road") {
231                    self.current.remove(&r);
232                    self.recalculate(ctx, app);
233                } else if !self.current.contains(&r) && app.per_obj.left_click(ctx, "include road")
234                {
235                    self.current.insert(r);
236                    self.recalculate(ctx, app);
237                }
238            }
239        }
240
241        Transition::Keep
242    }
243
244    fn draw(&self, g: &mut GfxCtx, _: &App) {
245        g.redraw(&self.draw);
246        self.panel.draw(g);
247
248        if let Some(r) = self.selected {
249            if let Some(cursor) = if self.current.contains(&r) {
250                Some("system/assets/tools/exclude.svg")
251            } else if self.candidates.contains(&r) {
252                Some("system/assets/tools/include.svg")
253            } else {
254                None
255            } {
256                let mut batch = GeomBatch::new();
257                batch.append(
258                    GeomBatch::load_svg(g, cursor)
259                        .scale(2.0)
260                        .centered_on(g.canvas.get_cursor().to_pt()),
261                );
262                g.fork_screenspace();
263                batch.draw(g);
264                g.unfork();
265            }
266
267            if r == self.base_road {
268                g.draw_mouse_tooltip(Text::from(format!("Edited {}", r)));
269            }
270        }
271    }
272
273    fn on_destroy(&mut self, ctx: &mut EventCtx, _: &mut App) {
274        // Don't forget to do this!
275        ctx.show_cursor();
276    }
277}