SOURCE / PINNED RELEASE
Made of little things.
Atlas map viewer
- Release
- 1d2807e80b44…
- Author-recorded commit
- 43810ed2b368…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-4757dbabc86
Archive hash verified: 8db6876d96c460ac…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
//! Resumable A* over independently loadable cells. Node identity is always the OSM ID.
use super::*;
use std::collections::HashMap;
mod snap;
use snap::{snap, snap_cells, Endpoint, Key};
mod bidirectional;
use bidirectional::Bidirectional;
mod cache;
use cache::CellCache;
mod codec;
pub use codec::{cell, extract_cell, key, partition, Cell, CELL_ZOOM};
#[derive(Clone, Copy)]
struct Entry {
f: f64,
g: f64,
key: Key,
}
impl PartialEq for Entry {
fn eq(&self, o: &Self) -> bool {
self.key == o.key && self.f == o.f && self.g == o.g
}
}
impl Eq for Entry {}
impl PartialOrd for Entry {
fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
Some(self.cmp(o))
}
}
impl Ord for Entry {
fn cmp(&self, o: &Self) -> Ordering {
o.f.total_cmp(&self.f)
.then_with(|| o.g.total_cmp(&self.g))
.then_with(|| o.key.cmp(&self.key))
}
}
struct Record {
p: [f64; 2],
g: f64,
meters: f64,
previous: Option<Key>,
way: i64,
}
struct Search {
start: Endpoint,
end: Option<Endpoint>,
queue: BinaryHeap<Entry>,
records: HashMap<Key, Record>,
best: f64,
best_meters: f64,
finish: Option<Key>,
direct: bool,
visited: usize,
pieces: HashMap<(u128, u8), crate::contours::Piece>,
}
impl Search {
fn heuristic(&self, p: [f64; 2], speed: f64) -> f64 {
self.end
.as_ref()
.map(|e| {
e.goals()
.iter()
.map(|(_, q, t, _, _)| distance(p, *q) / speed + t)
.fold(f64::INFINITY, f64::min)
})
.unwrap_or(0.)
}
}
#[derive(Serialize)]
#[serde(tag = "status", rename_all = "kebab-case")]
pub enum Step {
NeedBlocks {
keys: Vec<String>,
visited: usize,
},
Running {
visited: usize,
},
Complete {
result: Route,
},
Reachable {
result: crate::contours::Reachability,
},
BudgetExceeded {
visited: usize,
},
}
pub struct PagedRouter {
cells: CellCache,
touched: HashSet<(u32, u32)>,
search_limit: usize,
budget_exceeded: bool,
input: Option<([f64; 2], [f64; 2])>,
search: Option<Search>,
route: Option<Bidirectional>,
#[cfg(test)]
forward_only: bool,
profile: u32,
bands: Vec<f64>,
}
impl PagedRouter {
pub fn new(budget: usize, search_limit: usize) -> Self {
Self {
cells: CellCache::new(budget),
touched: HashSet::new(),
search_limit,
budget_exceeded: false,
input: None,
search: None,
route: None,
#[cfg(test)]
forward_only: false,
profile: 0,
bands: Vec::new(),
}
}
pub fn set_profile(&mut self, profile: u32) -> Result<(), String> {
if profile > 2 {
return Err("Unknown transport mode".into());
}
if self.profile != profile {
self.cells.clear();
self.input = None;
self.search = None;
self.route = None;
self.profile = profile;
}
Ok(())
}
pub fn begin(&mut self, a: [f64; 2], b: [f64; 2]) -> Result<(), String> {
if !valid_position(a) || !valid_position(b) {
return Err("Invalid route coordinates".into());
}
self.touched.clear();
self.budget_exceeded = false;
self.input = Some((a, b));
self.search = None;
self.route = None;
self.cells.clear_empty();
self.bands.clear();
Ok(())
}
pub fn begin_isochrone(&mut self, a: [f64; 2], bands: Vec<f64>) -> Result<(), String> {
if bands.is_empty()
|| bands.len() > 4
|| bands
.iter()
.any(|v| !v.is_finite() || *v <= 0. || *v > 1800.)
|| bands.windows(2).any(|w| w[0] >= w[1])
{
return Err("Use 1–4 ascending time bands up to 30 minutes".into());
}
self.begin(a, a)?;
self.bands = bands;
Ok(())
}
fn max_speed(&self) -> f64 {
[4.8 / 3.6, 25. / 3.6, 200. / 3.6][self.profile as usize]
}
pub fn used_keys(&self) -> Vec<String> {
let mut v: Vec<_> = self.touched.iter().map(|c| key(*c)).collect();
v.sort();
v
}
pub fn memory(&self) -> usize {
self.route.as_ref().map_or(0, Bidirectional::memory)
+ self.cells.memory()
+ self
.search
.as_ref()
.map(|s| s.records.len() * 192 + s.queue.len() * 64 + s.pieces.len() * 96)
.unwrap_or(0)
}
pub fn cell_count(&self) -> usize {
self.cells.len()
}
pub fn load(&mut self, bytes: &[u8]) -> Result<(), String> {
let pinned: HashSet<_> = if self.search.is_none() && self.route.is_none() {
self.input
.map(|(a, b)| snap_cells(a).into_iter().chain(snap_cells(b)).collect())
.unwrap_or_default()
} else {
HashSet::new()
};
self.budget_exceeded |= !self.cells.load(bytes, self.profile, &pinned)?;
Ok(())
}
pub fn empty(&mut self, x: u32, y: u32) {
self.cells.mark_empty((x, y));
}
fn has(&self, c: &(u32, u32)) -> bool {
self.cells.has(c)
}
pub fn step(&mut self, limit: usize) -> Result<Step, String> {
let (a, b) = self.input.ok_or("Begin a search first")?;
if self.budget_exceeded {
return Ok(Step::BudgetExceeded {
visited: self.route.as_ref().map_or_else(
|| self.search.as_ref().map_or(0, |s| s.visited),
|s| s.visited,
),
});
}
if self.search.is_none() && self.route.is_none() {
let required: HashSet<_> = snap_cells(a).into_iter().chain(snap_cells(b)).collect();
self.touched
.extend(required.iter().filter(|c| self.cells.contains(c)));
let mut missing: Vec<_> = required
.iter()
.filter(|c| !self.has(c))
.map(|c| key(*c))
.collect();
missing.sort();
if !missing.is_empty() {
return Ok(Step::NeedBlocks {
keys: missing,
visited: 0,
});
}
let Some(start) = snap(&self.cells, a) else {
return Ok(Step::Complete {
result: Route::Unsnappable {
endpoint: "start",
limit_meters: SNAP_LIMIT_METERS,
},
});
};
let end = if self.bands.is_empty() {
let Some(e) = snap(&self.cells, b) else {
return Ok(Step::Complete {
result: Route::Unsnappable {
endpoint: "end",
limit_meters: SNAP_LIMIT_METERS,
},
});
};
Some(e)
} else {
None
};
#[cfg(test)]
let bidirectional = !self.forward_only;
#[cfg(not(test))]
let bidirectional = true;
if bidirectional && end.is_some() {
self.route = Some(Bidirectional::new(start, end.unwrap(), self.max_speed()));
return self.route.as_mut().unwrap().step(
&mut self.cells,
&mut self.touched,
limit,
self.search_limit,
);
}
let mut s = Search {
start,
end,
queue: BinaryHeap::new(),
records: HashMap::new(),
best: self.bands.last().copied().unwrap_or(f64::INFINITY),
best_meters: 0.,
finish: None,
direct: false,
visited: 0,
pieces: HashMap::new(),
};
if let Some(end) = &s.end {
if s.start.id == end.id
&& ((end.snap.t >= s.start.snap.t && s.start.directions & 1 != 0)
|| (end.snap.t <= s.start.snap.t && s.start.directions & 2 != 0)
|| (end.snap.t - s.start.snap.t).abs() < 1e-12)
{
let delta = end.snap.t - s.start.snap.t;
s.best = delta.abs() * s.start.costs[usize::from(delta < 0.)];
s.best_meters = delta.abs() * s.start.length;
s.direct = true;
}
}
for (key, p, g, meters) in s.start.connections() {
if s.end.is_none() {
s.pieces.insert(
(s.start.id, if key.node == s.start.a.id { 2 } else { 3 }),
crate::contours::Piece {
a: s.start.snap.coordinates,
b: p,
start: 0.,
duration: g,
},
);
}
if g <= s.best {
s.queue.push(Entry {
f: g + s.heuristic(p, self.max_speed()),
g,
key,
});
s.records.insert(
key,
Record {
p,
g,
meters,
previous: None,
way: key.way,
},
);
}
}
self.search = Some(s);
}
if let Some(route) = &mut self.route {
return route.step(&mut self.cells, &mut self.touched, limit, self.search_limit);
}
let speed = self.max_speed();
for _ in 0..limit.clamp(1, 4096) {
let s = self.search.as_mut().unwrap();
let Some(entry) = s.queue.pop() else {
return self.finish();
};
if entry.g > s.records[&entry.key].g {
continue;
}
if entry.f > s.best + 1e-7 {
return self.finish();
}
let record = &s.records[&entry.key];
let pos = record.p;
let meters = record.meters;
let address = cell(pos);
if !self.cells.contains(&address) {
if self.cells.is_empty(&address) {
return Err("Missing owner cell for a known routing node".into());
}
s.queue.push(entry);
return Ok(Step::NeedBlocks {
keys: vec![key(address)],
visited: s.visited,
});
}
self.touched.insert(address);
let c = self.cells.get_mut(&address).unwrap();
let i = *c
.by_id
.get(&entry.key.node)
.ok_or("Routing cell is missing a referenced node")?;
if c.graph.nodes[i].position != pos {
return Err("Conflicting routing node coordinates".into());
}
s.visited += 1;
if let Some(end) = &s.end {
for (goal, _, tail, length, next) in end.goals() {
if entry.key.node == goal
&& (tail <= 1e-12 || c.allowed(entry.key, end.snap_way(), next))
&& entry.g + tail < s.best
{
s.best = entry.g + tail;
s.best_meters = meters + length;
s.finish = Some(entry.key);
s.direct = false;
}
}
}
for arc in &c.graph.adjacency[i] {
let n = &c.graph.nodes[arc.to];
let seg = &c.graph.segments[arc.segment];
if !c.allowed(entry.key, seg.way, n.id) {
continue;
}
let g = entry.g + arc.seconds;
if s.end.is_none() && entry.g < s.best {
let direction = usize::from(seg.a as usize != i);
let id = (c.ids[arc.segment], direction as u8);
if s.pieces.get(&id).is_none_or(|p| entry.g < p.start) {
if s.pieces.len() >= self.search_limit && !s.pieces.contains_key(&id) {
return Ok(Step::BudgetExceeded { visited: s.visited });
}
s.pieces.insert(
id,
crate::contours::Piece {
a: pos,
b: n.position,
start: entry.g,
duration: arc.seconds,
},
);
}
}
let key = Key {
node: n.id,
way: seg.way,
before: entry.key.node,
};
if g <= s.best && s.records.get(&key).is_none_or(|v| g < v.g) {
if s.queue.len() >= self.search_limit * 4
|| (!s.records.contains_key(&key) && s.records.len() >= self.search_limit)
{
return Ok(Step::BudgetExceeded { visited: s.visited });
}
s.queue.push(Entry {
f: g + s.heuristic(n.position, speed),
g,
key,
});
s.records.insert(
key,
Record {
p: n.position,
g,
meters: meters + arc.meters,
previous: Some(entry.key),
way: key.way,
},
);
}
}
}
Ok(Step::Running {
visited: self.search.as_ref().unwrap().visited,
})
}
fn finish(&self) -> Result<Step, String> {
let s = self.search.as_ref().unwrap();
let Some(end) = &s.end else {
return Ok(Step::Reachable {
result: crate::contours::build(
s.start.snap.clone(),
&self.bands,
s.pieces.values().copied().collect(),
s.visited,
),
});
};
if !s.best.is_finite() {
return Ok(Step::Complete {
result: Route::NoRoute {
start: s.start.snap.clone(),
end: end.snap.clone(),
visited: s.visited,
},
});
}
let mut coordinates = vec![s.start.snap.coordinates];
let mut way_ids = vec![s.start.snap.way_id.clone()];
if !s.direct {
let mut key = s.finish.ok_or("Route reconstruction failed")?;
let mut points = Vec::new();
let mut ways = Vec::new();
loop {
let r = &s.records[&key];
points.push(r.p);
if let Some(prev) = r.previous {
ways.push(format!("way/{}", r.way));
key = prev;
} else {
break;
}
}
coordinates.extend(points.into_iter().rev());
way_ids.extend(ways.into_iter().rev());
}
coordinates.push(end.snap.coordinates);
coordinates.dedup();
way_ids.push(end.snap.way_id.clone());
way_ids.dedup();
Ok(Step::Complete {
result: Route::Found {
coordinates,
meters: s.best_meters,
seconds: s.best,
visited: s.visited,
start: s.start.snap.clone(),
end: end.snap.clone(),
way_ids,
},
})
}
}
impl Cell {
fn allowed(&self, from: Key, to: i64, next: i64) -> bool {
if from.way == 0 {
return true;
}
let Some(turns) = self.turns_by_node.get(&from.node) else {
return true;
};
let mut only = false;
let mut allowed = false;
for t in turns.iter().filter(|t| t.from == from.way) {
let matches = t.to == to && (t.kind < 2 || next == from.before);
if t.kind % 2 == 0 && matches {
return false;
}
if t.kind % 2 == 1 {
only = true;
allowed |= matches;
}
}
!only || allowed
}
}
#[cfg(test)]
mod tests {
use super::*;
fn graph(points: &[[f64; 2]], directions: u8) -> Graph {
Graph::new(
points
.iter()
.enumerate()
.map(|(i, p)| Node {
id: i as i64 + 1,
position: *p,
})
.collect(),
(0..points.len() - 1)
.map(|i| Segment {
a: i as u32,
b: i as u32 + 1,
way: 100 + i as i64,
directions,
})
.collect(),
)
.unwrap()
}
fn payloads(g: &Graph) -> HashMap<String, Vec<u8>> {
partition(g)
.unwrap()
.into_iter()
.map(|(c, ids)| (key(c), extract_cell(g, c, ids).unwrap().encode()))
.collect()
}
fn supply(router: &mut PagedRouter, keys: Vec<String>, data: &HashMap<String, Vec<u8>>) {
for key in keys {
if let Some(bytes) = data.get(&key) {
router.load(bytes).unwrap();
} else {
let p: Vec<_> = key.split('/').collect();
router.empty(p[3].parse().unwrap(), p[4].parse().unwrap());
}
}
}
fn solve(router: &mut PagedRouter, data: &HashMap<String, Vec<u8>>) -> Route {
for _ in 0..10000 {
match router.step(2).unwrap() {
Step::NeedBlocks { keys, .. } => supply(router, keys, data),
Step::Running { .. } => {}
Step::Complete { result } => return result,
Step::BudgetExceeded { .. } => panic!("budget"),
Step::Reachable { .. } => panic!("unexpected isochrone"),
}
}
panic!("search did not terminate")
}
fn same(a: Route, b: Route) {
match (a, b) {
(Route::Found { meters: a, .. }, Route::Found { meters: b, .. }) => {
assert!((a - b).abs() < 1e-6, "{a} != {b}")
}
(Route::NoRoute { .. }, Route::NoRoute { .. }) => {}
(Route::Unsnappable { endpoint: a, .. }, Route::Unsnappable { endpoint: b, .. }) => {
assert_eq!(a, b)
}
_ => panic!("status mismatch"),
}
}
// A regional street grid with roads slower than the profile's speed ceiling.
// The route is eastward; downloading most of the surrounding region is wasteful.
#[test]
fn regional_route_limits_search_fanout() {
let width = 61;
let nodes: Vec<_> = (0..width * width)
.map(|i| Node {
id: i as i64 + 1,
position: [
16. + (i % width) as f64 * 0.01,
48. + (i / width) as f64 * 0.0067,
],
})
.collect();
let mut segments = Vec::new();
for i in 0..width * width {
for j in [
if i % width + 1 < width {
Some(i + 1)
} else {
None
},
if i / width + 1 < width {
Some(i + width)
} else {
None
},
]
.into_iter()
.flatten()
{
segments.push(Segment {
a: i as u32,
b: j as u32,
way: segments.len() as i64 + 1,
directions: 3,
});
}
}
let costs = segments
.iter()
.map(|e| {
let seconds = distance(nodes[e.a as usize].position, nodes[e.b as usize].position)
/ (50. / 3.6);
[seconds, seconds]
})
.collect();
let g = Graph::new(nodes, segments)
.unwrap()
.with_costs(2, costs, vec![])
.unwrap();
let a = g.nodes[30 * width + 15].position;
let b = g.nodes[30 * width + 45].position;
let data = payloads(&g);
let mut r = PagedRouter::new(16 * 1024 * 1024, 100_000);
r.set_profile(2).unwrap();
r.begin(a, b).unwrap();
let result = loop {
match r.step(4096).unwrap() {
Step::NeedBlocks { keys, .. } => supply(&mut r, keys, &data),
Step::Running { .. } => {}
Step::Complete { result } => break result,
_ => panic!("unexpected limit"),
}
};
let Route::Found {
seconds, visited, ..
} = result
else {
panic!("route")
};
let expected = (15..45)
.map(|x| {
distance(
g.nodes[30 * width + x].position,
g.nodes[30 * width + x + 1].position,
) / (50. / 3.6)
})
.sum::<f64>();
assert!((seconds - expected).abs() < 0.01, "{seconds} != {expected}");
eprintln!(
"regional grid: {visited} visited, {} cells, {seconds:.3} seconds",
r.used_keys().len()
);
assert!(visited < 1_000, "eastward route explored {visited} states");
assert!(
r.used_keys().len() < 180,
"route downloaded too much of the surrounding grid"
);
}
#[test]
fn bidirectional_matches_forward_search_with_directed_costs_and_turns() {
// Deterministic differential coverage: partial snaps, both directions,
// no/only/U-turn rules, shared way IDs, disconnected and same-edge trips.
let mut seed = 918_273_u64;
let mut random = || {
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
(seed >> 32) as usize
};
for profile in 0..3 {
let nodes: Vec<_> = (0..64)
.map(|i| Node {
id: i + 1,
position: [16. + (i % 8) as f64 * 0.003, 48. + (i / 8) as f64 * 0.002],
})
.collect();
let mut segments = Vec::new();
for i in 0..64 {
for j in [
if i % 8 < 7 { Some(i + 1) } else { None },
if i < 56 { Some(i + 8) } else { None },
]
.into_iter()
.flatten()
{
segments.push(Segment {
a: i,
b: j,
way: (random() % 12 + 1) as i64,
directions: (random() % 3 + 1) as u8,
});
}
}
let speed = [4.8 / 3.6, 25. / 3.6, 200. / 3.6][profile as usize];
let costs = segments
.iter()
.map(|e| {
let base = distance(nodes[e.a as usize].position, nodes[e.b as usize].position)
/ speed;
[
base * (1. + (random() % 50) as f64 / 10.),
base * (1. + (random() % 50) as f64 / 10.),
]
})
.collect();
let turns = (0..60)
.map(|_| Turn {
via: (random() % 64 + 1) as i64,
from: (random() % 12 + 1) as i64,
to: (random() % 12 + 1) as i64,
kind: (random() % 4) as u8,
})
.collect();
let g = Graph::new(nodes, segments)
.unwrap()
.with_costs(profile, costs, turns)
.unwrap();
let data = payloads(&g);
let mut forward = PagedRouter::new(1_000_000, 10000);
forward.forward_only = true;
let mut bidi = PagedRouter::new(1_000_000, 10000);
forward.set_profile(profile).unwrap();
bidi.set_profile(profile).unwrap();
for _ in 0..120 {
let points: Vec<_> = (0..2)
.map(|_| {
let e = &g.segments[random() % g.segments.len()];
let t = (random() % 5) as f64 / 4.;
let a = g.nodes[e.a as usize].position;
let b = g.nodes[e.b as usize].position;
[a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1])]
})
.collect();
forward.begin(points[0], points[1]).unwrap();
bidi.begin(points[0], points[1]).unwrap();
let expected = solve(&mut forward, &data);
let actual = solve(&mut bidi, &data);
match (&expected, &actual) {
(
Route::Found { seconds: a, .. },
Route::Found {
seconds: b,
coordinates,
meters,
..
},
) => {
assert!((a - b).abs() < 1e-6, "{profile}: {points:?}, {a} != {b}");
let length: f64 =
coordinates.windows(2).map(|p| distance(p[0], p[1])).sum();
assert!(
(length - meters).abs() < 0.01,
"reconstruction distance mismatch"
);
}
(Route::NoRoute { .. }, Route::NoRoute { .. }) => {}
_ => panic!("status mismatch: {expected:?} vs {actual:?}"),
}
}
}
}
#[test]
fn resumes_across_cells_and_finds_detour_outside_endpoint_corridor() {
let g = graph(
&[
[16., 48.],
[16.02, 48.],
[16.02, 48.04],
[16.06, 48.04],
[16.06, 48.],
[16.08, 48.],
],
3,
);
let data = payloads(&g);
let mut router = PagedRouter::new(8192, 10000);
let (a, b) = ([16.001, 48.], [16.079, 48.]);
router.begin(a, b).unwrap();
let Step::NeedBlocks { keys, .. } = router.step(2).unwrap() else {
panic!()
};
let Step::NeedBlocks { keys: again, .. } = router.step(2).unwrap() else {
panic!()
};
assert_eq!(keys, again);
supply(&mut router, keys, &data);
same(g.route(a, b).unwrap(), solve(&mut router, &data));
assert!(router
.used_keys()
.iter()
.any(|k| k == &key(cell([16.02, 48.04]))));
router.begin(b, a).unwrap();
same(g.route(b, a).unwrap(), solve(&mut router, &data));
}
#[test]
fn snaps_long_crossing_edges_and_preserves_partial_oneway_costs() {
let g = graph(&[[16., 48.], [16.2, 48.]], 1);
let data = payloads(&g);
let mut router = PagedRouter::new(8192, 1000);
for (a, b) in [
([16.091, 48.], [16.099, 48.]),
([16.099, 48.], [16.091, 48.]),
([16.095, 48.], [16.095, 48.]),
([15., 49.], [16.1, 48.]),
] {
router.begin(a, b).unwrap();
same(g.route(a, b).unwrap(), solve(&mut router, &data));
}
}
#[test]
fn cache_eviction_does_not_lose_search_or_reconstruction() {
let points: Vec<_> = (0..80)
.map(|i| [16. + i as f64 * 0.005, 48. + (i % 3) as f64 * 0.001])
.collect();
let g = graph(&points, 3);
let data = payloads(&g);
let mut router = PagedRouter::new(7000, 1000);
let a = points[0];
let b = *points.last().unwrap();
router.begin(a, b).unwrap();
same(g.route(a, b).unwrap(), solve(&mut router, &data));
assert!(router.cell_count() < router.used_keys().len());
assert!(router.memory() < 7000 + 1000 * 160);
}
#[test]
fn disconnected_crossings_and_budget_are_not_missing_data() {
let g = Graph::new(
vec![
Node {
id: 1,
position: [16., 48.],
},
Node {
id: 2,
position: [16.04, 48.],
},
Node {
id: 3,
position: [16.02, 47.98],
},
Node {
id: 4,
position: [16.02, 48.02],
},
],
vec![
Segment {
a: 0,
b: 1,
way: 1,
directions: 3,
},
Segment {
a: 2,
b: 3,
way: 2,
directions: 3,
},
],
)
.unwrap();
let data = payloads(&g);
let mut router = PagedRouter::new(8192, 1000);
router.begin([16., 48.], [16.02, 48.02]).unwrap();
assert!(matches!(solve(&mut router, &data), Route::NoRoute { .. }));
let points: Vec<_> = (0..20).map(|i| [16. + i as f64 * 0.002, 48.]).collect();
let g = graph(&points, 3);
let data = payloads(&g);
let mut router = PagedRouter::new(20000, 3);
router.begin(points[0], points[19]).unwrap();
loop {
match router.step(100).unwrap() {
Step::NeedBlocks { keys, .. } => supply(&mut router, keys, &data),
Step::Running { .. } => {}
Step::BudgetExceeded { .. } => break,
_ => panic!("Budget limit must not produce no-route"),
}
}
}
#[test]
fn oversized_snap_working_set_reports_budget_instead_of_thrashing() {
let g = graph(&[[16., 48.], [16.2, 48.]], 3);
let data = payloads(&g);
let size = Cell::decode(data.values().next().unwrap())
.unwrap()
.memory();
let mut router = PagedRouter::new(size, 1000);
router.begin([16.001, 48.], [16.199, 48.]).unwrap();
let Step::NeedBlocks { keys, .. } = router.step(10).unwrap() else {
panic!()
};
supply(&mut router, keys, &data);
assert!(matches!(
router.step(10).unwrap(),
Step::BudgetExceeded { .. }
));
assert!(router.memory() <= size);
router.begin([16.091, 48.], [16.092, 48.]).unwrap();
same(
g.route([16.091, 48.], [16.092, 48.]).unwrap(),
solve(&mut router, &data),
);
}
#[test]
fn cell_codec_rejects_corruption_and_duplicate_edge_identity() {
let g = graph(&[[16., 48.], [16.01, 48.], [16.02, 48.]], 3);
let c = extract_cell(&g, cell([16., 48.]), vec![0, 1]).unwrap();
let mut bytes = c.encode();
assert_eq!(Cell::decode(&bytes).unwrap().encode(), bytes);
assert!(Cell::decode(&bytes[..bytes.len() - 1]).is_err());
let n = bytes.len();
bytes[n - 4..].copy_from_slice(&0u32.to_le_bytes());
assert!(Cell::decode(&bytes).is_err());
}
#[test]
fn osm_cell_identity_roundtrip_and_contract_validation() {
let make = || transport_graph(vec![]);
let ids = make()
.segments
.iter()
.map(|s| (s.way as u128) << 32)
.collect::<Vec<_>>();
let c = Cell::osm(cell([16., 48.]), make(), ids.clone()).unwrap();
let mut bytes = c.encode();
assert_eq!(&bytes[..4], b"RCL3");
assert_eq!(Cell::decode(&bytes).unwrap().ids, ids);
assert_eq!(Cell::decode(&bytes).unwrap().encode(), bytes);
assert!(Cell::decode(&bytes[..bytes.len() - 1]).is_err());
let n = bytes.len();
bytes[n - 16..].copy_from_slice(&ids[0].to_le_bytes());
assert!(Cell::decode(&bytes).is_err());
assert!(Cell::osm(cell([16., 48.]), make(), vec![0, 1, 2, 3]).is_err());
let mut router = PagedRouter::new(1_000_000, 1000);
let legacy = Cell::new(cell([16., 48.]), make(), vec![0, 1, 2, 3]).unwrap();
router.set_profile(2).unwrap();
router.load(&c.encode()).unwrap();
assert!(router.load(&legacy.encode()).is_err());
}
fn transport_graph(turns: Vec<Turn>) -> Graph {
let points = [[16., 48.], [16.001, 48.], [16.001, 48.001], [16.002, 48.]];
Graph::new(
points
.iter()
.enumerate()
.map(|(i, p)| Node {
id: i as i64 + 1,
position: *p,
})
.collect(),
[(0, 1, 10), (1, 3, 11), (0, 2, 20), (2, 3, 21)]
.iter()
.map(|(a, b, w)| Segment {
a: *a,
b: *b,
way: *w,
directions: 1,
})
.collect(),
)
.unwrap()
.with_costs(
2,
vec![[10., 20.], [10., 20.], [30., 60.], [30., 60.]],
turns,
)
.unwrap()
}
#[test]
fn turn_rules_change_fastest_route_and_roundtrip_costs() {
for (turns, expected) in [
(vec![], 20.),
(
vec![Turn {
via: 2,
from: 10,
to: 11,
kind: 0,
}],
60.,
),
] {
let g = transport_graph(turns);
let bytes = g.encode();
let decoded = Graph::decode(&bytes).unwrap();
assert_eq!(decoded.encode(), bytes);
let data = payloads(&decoded);
let mut r = PagedRouter::new(1_000_000, 10000);
r.set_profile(2).unwrap();
r.begin([16., 48.], [16.002, 48.]).unwrap();
let Route::Found { seconds, .. } = solve(&mut r, &data) else {
panic!("route")
};
assert_eq!(seconds, expected);
r.begin([16.002, 48.], [16., 48.]).unwrap();
assert!(matches!(solve(&mut r, &data), Route::NoRoute { .. }));
}
}
#[test]
fn partial_edge_times_and_profile_mismatch() {
let g = transport_graph(vec![]);
let data = payloads(&g);
let mut r = PagedRouter::new(1_000_000, 10000);
assert!(r.load(data.values().next().unwrap()).is_err());
r.set_profile(2).unwrap();
r.begin([16.00025, 48.], [16.00075, 48.]).unwrap();
let Route::Found { seconds, .. } = solve(&mut r, &data) else {
panic!()
};
assert!((seconds - 5.).abs() < 0.01);
}
#[test]
fn isochrone_resumes_missing_cells_and_clips_time_bands() {
let g = transport_graph(vec![]);
let data = payloads(&g);
let mut r = PagedRouter::new(1_000_000, 10000);
r.set_profile(2).unwrap();
r.begin_isochrone([16., 48.], vec![5., 15., 40.]).unwrap();
let Step::NeedBlocks { keys, .. } = r.step(1).unwrap() else {
panic!()
};
let Step::NeedBlocks { keys: again, .. } = r.step(1).unwrap() else {
panic!()
};
assert_eq!(keys, again);
supply(&mut r, keys, &data);
loop {
match r.step(1).unwrap() {
Step::NeedBlocks { keys, .. } => supply(&mut r, keys, &data),
Step::Running { .. } => {}
Step::Reachable { result } => {
assert_eq!(result.contours.features.len(), 3);
assert!(result.segments > 0);
let areas: Vec<_> = result
.contours
.features
.iter()
.map(|f| f.properties.area_km2)
.collect();
assert!(areas.windows(2).all(|p| p[0] <= p[1]));
break;
}
_ => panic!(),
}
}
assert!(r.begin_isochrone([16., 48.], vec![60., 30.]).is_err());
assert!(r.begin_isochrone([16., 48.], vec![1801.]).is_err());
}
#[test]
fn only_turns_are_a_union_and_u_turn_is_direction_specific() {
let mut g = transport_graph(vec![
Turn {
via: 2,
from: 10,
to: 11,
kind: 1,
},
Turn {
via: 2,
from: 10,
to: 20,
kind: 1,
},
]);
let c = extract_cell(&g, cell([16., 48.]), vec![0, 1, 2, 3]).unwrap();
let k = Key {
node: 2,
way: 10,
before: 1,
};
assert!(c.allowed(k, 11, 4));
assert!(c.allowed(k, 20, 3));
assert!(!c.allowed(k, 21, 3));
g.turns = vec![Turn {
via: 2,
from: 10,
to: 10,
kind: 2,
}];
let c = extract_cell(&g, cell([16., 48.]), vec![0, 1, 2, 3]).unwrap();
assert!(!c.allowed(k, 10, 1));
assert!(c.allowed(k, 10, 4));
}
#[test]
fn fastest_route_can_be_longer_than_shortest_distance() {
let mut g = transport_graph(vec![]);
g = g
.with_costs(
2,
vec![[200., 200.], [200., 200.], [30., 30.], [30., 30.]],
vec![],
)
.unwrap();
let mut r = PagedRouter::new(1_000_000, 10000);
r.set_profile(2).unwrap();
r.begin([16., 48.], [16.002, 48.]).unwrap();
let Route::Found {
seconds, meters, ..
} = solve(&mut r, &payloads(&g))
else {
panic!()
};
assert_eq!(seconds, 60.);
assert!(meters > distance([16., 48.], [16.002, 48.]) * 1.3);
}
}
