ltn/pages/
census.rs

1use geom::Distance;
2use widgetry::mapspace::{ObjectID, World, WorldOutcome};
3use widgetry::tools::{open_browser, ColorLegend, PopupMsg};
4use widgetry::{
5    Color, EventCtx, GeomBatch, GfxCtx, Line, Outcome, Panel, State, Text, TextExt, Widget,
6};
7
8use crate::components::{AppwidePanel, BottomPanel, Mode};
9use crate::render::colors;
10use crate::{App, Transition};
11
12pub struct Census {
13    appwide_panel: AppwidePanel,
14    bottom_panel: Panel,
15    world: World<ZoneID>,
16}
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19struct ZoneID(usize);
20impl ObjectID for ZoneID {}
21
22impl Census {
23    pub fn new_state(ctx: &mut EventCtx, app: &mut App) -> Box<dyn State<App>> {
24        // What's the max number of cars in an OA?
25        // (Clamp to >= 1 to avoid division by zero)
26        let max_cars = app
27            .per_map
28            .map
29            .all_census_zones()
30            .iter()
31            .map(|(_, z)| z.total_cars())
32            .max()
33            .unwrap_or(0)
34            .max(1);
35        let buckets = bucketize(max_cars);
36
37        let appwide_panel = AppwidePanel::new(ctx, app, Mode::Census);
38        let legend = make_legend(ctx, buckets);
39        let bottom_panel = BottomPanel::new(
40            ctx,
41            &appwide_panel,
42            Widget::row(vec![
43                ctx.style()
44                    .btn_outline
45                    .text("About")
46                    .build_def(ctx)
47                    .centered_vert(),
48                "Total vehicles owned:".text_widget(ctx).centered_vert(),
49                legend,
50            ]),
51        );
52
53        // Just force the layers panel to align above the bottom panel
54        app.session
55            .layers
56            .event(ctx, &app.cs, Mode::Census, Some(&bottom_panel));
57
58        let mut world = World::new();
59
60        for (idx, (polygon, zone)) in app.per_map.map.all_census_zones().into_iter().enumerate() {
61            let n = zone.total_cars();
62            let color = if n < buckets[1] {
63                colors::SPEED_LIMITS[0]
64            } else if n < buckets[2] {
65                colors::SPEED_LIMITS[1]
66            } else if n < buckets[3] {
67                colors::SPEED_LIMITS[2]
68            } else {
69                colors::SPEED_LIMITS[3]
70            };
71
72            let mut draw_normal = GeomBatch::new();
73            draw_normal.push(color.alpha(0.5), polygon.clone());
74            draw_normal.push(Color::RED, polygon.to_outline(Distance::meters(5.0)));
75
76            let mut draw_hover = GeomBatch::new();
77            draw_hover.push(color.alpha(0.5), polygon.clone());
78            draw_hover.push(Color::RED, polygon.to_outline(Distance::meters(10.0)));
79
80            world
81                .add(ZoneID(idx))
82                .hitbox(polygon.clone())
83                .draw(draw_normal)
84                .draw_hovered(draw_hover)
85                .clickable()
86                .tooltip(Text::from_multiline(vec![
87                    Line(format!(
88                        "Output Area {} has {} vehicles total",
89                        zone.id,
90                        zone.total_cars()
91                    )),
92                    Line(""),
93                    Line(format!("Households with 0 vehicles: {}", zone.cars_0)),
94                    Line(format!("Households with 1 vehicles: {}", zone.cars_1)),
95                    Line(format!("Households with 2 vehicles: {}", zone.cars_2)),
96                    Line(format!(
97                        "Households with 3 or more vehicles: {}",
98                        zone.cars_3
99                    )),
100                ]))
101                .build(ctx);
102        }
103
104        Box::new(Self {
105            appwide_panel,
106            bottom_panel,
107            world,
108        })
109    }
110}
111
112impl State<App> for Census {
113    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
114        if let Some(t) =
115            self.appwide_panel
116                .event(ctx, app, &crate::save::PreserveState::Census, help)
117        {
118            self.world.hack_unset_hovering();
119            return t;
120        }
121        if let Some(t) =
122            app.session
123                .layers
124                .event(ctx, &app.cs, Mode::Census, Some(&self.bottom_panel))
125        {
126            return t;
127        }
128        if let Outcome::Clicked(x) = self.bottom_panel.event(ctx) {
129            if x == "About" {
130                // TODO Very England-specific! Actually, plumb through metadata from popgetter
131                // about the layers
132                return Transition::Push(PopupMsg::new_state(ctx, "About", vec!["This shows car or van availability per household, thanks to UK census 2021 data from ONS.", "The ONS data counts households with 0, 1, 2, and >= 3 cars or vans.", "This layer summarizes this by counting the total vehicles available through the entire Output Area.", "", "WARNING: This layer is experimental; there may be data quality problems!"]));
133            } else {
134                unreachable!()
135            }
136        }
137
138        if let WorldOutcome::ClickedObject(ZoneID(idx)) = self.world.event(ctx) {
139            open_browser(format!("https://www.ons.gov.uk/census/maps/choropleth/housing/number-of-cars-or-vans/number-of-cars-5a/no-cars-or-vans-in-household?oa={}", app.per_map.map.all_census_zones()[idx].1.id));
140        }
141
142        Transition::Keep
143    }
144
145    fn draw(&self, g: &mut GfxCtx, app: &App) {
146        self.appwide_panel.draw(g);
147        self.bottom_panel.draw(g);
148        app.per_map.draw_major_road_labels.draw(g);
149        app.session.layers.draw(g, app);
150        app.per_map.draw_all_filters.draw(g);
151        app.per_map.draw_poi_icons.draw(g);
152        self.world.draw(g);
153    }
154
155    fn recreate(&mut self, ctx: &mut EventCtx, app: &mut App) -> Box<dyn State<App>> {
156        Self::new_state(ctx, app)
157    }
158}
159
160fn help() -> Vec<&'static str> {
161    vec!["This shows census data that may be useful to decide where LTNs could be placed."]
162}
163
164// TODO Don't we have something in widgetry like this?
165fn bucketize(max_cars: u16) -> [u16; 5] {
166    let max = max_cars as f64;
167    let p25 = (0.25 * max) as u16;
168    let p50 = (0.5 * max) as u16;
169    let p75 = (0.75 * max) as u16;
170    [0, p25, p50, p75, max_cars]
171}
172
173fn make_legend(ctx: &mut EventCtx, buckets: [u16; 5]) -> Widget {
174    ColorLegend::categories(
175        ctx,
176        vec![
177            (colors::SPEED_LIMITS[0], &buckets[0].to_string()),
178            (colors::SPEED_LIMITS[1], &buckets[1].to_string()),
179            (colors::SPEED_LIMITS[2], &buckets[2].to_string()),
180            (colors::SPEED_LIMITS[3], &buckets[3].to_string()),
181        ],
182        &buckets[4].to_string(),
183    )
184}