game/sandbox/dashboards/
parking_overhead.rs

1use geom::Duration;
2use sim::{TripID, TripPhaseType};
3use synthpop::TripEndpoint;
4use widgetry::table::{Col, Filter, Table};
5use widgetry::{
6    EventCtx, Filler, GeomBatch, GfxCtx, Line, Outcome, Panel, State, Text, Toggle, Widget,
7};
8
9use crate::app::{App, Transition};
10use crate::sandbox::dashboards::generic_trip_table::{open_trip_transition, preview_trip};
11use crate::sandbox::dashboards::DashTab;
12
13// TODO Compare all of these things before/after
14
15pub struct ParkingOverhead {
16    tab: DashTab,
17    table: Table<App, Entry, Filters>,
18    panel: Panel,
19}
20
21impl ParkingOverhead {
22    pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
23        let table = make_table(app);
24        let col = Widget::col(vec![
25            DashTab::ParkingOverhead.picker(ctx, app),
26            Widget::col(vec![
27                Widget::row(vec![
28                    Text::from_multiline(vec![
29                        Line(
30                            "Trips taken by car also include time to walk between the building \
31                             and parking spot, as well as the time to find parking.",
32                        ),
33                        Line("Overhead is 1 - driving time / total time"),
34                        Line("Ideally, overhead is 0% -- the entire trip is just spent driving."),
35                        Line(""),
36                        Line("High overhead could mean:"),
37                        Line(
38                            "- the car burned more resources and caused more traffic looking for \
39                             parking",
40                        ),
41                        Line(
42                            "- somebody with impaired movement had to walk far to reach their \
43                             vehicle",
44                        ),
45                        Line("- the person was inconvenienced"),
46                        Line(""),
47                        Line(
48                            "Note: Trips beginning/ending outside the map have an artificially \
49                             high overhead,",
50                        ),
51                        Line("since the time spent driving off-map isn't shown here."),
52                    ])
53                    .into_widget(ctx),
54                    Filler::square_width(ctx, 0.15).named("preview"),
55                ])
56                .evenly_spaced(),
57                table.render(ctx, app),
58            ])
59            .section(ctx),
60        ]);
61
62        let panel = Panel::new_builder(col)
63            .exact_size_percent(90, 90)
64            .build(ctx);
65
66        Box::new(Self {
67            tab: DashTab::ParkingOverhead,
68            table,
69            panel,
70        })
71    }
72}
73
74impl State<App> for ParkingOverhead {
75    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
76        match self.panel.event(ctx) {
77            Outcome::Clicked(x) => {
78                if self.table.clicked(&x) {
79                    self.table.replace_render(ctx, app, &mut self.panel);
80                } else if let Ok(idx) = x.parse::<usize>() {
81                    return open_trip_transition(app, idx);
82                } else if x == "close" {
83                    return Transition::Pop;
84                } else {
85                    unreachable!()
86                }
87            }
88            Outcome::Changed(_) => {
89                if let Some(t) = self.tab.transition(ctx, app, &self.panel) {
90                    return t;
91                }
92
93                self.table.panel_changed(&self.panel);
94                self.table.replace_render(ctx, app, &mut self.panel);
95            }
96            _ => {}
97        }
98
99        Transition::Keep
100    }
101
102    fn draw(&self, g: &mut GfxCtx, app: &App) {
103        self.panel.draw(g);
104        preview_trip(g, app, &self.panel, GeomBatch::new(), None);
105    }
106}
107
108struct Entry {
109    trip: TripID,
110    total_duration: Duration,
111    driving_duration: Duration,
112    parking_duration: Duration,
113    walking_duration: Duration,
114    percent_overhead: usize,
115    starts_off_map: bool,
116    ends_off_map: bool,
117}
118
119struct Filters {
120    starts_off_map: bool,
121    ends_off_map: bool,
122}
123
124fn produce_raw_data(app: &App) -> Vec<Entry> {
125    // Gather raw data
126    let mut data = Vec::new();
127    for (id, phases) in app.primary.sim.get_analytics().get_all_trip_phases() {
128        let trip = app.primary.sim.trip_info(id);
129        let starts_off_map = matches!(trip.start, TripEndpoint::Border(_));
130        let ends_off_map = matches!(trip.end, TripEndpoint::Border(_));
131
132        let mut total_duration = Duration::ZERO;
133        let mut driving_duration = Duration::ZERO;
134        let mut parking_duration = Duration::ZERO;
135        let mut walking_duration = Duration::ZERO;
136        let mut ok = true;
137        for p in phases {
138            if let Some(t2) = p.end_time {
139                let dt = t2 - p.start_time;
140                total_duration += dt;
141                match p.phase_type {
142                    TripPhaseType::Driving => {
143                        driving_duration += dt;
144                    }
145                    TripPhaseType::Walking => {
146                        walking_duration += dt;
147                    }
148                    TripPhaseType::Parking => {
149                        parking_duration += dt;
150                    }
151                    _ => {}
152                }
153            } else {
154                ok = false;
155                break;
156            }
157        }
158        if !ok || driving_duration == Duration::ZERO {
159            continue;
160        }
161
162        data.push(Entry {
163            trip: id,
164            total_duration,
165            driving_duration,
166            parking_duration,
167            walking_duration,
168            percent_overhead: (100.0 * (1.0 - (driving_duration / total_duration))) as usize,
169            starts_off_map,
170            ends_off_map,
171        });
172    }
173    data
174}
175
176fn make_table(app: &App) -> Table<App, Entry, Filters> {
177    let filter: Filter<App, Entry, Filters> = Filter {
178        state: Filters {
179            starts_off_map: true,
180            ends_off_map: true,
181        },
182        to_controls: Box::new(|ctx, _, state| {
183            Widget::row(vec![
184                Toggle::switch(ctx, "starting off-map", None, state.starts_off_map),
185                Toggle::switch(ctx, "ending off-map", None, state.ends_off_map),
186            ])
187        }),
188        from_controls: Box::new(|panel| Filters {
189            starts_off_map: panel.is_checked("starting off-map"),
190            ends_off_map: panel.is_checked("ending off-map"),
191        }),
192        apply: Box::new(|state, x, _| {
193            if !state.starts_off_map && x.starts_off_map {
194                return false;
195            }
196            if !state.ends_off_map && x.ends_off_map {
197                return false;
198            }
199            true
200        }),
201    };
202
203    let mut table = Table::new(
204        "parking_overhead",
205        produce_raw_data(app),
206        Box::new(|x| x.trip.0.to_string()),
207        "Percent overhead",
208        filter,
209    );
210    table.static_col("Trip ID", Box::new(|x| x.trip.0.to_string()));
211    table.column(
212        "Total duration",
213        Box::new(|ctx, app, x| Text::from(x.total_duration.to_string(&app.opts.units)).render(ctx)),
214        Col::Sortable(Box::new(|rows| rows.sort_by_key(|x| x.total_duration))),
215    );
216    table.column(
217        "Driving duration",
218        Box::new(|ctx, app, x| {
219            Text::from(x.driving_duration.to_string(&app.opts.units)).render(ctx)
220        }),
221        Col::Sortable(Box::new(|rows| rows.sort_by_key(|x| x.driving_duration))),
222    );
223    table.column(
224        "Parking duration",
225        Box::new(|ctx, app, x| {
226            Text::from(x.parking_duration.to_string(&app.opts.units)).render(ctx)
227        }),
228        Col::Sortable(Box::new(|rows| rows.sort_by_key(|x| x.parking_duration))),
229    );
230    table.column(
231        "Walking duration",
232        Box::new(|ctx, app, x| {
233            Text::from(x.walking_duration.to_string(&app.opts.units)).render(ctx)
234        }),
235        Col::Sortable(Box::new(|rows| rows.sort_by_key(|x| x.walking_duration))),
236    );
237    table.column(
238        "Percent overhead",
239        Box::new(|ctx, _, x| Text::from(format!("{}%", x.percent_overhead)).render(ctx)),
240        Col::Sortable(Box::new(|rows| rows.sort_by_key(|x| x.percent_overhead))),
241    );
242
243    table
244}