unifying parking availability, intersection delay, thruput stats into one exclusive thing that consistently auto-updates

This commit is contained in:
Dustin Carlino 2019-10-05 13:34:30 -07:00
parent b40b26def8
commit 85ebb225cd
3 changed files with 284 additions and 283 deletions

View File

@ -0,0 +1,265 @@
use crate::common::{ObjectColorer, ObjectColorerBuilder, RoadColorer, RoadColorerBuilder};
use crate::game::{Transition, WizardState};
use crate::helpers::ID;
use crate::sandbox::SandboxMode;
use crate::ui::UI;
use abstutil::Counter;
use ezgui::{Choice, Color, EventCtx, GfxCtx, ModalMenu};
use geom::Duration;
use map_model::{IntersectionID, RoadID, Traversable};
use sim::{Event, ParkingSpot};
use std::collections::HashSet;
pub enum Analytics {
Inactive,
ParkingAvailability(Duration, RoadColorer),
IntersectionDelay(Duration, ObjectColorer),
Throughput(Duration, ObjectColorer),
}
impl Analytics {
pub fn event(
&mut self,
ctx: &mut EventCtx,
ui: &UI,
menu: &mut ModalMenu,
thruput_stats: &ThruputStats,
) -> Option<Transition> {
if menu.action("change analytics overlay") {
return Some(Transition::Push(WizardState::new(Box::new(
|wiz, ctx, _| {
let (choice, _) =
wiz.wrap(ctx).choose("Show which analytics overlay?", || {
vec![
Choice::new("none", ()),
Choice::new("parking availability", ()),
Choice::new("intersection delay", ()),
Choice::new("cumulative throughput", ()),
]
})?;
Some(Transition::PopWithData(Box::new(move |state, ui, ctx| {
let mut sandbox = state.downcast_mut::<SandboxMode>().unwrap();
sandbox.analytics =
Analytics::recalc(&choice, &sandbox.thruput_stats, ui, ctx);
})))
},
))));
}
let (choice, time) = match self {
Analytics::Inactive => {
return None;
}
Analytics::ParkingAvailability(t, _) => ("parking availability", *t),
Analytics::IntersectionDelay(t, _) => ("intersection delay", *t),
Analytics::Throughput(t, _) => ("cumulative throughput", *t),
};
if time != ui.primary.sim.time() {
*self = Analytics::recalc(choice, thruput_stats, ui, ctx);
}
None
}
// True if active and should block normal drawing
pub fn draw(&self, g: &mut GfxCtx, ui: &UI) -> bool {
match self {
Analytics::Inactive => false,
Analytics::ParkingAvailability(_, ref heatmap) => {
heatmap.draw(g, ui);
true
}
Analytics::IntersectionDelay(_, ref heatmap)
| Analytics::Throughput(_, ref heatmap) => {
heatmap.draw(g, ui);
true
}
}
}
fn recalc(
choice: &str,
thruput_stats: &ThruputStats,
ui: &UI,
ctx: &mut EventCtx,
) -> Analytics {
let time = ui.primary.sim.time();
match choice {
"none" => Analytics::Inactive,
"parking availability" => {
Analytics::ParkingAvailability(time, calculate_parking_heatmap(ctx, ui))
}
"intersection delay" => {
Analytics::IntersectionDelay(time, calculate_intersection_delay(ctx, ui))
}
"cumulative throughput" => {
Analytics::Throughput(time, calculate_thruput(thruput_stats, ctx, ui))
}
_ => unreachable!(),
}
}
}
fn calculate_parking_heatmap(ctx: &mut EventCtx, ui: &UI) -> RoadColorer {
let awful = Color::BLACK;
let bad = Color::RED;
let meh = Color::YELLOW;
let good = Color::GREEN;
let mut colorer = RoadColorerBuilder::new(
"parking availability",
vec![
("< 10%", awful),
("< 30%", bad),
("< 60%", meh),
(">= 60%", good),
],
);
let lane = |spot| match spot {
ParkingSpot::Onstreet(l, _) => l,
ParkingSpot::Offstreet(b, _) => ui
.primary
.map
.get_b(b)
.parking
.as_ref()
.unwrap()
.driving_pos
.lane(),
};
let mut filled = Counter::new();
let mut avail = Counter::new();
let mut keys = HashSet::new();
let (filled_spots, avail_spots) = ui.primary.sim.get_all_parking_spots();
for spot in filled_spots {
let l = lane(spot);
keys.insert(l);
filled.inc(l);
}
for spot in avail_spots {
let l = lane(spot);
keys.insert(l);
avail.inc(l);
}
for l in keys {
let open = avail.get(l);
let closed = filled.get(l);
let percent = (open as f64) / ((open + closed) as f64);
let color = if percent >= 0.6 {
good
} else if percent > 0.3 {
meh
} else if percent > 0.1 {
bad
} else {
awful
};
colorer.add(l, color, &ui.primary.map);
}
colorer.build(ctx, &ui.primary.map)
}
fn calculate_intersection_delay(ctx: &mut EventCtx, ui: &UI) -> ObjectColorer {
let fast = Color::GREEN;
let meh = Color::YELLOW;
let slow = Color::RED;
let mut colorer = ObjectColorerBuilder::new(
"intersection delay (90%ile)",
vec![("< 10s", fast), ("<= 60s", meh), ("> 60s", slow)],
);
for i in ui.primary.map.all_intersections() {
let delays = ui.primary.sim.get_intersection_delays(i.id);
if let Some(d) = delays.percentile(90.0) {
let color = if d < Duration::seconds(10.0) {
fast
} else if d <= Duration::seconds(60.0) {
meh
} else {
slow
};
colorer.add(ID::Intersection(i.id), color);
}
}
colorer.build(ctx, &ui.primary.map)
}
fn calculate_thruput(stats: &ThruputStats, ctx: &mut EventCtx, ui: &UI) -> ObjectColorer {
let light = Color::GREEN;
let medium = Color::YELLOW;
let heavy = Color::RED;
let mut colorer = ObjectColorerBuilder::new(
"Throughput",
vec![
("< 50%ile", light),
("< 90%ile", medium),
(">= 90%ile", heavy),
],
);
// TODO If there are many duplicate counts, arbitrarily some will look heavier! Find the
// disribution of counts instead.
// TODO Actually display the counts at these percentiles
// TODO Dump the data in debug mode
{
let roads = stats.count_per_road.sorted_asc();
let p50_idx = ((roads.len() as f64) * 0.5) as usize;
let p90_idx = ((roads.len() as f64) * 0.9) as usize;
for (idx, r) in roads.into_iter().enumerate() {
let color = if idx < p50_idx {
light
} else if idx < p90_idx {
medium
} else {
heavy
};
colorer.add(ID::Road(*r), color);
}
}
// TODO dedupe
{
let intersections = stats.count_per_intersection.sorted_asc();
let p50_idx = ((intersections.len() as f64) * 0.5) as usize;
let p90_idx = ((intersections.len() as f64) * 0.9) as usize;
for (idx, i) in intersections.into_iter().enumerate() {
let color = if idx < p50_idx {
light
} else if idx < p90_idx {
medium
} else {
heavy
};
colorer.add(ID::Intersection(*i), color);
}
}
colorer.build(ctx, &ui.primary.map)
}
pub struct ThruputStats {
count_per_road: Counter<RoadID>,
count_per_intersection: Counter<IntersectionID>,
}
impl ThruputStats {
pub fn new() -> ThruputStats {
ThruputStats {
count_per_road: Counter::new(),
count_per_intersection: Counter::new(),
}
}
pub fn record(&mut self, ui: &mut UI) {
for ev in ui.primary.sim.collect_events() {
if let Event::AgentEntersTraversable(_, to) = ev {
match to {
Traversable::Lane(l) => self.count_per_road.inc(ui.primary.map.get_l(l).parent),
Traversable::Turn(t) => self.count_per_intersection.inc(t.parent),
};
}
}
}
}

