diff --git a/game/src/ltn/rat_run_viewer.rs b/game/src/ltn/rat_run_viewer.rs index ba4b58bcc4..af5714916a 100644 --- a/game/src/ltn/rat_run_viewer.rs +++ b/game/src/ltn/rat_run_viewer.rs @@ -1,9 +1,10 @@ use geom::ArrowCap; -use map_model::NORMAL_LANE_THICKNESS; -use widgetry::mapspace::ToggleZoomed; +use map_gui::tools::ColorNetwork; +use map_model::{IntersectionID, RoadID, NORMAL_LANE_THICKNESS}; +use widgetry::mapspace::{ObjectID, ToggleZoomed, World}; use widgetry::{ - Color, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel, State, Text, TextExt, - VerticalAlignment, Widget, + Color, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel, State, + Text, TextExt, Toggle, VerticalAlignment, Widget, }; use super::rat_runs::{find_rat_runs, RatRuns}; @@ -16,6 +17,8 @@ pub struct BrowseRatRuns { current_idx: usize, draw_path: ToggleZoomed, + draw_heatmap: ToggleZoomed, + world: World, neighborhood: Neighborhood, } @@ -33,12 +36,23 @@ impl BrowseRatRuns { timer, ) }); + let mut colorer = ColorNetwork::no_fading(app); + colorer.ranked_roads(rat_runs.count_per_road.clone(), &app.cs.good_to_bad_red); + // TODO These two will be on different scales, which'll look really weird! + colorer.ranked_intersections( + rat_runs.count_per_intersection.clone(), + &app.cs.good_to_bad_red, + ); + let world = make_world(ctx, app, &neighborhood, &rat_runs); + let mut state = BrowseRatRuns { panel: Panel::empty(ctx), rat_runs, current_idx: 0, draw_path: ToggleZoomed::empty(ctx), + draw_heatmap: colorer.build(ctx), neighborhood, + world, }; state.recalculate(ctx, app); Box::new(state) @@ -91,6 +105,16 @@ impl BrowseRatRuns { .hotkey(Key::RightArrow) .build_widget(ctx, "next rat run"), ]), + // TODO This should disable the individual path controls, or maybe even be a different + // state entirely... + Toggle::checkbox( + ctx, + "show heatmap of all rat-runs", + Key::R, + self.panel + .maybe_is_checked("show heatmap of all rat-runs") + .unwrap_or(true), + ), ])) .aligned(HorizontalAlignment::Left, VerticalAlignment::Top) .build(ctx); @@ -140,28 +164,87 @@ impl State for BrowseRatRuns { } "previous rat run" => { self.current_idx -= 1; + self.panel + .set_checked("show heatmap of all rat-runs", false); self.recalculate(ctx, app); } "next rat run" => { self.current_idx += 1; + self.panel + .set_checked("show heatmap of all rat-runs", false); self.recalculate(ctx, app); } _ => unreachable!(), } } + // Just trigger tooltips; no other interactions possible + let _ = self.world.event(ctx); + Transition::Keep } fn draw(&self, g: &mut GfxCtx, app: &App) { self.panel.draw(g); + if self.panel.is_checked("show heatmap of all rat-runs") { + self.draw_heatmap.draw(g); + self.world.draw(g); + } else { + self.draw_path.draw(g); + } + g.redraw(&self.neighborhood.fade_irrelevant); self.neighborhood.draw_filters.draw(g); if g.canvas.is_unzoomed() { self.neighborhood.labels.draw(g, app); } - - self.draw_path.draw(g); } } + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum Obj { + InteriorRoad(RoadID), + InteriorIntersection(IntersectionID), +} +impl ObjectID for Obj {} + +fn make_world( + ctx: &mut EventCtx, + app: &App, + neighborhood: &Neighborhood, + rat_runs: &RatRuns, +) -> World { + let map = &app.primary.map; + let mut world = World::bounded(map.get_bounds()); + + for r in &neighborhood.orig_perimeter.interior { + world + .add(Obj::InteriorRoad(*r)) + .hitbox(map.get_r(*r).get_thick_polygon()) + .drawn_in_master_batch() + // TODO Not sure if tooltip() without this should imply it? + .draw_hovered(GeomBatch::new()) + .tooltip(Text::from(format!( + "{} rat-runs cross this street", + rat_runs.count_per_road.get(*r) + ))) + .build(ctx); + } + for i in &neighborhood.interior_intersections { + world + .add(Obj::InteriorIntersection(*i)) + .hitbox(map.get_i(*i).polygon.clone()) + .drawn_in_master_batch() + .draw_hovered(GeomBatch::new()) + .tooltip(Text::from(format!( + "{} rat-runs cross this intersection", + rat_runs.count_per_intersection.get(*i) + ))) + .build(ctx); + } + + world.initialize_hover(ctx); + + world +} diff --git a/game/src/ltn/rat_runs.rs b/game/src/ltn/rat_runs.rs index 0a13f1e747..63dd277b5c 100644 --- a/game/src/ltn/rat_runs.rs +++ b/game/src/ltn/rat_runs.rs @@ -1,15 +1,17 @@ use std::collections::HashSet; -use abstutil::Timer; +use abstutil::{Counter, Timer}; use map_model::{ DirectedRoadID, IntersectionID, LaneID, Map, Path, PathConstraints, PathRequest, PathStep, - Position, + Position, RoadID, }; use super::{ModalFilters, Neighborhood}; pub struct RatRuns { pub paths: Vec, + pub count_per_road: Counter, + pub count_per_intersection: Counter, } pub fn find_rat_runs( @@ -65,9 +67,33 @@ pub fn find_rat_runs( (pct * 1000.0) as usize }); - // TODO Heatmap of roads used (any direction) + // How many rat-runs pass through each street? + let mut count_per_road = Counter::new(); + let mut count_per_intersection = Counter::new(); + for path in &paths { + for step in path.get_steps() { + match step { + PathStep::Lane(l) => { + if neighborhood.orig_perimeter.interior.contains(&l.road) { + count_per_road.inc(l.road); + } + } + PathStep::Turn(t) => { + if neighborhood.interior_intersections.contains(&t.parent) { + count_per_intersection.inc(t.parent); + } + } + // Car paths don't make contraflow movements + _ => unreachable!(), + } + } + } - RatRuns { paths } + RatRuns { + paths, + count_per_road, + count_per_intersection, + } } struct EntryExit { diff --git a/widgetry/src/widgets/panel.rs b/widgetry/src/widgets/panel.rs index 98dda43890..35b74b5fb6 100644 --- a/widgetry/src/widgets/panel.rs +++ b/widgetry/src/widgets/panel.rs @@ -417,6 +417,9 @@ impl Panel { None } } + pub fn set_checked(&mut self, name: &str, on_off: bool) { + self.find_mut::(name).enabled = on_off + } pub fn text_box(&self, name: &str) -> String { self.find::(name).get_line()