use serde::Deserialize; use sha2::{Digest, Sha256}; use std::{ collections::{BTreeMap, VecDeque}, fs, io::Read, path::Path, }; #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Bounds { pub min_x: f64, pub min_y: f64, pub max_x: f64, pub max_y: f64, } #[derive(Clone, Deserialize)] pub struct Metadata { pub id: String, pub name: String, pub bounds: Bounds, } #[derive(Clone, Deserialize)] pub struct TileRef { pub id: String, pub url: String, pub bounds: Bounds, pub sha256: Option, } #[derive(Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Manifest { pub format_version: u32, pub city: Metadata, pub tiles: Vec, pub navigation: Option, } #[derive(Clone, Deserialize)] pub struct NavigationRef { pub url: String, pub sha256: String, } #[derive(Clone, Default)] pub struct Graph { pub points: Vec<[i32; 2]>, pub edges: Vec>, } #[derive(Clone)] pub struct City { pub id: String, pub manifest: Manifest, pub graph: Graph, pub revision: String, homes: BTreeMap, workplaces: BTreeMap, } pub struct Commute { pub home: String, pub workplace: String, pub points: Vec<[i32; 2]>, } pub fn safe_id(id: &str) -> bool { !id.is_empty() && id.len() <= 80 && id .bytes() .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') } fn point(v: &serde_json::Value) -> Result<[i32; 2], String> { let x = v.get(0).and_then(|v| v.as_f64()).ok_or("Invalid point")? * 100.0; let y = v.get(1).and_then(|v| v.as_f64()).ok_or("Invalid point")? * 100.0; if !x.is_finite() || !y.is_finite() || x.abs() > i32::MAX as f64 || y.abs() > i32::MAX as f64 { return Err("Point out of range".into()); } Ok([x.round() as i32, y.round() as i32]) } impl City { pub fn load(root: &Path, id: &str) -> Result { if !safe_id(id) { return Err("Invalid city ID".into()); } let folder = root.join(id); let manifest_bytes = fs::read(folder.join("manifest.json")).map_err(|e| e.to_string())?; let revision = format!("{:x}", Sha256::digest(&manifest_bytes)); let manifest: Manifest = serde_json::from_slice(&manifest_bytes).map_err(|e| e.to_string())?; if manifest.format_version != 1 { return Err("Unsupported city format".into()); } let mut graph = Graph::default(); let mut nodes = BTreeMap::<[i32; 2], usize>::new(); let mut roads = std::collections::HashSet::new(); let mut homes = BTreeMap::new(); let mut workplaces = BTreeMap::new(); let inputs = if let Some(nav) = &manifest.navigation { if nav.url != "navigation.json.gz" { return Err("Invalid navigation URL".into()); } vec![TileRef { id: "navigation".into(), url: nav.url.clone(), bounds: manifest.city.bounds.clone(), sha256: Some(nav.sha256.clone()), }] } else { manifest.tiles.clone() }; for tile in &inputs { let expected = if tile.id == "navigation" { "navigation.json.gz".to_string() } else { format!("tiles/{}.json.gz", tile.id) }; if tile.url != expected || (tile.id != "navigation" && tile .id .bytes() .any(|c| !(c.is_ascii_digit() || c == b'-' || c == b'_'))) { return Err("Invalid tile URL".into()); } let bytes = fs::read(folder.join(&tile.url)).map_err(|e| e.to_string())?; let mut text = String::new(); flate2::read::GzDecoder::new(&bytes[..]) .read_to_string(&mut text) .map_err(|e| e.to_string())?; if let Some(expected) = &tile.sha256 { if &format!("{:x}", Sha256::digest(text.as_bytes())) != expected { return Err("City tile checksum mismatch".into()); } } let value: serde_json::Value = serde_json::from_str(&text).map_err(|e| e.to_string())?; if value["formatVersion"] != 1 || value["cityId"] != manifest.city.id || value["id"] != tile.id { return Err("Tile does not belong to manifest".into()); } if let Some(buildings) = value["buildings"].as_array() { for building in buildings { let Some(id) = building["id"].as_str() else { continue; }; let mut entrance = None; if let Some(floors) = building["floors"].as_array() { for floor in floors { if floor["level"] == 0 { if let Some(doors) = floor["doors"].as_array() { for door in doors { if door["rooms"].as_array().is_some_and(|rooms| { rooms.iter().any(|r| r == "outside") }) { let a = point(&door["a"])?; let b = point(&door["b"])?; entrance = Some([ ((a[0] as i64 + b[0] as i64) / 2) as i32, ((a[1] as i64 + b[1] as i64) / 2) as i32, ]); break; } } } } } } if let Some(p) = entrance { match building["kind"].as_str().unwrap_or("") { "house" | "apartments" | "mixed" => { homes.insert(id.to_string(), p); } "office" | "retail" | "industrial" | "school" | "hospital" | "restaurant" | "warehouse" => { workplaces.insert(id.to_string(), p); } _ => {} } } } } for road in value["roads"].as_array().ok_or("Missing roads")? { let id = road["id"].as_str().ok_or("Missing road ID")?; if !roads.insert(id.to_string()) { continue; } if [ "river", "stream", "canal", "ditch", "rail", "tram", "motorway", "trunk", ] .contains(&road["kind"].as_str().unwrap_or("")) { continue; } let mut previous: Option = None; for v in road["pts"].as_array().ok_or("Missing road points")? { let p = point(v)?; let next_id = graph.points.len(); let index = *nodes.entry(p).or_insert_with(|| { graph.points.push(p); graph.edges.push(vec![]); next_id }); if let Some(prev) = previous { if prev != index { graph.edges[prev].push(index); graph.edges[index].push(prev); } } previous = Some(index); } } } if graph.points.len() < 2 { return Err("City has no walkable road graph".into()); } Ok(Self { id: id.into(), manifest, graph, revision, homes, workplaces, }) } pub fn commutes(&self, count: usize, seed: u64) -> Vec { let homes: Vec<_> = self.homes.iter().collect(); let works: Vec<_> = self.workplaces.iter().collect(); let nearest = |p: &[i32; 2]| { self.graph .points .iter() .enumerate() .min_by(|(_, a), (_, b)| { let distance = |q: &&[i32; 2]| { (q[0] as f64 - p[0] as f64).powi(2) + (q[1] as f64 - p[1] as f64).powi(2) }; distance(a).total_cmp(&distance(b)) }) .map(|(i, _)| i) .unwrap_or(0) }; let mut result = vec![]; if !homes.is_empty() && !works.is_empty() { for i in 0..count { let (home, a) = homes[(i.wrapping_mul(7919).wrapping_add(seed as usize)) % homes.len()]; let (work, b) = works[(i.wrapping_mul(31).wrapping_add(seed as usize)) % works.len()]; if let Some(points) = self.graph.between(nearest(a), nearest(b)) { if points.len() > 1 { result.push(Commute { home: home.clone(), workplace: work.clone(), points, }); } } } } if result.is_empty() { return self .graph .routes(count, seed) .into_iter() .map(|points| Commute { home: String::new(), workplace: String::new(), points, }) .collect(); } result } } impl Graph { fn between(&self, start: usize, end: usize) -> Option> { let mut parent = vec![usize::MAX; self.points.len()]; parent[start] = start; let mut queue = VecDeque::from([start]); while let Some(at) = queue.pop_front() { if at == end { let mut p = vec![self.points[at]]; let mut at = at; while at != start { at = parent[at]; p.push(self.points[at]); } p.reverse(); return Some(p); } for &next in &self.edges[at] { if parent[next] == usize::MAX { parent[next] = at; queue.push_back(next); } } } None } /// Stable breadth-first paths along actual road segments, shared by many residents. pub fn routes(&self, count: usize, seed: u64) -> Vec> { let mut routes = Vec::new(); for i in 0..count { let start = (seed as usize).wrapping_add(i.wrapping_mul(7919)) % self.points.len(); let mut parent = vec![usize::MAX; self.points.len()]; parent[start] = start; let mut queue = VecDeque::from([start]); let mut last = start; while let Some(node) = queue.pop_front() { last = node; for &next in &self.edges[node] { if parent[next] == usize::MAX { parent[next] = node; queue.push_back(next); } } } if last == start { continue; } let mut route = vec![self.points[last]]; while last != start { last = parent[last]; route.push(self.points[last]); } route.reverse(); routes.push(route); } routes } }