use std::fmt::Display;
use abstutil::{abbreviated_format, prettyprint_usize};
use geom::{Angle, Duration, Polygon, Pt2D, Time};
use map_gui::tools::ColorScale;
use sim::{Problem, TripMode};
use widgetry::{Color, DrawWithTooltips, GeomBatch, GeomBatchStack, Text, Widget};
use crate::{App, EventCtx};
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum ProblemType {
IntersectionDelay,
ComplexIntersectionCrossing,
OvertakeDesired,
ArterialIntersectionCrossing,
}
impl From<&Problem> for ProblemType {
fn from(problem: &Problem) -> Self {
match problem {
Problem::IntersectionDelay(_, _) => Self::IntersectionDelay,
Problem::ComplexIntersectionCrossing(_) => Self::ComplexIntersectionCrossing,
Problem::OvertakeDesired(_) => Self::OvertakeDesired,
Problem::ArterialIntersectionCrossing(_) => Self::ArterialIntersectionCrossing,
}
}
}
impl ProblemType {
pub fn count(self, problems: &[(Time, Problem)]) -> usize {
let mut cnt = 0;
for (_, problem) in problems {
if self == ProblemType::from(problem) {
cnt += 1;
}
}
cnt
}
pub fn all() -> Vec<ProblemType> {
vec![
ProblemType::IntersectionDelay,
ProblemType::ComplexIntersectionCrossing,
ProblemType::OvertakeDesired,
ProblemType::ArterialIntersectionCrossing,
]
}
}
pub trait TripProblemFilter {
fn includes_mode(&self, mode: &TripMode) -> bool;
fn include_no_changes(&self) -> bool;
fn trip_problems(&self, app: &App, problem_type: ProblemType) -> Vec<(Duration, isize)> {
let before = app.prebaked();
let after = app.primary.sim.get_analytics();
let empty = Vec::new();
let mut points = Vec::new();
for (id, _, time_after, mode) in after.both_finished_trips(app.primary.sim.time(), before) {
if self.includes_mode(&mode) {
let count_before = problem_type
.count(before.problems_per_trip.get(&id).unwrap_or(&empty))
as isize;
let count_after =
problem_type.count(after.problems_per_trip.get(&id).unwrap_or(&empty)) as isize;
if !self.include_no_changes() && count_after == count_before {
continue;
}
points.push((time_after, count_after - count_before));
}
}
points
}
fn finished_trip_count(&self, app: &App) -> usize {
let before = app.prebaked();
let after = app.primary.sim.get_analytics();
let mut count = 0;
for (_, _, _, mode) in after.both_finished_trips(app.primary.sim.time(), before) {
if self.includes_mode(&mode) {
count += 1;
}
}
count
}
}
lazy_static::lazy_static! {
static ref CLEAR_COLOR_SCALE: ColorScale = ColorScale(vec![Color::CLEAR, Color::CLEAR]);
}
pub fn problem_matrix(ctx: &mut EventCtx, app: &App, trips: &[(Duration, isize)]) -> Widget {
let points = trips;
let duration_buckets = vec![
Duration::ZERO,
Duration::minutes(5),
Duration::minutes(15),
Duration::minutes(30),
Duration::hours(1),
Duration::hours(2),
];
let num_buckets = 7;
let mut matrix = Matrix::new(duration_buckets, bucketize_isizes(num_buckets, &points));
for (x, y) in points {
matrix.add_pt(*x, *y);
}
matrix.draw(
ctx,
app,
MatrixOptions {
total_width: 600.0,
total_height: 600.0,
color_scale_for_bucket: Box::new(|app, _, n| match n.cmp(&0) {
std::cmp::Ordering::Equal => &CLEAR_COLOR_SCALE,
std::cmp::Ordering::Less => &app.cs.good_to_bad_green,
std::cmp::Ordering::Greater => &app.cs.good_to_bad_red,
}),
tooltip_for_bucket: Box::new(|(t1, t2), (problems1, problems2), count| {
let trip_string = if count == 1 {
"1 trip".to_string()
} else {
format!("{} trips", prettyprint_usize(count))
};
let duration_string = match (t1, t2) {
(None, Some(end)) => format!("shorter than {}", end),
(Some(start), None) => format!("longer than {}", start),
(Some(start), Some(end)) => format!("between {} and {}", start, end),
(None, None) => {
unreachable!("at least one end of the duration range must be specified")
}
};
let mut txt = Text::from(format!("{} {}", trip_string, duration_string));
txt.add_line(match problems1.cmp(&0) {
std::cmp::Ordering::Equal => {
"had no change in the number of problems encountered.".to_string()
}
std::cmp::Ordering::Less => {
if problems1.abs() == problems2.abs() + 1 {
if problems1.abs() == 1 {
"encountered 1 fewer problem.".to_string()
} else {
format!("encountered {} fewer problems.", problems1.abs())
}
} else {
format!(
"encountered {}-{} fewer problems.",
problems2.abs() + 1,
problems1.abs()
)
}
}
std::cmp::Ordering::Greater => {
if problems1 == problems2 - 1 {
if problems1 == 1 {
"encountered 1 more problems.".to_string()
} else {
format!("encountered {} more problems.", problems1,)
}
} else {
format!("encountered {}-{} more problems.", problems1, problems2 - 1)
}
}
});
txt
}),
},
)
}
struct Matrix<X, Y> {
counts: Vec<usize>,
buckets_x: Vec<X>,
buckets_y: Vec<Y>,
}
impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y> {
fn new(buckets_x: Vec<X>, buckets_y: Vec<Y>) -> Matrix<X, Y> {
Matrix {
counts: std::iter::repeat(0)
.take(buckets_x.len() * buckets_y.len())
.collect(),
buckets_x,
buckets_y,
}
}
fn add_pt(&mut self, x: X, y: Y) {
let x_idx = self
.buckets_x
.iter()
.position(|min| *min > x)
.unwrap_or(self.buckets_x.len())
- 1;
let y_idx = self
.buckets_y
.iter()
.position(|min| *min > y)
.unwrap_or(self.buckets_y.len())
- 1;
let idx = self.idx(x_idx, y_idx);
self.counts[idx] += 1;
}
fn idx(&self, x: usize, y: usize) -> usize {
y * self.buckets_x.len() + x
}
fn draw(self, ctx: &mut EventCtx, app: &App, opts: MatrixOptions<X, Y>) -> Widget {
let mut batch = GeomBatch::new();
let mut tooltips = Vec::new();
let cell_width = opts.total_width / (self.buckets_x.len() as f64);
let cell_height = opts.total_height / (self.buckets_y.len() as f64);
let cell = Polygon::rectangle(cell_width, cell_height);
let max_count = *self.counts.iter().max().unwrap() as f64;
for x in 0..self.buckets_x.len() - 1 {
for y in 0..self.buckets_y.len() - 1 {
let is_first_xbucket = x == 0;
let is_last_xbucket = x == self.buckets_x.len() - 2;
let is_middle_ybucket = y + 1 == self.buckets_y.len() / 2;
let count = self.counts[self.idx(x, y)];
let color = if count == 0 {
widgetry::Color::CLEAR
} else {
let density_pct = (count as f64) / max_count;
(opts.color_scale_for_bucket)(app, self.buckets_x[x], self.buckets_y[y])
.eval(density_pct)
};
let x1 = cell_width * (x as f64);
let y1 = cell_height * (y as f64);
let rect = cell.clone().translate(x1, y1);
batch.push(color, rect.clone());
batch.append(
Text::from(if count == 0 && is_middle_ybucket {
"-".to_string()
} else {
abbreviated_format(count)
})
.change_fg(if count == 0 || is_middle_ybucket {
ctx.style().text_primary_color
} else {
Color::WHITE
})
.render(ctx)
.centered_on(Pt2D::new(x1 + cell_width / 2.0, y1 + cell_height / 2.0)),
);
if count != 0 || !is_middle_ybucket {
tooltips.push((
rect,
(opts.tooltip_for_bucket)(
(
if is_first_xbucket {
None
} else {
Some(self.buckets_x[x])
},
if is_last_xbucket {
None
} else {
Some(self.buckets_x[x + 1])
},
),
(self.buckets_y[y], self.buckets_y[y + 1]),
count,
),
));
}
}
}
let mut y_axis_label = Text::from("More Problems <--------> Fewer Problems")
.change_fg(ctx.style().text_secondary_color)
.render(ctx)
.rotate(Angle::degrees(-90.0));
y_axis_label.autocrop_dims = true;
y_axis_label = y_axis_label.autocrop();
let x_axis_label = Text::from(" Short Trips <--------> Long Trips")
.change_fg(ctx.style().text_secondary_color)
.render(ctx);
let vmargin = 32.0;
for (polygon, _) in tooltips.iter_mut() {
let mut translated =
polygon.translate(vmargin + y_axis_label.get_bounds().width(), 0.0);
std::mem::swap(&mut translated, polygon);
}
let mut row = GeomBatchStack::horizontal(vec![y_axis_label, batch]);
row.set_spacing(vmargin);
let mut chart = GeomBatchStack::vertical(vec![row.batch(), x_axis_label]);
chart.set_spacing(16);
DrawWithTooltips::new_widget(ctx, chart.batch(), tooltips, Box::new(|_| GeomBatch::new()))
}
}
struct MatrixOptions<X, Y> {
total_width: f64,
total_height: f64,
color_scale_for_bucket: Box<dyn Fn(&App, X, Y) -> &ColorScale>,
tooltip_for_bucket: Box<dyn Fn((Option<X>, Option<X>), (Y, Y), usize) -> Text>,
}
fn bucketize_isizes(max_buckets: usize, pts: &[(Duration, isize)]) -> Vec<isize> {
debug_assert!(
max_buckets % 2 == 1,
"num_buckets must be odd to have a symmetrical number of buckets around axis"
);
debug_assert!(max_buckets >= 3, "num_buckets must be at least 3");
let positive_buckets = (max_buckets - 1) / 2;
let max = match pts.iter().max_by_key(|(_, cnt)| cnt.abs()) {
Some(t) if (t.1.abs() as usize) >= positive_buckets => t.1.abs(),
_ => {
let negative_buckets = -(positive_buckets as isize);
return (negative_buckets..=(positive_buckets as isize + 1)).collect();
}
};
let bucket_size = (max as f64 / positive_buckets as f64).ceil() as isize;
let mut buckets = vec![0];
for i in 0..=positive_buckets {
buckets.push(1 + (i as isize) * bucket_size);
}
for i in 1..=positive_buckets {
buckets.push(-(i as isize) * bucket_size);
}
buckets.sort_unstable();
debug!("buckets: {:?}", buckets);
buckets
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bucketize_isizes() {
let buckets = bucketize_isizes(
7,
&vec![
(Duration::minutes(3), -3),
(Duration::minutes(3), -3),
(Duration::minutes(3), -1),
(Duration::minutes(3), 2),
(Duration::minutes(3), 5),
],
);
assert_eq!(buckets, vec![-6, -4, -2, 0, 1, 3, 5, 7])
}
#[test]
fn test_bucketize_empty_isizes() {
let buckets = bucketize_isizes(7, &vec![]);
assert_eq!(buckets, vec![-2, -1, 0, 1, 2])
}
#[test]
fn test_bucketize_small_isizes() {
let buckets = bucketize_isizes(
7,
&vec![
(Duration::minutes(3), -1),
(Duration::minutes(3), -1),
(Duration::minutes(3), 0),
(Duration::minutes(3), -1),
(Duration::minutes(3), 0),
],
);
assert_eq!(buckets, vec![-3, -2, -1, 0, 1, 2, 3, 4])
}
}