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 preserve_state: PreserveState,
31
32 pathfinder_before: Pathfinder,
33 pathfinder_after: Pathfinder,
34
35 current_target: Option<BuildingID>,
36 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 let render_cells = render::RenderCells::new(map, &neighbourhood);
74 let cell_outline = render_cells.draw_island_outlines();
75
76 let (pathfinder_before, pathfinder_after) =
79 ctx.loading_screen("prepare per-resident impact", |_, timer| {
80 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 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 (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 {}