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};
mod collisions;
pub mod compare_counts;
mod destinations;
pub mod kml;
mod polygon;

View File

@ -114,6 +114,9 @@ struct Args {
/// Start by showing an ActDev scenario. Either "base" or "go_active".
#[structopt(long)]
actdev_scenario: Option<String>,
/// Start in a tool for comparing traffic counts
#[structopt(long)]
compare_counts: Option<Vec<String>>,
}
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]

View File

@ -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?

View File

@ -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<App> 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 {

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 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<Obj>,
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<RoadID>,
@ -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<Obj> {
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<Obj> {
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<Obj> {
.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())

View File

@ -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"))]