View File

@ -1,36 +1,31 @@
mod analytics;
mod score;
mod spawner;
mod thruput_stats;
mod time_travel;
mod trip_stats;
use crate::common::{
time_controls, AgentTools, CommonState, ObjectColorer, ObjectColorerBuilder, RoadColorer,
RoadColorerBuilder, RouteExplorer, SpeedControls, TripExplorer,
time_controls, AgentTools, CommonState, RouteExplorer, SpeedControls, TripExplorer,
};
use crate::debug::DebugMode;
use crate::edit::EditMode;
use crate::game::{State, Transition, WizardState};
use crate::helpers::ID;
use crate::ui::{PerMapUI, ShowEverything, UI};
use abstutil::Counter;
use crate::ui::{ShowEverything, UI};
use ezgui::{
hotkey, lctrl, Choice, Color, EventCtx, EventLoopMode, GfxCtx, Key, Line, ModalMenu, Text,
Wizard,
hotkey, lctrl, Choice, EventCtx, EventLoopMode, GfxCtx, Key, Line, ModalMenu, Text, Wizard,
};
use geom::Duration;
use sim::{ParkingSpot, Sim};
use std::collections::HashSet;
use sim::Sim;
pub struct SandboxMode {
speed: SpeedControls,
agent_tools: AgentTools,
pub time_travel: time_travel::InactiveTimeTravel,
trip_stats: trip_stats::TripStats,
thruput_stats: thruput_stats::ThruputStats,
thruput_stats: analytics::ThruputStats,
analytics: analytics::Analytics,
common: CommonState,
parking_heatmap: Option<(Duration, RoadColorer)>,
intersection_delay_heatmap: Option<(Duration, ObjectColorer)>,
menu: ModalMenu,
}
@ -43,10 +38,9 @@ impl SandboxMode {
trip_stats: trip_stats::TripStats::new(
ui.primary.current_flags.sim_flags.opts.record_stats,
),
thruput_stats: thruput_stats::ThruputStats::new(),
thruput_stats: analytics::ThruputStats::new(),
analytics: analytics::Analytics::Inactive,
common: CommonState::new(),
parking_heatmap: None,
intersection_delay_heatmap: None,
menu: ModalMenu::new(
"Sandbox Mode",
vec![
@ -68,12 +62,10 @@ impl SandboxMode {
],
vec![
// TODO Strange to always have this. Really it's a case of stacked modal?
(hotkey(Key::A), "show/hide parking availability"),
(hotkey(Key::I), "show/hide intersection delay"),
(hotkey(Key::T), "start time traveling"),
(hotkey(Key::Q), "scoreboard"),
(None, "trip stats"),
(None, "throughput stats"),
(hotkey(Key::L), "change analytics overlay"),
],
vec![
(hotkey(Key::Escape), "quit"),
@ -110,6 +102,12 @@ impl State for SandboxMode {
if let Some(t) = self.common.event(ctx, ui, &mut self.menu) {
return t;
}
if let Some(t) = self
.analytics
.event(ctx, ui, &mut self.menu, &self.thruput_stats)
{
return t;
}
if let Some(new_state) = spawner::AgentSpawner::new(ctx, ui, &mut self.menu) {
return Transition::Push(new_state);
@ -137,55 +135,6 @@ impl State for SandboxMode {
println!("No trip stats available");
}
}
if self.menu.action("throughput stats") {
return Transition::Push(Box::new(thruput_stats::ShowStats::new(
&self.thruput_stats,
ui,
ctx,
)));
}
if self.menu.action("show/hide parking availability") {
if self.parking_heatmap.is_some() {
self.parking_heatmap = None;
} else {
self.parking_heatmap = Some((
ui.primary.sim.time(),
calculate_parking_heatmap(ctx, &ui.primary),
));
}
}
if self
.parking_heatmap
.as_ref()
.map(|(t, _)| *t != ui.primary.sim.time())
.unwrap_or(false)
{
self.parking_heatmap = Some((
ui.primary.sim.time(),
calculate_parking_heatmap(ctx, &ui.primary),
));
}
if self.menu.action("show/hide intersection delay") {
if self.intersection_delay_heatmap.is_some() {
self.intersection_delay_heatmap = None;
} else {
self.intersection_delay_heatmap = Some((
ui.primary.sim.time(),
calculate_intersection_delay(ctx, &ui.primary),
));
}
}
if self
.intersection_delay_heatmap
.as_ref()
.map(|(t, _)| *t != ui.primary.sim.time())
.unwrap_or(false)
{
self.intersection_delay_heatmap = Some((
ui.primary.sim.time(),
calculate_intersection_delay(ctx, &ui.primary),
));
}
if self.menu.action("quit") {
return Transition::Pop;
@ -293,11 +242,8 @@ impl State for SandboxMode {
}
fn draw(&self, g: &mut GfxCtx, ui: &UI) {
// TODO Oh no, these're actually exclusive, represent that better.
if let Some((_, ref c)) = self.parking_heatmap {
c.draw(g, ui);
} else if let Some((_, ref c)) = self.intersection_delay_heatmap {
c.draw(g, ui);
if self.analytics.draw(g, ui) {
// Don't draw agent tools!
} else {
ui.draw(
g,
@ -305,9 +251,9 @@ impl State for SandboxMode {
&ui.primary.sim,
&ShowEverything::new(),
);
self.agent_tools.draw(g, ui);
}
self.common.draw(g, ui);
self.agent_tools.draw(g, ui);
self.menu.draw(g);
self.speed.draw(g);
}
@ -330,90 +276,3 @@ fn load_savestate(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Option<T
});
Some(Transition::Pop)
}
fn calculate_parking_heatmap(ctx: &mut EventCtx, primary: &PerMapUI) -> RoadColorer {
let awful = Color::BLACK;
let bad = Color::RED;
let meh = Color::YELLOW;
let good = Color::GREEN;
let mut colorer = RoadColorerBuilder::new(
"parking availability",
vec![
("< 10%", awful),
("< 30%", bad),
("< 60%", meh),
(">= 60%", good),
],
);
let lane = |spot| match spot {
ParkingSpot::Onstreet(l, _) => l,
ParkingSpot::Offstreet(b, _) => primary
.map
.get_b(b)
.parking
.as_ref()
.unwrap()
.driving_pos
.lane(),
};
let mut filled = Counter::new();
let mut avail = Counter::new();
let mut keys = HashSet::new();
let (filled_spots, avail_spots) = primary.sim.get_all_parking_spots();
for spot in filled_spots {
let l = lane(spot);
keys.insert(l);
filled.inc(l);
}
for spot in avail_spots {
let l = lane(spot);
keys.insert(l);
avail.inc(l);
}
for l in keys {
let open = avail.get(l);
let closed = filled.get(l);
let percent = (open as f64) / ((open + closed) as f64);
let color = if percent >= 0.6 {
good
} else if percent > 0.3 {
meh
} else if percent > 0.1 {
bad
} else {
awful
};
colorer.add(l, color, &primary.map);
}
colorer.build(ctx, &primary.map)
}
fn calculate_intersection_delay(ctx: &mut EventCtx, primary: &PerMapUI) -> ObjectColorer {
let fast = Color::GREEN;
let meh = Color::YELLOW;
let slow = Color::RED;
let mut colorer = ObjectColorerBuilder::new(
"intersection delay (90%ile)",
vec![("< 10s", fast), ("<= 60s", meh), ("> 60s", slow)],
);
for i in primary.map.all_intersections() {
let delays = primary.sim.get_intersection_delays(i.id);
if let Some(d) = delays.percentile(90.0) {
let color = if d < Duration::seconds(10.0) {
fast
} else if d <= Duration::seconds(60.0) {
meh
} else {
slow
};
colorer.add(ID::Intersection(i.id), color);
}
}
colorer.build(ctx, &primary.map)
}

View File

@ -1,123 +0,0 @@
use crate::common::{ObjectColorer, ObjectColorerBuilder};
use crate::game::{State, Transition};
use crate::helpers::ID;
use crate::ui::UI;
use abstutil::Counter;
use ezgui::{hotkey, Color, EventCtx, GfxCtx, Key, ModalMenu};
use map_model::{IntersectionID, RoadID, Traversable};
use sim::Event;
pub struct ThruputStats {
count_per_road: Counter<RoadID>,
count_per_intersection: Counter<IntersectionID>,
}
impl ThruputStats {
pub fn new() -> ThruputStats {
ThruputStats {
count_per_road: Counter::new(),
count_per_intersection: Counter::new(),
}
}
pub fn record(&mut self, ui: &mut UI) {
for ev in ui.primary.sim.collect_events() {
if let Event::AgentEntersTraversable(_, to) = ev {
match to {
Traversable::Lane(l) => self.count_per_road.inc(ui.primary.map.get_l(l).parent),
Traversable::Turn(t) => self.count_per_intersection.inc(t.parent),
};
}
}
}
}
pub struct ShowStats {
menu: ModalMenu,
heatmap: ObjectColorer,
}
impl State for ShowStats {
fn event(&mut self, ctx: &mut EventCtx, ui: &mut UI) -> Transition {
ctx.canvas.handle_event(ctx.input);
if ctx.redo_mouseover() {
ui.recalculate_current_selection(ctx);
}
self.menu.handle_event(ctx, None);
if self.menu.action("quit") {
return Transition::Pop;
}
Transition::Keep
}
fn draw_default_ui(&self) -> bool {
false
}
fn draw(&self, g: &mut GfxCtx, ui: &UI) {
self.heatmap.draw(g, ui);
self.menu.draw(g);
}
}
impl ShowStats {
pub fn new(stats: &ThruputStats, ui: &UI, ctx: &mut EventCtx) -> ShowStats {
let light = Color::GREEN;
let medium = Color::YELLOW;
let heavy = Color::RED;
let mut colorer = ObjectColorerBuilder::new(
"Throughput",
vec![
("< 50%ile", light),
("< 90%ile", medium),
(">= 90%ile", heavy),
],
);
// TODO If there are many duplicate counts, arbitrarily some will look heavier! Find the
// disribution of counts instead.
// TODO Actually display the counts at these percentiles
// TODO Dump the data in debug mode
{
let roads = stats.count_per_road.sorted_asc();
let p50_idx = ((roads.len() as f64) * 0.5) as usize;
let p90_idx = ((roads.len() as f64) * 0.9) as usize;
for (idx, r) in roads.into_iter().enumerate() {
let color = if idx < p50_idx {
light
} else if idx < p90_idx {
medium
} else {
heavy
};
colorer.add(ID::Road(*r), color);
}
}
// TODO dedupe
{
let intersections = stats.count_per_intersection.sorted_asc();
let p50_idx = ((intersections.len() as f64) * 0.5) as usize;
let p90_idx = ((intersections.len() as f64) * 0.9) as usize;
for (idx, i) in intersections.into_iter().enumerate() {
let color = if idx < p50_idx {
light
} else if idx < p90_idx {
medium
} else {
heavy
};
colorer.add(ID::Intersection(*i), color);
}
}
ShowStats {
menu: ModalMenu::new(
"Thruput Stats",
vec![vec![(hotkey(Key::Escape), "quit")]],
ctx,
),
heatmap: colorer.build(ctx, &ui.primary.map),
}
}
}