game/ungap/trip/
results.rs

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
17/// A temporary structure that the caller should unpack and use as needed.
18pub 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    // It's tempting to glue together all of the paths. But since some waypoints might force the
31    // path to double back on itself, rendering the path as a single PolyLine would break.
32    paths: Vec<(Path, Option<PolyLine>)>,
33    // Match each polyline to the index in paths
34    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    /// "main" is determined by `app.session.routing_preferences`
57    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        // Only used for alts
98        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                                // TODO It'd be nicer to build up contiguous subsets of the path
142                                // that're stressful, and use trace
143                                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                // No effect. Maybe these should be toggles, so people can pan the map around and
258                // see these in more detail?
259                "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                    // Find this position on the trip
274                    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                    // Find the total distance along the trip
303                    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                            // The LinePlot doesn't hold onto the original Series, so it can't help
310                            // us figure out elevation here. Let's match this point to the original
311                            // path and guess elevation ourselves...
312                            let map = &app.primary.map;
313                            let elevation = match self.paths[idx]
314                                .0
315                                .get_step_at_dist_along(map, dist_here)
316                                // We often seem to slightly exceed the total length, so just clamp
317                                // here...
318                                .unwrap_or_else(|_| self.paths[idx].0.last_step())
319                            {
320                                PathStep::Lane(l) | PathStep::ContraflowLane(l) => {
321                                    // TODO Interpolate
322                                    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}