1use std::collections::HashMap;
2
3use abstutil::{prettyprint_usize, Counter, Timer};
4use geom::{Duration, Polygon};
5use map_gui::colors::ColorSchemeChoice;
6use map_gui::tools::{cmp_count, ColorNetwork};
7use map_gui::AppLike;
8use map_model::{
9 DirectedRoadID, Direction, PathConstraints, PathRequest, PathStepV2, Pathfinder, RoadID,
10 RoutingParams, NORMAL_LANE_THICKNESS,
11};
12use synthpop::{TripEndpoint, TripMode};
13use widgetry::mapspace::ToggleZoomed;
14use widgetry::{
15 Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel,
16 RoundedF64, Spinner, State, Text, TextExt, VerticalAlignment, Widget,
17};
18
19use crate::app::{App, Transition};
20use crate::common::CommonState;
21use crate::ID;
22
23pub struct RouteExplorer {
25 panel: Panel,
26 start: TripEndpoint,
27 goal: Option<(TripEndpoint, bool, Drawable)>,
29}
30
31impl RouteExplorer {
32 pub fn new_state(ctx: &mut EventCtx, app: &App, start: TripEndpoint) -> Box<dyn State<App>> {
33 Box::new(RouteExplorer {
34 start,
35 goal: None,
36 panel: Panel::new_builder(Widget::col(vec![
37 Widget::row(vec![
38 Line("Route explorer").small_heading().into_widget(ctx),
39 ctx.style().btn_close_widget(ctx),
40 ]),
41 ctx.style()
42 .btn_outline
43 .text("All routes")
44 .hotkey(Key::A)
45 .build_def(ctx),
46 params_to_controls(ctx, TripMode::Bike, app.primary.map.routing_params())
47 .named("params"),
48 ]))
49 .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
50 .build(ctx),
51 })
52 }
53
54 fn recalc_paths(&mut self, ctx: &mut EventCtx, app: &App) {
55 let (mode, params) = controls_to_params(&self.panel);
56
57 if let Some((ref goal, _, ref mut preview)) = self.goal {
58 *preview = Drawable::empty(ctx);
59 if let Some(polygon) = TripEndpoint::path_req(self.start, *goal, mode, &app.primary.map)
60 .and_then(|req| {
61 Pathfinder::new_dijkstra(
62 &app.primary.map,
63 params,
64 vec![req.constraints],
65 &mut Timer::throwaway(),
66 )
67 .pathfind_v2(req, &app.primary.map)
68 })
69 .and_then(|path| path.into_v1(&app.primary.map).ok())
70 .and_then(|path| path.trace(&app.primary.map))
71 .map(|pl| pl.make_polygons(NORMAL_LANE_THICKNESS))
72 {
73 *preview = GeomBatch::from(vec![(Color::PURPLE, polygon)]).upload(ctx);
74 }
75 }
76 }
77}
78
79impl State<App> for RouteExplorer {
80 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
81 ctx.canvas_movement();
82
83 match self.panel.event(ctx) {
84 Outcome::Clicked(x) => match x.as_ref() {
85 "close" => {
86 return Transition::Pop;
87 }
88 "bikes" => {
89 let controls =
90 params_to_controls(ctx, TripMode::Bike, app.primary.map.routing_params());
91 self.panel.replace(ctx, "params", controls);
92 self.recalc_paths(ctx, app);
93 }
94 "cars" => {
95 let controls =
96 params_to_controls(ctx, TripMode::Drive, app.primary.map.routing_params());
97 self.panel.replace(ctx, "params", controls);
98 self.recalc_paths(ctx, app);
99 }
100 "pedestrians" => {
101 let controls =
102 params_to_controls(ctx, TripMode::Walk, app.primary.map.routing_params());
103 self.panel.replace(ctx, "params", controls);
104 self.recalc_paths(ctx, app);
105 }
106 "All routes" => {
107 return Transition::Replace(AllRoutesExplorer::new_state(ctx, app));
108 }
109 _ => unreachable!(),
110 },
111 Outcome::Changed(_) => {
112 self.recalc_paths(ctx, app);
113 }
114 _ => {}
115 }
116
117 if self
118 .goal
119 .as_ref()
120 .map(|(_, confirmed, _)| *confirmed)
121 .unwrap_or(false)
122 {
123 return Transition::Keep;
124 }
125
126 if ctx.redo_mouseover() {
127 app.primary.current_selection = app.mouseover_unzoomed_everything(ctx);
128 if match app.primary.current_selection {
129 Some(ID::Intersection(i)) => !app.primary.map.get_i(i).is_border(),
130 Some(ID::Building(_)) => false,
131 _ => true,
132 } {
133 app.primary.current_selection = None;
134 }
135 }
136 if let Some(hovering) = match app.primary.current_selection {
137 Some(ID::Intersection(i)) => Some(TripEndpoint::Border(i)),
138 Some(ID::Building(b)) => Some(TripEndpoint::Building(b)),
139 None => None,
140 _ => unreachable!(),
141 } {
142 if self.start != hovering {
143 if self
144 .goal
145 .as_ref()
146 .map(|(to, _, _)| to != &hovering)
147 .unwrap_or(true)
148 {
149 self.goal = Some((hovering, false, Drawable::empty(ctx)));
150 self.recalc_paths(ctx, app);
151 }
152 } else {
153 self.goal = None;
154 }
155 } else {
156 self.goal = None;
157 }
158
159 if let Some((_, ref mut confirmed, _)) = self.goal {
160 if app.per_obj.left_click(ctx, "end here") {
161 app.primary.current_selection = None;
162 *confirmed = true;
163 }
164 }
165
166 Transition::Keep
167 }
168
169 fn draw(&self, g: &mut GfxCtx, app: &App) {
170 self.panel.draw(g);
171 CommonState::draw_osd(g, app);
172
173 g.draw_polygon(
174 Color::BLUE.alpha(0.8),
175 match self.start {
176 TripEndpoint::Border(i) => app.primary.map.get_i(i).polygon.clone(),
177 TripEndpoint::Building(b) => app.primary.map.get_b(b).polygon.clone(),
178 TripEndpoint::SuddenlyAppear(_) => unreachable!(),
179 },
180 );
181 if let Some((ref endpt, _, ref draw)) = self.goal {
182 g.draw_polygon(
183 Color::GREEN.alpha(0.8),
184 match endpt {
185 TripEndpoint::Border(i) => app.primary.map.get_i(*i).polygon.clone(),
186 TripEndpoint::Building(b) => app.primary.map.get_b(*b).polygon.clone(),
187 TripEndpoint::SuddenlyAppear(_) => unreachable!(),
188 },
189 );
190 g.redraw(draw);
191 }
192 }
193}
194
195fn params_to_controls(ctx: &mut EventCtx, mode: TripMode, params: &RoutingParams) -> Widget {
196 let mut rows = vec![Widget::custom_row(vec![
197 ctx.style()
198 .btn_plain
199 .icon("system/assets/meters/bike.svg")
200 .disabled(mode == TripMode::Bike)
201 .build_widget(ctx, "bikes"),
202 ctx.style()
203 .btn_plain
204 .icon("system/assets/meters/car.svg")
205 .disabled(mode == TripMode::Drive)
206 .build_widget(ctx, "cars"),
207 ctx.style()
208 .btn_plain
209 .icon("system/assets/meters/pedestrian.svg")
210 .disabled(mode == TripMode::Walk)
211 .build_widget(ctx, "pedestrians"),
212 ])
213 .evenly_spaced()];
214 if mode == TripMode::Drive || mode == TripMode::Bike {
215 rows.push(Widget::row(vec![
216 "Unprotected turn penalty:"
217 .text_widget(ctx)
218 .margin_right(20),
219 Spinner::widget(
220 ctx,
221 "unprotected_turn_penalty",
222 (Duration::seconds(1.0), Duration::seconds(100.0)),
223 params.unprotected_turn_penalty,
224 Duration::seconds(1.0),
225 ),
226 ]));
227 }
228 if mode == TripMode::Bike {
229 rows.push(Widget::row(vec![
230 "Bike lane penalty:".text_widget(ctx).margin_right(20),
231 Spinner::f64_widget(
232 ctx,
233 "bike_lane_penalty",
234 (0.0, 2.0),
235 params.bike_lane_penalty,
236 0.1,
237 ),
238 ]));
239 rows.push(Widget::row(vec![
240 "Bus lane penalty:".text_widget(ctx).margin_right(20),
241 Spinner::f64_widget(
242 ctx,
243 "bus_lane_penalty",
244 (0.0, 2.0),
245 params.bus_lane_penalty,
246 0.1,
247 ),
248 ]));
249 rows.push(Widget::row(vec![
250 "Driving lane penalty:".text_widget(ctx).margin_right(20),
251 Spinner::f64_widget(
252 ctx,
253 "driving_lane_penalty",
254 (0.0, 2.0),
255 params.driving_lane_penalty,
256 0.1,
257 ),
258 ]));
259 rows.push(Widget::row(vec![
260 "Avoid steep inclines (>= 8%):"
261 .text_widget(ctx)
262 .margin_right(20),
263 Spinner::f64_widget(
264 ctx,
265 "avoid_steep_incline_penalty",
266 (0.0, 2.0),
267 params.avoid_steep_incline_penalty,
268 0.1,
269 ),
270 ]));
271 rows.push(Widget::row(vec![
272 "Avoid high-stress roads:".text_widget(ctx).margin_right(20),
273 Spinner::f64_widget(
274 ctx,
275 "avoid_high_stress",
276 (0.0, 2.0),
277 params.avoid_high_stress,
278 0.1,
279 ),
280 ]));
281 }
282 Widget::col(rows)
283}
284
285fn controls_to_params(panel: &Panel) -> (TripMode, RoutingParams) {
286 let mut params = RoutingParams::default();
287 if !panel.is_button_enabled("cars") {
288 params.unprotected_turn_penalty = panel.spinner("unprotected_turn_penalty");
289 return (TripMode::Drive, params);
290 }
291 if !panel.is_button_enabled("pedestrians") {
292 return (TripMode::Walk, params);
293 }
294 params.unprotected_turn_penalty = panel.spinner("unprotected_turn_penalty");
295 params.bike_lane_penalty = panel.spinner::<RoundedF64>("bike_lane_penalty").0;
296 params.bus_lane_penalty = panel.spinner::<RoundedF64>("bus_lane_penalty").0;
297 params.driving_lane_penalty = panel.spinner::<RoundedF64>("driving_lane_penalty").0;
298 params.avoid_steep_incline_penalty =
299 panel.spinner::<RoundedF64>("avoid_steep_incline_penalty").0;
300 params.avoid_high_stress = panel.spinner::<RoundedF64>("avoid_high_stress").0;
301 (TripMode::Bike, params)
302}
303
304struct AllRoutesExplorer {
306 panel: Panel,
307 requests: Vec<PathRequest>,
308 baseline_counts: Counter<RoadID>,
309
310 current_counts: Counter<RoadID>,
311 draw: ToggleZoomed,
312 tooltip: Option<Text>,
313}
314
315impl AllRoutesExplorer {
316 fn new_state(ctx: &mut EventCtx, app: &mut App) -> Box<dyn State<App>> {
317 app.change_color_scheme(ctx, ColorSchemeChoice::DayMode);
319
320 let (requests, baseline_counts) =
321 ctx.loading_screen("calculate baseline paths", |_, timer| {
322 let map = &app.primary.map;
323 let requests = timer
324 .parallelize(
325 "predict route requests",
326 app.primary.sim.all_trip_info(),
327 |(_, trip)| TripEndpoint::path_req(trip.start, trip.end, trip.mode, map),
328 )
329 .into_iter()
330 .flatten()
331 .collect::<Vec<_>>();
332 let baseline_counts = calculate_demand(app, map.get_pathfinder(), &requests, timer);
333 (requests, baseline_counts)
334 });
335 let current_counts = baseline_counts.clone();
336
337 let mut colorer = ColorNetwork::new(app);
339 colorer.ranked_roads(current_counts.clone(), &app.cs.good_to_bad_red);
340
341 Box::new(AllRoutesExplorer {
342 panel: Panel::new_builder(Widget::col(vec![
343 Widget::row(vec![
344 Line("All routes explorer").small_heading().into_widget(ctx),
345 ctx.style().btn_close_widget(ctx),
346 ]),
347 format!("{} total requests", prettyprint_usize(requests.len())).text_widget(ctx),
348 params_to_controls(ctx, TripMode::Bike, app.primary.map.routing_params())
349 .named("params"),
350 ctx.style()
351 .btn_outline
352 .text("Calculate differential demand")
353 .build_def(ctx),
354 ]))
355 .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
356 .build(ctx),
357 requests,
358 baseline_counts,
359 current_counts,
360 draw: colorer.build(ctx),
361 tooltip: None,
362 })
363 }
364}
365
366impl State<App> for AllRoutesExplorer {
367 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
368 ctx.canvas_movement();
369
370 if let Outcome::Clicked(x) = self.panel.event(ctx) {
371 match x.as_ref() {
372 "close" => {
373 return Transition::Pop;
374 }
375 "bikes" => {
376 let controls =
377 params_to_controls(ctx, TripMode::Bike, app.primary.map.routing_params());
378 self.panel.replace(ctx, "params", controls);
379 }
380 "cars" => {
381 let controls =
382 params_to_controls(ctx, TripMode::Drive, app.primary.map.routing_params());
383 self.panel.replace(ctx, "params", controls);
384 }
385 "pedestrians" => {
386 let controls =
387 params_to_controls(ctx, TripMode::Walk, app.primary.map.routing_params());
388 self.panel.replace(ctx, "params", controls);
389 }
390 "Calculate differential demand" => {
391 ctx.loading_screen(
392 "calculate differential demand due to routing params",
393 |ctx, timer| {
394 let (_, params) = controls_to_params(&self.panel);
395 let pathfinder = Pathfinder::new_ch(
396 &app.primary.map,
397 params,
398 PathConstraints::all(),
399 timer,
400 );
401 self.current_counts =
402 calculate_demand(app, &pathfinder, &self.requests, timer);
403
404 let mut colorer = ColorNetwork::new(app);
406 let more = &app.cs.good_to_bad_red;
408 let less = &app.cs.good_to_bad_green;
409 let comparisons = self
410 .baseline_counts
411 .clone()
412 .compare(self.current_counts.clone());
413 let diff = comparisons
415 .iter()
416 .map(|(_, after, before)| {
417 ((*after as isize) - (*before as isize)).abs() as usize
418 })
419 .max()
420 .unwrap_or(0) as f64;
421 for (r, before, after) in comparisons {
422 match after.cmp(&before) {
423 std::cmp::Ordering::Less => {
424 colorer.add_r(r, less.eval((before - after) as f64 / diff));
425 }
426 std::cmp::Ordering::Greater => {
427 colorer.add_r(r, more.eval((after - before) as f64 / diff));
428 }
429 std::cmp::Ordering::Equal => {}
430 }
431 }
432 self.draw = colorer.build(ctx);
433 },
434 );
435 }
436 _ => unreachable!(),
437 }
438 }
439
440 if ctx.redo_mouseover() {
441 self.tooltip = None;
442 if let Some(ID::Road(r)) = app.mouseover_unzoomed_roads_and_intersections(ctx) {
443 let baseline = self.baseline_counts.get(r);
444 let current = self.current_counts.get(r);
445 let mut txt = Text::new();
446 cmp_count(&mut txt, baseline, current);
447 txt.add_line(format!("{} baseline", prettyprint_usize(baseline)));
448 txt.add_line(format!("{} now", prettyprint_usize(current)));
449 self.tooltip = Some(txt);
450 }
451 }
452
453 Transition::Keep
454 }
455
456 fn draw(&self, g: &mut GfxCtx, app: &App) {
457 self.panel.draw(g);
458 CommonState::draw_osd(g, app);
459 self.draw.draw(g);
460 if let Some(ref txt) = self.tooltip {
461 g.draw_mouse_tooltip(txt.clone());
462 }
463 }
464}
465
466fn calculate_demand(
467 app: &App,
468 pathfinder: &Pathfinder,
469 requests: &[PathRequest],
470 timer: &mut Timer,
471) -> Counter<RoadID> {
472 let map = &app.primary.map;
473 let paths = timer
474 .parallelize("pathfind", requests.to_vec(), |req| {
475 pathfinder.pathfind_v2(req, map)
476 })
477 .into_iter()
478 .flatten()
479 .collect::<Vec<_>>();
480 let mut counter = Counter::new();
481 timer.start_iter("compute demand", paths.len());
482 for path in paths {
483 timer.next();
484 for step in path.get_steps() {
485 if let PathStepV2::Along(dr) | PathStepV2::Contraflow(dr) = step {
486 counter.inc(dr.road);
487 }
488 }
489 }
490 counter
491}
492
493pub struct PathCostDebugger {
496 draw_path: Drawable,
497 costs: HashMap<DirectedRoadID, Duration>,
498 tooltip: Option<Text>,
499 panel: Panel,
500}
501
502impl PathCostDebugger {
503 pub fn maybe_new(
504 ctx: &mut EventCtx,
505 app: &App,
506 req: PathRequest,
507 draw_path: Polygon,
508 ) -> Option<Box<dyn State<App>>> {
509 let (full_cost, all_costs) = app.primary.map.all_costs_from(req)?;
510 let mut batch = GeomBatch::new();
511 for (dr, cost) in &all_costs {
515 if *cost <= full_cost {
516 if let Ok(p) = app
517 .primary
518 .map
519 .get_r(dr.road)
520 .get_half_polygon(dr.dir, &app.primary.map)
521 {
522 batch.push(Color::BLUE.alpha(0.5), p);
523 }
524 }
525 }
526 batch.push(Color::PURPLE, draw_path);
527
528 Some(Box::new(PathCostDebugger {
529 draw_path: ctx.upload(batch),
530 costs: all_costs,
531 tooltip: None,
532 panel: Panel::new_builder(Widget::col(vec![
533 Widget::row(vec![
534 Line("Path cost debugger").small_heading().into_widget(ctx),
535 ctx.style().btn_close_widget(ctx),
536 ]),
537 format!("Cost of chosen path: {}", full_cost).text_widget(ctx),
538 ]))
539 .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
540 .build(ctx),
541 }))
542 }
543}
544
545impl State<App> for PathCostDebugger {
546 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
547 ctx.canvas_movement();
548
549 if ctx.redo_mouseover() {
550 self.tooltip = None;
551 if let Some(ID::Road(r)) = app.mouseover_unzoomed_roads_and_intersections(ctx) {
552 let mut txt = Text::new();
554 for dir in [Direction::Fwd, Direction::Back] {
555 if let Some(cost) = self.costs.get(&DirectedRoadID { road: r, dir }) {
556 txt.add_line(format!("Cost {:?}: {}", dir, cost));
557 } else {
558 txt.add_line(format!("No cost {:?}", dir));
559 }
560 }
561 self.tooltip = Some(txt);
562 }
563 }
564
565 if let Outcome::Clicked(x) = self.panel.event(ctx) {
566 match x.as_ref() {
567 "close" => {
568 return Transition::Pop;
569 }
570 _ => unreachable!(),
571 }
572 }
573
574 Transition::Keep
575 }
576
577 fn draw(&self, g: &mut GfxCtx, _: &App) {
578 self.panel.draw(g);
579 g.redraw(&self.draw_path);
580 if let Some(ref txt) = self.tooltip {
581 g.draw_mouse_tooltip(txt.clone());
582 }
583 }
584}