1use std::collections::BTreeSet;
2
3use anyhow::Result;
4
5use geom::{Distance, Polygon};
6use widgetry::mapspace::{World, WorldOutcome};
7use widgetry::tools::{Lasso, PopupMsg};
8use widgetry::{
9 Color, Drawable, EventCtx, GeomBatch, GfxCtx, Key, Line, Outcome, Panel, State, Text, TextExt,
10 Toggle, Widget,
11};
12
13use crate::components::{legend_entry, AppwidePanel, Mode};
14use crate::logic::{BlockID, Partitioning};
15use crate::render::colors;
16use crate::{mut_partitioning, pages, App, NeighbourhoodID, Transition};
17
18pub struct SelectBoundary {
19 appwide_panel: AppwidePanel,
20 left_panel: Panel,
21 id: NeighbourhoodID,
22 world: World<BlockID>,
23 frontier: BTreeSet<BlockID>,
24
25 orig_partitioning: Partitioning,
26
27 last_failed_change: Option<(BlockID, bool)>,
30 draw_last_error: Drawable,
31
32 lasso: Option<Lasso>,
33}
34
35impl SelectBoundary {
36 pub fn new_state(
37 ctx: &mut EventCtx,
38 app: &mut App,
39 id: NeighbourhoodID,
40 ) -> Box<dyn State<App>> {
41 if app.partitioning().broken {
42 return PopupMsg::new_state(
43 ctx,
44 "Error",
45 vec![
46 "Sorry, you can't adjust any boundaries on this map.",
47 "This is a known problem without any workaround yet.",
48 ],
49 );
50 }
51
52 app.calculate_draw_all_local_road_labels(ctx);
53
54 if let pages::EditMode::Shortcuts(ref mut maybe_focus) = app.session.edit_mode {
56 *maybe_focus = None;
57 }
58 if let pages::EditMode::FreehandFilters(_) = app.session.edit_mode {
59 app.session.edit_mode = pages::EditMode::Filters;
60 }
61
62 let appwide_panel = AppwidePanel::new(ctx, app, Mode::SelectBoundary);
63 let left_panel = make_panel(ctx, app, id, &appwide_panel.top_panel);
64 let mut state = SelectBoundary {
65 appwide_panel,
66 left_panel,
67 id,
68 world: World::new(),
69 frontier: BTreeSet::new(),
70
71 orig_partitioning: app.partitioning().clone(),
72 last_failed_change: None,
73 draw_last_error: Drawable::empty(ctx),
74
75 lasso: None,
76 };
77
78 let initial_boundary = app.partitioning().neighbourhood_block(id);
79 state.frontier = app
80 .partitioning()
81 .calculate_frontier(&initial_boundary.perimeter);
82
83 for id in app.partitioning().all_block_ids() {
85 state.add_block(ctx, app, id);
86 }
87
88 state.world.initialize_hover(ctx);
89 Box::new(state)
90 }
91
92 fn add_block(&mut self, ctx: &mut EventCtx, app: &App, id: BlockID) {
93 if self.currently_have_block(app, id) {
94 let mut obj = self
95 .world
96 .add(id)
97 .hitbox(app.partitioning().get_block(id).polygon.clone())
98 .draw_color(colors::BLOCK_IN_BOUNDARY)
99 .hover_alpha(0.8);
100 if self.frontier.contains(&id) {
101 obj = obj
102 .hotkey(Key::Space, "remove")
103 .hotkey(Key::LeftShift, "remove")
104 .clickable();
105 }
106 obj.build(ctx);
107 } else if self.frontier.contains(&id) {
108 self.world
109 .add(id)
110 .hitbox(app.partitioning().get_block(id).polygon.clone())
111 .draw_color(colors::BLOCK_IN_FRONTIER)
112 .hover_alpha(0.8)
113 .hotkey(Key::Space, "add")
114 .hotkey(Key::LeftControl, "add")
115 .clickable()
116 .build(ctx);
117 } else {
118 self.world
120 .add(id)
121 .hitbox(app.partitioning().get_block(id).polygon.clone())
122 .draw(GeomBatch::new())
123 .build(ctx);
124 }
125 }
126
127 fn toggle_block(&mut self, ctx: &mut EventCtx, app: &mut App, id: BlockID) -> Transition {
130 if self.last_failed_change == Some((id, self.currently_have_block(app, id))) {
131 return Transition::Keep;
132 }
133 self.last_failed_change = None;
134 self.draw_last_error = Drawable::empty(ctx);
135
136 match self.try_toggle_block(app, id) {
137 Ok(Some(new_neighbourhood)) => {
138 return Transition::Replace(SelectBoundary::new_state(ctx, app, new_neighbourhood));
139 }
140 Ok(None) => {
141 let old_frontier = std::mem::take(&mut self.frontier);
142 self.frontier = app
143 .partitioning()
144 .calculate_frontier(&app.partitioning().neighbourhood_block(self.id).perimeter);
145
146 let mut changed_blocks: Vec<BlockID> = old_frontier
148 .symmetric_difference(&self.frontier)
149 .cloned()
150 .collect();
151 changed_blocks.push(id);
153
154 for changed in changed_blocks {
155 self.world.delete_before_replacement(changed);
156 self.add_block(ctx, app, changed);
157 }
158
159 self.left_panel = make_panel(ctx, app, self.id, &self.appwide_panel.top_panel);
160 }
161 Err(err) => {
162 self.last_failed_change = Some((id, self.currently_have_block(app, id)));
163 let label = Text::from(Line(err.to_string()))
164 .wrap_to_pct(ctx, 15)
165 .into_widget(ctx);
166 self.left_panel.replace(ctx, "warning", label);
167
168 if !self.currently_have_block(app, id) {
170 let mut batch = GeomBatch::new();
171 for block in app.partitioning().find_intermediate_blocks(self.id, id) {
172 batch.push(
173 Color::PINK.alpha(0.5),
174 app.partitioning().get_block(block).polygon.clone(),
175 );
176 }
177 self.draw_last_error = ctx.upload(batch);
178 }
179 }
180 }
181
182 Transition::Keep
183 }
184
185 fn try_toggle_block(&mut self, app: &mut App, id: BlockID) -> Result<Option<NeighbourhoodID>> {
188 if self.currently_have_block(app, id) {
189 mut_partitioning!(app).remove_block_from_neighbourhood(&app.per_map.map, id)
190 } else {
191 match mut_partitioning!(app).transfer_blocks(&app.per_map.map, vec![id], self.id) {
192 Ok(_) => Ok(None),
194 Err(err) => {
195 if app.session.add_intermediate_blocks {
196 let mut add_all = app.partitioning().find_intermediate_blocks(self.id, id);
197 add_all.push(id);
198 mut_partitioning!(app).transfer_blocks(&app.per_map.map, add_all, self.id)
199 } else {
200 Err(err)
201 }
202 }
203 }
204 }
205 }
206
207 fn currently_have_block(&self, app: &App, id: BlockID) -> bool {
208 app.partitioning().block_to_neighbourhood(id) == self.id
209 }
210
211 fn add_blocks_freehand(&mut self, ctx: &mut EventCtx, app: &mut App, lasso_polygon: Polygon) {
212 self.last_failed_change = None;
213 self.draw_last_error = Drawable::empty(ctx);
214
215 ctx.loading_screen("expand current neighbourhood boundary", |ctx, timer| {
216 timer.start("find matching blocks");
217 let mut add_blocks = Vec::new();
219 for (id, block) in app.partitioning().all_single_blocks() {
220 if lasso_polygon.contains_pt(block.polygon.center()) {
221 if app.partitioning().block_to_neighbourhood(id) != self.id {
222 add_blocks.push(id);
223 }
224 }
225 }
226 timer.stop("find matching blocks");
227
228 while !add_blocks.is_empty() {
229 let mut changed = false;
235 let mut still_todo = Vec::new();
236 timer.start_iter("try to add blocks", add_blocks.len());
237 for block_id in add_blocks.drain(..) {
239 timer.next();
240 if self.frontier.contains(&block_id) {
241 if let Ok(_) = mut_partitioning!(app).transfer_blocks(
242 &app.per_map.map,
243 vec![block_id],
244 self.id,
245 ) {
246 changed = true;
247 } else {
248 still_todo.push(block_id);
249 }
250 } else {
251 still_todo.push(block_id);
252 }
253 }
254 if changed {
255 add_blocks = still_todo;
256 self.frontier = app.partitioning().calculate_frontier(
257 &app.partitioning().neighbourhood_block(self.id).perimeter,
258 );
259 } else {
260 info!("Giving up on adding {} blocks", still_todo.len());
261 break;
262 }
263 }
264
265 self.world = World::new();
267 for id in app.partitioning().all_block_ids() {
268 self.add_block(ctx, app, id);
269 }
270 });
271 }
272}
273
274impl State<App> for SelectBoundary {
275 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
276 if let Some(ref mut lasso) = self.lasso {
277 if let Some(polygon) = lasso.event(ctx) {
278 self.lasso = None;
279 self.add_blocks_freehand(ctx, app, polygon);
280 self.left_panel = make_panel(ctx, app, self.id, &self.appwide_panel.top_panel);
281 return Transition::Keep;
282 }
283
284 if let Outcome::Clicked(x) = self.left_panel.event(ctx) {
285 if x == "Cancel" {
286 self.lasso = None;
287 self.left_panel = make_panel(ctx, app, self.id, &self.appwide_panel.top_panel);
288 }
289 }
290
291 return Transition::Keep;
292 }
293
294 if let Some(t) =
296 self.appwide_panel
297 .event(ctx, app, &crate::save::PreserveState::Route, help)
298 {
299 return t;
300 }
301 if let Some(t) = app
302 .session
303 .layers
304 .event(ctx, &app.cs, Mode::SelectBoundary, None)
305 {
306 return t;
307 }
308 match self.left_panel.event(ctx) {
309 Outcome::Clicked(x) => match x.as_ref() {
310 "Cancel" => {
311 mut_partitioning!(app) = self.orig_partitioning.clone();
315 return Transition::Replace(pages::DesignLTN::new_state(ctx, app, self.id));
316 }
317 "Confirm" => {
318 return Transition::Replace(pages::DesignLTN::new_state(ctx, app, self.id));
319 }
320 "Select freehand" => {
321 self.lasso = Some(Lasso::new(Distance::meters(1.0)));
322 self.left_panel = make_panel_for_lasso(ctx, &self.appwide_panel.top_panel);
323 }
324 _ => unreachable!(),
325 },
326 Outcome::Changed(_) => {
327 app.session.add_intermediate_blocks = self
328 .left_panel
329 .is_checked("add intermediate blocks automatically");
330 }
331 _ => {}
332 }
333
334 match self.world.event(ctx) {
335 WorldOutcome::Keypress("add" | "remove", id) | WorldOutcome::ClickedObject(id) => {
336 return self.toggle_block(ctx, app, id);
337 }
338 _ => {}
339 }
340 if ctx.redo_mouseover() {
342 if let Some(id) = self.world.get_hovering() {
343 if ctx.is_key_down(Key::LeftControl) {
344 if !self.currently_have_block(app, id) {
345 return self.toggle_block(ctx, app, id);
346 }
347 } else if ctx.is_key_down(Key::LeftShift) {
348 if self.currently_have_block(app, id) {
349 return self.toggle_block(ctx, app, id);
350 }
351 }
352 }
353 }
354
355 Transition::Keep
356 }
357
358 fn draw(&self, g: &mut GfxCtx, app: &App) {
359 self.world.draw(g);
360 g.redraw(&self.draw_last_error);
361 self.appwide_panel.draw(g);
362 self.left_panel.draw(g);
363 app.per_map
364 .draw_all_local_road_labels
365 .as_ref()
366 .unwrap()
367 .draw(g);
368 app.per_map.draw_major_road_labels.draw(g);
369 app.session.layers.draw(g, app);
370 if let Some(ref lasso) = self.lasso {
371 lasso.draw(g);
372 }
373 }
374}
375
376fn make_panel(ctx: &mut EventCtx, app: &App, id: NeighbourhoodID, top_panel: &Panel) -> Panel {
377 crate::components::LeftPanel::builder(
378 ctx,
379 top_panel,
380 Widget::col(vec![
381 Line("Adjusting neighbourhood boundary")
382 .small_heading()
383 .into_widget(ctx),
384 Text::from_all(vec![
385 Line("Click").fg(ctx.style().text_hotkey_color),
386 Line(" to add/remove a block"),
387 ])
388 .into_widget(ctx),
389 Text::from_all(vec![
390 Line("Hold "),
391 Line(Key::LeftControl.describe()).fg(ctx.style().text_hotkey_color),
392 Line(" and paint over blocks to add"),
393 ])
394 .into_widget(ctx),
395 Text::from_all(vec![
396 Line("Hold "),
397 Line(Key::LeftShift.describe()).fg(ctx.style().text_hotkey_color),
398 Line(" and paint over blocks to remove"),
399 ])
400 .into_widget(ctx),
401 Toggle::checkbox(
402 ctx,
403 "add intermediate blocks automatically",
404 None,
405 app.session.add_intermediate_blocks,
406 ),
407 format!(
408 "Neighbourhood area: {}",
409 app.partitioning().neighbourhood_area_km2(id)
410 )
411 .text_widget(ctx),
412 ctx.style()
413 .btn_outline
414 .icon_text("system/assets/tools/select.svg", "Select freehand")
415 .hotkey(Key::F)
416 .build_def(ctx),
417 Widget::row(vec![
418 ctx.style()
419 .btn_solid_primary
420 .text("Confirm")
421 .hotkey(Key::Enter)
422 .build_def(ctx),
423 ctx.style()
424 .btn_solid_destructive
425 .text("Cancel")
426 .hotkey(Key::Escape)
427 .build_def(ctx),
428 ]),
429 Widget::placeholder(ctx, "warning"),
430 legend_entry(
431 ctx,
432 colors::BLOCK_IN_BOUNDARY,
433 "block part of current neighbourhood",
434 ),
435 legend_entry(ctx, colors::BLOCK_IN_FRONTIER, "block could be added"),
436 ]),
437 )
438 .build(ctx)
439}
440
441fn make_panel_for_lasso(ctx: &mut EventCtx, top_panel: &Panel) -> Panel {
442 crate::components::LeftPanel::builder(
443 ctx,
444 top_panel,
445 Widget::col(vec![
446 "Draw a custom boundary for a neighbourhood"
447 .text_widget(ctx)
448 .centered_vert(),
449 Text::from_all(vec![
450 Line("Click and drag").fg(ctx.style().text_hotkey_color),
451 Line(" to select the blocks to add to this neighbourhood"),
452 ])
453 .into_widget(ctx),
454 ctx.style()
455 .btn_solid_destructive
456 .text("Cancel")
457 .hotkey(Key::Escape)
458 .build_def(ctx),
459 ]),
460 )
461 .build(ctx)
462}
463
464fn help() -> Vec<&'static str> {
465 vec![
466 "You can grow or shrink the blue neighbourhood boundary here.",
467 "Due to various known issues, it's not always possible to draw the boundary you want.",
468 "",
469 "The aqua blocks show where you can currently expand the boundary.",
470 "Hint: There may be very small blocks near complex roads.",
471 "Try the freehand tool to select them.",
472 ]
473}