diff --git a/game/src/debug/routes.rs b/game/src/debug/routes.rs index 7554b7ef01..b32c20ad59 100644 --- a/game/src/debug/routes.rs +++ b/game/src/debug/routes.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use abstutil::{prettyprint_usize, Counter, Timer}; use geom::{Duration, Polygon}; use map_gui::colors::ColorSchemeChoice; -use map_gui::tools::ColorNetwork; +use map_gui::tools::{cmp_count, ColorNetwork}; use map_gui::{AppLike, ID}; use map_model::{ DirectedRoadID, Direction, PathRequest, RoadID, RoutingParams, Traversable, @@ -450,7 +450,7 @@ impl State for AllRoutesExplorer { let baseline = self.baseline_counts.get(r); let current = self.current_counts.get(r); let mut txt = Text::new(); - txt.append_all(cmp_count(current, baseline)); + cmp_count(&mut txt, baseline, current); txt.add_line(format!("{} baseline", prettyprint_usize(baseline))); txt.add_line(format!("{} now", prettyprint_usize(current))); self.tooltip = Some(txt); @@ -490,26 +490,6 @@ fn calculate_demand(app: &App, requests: &[PathRequest], timer: &mut Timer) -> C counter } -fn cmp_count(after: usize, before: usize) -> Vec { - match after.cmp(&before) { - std::cmp::Ordering::Equal => { - vec![Line("same")] - } - std::cmp::Ordering::Less => { - vec![ - Line(prettyprint_usize(before - after)).fg(Color::GREEN), - Line(" less"), - ] - } - std::cmp::Ordering::Greater => { - vec![ - Line(prettyprint_usize(after - before)).fg(Color::RED), - Line(" more"), - ] - } - } -} - /// Evaluate why an alternative path wasn't chosen, by showing the cost to reach every road from /// one start. pub struct PathCostDebugger { diff --git a/ltn/src/browse.rs b/ltn/src/browse.rs index baa17d0b72..228f085a28 100644 --- a/ltn/src/browse.rs +++ b/ltn/src/browse.rs @@ -125,6 +125,10 @@ impl State for BrowseNeighborhoods { "Calculate" => { return Transition::Push(super::impact::ShowResults::new_state(ctx, app)); } + "Force recalculation" => { + app.session.impact = None; + return Transition::Push(super::impact::ShowResults::new_state(ctx, app)); + } _ => unreachable!(), }, Outcome::Changed(_) => { @@ -270,9 +274,16 @@ fn impact_widget(ctx: &EventCtx, app: &App) -> Widget { Line(format!("We need to load a {} file", size)), ]) .into_widget(ctx), - ctx.style() - .btn_solid_primary - .text("Calculate") - .build_def(ctx), + Widget::row(vec![ + ctx.style() + .btn_solid_primary + .text("Calculate") + .build_def(ctx), + // TODO Bad UI! Detect edits and do this. I'm being lazy. + ctx.style() + .btn_solid_primary + .text("Force recalculation") + .build_def(ctx), + ]), ]) } diff --git a/ltn/src/impact.rs b/ltn/src/impact.rs index 248e42a4d9..f15700419b 100644 --- a/ltn/src/impact.rs +++ b/ltn/src/impact.rs @@ -1,21 +1,23 @@ use abstio::MapName; -use abstutil::{Counter, Timer}; +use abstutil::{prettyprint_usize, Counter, Timer}; use map_gui::load::FileLoader; -use map_gui::tools::ColorNetwork; +use map_gui::tools::{cmp_count, ColorNetwork}; +use map_gui::ID; use map_model::{PathRequest, PathStepV2, RoadID}; use sim::{Scenario, TripEndpoint, TripMode}; use widgetry::mapspace::ToggleZoomed; use widgetry::{ - EventCtx, GfxCtx, HorizontalAlignment, Panel, SimpleState, State, TextExt, VerticalAlignment, - Widget, + Choice, EventCtx, GfxCtx, HorizontalAlignment, Line, Panel, SimpleState, State, Text, TextExt, + VerticalAlignment, Widget, }; use crate::{App, Transition}; -// TODO Tooltips // TODO Intersections -// TODO Toggle before/after / compare directly +// TODO Configurable main road penalty, like in the pathfinding tool +// TODO Don't allow crossing filters at all -- don't just disincentivize // TODO Share structure or pieces with Ungap's predict mode +// ... can't we just produce data of a certain shape, and have a UI pretty tuned for that? pub struct Results { map: MapName, @@ -66,12 +68,15 @@ impl Results { fn recalculate_impact(&mut self, ctx: &mut EventCtx, app: &App, timer: &mut Timer) { self.before_counts = Counter::new(); self.after_counts = Counter::new(); - let map = &app.map; + + // Before the filters for path in timer - .parallelize("calculate routes", self.all_driving_trips.clone(), |req| { - map.pathfind_v2(req) - }) + .parallelize( + "calculate routes before filters", + self.all_driving_trips.clone(), + |req| map.pathfind_v2(req), + ) .into_iter() .flatten() { @@ -82,14 +87,46 @@ impl Results { } } } - let mut colorer = ColorNetwork::no_fading(app); colorer.ranked_roads(self.before_counts.clone(), &app.cs.good_to_bad_red); self.before_draw_heatmap = colorer.build(ctx); + + // After the filters + let mut params = map.routing_params().clone(); + app.session.modal_filters.update_routing_params(&mut params); + let cache_custom = true; + for path in timer + .parallelize( + "calculate routes after filters", + self.all_driving_trips.clone(), + |req| map.pathfind_v2_with_params(req, ¶ms, cache_custom), + ) + .into_iter() + .flatten() + { + for step in path.get_steps() { + // No Contraflow steps for driving paths + if let PathStepV2::Along(dr) = step { + self.after_counts.inc(dr.road); + } + } + } + let mut colorer = ColorNetwork::no_fading(app); + colorer.ranked_roads(self.after_counts.clone(), &app.cs.good_to_bad_red); + self.after_draw_heatmap = colorer.build(ctx); } } -pub struct ShowResults; +#[derive(Clone, Copy, Debug, PartialEq)] +enum Layer { + Before, + After, +} + +pub struct ShowResults { + layer: Layer, + tooltip: Option, +} impl ShowResults { pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box> { @@ -115,35 +152,89 @@ impl ShowResults { ); } + let layer = Layer::Before; let panel = Panel::new_builder(Widget::col(vec![ map_gui::tools::app_header(ctx, app, "Low traffic neighborhoods"), Widget::row(vec![ "Impact prediction".text_widget(ctx), ctx.style().btn_close_widget(ctx), ]), + "This shows how many driving trips cross each road".text_widget(ctx), + Widget::row(vec![ + "Show what?".text_widget(ctx).centered_vert(), + Widget::dropdown( + ctx, + "layer", + layer, + vec![ + Choice::new("before", Layer::Before), + Choice::new("after", Layer::After), + ], + ), + ]), ])) .aligned(HorizontalAlignment::Left, VerticalAlignment::Top) .build(ctx); - >::new_state(panel, Box::new(ShowResults)) + >::new_state( + panel, + Box::new(ShowResults { + layer, + tooltip: None, + }), + ) } } impl SimpleState for ShowResults { - fn on_click(&mut self, ctx: &mut EventCtx, app: &mut App, x: &str, _: &Panel) -> Transition { + fn on_click(&mut self, _: &mut EventCtx, _: &mut App, x: &str, _: &Panel) -> Transition { if x == "close" { return Transition::Pop; } unreachable!() } - // TODO Or on_mouseover? - fn other_event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition { + fn on_mouseover(&mut self, ctx: &mut EventCtx, app: &mut App) { ctx.canvas_movement(); - Transition::Keep + self.tooltip = None; + if let Some(r) = match app.mouseover_unzoomed_roads_and_intersections(ctx) { + Some(ID::Road(r)) => Some(r), + Some(ID::Lane(l)) => Some(l.road), + _ => None, + } { + let impact = app.session.impact.as_ref().unwrap(); + let before = impact.before_counts.get(r); + let after = impact.after_counts.get(r); + let mut txt = Text::from_multiline(vec![ + Line(format!("Before: {}", prettyprint_usize(before))), + Line(format!("After: {}", prettyprint_usize(after))), + ]); + cmp_count(&mut txt, before, after); + self.tooltip = Some(txt); + } + } + + fn panel_changed( + &mut self, + _: &mut EventCtx, + _: &mut App, + panel: &mut Panel, + ) -> Option { + self.layer = panel.dropdown_value("layer"); + None } fn draw(&self, g: &mut GfxCtx, app: &App) { let impact = app.session.impact.as_ref().unwrap(); - impact.before_draw_heatmap.draw(g); + match self.layer { + Layer::Before => { + impact.before_draw_heatmap.draw(g); + } + Layer::After => { + impact.after_draw_heatmap.draw(g); + } + } + if let Some(ref txt) = self.tooltip { + g.draw_mouse_tooltip(txt.clone()); + } } } diff --git a/ltn/src/pathfinding.rs b/ltn/src/pathfinding.rs index 567748e2c6..62955aa00c 100644 --- a/ltn/src/pathfinding.rs +++ b/ltn/src/pathfinding.rs @@ -126,6 +126,8 @@ impl RoutePlanner { let mut draw = ToggleZoomed::builder(); // First the route respecting the filters + // TODO Like in rat_runs, we need to actually enforce this, not just penalize... though, + // should it matter except for when a cell is disconnected? let (total_time_after, total_dist_after) = { let mut params = map.routing_params().clone(); app.session.modal_filters.update_routing_params(&mut params); diff --git a/map_gui/src/tools/mod.rs b/map_gui/src/tools/mod.rs index 7ca3f2a1a0..366755bdbe 100644 --- a/map_gui/src/tools/mod.rs +++ b/map_gui/src/tools/mod.rs @@ -19,7 +19,8 @@ pub use self::title_screen::{Executable, TitleScreen}; pub use self::trip_files::{TripManagement, TripManagementState}; pub use self::turn_explorer::TurnExplorer; pub use self::ui::{ - cmp_dist, cmp_duration, percentage_bar, ChooseSomething, FilePicker, PopupMsg, PromptInput, + cmp_count, cmp_dist, cmp_duration, percentage_bar, ChooseSomething, FilePicker, PopupMsg, + PromptInput, }; pub use self::url::URLManager; pub use self::waypoints::{InputWaypoints, WaypointID}; diff --git a/map_gui/src/tools/ui.rs b/map_gui/src/tools/ui.rs index 95be003e4e..fe2e1b9d1a 100644 --- a/map_gui/src/tools/ui.rs +++ b/map_gui/src/tools/ui.rs @@ -4,6 +4,7 @@ use std::cmp::Ordering; use anyhow::Result; +use abstutil::prettyprint_usize; use geom::{Distance, Duration, Polygon}; use widgetry::{ hotkeys, Choice, Color, DrawBaselayer, EventCtx, GeomBatch, GfxCtx, Key, Line, Menu, Outcome, @@ -316,3 +317,24 @@ pub fn cmp_duration( Ordering::Equal => {} } } + +/// Less is better +pub fn cmp_count(txt: &mut Text, before: usize, after: usize) { + match after.cmp(&before) { + std::cmp::Ordering::Equal => { + txt.add_line(Line("same")); + } + std::cmp::Ordering::Less => { + txt.add_appended(vec![ + Line(prettyprint_usize(before - after)).fg(Color::GREEN), + Line(" less"), + ]); + } + std::cmp::Ordering::Greater => { + txt.add_appended(vec![ + Line(prettyprint_usize(after - before)).fg(Color::RED), + Line(" more"), + ]); + } + } +}