map_gui/tools/
compare_counts.rs

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    /// Start with the relative layer if anything has changed
61    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            // TODO Maybe tab style
97            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        // Manually generate tooltips last-minute
136        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    /// If clickable_roads was enabled and a road was clicked, this returns the ID.
185    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    /// If a button owned by this was clicked, returns the new widget to replace
193    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        // TODO This is still a bit arbitrary
213        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    // TODO The scale will be different for roads and intersections
220    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    // First just understand the counts...
232    // TODO This doesn't have explicit 0's
233    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    // What's physical road width look like?
245    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    // Draw road width based on the count before
252    // TODO unwrap will crash on an empty demand model
253    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        // TODO Refactor histogram helpers
268        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        // TODO Pretty arbitrary. Ideally we'd hide roads and intersections underneath...
274        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}