game/layer/
population.rs

1use std::collections::HashSet;
2
3use abstutil::prettyprint_usize;
4use geom::{Circle, Distance, Pt2D, Time};
5use map_gui::tools::{make_heatmap, HeatmapOptions};
6use sim::PersonState;
7use widgetry::mapspace::ToggleZoomed;
8use widgetry::{Color, EventCtx, GfxCtx, Image, Line, Outcome, Panel, Toggle, Widget};
9
10use crate::app::App;
11use crate::layer::{header, Layer, LayerOutcome, PANEL_PLACEMENT};
12
13// TODO Disable drawing unzoomed agents... or alternatively, implement this by asking Sim to
14// return this kind of data instead!
15pub struct PopulationMap {
16    time: Time,
17    opts: Options,
18    draw: ToggleZoomed,
19    panel: Panel,
20}
21
22impl Layer for PopulationMap {
23    fn name(&self) -> Option<&'static str> {
24        Some("population map")
25    }
26    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Option<LayerOutcome> {
27        if app.primary.sim.time() != self.time {
28            let mut new = PopulationMap::new(ctx, app, self.opts.clone());
29            new.panel.restore(ctx, &self.panel);
30            *self = new;
31        }
32
33        match self.panel.event(ctx) {
34            Outcome::Clicked(x) => match x.as_ref() {
35                "close" => {
36                    return Some(LayerOutcome::Close);
37                }
38                _ => unreachable!(),
39            },
40            _ => {
41                let new_opts = self.options();
42                if self.opts != new_opts {
43                    *self = PopulationMap::new(ctx, app, new_opts);
44                }
45            }
46        }
47        None
48    }
49    fn draw(&self, g: &mut GfxCtx, _: &App) {
50        self.panel.draw(g);
51        self.draw.draw(g);
52    }
53    fn draw_minimap(&self, g: &mut GfxCtx) {
54        g.redraw(&self.draw.unzoomed);
55    }
56}
57
58impl PopulationMap {
59    pub fn new(ctx: &mut EventCtx, app: &App, opts: Options) -> PopulationMap {
60        let mut pts = Vec::new();
61        // Faster to grab all agent positions than individually map trips to agent positions.
62        for a in app
63            .primary
64            .sim
65            .get_unzoomed_agents(&app.primary.map)
66            .into_iter()
67            .chain(
68                app.primary
69                    .sim
70                    .get_unzoomed_transit_riders(&app.primary.map),
71            )
72        {
73            if a.person.is_some() {
74                pts.push(a.pos);
75            }
76        }
77
78        // Many people are probably in the same building. If we're building a heatmap, we
79        // absolutely care about these repeats! If we're just drawing the simple dot map, avoid
80        // drawing repeat circles.
81        let mut seen_bldgs = HashSet::new();
82        let mut repeat_pts = Vec::new();
83        for person in app.primary.sim.get_all_people() {
84            match person.state {
85                // Already covered above
86                PersonState::Trip(_) => {}
87                PersonState::Inside(b) => {
88                    let pt = app.primary.map.get_b(b).polygon.center();
89                    if seen_bldgs.contains(&b) {
90                        repeat_pts.push(pt);
91                    } else {
92                        seen_bldgs.insert(b);
93                        pts.push(pt);
94                    }
95                }
96                PersonState::OffMap => {}
97            }
98        }
99
100        let mut draw = ToggleZoomed::builder();
101        let legend = if let Some(ref o) = opts.heatmap {
102            pts.extend(repeat_pts);
103            Some(make_heatmap(
104                ctx,
105                &mut draw.unzoomed,
106                app.primary.map.get_bounds(),
107                pts,
108                o,
109            ))
110        } else {
111            // It's quite silly to produce triangles for the same circle over and over again. ;)
112            let circle = Circle::new(Pt2D::new(0.0, 0.0), Distance::meters(10.0)).to_polygon();
113            for pt in pts {
114                draw.unzoomed
115                    .push(Color::RED.alpha(0.8), circle.translate(pt.x(), pt.y()));
116            }
117            None
118        };
119        let controls = make_controls(ctx, app, &opts, legend);
120        PopulationMap {
121            time: app.primary.sim.time(),
122            opts,
123            draw: draw.build(ctx),
124            panel: controls,
125        }
126    }
127
128    fn options(&self) -> Options {
129        let heatmap = if self.panel.is_checked("Show heatmap") {
130            Some(HeatmapOptions::from_controls(&self.panel))
131        } else {
132            None
133        };
134        Options { heatmap }
135    }
136}
137
138#[derive(Clone, PartialEq)]
139pub struct Options {
140    // If None, just a dot map
141    pub heatmap: Option<HeatmapOptions>,
142}
143
144fn make_controls(ctx: &mut EventCtx, app: &App, opts: &Options, legend: Option<Widget>) -> Panel {
145    let (total_ppl, ppl_in_bldg, ppl_off_map) = app.primary.sim.num_ppl();
146
147    let mut col = vec![
148        header(
149            ctx,
150            &format!("Population: {}", prettyprint_usize(total_ppl)),
151        ),
152        Widget::row(vec![
153            Widget::row(vec![
154                Image::from_path("system/assets/tools/home.svg").into_widget(ctx),
155                Line(prettyprint_usize(ppl_in_bldg))
156                    .small()
157                    .into_widget(ctx),
158            ]),
159            Line(format!("Off-map: {}", prettyprint_usize(ppl_off_map)))
160                .small()
161                .into_widget(ctx),
162        ])
163        .centered(),
164    ];
165
166    col.push(Toggle::switch(
167        ctx,
168        "Show heatmap",
169        None,
170        opts.heatmap.is_some(),
171    ));
172    if let Some(ref o) = opts.heatmap {
173        col.extend(o.to_controls(ctx, legend.unwrap()));
174    }
175
176    Panel::new_builder(Widget::col(col))
177        .aligned_pair(PANEL_PLACEMENT)
178        .build(ctx)
179}