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
13pub 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 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}