ltn/pages/
route_planner.rs

1use geom::{Distance, Duration, Polygon};
2use map_gui::tools::{InputWaypoints, TripManagement, TripManagementState, WaypointID};
3use map_model::{PathConstraints, PathV2, PathfinderCache};
4use synthpop::{TripEndpoint, TripMode};
5use widgetry::mapspace::World;
6use widgetry::{
7    Color, DrawBaselayer, Drawable, EventCtx, GeomBatch, GfxCtx, Image, Line, Outcome, Panel,
8    RoundedF64, Spinner, State, TextExt, TextSpan, Toggle, Widget,
9};
10
11use crate::components::{AppwidePanel, Mode};
12use crate::render::colors;
13use crate::{App, Transition};
14
15pub struct RoutePlanner {
16    appwide_panel: AppwidePanel,
17    left_panel: Panel,
18    waypoints: InputWaypoints,
19    files: TripManagement<App, RoutePlanner>,
20    world: World<WaypointID>,
21    show_main_roads: Drawable,
22    draw_driveways: Drawable,
23    draw_routes: Drawable,
24    // TODO We could save the no-filter variations map-wide
25    pathfinder_cache: PathfinderCache,
26}
27
28impl TripManagementState<App> for RoutePlanner {
29    fn mut_files(&mut self) -> &mut TripManagement<App, Self> {
30        &mut self.files
31    }
32
33    fn app_session_current_trip_name(app: &mut App) -> &mut Option<String> {
34        &mut app.per_map.current_trip_name
35    }
36
37    fn sync_from_file_management(&mut self, ctx: &mut EventCtx, app: &mut App) {
38        self.waypoints
39            .overwrite(app, self.files.current.waypoints.clone());
40        self.update_everything(ctx, app);
41    }
42}
43
44impl RoutePlanner {
45    pub fn new_state(ctx: &mut EventCtx, app: &mut App) -> Box<dyn State<App>> {
46        app.calculate_draw_all_local_road_labels(ctx);
47
48        // Fade all neighbourhood interiors, so it's very clear when a route cuts through
49        let mut batch = GeomBatch::new();
50        for info in app.partitioning().all_neighbourhoods().values() {
51            batch.push(app.cs.fade_map_dark, info.block.polygon.clone());
52        }
53
54        // Just so there's some explanation for occasionally odd building<->road snapping, show
55        // driveways very faintly
56        let mut driveways = GeomBatch::new();
57        for b in app.per_map.map.all_buildings() {
58            driveways.push(
59                Color::BLACK.alpha(0.2),
60                b.driveway_geom.make_polygons(Distance::meters(0.5)),
61            );
62        }
63
64        let mut rp = RoutePlanner {
65            appwide_panel: AppwidePanel::new(ctx, app, Mode::RoutePlanner),
66            left_panel: Panel::empty(ctx),
67            waypoints: InputWaypoints::new_max_2(app, vec![PathConstraints::Car]),
68            files: TripManagement::new(app),
69            world: World::new(),
70            show_main_roads: ctx.upload(batch),
71            draw_driveways: ctx.upload(driveways),
72            draw_routes: Drawable::empty(ctx),
73            pathfinder_cache: PathfinderCache::new(),
74        };
75
76        if let Some(current_name) = &app.per_map.current_trip_name {
77            rp.files.set_current(current_name);
78        }
79        rp.sync_from_file_management(ctx, app);
80
81        Box::new(rp)
82    }
83
84    /// Add a new trip while outside of this state and make it current, so that when we switch into
85    /// this state, it'll appear
86    pub fn add_new_trip(app: &mut App, from: TripEndpoint, to: TripEndpoint) {
87        let mut files = TripManagement::<App, RoutePlanner>::new(app);
88        files.add_new_trip(app, from, to);
89    }
90
91    // Updates the panel and draw_routes
92    fn update_everything(&mut self, ctx: &mut EventCtx, app: &mut App) {
93        self.files.autosave(app);
94        let results_widget = self.recalculate_paths(ctx, app);
95
96        let contents = Widget::col(vec![
97            Line("Plan a route").small_heading().into_widget(ctx),
98            Widget::col(vec![
99                self.files.get_panel_widget(ctx),
100                Widget::horiz_separator(ctx, 1.0),
101                self.waypoints.get_panel_widget(ctx).named("waypoints"),
102            ]),
103            if self.waypoints.get_waypoints().len() < 2 {
104                Widget::nothing()
105            } else {
106                Widget::col(vec![
107                    Widget::row(vec![
108                        Line("Slow-down factor for main roads:")
109                            .into_widget(ctx)
110                            .centered_vert(),
111                        Spinner::f64_widget(
112                            ctx,
113                            "main road penalty",
114                            (1.0, 10.0),
115                            app.session.main_road_penalty,
116                            0.5,
117                        ),
118                        ctx.style()
119                            .btn_plain
120                            .icon("system/assets/tools/help.svg")
121                            .tooltip(
122                                "Increase to see how drivers may try to detour in heavy traffic",
123                            )
124                            .build_widget(ctx, "penalty instructions")
125                            .align_right(),
126                    ]),
127                    Line("1 means free-flow traffic conditions")
128                        .secondary()
129                        .into_widget(ctx),
130                ])
131            },
132            // Invisible separator
133            GeomBatch::from(vec![(Color::CLEAR, Polygon::rectangle(0.1, 30.0))]).into_widget(ctx),
134            results_widget.named("results"),
135        ]);
136        let mut panel =
137            crate::components::LeftPanel::right_of_proposals(ctx, &self.appwide_panel, contents)
138                // Hovering on waypoint cards
139                .ignore_initial_events()
140                .build(ctx);
141        panel.restore(ctx, &self.left_panel);
142        self.left_panel = panel;
143
144        let mut world = World::new();
145        self.waypoints.rebuild_world(ctx, &mut world, |x| x, 0);
146        world.initialize_hover(ctx);
147        world.rebuilt_during_drag(ctx, &self.world);
148        self.world = world;
149    }
150
151    // Called when waypoints changed, but the number has stayed the same. Aka, the common case of a
152    // waypoint being dragged. Does less work for speed.
153    fn update_minimal(&mut self, ctx: &mut EventCtx, app: &mut App) {
154        self.files.autosave(app);
155        let results_widget = self.recalculate_paths(ctx, app);
156
157        let mut world = World::new();
158        self.waypoints.rebuild_world(ctx, &mut world, |x| x, 0);
159        world.initialize_hover(ctx);
160        world.rebuilt_during_drag(ctx, &self.world);
161        self.world = world;
162
163        self.left_panel.replace(ctx, "results", results_widget);
164
165        // TODO This is the most expensive part. While we're dragging, can we just fade out the
166        // cards or de-emphasize them somehow, and only do the recalculation when done?
167        let waypoints_widget = self.waypoints.get_panel_widget(ctx);
168        self.left_panel.replace(ctx, "waypoints", waypoints_widget);
169    }
170
171    // Returns a widget to display
172    fn recalculate_paths(&mut self, ctx: &mut EventCtx, app: &App) -> Widget {
173        if self.waypoints.get_waypoints().len() < 2 {
174            self.draw_routes = Drawable::empty(ctx);
175            return Widget::nothing();
176        }
177
178        let map = &app.per_map.map;
179
180        let mut paths: Vec<(PathV2, Color)> = Vec::new();
181
182        let driving_before_changes_time = {
183            let mut total_time = Duration::ZERO;
184            let mut params = app.per_map.routing_params_before_changes.clone();
185            params.main_road_penalty = app.session.main_road_penalty;
186
187            let mut ok = true;
188
189            for pair in self.waypoints.get_waypoints().windows(2) {
190                if let Some(path) = TripEndpoint::path_req(pair[0], pair[1], TripMode::Drive, map)
191                    .and_then(|req| {
192                        self.pathfinder_cache
193                            .pathfind_with_params(map, req, params.clone())
194                    })
195                {
196                    total_time += path.estimate_duration(map, None, Some(params.main_road_penalty));
197                    paths.push((path, *colors::PLAN_ROUTE_BEFORE));
198                } else {
199                    ok = false;
200                    break;
201                }
202            }
203
204            if ok {
205                Some(total_time)
206            } else {
207                None
208            }
209        };
210
211        // The route respecting the filters
212        let driving_after_changes_time = {
213            let mut params = map.routing_params_respecting_modal_filters();
214            params.main_road_penalty = app.session.main_road_penalty;
215
216            let mut ok = true;
217
218            let mut total_time = Duration::ZERO;
219            let mut paths_after = Vec::new();
220            for pair in self.waypoints.get_waypoints().windows(2) {
221                if let Some(path) = TripEndpoint::path_req(pair[0], pair[1], TripMode::Drive, map)
222                    .and_then(|req| {
223                        self.pathfinder_cache
224                            .pathfind_with_params(map, req, params.clone())
225                    })
226                {
227                    total_time += path.estimate_duration(map, None, Some(params.main_road_penalty));
228                    paths_after.push((path, *colors::PLAN_ROUTE_AFTER));
229                } else {
230                    ok = false;
231                }
232            }
233            // To simplify colors, don't draw this path when it's the same as the baseline
234            // TODO Actually compare the paths! This could be dangerous.
235            if Some(total_time) != driving_before_changes_time {
236                paths.append(&mut paths_after);
237            }
238
239            if ok {
240                Some(total_time)
241            } else {
242                None
243            }
244        };
245
246        let biking_time = if app.session.show_walking_cycling_routes {
247            // No custom params, but don't use the map's built-in bike CH. Changes to one-way
248            // streets haven't been reflected, and it's cheap enough to use Dijkstra's for
249            // calculating one path at a time anyway.
250            let mut total_time = Duration::ZERO;
251            let mut ok = true;
252            for pair in self.waypoints.get_waypoints().windows(2) {
253                if let Some(path) = TripEndpoint::path_req(pair[0], pair[1], TripMode::Bike, map)
254                    .and_then(|req| {
255                        self.pathfinder_cache.pathfind_with_params(
256                            map,
257                            req,
258                            map.routing_params().clone(),
259                        )
260                    })
261                {
262                    total_time +=
263                        path.estimate_duration(map, Some(map_model::MAX_BIKE_SPEED), None);
264                    paths.push((path, *colors::PLAN_ROUTE_BIKE));
265                } else {
266                    ok = false;
267                }
268            }
269            if ok {
270                Some(total_time)
271            } else {
272                None
273            }
274        } else {
275            None
276        };
277
278        let walking_time = if app.session.show_walking_cycling_routes {
279            // Same as above -- don't use the built-in CH.
280            let mut total_time = Duration::ZERO;
281            let mut ok = true;
282            for pair in self.waypoints.get_waypoints().windows(2) {
283                if let Some(path) = TripEndpoint::path_req(pair[0], pair[1], TripMode::Walk, map)
284                    .and_then(|req| {
285                        self.pathfinder_cache.pathfind_with_params(
286                            map,
287                            req,
288                            map.routing_params().clone(),
289                        )
290                    })
291                {
292                    total_time +=
293                        path.estimate_duration(map, Some(map_model::MAX_WALKING_SPEED), None);
294                    paths.push((path, *colors::PLAN_ROUTE_WALK));
295                } else {
296                    ok = false;
297                }
298            }
299            if ok {
300                Some(total_time)
301            } else {
302                None
303            }
304        } else {
305            None
306        };
307
308        self.draw_routes = map_gui::tools::draw_overlapping_paths(app, paths)
309            .unzoomed
310            .upload(ctx);
311
312        fn render_time(d: Option<Duration>) -> TextSpan {
313            if let Some(d) = d {
314                Line(d.to_rounded_string(0))
315            } else {
316                Line("Error")
317            }
318        }
319
320        Widget::col(vec![
321            // TODO Circle icons
322            Widget::row(vec![
323                Image::from_path("system/assets/meters/car.svg")
324                    .color(*colors::PLAN_ROUTE_BEFORE)
325                    .into_widget(ctx),
326                "Driving before any changes".text_widget(ctx),
327                render_time(driving_before_changes_time)
328                    .into_widget(ctx)
329                    .align_right(),
330            ]),
331            if driving_before_changes_time == driving_after_changes_time {
332                Widget::row(vec![
333                    Image::from_path("system/assets/meters/car.svg")
334                        .color(*colors::PLAN_ROUTE_BEFORE)
335                        .into_widget(ctx),
336                    "Driving after changes".text_widget(ctx),
337                    "Same".text_widget(ctx).align_right(),
338                ])
339            } else {
340                Widget::row(vec![
341                    Image::from_path("system/assets/meters/car.svg")
342                        .color(*colors::PLAN_ROUTE_AFTER)
343                        .into_widget(ctx),
344                    "Driving after changes".text_widget(ctx),
345                    render_time(driving_after_changes_time)
346                        .into_widget(ctx)
347                        .align_right(),
348                ])
349            },
350            if app.session.show_walking_cycling_routes {
351                Widget::col(vec![
352                    // TODO Is the tooltip that important? "This cycling route doesn't avoid
353                    // high-stress roads or hills, and assumes an average 10mph pace"
354                    Widget::row(vec![
355                        Image::from_path("system/assets/meters/bike.svg")
356                            .color(*colors::PLAN_ROUTE_BIKE)
357                            .into_widget(ctx),
358                        "Cycling".text_widget(ctx),
359                        render_time(biking_time).into_widget(ctx).align_right(),
360                    ]),
361                    Widget::row(vec![
362                        Image::from_path("system/assets/meters/pedestrian.svg")
363                            .color(*colors::PLAN_ROUTE_WALK)
364                            .into_widget(ctx),
365                        "Walking".text_widget(ctx),
366                        render_time(walking_time).into_widget(ctx).align_right(),
367                    ]),
368                ])
369            } else {
370                Widget::nothing()
371            },
372            // TODO Tooltip to explain how these routes remain direct?
373            Toggle::checkbox(
374                ctx,
375                "Show walking & cycling route",
376                None,
377                app.session.show_walking_cycling_routes,
378            ),
379        ])
380    }
381}
382
383impl State<App> for RoutePlanner {
384    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
385        if let Some(t) =
386            self.appwide_panel
387                .event(ctx, app, &crate::save::PreserveState::Route, help)
388        {
389            return t;
390        }
391        if let Some(t) = app
392            .session
393            .layers
394            .event(ctx, &app.cs, Mode::RoutePlanner, None)
395        {
396            return t;
397        }
398
399        let panel_outcome = self.left_panel.event(ctx);
400        if let Outcome::Clicked(ref x) = panel_outcome {
401            if let Some(t) = self.files.on_click(ctx, app, x) {
402                // Bit hacky...
403                if matches!(t, Transition::Keep) {
404                    self.sync_from_file_management(ctx, app);
405                }
406                return t;
407            }
408            if x == "penalty instructions" {
409                return Transition::Keep;
410            }
411            // Might be for waypoints
412        }
413
414        if let Outcome::Changed(ref x) = panel_outcome {
415            if x == "main road penalty" {
416                app.session.main_road_penalty =
417                    self.left_panel.spinner::<RoundedF64>("main road penalty").0;
418                self.update_everything(ctx, app);
419            } else if x == "Show walking & cycling route" {
420                app.session.show_walking_cycling_routes =
421                    self.left_panel.is_checked("Show walking & cycling route");
422                self.update_everything(ctx, app);
423            }
424        }
425
426        let waypoints_before = self.waypoints.get_waypoints().len();
427        if self
428            .waypoints
429            .event(app, panel_outcome, self.world.event(ctx))
430        {
431            // Sync from waypoints to file management
432            // TODO Maaaybe this directly live in the InputWaypoints system?
433            self.files.current.waypoints = self.waypoints.get_waypoints();
434
435            if self.waypoints.get_waypoints().len() == waypoints_before {
436                self.update_minimal(ctx, app);
437            } else {
438                self.update_everything(ctx, app);
439            }
440        }
441
442        Transition::Keep
443    }
444
445    fn draw_baselayer(&self) -> DrawBaselayer {
446        DrawBaselayer::Custom
447    }
448
449    fn draw(&self, g: &mut GfxCtx, app: &App) {
450        app.draw_with_layering(g, |g| g.redraw(&self.draw_driveways));
451
452        g.redraw(&self.show_main_roads);
453        self.draw_routes.draw(g);
454        self.world.draw(g);
455        app.per_map
456            .draw_all_local_road_labels
457            .as_ref()
458            .unwrap()
459            .draw(g);
460        app.per_map.draw_major_road_labels.draw(g);
461        app.per_map.draw_all_filters.draw(g);
462        app.per_map.draw_poi_icons.draw(g);
463
464        self.appwide_panel.draw(g);
465        self.left_panel.draw(g);
466        app.session.layers.draw(g, app);
467    }
468
469    fn recreate(&mut self, ctx: &mut EventCtx, app: &mut App) -> Box<dyn State<App>> {
470        Self::new_state(ctx, app)
471    }
472}
473
474fn help() -> Vec<&'static str> {
475    vec![
476        "You can test how different driving routes are affected by proposed LTNs.",
477        "",
478        "The fastest route may not cut through neighbourhoods normally,",
479        "but you can adjust the slow-down factor to mimic rush hour conditions",
480    ]
481}