1use std::collections::BTreeSet;
2use std::fmt::Write;
3
4use anyhow::Result;
5
6use abstutil::prettyprint_usize;
7use sim::{ProblemType, TripID};
8use synthpop::TripMode;
9use widgetry::tools::PopupMsg;
10use widgetry::{EventCtx, GfxCtx, Image, Line, Outcome, Panel, State, TextExt, Toggle, Widget};
11
12use super::trip_problems::{problem_matrix, TripProblemFilter};
13use crate::app::{App, Transition};
14use crate::sandbox::dashboards::generic_trip_table::open_trip_transition;
15use crate::sandbox::dashboards::DashTab;
16
17pub struct RiskSummaries {
18 panel: Panel,
19}
20
21impl RiskSummaries {
22 pub fn new_state(
23 ctx: &mut EventCtx,
24 app: &App,
25 include_no_changes: bool,
26 ) -> Box<dyn State<App>> {
27 let bike_filter = Filter {
28 modes: maplit::btreeset! { TripMode::Bike },
29 include_no_changes,
30 };
31
32 let ped_filter = Filter {
33 modes: maplit::btreeset! { TripMode::Walk },
34 include_no_changes,
35 };
36
37 Box::new(RiskSummaries {
38 panel: Panel::new_builder(Widget::col(vec![
39 DashTab::RiskSummaries.picker(ctx, app),
40 Widget::col(vec![
41 "Filters".text_widget(ctx),
42 Toggle::checkbox(
43 ctx,
44 "include trips without any changes",
45 None,
46 include_no_changes,
47 ),
48 ])
49 .section(ctx),
50 Widget::row(vec![
51 Image::from_path("system/assets/meters/pedestrian.svg")
52 .dims(36.0)
53 .into_widget(ctx)
54 .centered_vert(),
55 Line(format!(
56 "Pedestrian Risks - {} Finished Trips",
57 prettyprint_usize(ped_filter.finished_trip_count(app))
58 ))
59 .big_heading_plain()
60 .into_widget(ctx)
61 .centered_vert(),
62 ])
63 .margin_above(30),
64 Widget::evenly_spaced_row(
65 32,
66 vec![
67 Widget::col(vec![
68 Line("Arterial intersection crossings")
69 .small_heading()
70 .into_widget(ctx)
71 .centered_horiz(),
72 problem_matrix(
73 ctx,
74 app,
75 ped_filter
76 .trip_problems(app, ProblemType::ArterialIntersectionCrossing),
77 ),
78 ])
79 .section(ctx),
80 Widget::col(vec![
81 Line("Overcrowded sidewalks")
82 .small_heading()
83 .into_widget(ctx)
84 .centered_horiz(),
85 problem_matrix(
86 ctx,
87 app,
88 ped_filter.trip_problems(app, ProblemType::PedestrianOvercrowding),
89 ),
90 ])
91 .section(ctx),
92 ],
93 )
94 .margin_above(30),
95 Widget::row(vec![
96 Image::from_path("system/assets/meters/bike.svg")
97 .dims(36.0)
98 .into_widget(ctx)
99 .centered_vert(),
100 Line(format!(
101 "Cyclist Risks - {} Finished Trips",
102 prettyprint_usize(bike_filter.finished_trip_count(app))
103 ))
104 .big_heading_plain()
105 .into_widget(ctx)
106 .centered_vert(),
107 ])
108 .margin_above(30),
109 Widget::evenly_spaced_row(
110 32,
111 vec![
112 Widget::col(vec![
113 Line("Complex intersection crossings")
114 .small_heading()
115 .into_widget(ctx)
116 .centered_horiz(),
117 problem_matrix(
118 ctx,
119 app,
120 bike_filter
121 .trip_problems(app, ProblemType::ComplexIntersectionCrossing),
122 ),
123 ])
124 .section(ctx),
125 Widget::col(vec![
126 Line("Cars wanting to over-take cyclists")
127 .small_heading()
128 .into_widget(ctx)
129 .centered_horiz(),
130 problem_matrix(
131 ctx,
132 app,
133 bike_filter.trip_problems(app, ProblemType::OvertakeDesired),
134 ),
135 ])
136 .section(ctx),
137 ],
138 )
139 .margin_above(30),
140 ctx.style().btn_plain.text("Export to CSV").build_def(ctx),
141 ]))
142 .exact_size_percent(90, 90)
143 .build(ctx),
144 })
145 }
146}
147
148impl State<App> for RiskSummaries {
149 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
150 match self.panel.event(ctx) {
151 Outcome::Clicked(x) => match x.as_ref() {
152 "close" => Transition::Pop,
153 "Export to CSV" => {
154 return Transition::Push(match export_problems(app) {
155 Ok(path) => PopupMsg::new_state(
156 ctx,
157 "Data exported",
158 vec![format!("Data exported to {}", path)],
159 ),
160 Err(err) => {
161 PopupMsg::new_state(ctx, "Export failed", vec![err.to_string()])
162 }
163 });
164 }
165 _ => unreachable!(),
166 },
167 Outcome::ClickCustom(data) => {
168 let trips = data.as_any().downcast_ref::<Vec<TripID>>().unwrap();
169 open_trip_transition(app, trips[0].0)
171 }
172 Outcome::Changed(_) => {
173 if let Some(t) = DashTab::RiskSummaries.transition(ctx, app, &self.panel) {
174 return t;
175 }
176
177 let include_no_changes = self.panel.is_checked("include trips without any changes");
178 Transition::Replace(RiskSummaries::new_state(ctx, app, include_no_changes))
179 }
180 _ => Transition::Keep,
181 }
182 }
183
184 fn draw(&self, g: &mut GfxCtx, _app: &App) {
185 self.panel.draw(g);
186 }
187}
188
189pub struct Filter {
190 modes: BTreeSet<TripMode>,
191 include_no_changes: bool,
192}
193
194impl TripProblemFilter for Filter {
195 fn includes_mode(&self, mode: &TripMode) -> bool {
196 self.modes.contains(mode)
197 }
198 fn include_no_changes(&self) -> bool {
199 self.include_no_changes
200 }
201}
202
203fn export_problems(app: &App) -> Result<String> {
204 let path = format!(
205 "trip_problems_{}_{}.csv",
206 app.primary.map.get_name().as_filename(),
207 app.primary.sim.time().as_filename()
208 );
209 let mut out = String::new();
210 writeln!(
211 out,
212 "id,mode,seconds_after,problem_type,problems_before,problems_after"
213 )?;
214
215 let before = app.prebaked();
216 let after = app.primary.sim.get_analytics();
217 let empty = Vec::new();
218
219 for (id, _, time_after, mode) in after.both_finished_trips(app.primary.sim.time(), before) {
220 for problem_type in ProblemType::all() {
221 let count_before =
222 problem_type.count(before.problems_per_trip.get(&id).unwrap_or(&empty));
223 let count_after =
224 problem_type.count(after.problems_per_trip.get(&id).unwrap_or(&empty));
225 if count_before != 0 || count_after != 0 {
226 writeln!(
227 out,
228 "{},{:?},{},{:?},{},{}",
229 id.0,
230 mode,
231 time_after.inner_seconds(),
232 problem_type,
233 count_before,
234 count_after
235 )?;
236 }
237 }
238 }
239
240 abstio::write_file(path, out)
241}