game/devtools/
destinations.rs
1use crate::ID;
2use abstutil::Counter;
3use map_gui::tools::{make_heatmap, HeatmapOptions};
4use map_model::{AmenityType, BuildingID};
5use synthpop::{Scenario, TripEndpoint};
6use widgetry::{
7 Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Line, Outcome, Panel, State,
8 Text, Toggle, VerticalAlignment, Widget,
9};
10
11use crate::app::{App, Transition};
12
13pub struct PopularDestinations {
14 per_bldg: Counter<BuildingID>,
15 panel: Panel,
16 draw: Drawable,
17}
18
19impl PopularDestinations {
20 pub fn new_state(ctx: &mut EventCtx, app: &App, scenario: &Scenario) -> Box<dyn State<App>> {
21 let mut per_bldg = Counter::new();
22 for p in &scenario.people {
23 for trip in &p.trips {
24 if let TripEndpoint::Building(b) = trip.destination {
25 per_bldg.inc(b);
26 }
27 }
28 }
29 PopularDestinations::make(ctx, app, per_bldg, None)
30 }
31
32 fn make(
33 ctx: &mut EventCtx,
34 app: &App,
35 per_bldg: Counter<BuildingID>,
36 opts: Option<HeatmapOptions>,
37 ) -> Box<dyn State<App>> {
38 let map = &app.primary.map;
39 let mut batch = GeomBatch::new();
40 let controls = if let Some(ref o) = opts {
41 let mut pts = Vec::new();
42 for (b, cnt) in per_bldg.borrow() {
43 let pt = map.get_b(*b).label_center;
44 for _ in 0..*cnt {
45 pts.push(pt);
46 }
47 }
48 let legend = make_heatmap(ctx, &mut batch, map.get_bounds(), pts, o);
50 Widget::col(o.to_controls(ctx, legend))
51 } else {
52 let max = per_bldg.max();
53 let gradient = colorous::REDS;
54 for (b, cnt) in per_bldg.borrow() {
55 let c = gradient.eval_rational(*cnt, max);
56 batch.push(
57 Color::rgb(c.r as usize, c.g as usize, c.b as usize),
58 map.get_b(*b).polygon.clone(),
59 );
60 }
61 Widget::nothing()
62 };
63
64 let mut by_type = Counter::new();
65 for (b, cnt) in per_bldg.borrow() {
66 let mut other = true;
67 for a in &map.get_b(*b).amenities {
68 if let Some(t) = AmenityType::categorize(&a.amenity_type) {
69 by_type.add(Some(t), *cnt);
70 other = false;
71 }
72 }
73 if other {
74 by_type.add(None, *cnt);
75 }
76 }
77 let mut breakdown = Text::from("Breakdown by type");
78 let mut list = by_type.consume().into_iter().collect::<Vec<_>>();
79 list.sort_by_key(|(_, cnt)| *cnt);
80 list.reverse();
81 let sum = per_bldg.sum() as f64;
82 for (category, cnt) in list {
83 breakdown.add_line(format!(
84 "{}: {}%",
85 category
86 .map(|x| x.to_string())
87 .unwrap_or_else(|| "other".to_string()),
88 ((cnt as f64) / sum * 100.0) as usize
89 ));
90 }
91
92 Box::new(PopularDestinations {
93 per_bldg,
94 draw: ctx.upload(batch),
95 panel: Panel::new_builder(Widget::col(vec![
96 Widget::row(vec![
97 Line("Most popular destinations")
98 .small_heading()
99 .into_widget(ctx),
100 ctx.style().btn_close_widget(ctx),
101 ]),
102 Toggle::switch(ctx, "Show heatmap", None, opts.is_some()),
103 controls,
104 breakdown.into_widget(ctx),
105 ]))
106 .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
107 .build(ctx),
108 })
109 }
110}
111
112impl State<App> for PopularDestinations {
113 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
114 ctx.canvas_movement();
115 if ctx.redo_mouseover() {
116 app.primary.current_selection = app.mouseover_unzoomed_buildings(ctx);
117 if let Some(ID::Building(_)) = app.primary.current_selection {
118 } else {
119 app.primary.current_selection = None;
120 }
121 }
122
123 match self.panel.event(ctx) {
124 Outcome::Clicked(x) => match x.as_ref() {
125 "close" => {
126 return Transition::Pop;
127 }
128 _ => unreachable!(),
129 },
130 Outcome::Changed(_) => {
131 return Transition::Replace(PopularDestinations::make(
132 ctx,
133 app,
134 self.per_bldg.clone(),
135 if self.panel.is_checked("Show heatmap") {
136 Some(HeatmapOptions::from_controls(&self.panel))
137 } else {
138 None
139 },
140 ));
141 }
142 _ => {}
143 }
144
145 Transition::Keep
146 }
147
148 fn draw(&self, g: &mut GfxCtx, app: &App) {
149 g.redraw(&self.draw);
150 self.panel.draw(g);
151
152 if let Some(ID::Building(b)) = app.primary.current_selection {
153 let mut txt = Text::new();
154 txt.add_line(format!(
155 "{} trips to here",
156 abstutil::prettyprint_usize(self.per_bldg.get(b))
157 ));
158 for a in &app.primary.map.get_b(b).amenities {
159 txt.add_line(format!(
160 " {} ({})",
161 a.names.get(app.opts.language.as_ref()),
162 a.amenity_type
163 ));
164 }
165 g.draw_mouse_tooltip(txt);
166 }
167 }
168}