1use abstutil::prettyprint_usize;
2use geom::{Distance, Histogram, Statistic};
3use map_model::{IntersectionID, RoadID};
4use synthpop::TrafficCounts;
5use widgetry::mapspace::{ObjectID, ToggleZoomed, ToggleZoomedBuilder, World, WorldOutcome};
6use widgetry::tools::DivergingScale;
7use widgetry::{Color, EventCtx, GeomBatch, GfxCtx, Key, Line, Text, TextExt, Widget};
8
9use crate::tools::{cmp_count, ColorNetwork};
10use crate::AppLike;
11
12pub struct CompareCounts {
13 pub layer: Layer,
14 world: World<Obj>,
15 pub counts_a: TrafficCounts,
16 heatmap_a: ToggleZoomed,
17 pub counts_b: TrafficCounts,
18 heatmap_b: ToggleZoomed,
19 relative_heatmap: ToggleZoomed,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
23enum Obj {
24 Road(RoadID),
25 Intersection(IntersectionID),
26}
27impl ObjectID for Obj {}
28
29#[derive(Clone, Copy, Debug, PartialEq)]
30pub enum Layer {
31 A,
32 B,
33 Compare,
34}
35
36impl CompareCounts {
37 pub fn new(
38 ctx: &mut EventCtx,
39 app: &dyn AppLike,
40 counts_a: TrafficCounts,
41 counts_b: TrafficCounts,
42 layer: Layer,
43 clickable_roads: bool,
44 ) -> CompareCounts {
45 let heatmap_a = calculate_heatmap(ctx, app, counts_a.clone());
46 let heatmap_b = calculate_heatmap(ctx, app, counts_b.clone());
47 let relative_heatmap = calculate_relative_heatmap(ctx, app, &counts_a, &counts_b);
48
49 CompareCounts {
50 layer,
51 world: make_world(ctx, app, clickable_roads),
52 counts_a,
53 heatmap_a,
54 counts_b,
55 heatmap_b,
56 relative_heatmap,
57 }
58 }
59
60 pub fn autoselect_layer(&mut self) {
62 self.layer = if self.counts_a.per_road == self.counts_b.per_road
63 && self.counts_a.per_intersection == self.counts_b.per_intersection
64 {
65 Layer::A
66 } else {
67 Layer::Compare
68 };
69 }
70
71 pub fn recalculate_b(&mut self, ctx: &EventCtx, app: &dyn AppLike, counts_b: TrafficCounts) {
72 self.counts_b = counts_b;
73 self.heatmap_b = calculate_heatmap(ctx, app, self.counts_b.clone());
74 self.relative_heatmap =
75 calculate_relative_heatmap(ctx, app, &self.counts_a, &self.counts_b);
76 if self.layer == Layer::A {
77 self.autoselect_layer();
78 }
79 }
80
81 pub fn empty(ctx: &EventCtx) -> CompareCounts {
82 CompareCounts {
83 layer: Layer::A,
84 world: World::new(),
85 counts_a: TrafficCounts::default(),
86 heatmap_a: ToggleZoomed::empty(ctx),
87 counts_b: TrafficCounts::default(),
88 heatmap_b: ToggleZoomed::empty(ctx),
89 relative_heatmap: ToggleZoomed::empty(ctx),
90 }
91 }
92
93 pub fn get_panel_widget(&self, ctx: &EventCtx) -> Widget {
94 Widget::col(vec![
95 "Show which traffic counts?".text_widget(ctx),
96 Widget::row(vec![
98 ctx.style()
99 .btn_solid_primary
100 .text(&self.counts_a.description)
101 .disabled(self.layer == Layer::A)
102 .hotkey(Key::Num1)
103 .build_widget(ctx, "A counts"),
104 ctx.style()
105 .btn_solid_primary
106 .text(&self.counts_b.description)
107 .disabled(self.layer == Layer::B)
108 .hotkey(Key::Num2)
109 .build_widget(ctx, "B counts"),
110 ctx.style()
111 .btn_solid_primary
112 .text("Compare")
113 .disabled(self.layer == Layer::Compare)
114 .hotkey(Key::Num3)
115 .build_def(ctx),
116 ]),
117 ctx.style().btn_outline.text("Swap A<->B").build_def(ctx),
118 ])
119 .section(ctx)
120 }
121
122 pub fn draw(&self, g: &mut GfxCtx, app: &dyn AppLike) {
123 match self.layer {
124 Layer::A => {
125 self.heatmap_a.draw(g);
126 }
127 Layer::B => {
128 self.heatmap_b.draw(g);
129 }
130 Layer::Compare => {
131 self.relative_heatmap.draw(g);
132 }
133 }
134
135 if let Some(id) = self.world.get_hovering() {
137 let count = match id {
138 Obj::Road(r) => match self.layer {
139 Layer::A => self.counts_a.per_road.get(r),
140 Layer::B => self.counts_b.per_road.get(r),
141 Layer::Compare => {
142 g.draw_mouse_tooltip(self.relative_road_tooltip(app, r));
143 return;
144 }
145 },
146 Obj::Intersection(i) => match self.layer {
147 Layer::A => self.counts_a.per_intersection.get(i),
148 Layer::B => self.counts_b.per_intersection.get(i),
149 Layer::Compare => {
150 return;
151 }
152 },
153 };
154 g.draw_mouse_tooltip(Text::from(Line(prettyprint_usize(count))));
155 }
156 }
157
158 fn relative_road_tooltip(&self, app: &dyn AppLike, r: RoadID) -> Text {
159 let a = self.counts_a.per_road.get(r);
160 let b = self.counts_b.per_road.get(r);
161 let ratio = (b as f64) / (a as f64);
162
163 let mut txt = Text::from_multiline(vec![
164 Line(app.map().get_r(r).get_name(app.opts().language.as_ref())),
165 Line(format!(
166 "{}: {}",
167 self.counts_a.description,
168 prettyprint_usize(a)
169 )),
170 Line(format!(
171 "{}: {}",
172 self.counts_b.description,
173 prettyprint_usize(b)
174 )),
175 ]);
176 cmp_count(&mut txt, a, b);
177 txt.add_line(Line(format!(
178 "{}/{}: {:.2}",
179 self.counts_b.description, self.counts_a.description, ratio
180 )));
181 txt
182 }
183
184 pub fn other_event(&mut self, ctx: &mut EventCtx) -> Option<RoadID> {
186 match self.world.event(ctx) {
187 WorldOutcome::ClickedObject(Obj::Road(r)) => Some(r),
188 _ => None,
189 }
190 }
191
192 pub fn on_click(&mut self, ctx: &EventCtx, app: &dyn AppLike, x: &str) -> Option<Widget> {
194 self.layer = match x {
195 "A counts" => Layer::A,
196 "B counts" => Layer::B,
197 "Compare" => Layer::Compare,
198 "Swap A<->B" => {
199 std::mem::swap(&mut self.counts_a, &mut self.counts_b);
200 self.relative_heatmap =
201 calculate_relative_heatmap(ctx, app, &self.counts_a, &self.counts_b);
202 self.layer
203 }
204 _ => {
205 return None;
206 }
207 };
208 Some(self.get_panel_widget(ctx))
209 }
210
211 pub fn relative_scale() -> DivergingScale {
212 DivergingScale::new(Color::GREEN, Color::grey(0.2), Color::RED).range(0.0, 2.0)
214 }
215}
216
217fn calculate_heatmap(ctx: &EventCtx, app: &dyn AppLike, counts: TrafficCounts) -> ToggleZoomed {
218 let mut colorer = ColorNetwork::no_fading(app);
219 colorer.ranked_roads(counts.per_road, &app.cs().good_to_bad_red);
221 colorer.ranked_intersections(counts.per_intersection, &app.cs().good_to_bad_red);
222 colorer.build(ctx)
223}
224
225fn calculate_relative_heatmap(
226 ctx: &EventCtx,
227 app: &dyn AppLike,
228 counts_a: &TrafficCounts,
229 counts_b: &TrafficCounts,
230) -> ToggleZoomed {
231 let mut hgram_before = Histogram::new();
234 for (_, cnt) in counts_a.per_road.borrow() {
235 hgram_before.add(*cnt);
236 }
237 let mut hgram_after = Histogram::new();
238 for (_, cnt) in counts_b.per_road.borrow() {
239 hgram_after.add(*cnt);
240 }
241 info!("Road counts before: {}", hgram_before.describe());
242 info!("Road counts after: {}", hgram_after.describe());
243
244 let mut hgram_width = Histogram::new();
246 for r in app.map().all_roads() {
247 hgram_width.add(r.get_width());
248 }
249 info!("Physical road widths: {}", hgram_width.describe());
250
251 let min_count = hgram_before.select(Statistic::Min).unwrap();
254 let max_count = hgram_before.select(Statistic::Max).unwrap();
255
256 let scale = CompareCounts::relative_scale();
257
258 let mut draw_roads = GeomBatch::new();
259 for (r, before, after) in counts_a.per_road.clone().compare(counts_b.per_road.clone()) {
260 let ratio = (after as f64) / (before as f64);
261 let color = if let Some(c) = scale.eval(ratio) {
262 c
263 } else {
264 continue;
265 };
266
267 let pct_count = if before == 0 {
269 0.0
270 } else {
271 (before - min_count) as f64 / (max_count - min_count) as f64
272 };
273 let width = Distance::meters(6.0) + pct_count * Distance::meters(15.0);
275
276 draw_roads.push(color, app.map().get_r(r).center_pts.make_polygons(width));
277 }
278 ToggleZoomedBuilder::from(draw_roads).build(ctx)
279}
280
281fn make_world(ctx: &mut EventCtx, app: &dyn AppLike, clickable_roads: bool) -> World<Obj> {
282 let mut world = World::new();
283 for r in app.map().all_roads() {
284 world
285 .add(Obj::Road(r.id))
286 .hitbox(r.get_thick_polygon())
287 .drawn_in_master_batch()
288 .invisibly_hoverable()
289 .set_clickable(clickable_roads)
290 .build(ctx);
291 }
292 for i in app.map().all_intersections() {
293 world
294 .add(Obj::Intersection(i.id))
295 .hitbox(i.polygon.clone())
296 .drawn_in_master_batch()
297 .invisibly_hoverable()
298 .build(ctx);
299 }
300 world
301}