ltn/pages/
select_boundary.rs

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    // As an optimization, don't repeatedly attempt to make an edit that'll fail. The bool is
28    // whether the block is already included or not
29    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        // Make sure we clear this state if we ever modify neighbourhood boundaries
55        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        // Fill out the world initially
84        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            // TODO Adds an invisible, non-clickable block. Don't add the block at all then?
119            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    // If the block is part of the current neighbourhood, remove it. Otherwise add it. It's assumed
128    // this block is in the previous frontier
129    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                // Redraw all of the blocks that changed
147                let mut changed_blocks: Vec<BlockID> = old_frontier
148                    .symmetric_difference(&self.frontier)
149                    .cloned()
150                    .collect();
151                // And always the current block
152                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                // Figure out what we need to do first
169                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    // Ok(Some(x)) means the current neighbourhood was destroyed, and the caller should switch to
186    // focusing on a different neighborhood
187    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                // Ignore the return value if the old neighbourhood is deleted
193                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            // Find all of the blocks within the polygon
218            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                // Proceed in rounds. Calculate the current frontier, find all of the blocks in there,
230                // try to add them, repeat.
231                //
232                // It should be safe to add multiple blocks in a round without recalculating the
233                // frontier; adding one block shouldn't mess up the frontier for another
234                let mut changed = false;
235                let mut still_todo = Vec::new();
236                timer.start_iter("try to add blocks", add_blocks.len());
237                // TODO Sometimes it'd help to add all at once!
238                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            // Just redraw everything
266            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        // PreserveState doesn't matter, can't switch proposals in SelectBoundary anyway
295        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                    // TODO If we destroyed the current neighbourhood, then we cancel, we'll pop
312                    // back to a different neighbourhood than we started with. And also the original
313                    // partitioning will have been lost!!!
314                    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        // TODO Bypasses World...
341        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}