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 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 if self.queued_recalculate && ctx.input.left_mouse_button_released() {
148 self.recalculate(ctx, app);
149 }
150
151 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 mut_partitioning!(app).custom_boundaries.insert(id, custom);
175 id
176 } else {
177 mut_partitioning!(app).add_custom_boundary(custom)
178 };
179 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 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 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}