This commit is contained in:
Andras Schmelczer 2026-02-15 22:39:49 +00:00
parent 03445188ea
commit 524580eb25
102 changed files with 36625 additions and 1295 deletions

View file

@ -0,0 +1,179 @@
use anyhow::Context;
use rayon::prelude::*;
use rustc_hash::FxHashMap;
use serde::Deserialize;
use std::fs;
use std::path::Path;
use tracing::{debug, info};
/// GeoJSON structures for parsing postcode boundary files
#[derive(Deserialize)]
struct FeatureCollection {
features: Vec<Feature>,
}
#[derive(Deserialize)]
struct Feature {
geometry: Geometry,
properties: Properties,
}
#[derive(Deserialize)]
#[serde(tag = "type")]
enum Geometry {
Polygon {
coordinates: Vec<Vec<[f64; 2]>>,
},
MultiPolygon {
coordinates: Vec<Vec<Vec<[f64; 2]>>>,
},
}
#[derive(Deserialize)]
struct Properties {
postcodes: String,
}
/// Postcode boundary data: polygon vertices and spatial index for fast queries.
pub struct PostcodeData {
/// Postcode strings
pub postcodes: Vec<String>,
/// All polygon parts per postcode: polygons[i] = list of outer rings
/// Single Polygon → 1 ring, MultiPolygon → N rings
pub polygons: Vec<Vec<Vec<[f32; 2]>>>,
/// Centroid (lat, lon) for lookups
pub centroids: Vec<(f32, f32)>,
/// Lookup from postcode string to index
pub postcode_to_idx: FxHashMap<String, usize>,
}
impl PostcodeData {
/// Load postcode boundaries from a directory of GeoJSON files.
/// Expects the directory to have a `units/` subdirectory containing .geojson files.
pub fn load(dir_path: &Path) -> anyhow::Result<Self> {
info!("Loading postcode boundaries from {:?}", dir_path);
let units_dir = dir_path.join("units");
if !units_dir.exists() {
anyhow::bail!(
"Expected 'units' subdirectory in postcode boundaries path: {:?}",
dir_path
);
}
let mut postcodes: Vec<String> = Vec::new();
let mut polygons: Vec<Vec<Vec<[f32; 2]>>> = Vec::new();
let mut centroids: Vec<(f32, f32)> = Vec::new();
// Read all .geojson files in the units directory
let mut entries: Vec<_> = fs::read_dir(&units_dir)
.with_context(|| format!("Failed to read directory: {:?}", units_dir))?
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry
.path()
.extension()
.map(|ext| ext == "geojson")
.unwrap_or(false)
})
.collect();
entries.sort_by_key(|entry| entry.path());
info!(files = entries.len(), "Found GeoJSON files to process");
// Parse files in parallel
let file_results: Vec<_> = entries
.into_par_iter()
.map(|entry| {
let file_path = entry.path();
let content = fs::read_to_string(&file_path)
.with_context(|| format!("Failed to read file: {:?}", file_path))?;
let collection: FeatureCollection = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse GeoJSON: {:?}", file_path))?;
let mut local_postcodes = Vec::new();
let mut local_polygons = Vec::new();
let mut local_centroids = Vec::new();
for feature in collection.features {
let postcode = feature.properties.postcodes;
// Extract all outer rings from the geometry
let rings: Vec<Vec<[f32; 2]>> = match feature.geometry {
Geometry::Polygon { coordinates } => coordinates
.first()
.map(|ring| {
vec![ring
.iter()
.map(|[lon, lat]| [*lon as f32, *lat as f32])
.collect()]
})
.unwrap_or_default(),
Geometry::MultiPolygon { coordinates } => coordinates
.iter()
.filter_map(|poly| {
poly.first().map(|ring| {
ring.iter()
.map(|[lon, lat]| [*lon as f32, *lat as f32])
.collect()
})
})
.collect(),
};
// Compute centroid across all vertices from all rings
let total_vertices: usize = rings.iter().map(|ring| ring.len()).sum();
let centroid = if total_vertices == 0 {
(0.0, 0.0)
} else {
let mut sum_lat: f32 = 0.0;
let mut sum_lon: f32 = 0.0;
for ring in &rings {
for &[lon, lat] in ring {
sum_lat += lat;
sum_lon += lon;
}
}
let count = total_vertices as f32;
(sum_lat / count, sum_lon / count)
};
local_postcodes.push(postcode);
local_polygons.push(rings);
local_centroids.push(centroid);
}
Ok::<_, anyhow::Error>((local_postcodes, local_polygons, local_centroids))
})
.collect::<Result<Vec<_>, _>>()?;
// Flatten results
for (local_postcodes, local_polygons, local_centroids) in file_results {
postcodes.extend(local_postcodes);
polygons.extend(local_polygons);
centroids.extend(local_centroids);
}
debug!(
postcodes = postcodes.len(),
"Extracted postcodes from GeoJSON"
);
// Build postcode -> index lookup
let mut postcode_to_idx: FxHashMap<String, usize> = FxHashMap::default();
for (idx, postcode) in postcodes.iter().enumerate() {
postcode_to_idx.insert(postcode.clone(), idx);
}
info!(postcodes = postcodes.len(), "Postcode boundary data ready");
Ok(PostcodeData {
postcodes,
polygons,
centroids,
postcode_to_idx,
})
}
}