A new generic UI to compare counts from two files.

Also moving some of the counts stuff to map_gui, accordingly
This commit is contained in:
Dustin Carlino 2022-01-27 13:59:38 +00:00
parent c33ba85cfe
commit edfd320908
7 changed files with 176 additions and 41 deletions

View File

@ -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<dyn State<App>> {
let mut timer = Timer::throwaway();
// TODO File loaders
let counts_a = match abstio::maybe_read_json::<Counts>(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::<Counts>(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);
<dyn SimpleState<_>>::new_state(panel, Box::new(GenericCompareCounts { compare }))
}
}
impl SimpleState<App> 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<Transition> {
assert!(self.compare.panel_changed(panel));
None
}
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.compare.draw(g);
}
}

View File

@ -11,6 +11,7 @@ use widgetry::{Choice, EventCtx, Key, Line, Panel, SimpleState, State, Widget};
use crate::app::{App, Transition}; use crate::app::{App, Transition};
mod collisions; mod collisions;
pub mod compare_counts;
mod destinations; mod destinations;
pub mod kml; pub mod kml;
mod polygon; mod polygon;

View File

@ -114,6 +114,9 @@ struct Args {
/// Start by showing an ActDev scenario. Either "base" or "go_active". /// Start by showing an ActDev scenario. Either "base" or "go_active".
#[structopt(long)] #[structopt(long)]
actdev_scenario: Option<String>, actdev_scenario: Option<String>,
/// Start in a tool for comparing traffic counts
#[structopt(long)]
compare_counts: Option<Vec<String>>,
} }
struct Setup { struct Setup {
@ -139,6 +142,7 @@ enum Mode {
Ungap, Ungap,
Devtools, Devtools,
LoadKML(String), LoadKML(String),
CompareCounts(String, String),
Gameplay(GameplayMode), Gameplay(GameplayMode),
} }
@ -182,6 +186,11 @@ fn run(mut settings: Settings) {
Mode::Devtools Mode::Devtools
} else if let Some(kml) = args.load_kml { } else if let Some(kml) = args.load_kml {
Mode::LoadKML(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 { } else {
Mode::SomethingElse Mode::SomethingElse
}, },
@ -557,6 +566,11 @@ fn finish_app_setup(
} }
Mode::Devtools => devtools::DevToolsMode::new_state(ctx, app), Mode::Devtools => devtools::DevToolsMode::new_state(ctx, app),
Mode::LoadKML(path) => crate::devtools::kml::ViewKML::new_state(ctx, app, Some(path)), 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] vec![TitleScreen::new_state(ctx, app), state]

View File

@ -1,4 +1,3 @@
mod compare;
mod ui; mod ui;
use std::collections::BTreeSet; use std::collections::BTreeSet;
@ -6,16 +5,15 @@ use std::collections::BTreeSet;
use abstio::MapName; use abstio::MapName;
use abstutil::{Counter, Timer}; use abstutil::{Counter, Timer};
use geom::{Duration, Time}; use geom::{Duration, Time};
use map_gui::tools::compare_counts::{CompareCounts, Counts};
use map_model::{Map, PathConstraints, PathRequest, PathStepV2, PathfinderCaching, RoutingParams}; use map_model::{Map, PathConstraints, PathRequest, PathStepV2, PathfinderCaching, RoutingParams};
use sim::{Scenario, TripEndpoint, TripMode}; use sim::{Scenario, TripEndpoint, TripMode};
use widgetry::EventCtx; use widgetry::EventCtx;
use self::compare::{CompareCounts, Counts};
pub use self::ui::ShowResults; pub use self::ui::ShowResults;
use crate::App; use crate::App;
// TODO Configurable main road penalty, like in the pathfinding tool // 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 // 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? // ... can't we just produce data of a certain shape, and have a UI pretty tuned for that?

View File

