1use 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 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 let mut batch = GeomBatch::new();
83 let map = &app.primary.map;
84 let color = Color::CYAN;
85 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 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 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 ctx.show_cursor();
276 }
277}