mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-11-28 03:35:51 +03:00
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:
parent
c33ba85cfe
commit
edfd320908
72
game/src/devtools/compare_counts.rs
Normal file
72
game/src/devtools/compare_counts.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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]
|
||||
|
@ -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?
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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())
|
@ -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"))]
|
||||
|
Loading…
Reference in New Issue
Block a user