ltn/pages/
freehand_boundary.rs

1use std::collections::BTreeSet;
2
3use geom::{Distance, Polygon, QuadTree};
4use map_gui::tools::EditPolygon;
5use map_model::RoadID;
6use widgetry::tools::Lasso;
7use widgetry::{
8    Color, Drawable, EventCtx, GeomBatch, GfxCtx, Key, Line, Outcome, Panel, State, Text, TextExt,
9    Widget,
10};
11
12use crate::components::{AppwidePanel, Mode};
13use crate::logic::CustomBoundary;
14use crate::{mut_partitioning, pages, App, NeighbourhoodID, Transition};
15
16pub struct FreehandBoundary {
17    appwide_panel: AppwidePanel,
18    left_panel: Panel,
19
20    name: String,
21    id: Option<NeighbourhoodID>,
22    custom: Option<CustomBoundary>,
23    draw_custom: Drawable,
24    edit: EditPolygon,
25    lasso: Option<Lasso>,
26    quadtree: QuadTree<RoadID>,
27
28    // TODO Add WorldOutcome::DragEnd and plumb here if this is useful otherwise, or some kind of
29    // rate-limiting
30    queued_recalculate: bool,
31}
32
33impl FreehandBoundary {
34    pub fn blank(ctx: &mut EventCtx, app: &mut App, name: String) -> Box<dyn State<App>> {
35        let appwide_panel = AppwidePanel::new(ctx, app, Mode::FreehandBoundary);
36        let left_panel = make_panel(ctx, &appwide_panel.top_panel);
37
38        Box::new(Self {
39            appwide_panel,
40            left_panel,
41            id: None,
42            custom: None,
43            draw_custom: Drawable::empty(ctx),
44            edit: EditPolygon::new(ctx, Vec::new(), false),
45            lasso: None,
46            name,
47            quadtree: make_quadtree(app),
48            queued_recalculate: false,
49        })
50    }
51
52    pub fn edit_existing(
53        ctx: &mut EventCtx,
54        app: &mut App,
55        name: String,
56        id: NeighbourhoodID,
57        custom: CustomBoundary,
58    ) -> Box<dyn State<App>> {
59        let appwide_panel = AppwidePanel::new(ctx, app, Mode::FreehandBoundary);
60        let left_panel = make_panel(ctx, &appwide_panel.top_panel);
61        let mut state = Self {
62            appwide_panel,
63            left_panel,
64            id: Some(id),
65            custom: Some(custom),
66            draw_custom: Drawable::empty(ctx),
67            edit: EditPolygon::new(ctx, Vec::new(), false),
68            lasso: None,
69            name,
70            quadtree: make_quadtree(app),
71            queued_recalculate: false,
72        };
73        state.edit = EditPolygon::new(
74            ctx,
75            state
76                .custom
77                .as_ref()
78                .unwrap()
79                .boundary_polygon
80                .clone()
81                .into_outer_ring()
82                .into_points(),
83            false,
84        );
85        state.draw_custom = render(ctx, app, state.custom.as_ref().unwrap());
86        Box::new(state)
87    }
88
89    pub fn new_from_polygon(
90        ctx: &mut EventCtx,
91        app: &mut App,
92        name: String,
93        polygon: Polygon,
94    ) -> Box<dyn State<App>> {
95        let appwide_panel = AppwidePanel::new(ctx, app, Mode::FreehandBoundary);
96        let left_panel = make_panel(ctx, &appwide_panel.top_panel);
97        let mut state = Self {
98            appwide_panel,
99            left_panel,
100            id: None,
101            custom: None,
102            draw_custom: Drawable::empty(ctx),
103            edit: EditPolygon::new(ctx, polygon.into_outer_ring().into_points(), false),
104            lasso: None,
105            name,
106            quadtree: make_quadtree(app),
107            queued_recalculate: false,
108        };
109        state.recalculate(ctx, app);
110        Box::new(state)
111    }
112
113    fn recalculate(&mut self, ctx: &EventCtx, app: &App) {
114        self.queued_recalculate = false;
115        if let Ok(ring) = self.edit.get_ring() {
116            self.custom = Some(polygon_to_custom_boundary(
117                app,
118                ring.into_polygon(),
119                self.name.clone(),
120                &self.quadtree,
121            ));
122            self.draw_custom = render(ctx, app, self.custom.as_ref().unwrap());
123        }
124    }
125}
126
127impl State<App> for FreehandBoundary {
128    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
129        if let Some(ref mut lasso) = self.lasso {
130            if let Some(polygon) = lasso.event(ctx) {
131                let polygon = polygon.simplify(50.0);
132
133                self.lasso = None;
134                self.edit =
135                    EditPolygon::new(ctx, polygon.clone().into_outer_ring().into_points(), false);
136
137                self.recalculate(ctx, app);
138                self.left_panel = make_panel(ctx, &self.appwide_panel.top_panel);
139            }
140            return Transition::Keep;
141        }
142
143        if self.edit.event(ctx) {
144            self.queued_recalculate = true;
145        }
146        // TODO This doesn't recalculate when pressing the leafblower key
147        if self.queued_recalculate && ctx.input.left_mouse_button_released() {
148            self.recalculate(ctx, app);
149        }
150
151        // PreserveState doesn't matter, can't switch proposals in FreehandBoundary anyway
152        if let Some(t) =
153            self.appwide_panel
154                .event(ctx, app, &crate::save::PreserveState::Route, help)
155        {
156            return t;
157        }
158        if let Some(t) = app
159            .session
160            .layers
161            .event(ctx, &app.cs, Mode::FreehandBoundary, None)
162        {
163            return t;
164        }
165        if let Outcome::Clicked(x) = self.left_panel.event(ctx) {
166            match x.as_ref() {
167                "Cancel" => {
168                    return Transition::Replace(pages::PickArea::new_state(ctx, app));
169                }
170                "Confirm" => {
171                    if let Some(custom) = self.custom.take() {
172                        let id = if let Some(id) = self.id {
173                            // Overwrite the existing one
174                            mut_partitioning!(app).custom_boundaries.insert(id, custom);
175                            id
176                        } else {
177                            mut_partitioning!(app).add_custom_boundary(custom)
178                        };
179                        // TODO Clicking is weird, acts like we click load proposal
180                        return Transition::Replace(pages::DesignLTN::new_state(ctx, app, id));
181                    }
182                }
183                "Select freehand" => {
184                    self.lasso = Some(Lasso::new(Distance::meters(1.0)));
185                    self.left_panel = make_panel_for_lasso(ctx, &self.appwide_panel.top_panel);
186                }
187                _ => unreachable!(),
188            }
189        }
190
191        Transition::Keep
192    }
193
194    fn draw(&self, g: &mut GfxCtx, app: &App) {
195        self.appwide_panel.draw(g);
196        self.left_panel.draw(g);
197        app.session.layers.draw(g, app);
198        if let Some(ref lasso) = self.lasso {
199            lasso.draw(g);
200        }
201        self.edit.draw(g);
202        g.redraw(&self.draw_custom);
203    }
204}
205
206fn make_panel(ctx: &mut EventCtx, top_panel: &Panel) -> Panel {
207    crate::components::LeftPanel::builder(
208        ctx,
209        top_panel,
210        Widget::col(vec![
211            Line("Draw custom neighbourhood boundary")
212                .small_heading()
213                .into_widget(ctx),
214            ctx.style()
215                .btn_outline
216                .icon_text("system/assets/tools/select.svg", "Select freehand")
217                .hotkey(Key::F)
218                .build_def(ctx),
219            Text::from_all(vec![
220                Line("Press "),
221                Line(Key::D.describe()).fg(ctx.style().text_hotkey_color),
222                Line(" to displace points in some direction"),
223            ])
224            .into_widget(ctx),
225            Widget::row(vec![
226                ctx.style()
227                    .btn_solid_primary
228                    .text("Confirm")
229                    .hotkey(Key::Enter)
230                    .build_def(ctx),
231                ctx.style()
232                    .btn_solid_destructive
233                    .text("Cancel")
234                    .hotkey(Key::Escape)
235                    .build_def(ctx),
236            ]),
237        ]),
238    )
239    .build(ctx)
240}
241
242fn make_panel_for_lasso(ctx: &mut EventCtx, top_panel: &Panel) -> Panel {
243    crate::components::LeftPanel::builder(
244        ctx,
245        top_panel,
246        Widget::col(vec![
247            "Draw a custom boundary for a neighbourhood"
248                .text_widget(ctx)
249                .centered_vert(),
250            Text::from_all(vec![
251                Line("Click and drag").fg(ctx.style().text_hotkey_color),
252                Line(" to sketch the boundary of this neighbourhood"),
253            ])
254            .into_widget(ctx),
255        ]),
256    )
257    .build(ctx)
258}
259
260fn help() -> Vec<&'static str> {
261    vec![
262        "Draw neighbourhood boundaries here freeform.",
263        "This is still experimental, but is useful when the regular Adjust Boundary tool fails.",
264    ]
265}
266
267fn polygon_to_custom_boundary(
268    app: &App,
269    boundary_polygon: Polygon,
270    name: String,
271    quadtree: &QuadTree<RoadID>,
272) -> CustomBoundary {
273    let map = &app.per_map.map;
274
275    // Find all roads inside the polygon at least partly
276    let mut interior_roads = BTreeSet::new();
277    for id in quadtree.query_bbox(boundary_polygon.get_bounds()) {
278        let r = map.get_r(id);
279        if boundary_polygon.intersects_polyline(&r.center_pts) && crate::is_driveable(r, map) {
280            interior_roads.insert(r.id);
281        }
282    }
283
284    // Border intersections are connected to these roads, but not inside the polygon
285    let mut borders = BTreeSet::new();
286    for r in &interior_roads {
287        for i in map.get_r(*r).endpoints() {
288            if !boundary_polygon.contains_pt(map.get_i(i).polygon.center()) {
289                borders.insert(i);
290            }
291        }
292    }
293
294    CustomBoundary {
295        name,
296        boundary_polygon,
297        borders,
298        interior_roads,
299    }
300}
301
302fn render(ctx: &EventCtx, app: &App, custom: &CustomBoundary) -> Drawable {
303    let mut batch = GeomBatch::new();
304
305    for i in &custom.borders {
306        batch.push(Color::BLACK, app.per_map.map.get_i(*i).polygon.clone());
307    }
308
309    for r in &custom.interior_roads {
310        batch.push(
311            Color::GREEN.alpha(0.5),
312            app.per_map.map.get_r(*r).get_thick_polygon(),
313        );
314    }
315
316    ctx.upload(batch)
317}
318
319fn make_quadtree(app: &App) -> QuadTree<RoadID> {
320    QuadTree::bulk_load(
321        app.per_map
322            .map
323            .all_roads()
324            .into_iter()
325            .map(|r| r.center_pts.get_bounds().as_bbox(r.id))
326            .collect(),
327    )
328}