mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-11-24 09:24:26 +03:00
started a histogram to show relative trip durations for fixing traffic
signals. lots of work needed.
This commit is contained in:
parent
7ca08dfaff
commit
954ac97a9f
@ -16,7 +16,7 @@ pub use self::colors::{
|
||||
ColorLegend, ObjectColorer, ObjectColorerBuilder, RoadColorer, RoadColorerBuilder,
|
||||
};
|
||||
pub use self::minimap::Minimap;
|
||||
pub use self::plot::{Plot, Series};
|
||||
pub use self::plot::{Histogram, Plot, Series};
|
||||
pub use self::route_explorer::RouteExplorer;
|
||||
pub use self::trip_explorer::TripExplorer;
|
||||
pub use self::warp::Warping;
|
||||
|
@ -4,7 +4,9 @@ use ezgui::{
|
||||
};
|
||||
use geom::{Bounds, Circle, Distance, Duration, FindClosest, PolyLine, Polygon, Pt2D, Time};
|
||||
|
||||
// The X is always time
|
||||
pub struct Plot<T> {
|
||||
// TODO Could DrawBoth instead of MultiText here
|
||||
draw: Drawable,
|
||||
legend: ColorLegend,
|
||||
labels: MultiText,
|
||||
@ -200,3 +202,101 @@ pub struct Series<T> {
|
||||
// X-axis is time. Assume this is sorted by X.
|
||||
pub pts: Vec<(Time, T)>,
|
||||
}
|
||||
|
||||
// The X axis is Durations, with positive meaning "faster" (considered good) and negative "slower"
|
||||
pub struct Histogram {
|
||||
draw: Drawable,
|
||||
labels: MultiText,
|
||||
rect: ScreenRectangle,
|
||||
}
|
||||
|
||||
impl Histogram {
|
||||
pub fn new(title: &str, unsorted_dts: Vec<Duration>, ctx: &EventCtx) -> Histogram {
|
||||
let mut batch = GeomBatch::new();
|
||||
let mut labels = MultiText::new();
|
||||
|
||||
let x1 = 0.5 * ctx.canvas.window_width;
|
||||
let x2 = 0.9 * ctx.canvas.window_width;
|
||||
let y1 = 0.4 * 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),
|
||||
),
|
||||
);
|
||||
|
||||
if unsorted_dts.len() < 10 {
|
||||
// TODO Add some warning label or something
|
||||
} else {
|
||||
// TODO Generic "bucket into 10 groups, give (min, max, count)"
|
||||
let min_x = *unsorted_dts.iter().min().unwrap();
|
||||
let max_x = *unsorted_dts.iter().max().unwrap();
|
||||
|
||||
let num_buckets = 10;
|
||||
let bucket_size = (max_x - min_x) / (num_buckets as f64);
|
||||
// lower, upper, count
|
||||
let mut bars: Vec<(Duration, Duration, usize)> = (0..num_buckets)
|
||||
.map(|idx| {
|
||||
let i = idx as f64;
|
||||
(min_x + bucket_size * i, min_x + bucket_size * (i + 1.0), 0)
|
||||
})
|
||||
.collect();
|
||||
for dt in unsorted_dts {
|
||||
// TODO Could sort them and do this more efficiently.
|
||||
if dt == max_x {
|
||||
// Most bars represent [low, high) except the last
|
||||
bars[num_buckets - 1].2 += 1;
|
||||
} else {
|
||||
let bin = ((dt - min_x) / bucket_size).floor() as usize;
|
||||
bars[bin].2 += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let min_y = 0;
|
||||
let max_y = bars.iter().map(|(_, _, cnt)| *cnt).max().unwrap();
|
||||
for (idx, (min, max, cnt)) in bars.into_iter().enumerate() {
|
||||
// TODO Or maybe the average?
|
||||
let color = if min < Duration::ZERO {
|
||||
Color::RED
|
||||
} else {
|
||||
Color::GREEN
|
||||
};
|
||||
let percent_x_left = (idx as f64) / (num_buckets as f64);
|
||||
let percent_x_right = ((idx + 1) as f64) / (num_buckets as f64);
|
||||
if let Some(rect) = Polygon::rectangle_two_corners(
|
||||
// Top-left
|
||||
Pt2D::new(
|
||||
x1 + (x2 - x1) * percent_x_left,
|
||||
y2 - (y2 - y1) * ((cnt as f64) / ((max_y - min_y) as f64)),
|
||||
),
|
||||
// Bottom-right
|
||||
Pt2D::new(x1 + (x2 - x1) * percent_x_right, y2),
|
||||
) {
|
||||
batch.push(color, rect);
|
||||
}
|
||||
labels.add(
|
||||
Text::from(Line(min.to_string())),
|
||||
ScreenPt::new(x1 + (x2 - x1) * percent_x_left, y2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Histogram {
|
||||
draw: batch.upload(ctx),
|
||||
labels,
|
||||
rect: ScreenRectangle { x1, y1, x2, y2 },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(&self, g: &mut GfxCtx) {
|
||||
g.canvas.mark_covered_area(self.rect.clone());
|
||||
|
||||
g.fork_screenspace();
|
||||
g.redraw(&self.draw);
|
||||
g.unfork();
|
||||
self.labels.draw(g);
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ impl FixTrafficSignals {
|
||||
"Fix traffic signals",
|
||||
vec![
|
||||
(hotkey(Key::F), "find slowest traffic signals"),
|
||||
(hotkey(Key::D), "show finished trip distribution"),
|
||||
(hotkey(Key::H), "help"),
|
||||
(hotkey(Key::S), "final score"),
|
||||
],
|
||||
@ -57,6 +58,20 @@ impl GameplayState for FixTrafficSignals {
|
||||
) {
|
||||
*overlays = Overlays::intersection_delay(ctx, ui);
|
||||
}
|
||||
if manage_overlays(
|
||||
menu,
|
||||
ctx,
|
||||
"show finished trip distribution",
|
||||
"hide finished trip distribution",
|
||||
overlays,
|
||||
match overlays {
|
||||
Overlays::FinishedTripsHistogram(_, _) => true,
|
||||
_ => false,
|
||||
},
|
||||
self.time != ui.primary.sim.time(),
|
||||
) {
|
||||
*overlays = Overlays::finished_trips_histogram(ctx, ui, prebaked);
|
||||
}
|
||||
|
||||
if self.time != ui.primary.sim.time() {
|
||||
self.time = ui.primary.sim.time();
|
||||
|
@ -125,7 +125,10 @@ impl State for SandboxMode {
|
||||
if let Some(t) = self.common.event(ctx, ui) {
|
||||
return t;
|
||||
}
|
||||
if let Some(t) = self.overlay.event(ctx, ui, &mut self.info_tools) {
|
||||
if let Some(t) = self
|
||||
.overlay
|
||||
.event(ctx, ui, &mut self.info_tools, &self.gameplay.prebaked)
|
||||
{
|
||||
return t;
|
||||
}
|
||||
self.minimap.event(ui, ctx);
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::common::{
|
||||
ObjectColorer, ObjectColorerBuilder, Plot, RoadColorer, RoadColorerBuilder, Series,
|
||||
Histogram, ObjectColorer, ObjectColorerBuilder, Plot, RoadColorer, RoadColorerBuilder, Series,
|
||||
};
|
||||
use crate::game::{Transition, WizardState};
|
||||
use crate::helpers::{rotating_color, ID};
|
||||
@ -11,7 +11,7 @@ use abstutil::{prettyprint_usize, Counter};
|
||||
use ezgui::{Choice, Color, EventCtx, GfxCtx, Key, Line, MenuUnderButton, Text};
|
||||
use geom::{Duration, Statistic, Time};
|
||||
use map_model::{IntersectionID, LaneID, PathConstraints, PathStep, RoadID};
|
||||
use sim::{ParkingSpot, TripMode};
|
||||
use sim::{Analytics, ParkingSpot, TripMode};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
pub enum Overlays {
|
||||
@ -20,6 +20,7 @@ pub enum Overlays {
|
||||
IntersectionDelay(Time, ObjectColorer),
|
||||
CumulativeThroughput(Time, ObjectColorer),
|
||||
FinishedTrips(Time, Plot<usize>),
|
||||
FinishedTripsHistogram(Time, Histogram),
|
||||
Chokepoints(Time, ObjectColorer),
|
||||
BikeNetwork(RoadColorer),
|
||||
BikePathCosts(RoadColorer),
|
||||
@ -53,6 +54,7 @@ impl Overlays {
|
||||
ctx: &mut EventCtx,
|
||||
ui: &UI,
|
||||
menu: &mut MenuUnderButton,
|
||||
baseline: &Analytics,
|
||||
) -> Option<Transition> {
|
||||
if menu.action("change analytics overlay") {
|
||||
return Some(Transition::Push(WizardState::new(Box::new(
|
||||
@ -66,6 +68,8 @@ impl Overlays {
|
||||
Choice::new("intersection delay", ()).key(Key::I),
|
||||
Choice::new("cumulative throughput", ()).key(Key::T),
|
||||
Choice::new("finished trips", ()).key(Key::F),
|
||||
// TODO baseline borrow doesn't live long enough
|
||||
//Choice::new("finished trips histogram", ()).key(Key::H),
|
||||
Choice::new("chokepoints", ()).key(Key::C),
|
||||
Choice::new("bike network", ()).key(Key::B),
|
||||
Choice::new("bike path costs", ()).key(Key::X),
|
||||
@ -116,6 +120,9 @@ impl Overlays {
|
||||
Overlays::FinishedTrips(t, _) if now != *t => {
|
||||
*self = Overlays::finished_trips(ctx, ui);
|
||||
}
|
||||
Overlays::FinishedTripsHistogram(t, _) if now != *t => {
|
||||
*self = Overlays::finished_trips_histogram(ctx, ui, baseline);
|
||||
}
|
||||
Overlays::Chokepoints(t, _) if now != *t => {
|
||||
*self = Overlays::chokepoints(ctx, ui);
|
||||
}
|
||||
@ -153,6 +160,16 @@ impl Overlays {
|
||||
plot.draw(g);
|
||||
true
|
||||
}
|
||||
Overlays::FinishedTripsHistogram(_, ref hgram) => {
|
||||
ui.draw(
|
||||
g,
|
||||
DrawOptions::new(),
|
||||
&ui.primary.sim,
|
||||
&ShowEverything::new(),
|
||||
);
|
||||
hgram.draw(g);
|
||||
true
|
||||
}
|
||||
Overlays::BusDelaysOverTime(ref plot)
|
||||
| Overlays::IntersectionDelayOverTime { ref plot, .. } => {
|
||||
ui.draw(
|
||||
@ -560,6 +577,21 @@ impl Overlays {
|
||||
Overlays::FinishedTrips(ui.primary.sim.time(), plot)
|
||||
}
|
||||
|
||||
pub fn finished_trips_histogram(ctx: &EventCtx, ui: &UI, baseline: &Analytics) -> Overlays {
|
||||
let now = ui.primary.sim.time();
|
||||
Overlays::FinishedTripsHistogram(
|
||||
now,
|
||||
Histogram::new(
|
||||
"Finished trip time deltas",
|
||||
ui.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.finished_trip_deltas(now, baseline),
|
||||
ctx,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn bike_path_costs(ctx: &EventCtx, ui: &UI) -> Overlays {
|
||||
let mut cost_per_lane: BTreeMap<LaneID, usize> = BTreeMap::new();
|
||||
for l in ui.primary.map.all_lanes() {
|
||||
|
@ -221,6 +221,17 @@ impl ops::Div<Duration> for Duration {
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div<f64> for Duration {
|
||||
type Output = Duration;
|
||||
|
||||
fn div(self, other: f64) -> Duration {
|
||||
if other == 0.0 {
|
||||
panic!("Can't divide {} / {}", self, other);
|
||||
}
|
||||
Duration::seconds(self.0 / other)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Rem<Duration> for Duration {
|
||||
type Output = Duration;
|
||||
|
||||
|
@ -175,6 +175,37 @@ impl Analytics {
|
||||
(all, num_aborted, per_mode)
|
||||
}
|
||||
|
||||
// Returns unsorted list of deltas, one for each trip finished in both worlds. Positive dt
|
||||
// means faster.
|
||||
pub fn finished_trip_deltas(&self, now: Time, baseline: &Analytics) -> Vec<Duration> {
|
||||
let a: BTreeMap<TripID, Duration> = self
|
||||
.finished_trips
|
||||
.iter()
|
||||
.filter_map(|(t, id, mode, dt)| {
|
||||
if *t <= now && mode.is_some() {
|
||||
Some((*id, *dt))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let b: BTreeMap<TripID, Duration> = baseline
|
||||
.finished_trips
|
||||
.iter()
|
||||
.filter_map(|(t, id, mode, dt)| {
|
||||
if *t <= now && mode.is_some() {
|
||||
Some((*id, *dt))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
a.into_iter()
|
||||
.filter_map(|(id, dt1)| b.get(&id).map(|dt2| *dt2 - dt1))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn bus_arrivals(&self, now: Time, r: BusRouteID) -> BTreeMap<BusStopID, DurationHistogram> {
|
||||
let mut per_bus: BTreeMap<CarID, Vec<(Time, BusStopID)>> = BTreeMap::new();
|
||||
for (t, car, route, stop) in &self.bus_arrivals {
|
||||
|
Loading…
Reference in New Issue
Block a user