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
12pub 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 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 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 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 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 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 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 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 Position::new(l, dist.min(pl.length()))
223 } else {
224 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 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}