@ -1,7 +1,7 @@
use std::collections::BTreeSet; use std::collections::BTreeSet;
use map_gui::load::FileLoader; 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 sim::{Scenario, TripMode};
use widgetry::mapspace::ToggleZoomed; use widgetry::mapspace::ToggleZoomed;
use widgetry::{ use widgetry::{
@ -56,6 +56,7 @@ impl ShowResults {
// TODO Dropdown for the scenario, and explain its source/limitations // TODO Dropdown for the scenario, and explain its source/limitations
app.session.impact.filters.to_panel(ctx, app), app.session.impact.filters.to_panel(ctx, app),
app.session.impact.compare_counts.get_panel_widget(ctx), 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) .aligned(HorizontalAlignment::Left, VerticalAlignment::Top)
.build(ctx); .build(ctx);
@ -77,12 +78,31 @@ impl ShowResults {
impl SimpleState<App> for ShowResults { impl SimpleState<App> for ShowResults {
fn on_click(&mut self, ctx: &mut EventCtx, app: &mut App, x: &str, _: &Panel) -> Transition { fn on_click(&mut self, ctx: &mut EventCtx, app: &mut App, x: &str, _: &Panel) -> Transition {
if x == "close" { match x {
// Don't just Pop; if we updated the results, the UI won't warn the user about a slow "close" => {
// loading // Don't just Pop; if we updated the results, the UI won't warn the user about a slow
return Transition::Replace(BrowseNeighborhoods::new_state(ctx, app)); // 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 { fn other_event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {

View File

@ -1,19 +1,18 @@
// TODO Some of this may warrant a standalone tool, or being in game/devtools
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use abstio::MapName; use abstio::MapName;
use abstutil::{prettyprint_usize, Counter}; use abstutil::{prettyprint_usize, Counter};
use geom::{Distance, Histogram, Statistic}; use geom::{Distance, Histogram, Statistic};
use map_gui::tools::{cmp_count, ColorNetwork, DivergingScale};
use map_model::{IntersectionID, RoadID}; use map_model::{IntersectionID, RoadID};
use widgetry::mapspace::{ObjectID, ToggleZoomed, ToggleZoomedBuilder, World}; 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 // TODO Document all of this!
// 3) Make a new UI with a file picker and CLI shortcuts
// 4) See if we can dedupe requests in the impact prediction -- using this tool to validate // 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 // 5) Download the sensor data and get it in this format (and maybe filter simulated data to only
// match roads we have) // match roads we have)
@ -31,8 +30,8 @@ pub struct Counts {
pub struct CompareCounts { pub struct CompareCounts {
pub layer: Layer, pub layer: Layer,
counts_a: CountsUI, pub counts_a: CountsUI,
counts_b: CountsUI, pub counts_b: CountsUI,
world: World<Obj>, world: World<Obj>,
relative_heatmap: ToggleZoomed, relative_heatmap: ToggleZoomed,
} }
@ -51,7 +50,9 @@ pub enum Layer {
Compare, Compare,
} }
struct CountsUI { pub struct CountsUI {
// TODO Just embed Counts directly, and make that serialize a Counter?
map: MapName,
description: String, description: String,
heatmap: ToggleZoomed, heatmap: ToggleZoomed,
per_road: Counter<RoadID>, per_road: Counter<RoadID>,
@ -59,7 +60,7 @@ struct CountsUI {
} }
impl 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(); let mut per_road = Counter::new();
for (r, count) in counts.per_road { for (r, count) in counts.per_road {
per_road.add(r, count); per_road.add(r, count);
@ -70,9 +71,10 @@ impl CountsUI {
} }
let mut colorer = ColorNetwork::no_fading(app); let mut colorer = ColorNetwork::no_fading(app);
colorer.ranked_roads(per_road.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); colorer.ranked_intersections(per_intersection.clone(), &app.cs().good_to_bad_red);
CountsUI { CountsUI {
map: counts.map,
description: counts.description, description: counts.description,
heatmap: colorer.build(ctx), heatmap: colorer.build(ctx),
per_road, per_road,
@ -82,18 +84,33 @@ impl CountsUI {
fn empty(ctx: &EventCtx) -> Self { fn empty(ctx: &EventCtx) -> Self {
Self { Self {
map: MapName::new("zz", "place", "holder"),
description: String::new(), description: String::new(),
heatmap: ToggleZoomed::empty(ctx), heatmap: ToggleZoomed::empty(ctx),
per_road: Counter::new(), per_road: Counter::new(),
per_intersection: 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 { impl CompareCounts {
pub fn new( pub fn new(
ctx: &mut EventCtx, ctx: &mut EventCtx,
app: &App, app: &dyn AppLike,
counts_a: Counts, counts_a: Counts,
counts_b: Counts, counts_b: Counts,
layer: Layer, 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.counts_b = CountsUI::new(ctx, app, counts_b);
self.relative_heatmap = self.relative_heatmap =
calculate_relative_heatmap(ctx, app, &self.counts_a, &self.counts_b); calculate_relative_heatmap(ctx, app, &self.counts_a, &self.counts_b);
@ -152,10 +169,11 @@ impl CompareCounts {
ctx, ctx,
"layer", "layer",
self.layer, self.layer,
// TODO A dropdown is actually annoying, the hotkeys don't work without a click
vec![ vec![
Choice::new(&self.counts_a.description, Layer::A), Choice::new(&self.counts_a.description, Layer::A).key(Key::Num1),
Choice::new(&self.counts_b.description, Layer::B), Choice::new(&self.counts_b.description, Layer::B).key(Key::Num2),
Choice::new("compare", Layer::Compare), Choice::new("compare", Layer::Compare).key(Key::Num3),
], ],
), ),
]) ])
@ -198,16 +216,27 @@ impl CompareCounts {
} }
fn relative_road_tooltip(&self, r: RoadID) -> Text { fn relative_road_tooltip(&self, r: RoadID) -> Text {
let before = self.counts_a.per_road.get(r); let a = self.counts_a.per_road.get(r);
let after = self.counts_b.per_road.get(r); let b = self.counts_b.per_road.get(r);
let ratio = (after as f64) / (before as f64); let ratio = (b as f64) / (a as f64);
let mut txt = Text::from_multiline(vec![ let mut txt = Text::from_multiline(vec![
Line(format!("Before: {}", prettyprint_usize(before))), Line(format!(
Line(format!("After: {}", prettyprint_usize(after))), "{}: {}",
self.counts_a.description,
prettyprint_usize(a)
)),
Line(format!(
"{}: {}",
self.counts_b.description,
prettyprint_usize(b)
)),
]); ]);
cmp_count(&mut txt, before, after); cmp_count(&mut txt, a, b);
txt.add_line(Line(format!("After/before: {:.2}", ratio))); txt.add_line(Line(format!(
"{}/{}: {:.2}",
self.counts_b.description, self.counts_a.description, ratio
)));
txt txt
} }
@ -229,7 +258,7 @@ impl CompareCounts {
fn calculate_relative_heatmap( fn calculate_relative_heatmap(
ctx: &EventCtx, ctx: &EventCtx,
app: &App, app: &dyn AppLike,
counts_a: &CountsUI, counts_a: &CountsUI,
counts_b: &CountsUI, counts_b: &CountsUI,
) -> ToggleZoomed { ) -> ToggleZoomed {
@ -247,7 +276,7 @@ fn calculate_relative_heatmap(
// What's physical road width look like? // What's physical road width look like?
let mut hgram_width = Histogram::new(); 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()); hgram_width.add(r.get_width());
} }
info!("Physical road widths: {}", hgram_width.describe()); 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... // TODO Pretty arbitrary. Ideally we'd hide roads and intersections underneath...
let width = Distance::meters(2.0) + pct_count * Distance::meters(10.0); 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) ToggleZoomedBuilder::from(draw_roads).build(ctx)
} }
fn make_world(ctx: &mut EventCtx, app: &App) -> World<Obj> { fn make_world(ctx: &mut EventCtx, app: &dyn AppLike) -> World<Obj> {
let mut world = World::bounded(app.map.get_bounds()); let mut world = World::bounded(app.map().get_bounds());
for r in app.map.all_roads() { for r in app.map().all_roads() {
world world
.add(Obj::Road(r.id)) .add(Obj::Road(r.id))
.hitbox(r.get_thick_polygon()) .hitbox(r.get_thick_polygon())
@ -290,7 +319,7 @@ fn make_world(ctx: &mut EventCtx, app: &App) -> World<Obj> {
.invisibly_hoverable() .invisibly_hoverable()
.build(ctx); .build(ctx);
} }
for i in app.map.all_intersections() { for i in app.map().all_intersections() {
world world
.add(Obj::Intersection(i.id)) .add(Obj::Intersection(i.id))
.hitbox(i.polygon.clone()) .hitbox(i.polygon.clone())

View File

@ -36,6 +36,7 @@ mod city_picker;
mod colors; mod colors;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
mod command; mod command;
pub mod compare_counts;
mod heatmap; mod heatmap;
mod icons; mod icons;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]