1use geom::{Circle, Distance, FindClosest};
2use map_model::{IntersectionID, Map, PathConstraints, RoadID};
3use widgetry::mapspace::DrawUnzoomedShapes;
4use widgetry::{Color, EventCtx, GfxCtx, TextExt, Widget};
5
6use crate::app::App;
7
8const INTERSECTON_RADIUS: Distance = Distance::const_meters(10.0);
9
10pub struct RouteSketcher {
12 snap_to_intersections: FindClosest<IntersectionID>,
13 route: Route,
14 mode: Mode,
15 preview: DrawUnzoomedShapes,
16}
17
18impl RouteSketcher {
19 pub fn new(app: &App) -> RouteSketcher {
20 let mut snap_to_intersections = FindClosest::new();
21 for i in app.primary.map.all_intersections() {
22 snap_to_intersections.add_polygon(i.id, &i.polygon);
23 }
24
25 RouteSketcher {
26 snap_to_intersections,
27 route: Route::new(),
28 mode: Mode::Neutral,
29 preview: DrawUnzoomedShapes::empty(),
30 }
31 }
32
33 fn mouseover_i(&self, ctx: &EventCtx) -> Option<IntersectionID> {
34 let pt = ctx.canvas.get_cursor_in_map_space()?;
35 let threshold = Distance::meters(30.0) / ctx.canvas.cam_zoom;
39 let (i, _) = self.snap_to_intersections.closest_pt(pt, threshold)?;
40 if self.route.waypoints.len() > 1
42 && !matches!(self.mode, Mode::Dragging { .. })
43 && !self.route.full_path.contains(&i)
44 {
45 return None;
46 }
47 Some(i)
48 }
49
50 fn update_mode(&mut self, ctx: &mut EventCtx, app: &App) {
51 match self.mode {
52 Mode::Neutral => {
53 ctx.canvas_movement();
54 if ctx.redo_mouseover() {
55 if let Some(i) = self.mouseover_i(ctx) {
56 self.mode = Mode::Hovering(i);
57 }
58 }
59 }
60 Mode::Hovering(i) => {
61 if ctx.input.left_mouse_button_pressed() {
62 if let Some(idx) = self.route.idx(i) {
63 self.mode = Mode::Dragging { idx, at: i };
64 return;
65 }
66 }
67
68 ctx.canvas_movement();
69
70 if ctx.normal_left_click() {
71 self.route.add_waypoint(app, i);
72 return;
73 }
74
75 if ctx.redo_mouseover() {
76 if let Some(i) = self.mouseover_i(ctx) {
77 self.mode = Mode::Hovering(i);
78 } else {
79 self.mode = Mode::Neutral;
80 }
81 }
82 }
83 Mode::Dragging { idx, at } => {
84 if ctx.input.left_mouse_button_released() {
85 self.mode = Mode::Hovering(at);
86 return;
87 }
88
89 if ctx.redo_mouseover() {
90 if let Some(i) = self.mouseover_i(ctx) {
91 if i != at {
92 let new_idx = self.route.move_waypoint(&app.primary.map, idx, i);
93 self.mode = Mode::Dragging {
94 idx: new_idx,
95 at: i,
96 };
97 }
98 }
99 }
100 }
101 }
102 }
103
104 fn update_preview(&mut self, app: &App) {
105 let map = &app.primary.map;
106 let mut shapes = DrawUnzoomedShapes::builder();
107
108 for pair in self.route.full_path.windows(2) {
110 let r = map.get_r(map.find_road_between(pair[0], pair[1]).unwrap());
112 shapes.add_line(r.center_pts.clone(), r.get_width(), Color::RED.alpha(0.5));
113 }
114 for i in &self.route.full_path {
115 shapes.add_circle(
116 map.get_i(*i).polygon.center(),
117 INTERSECTON_RADIUS,
118 Color::BLUE.alpha(0.5),
119 );
120 }
121
122 if let Mode::Hovering(i) = self.mode {
124 shapes.add_circle(
125 map.get_i(i).polygon.center(),
126 INTERSECTON_RADIUS,
127 Color::BLUE,
128 );
129 if self.route.waypoints.len() == 1 {
130 if let Some((roads, intersections)) =
131 map.simple_path_btwn_v2(self.route.waypoints[0], i, PathConstraints::Car)
132 {
133 for r in roads {
134 let r = map.get_r(r);
135 shapes.add_line(
136 r.center_pts.clone(),
137 r.get_width(),
138 Color::BLUE.alpha(0.5),
139 );
140 }
141 for i in intersections {
142 shapes.add_circle(
143 map.get_i(i).polygon.center(),
144 INTERSECTON_RADIUS,
145 Color::BLUE.alpha(0.5),
146 );
147 }
148 }
149 }
150 }
151 if let Mode::Dragging { at, .. } = self.mode {
152 shapes.add_circle(
153 map.get_i(at).polygon.center(),
154 INTERSECTON_RADIUS,
155 Color::BLUE,
156 );
157 }
158
159 self.preview = shapes.build();
160 }
161
162 pub fn get_widget_to_describe(&self, ctx: &mut EventCtx) -> Widget {
163 Widget::col(vec![
164 if self.route.waypoints.is_empty() {
165 "Click to start a route"
166 } else if self.route.waypoints.len() == 1 {
167 "Click to end the route"
168 } else {
169 "Click and drag to adjust the route"
170 }
171 .text_widget(ctx),
172 if self.route.waypoints.len() > 1 {
173 format!(
174 "{} road segments selected",
175 self.route.full_path.len().max(1) - 1
176 )
177 .text_widget(ctx)
178 } else {
179 Widget::nothing()
180 },
181 if self.route.waypoints.is_empty() {
182 Widget::nothing()
183 } else {
184 ctx.style()
185 .btn_plain_destructive
186 .text("Start over")
187 .build_def(ctx)
188 },
189 ])
190 }
191
192 pub fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> bool {
194 let orig_route = self.route.clone();
195 let orig_mode = self.mode.clone();
196 self.update_mode(ctx, app);
197 if self.route != orig_route || self.mode != orig_mode {
198 self.update_preview(app);
199 self.route != orig_route
202 } else {
203 false
204 }
205 }
206
207 pub fn on_click(&mut self, x: &str) -> bool {
209 if x == "Start over" {
210 self.route = Route::new();
211 self.mode = Mode::Neutral;
212 self.preview = DrawUnzoomedShapes::empty();
213 return true;
214 }
215 false
216 }
217
218 pub fn draw(&self, g: &mut GfxCtx) {
219 self.preview.draw(g);
220 if matches!(self.mode, Mode::Dragging { .. }) {
221 if let Some(pt) = g.canvas.get_cursor_in_map_space() {
222 g.draw_polygon(
223 Color::BLUE.alpha(0.5),
224 Circle::new(pt, INTERSECTON_RADIUS).to_polygon(),
225 );
226 }
227 }
228 }
229
230 pub fn all_roads(&self, app: &App) -> Vec<RoadID> {
231 let mut roads = Vec::new();
232 for pair in self.route.full_path.windows(2) {
233 roads.push(app.primary.map.find_road_between(pair[0], pair[1]).unwrap());
235 }
236 roads
237 }
238
239 pub fn is_route_started(&self) -> bool {
241 !self.route.waypoints.is_empty()
242 }
243
244 pub fn is_route_valid(&self) -> bool {
246 self.route.waypoints.len() > 1
247 }
248}
249
250#[derive(Clone, PartialEq)]
251struct Route {
252 waypoints: Vec<IntersectionID>,
253 full_path: Vec<IntersectionID>,
254}
255
256impl Route {
257 fn new() -> Route {
258 Route {
259 waypoints: Vec::new(),
260 full_path: Vec::new(),
261 }
262 }
263
264 fn add_waypoint(&mut self, app: &App, i: IntersectionID) {
265 if self.waypoints.is_empty() {
266 self.waypoints.push(i);
267 assert!(self.full_path.is_empty());
268 self.full_path.push(i);
269 } else if self.waypoints.len() == 1 && i != self.waypoints[0] {
270 if let Some((_, intersections)) =
274 app.primary
275 .map
276 .simple_path_btwn_v2(self.waypoints[0], i, PathConstraints::Car)
277 {
278 self.waypoints.push(i);
279 assert_eq!(self.full_path.len(), 1);
280 self.full_path = intersections;
281 }
282 }
283 }
285
286 fn idx(&self, i: IntersectionID) -> Option<usize> {
287 self.full_path.iter().position(|x| *x == i)
288 }
289
290 fn move_waypoint(&mut self, map: &Map, full_idx: usize, new_i: IntersectionID) -> usize {
292 let old_i = self.full_path[full_idx];
293
294 if self.waypoints.len() == 1 {
296 assert_eq!(self.waypoints[0], old_i);
297 self.waypoints = vec![new_i];
298 self.full_path = vec![new_i];
299 return 0;
300 }
301
302 let orig = self.clone();
303
304 if let Some(way_idx) = self.waypoints.iter().position(|x| *x == old_i) {
306 self.waypoints[way_idx] = new_i;
307 } else {
308 for i in &self.full_path[full_idx..] {
310 if let Some(way_idx) = self.waypoints.iter().position(|x| x == i) {
311 self.waypoints.insert(way_idx, new_i);
313 break;
314 }
315 }
316 }
317
318 self.full_path.clear();
321 for pair in self.waypoints.windows(2) {
322 if let Some((_, intersections)) =
323 map.simple_path_btwn_v2(pair[0], pair[1], PathConstraints::Car)
324 {
325 self.full_path.pop();
326 self.full_path.extend(intersections);
327 } else {
328 *self = orig;
330 return full_idx;
331 }
332 }
333 self.idx(new_i).unwrap()
334 }
335}
336
337#[derive(Clone, PartialEq)]
338enum Mode {
339 Neutral,
340 Hovering(IntersectionID),
341 Dragging { idx: usize, at: IntersectionID },
342}