popgetter/
lib.rs

1use std::time::Instant;
2
3use anyhow::{bail, Result};
4use geo::Intersects;
5use geojson::Feature;
6use serde::{Deserialize, Serialize};
7use topojson::{to_geojson, TopoJson};
8
9#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct CensusZone {
11    // England: OA11CD
12    pub id: String,
13
14    // (England-only for now)
15    // 0 cars or vars per household. See https://www.ons.gov.uk/datasets/TS045/editions/2021/versions/3 for details.
16    pub cars_0: u16,
17    pub cars_1: u16,
18    pub cars_2: u16,
19    // 3 or more cars or vans per household
20    pub cars_3: u16,
21}
22
23impl CensusZone {
24    // Assumes "3 or more" just means 3
25    pub fn total_cars(&self) -> u16 {
26        self.cars_1 + 2 * self.cars_2 + 3 * self.cars_3
27    }
28}
29
30/// Clips existing TopoJSON files to the given boundary. All polygons are in WGS84.
31pub fn clip_zones(
32    topojson_path: &str,
33    boundary: geo::Polygon<f64>,
34) -> Result<Vec<(geo::Polygon<f64>, CensusZone)>> {
35    let gj = load_all_zones_as_geojson(topojson_path)?;
36
37    let start = Instant::now();
38    let mut output = Vec::new();
39    for gj_feature in gj {
40        let geom: geo::Geometry<f64> = gj_feature.clone().try_into()?;
41        if boundary.intersects(&geom) {
42            let polygon = match geom {
43                geo::Geometry::Polygon(p) => p,
44                // TODO What're these, and what should we do with them?
45                geo::Geometry::MultiPolygon(mut mp) => mp.0.remove(0),
46                _ => bail!("Unexpected geometry type for {:?}", gj_feature.properties),
47            };
48            let census_zone = CensusZone {
49                id: gj_feature
50                    .property("ID")
51                    .unwrap()
52                    .as_str()
53                    .unwrap()
54                    .to_string(),
55                cars_0: gj_feature
56                    .property("cars_0")
57                    .unwrap()
58                    .as_u64()
59                    .unwrap()
60                    .try_into()?,
61                cars_1: gj_feature
62                    .property("cars_1")
63                    .unwrap()
64                    .as_u64()
65                    .unwrap()
66                    .try_into()?,
67                cars_2: gj_feature
68                    .property("cars_2")
69                    .unwrap()
70                    .as_u64()
71                    .unwrap()
72                    .try_into()?,
73                cars_3: gj_feature
74                    .property("cars_3")
75                    .unwrap()
76                    .as_u64()
77                    .unwrap()
78                    .try_into()?,
79            };
80            output.push((polygon, census_zone));
81        }
82    }
83    println!(
84        "Filtering took {:?}. {} results",
85        start.elapsed(),
86        output.len()
87    );
88
89    Ok(output)
90}
91
92fn load_all_zones_as_geojson(path: &str) -> Result<Vec<Feature>> {
93    let mut start = Instant::now();
94    let topojson_str = fs_err::read_to_string(path)?;
95    println!("Reading file took {:?}", start.elapsed());
96
97    start = Instant::now();
98    let topo = topojson_str.parse::<TopoJson>()?;
99    println!("Parsing topojson took {:?}", start.elapsed());
100
101    start = Instant::now();
102    let fc = match topo {
103        TopoJson::Topology(t) => to_geojson(&t, "zones")?,
104        _ => bail!("Unexpected topojson contents"),
105    };
106    println!("Converting to geojson took {:?}", start.elapsed());
107
108    Ok(fc.features)
109}