game/sandbox/dashboards/
mode_shift.rs

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                    // TODO Automatically recalculate as filters change? Too slow.
72                    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        // TODO This only draws a route if the trip has already happened in the simulation
94        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    // Only when we prebaked data?
108    //actual_driving_time: Duration,
109    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                // TODO Does ? work
144                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                        // TODO The distance (and elevation change) might differ between the two
158                        // paths if there's a highway or a trail. For now, just use the biking
159                        // distance.
160                        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            // Just some sample defaults
179            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        // Maybe some kind of sorting / filtering actually would be useful here
285        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        // Find all high-stress roads, since we'll filter by them next
297        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                // No Contraflow steps for bike paths
318                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}