lmao
This commit is contained in:
parent
03445188ea
commit
524580eb25
102 changed files with 36625 additions and 1295 deletions
179
server-rs/src/data/postcodes.rs
Normal file
179
server-rs/src/data/postcodes.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue