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
24pub 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 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 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 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 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 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 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 let filters = Filters::from_panel(&self.left_panel);
182 if filters == app.per_map.impact.filters {
183 return Transition::Keep;
184 }
185
186 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 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 ])
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 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}