1use std::collections::HashSet;
2
3use abstutil::Counter;
4use geom::{Distance, Duration};
5use map_gui::tools::ColorNetwork;
6use map_model::PathStepV2;
7use sim::TripID;
8use synthpop::{TripEndpoint, TripMode};
9use widgetry::table::{Col, Filter, Table};
10use widgetry::{
11 Drawable, EventCtx, Filler, GeomBatch, GfxCtx, Line, Outcome, Panel, Spinner, State, Text,
12 TextExt, Widget,
13};
14
15use crate::app::{App, Transition};
16use crate::sandbox::dashboards::generic_trip_table::{open_trip_transition, preview_trip};
17use crate::sandbox::dashboards::DashTab;
18
19pub struct ModeShift {
20 tab: DashTab,
21 table: Table<App, Entry, Filters>,
22 panel: Panel,
23 show_route_gaps: Drawable,
24}
25
26impl ModeShift {
27 pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
28 let table = make_table(ctx, app);
29 let col = Widget::col(vec![
30 DashTab::ModeShift.picker(ctx, app),
31 Widget::col(vec![
32 Text::from_multiline(vec![
33 Line("This looks at transforming driving trips into cycling."),
34 Line("Off-map starts/ends are excluded."),
35 ])
36 .into_widget(ctx),
37 ctx.style()
38 .btn_outline
39 .text("Show most important gaps in cycling infrastructure")
40 .build_def(ctx),
41 table.render(ctx, app),
42 Filler::square_width(ctx, 0.15).named("preview"),
43 ])
44 .section(ctx),
45 ]);
46
47 let panel = Panel::new_builder(col)
48 .exact_size_percent(90, 90)
49 .build(ctx);
50
51 Box::new(Self {
52 tab: DashTab::ModeShift,
53 table,
54 panel,
55 show_route_gaps: Drawable::empty(ctx),
56 })
57 }
58}
59
60impl State<App> for ModeShift {
61 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
62 match self.panel.event(ctx) {
63 Outcome::Clicked(x) => {
64 if self.table.clicked(&x) {
65 self.table.replace_render(ctx, app, &mut self.panel);
66 } else if let Ok(idx) = x.parse::<usize>() {
67 return open_trip_transition(app, idx);
68 } else if x == "close" {
69 return Transition::Pop;
70 } else if x == "Show most important gaps in cycling infrastructure" {
71 self.show_route_gaps = show_route_gaps(ctx, app, &self.table);
73 } else {
74 unreachable!()
75 }
76 }
77 Outcome::Changed(_) => {
78 if let Some(t) = self.tab.transition(ctx, app, &self.panel) {
79 return t;
80 }
81
82 self.table.panel_changed(&self.panel);
83 self.table.replace_render(ctx, app, &mut self.panel);
84 }
85 _ => {}
86 }
87
88 Transition::Keep
89 }
90
91 fn draw(&self, g: &mut GfxCtx, app: &App) {
92 self.panel.draw(g);
93 preview_trip(
95 g,
96 app,
97 &self.panel,
98 GeomBatch::new(),
99 Some(&self.show_route_gaps),
100 );
101 }
102}
103
104struct Entry {
105 trip: TripID,
106 estimated_driving_time: Duration,
107 estimated_biking_time: Duration,
110 distance: Distance,
111 total_elevation_gain: Distance,
112 total_elevation_loss: Distance,
113}
114
115struct Filters {
116 max_driving_time: Duration,
117 max_biking_time: Duration,
118 max_distance: Distance,
119 max_elevation_gain: Distance,
120}
121
122fn produce_raw_data(ctx: &mut EventCtx, app: &App) -> Vec<Entry> {
123 let map = &app.primary.map;
124 ctx.loading_screen("shift modes", |_, timer| {
125 timer.parallelize(
126 "analyze trips",
127 app.primary
128 .sim
129 .all_trip_info()
130 .into_iter()
131 .filter_map(|(id, info)| {
132 if info.mode == TripMode::Drive
133 && matches!(info.start, TripEndpoint::Building(_))
134 && matches!(info.end, TripEndpoint::Building(_))
135 {
136 Some((id, info))
137 } else {
138 None
139 }
140 })
141 .collect(),
142 |(id, info)| {
143 if let (Some(driving_path), Some(biking_path)) = (
145 TripEndpoint::path_req(info.start, info.end, TripMode::Drive, map)
146 .and_then(|req| map.pathfind(req).ok()),
147 TripEndpoint::path_req(info.start, info.end, TripMode::Bike, map)
148 .and_then(|req| map.pathfind(req).ok()),
149 ) {
150 let (total_elevation_gain, total_elevation_loss) =
151 biking_path.get_total_elevation_change(map);
152 Some(Entry {
153 trip: id,
154 estimated_driving_time: driving_path.estimate_duration(map, None),
155 estimated_biking_time: biking_path
156 .estimate_duration(map, Some(map_model::MAX_BIKE_SPEED)),
157 distance: biking_path.total_length(),
161 total_elevation_gain,
162 total_elevation_loss,
163 })
164 } else {
165 None
166 }
167 },
168 )
169 })
170 .into_iter()
171 .flatten()
172 .collect()
173}
174
175fn make_table(ctx: &mut EventCtx, app: &App) -> Table<App, Entry, Filters> {
176 let filter: Filter<App, Entry, Filters> = Filter {
177 state: Filters {
178 max_driving_time: Duration::minutes(30),
180 max_biking_time: Duration::minutes(30),
181 max_distance: Distance::miles(10.0),
182 max_elevation_gain: Distance::feet(30.0),
183 },
184 to_controls: Box::new(|ctx, _, state| {
185 Widget::row(vec![
186 Widget::row(vec![
187 "Max driving time".text_widget(ctx).centered_vert(),
188 Spinner::widget(
189 ctx,
190 "max_driving_time",
191 (Duration::ZERO, Duration::hours(12)),
192 state.max_driving_time,
193 Duration::minutes(1),
194 ),
195 ]),
196 Widget::row(vec![
197 "Max biking time".text_widget(ctx).centered_vert(),
198 Spinner::widget(
199 ctx,
200 "max_biking_time",
201 (Duration::ZERO, Duration::hours(12)),
202 state.max_biking_time,
203 Duration::minutes(1),
204 ),
205 ]),
206 Widget::row(vec![
207 "Max distance".text_widget(ctx).centered_vert(),
208 Spinner::widget(
209 ctx,
210 "max_distance",
211 (Distance::ZERO, Distance::miles(20.0)),
212 state.max_distance,
213 Distance::miles(0.1),
214 ),
215 ]),
216 Widget::row(vec![
217 "Max elevation gain".text_widget(ctx).centered_vert(),
218 Spinner::widget(
219 ctx,
220 "max_elevation_gain",
221 (Distance::ZERO, Distance::feet(500.0)),
222 state.max_elevation_gain,
223 Distance::feet(10.0),
224 ),
225 ]),
226 ])
227 .evenly_spaced()
228 }),
229 from_controls: Box::new(|panel| Filters {
230 max_driving_time: panel.spinner("max_driving_time"),
231 max_biking_time: panel.spinner("max_biking_time"),
232 max_distance: panel.spinner("max_distance"),
233 max_elevation_gain: panel.spinner("max_elevation_gain"),
234 }),
235 apply: Box::new(|state, x, _| {
236 x.estimated_driving_time <= state.max_driving_time
237 && x.estimated_biking_time <= state.max_biking_time
238 && x.distance <= state.max_distance
239 && x.total_elevation_gain <= state.max_elevation_gain
240 }),
241 };
242
243 let mut table = Table::new(
244 "mode_shift",
245 produce_raw_data(ctx, app),
246 Box::new(|x| x.trip.0.to_string()),
247 "Estimated driving time",
248 filter,
249 );
250 table.static_col("Trip ID", Box::new(|x| x.trip.0.to_string()));
251 table.column(
252 "Estimated driving time",
253 Box::new(|ctx, app, x| {
254 Text::from(x.estimated_driving_time.to_string(&app.opts.units)).render(ctx)
255 }),
256 Col::Sortable(Box::new(|rows| {
257 rows.sort_by_key(|x| x.estimated_driving_time)
258 })),
259 );
260 table.column(
261 "Estimated biking time",
262 Box::new(|ctx, app, x| {
263 Text::from(x.estimated_biking_time.to_string(&app.opts.units)).render(ctx)
264 }),
265 Col::Sortable(Box::new(|rows| {
266 rows.sort_by_key(|x| x.estimated_biking_time)
267 })),
268 );
269 table.column(
270 "Distance",
271 Box::new(|ctx, app, x| Text::from(x.distance.to_string(&app.opts.units)).render(ctx)),
272 Col::Sortable(Box::new(|rows| rows.sort_by_key(|x| x.distance))),
273 );
274 table.column(
275 "Elevation gain/loss",
276 Box::new(|ctx, app, x| {
277 Text::from(format!(
278 "Up {}, down {}",
279 x.total_elevation_gain.to_string(&app.opts.units),
280 x.total_elevation_loss.to_string(&app.opts.units)
281 ))
282 .render(ctx)
283 }),
284 Col::Static,
286 );
287
288 table
289}
290
291fn show_route_gaps(ctx: &mut EventCtx, app: &App, table: &Table<App, Entry, Filters>) -> Drawable {
292 ctx.loading_screen("calculate all routes", |ctx, timer| {
293 let map = &app.primary.map;
294 let sim = &app.primary.sim;
295
296 let mut high_stress = HashSet::new();
298 for r in map.all_roads() {
299 for dr in r.id.both_directions() {
300 if r.high_stress_for_bikes(map, dr.dir) {
301 high_stress.insert(dr);
302 }
303 }
304 }
305
306 let mut road_counter = Counter::new();
307 for path in timer
308 .parallelize("calculate routes", table.get_filtered_data(app), |entry| {
309 let info = sim.trip_info(entry.trip);
310 TripEndpoint::path_req(info.start, info.end, TripMode::Bike, map)
311 .and_then(|req| map.pathfind_v2(req).ok())
312 })
313 .into_iter()
314 .flatten()
315 {
316 for step in path.get_steps() {
317 if let PathStepV2::Along(dr) = step {
319 if high_stress.contains(dr) {
320 road_counter.inc(dr.road);
321 }
322 }
323 }
324 }
325
326 let mut colorer = ColorNetwork::new(app);
327 colorer.ranked_roads(road_counter, &app.cs.good_to_bad_red);
328 colorer.build(ctx).unzoomed
329 })
330}