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 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 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 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
164fn 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}