game/sandbox/dashboards/
risks.rs

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                // TODO Handle browsing multiple trips
170                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}