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 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 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 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 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 let total = per_road.max().max(per_intersection.max());
187
188 for (r, count) in per_road.consume() {
189 world
190 .add_unnamed()
191 .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 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
306fn pct(value: usize, total: usize) -> f64 {
308 if total == 0 {
309 1.0
310 } else {
311 value as f64 / total as f64
312 }
313}