From edfd3209082cfb29bfc45e7bb7f16695ffb9a4c6 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Thu, 27 Jan 2022 13:59:38 +0000 Subject: [PATCH] A new generic UI to compare counts from two files. Also moving some of the counts stuff to map_gui, accordingly --- game/src/devtools/compare_counts.rs | 72 ++++++++++++++ game/src/devtools/mod.rs | 1 + game/src/lib.rs | 14 +++ ltn/src/impact/mod.rs | 4 +- ltn/src/impact/ui.rs | 32 +++++-- .../src/tools/compare_counts.rs | 93 ++++++++++++------- map_gui/src/tools/mod.rs | 1 + 7 files changed, 176 insertions(+), 41 deletions(-) create mode 100644 game/src/devtools/compare_counts.rs rename ltn/src/impact/compare.rs => map_gui/src/tools/compare_counts.rs (77%) diff --git a/game/src/devtools/compare_counts.rs b/game/src/devtools/compare_counts.rs new file mode 100644 index 0000000000..86929b292f --- /dev/null +++ b/game/src/devtools/compare_counts.rs @@ -0,0 +1,72 @@ +use abstutil::Timer; +use map_gui::tools::compare_counts::{CompareCounts, Counts, Layer}; +use map_gui::tools::PopupMsg; +use widgetry::{ + EventCtx, GfxCtx, HorizontalAlignment, Panel, SimpleState, State, VerticalAlignment, Widget, +}; + +use crate::app::{App, Transition}; + +pub struct GenericCompareCounts { + compare: CompareCounts, +} + +impl GenericCompareCounts { + pub fn new_state( + ctx: &mut EventCtx, + app: &mut App, + path1: String, + path2: String, + ) -> Box> { + let mut timer = Timer::throwaway(); + // TODO File loaders + let counts_a = match abstio::maybe_read_json::(path1, &mut timer) { + Ok(c) => c, + Err(err) => { + return PopupMsg::new_state(ctx, "Error", vec![err.to_string()]); + } + }; + let counts_b = match abstio::maybe_read_json::(path2, &mut timer) { + Ok(c) => c, + Err(err) => { + return PopupMsg::new_state(ctx, "Error", vec![err.to_string()]); + } + }; + let mut compare = CompareCounts::new(ctx, app, counts_a, counts_b, Layer::A); + compare.autoselect_layer(); + + let panel = Panel::new_builder(Widget::col(vec![ + map_gui::tools::app_header(ctx, app, "Traffic count comparator"), + compare.get_panel_widget(ctx), + ])) + .aligned(HorizontalAlignment::Left, VerticalAlignment::Top) + .build(ctx); + + >::new_state(panel, Box::new(GenericCompareCounts { compare })) + } +} + +impl SimpleState for GenericCompareCounts { + fn on_click(&mut self, _: &mut EventCtx, _: &mut App, _: &str, _: &Panel) -> Transition { + unreachable!() + } + + fn other_event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition { + self.compare.other_event(ctx); + Transition::Keep + } + + fn panel_changed( + &mut self, + _: &mut EventCtx, + _: &mut App, + panel: &mut Panel, + ) -> Option { + assert!(self.compare.panel_changed(panel)); + None + } + + fn draw(&self, g: &mut GfxCtx, _: &App) { + self.compare.draw(g); + } +} diff --git a/game/src/devtools/mod.rs b/game/src/devtools/mod.rs index 3713dcb894..b2447d6e48 100644 --- a/game/src/devtools/mod.rs +++ b/game/src/devtools/mod.rs @@ -11,6 +11,7 @@ use widgetry::{Choice, EventCtx, Key, Line, Panel, SimpleState, State, Widget}; use crate::app::{App, Transition}; mod collisions; +pub mod compare_counts; mod destinations; pub mod kml; mod polygon; diff --git a/game/src/lib.rs b/game/src/lib.rs index 0a420e6abf..37fe92668b 100644 --- a/game/src/lib.rs +++ b/game/src/lib.rs @@ -114,6 +114,9 @@ struct Args { /// Start by showing an ActDev scenario. Either "base" or "go_active". #[structopt(long)] actdev_scenario: Option, + /// Start in a tool for comparing traffic counts + #[structopt(long)] + compare_counts: Option>, } struct Setup { @@ -139,6 +142,7 @@ enum Mode { Ungap, Devtools, LoadKML(String), + CompareCounts(String, String), Gameplay(GameplayMode), } @@ -182,6 +186,11 @@ fn run(mut settings: Settings) { Mode::Devtools } else if let Some(kml) = args.load_kml { Mode::LoadKML(kml) + } else if let Some(mut paths) = args.compare_counts { + if paths.len() != 2 { + panic!("--compare-counts takes exactly two paths"); + } + Mode::CompareCounts(paths.remove(0), paths.remove(0)) } else { Mode::SomethingElse }, @@ -557,6 +566,11 @@ fn finish_app_setup( } Mode::Devtools => devtools::DevToolsMode::new_state(ctx, app), Mode::LoadKML(path) => crate::devtools::kml::ViewKML::new_state(ctx, app, Some(path)), + Mode::CompareCounts(path1, path2) => { + crate::devtools::compare_counts::GenericCompareCounts::new_state( + ctx, app, path1, path2, + ) + } } }; vec![TitleScreen::new_state(ctx, app), state] diff --git a/ltn/src/impact/mod.rs b/ltn/src/impact/mod.rs index dfdce49e54..4e046eb0ed 100644 --- a/ltn/src/impact/mod.rs +++ b/ltn/src/impact/mod.rs @@ -1,4 +1,3 @@ -mod compare; mod ui; use std::collections::BTreeSet; @@ -6,16 +5,15 @@ use std::collections::BTreeSet; use abstio::MapName; use abstutil::{Counter, Timer}; use geom::{Duration, Time}; +use map_gui::tools::compare_counts::{CompareCounts, Counts}; use map_model::{Map, PathConstraints, PathRequest, PathStepV2, PathfinderCaching, RoutingParams}; use sim::{Scenario, TripEndpoint, TripMode}; use widgetry::EventCtx; -use self::compare::{CompareCounts, Counts}; pub use self::ui::ShowResults; use crate::App; // 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? diff --git a/ltn/src/impact/ui.rs b/ltn/src/impact/ui.rs index 7d31b9efed..e198d6b6d0 100644 --- a/ltn/src/impact/ui.rs +++ b/ltn/src/impact/ui.rs @@ -1,7 +1,7 @@ use std::collections::BTreeSet; use map_gui::load::FileLoader; -use map_gui::tools::checkbox_per_mode; +use map_gui::tools::{checkbox_per_mode, PopupMsg}; use sim::{Scenario, TripMode}; use widgetry::mapspace::ToggleZoomed; use widgetry::{ @@ -56,6 +56,7 @@ impl ShowResults { // TODO Dropdown for the scenario, and explain its source/limitations app.session.impact.filters.to_panel(ctx, app), app.session.impact.compare_counts.get_panel_widget(ctx), + ctx.style().btn_plain.text("Save before/after counts to files").build_def(ctx), ])) .aligned(HorizontalAlignment::Left, VerticalAlignment::Top) .build(ctx); @@ -77,12 +78,31 @@ impl ShowResults { impl SimpleState for ShowResults { fn on_click(&mut self, ctx: &mut EventCtx, app: &mut App, x: &str, _: &Panel) -> Transition { - if x == "close" { - // Don't just Pop; if we updated the results, the UI won't warn the user about a slow - // loading - return Transition::Replace(BrowseNeighborhoods::new_state(ctx, app)); + match x { + "close" => { + // Don't just Pop; if we updated the results, the UI won't warn the user about a slow + // loading + Transition::Replace(BrowseNeighborhoods::new_state(ctx, app)) + } + "Save before/after counts to files" => { + let path1 = "counts_a.json"; + let path2 = "counts_b.json"; + abstio::write_json( + path1.to_string(), + &app.session.impact.compare_counts.counts_a.to_counts(), + ); + abstio::write_json( + path2.to_string(), + &app.session.impact.compare_counts.counts_b.to_counts(), + ); + Transition::Push(PopupMsg::new_state( + ctx, + "Saved", + vec![format!("Saved {} and {}", path1, path2)], + )) + } + _ => unreachable!(), } - unreachable!() } fn other_event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition { diff --git a/ltn/src/impact/compare.rs b/map_gui/src/tools/compare_counts.rs similarity index 77% rename from ltn/src/impact/compare.rs rename to map_gui/src/tools/compare_counts.rs index 586a4f4df9..b2329a450a 100644 --- a/ltn/src/impact/compare.rs +++ b/map_gui/src/tools/compare_counts.rs @@ -1,19 +1,18 @@ -// TODO Some of this may warrant a standalone tool, or being in game/devtools - use serde::{Deserialize, Serialize}; use abstio::MapName; use abstutil::{prettyprint_usize, Counter}; use geom::{Distance, Histogram, Statistic}; -use map_gui::tools::{cmp_count, ColorNetwork, DivergingScale}; use map_model::{IntersectionID, RoadID}; use widgetry::mapspace::{ObjectID, ToggleZoomed, ToggleZoomedBuilder, World}; -use widgetry::{Choice, Color, EventCtx, GeomBatch, GfxCtx, Line, Panel, Text, TextExt, Widget}; +use widgetry::{ + Choice, Color, EventCtx, GeomBatch, GfxCtx, Key, Line, Panel, Text, TextExt, Widget, +}; -use super::App; +use crate::tools::{cmp_count, ColorNetwork, DivergingScale}; +use crate::AppLike; -// TODO -// 3) Make a new UI with a file picker and CLI shortcuts +// TODO Document all of this! // 4) See if we can dedupe requests in the impact prediction -- using this tool to validate // 5) Download the sensor data and get it in this format (and maybe filter simulated data to only // match roads we have) @@ -31,8 +30,8 @@ pub struct Counts { pub struct CompareCounts { pub layer: Layer, - counts_a: CountsUI, - counts_b: CountsUI, + pub counts_a: CountsUI, + pub counts_b: CountsUI, world: World, relative_heatmap: ToggleZoomed, } @@ -51,7 +50,9 @@ pub enum Layer { Compare, } -struct CountsUI { +pub struct CountsUI { + // TODO Just embed Counts directly, and make that serialize a Counter? + map: MapName, description: String, heatmap: ToggleZoomed, per_road: Counter, @@ -59,7 +60,7 @@ struct CountsUI { } impl CountsUI { - fn new(ctx: &EventCtx, app: &App, counts: Counts) -> CountsUI { + fn new(ctx: &EventCtx, app: &dyn AppLike, counts: Counts) -> CountsUI { let mut per_road = Counter::new(); for (r, count) in counts.per_road { per_road.add(r, count); @@ -70,9 +71,10 @@ impl CountsUI { } let mut colorer = ColorNetwork::no_fading(app); - colorer.ranked_roads(per_road.clone(), &app.cs.good_to_bad_red); - colorer.ranked_intersections(per_intersection.clone(), &app.cs.good_to_bad_red); + colorer.ranked_roads(per_road.clone(), &app.cs().good_to_bad_red); + colorer.ranked_intersections(per_intersection.clone(), &app.cs().good_to_bad_red); CountsUI { + map: counts.map, description: counts.description, heatmap: colorer.build(ctx), per_road, @@ -82,18 +84,33 @@ impl CountsUI { fn empty(ctx: &EventCtx) -> Self { Self { + map: MapName::new("zz", "place", "holder"), description: String::new(), heatmap: ToggleZoomed::empty(ctx), per_road: Counter::new(), per_intersection: Counter::new(), } } + + pub fn to_counts(&self) -> Counts { + Counts { + map: self.map.clone(), + description: self.description.clone(), + per_road: self.per_road.clone().consume().into_iter().collect(), + per_intersection: self + .per_intersection + .clone() + .consume() + .into_iter() + .collect(), + } + } } impl CompareCounts { pub fn new( ctx: &mut EventCtx, - app: &App, + app: &dyn AppLike, counts_a: Counts, counts_b: Counts, layer: Layer, @@ -123,7 +140,7 @@ impl CompareCounts { }; } - pub fn recalculate_b(&mut self, ctx: &EventCtx, app: &App, counts_b: Counts) { + pub fn recalculate_b(&mut self, ctx: &EventCtx, app: &dyn AppLike, counts_b: Counts) { self.counts_b = CountsUI::new(ctx, app, counts_b); self.relative_heatmap = calculate_relative_heatmap(ctx, app, &self.counts_a, &self.counts_b); @@ -152,10 +169,11 @@ impl CompareCounts { ctx, "layer", self.layer, + // TODO A dropdown is actually annoying, the hotkeys don't work without a click vec![ - Choice::new(&self.counts_a.description, Layer::A), - Choice::new(&self.counts_b.description, Layer::B), - Choice::new("compare", Layer::Compare), + Choice::new(&self.counts_a.description, Layer::A).key(Key::Num1), + Choice::new(&self.counts_b.description, Layer::B).key(Key::Num2), + Choice::new("compare", Layer::Compare).key(Key::Num3), ], ), ]) @@ -198,16 +216,27 @@ impl CompareCounts { } fn relative_road_tooltip(&self, r: RoadID) -> Text { - let before = self.counts_a.per_road.get(r); - let after = self.counts_b.per_road.get(r); - let ratio = (after as f64) / (before as f64); + let a = self.counts_a.per_road.get(r); + let b = self.counts_b.per_road.get(r); + let ratio = (b as f64) / (a as f64); let mut txt = Text::from_multiline(vec![ - Line(format!("Before: {}", prettyprint_usize(before))), - Line(format!("After: {}", prettyprint_usize(after))), + Line(format!( + "{}: {}", + self.counts_a.description, + prettyprint_usize(a) + )), + Line(format!( + "{}: {}", + self.counts_b.description, + prettyprint_usize(b) + )), ]); - cmp_count(&mut txt, before, after); - txt.add_line(Line(format!("After/before: {:.2}", ratio))); + cmp_count(&mut txt, a, b); + txt.add_line(Line(format!( + "{}/{}: {:.2}", + self.counts_b.description, self.counts_a.description, ratio + ))); txt } @@ -229,7 +258,7 @@ impl CompareCounts { fn calculate_relative_heatmap( ctx: &EventCtx, - app: &App, + app: &dyn AppLike, counts_a: &CountsUI, counts_b: &CountsUI, ) -> ToggleZoomed { @@ -247,7 +276,7 @@ fn calculate_relative_heatmap( // What's physical road width look like? let mut hgram_width = Histogram::new(); - for r in app.map.all_roads() { + for r in app.map().all_roads() { hgram_width.add(r.get_width()); } info!("Physical road widths: {}", hgram_width.describe()); @@ -275,14 +304,14 @@ fn calculate_relative_heatmap( // TODO Pretty arbitrary. Ideally we'd hide roads and intersections underneath... let width = Distance::meters(2.0) + pct_count * Distance::meters(10.0); - draw_roads.push(color, app.map.get_r(r).center_pts.make_polygons(width)); + draw_roads.push(color, app.map().get_r(r).center_pts.make_polygons(width)); } ToggleZoomedBuilder::from(draw_roads).build(ctx) } -fn make_world(ctx: &mut EventCtx, app: &App) -> World { - let mut world = World::bounded(app.map.get_bounds()); - for r in app.map.all_roads() { +fn make_world(ctx: &mut EventCtx, app: &dyn AppLike) -> World { + let mut world = World::bounded(app.map().get_bounds()); + for r in app.map().all_roads() { world .add(Obj::Road(r.id)) .hitbox(r.get_thick_polygon()) @@ -290,7 +319,7 @@ fn make_world(ctx: &mut EventCtx, app: &App) -> World { .invisibly_hoverable() .build(ctx); } - for i in app.map.all_intersections() { + for i in app.map().all_intersections() { world .add(Obj::Intersection(i.id)) .hitbox(i.polygon.clone()) diff --git a/map_gui/src/tools/mod.rs b/map_gui/src/tools/mod.rs index 8710192018..7c793b2d30 100644 --- a/map_gui/src/tools/mod.rs +++ b/map_gui/src/tools/mod.rs @@ -36,6 +36,7 @@ mod city_picker; mod colors; #[cfg(not(target_arch = "wasm32"))] mod command; +pub mod compare_counts; mod heatmap; mod icons; #[cfg(not(target_arch = "wasm32"))]