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 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 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 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 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 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 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 .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 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 let waypoints_widget = self.waypoints.get_panel_widget(ctx);
168 self.left_panel.replace(ctx, "waypoints", waypoints_widget);
169 }
170
171 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 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 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 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 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 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 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 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 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 }
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 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}