1use std::cmp::Ordering;
2
3use geom::{Circle, Distance, Duration, FindClosest, PolyLine, Polygon};
4use map_gui::tools::{cmp_dist, cmp_duration};
5use map_model::{DrivingSide, Path, PathStep, PathfinderCaching, NORMAL_LANE_THICKNESS};
6use synthpop::{TripEndpoint, TripMode};
7use widgetry::mapspace::{ToggleZoomed, ToggleZoomedBuilder};
8use widgetry::tools::PopupMsg;
9use widgetry::{
10 Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, LinePlot, Outcome, Panel, PlotOptions,
11 ScreenDims, Series, Text, Widget,
12};
13
14use super::{before_after_button, RoutingPreferences};
15use crate::app::{App, Transition};
16
17pub struct BuiltRoute {
19 pub details: RouteDetails,
20 pub details_widget: Widget,
21 pub draw: ToggleZoomedBuilder,
22 pub hitboxes: Vec<Polygon>,
23 pub tooltip_for_alt: Option<Text>,
24}
25
26pub struct RouteDetails {
27 pub preferences: RoutingPreferences,
28 pub stats: RouteStats,
29
30 paths: Vec<(Path, Option<PolyLine>)>,
33 closest_path_segment: FindClosest<usize>,
35
36 hover_on_line_plot: Option<(Distance, Drawable)>,
37 hover_on_route_tooltip: Option<Text>,
38
39 draw_high_stress: Drawable,
40 draw_traffic_signals: Drawable,
41 draw_unprotected_turns: Drawable,
42}
43
44#[derive(PartialEq)]
45pub struct RouteStats {
46 total_distance: Distance,
47 dist_along_high_stress_roads: Distance,
48 total_time: Duration,
49 num_traffic_signals: usize,
50 num_unprotected_turns: usize,
51 total_up: Distance,
52 total_down: Distance,
53}
54
55impl RouteDetails {
56 pub fn main_route(ctx: &mut EventCtx, app: &App, waypoints: Vec<TripEndpoint>) -> BuiltRoute {
58 RouteDetails::new_route(
59 ctx,
60 app,
61 waypoints,
62 Color::RED,
63 None,
64 app.session.routing_preferences,
65 )
66 }
67
68 pub fn alt_route(
69 ctx: &mut EventCtx,
70 app: &App,
71 waypoints: Vec<TripEndpoint>,
72 main: &RouteDetails,
73 preferences: RoutingPreferences,
74 ) -> BuiltRoute {
75 let mut built = RouteDetails::new_route(
76 ctx,
77 app,
78 waypoints,
79 Color::grey(0.3),
80 Some(Color::RED),
81 preferences,
82 );
83 built.tooltip_for_alt = Some(compare_routes(
84 app,
85 &main.stats,
86 &built.details.stats,
87 preferences,
88 ));
89 built
90 }
91
92 fn new_route(
93 ctx: &mut EventCtx,
94 app: &App,
95 waypoints: Vec<TripEndpoint>,
96 route_color: Color,
97 outline_color: Option<Color>,
99 preferences: RoutingPreferences,
100 ) -> BuiltRoute {
101 let mut draw_route = ToggleZoomed::builder();
102 let mut hitboxes = Vec::new();
103 let mut draw_high_stress = GeomBatch::new();
104 let mut draw_traffic_signals = GeomBatch::new();
105 let mut draw_unprotected_turns = GeomBatch::new();
106 let map = &app.primary.map;
107
108 let mut total_distance = Distance::ZERO;
109 let mut total_time = Duration::ZERO;
110
111 let mut dist_along_high_stress_roads = Distance::ZERO;
112 let mut num_traffic_signals = 0;
113 let mut num_unprotected_turns = 0;
114
115 let mut elevation_pts: Vec<(Distance, Distance)> = Vec::new();
116 let mut current_dist = Distance::ZERO;
117
118 let mut paths = Vec::new();
119 let mut closest_path_segment = FindClosest::new();
120
121 let routing_params = preferences.routing_params();
122
123 for pair in waypoints.windows(2) {
124 if let Some(path) = TripEndpoint::path_req(pair[0], pair[1], TripMode::Bike, map)
125 .and_then(|req| {
126 map.pathfind_with_params(req, &routing_params, PathfinderCaching::CacheDijkstra)
127 .ok()
128 })
129 {
130 total_distance += path.total_length();
131 total_time += path.estimate_duration(map, Some(map_model::MAX_BIKE_SPEED));
132
133 for step in path.get_steps() {
134 let this_pl = step.as_traversable().get_polyline(map);
135 match step {
136 PathStep::Lane(l) | PathStep::ContraflowLane(l) => {
137 let road = map.get_parent(*l);
138 if road.high_stress_for_bikes(map, road.lanes[l.offset].dir) {
139 dist_along_high_stress_roads += this_pl.length();
140
141 draw_high_stress.push(
144 Color::YELLOW,
145 this_pl.make_polygons(5.0 * NORMAL_LANE_THICKNESS),
146 );
147 }
148 }
149 PathStep::Turn(t) | PathStep::ContraflowTurn(t) => {
150 let i = map.get_i(t.parent);
151 elevation_pts.push((current_dist, i.elevation));
152 if i.is_traffic_signal() {
153 num_traffic_signals += 1;
154 draw_traffic_signals.push(Color::YELLOW, i.polygon.clone());
155 }
156 if map.is_unprotected_turn(
157 t.src.road,
158 t.dst.road,
159 map.get_t(*t).turn_type,
160 ) {
161 num_unprotected_turns += 1;
162 draw_unprotected_turns.push(Color::YELLOW, i.polygon.clone());
163 }
164 }
165 }
166 current_dist += this_pl.length();
167 }
168
169 let maybe_pl = path.trace(map);
170 if let Some(ref pl) = maybe_pl {
171 let shape = pl.make_polygons(5.0 * NORMAL_LANE_THICKNESS);
172 draw_route
173 .unzoomed
174 .push(route_color.alpha(0.8), shape.clone());
175 draw_route
176 .zoomed
177 .push(route_color.alpha(0.5), shape.clone());
178
179 hitboxes.push(shape);
180
181 if let Some(color) = outline_color {
182 if let Some(outline) =
183 pl.to_thick_boundary(5.0 * NORMAL_LANE_THICKNESS, NORMAL_LANE_THICKNESS)
184 {
185 draw_route.unzoomed.push(color, outline.clone());
186 draw_route.zoomed.push(color.alpha(0.5), outline);
187 }
188 }
189
190 closest_path_segment.add(paths.len(), pl.points());
191 }
192 paths.push((path, maybe_pl));
193 }
194 }
195
196 let mut total_up = Distance::ZERO;
197 let mut total_down = Distance::ZERO;
198 for pair in elevation_pts.windows(2) {
199 let dy = pair[1].1 - pair[0].1;
200 if dy < Distance::ZERO {
201 total_down -= dy;
202 } else {
203 total_up += dy;
204 }
205 }
206 let stats = RouteStats {
207 total_distance,
208 dist_along_high_stress_roads,
209 total_time,
210 num_traffic_signals,
211 num_unprotected_turns,
212 total_up,
213 total_down,
214 };
215
216 let details_widget = make_detail_widget(ctx, app, &stats, elevation_pts);
217
218 BuiltRoute {
219 details: RouteDetails {
220 preferences,
221 draw_high_stress: ctx.upload(draw_high_stress),
222 draw_traffic_signals: ctx.upload(draw_traffic_signals),
223 draw_unprotected_turns: ctx.upload(draw_unprotected_turns),
224 paths,
225 closest_path_segment,
226 hover_on_line_plot: None,
227 hover_on_route_tooltip: None,
228 stats,
229 },
230 details_widget,
231 draw: draw_route,
232 hitboxes,
233 tooltip_for_alt: None,
234 }
235 }
236
237 pub fn event(
238 &mut self,
239 ctx: &mut EventCtx,
240 app: &App,
241 outcome: &Outcome,
242 panel: &mut Panel,
243 ) -> Option<Transition> {
244 if let Outcome::Clicked(x) = outcome {
245 match x.as_ref() {
246 "high-stress roads" => {
247 return Some(Transition::Push(PopupMsg::new_state(
248 ctx,
249 "High-stress roads",
250 vec![
251 "Roads are defined as high-stress for biking if:",
252 "- they're classified as arterials",
253 "- they lack dedicated space for biking",
254 ],
255 )));
256 }
257 "traffic signals" | "unprotected turns" => {
260 return Some(Transition::Keep);
261 }
262 _ => {
263 return None;
264 }
265 }
266 }
267
268 if let Some(line_plot) = panel.maybe_find::<LinePlot<Distance, Distance>>("elevation") {
269 let current_dist_along = line_plot.get_hovering().get(0).map(|pair| pair.0);
270 if self.hover_on_line_plot.as_ref().map(|pair| pair.0) != current_dist_along {
271 self.hover_on_line_plot = current_dist_along.map(|mut dist| {
272 let mut batch = GeomBatch::new();
273 for (path, maybe_pl) in &self.paths {
275 if dist > path.total_length() {
276 dist -= path.total_length();
277 continue;
278 }
279 if let Some(ref pl) = maybe_pl {
280 if let Ok((pt, _)) = pl.dist_along(dist) {
281 batch.push(
282 Color::YELLOW,
283 Circle::new(pt, Distance::meters(30.0)).to_polygon(),
284 );
285 }
286 }
287 break;
288 }
289
290 (dist, batch.upload(ctx))
291 });
292 }
293 }
294
295 if ctx.redo_mouseover() {
296 self.hover_on_route_tooltip = None;
297 if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
298 if let Some((idx, pt)) = self
299 .closest_path_segment
300 .closest_pt(pt, 10.0 * NORMAL_LANE_THICKNESS)
301 {
302 let mut dist = Distance::ZERO;
304 for (path, _) in &self.paths[0..idx] {
305 dist += path.total_length();
306 }
307 if let Some(ref pl) = self.paths[idx].1 {
308 if let Some((dist_here, _)) = pl.dist_along_of_point(pt) {
309 let map = &app.primary.map;
313 let elevation = match self.paths[idx]
314 .0
315 .get_step_at_dist_along(map, dist_here)
316 .unwrap_or_else(|_| self.paths[idx].0.last_step())
319 {
320 PathStep::Lane(l) | PathStep::ContraflowLane(l) => {
321 map.get_i(map.get_l(l).src_i).elevation
323 }
324 PathStep::Turn(t) | PathStep::ContraflowTurn(t) => {
325 map.get_i(t.parent).elevation
326 }
327 };
328 panel
329 .find_mut::<LinePlot<Distance, Distance>>("elevation")
330 .set_hovering(ctx, "Elevation", dist + dist_here, elevation);
331 self.hover_on_route_tooltip = Some(Text::from(Line(format!(
332 "Elevation: {}",
333 elevation.to_string(&app.opts.units)
334 ))));
335 }
336 }
337 }
338 }
339 }
340
341 None
342 }
343
344 pub fn draw(&self, g: &mut GfxCtx, panel: &Panel) {
345 if let Some((_, ref draw)) = self.hover_on_line_plot {
346 g.redraw(draw);
347 }
348 if let Some(ref txt) = self.hover_on_route_tooltip {
349 g.draw_mouse_tooltip(txt.clone());
350 }
351 if panel.currently_hovering() == Some(&"high-stress roads".to_string()) {
352 g.redraw(&self.draw_high_stress);
353 }
354 if panel.currently_hovering() == Some(&"traffic signals".to_string()) {
355 g.redraw(&self.draw_traffic_signals);
356 }
357 if panel.currently_hovering() == Some(&"unprotected turns".to_string()) {
358 g.redraw(&self.draw_unprotected_turns);
359 }
360 }
361}
362
363fn make_detail_widget(
364 ctx: &mut EventCtx,
365 app: &App,
366 stats: &RouteStats,
367 elevation_pts: Vec<(Distance, Distance)>,
368) -> Widget {
369 let pct_stressful = if stats.total_distance == Distance::ZERO {
370 0.0
371 } else {
372 ((stats.dist_along_high_stress_roads / stats.total_distance) * 100.0).round()
373 };
374
375 let unprotected_turn = if app.primary.map.get_config().driving_side == DrivingSide::Right {
376 "left"
377 } else {
378 "right"
379 };
380
381 Widget::col(vec![
382 Line("Route details").small_heading().into_widget(ctx),
383 before_after_button(ctx, app),
384 Text::from_all(vec![
385 Line("Distance: ").secondary(),
386 Line(stats.total_distance.to_string(&app.opts.units)),
387 ])
388 .into_widget(ctx),
389 Widget::row(vec![
390 Text::from_all(vec![
391 Line(format!(
392 " {} or {}%",
393 stats
394 .dist_along_high_stress_roads
395 .to_string(&app.opts.units),
396 pct_stressful
397 )),
398 Line(" along ").secondary(),
399 ])
400 .into_widget(ctx)
401 .centered_vert(),
402 ctx.style()
403 .btn_plain
404 .btn()
405 .label_underlined_text("high-stress roads")
406 .build_def(ctx),
407 ]),
408 Text::from_all(vec![
409 Line("Estimated time: ").secondary(),
410 Line(stats.total_time.to_string(&app.opts.units)),
411 ])
412 .into_widget(ctx),
413 Widget::row(vec![
414 Line("Traffic signals crossed: ")
415 .secondary()
416 .into_widget(ctx)
417 .centered_vert(),
418 ctx.style()
419 .btn_plain
420 .btn()
421 .label_underlined_text(stats.num_traffic_signals.to_string())
422 .build_widget(ctx, "traffic signals"),
423 ]),
424 Widget::row(vec![
425 Line(format!(
426 "Unprotected {} turns onto busy roads: ",
427 unprotected_turn
428 ))
429 .secondary()
430 .into_widget(ctx)
431 .centered_vert(),
432 ctx.style()
433 .btn_plain
434 .btn()
435 .label_underlined_text(stats.num_unprotected_turns.to_string())
436 .build_widget(ctx, "unprotected turns"),
437 ]),
438 Text::from_all(vec![
439 Line("Elevation change: ").secondary(),
440 Line(format!(
441 "{}↑, {}↓",
442 stats.total_up.to_string(&app.opts.units),
443 stats.total_down.to_string(&app.opts.units)
444 )),
445 ])
446 .into_widget(ctx),
447 LinePlot::new_widget(
448 ctx,
449 "elevation",
450 vec![Series {
451 label: "Elevation".to_string(),
452 color: Color::RED,
453 pts: elevation_pts,
454 }],
455 PlotOptions {
456 max_x: Some(stats.total_distance.round_up_for_axis()),
457 max_y: Some(app.primary.map.max_elevation().round_up_for_axis()),
458 dims: Some(ScreenDims {
459 width: 400.0,
460 height: 200.0,
461 }),
462 ..Default::default()
463 },
464 app.opts.units,
465 ),
466 ])
467}
468
469fn compare_routes(
470 app: &App,
471 main: &RouteStats,
472 alt: &RouteStats,
473 preferences: RoutingPreferences,
474) -> Text {
475 let mut txt = Text::new();
476 txt.add_line(Line(format!("Click to use {} trip", preferences.name())));
477
478 cmp_dist(
479 &mut txt,
480 app,
481 alt.total_distance - main.total_distance,
482 "shorter",
483 "longer",
484 );
485 cmp_duration(
486 &mut txt,
487 app,
488 alt.total_time - main.total_time,
489 "shorter",
490 "longer",
491 );
492 cmp_dist(
493 &mut txt,
494 app,
495 alt.dist_along_high_stress_roads - main.dist_along_high_stress_roads,
496 "less on high-stress roads",
497 "more on high-stress roads",
498 );
499
500 if alt.total_up != main.total_up || alt.total_down != main.total_down {
501 txt.add_line(Line("Elevation change: "));
502 let up = alt.total_up - main.total_up;
503 match up.cmp(&Distance::ZERO) {
504 Ordering::Less => {
505 txt.append(
506 Line(format!("{} less ↑", (-up).to_string(&app.opts.units))).fg(Color::GREEN),
507 );
508 }
509 Ordering::Greater => {
510 txt.append(
511 Line(format!("{} more ↑", up.to_string(&app.opts.units))).fg(Color::RED),
512 );
513 }
514 Ordering::Equal => {}
515 }
516 }
517
518 txt
519}