game/common/
route_sketcher.rs

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
10// TODO Supercede RoadSelector, probably..
11pub 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        // When zoomed really far out, it's harder to click small intersections, so snap more
36        // aggressively. Note this should always be a larger hitbox than how the waypoint circles
37        // are drawn.
38        let threshold = Distance::meters(30.0) / ctx.canvas.cam_zoom;
39        let (i, _) = self.snap_to_intersections.closest_pt(pt, threshold)?;
40        // After we have a path started, only snap to points on the path to drag them
41        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        // Draw the confirmed route
109        for pair in self.route.full_path.windows(2) {
110            // TODO Inefficient!
111            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        // Draw the current operation
123        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    /// True if the route changed
193    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            // Only route changes count as a change for the caller, not just hovering on something
200            // different
201            self.route != orig_route
202        } else {
203            false
204        }
205    }
206
207    /// True if something changed. False if this component doesn't even handle that kind of click.
208    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            // TODO Inefficient!
234            roads.push(app.primary.map.find_road_between(pair[0], pair[1]).unwrap());
235        }
236        roads
237    }
238
239    /// Has the user even picked a start point?
240    pub fn is_route_started(&self) -> bool {
241        !self.route.waypoints.is_empty()
242    }
243
244    /// Has the user specified a full route?
245    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            // Route for cars, because we're doing this to transform roads meant for cars. We could
271            // equivalently use Bike in most cases, except for highways where biking is currently
272            // banned. This tool could be used to carve out space and allow that.
273            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        // If there's already two waypoints, can't add more -- can only drag things.
284    }
285
286    fn idx(&self, i: IntersectionID) -> Option<usize> {
287        self.full_path.iter().position(|x| *x == i)
288    }
289
290    // Returns the new full_path index
291    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        // Edge case when we've placed just one point, then try to drag it
295        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        // Move an existing waypoint?
305        if let Some(way_idx) = self.waypoints.iter().position(|x| *x == old_i) {
306            self.waypoints[way_idx] = new_i;
307        } else {
308            // Find the next waypoint after this intersection
309            for i in &self.full_path[full_idx..] {
310                if let Some(way_idx) = self.waypoints.iter().position(|x| x == i) {
311                    // Insert a new waypoint before this
312                    self.waypoints.insert(way_idx, new_i);
313                    break;
314                }
315            }
316        }
317
318        // Recalculate the full path. We could be more efficient and just fix up the part that's
319        // changed, but eh.
320        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                // Moving the waypoint broke the path, just revert.
329                *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}