map_gui/tools/
waypoints.rs

1use geom::{Circle, Distance, FindClosest, Pt2D};
2use map_model::{LaneID, PathConstraints, Position};
3use synthpop::TripEndpoint;
4use widgetry::mapspace::{ObjectID, World, WorldOutcome};
5use widgetry::{
6    Color, ControlState, CornerRounding, DragDrop, EventCtx, GeomBatch, Image, Key, Line, Outcome,
7    RewriteColor, StackAxis, Text, Widget,
8};
9
10use crate::AppLike;
11
12/// Click to add waypoints, drag them, see the list on a panel and delete them. The caller owns the
13/// Panel and the World, since there's probably more stuff there too.
14pub struct InputWaypoints {
15    waypoints: Vec<Waypoint>,
16    snap_to_main_endpts: FindClosest<TripEndpoint>,
17    snap_to_road_endpts: FindClosest<LaneID>,
18    max_waypts: Option<usize>,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub struct WaypointID(usize);
23impl ObjectID for WaypointID {}
24
25struct Waypoint {
26    at: TripEndpoint,
27    label: String,
28    center: Pt2D,
29}
30
31impl InputWaypoints {
32    /// Allows any number of waypoints
33    pub fn new(app: &dyn AppLike, snap_to_lanes_for: Vec<PathConstraints>) -> InputWaypoints {
34        let map = app.map();
35
36        let mut snap_to_main_endpts = FindClosest::new();
37        for i in map.all_intersections() {
38            if i.is_border() {
39                snap_to_main_endpts.add_polygon(TripEndpoint::Border(i.id), &i.polygon);
40            }
41        }
42        for b in map.all_buildings() {
43            snap_to_main_endpts.add_polygon(TripEndpoint::Building(b.id), &b.polygon);
44        }
45
46        let mut snap_to_road_endpts = FindClosest::new();
47        for l in map.all_lanes() {
48            if snap_to_lanes_for.iter().any(|c| c.can_use(l, map)) {
49                snap_to_road_endpts.add_polygon(l.id, &l.get_thick_polygon());
50            }
51        }
52
53        InputWaypoints {
54            waypoints: Vec::new(),
55            snap_to_main_endpts,
56            snap_to_road_endpts,
57            max_waypts: None,
58        }
59    }
60
61    /// Only allow drawing routes with 2 waypoints. If a route is loaded with more than that, it
62    /// can be modified.
63    pub fn new_max_2(app: &dyn AppLike, snap_to_lanes_for: Vec<PathConstraints>) -> Self {
64        let mut i = Self::new(app, snap_to_lanes_for);
65        i.max_waypts = Some(2);
66        i
67    }
68
69    /// The caller should call `rebuild_world` after this
70    pub fn overwrite(&mut self, app: &dyn AppLike, waypoints: Vec<TripEndpoint>) {
71        self.waypoints.clear();
72        for at in waypoints {
73            self.waypoints.push(Waypoint::new(app, at));
74        }
75    }
76
77    pub fn get_panel_widget(&self, ctx: &mut EventCtx) -> Widget {
78        let mut drag_drop = DragDrop::new(ctx, "waypoint cards", StackAxis::Vertical);
79        let mut delete_buttons = Vec::new();
80
81        for (idx, waypt) in self.waypoints.iter().enumerate() {
82            let text = get_waypoint_text(idx);
83            let icon = {
84                let text = Text::from(Line(text).fg(Color::WHITE).bold_body());
85                let batch = text.render(ctx);
86                let bounds = batch.get_bounds();
87                let image = Image::from_batch(batch, bounds)
88                    .untinted()
89                    .bg_color(self.get_waypoint_color(idx))
90                    .padding(10)
91                    .dims(16)
92                    .corner_rounding(CornerRounding::FullyRounded);
93                image
94            };
95
96            let waypoint = ctx
97                .style()
98                .btn_plain
99                .text(&waypt.label)
100                .image(icon)
101                .padding(10);
102
103            let build_batch = |control_state: ControlState| {
104                let batch = waypoint.batch(ctx, control_state);
105                let bounds = batch.get_bounds();
106                let image = Image::from_batch(batch, bounds).untinted();
107                image.build_batch(ctx).unwrap()
108            };
109
110            let (default_batch, bounds) = build_batch(ControlState::Default);
111            let (hovering_batch, _) = build_batch(ControlState::Hovered);
112            let (selected_batch, _) = build_batch(ControlState::Hovered);
113
114            drag_drop.push_card(
115                idx,
116                bounds.into(),
117                default_batch,
118                hovering_batch,
119                selected_batch,
120            );
121
122            delete_buttons.push(
123                ctx.style()
124                    .btn_close()
125                    .override_style(&ctx.style().btn_plain_destructive)
126                    .build_widget(ctx, &format!("delete waypoint {}", idx)),
127            );
128        }
129
130        Widget::row(vec![
131            drag_drop.into_widget(ctx),
132            Widget::custom_col(delete_buttons)
133                .evenly_spaced()
134                .margin_above(8)
135                .margin_below(8),
136        ])
137    }
138
139    pub fn get_waypoints(&self) -> Vec<TripEndpoint> {
140        self.waypoints.iter().map(|w| w.at).collect()
141    }
142
143    pub fn len(&self) -> usize {
144        self.waypoints.len()
145    }
146
147    /// If the outcome from the panel or world isn't used by the caller, pass it along here. When this
148    /// returns true, something has changed, so the caller may want to update their view of the
149    /// route and call `get_panel_widget` and `rebuild_world` again.
150    pub fn event(
151        &mut self,
152        app: &dyn AppLike,
153        panel_outcome: Outcome,
154        world_outcome: WorldOutcome<WaypointID>,
155    ) -> bool {
156        match world_outcome {
157            WorldOutcome::ClickedFreeSpace(pt) => {
158                if Some(self.waypoints.len()) == self.max_waypts {
159                    return false;
160                }
161                if let Some(at) = self.snap(app, pt) {
162                    self.waypoints.push(Waypoint::new(app, at));
163                    return true;
164                }
165                return false;
166            }
167            WorldOutcome::Dragging {
168                obj: WaypointID(idx),
169                cursor,
170                ..
171            } => {
172                if let Some(at) = self.snap(app, cursor) {
173                    if self.waypoints[idx].at != at {
174                        self.waypoints[idx] = Waypoint::new(app, at);
175                        return true;
176                    }
177                }
178            }
179            WorldOutcome::Keypress("delete", WaypointID(idx)) => {
180                self.waypoints.remove(idx);
181                return true;
182            }
183            _ => {}
184        }
185
186        match panel_outcome {
187            Outcome::Clicked(x) => {
188                if let Some(x) = x.strip_prefix("delete waypoint ") {
189                    let idx = x.parse::<usize>().unwrap();
190                    self.waypoints.remove(idx);
191                    return true;
192                } else {
193                    panic!("Unknown InputWaypoints click {}", x);
194                }
195            }
196            Outcome::DragDropReleased(_, old_idx, new_idx) => {
197                self.waypoints.swap(old_idx, new_idx);
198                // The order field is baked in, so calculate everything again from scratch
199                let waypoints = self.get_waypoints();
200                self.overwrite(app, waypoints);
201                return true;
202            }
203            _ => {}
204        }
205
206        false
207    }
208
209    fn snap(&self, app: &dyn AppLike, cursor: Pt2D) -> Option<TripEndpoint> {
210        // Prefer buildings and borders. Some maps have few buildings, or have roads not near
211        // buildings, so snap to positions along lanes as a fallback.
212        let threshold = Distance::meters(30.0);
213        if let Some((at, _)) = self.snap_to_main_endpts.closest_pt(cursor, threshold) {
214            return Some(at);
215        }
216        let (l, _) = self.snap_to_road_endpts.closest_pt(cursor, threshold)?;
217        // Try to find the most appropriate position along the lane
218        let pl = &app.map().get_l(l).lane_center_pts;
219        Some(TripEndpoint::SuddenlyAppear(
220            if let Some((dist, _)) = pl.dist_along_of_point(pl.project_pt(cursor)) {
221                // Rounding error can snap slightly past the end
222                Position::new(l, dist.min(pl.length()))
223            } else {
224                // If we couldn't figure it out for some reason, just use the middle
225                Position::new(l, pl.length() / 2.0)
226            },
227        ))
228    }
229
230    pub fn get_waypoint_color(&self, idx: usize) -> Color {
231        let total_waypoints = self.waypoints.len();
232        match idx {
233            0 => Color::BLACK,
234            idx if idx == total_waypoints - 1 => Color::PINK,
235            _ => [Color::BLUE, Color::ORANGE, Color::PURPLE][idx % 3],
236        }
237    }
238
239    /// The caller is responsible for calling `initialize_hover` and `rebuilt_during_drag`.
240    pub fn rebuild_world<T: ObjectID, F: Fn(WaypointID) -> T>(
241        &self,
242        ctx: &mut EventCtx,
243        world: &mut World<T>,
244        wrap_id: F,
245        zorder: usize,
246    ) {
247        for (idx, waypoint) in self.waypoints.iter().enumerate() {
248            let hitbox = Circle::new(waypoint.center, Distance::meters(30.0)).to_polygon();
249            let color = self.get_waypoint_color(idx);
250
251            let mut draw_normal = GeomBatch::new();
252            draw_normal.push(color, hitbox.clone());
253            draw_normal.append(
254                Text::from(Line(get_waypoint_text(idx).to_string()).fg(Color::WHITE))
255                    .render(ctx)
256                    .centered_on(waypoint.center),
257            );
258
259            world
260                .add(wrap_id(WaypointID(idx)))
261                .hitbox(hitbox)
262                .zorder(zorder)
263                .draw(draw_normal)
264                .draw_hover_rewrite(RewriteColor::Change(color, Color::BLUE.alpha(0.5)))
265                .hotkey(Key::Backspace, "delete")
266                .draggable()
267                .build(ctx);
268        }
269    }
270}
271
272impl Waypoint {
273    fn new(app: &dyn AppLike, at: TripEndpoint) -> Waypoint {
274        let map = app.map();
275        let (center, label) = match at {
276            TripEndpoint::Building(b) => {
277                let b = map.get_b(b);
278                (b.polygon.center(), b.address.clone())
279            }
280            TripEndpoint::Border(i) => {
281                let i = map.get_i(i);
282                (
283                    i.polygon.center(),
284                    i.name(app.opts().language.as_ref(), map),
285                )
286            }
287            TripEndpoint::SuddenlyAppear(pos) => (
288                pos.pt(map),
289                map.get_parent(pos.lane())
290                    .get_name(app.opts().language.as_ref()),
291            ),
292        };
293        Waypoint { at, label, center }
294    }
295}
296
297fn get_waypoint_text(idx: usize) -> char {
298    char::from_u32('A' as u32 + idx as u32).unwrap()
299}