334 lines
12 KiB
Rust
334 lines
12 KiB
Rust
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<String>,
|
|
}
|
|
#[derive(Clone, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Manifest {
|
|
pub format_version: u32,
|
|
pub city: Metadata,
|
|
pub tiles: Vec<TileRef>,
|
|
pub navigation: Option<NavigationRef>,
|
|
}
|
|
#[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<Vec<usize>>,
|
|
}
|
|
#[derive(Clone)]
|
|
pub struct City {
|
|
pub id: String,
|
|
pub manifest: Manifest,
|
|
pub graph: Graph,
|
|
pub revision: String,
|
|
homes: BTreeMap<String, [i32; 2]>,
|
|
workplaces: BTreeMap<String, [i32; 2]>,
|
|
}
|
|
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<Self, String> {
|
|
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<usize> = 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<Commute> {
|
|
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<Vec<[i32; 2]>> {
|
|
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<Vec<[i32; 2]>> {
|
|
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
|
|
}
|
|
}
|