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            // TODO Er, the heatmap actually looks terrible.
49            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}