ltn/pages/
predict_impact.rs

1use std::collections::BTreeSet;
2
3use anyhow::Result;
4use geo::MapCoordsInPlace;
5use rand::SeedableRng;
6use rand_xorshift::XorShiftRng;
7use serde::Serialize;
8
9use map_gui::tools::checkbox_per_mode;
10use map_model::{PathV2, Road};
11use synthpop::make::ScenarioGenerator;
12use synthpop::{Scenario, TripMode};
13use widgetry::tools::{FileLoader, PopupMsg};
14use widgetry::{
15    Color, DrawBaselayer, Drawable, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome,
16    Panel, Slider, State, Text, TextExt, Toggle, VerticalAlignment, Widget,
17};
18
19use crate::components::{AppwidePanel, Mode};
20use crate::logic::impact::{end_of_day, Filters, Impact};
21use crate::render::colors;
22use crate::{App, Transition};
23
24// TODO Share structure or pieces with Ungap's predict mode
25// ... can't we just produce data of a certain shape, and have a UI pretty tuned for that?
26
27pub struct ShowImpactResults {
28    appwide_panel: AppwidePanel,
29    left_panel: Panel,
30}
31
32impl ShowImpactResults {
33    pub fn new_state(ctx: &mut EventCtx, app: &mut App) -> Box<dyn State<App>> {
34        let map_name = app.per_map.map.get_name().clone();
35        if app.per_map.impact.map != map_name {
36            let scenario_name = Scenario::default_scenario_for_map(&map_name);
37
38            if scenario_name != "home_to_work" {
39                return FileLoader::<App, Scenario>::new_state(
40                    ctx,
41                    abstio::path_scenario(&map_name, &scenario_name),
42                    Box::new(move |ctx, app, timer, maybe_scenario| {
43                        // TODO Handle corrupt files
44                        let scenario = maybe_scenario.unwrap();
45                        app.per_map.impact = Impact::from_scenario(ctx, app, scenario, timer);
46                        Transition::Replace(ShowImpactResults::new_state(ctx, app))
47                    }),
48                );
49            }
50            ctx.loading_screen("synthesize travel demand model", |ctx, timer| {
51                // TODO Argh, this internally uses the map's pathfinder to estimate mode split.
52                // Just ignore any edits or pre-existing files.
53                app.per_map.map.keep_pathfinder_despite_edits();
54
55                let scenario = ScenarioGenerator::proletariat_robot(
56                    &app.per_map.map,
57                    &mut XorShiftRng::seed_from_u64(42),
58                    timer,
59                );
60                app.per_map.impact = Impact::from_scenario(ctx, app, scenario, timer);
61            });
62        }
63
64        if app.per_map.impact.map_edit_key != app.per_map.map.get_edits_change_key() {
65            ctx.loading_screen("recalculate impact", |ctx, timer| {
66                // Avoid a double borrow
67                let mut impact = std::mem::replace(&mut app.per_map.impact, Impact::empty(ctx));
68                impact.map_edits_changed(ctx, app, timer);
69                app.per_map.impact = impact;
70            });
71        }
72
73        let contents = Widget::col(vec![
74            Line("Impact prediction").small_heading().into_widget(ctx),
75            Text::from(Line("This tool starts with a travel demand model, calculates the route every trip takes before and after changes, and displays volumes along roads")).wrap_to_pct(ctx, 20).into_widget(ctx),
76            Text::from_all(vec![
77                    Line("Red").fg(Color::RED),
78                    Line(" roads have increased volume, and "),
79                    Line("green").fg(Color::GREEN),
80                    Line(" roads have less. Width of the road shows how much baseline traffic it has."),
81                ]).wrap_to_pct(ctx, 20).into_widget(ctx),
82                Text::from(Line("Click a road to see changed routes through it.")).wrap_to_pct(ctx, 20).into_widget(ctx),
83                Text::from(Line("Results may be wrong for various reasons. Interpret carefully.").bold_body()).wrap_to_pct(ctx, 20).into_widget(ctx),
84            // TODO Dropdown for the scenario, and explain its source/limitations
85            app.per_map.impact.filters.to_panel(ctx, app),
86            app.per_map
87                .impact
88                .compare_counts
89                .get_panel_widget(ctx)
90                .named("compare counts"),
91            ctx.style()
92                .btn_outline
93                .text("Save before/after counts to files (JSON)")
94                .build_def(ctx),
95            ctx.style()
96                .btn_outline
97                .text("Save before/after counts to files (CSV)")
98                .build_def(ctx),
99            ctx.style()
100                .btn_outline
101                .text("Save before/after counts to files (GeoJSON)")
102                .build_def(ctx),
103        ]);
104        let appwide_panel = AppwidePanel::new(ctx, app, Mode::Impact);
105        let left_panel =
106            crate::components::LeftPanel::builder(ctx, &appwide_panel.top_panel, contents)
107                .build(ctx);
108
109        Box::new(Self {
110            appwide_panel,
111            left_panel,
112        })
113    }
114}
115impl State<App> for ShowImpactResults {
116    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
117        // PreserveState doesn't matter
118        if let Some(t) =
119            self.appwide_panel
120                .event(ctx, app, &crate::save::PreserveState::Route, help)
121        {
122            return t;
123        }
124        if let Some(t) = app.session.layers.event(ctx, &app.cs, Mode::Impact, None) {
125            return t;
126        }
127        match self.left_panel.event(ctx) {
128            Outcome::Clicked(x) => match x.as_ref() {
129                "Save before/after counts to files (JSON)" => {
130                    let path1 = "counts_a.json";
131                    let path2 = "counts_b.json";
132                    abstio::write_json(
133                        path1.to_string(),
134                        &app.per_map.impact.compare_counts.counts_a,
135                    );
136                    abstio::write_json(
137                        path2.to_string(),
138                        &app.per_map.impact.compare_counts.counts_b,
139                    );
140                    return Transition::Push(PopupMsg::new_state(
141                        ctx,
142                        "Saved",
143                        vec![format!("Saved {} and {}", path1, path2)],
144                    ));
145                }
146                "Save before/after counts to files (CSV)" => {
147                    let path = "before_after_counts.csv";
148                    let msg = match export_csv(app)
149                        .and_then(|contents| abstio::write_file(path.to_string(), contents))
150                    {
151                        Ok(_) => format!("Saved {path}"),
152                        Err(err) => format!("Failed to export: {err}"),
153                    };
154                    return Transition::Push(PopupMsg::new_state(ctx, "CSV export", vec![msg]));
155                }
156                "Save before/after counts to files (GeoJSON)" => {
157                    let path = "before_after_counts.geojson";
158                    let msg = match export_geojson(app)
159                        .and_then(|contents| abstio::write_file(path.to_string(), contents))
160                    {
161                        Ok(_) => format!("Saved {path}"),
162                        Err(err) => format!("Failed to export: {err}"),
163                    };
164                    return Transition::Push(PopupMsg::new_state(ctx, "GeoJSON export", vec![msg]));
165                }
166                x => {
167                    // Avoid a double borrow
168                    let mut impact = std::mem::replace(&mut app.per_map.impact, Impact::empty(ctx));
169                    let widget = impact
170                        .compare_counts
171                        .on_click(ctx, app, x)
172                        .expect("button click didn't belong to CompareCounts");
173                    app.per_map.impact = impact;
174                    self.left_panel.replace(ctx, "compare counts", widget);
175                    return Transition::Keep;
176                }
177            },
178            Outcome::Changed(_) => {
179                // TODO The sliders should only trigger updates when the user lets go; way too slow
180                // otherwise
181                let filters = Filters::from_panel(&self.left_panel);
182                if filters == app.per_map.impact.filters {
183                    return Transition::Keep;
184                }
185
186                // Avoid a double borrow
187                let mut impact = std::mem::replace(&mut app.per_map.impact, Impact::empty(ctx));
188                impact.filters = Filters::from_panel(&self.left_panel);
189                ctx.loading_screen("update filters", |ctx, timer| {
190                    impact.trips_changed(ctx, app, timer);
191                });
192                app.per_map.impact = impact;
193                return Transition::Keep;
194            }
195            _ => {}
196        }
197
198        if let Some(r) = app.per_map.impact.compare_counts.other_event(ctx) {
199            let results = ctx.loading_screen("find changed routes", |_, timer| {
200                app.per_map.impact.find_changed_routes(app, r, timer)
201            });
202            return Transition::Push(ChangedRoutes::new_state(ctx, app, results));
203        }
204
205        Transition::Keep
206    }
207
208    fn draw_baselayer(&self) -> DrawBaselayer {
209        DrawBaselayer::Custom
210    }
211
212    fn draw(&self, g: &mut GfxCtx, app: &App) {
213        // Just emphasize roads that've changed, so don't draw the baselayer of roads. Even
214        // buildings are a distraction.
215        g.clear(app.cs.void_background);
216        g.redraw(&app.per_map.draw_map.boundary_polygon);
217        g.redraw(&app.per_map.draw_map.draw_all_areas);
218        app.per_map.impact.compare_counts.draw(g, app);
219        app.per_map.draw_all_filters.draw(g);
220
221        self.appwide_panel.draw(g);
222        self.left_panel.draw(g);
223        app.session.layers.draw(g, app);
224    }
225
226    fn recreate(&mut self, ctx: &mut EventCtx, app: &mut App) -> Box<dyn State<App>> {
227        Self::new_state(ctx, app)
228    }
229}
230
231impl Filters {
232    fn from_panel(panel: &Panel) -> Filters {
233        let (p1, p2) = (
234            panel.slider("depart from").get_percent(),
235            panel.slider("depart until").get_percent(),
236        );
237        let departure_time = (end_of_day().percent_of(p1), end_of_day().percent_of(p2));
238        let modes = TripMode::all()
239            .into_iter()
240            .filter(|m| panel.is_checked(m.ongoing_verb()))
241            .collect::<BTreeSet<_>>();
242        Filters {
243            modes,
244            include_borders: panel.is_checked("include borders"),
245            departure_time,
246        }
247    }
248
249    fn to_panel(&self, ctx: &mut EventCtx, app: &App) -> Widget {
250        Widget::col(vec![
251            "Filter trips".text_widget(ctx),
252            Toggle::switch(ctx, "include borders", None, self.include_borders),
253            Widget::row(vec![
254                "Departing from:".text_widget(ctx).margin_right(20),
255                Slider::area(
256                    ctx,
257                    0.15 * ctx.canvas.window_width,
258                    self.departure_time.0.to_percent(end_of_day()),
259                    "depart from",
260                ),
261            ]),
262            Widget::row(vec![
263                "Departing until:".text_widget(ctx).margin_right(20),
264                Slider::area(
265                    ctx,
266                    0.15 * ctx.canvas.window_width,
267                    self.departure_time.1.to_percent(end_of_day()),
268                    "depart until",
269                ),
270            ]),
271            checkbox_per_mode(ctx, app, &self.modes),
272            // TODO Filter by trip purpose
273        ])
274        .section(ctx)
275    }
276}
277
278fn help() -> Vec<&'static str> {
279    vec![
280        "This tool is still experimental.",
281        "Until better travel demand models are available, we can't predict where most detours will occur,",
282        "because we don't know where trips begin and end.",
283        "",
284        "And note this tool doesn't predict traffic dissipation as people decide to not drive.",
285    ]
286}
287
288struct ChangedRoutes {
289    panel: Panel,
290    // TODO Not sure what to precompute. Smallest memory would be the PathRequest.
291    paths: Vec<(PathV2, PathV2)>,
292    current: usize,
293    draw_paths: Drawable,
294}
295
296impl ChangedRoutes {
297    fn new_state(
298        ctx: &mut EventCtx,
299        app: &App,
300        paths: Vec<(PathV2, PathV2)>,
301    ) -> Box<dyn State<App>> {
302        if paths.is_empty() {
303            return PopupMsg::new_state(
304                ctx,
305                "No changes",
306                vec!["No routes changed near this road"],
307            );
308        }
309
310        let mut state = ChangedRoutes {
311            panel: Panel::new_builder(Widget::col(vec![
312                Widget::row(vec![
313                    Line("Routes that changed near a road")
314                        .small_heading()
315                        .into_widget(ctx),
316                    ctx.style().btn_close_widget(ctx),
317                ]),
318                Widget::row(vec![
319                    ctx.style()
320                        .btn_prev()
321                        .hotkey(Key::LeftArrow)
322                        .build_widget(ctx, "previous"),
323                    "route X/Y"
324                        .text_widget(ctx)
325                        .named("pointer")
326                        .centered_vert(),
327                    ctx.style()
328                        .btn_next()
329                        .hotkey(Key::RightArrow)
330                        .build_widget(ctx, "next"),
331                ])
332                .evenly_spaced(),
333                Line("Route before changes")
334                    .fg(*colors::PLAN_ROUTE_BEFORE)
335                    .into_widget(ctx),
336                Line("Route after changes")
337                    .fg(*colors::PLAN_ROUTE_AFTER)
338                    .into_widget(ctx),
339            ]))
340            .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
341            .build(ctx),
342            paths,
343            current: 0,
344            draw_paths: Drawable::empty(ctx),
345        };
346        state.recalculate(ctx, app);
347        Box::new(state)
348    }
349
350    fn recalculate(&mut self, ctx: &mut EventCtx, app: &App) {
351        self.panel.replace(
352            ctx,
353            "pointer",
354            format!("route {}/{}", self.current + 1, self.paths.len()).text_widget(ctx),
355        );
356
357        let mut batch = map_gui::tools::draw_overlapping_paths(
358            app,
359            vec![
360                (
361                    self.paths[self.current].0.clone(),
362                    *colors::PLAN_ROUTE_BEFORE,
363                ),
364                (
365                    self.paths[self.current].1.clone(),
366                    *colors::PLAN_ROUTE_AFTER,
367                ),
368            ],
369        )
370        .unzoomed;
371        let req = self.paths[self.current].0.get_req();
372        batch.append(map_gui::tools::start_marker(
373            ctx,
374            req.start.pt(&app.per_map.map),
375            2.0,
376        ));
377        batch.append(map_gui::tools::goal_marker(
378            ctx,
379            req.end.pt(&app.per_map.map),
380            2.0,
381        ));
382        self.draw_paths = ctx.upload(batch);
383    }
384}
385
386impl State<App> for ChangedRoutes {
387    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
388        ctx.canvas_movement();
389
390        if let Outcome::Clicked(x) = self.panel.event(ctx) {
391            match x.as_ref() {
392                "close" => {
393                    return Transition::Pop;
394                }
395                "previous" => {
396                    if self.current != 0 {
397                        self.current -= 1;
398                    }
399                    self.recalculate(ctx, app);
400                }
401                "next" => {
402                    if self.current != self.paths.len() - 1 {
403                        self.current += 1;
404                    }
405                    self.recalculate(ctx, app);
406                }
407                _ => unreachable!(),
408            }
409        }
410
411        Transition::Keep
412    }
413
414    fn draw(&self, g: &mut GfxCtx, app: &App) {
415        self.panel.draw(g);
416        g.redraw(&self.draw_paths);
417        app.per_map.draw_all_filters.draw(g);
418        app.per_map.draw_poi_icons.draw(g);
419    }
420}
421
422fn export_csv(app: &App) -> Result<String> {
423    let mut out = Vec::new();
424    {
425        let mut writer = csv::Writer::from_writer(&mut out);
426        for r in app.per_map.map.all_roads() {
427            writer.serialize(ExportRow::new(r, app))?;
428        }
429        writer.flush()?;
430    }
431    let out = String::from_utf8(out)?;
432    Ok(out)
433}
434
435#[derive(Serialize)]
436struct ExportRow {
437    road_name: String,
438    osm_way_id: i64,
439    osm_intersection1: i64,
440    osm_intersection2: i64,
441    total_count_before: usize,
442    total_count_after: usize,
443}
444
445impl ExportRow {
446    fn new(r: &Road, app: &App) -> Self {
447        Self {
448            road_name: r.get_name(None),
449            osm_way_id: r.orig_id.osm_way_id.0,
450            osm_intersection1: r.orig_id.i1.0,
451            osm_intersection2: r.orig_id.i2.0,
452            total_count_before: app
453                .per_map
454                .impact
455                .compare_counts
456                .counts_a
457                .per_road
458                .get(r.id),
459            total_count_after: app
460                .per_map
461                .impact
462                .compare_counts
463                .counts_b
464                .per_road
465                .get(r.id),
466        }
467    }
468}
469
470fn export_geojson(app: &App) -> Result<String> {
471    let mut string_buffer: Vec<u8> = vec![];
472    {
473        let mut writer = geojson::FeatureWriter::from_writer(&mut string_buffer);
474
475        #[derive(Serialize)]
476        struct RoadGeoJson {
477            #[serde(serialize_with = "geojson::ser::serialize_geometry")]
478            geometry: geo::LineString,
479            #[serde(flatten)]
480            export_row: ExportRow,
481        }
482
483        for r in app.per_map.map.all_roads() {
484            let bounds = app.per_map.map.get_gps_bounds();
485            let mut geometry = geo::LineString::from(&r.center_pts);
486            geometry.map_coords_in_place(|c| {
487                let lonlat = bounds.convert_back_xy(c.x, c.y);
488                return geo::coord! { x: lonlat.x(), y: lonlat.y() };
489            });
490
491            let sr = RoadGeoJson {
492                export_row: ExportRow::new(r, app),
493                geometry,
494            };
495
496            writer.serialize(&sr)?;
497        }
498    }
499    let out = String::from_utf8(string_buffer)?;
500    Ok(out)
501}