mvp of graphing finished trips over time

This commit is contained in:
Dustin Carlino 2019-10-01 14:09:17 -07:00
parent 71096cf122
commit 6f75cf32eb
11 changed files with 214 additions and 8 deletions

View File

@ -170,6 +170,7 @@ fn launch_test(test: &ABTest, ui: &mut UI, ctx: &mut EventCtx) -> ABTestMode {
savestate_every: None, savestate_every: None,
freeform_policy: current_flags.sim_flags.freeform_policy, freeform_policy: current_flags.sim_flags.freeform_policy,
disable_block_the_box: current_flags.sim_flags.disable_block_the_box, disable_block_the_box: current_flags.sim_flags.disable_block_the_box,
record_stats: false,
}, },
..current_flags.clone() ..current_flags.clone()
}, },

View File

@ -91,7 +91,7 @@ impl State for EditMode {
return Transition::Pop; return Transition::Pop;
} }
if self.menu.action("sandbox mode") { if self.menu.action("sandbox mode") {
return Transition::Replace(Box::new(SandboxMode::new(ctx))); return Transition::Replace(Box::new(SandboxMode::new(ctx, ui)));
} }
if self.menu.action("debug mode") { if self.menu.action("debug mode") {
return Transition::Push(Box::new(DebugMode::new(ctx, ui))); return Transition::Push(Box::new(DebugMode::new(ctx, ui)));

View File

@ -21,7 +21,7 @@ impl Game {
} else { } else {
vec![ vec![
Box::new(SplashScreen::new_without_screensaver()), Box::new(SplashScreen::new_without_screensaver()),
Box::new(SandboxMode::new(ctx)), Box::new(SandboxMode::new(ctx, &ui)),
] ]
}; };
Game { states, ui } Game { states, ui }

View File

@ -78,7 +78,7 @@ impl State for MissionEditMode {
.sim .sim
.step(&ui.primary.map, Duration::const_seconds(0.1)); .step(&ui.primary.map, Duration::const_seconds(0.1));
}); });
return Transition::Replace(Box::new(SandboxMode::new(ctx))); return Transition::Replace(Box::new(SandboxMode::new(ctx, ui)));
} else if self.menu.action("create scenario from PSRC trips") { } else if self.menu.action("create scenario from PSRC trips") {
return Transition::Push(WizardState::new(Box::new(convert_trips_to_scenario))); return Transition::Push(WizardState::new(Box::new(convert_trips_to_scenario)));
} else if self.menu.action("manage neighborhoods") { } else if self.menu.action("manage neighborhoods") {

View File

@ -198,7 +198,7 @@ impl State for ScenarioManager {
); );
ui.primary.sim.step(&ui.primary.map, Duration::seconds(0.1)); ui.primary.sim.step(&ui.primary.map, Duration::seconds(0.1));
}); });
return Transition::Replace(Box::new(SandboxMode::new(ctx))); return Transition::Replace(Box::new(SandboxMode::new(ctx, ui)));
} }
if let Some(ID::Building(b)) = ui.primary.current_selection { if let Some(ID::Building(b)) = ui.primary.current_selection {

View File

@ -1,5 +1,6 @@
mod score; mod score;
mod spawner; mod spawner;
mod stats;
mod time_travel; mod time_travel;
use crate::common::{ use crate::common::{
@ -24,6 +25,7 @@ pub struct SandboxMode {
speed: SpeedControls, speed: SpeedControls,
agent_tools: AgentTools, agent_tools: AgentTools,
pub time_travel: time_travel::InactiveTimeTravel, pub time_travel: time_travel::InactiveTimeTravel,
stats: stats::TripStats,
common: CommonState, common: CommonState,
parking_heatmap: Option<(Duration, RoadColorer)>, parking_heatmap: Option<(Duration, RoadColorer)>,
intersection_delay_heatmap: Option<(Duration, ObjectColorer)>, intersection_delay_heatmap: Option<(Duration, ObjectColorer)>,
@ -31,11 +33,12 @@ pub struct SandboxMode {
} }
impl SandboxMode { impl SandboxMode {
pub fn new(ctx: &mut EventCtx) -> SandboxMode { pub fn new(ctx: &mut EventCtx, ui: &UI) -> SandboxMode {
SandboxMode { SandboxMode {
speed: SpeedControls::new(ctx, None), speed: SpeedControls::new(ctx, None),
agent_tools: AgentTools::new(ctx), agent_tools: AgentTools::new(ctx),
time_travel: time_travel::InactiveTimeTravel::new(), time_travel: time_travel::InactiveTimeTravel::new(),
stats: stats::TripStats::new(ui.primary.current_flags.sim_flags.record_stats),
common: CommonState::new(), common: CommonState::new(),
parking_heatmap: None, parking_heatmap: None,
intersection_delay_heatmap: None, intersection_delay_heatmap: None,
@ -64,6 +67,7 @@ impl SandboxMode {
(hotkey(Key::I), "show/hide intersection delay"), (hotkey(Key::I), "show/hide intersection delay"),
(hotkey(Key::T), "start time traveling"), (hotkey(Key::T), "start time traveling"),
(hotkey(Key::Q), "scoreboard"), (hotkey(Key::Q), "scoreboard"),
(None, "trip stats"),
], ],
vec![ vec![
(hotkey(Key::Escape), "quit"), (hotkey(Key::Escape), "quit"),
@ -84,6 +88,7 @@ impl SandboxMode {
impl State for SandboxMode { impl State for SandboxMode {
fn event(&mut self, ctx: &mut EventCtx, ui: &mut UI) -> Transition { fn event(&mut self, ctx: &mut EventCtx, ui: &mut UI) -> Transition {
self.time_travel.record(ui); self.time_travel.record(ui);
self.stats.record(ui);
{ {
let mut txt = Text::prompt("Sandbox Mode"); let mut txt = Text::prompt("Sandbox Mode");
@ -118,6 +123,9 @@ impl State for SandboxMode {
if self.menu.action("scoreboard") { if self.menu.action("scoreboard") {
return Transition::Push(Box::new(score::Scoreboard::new(ctx, ui))); return Transition::Push(Box::new(score::Scoreboard::new(ctx, ui)));
} }
if self.menu.action("trip stats") {
return Transition::Push(Box::new(stats::ShowStats::new(&self.stats, ui, ctx)));
}
if self.menu.action("show/hide parking availability") { if self.menu.action("show/hide parking availability") {
if self.parking_heatmap.is_some() { if self.parking_heatmap.is_some() {
self.parking_heatmap = None; self.parking_heatmap = None;
@ -206,7 +214,7 @@ impl State for SandboxMode {
if self.speed.is_paused() { if self.speed.is_paused() {
if !ui.primary.sim.is_empty() && self.menu.action("reset sim") { if !ui.primary.sim.is_empty() && self.menu.action("reset sim") {
ui.primary.reset_sim(); ui.primary.reset_sim();
return Transition::Replace(Box::new(SandboxMode::new(ctx))); return Transition::Replace(Box::new(SandboxMode::new(ctx, ui)));
} }
if self.menu.action("save sim state") { if self.menu.action("save sim state") {
ctx.loading_screen("savestate", |_, timer| { ctx.loading_screen("savestate", |_, timer| {

190
game/src/sandbox/stats.rs Normal file
View File

@ -0,0 +1,190 @@
use crate::common::ColorLegend;
use crate::game::{State, Transition};
use crate::ui::UI;
use ezgui::{hotkey, Color, Drawable, EventCtx, GeomBatch, GfxCtx, Key, Line, ModalMenu, Text};
use geom::{Distance, Duration, PolyLine, Polygon, Pt2D};
use sim::TripMode;
pub struct TripStats {
should_record: bool,
samples: Vec<StateAtTime>,
}
struct StateAtTime {
time: Duration,
// These're all cumulative
finished_walk_trips: usize,
finished_bike_trips: usize,
finished_transit_trips: usize,
finished_drive_trips: usize,
aborted_trips: usize,
}
impl TripStats {
pub fn new(should_record: bool) -> TripStats {
TripStats {
should_record,
samples: Vec::new(),
}
}
pub fn record(&mut self, ui: &UI) {
if !self.should_record {
return;
}
if let Some(ref state) = self.samples.last() {
// Already have this
if ui.primary.sim.time() == state.time {
return;
}
// We just loaded a new savestate or reset or something. Clear out our memory.
if ui.primary.sim.time() < state.time {
self.samples.clear();
}
}
let t = ui.primary.sim.get_finished_trips();
let mut state = StateAtTime {
time: ui.primary.sim.time(),
finished_walk_trips: 0,
finished_bike_trips: 0,
finished_transit_trips: 0,
finished_drive_trips: 0,
aborted_trips: t.aborted_trips,
};
for (_, m, _) in t.finished_trips {
match m {
TripMode::Walk => {
state.finished_walk_trips += 1;
}
TripMode::Bike => {
state.finished_bike_trips += 1;
}
TripMode::Transit => {
state.finished_transit_trips += 1;
}
TripMode::Drive => {
state.finished_drive_trips += 1;
}
}
}
self.samples.push(state);
}
}
pub struct ShowStats {
menu: ModalMenu,
draw: Drawable,
legend: ColorLegend,
}
impl State for ShowStats {
fn event(&mut self, ctx: &mut EventCtx, _: &mut UI) -> Transition {
self.menu.handle_event(ctx, None);
if self.menu.action("quit") {
return Transition::Pop;
}
Transition::Keep
}
fn draw(&self, g: &mut GfxCtx, _: &UI) {
self.menu.draw(g);
self.legend.draw(g);
g.fork_screenspace();
g.redraw(&self.draw);
g.unfork();
}
}
impl ShowStats {
pub fn new(stats: &TripStats, ui: &UI, ctx: &mut EventCtx) -> ShowStats {
let mut batch = GeomBatch::new();
let x1 = 0.2 * ctx.canvas.window_width;
let x2 = 0.8 * ctx.canvas.window_width;
let y1 = 0.2 * ctx.canvas.window_height;
let y2 = 0.8 * ctx.canvas.window_height;
batch.push(
Color::grey(0.8),
Polygon::rectangle_topleft(
Pt2D::new(x1, y1),
Distance::meters(x2 - x1),
Distance::meters(y2 - y1),
),
);
let lines: Vec<(&str, Color, Box<dyn Fn(&StateAtTime) -> usize>)> = vec![
(
"walking",
ui.cs.get("unzoomed pedestrian").alpha(1.0),
Box::new(|s| s.finished_walk_trips),
),
(
"biking",
ui.cs.get("unzoomed bike").alpha(1.0),
Box::new(|s| s.finished_bike_trips),
),
(
"transit",
ui.cs.get("unzoomed bus").alpha(1.0),
Box::new(|s| s.finished_transit_trips),
),
(
"driving",
ui.cs.get("unzoomed car").alpha(1.0),
Box::new(|s| s.finished_drive_trips),
),
("aborted", Color::PURPLE, Box::new(|s| s.aborted_trips)),
];
let legend = ColorLegend::new(
"finished trips",
lines
.iter()
.map(|(name, color, _)| (*name, *color))
.collect(),
);
for (_, color, getter) in lines {
if stats.samples.is_empty() {
continue;
}
let max_y = stats.samples.iter().map(&getter).max().unwrap();
let mut pts = Vec::new();
if max_y == 0 {
pts.push(Pt2D::new(x1, y2));
pts.push(Pt2D::new(x2, y2));
} else {
let num_pts = stats.samples.len().min(10);
for i in 0..num_pts {
let percent_x = (i as f64) / ((num_pts - 1) as f64);
let value = getter(
&stats.samples[(percent_x * ((stats.samples.len() - 1) as f64)) as usize],
);
let percent_y = (value as f64) / (max_y as f64);
pts.push(Pt2D::new(
x1 + (x2 - x1) * percent_x,
// Y inversion! :D
y2 - (y2 - y1) * percent_y,
));
}
}
batch.push(
color,
PolyLine::new(pts).make_polygons(Distance::meters(5.0)),
);
}
let mut txt = Text::prompt("Trip Stats");
txt.add(Line(format!(
"{} samples",
abstutil::prettyprint_usize(stats.samples.len())
)));
ShowStats {
menu: ModalMenu::new("Trip Stats", vec![vec![(hotkey(Key::Escape), "quit")]], ctx)
.set_prompt(ctx, txt),
draw: ctx.prerender.upload(batch),
legend,
}
}
}

View File

@ -156,7 +156,7 @@ fn splash_screen(
.0 .0
.as_str() .as_str()
{ {
x if x == sandbox => Some(Transition::Push(Box::new(SandboxMode::new(ctx)))), x if x == sandbox => Some(Transition::Push(Box::new(SandboxMode::new(ctx, ui)))),
x if x == load_map => { x if x == load_map => {
if let Some(name) = wizard.choose_string("Load which map?", || { if let Some(name) = wizard.choose_string("Load which map?", || {
let current_map = ui.primary.map.get_name(); let current_map = ui.primary.map.get_name();
@ -171,7 +171,7 @@ fn splash_screen(
flags.sim_flags.load = abstutil::path_map(&name); flags.sim_flags.load = abstutil::path_map(&name);
*ui = UI::new(flags, ctx, false); *ui = UI::new(flags, ctx, false);
// TODO want to clear wizard and screensaver as we leave this state. // TODO want to clear wizard and screensaver as we leave this state.
Some(Transition::Push(Box::new(SandboxMode::new(ctx)))) Some(Transition::Push(Box::new(SandboxMode::new(ctx, ui))))
} else if wizard.aborted() { } else if wizard.aborted() {
Some(Transition::ReplaceWithMode( Some(Transition::ReplaceWithMode(
Box::new(SplashScreen { Box::new(SplashScreen {

View File

@ -488,6 +488,7 @@ impl PerMapUI {
savestate_every: flags.savestate_every, savestate_every: flags.savestate_every,
use_freeform_policy_everywhere: flags.freeform_policy, use_freeform_policy_everywhere: flags.freeform_policy,
disable_block_the_box: flags.disable_block_the_box, disable_block_the_box: flags.disable_block_the_box,
record_stats: flags.record_stats,
}, },
); );
} }

View File

@ -13,6 +13,7 @@ pub struct SimFlags {
pub savestate_every: Option<Duration>, pub savestate_every: Option<Duration>,
pub freeform_policy: bool, pub freeform_policy: bool,
pub disable_block_the_box: bool, pub disable_block_the_box: bool,
pub record_stats: bool,
} }
impl SimFlags { impl SimFlags {
@ -26,6 +27,7 @@ impl SimFlags {
savestate_every: args.optional_parse("--savestate_every", Duration::parse), savestate_every: args.optional_parse("--savestate_every", Duration::parse),
freeform_policy: args.enabled("--freeform_policy"), freeform_policy: args.enabled("--freeform_policy"),
disable_block_the_box: args.enabled("--disable_block_the_box"), disable_block_the_box: args.enabled("--disable_block_the_box"),
record_stats: args.enabled("--record_stats"),
} }
} }
@ -42,6 +44,7 @@ impl SimFlags {
savestate_every: None, savestate_every: None,
freeform_policy: false, freeform_policy: false,
disable_block_the_box: false, disable_block_the_box: false,
record_stats: false,
} }
} }
@ -65,6 +68,7 @@ impl SimFlags {
savestate_every: self.savestate_every, savestate_every: self.savestate_every,
use_freeform_policy_everywhere: self.freeform_policy, use_freeform_policy_everywhere: self.freeform_policy,
disable_block_the_box: self.disable_block_the_box, disable_block_the_box: self.disable_block_the_box,
record_stats: self.record_stats,
}; };
if self.load.starts_with("../data/save/") { if self.load.starts_with("../data/save/") {

View File

@ -62,6 +62,7 @@ pub struct SimOptions {
pub savestate_every: Option<Duration>, pub savestate_every: Option<Duration>,
pub use_freeform_policy_everywhere: bool, pub use_freeform_policy_everywhere: bool,
pub disable_block_the_box: bool, pub disable_block_the_box: bool,
pub record_stats: bool,
} }
impl SimOptions { impl SimOptions {
@ -71,6 +72,7 @@ impl SimOptions {
savestate_every: None, savestate_every: None,
use_freeform_policy_everywhere: false, use_freeform_policy_everywhere: false,
disable_block_the_box: false, disable_block_the_box: false,
record_stats: false,
} }
} }
} }