mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-24 15:02:59 +03:00
Display individual collisions and allow filtering by severity/time. #87
This commit is contained in:
parent
8ab7855926
commit
b17d638d34
@ -34,7 +34,7 @@ pub struct Collision {
|
||||
/// A simple ranking for how severe the collision was. Different agencies use different
|
||||
/// classification systems, each of which likely has their own nuance and bias. This is
|
||||
/// deliberately simplified.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Severity {
|
||||
Slight,
|
||||
Serious,
|
||||
|
@ -1,9 +1,10 @@
|
||||
use abstutil::{prettyprint_usize, Counter};
|
||||
use collisions::CollisionDataset;
|
||||
use geom::{Distance, FindClosest, Pt2D};
|
||||
use collisions::{CollisionDataset, Severity};
|
||||
use geom::{Circle, Distance, Duration, FindClosest, Pt2D};
|
||||
use map_model::{IntersectionID, RoadID};
|
||||
use widgetry::{
|
||||
Btn, Drawable, EventCtx, GfxCtx, HorizontalAlignment, Line, Outcome, Panel, State,
|
||||
VerticalAlignment, Widget,
|
||||
Btn, Checkbox, Choice, Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Line,
|
||||
Outcome, Panel, Slider, State, TextExt, VerticalAlignment, Widget,
|
||||
};
|
||||
|
||||
use crate::app::App;
|
||||
@ -12,21 +13,150 @@ use crate::game::Transition;
|
||||
use crate::helpers::ID;
|
||||
|
||||
pub struct CollisionsViewer {
|
||||
data: CollisionDataset,
|
||||
dataviz: Dataviz,
|
||||
panel: Panel,
|
||||
unzoomed: Drawable,
|
||||
zoomed: Drawable,
|
||||
}
|
||||
|
||||
impl CollisionsViewer {
|
||||
pub fn new(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
|
||||
let map = &app.primary.map;
|
||||
let dataset: CollisionDataset =
|
||||
ctx.loading_screen("load collision data", |_, mut timer| {
|
||||
abstutil::read_binary(
|
||||
abstutil::path(format!("input/{}/collisions.bin", map.get_city_name())),
|
||||
&mut timer,
|
||||
)
|
||||
let data = ctx.loading_screen("load collision data", |_, mut timer| {
|
||||
let mut all: CollisionDataset = abstutil::read_binary(
|
||||
abstutil::path(format!("input/{}/collisions.bin", map.get_city_name())),
|
||||
&mut timer,
|
||||
);
|
||||
all.collisions.retain(|c| {
|
||||
map.get_boundary_polygon()
|
||||
.contains_pt(Pt2D::from_gps(c.location, map.get_gps_bounds()))
|
||||
});
|
||||
all
|
||||
});
|
||||
|
||||
let filters = Filters::new();
|
||||
let indices = filters.apply(&data);
|
||||
let count = indices.len();
|
||||
let dataviz = Dataviz::aggregated(ctx, app, &data, indices);
|
||||
|
||||
Box::new(CollisionsViewer {
|
||||
panel: Panel::new(Widget::col(vec![
|
||||
Widget::row(vec![
|
||||
Line("Collisions viewer").small_heading().draw(ctx),
|
||||
Btn::close(ctx),
|
||||
]),
|
||||
format!("{} collisions", prettyprint_usize(count))
|
||||
.draw_text(ctx)
|
||||
.named("count"),
|
||||
Filters::to_controls(ctx).named("controls"),
|
||||
]))
|
||||
.aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
|
||||
.build(ctx),
|
||||
data,
|
||||
dataviz,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct Filters {
|
||||
show_individual: bool,
|
||||
time_range: (Duration, Duration),
|
||||
severity: Option<Severity>,
|
||||
}
|
||||
|
||||
impl Filters {
|
||||
fn new() -> Filters {
|
||||
Filters {
|
||||
show_individual: false,
|
||||
time_range: (Duration::ZERO, Duration::hours(24)),
|
||||
severity: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the indices of all matching collisions
|
||||
fn apply(&self, data: &CollisionDataset) -> Vec<usize> {
|
||||
let mut indices = Vec::new();
|
||||
for (idx, c) in data.collisions.iter().enumerate() {
|
||||
if c.time < self.time_range.0 || c.time > self.time_range.1 {
|
||||
continue;
|
||||
}
|
||||
if self.severity.map(|s| s != c.severity).unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
indices.push(idx);
|
||||
}
|
||||
indices
|
||||
}
|
||||
|
||||
fn to_controls(ctx: &mut EventCtx) -> Widget {
|
||||
Widget::col(vec![
|
||||
Checkbox::toggle(
|
||||
ctx,
|
||||
"individual / aggregated",
|
||||
"individual",
|
||||
"aggregated",
|
||||
None,
|
||||
false,
|
||||
),
|
||||
Widget::row(vec![
|
||||
"Between:".draw_text(ctx).margin_right(20),
|
||||
Slider::area(ctx, 0.1 * ctx.canvas.window_width, 0.0).named("time1"),
|
||||
]),
|
||||
Widget::row(vec![
|
||||
"and:".draw_text(ctx).margin_right(20),
|
||||
Slider::area(ctx, 0.1 * ctx.canvas.window_width, 1.0).named("time2"),
|
||||
]),
|
||||
Widget::row(vec![
|
||||
"Severity:".draw_text(ctx).margin_right(20),
|
||||
Widget::dropdown(
|
||||
ctx,
|
||||
"severity",
|
||||
None,
|
||||
vec![
|
||||
Choice::new("any", None),
|
||||
Choice::new("slight", Some(Severity::Slight)),
|
||||
Choice::new("serious", Some(Severity::Serious)),
|
||||
Choice::new("fatal", Some(Severity::Fatal)),
|
||||
],
|
||||
),
|
||||
]),
|
||||
])
|
||||
}
|
||||
|
||||
fn from_controls(panel: &Panel) -> Filters {
|
||||
let end_of_day = Duration::hours(24);
|
||||
Filters {
|
||||
show_individual: panel.is_checked("individual / aggregated"),
|
||||
time_range: (
|
||||
end_of_day * panel.slider("time1").get_percent(),
|
||||
end_of_day * panel.slider("time2").get_percent(),
|
||||
),
|
||||
severity: panel.dropdown_value("severity"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Dataviz {
|
||||
Individual {
|
||||
draw_all_circles: Drawable,
|
||||
hitboxes: Vec<(Circle, usize)>,
|
||||
},
|
||||
Aggregated {
|
||||
unzoomed: Drawable,
|
||||
zoomed: Drawable,
|
||||
per_road: Counter<RoadID>,
|
||||
per_intersection: Counter<IntersectionID>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Dataviz {
|
||||
fn aggregated(
|
||||
ctx: &mut EventCtx,
|
||||
app: &App,
|
||||
data: &CollisionDataset,
|
||||
indices: Vec<usize>,
|
||||
) -> Dataviz {
|
||||
let map = &app.primary.map;
|
||||
|
||||
// Match each collision to the nearest road and intersection
|
||||
let mut closest: FindClosest<ID> = FindClosest::new(map.get_bounds());
|
||||
@ -41,7 +171,8 @@ impl CollisionsViewer {
|
||||
let mut per_road = Counter::new();
|
||||
let mut per_intersection = Counter::new();
|
||||
let mut unsnapped = 0;
|
||||
for collision in dataset.collisions {
|
||||
for idx in indices {
|
||||
let collision = &data.collisions[idx];
|
||||
// Search up to 10m away
|
||||
if let Some((id, _)) = closest.closest_pt(
|
||||
Pt2D::from_gps(collision.location, map.get_gps_bounds()),
|
||||
@ -70,27 +201,47 @@ impl CollisionsViewer {
|
||||
// Color roads and intersections using the counts
|
||||
let mut colorer = ColorNetwork::new(app);
|
||||
// TODO We should use some scale for both!
|
||||
colorer.pct_roads(per_road, &app.cs.good_to_bad_red);
|
||||
colorer.pct_intersections(per_intersection, &app.cs.good_to_bad_red);
|
||||
colorer.pct_roads(per_road.clone(), &app.cs.good_to_bad_red);
|
||||
colorer.pct_intersections(per_intersection.clone(), &app.cs.good_to_bad_red);
|
||||
let (unzoomed, zoomed) = colorer.build(ctx);
|
||||
|
||||
Box::new(CollisionsViewer {
|
||||
Dataviz::Aggregated {
|
||||
unzoomed,
|
||||
zoomed,
|
||||
panel: Panel::new(Widget::col(vec![Widget::row(vec![
|
||||
Line("Collisions viewer").small_heading().draw(ctx),
|
||||
Btn::close(ctx),
|
||||
])]))
|
||||
.aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
|
||||
.build(ctx),
|
||||
})
|
||||
per_road,
|
||||
per_intersection,
|
||||
}
|
||||
}
|
||||
|
||||
fn individual(
|
||||
ctx: &mut EventCtx,
|
||||
app: &App,
|
||||
data: &CollisionDataset,
|
||||
indices: Vec<usize>,
|
||||
) -> Dataviz {
|
||||
let mut hitboxes = Vec::new();
|
||||
let mut batch = GeomBatch::new();
|
||||
for idx in indices {
|
||||
let collision = &data.collisions[idx];
|
||||
let circle = Circle::new(
|
||||
Pt2D::from_gps(collision.location, app.primary.map.get_gps_bounds()),
|
||||
Distance::meters(5.0),
|
||||
);
|
||||
batch.push(Color::RED, circle.to_polygon());
|
||||
hitboxes.push((circle, idx));
|
||||
}
|
||||
Dataviz::Individual {
|
||||
hitboxes,
|
||||
draw_all_circles: ctx.upload(batch),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State<App> for CollisionsViewer {
|
||||
fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition {
|
||||
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
|
||||
ctx.canvas_movement();
|
||||
|
||||
let old_filters = Filters::from_controls(&self.panel);
|
||||
match self.panel.event(ctx) {
|
||||
Outcome::Clicked(x) => match x.as_ref() {
|
||||
"close" => {
|
||||
@ -100,15 +251,44 @@ impl State<App> for CollisionsViewer {
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
// TODO Should fiddling with sliders produce Outcome::Changed?
|
||||
let filters = Filters::from_controls(&self.panel);
|
||||
if filters != old_filters {
|
||||
let indices = filters.apply(&self.data);
|
||||
let count = indices.len();
|
||||
self.dataviz = if filters.show_individual {
|
||||
Dataviz::individual(ctx, app, &self.data, indices)
|
||||
} else {
|
||||
Dataviz::aggregated(ctx, app, &self.data, indices)
|
||||
};
|
||||
let count = format!("{} collisions", prettyprint_usize(count))
|
||||
.draw_text(ctx)
|
||||
.named("count");
|
||||
self.panel.replace(ctx, "count", count);
|
||||
}
|
||||
|
||||
Transition::Keep
|
||||
}
|
||||
|
||||
fn draw(&self, g: &mut GfxCtx, app: &App) {
|
||||
if g.canvas.cam_zoom < app.opts.min_zoom_for_detail {
|
||||
g.redraw(&self.unzoomed);
|
||||
} else {
|
||||
g.redraw(&self.zoomed);
|
||||
match self.dataviz {
|
||||
Dataviz::Aggregated {
|
||||
ref unzoomed,
|
||||
ref zoomed,
|
||||
..
|
||||
} => {
|
||||
if g.canvas.cam_zoom < app.opts.min_zoom_for_detail {
|
||||
g.redraw(unzoomed);
|
||||
} else {
|
||||
g.redraw(zoomed);
|
||||
}
|
||||
}
|
||||
Dataviz::Individual {
|
||||
ref draw_all_circles,
|
||||
..
|
||||
} => {
|
||||
g.redraw(draw_all_circles);
|
||||
}
|
||||
}
|
||||
self.panel.draw(g);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user