ltn/pages/
per_resident_impact.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use abstutil::Timer;
4use geom::{Duration, UnitFmt};
5use map_gui::tools::DrawSimpleRoadLabels;
6use map_model::{BuildingID, PathConstraints, PathRequest, Pathfinder};
7use synthpop::TripEndpoint;
8use widgetry::mapspace::{ObjectID, World, WorldOutcome};
9use widgetry::tools::{ColorLegend, ColorScale};
10use widgetry::{
11    Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, Outcome, Panel, State, Text, TextExt,
12    Widget,
13};
14
15use crate::components::{AppwidePanel, BottomPanel, Mode};
16use crate::render::colors;
17use crate::save::PreserveState;
18use crate::{pages, render, App, Neighbourhood, NeighbourhoodID, Transition};
19
20pub struct PerResidentImpact {
21    appwide_panel: AppwidePanel,
22    bottom_panel: Panel,
23    world: World<Obj>,
24    labels: DrawSimpleRoadLabels,
25    neighbourhood: Neighbourhood,
26    fade_irrelevant: Drawable,
27    cell_outline: Drawable,
28    buildings_inside: BTreeSet<BuildingID>,
29    // Expensive to calculate
30    preserve_state: PreserveState,
31
32    pathfinder_before: Pathfinder,
33    pathfinder_after: Pathfinder,
34
35    current_target: Option<BuildingID>,
36    // Time from a building to current_target, (before, after)
37    times_from_building: BTreeMap<BuildingID, (Duration, Duration)>,
38    compare_routes: Option<(BuildingID, Drawable)>,
39}
40
41impl PerResidentImpact {
42    pub fn new_state(
43        ctx: &mut EventCtx,
44        app: &App,
45        id: NeighbourhoodID,
46        current_target: Option<BuildingID>,
47    ) -> Box<dyn State<App>> {
48        let map = &app.per_map.map;
49        let appwide_panel = AppwidePanel::new(ctx, app, Mode::PerResidentImpact);
50
51        let neighbourhood = Neighbourhood::new(app, id);
52        let fade_irrelevant = neighbourhood.fade_irrelevant(ctx, app);
53        let mut label_roads = neighbourhood.perimeter_roads.clone();
54        label_roads.extend(neighbourhood.interior_roads.clone());
55        let labels = DrawSimpleRoadLabels::new(
56            ctx,
57            app,
58            colors::LOCAL_ROAD_LABEL,
59            Box::new(move |r| label_roads.contains(&r.id)),
60        );
61
62        let mut buildings_inside = BTreeSet::new();
63        for b in map.all_buildings() {
64            if neighbourhood
65                .boundary_polygon
66                .contains_pt(b.polygon.center())
67            {
68                buildings_inside.insert(b.id);
69            }
70        }
71
72        // It's a subtle effect, but maybe useful to see
73        let render_cells = render::RenderCells::new(map, &neighbourhood);
74        let cell_outline = render_cells.draw_island_outlines();
75
76        // Depending on the number of buildings_inside, Dijkstra may be faster, but this seems fast
77        // enough so far
78        let (pathfinder_before, pathfinder_after) =
79            ctx.loading_screen("prepare per-resident impact", |_, timer| {
80                // TODO Can we share with RoutePlanner maybe?
81                timer.start("prepare pathfinding before changes");
82                let pathfinder_before = Pathfinder::new_ch(
83                    map,
84                    app.per_map.routing_params_before_changes.clone(),
85                    vec![PathConstraints::Car],
86                    timer,
87                );
88                timer.stop("prepare pathfinding before changes");
89
90                timer.start("prepare pathfinding after changes");
91                let params = map.routing_params_respecting_modal_filters();
92                let pathfinder_after =
93                    Pathfinder::new_ch(map, params, vec![PathConstraints::Car], timer);
94                timer.stop("prepare pathfinding after changes");
95
96                (pathfinder_before, pathfinder_after)
97            });
98
99        let mut state = Self {
100            appwide_panel,
101            bottom_panel: Panel::empty(ctx),
102            world: World::new(),
103            labels,
104            neighbourhood,
105            fade_irrelevant,
106            cell_outline: cell_outline.upload(ctx),
107            buildings_inside,
108            preserve_state: PreserveState::PerResidentImpact(
109                app.partitioning().neighbourhood_to_blocks(id),
110                current_target,
111            ),
112
113            pathfinder_before,
114            pathfinder_after,
115
116            current_target,
117            times_from_building: BTreeMap::new(),
118            compare_routes: None,
119        };
120        state.update(ctx, app);
121        Box::new(state)
122    }
123
124    fn update(&mut self, ctx: &mut EventCtx, app: &App) {
125        ctx.loading_screen("calculate per-building impacts", |_, timer| {
126            self.recalculate_times(app, timer);
127        });
128        // We should only ever get slower. If there's no change anywhere, use 1s to avoid division
129        // by zero
130        let max_change = self
131            .times_from_building
132            .values()
133            .map(|(before, after)| *after - *before)
134            .max()
135            .unwrap_or(Duration::ZERO)
136            .max(Duration::seconds(1.0));
137
138        let scale = ColorScale(vec![Color::CLEAR, Color::RED]);
139        let mut row = vec![
140            ctx.style()
141                .btn_outline
142                .text("Back")
143                .build_def(ctx)
144                .centered_vert(),
145            Widget::vertical_separator(ctx),
146        ];
147        if self.current_target.is_none() {
148            row.push(
149                "Click a building outside the neighbourhood to see driving times there"
150                    .text_widget(ctx)
151                    .centered_vert(),
152            );
153        } else {
154            row.extend(vec![
155                "The time to drive from the neighbourhood to this destination changes:"
156                    .text_widget(ctx)
157                    .centered_vert(),
158                ColorLegend::gradient(
159                    ctx,
160                    &scale,
161                    vec!["0", &max_change.to_string(&UnitFmt::metric())],
162                )
163                .centered_vert(),
164                ColorLegend::row(ctx, *colors::PLAN_ROUTE_BEFORE, "before changes"),
165                ColorLegend::row(ctx, *colors::PLAN_ROUTE_AFTER, "after changes"),
166            ]);
167        }
168        self.bottom_panel = BottomPanel::new(ctx, &self.appwide_panel, Widget::row(row));
169
170        let map = &app.per_map.map;
171        self.world = World::new();
172
173        for b in map.all_buildings() {
174            if let Some((before, after)) = self.times_from_building.get(&b.id) {
175                let color = scale.eval((*after - *before) / max_change);
176                let mut txt = Text::from(if before == after {
177                    format!("No change -- {before}")
178                } else {
179                    format!(
180                        "{} slower -- {before} before this proposal, {after} after",
181                        *after - *before
182                    )
183                });
184                if before != after {
185                    txt.add_line(Line("Click").fg(ctx.style().text_hotkey_color));
186                    txt.append(Line(" to investigate"));
187                }
188
189                self.world
190                    .add(Obj::Building(b.id))
191                    .hitbox(b.polygon.clone())
192                    .draw_color(color)
193                    .hover_color(colors::HOVER)
194                    .tooltip(txt)
195                    .clickable()
196                    .build(ctx);
197            } else {
198                self.world
199                    .add(Obj::Building(b.id))
200                    .hitbox(b.polygon.clone())
201                    .drawn_in_master_batch()
202                    .hover_color(colors::HOVER)
203                    .clickable()
204                    .build(ctx);
205            }
206        }
207        self.world.initialize_hover(ctx);
208
209        if let Some(b) = self.current_target {
210            self.world.draw_master_batch(
211                ctx,
212                GeomBatch::load_svg(ctx, "system/assets/tools/star.svg")
213                    .centered_on(map.get_b(b).polygon.center()),
214            );
215        }
216    }
217
218    fn recalculate_times(&mut self, app: &App, timer: &mut Timer) {
219        self.times_from_building.clear();
220        self.compare_routes = None;
221        let target = if let Some(b) = self.current_target {
222            b
223        } else {
224            return;
225        };
226
227        let map = &app.per_map.map;
228
229        let requests: Vec<(BuildingID, PathRequest)> = self
230            .buildings_inside
231            .iter()
232            .filter_map(|b| {
233                PathRequest::between_buildings(map, *b, target, PathConstraints::Car)
234                    .map(|req| (*b, req))
235            })
236            .collect();
237
238        // For each request, calculate the time for each
239        for (b, before, after) in timer.parallelize("calculate routes", requests, |(b, req)| {
240            (
241                b,
242                self.pathfinder_before
243                    .pathfind_v2(req.clone(), map)
244                    .map(|p| p.get_cost()),
245                self.pathfinder_after
246                    .pathfind_v2(req.clone(), map)
247                    .map(|p| p.get_cost()),
248            )
249        }) {
250            if let (Some(before), Some(after)) = (before, after) {
251                self.times_from_building.insert(b, (before, after));
252            }
253        }
254    }
255
256    fn compare_routes(&self, ctx: &EventCtx, app: &App, from: BuildingID) -> Option<Drawable> {
257        if !self.buildings_inside.contains(&from) {
258            return None;
259        }
260
261        let map = &app.per_map.map;
262        let req = PathRequest::between_buildings(
263            map,
264            from,
265            self.current_target.unwrap(),
266            PathConstraints::Car,
267        )?;
268
269        Some(
270            map_gui::tools::draw_overlapping_paths(
271                app,
272                vec![
273                    (
274                        self.pathfinder_before.pathfind_v2(req.clone(), map)?,
275                        *colors::PLAN_ROUTE_BEFORE,
276                    ),
277                    (
278                        self.pathfinder_after.pathfind_v2(req.clone(), map)?,
279                        *colors::PLAN_ROUTE_AFTER,
280                    ),
281                ],
282            )
283            .unzoomed
284            .upload(ctx),
285        )
286    }
287}
288
289impl State<App> for PerResidentImpact {
290    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
291        if let PreserveState::PerResidentImpact(_, ref mut x) = self.preserve_state {
292            *x = self.current_target;
293        } else {
294            unreachable!();
295        }
296        if let Some(t) = self
297            .appwide_panel
298            .event(ctx, app, &self.preserve_state, help)
299        {
300            return t;
301        }
302        if let Some(t) = app.session.layers.event(
303            ctx,
304            &app.cs,
305            Mode::PerResidentImpact,
306            Some(&self.bottom_panel),
307        ) {
308            return t;
309        }
310        if let Outcome::Clicked(x) = self.bottom_panel.event(ctx) {
311            if x == "Back" {
312                return Transition::Replace(pages::DesignLTN::new_state(
313                    ctx,
314                    app,
315                    self.neighbourhood.id,
316                ));
317            } else {
318                unreachable!()
319            }
320        }
321
322        match self.world.event(ctx) {
323            WorldOutcome::ClickedObject(Obj::Building(b)) => {
324                if self.buildings_inside.contains(&b) {
325                    if let Some(target) = self.current_target {
326                        pages::RoutePlanner::add_new_trip(
327                            app,
328                            TripEndpoint::Building(b),
329                            TripEndpoint::Building(target),
330                        );
331                        return Transition::Replace(pages::RoutePlanner::new_state(ctx, app));
332                    }
333                } else {
334                    self.current_target = Some(b);
335                    self.update(ctx, app);
336                }
337            }
338            _ => {}
339        }
340
341        let key = self.world.get_hovering().map(|x| match x {
342            Obj::Building(b) => b,
343        });
344        if self.current_target.is_some() && key != self.compare_routes.as_ref().map(|(b, _)| *b) {
345            if let Some(b) = key {
346                self.compare_routes = Some((
347                    b,
348                    self.compare_routes(ctx, app, b)
349                        .unwrap_or_else(|| Drawable::empty(ctx)),
350                ));
351            } else {
352                self.compare_routes = None;
353            }
354        }
355
356        Transition::Keep
357    }
358
359    fn draw(&self, g: &mut GfxCtx, app: &App) {
360        g.redraw(&self.fade_irrelevant);
361        g.redraw(&self.cell_outline);
362        self.appwide_panel.draw(g);
363        self.bottom_panel.draw(g);
364        self.labels.draw(g);
365        app.per_map.draw_major_road_labels.draw(g);
366        app.session.layers.draw(g, app);
367        app.per_map.draw_all_filters.draw(g);
368        self.world.draw(g);
369        if let Some((_, ref draw)) = self.compare_routes {
370            g.redraw(draw);
371        }
372        app.per_map.draw_poi_icons.draw(g);
373    }
374
375    fn recreate(&mut self, ctx: &mut EventCtx, app: &mut App) -> Box<dyn State<App>> {
376        Self::new_state(ctx, app, self.neighbourhood.id, self.current_target)
377    }
378}
379
380fn help() -> Vec<&'static str> {
381    vec!["Use this tool to determine if some residents may have more trouble than others driving somewhere outside the neighbourhood."]
382}
383
384#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
385enum Obj {
386    Building(BuildingID),
387}
388
389impl ObjectID for Obj {}