game/devtools/
collisions.rs

1use crate::ID;
2use abstutil::{prettyprint_usize, Counter};
3use collisions::{CollisionDataset, Severity};
4use geom::{Circle, Distance, Duration, FindClosest, Time};
5use widgetry::mapspace::{DummyID, World};
6use widgetry::{
7    Choice, Color, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Line, Outcome, Panel, Slider,
8    State, Text, TextExt, Toggle, VerticalAlignment, Widget,
9};
10
11use crate::app::{App, Transition};
12
13pub struct CollisionsViewer {
14    data: CollisionDataset,
15    world: World<DummyID>,
16    panel: Panel,
17}
18
19impl CollisionsViewer {
20    pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
21        let map = &app.primary.map;
22        let data = ctx.loading_screen("load collision data", |_, timer| {
23            let mut all: CollisionDataset =
24                abstio::read_binary(map.get_city_name().input_path("collisions.bin"), timer);
25            all.collisions.retain(|c| {
26                map.get_boundary_polygon()
27                    .contains_pt(c.location.to_pt(map.get_gps_bounds()))
28            });
29            all
30        });
31
32        let filters = Filters::new();
33        let indices = filters.apply(&data);
34        let count = indices.len();
35        let world = aggregated(ctx, app, &data, indices);
36
37        Box::new(CollisionsViewer {
38            panel: Panel::new_builder(Widget::col(vec![
39                Widget::row(vec![
40                    Line("Collisions viewer").small_heading().into_widget(ctx),
41                    ctx.style().btn_close_widget(ctx),
42                ]),
43                format!("{} collisions", prettyprint_usize(count))
44                    .text_widget(ctx)
45                    .named("count"),
46                Filters::make_controls(ctx).named("controls"),
47            ]))
48            .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
49            .build(ctx),
50            data,
51            world,
52        })
53    }
54}
55
56#[derive(PartialEq)]
57struct Filters {
58    show_individual: bool,
59    time_range: (Duration, Duration),
60    severity: Option<Severity>,
61}
62
63impl Filters {
64    fn new() -> Filters {
65        Filters {
66            show_individual: false,
67            time_range: (Duration::ZERO, Duration::hours(24)),
68            severity: None,
69        }
70    }
71
72    /// Returns the indices of all matching collisions
73    fn apply(&self, data: &CollisionDataset) -> Vec<usize> {
74        let mut indices = Vec::new();
75        for (idx, c) in data.collisions.iter().enumerate() {
76            if c.time < self.time_range.0 || c.time > self.time_range.1 {
77                continue;
78            }
79            if self.severity.map(|s| s != c.severity).unwrap_or(false) {
80                continue;
81            }
82            indices.push(idx);
83        }
84        indices
85    }
86
87    fn make_controls(ctx: &mut EventCtx) -> Widget {
88        Widget::col(vec![
89            Toggle::choice(
90                ctx,
91                "individual / aggregated",
92                "individual",
93                "aggregated",
94                None,
95                false,
96            ),
97            Widget::row(vec![
98                "Between:".text_widget(ctx).margin_right(20),
99                Slider::area(ctx, 0.1 * ctx.canvas.window_width, 0.0, "time1"),
100            ]),
101            Widget::row(vec![
102                "and:".text_widget(ctx).margin_right(20),
103                Slider::area(ctx, 0.1 * ctx.canvas.window_width, 1.0, "time2"),
104            ]),
105            Widget::row(vec![
106                "Severity:".text_widget(ctx).margin_right(20),
107                Widget::dropdown(
108                    ctx,
109                    "severity",
110                    None,
111                    vec![
112                        Choice::new("any", None),
113                        Choice::new("slight", Some(Severity::Slight)),
114                        Choice::new("serious", Some(Severity::Serious)),
115                        Choice::new("fatal", Some(Severity::Fatal)),
116                    ],
117                ),
118            ]),
119        ])
120    }
121
122    fn from_controls(panel: &Panel) -> Filters {
123        let end_of_day = Duration::hours(24);
124        Filters {
125            show_individual: panel.is_checked("individual / aggregated"),
126            time_range: (
127                end_of_day * panel.slider("time1").get_percent(),
128                end_of_day * panel.slider("time2").get_percent(),
129            ),
130            severity: panel.dropdown_value("severity"),
131        }
132    }
133}
134
135fn aggregated(
136    ctx: &mut EventCtx,
137    app: &App,
138    data: &CollisionDataset,
139    indices: Vec<usize>,
140) -> World<DummyID> {
141    let map = &app.primary.map;
142
143    // Match each collision to the nearest road and intersection
144    let mut closest: FindClosest<ID> = FindClosest::new();
145    for i in map.all_intersections() {
146        closest.add_polygon(ID::Intersection(i.id), &i.polygon);
147    }
148    for r in map.all_roads() {
149        closest.add(ID::Road(r.id), r.center_pts.points());
150    }
151
152    // How many collisions occurred at each road and intersection?
153    let mut per_road = Counter::new();
154    let mut per_intersection = Counter::new();
155    let mut unsnapped = 0;
156    for idx in indices {
157        let collision = &data.collisions[idx];
158        // Search up to 10m away
159        if let Some((id, _)) = closest.closest_pt(
160            collision.location.to_pt(map.get_gps_bounds()),
161            Distance::meters(10.0),
162        ) {
163            match id {
164                ID::Road(r) => {
165                    per_road.inc(r);
166                }
167                ID::Intersection(i) => {
168                    per_intersection.inc(i);
169                }
170                _ => unreachable!(),
171            }
172        } else {
173            unsnapped += 1;
174        }
175    }
176    if unsnapped > 0 {
177        warn!(
178            "{} collisions weren't close enough to a road or intersection",
179            prettyprint_usize(unsnapped)
180        );
181    }
182
183    let mut world = World::new();
184    let scale = &app.cs.good_to_bad_red;
185    // Same scale for both roads and intersections
186    let total = per_road.max().max(per_intersection.max());
187
188    for (r, count) in per_road.consume() {
189        world
190            .add_unnamed()
191            // TODO Moving a very small bit of logic from ColorNetwork::pct_roads here...
192            .hitbox(map.get_r(r).get_thick_polygon())
193            .draw_color(scale.eval(pct(count, total)))
194            .hover_alpha(0.5)
195            .tooltip(Text::from(format!(
196                "{} collisions",
197                prettyprint_usize(count)
198            )))
199            .build(ctx);
200    }
201    for (i, count) in per_intersection.consume() {
202        world
203            .add_unnamed()
204            .hitbox(map.get_i(i).polygon.clone())
205            .draw_color(scale.eval(pct(count, total)))
206            .hover_alpha(0.5)
207            .tooltip(Text::from(format!(
208                "{} collisions",
209                prettyprint_usize(count)
210            )))
211            .build(ctx);
212    }
213
214    world.draw_master_batch(
215        ctx,
216        GeomBatch::from(vec![(
217            app.cs.fade_map_dark,
218            map.get_boundary_polygon().clone(),
219        )]),
220    );
221    world.initialize_hover(ctx);
222    world
223}
224
225fn individual(
226    ctx: &mut EventCtx,
227    app: &App,
228    data: &CollisionDataset,
229    indices: Vec<usize>,
230) -> World<DummyID> {
231    let map = &app.primary.map;
232    let mut world = World::new();
233
234    for idx in indices {
235        let collision = &data.collisions[idx];
236
237        // TODO Multiple collisions can occur at exactly the same spot. Need to add support for
238        // that in World -- the KML viewer is the example to follow.
239        world
240            .add_unnamed()
241            .hitbox(
242                Circle::new(
243                    collision.location.to_pt(map.get_gps_bounds()),
244                    Distance::meters(5.0),
245                )
246                .to_polygon(),
247            )
248            .draw_color(Color::RED)
249            .hover_alpha(0.5)
250            .tooltip(Text::from_multiline(vec![
251                Line(format!(
252                    "Time: {}",
253                    (Time::START_OF_DAY + collision.time).ampm_tostring()
254                )),
255                Line(format!("Severity: {:?}", collision.severity)),
256            ]))
257            .build(ctx);
258    }
259
260    world.draw_master_batch(
261        ctx,
262        GeomBatch::from(vec![(
263            app.cs.fade_map_dark,
264            map.get_boundary_polygon().clone(),
265        )]),
266    );
267    world.initialize_hover(ctx);
268    world
269}
270
271impl State<App> for CollisionsViewer {
272    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
273        self.world.event(ctx);
274
275        match self.panel.event(ctx) {
276            Outcome::Clicked(x) => match x.as_ref() {
277                "close" => {
278                    return Transition::Pop;
279                }
280                _ => unreachable!(),
281            },
282            Outcome::Changed(_) => {
283                let filters = Filters::from_controls(&self.panel);
284                let indices = filters.apply(&self.data);
285                let count = indices.len();
286                self.world = if filters.show_individual {
287                    individual(ctx, app, &self.data, indices)
288                } else {
289                    aggregated(ctx, app, &self.data, indices)
290                };
291                let count = format!("{} collisions", prettyprint_usize(count)).text_widget(ctx);
292                self.panel.replace(ctx, "count", count);
293            }
294            _ => {}
295        }
296
297        Transition::Keep
298    }
299
300    fn draw(&self, g: &mut GfxCtx, _: &App) {
301        self.world.draw(g);
302        self.panel.draw(g);
303    }
304}
305
306// TODO Refactor -- wasn't geom Percent supposed to help?
307fn pct(value: usize, total: usize) -> f64 {
308    if total == 0 {
309        1.0
310    } else {
311        value as f64 / total as f64
312    }
313}