use std::cmp::Ordering;
use std::collections::BinaryHeap;
use crate::{
Grid,
any_angle::AnyAngleSearchRequest,
any_angle::geometry::{approximately_equal, segment_is_legal},
};
use condor_core::{BudgetExhausted, BudgetWatch, Point2};
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct VisibilityGraphCsr {
pub offsets: Vec<u32>,
pub neighbors: Vec<u32>,
pub weights: Vec<f64>,
}
impl VisibilityGraphCsr {
#[must_use]
pub fn directed_edge_count(&self) -> usize {
self.neighbors.len()
}
#[must_use]
pub fn retained_bytes(&self) -> usize {
self.offsets.len() * size_of::<u32>()
+ self.neighbors.len() * size_of::<u32>()
+ self.weights.len() * size_of::<f64>()
}
pub fn neighbors_of(&self, node: usize) -> impl Iterator<Item = (usize, f64)> + '_ {
let start = self.offsets[node] as usize;
let end = self.offsets[node + 1] as usize;
(start..end).map(move |index| (self.neighbors[index] as usize, self.weights[index]))
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct VisibilityGraphBuildStats {
pub visibility_checks: usize,
pub accepted_undirected_edges: usize,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct DijkstraSearchStats {
pub settled_nodes: usize,
pub relaxations: usize,
pub pushes: usize,
pub stale_pops: usize,
pub frontier_peak: usize,
}
pub(crate) fn build_visibility_adjacency(
grid: &Grid,
nodes: &[Point2],
segment_legal: fn(&Grid, Point2, Point2) -> bool,
stats: &mut VisibilityGraphBuildStats,
) -> Vec<Vec<(usize, f64)>> {
let node_count = nodes.len();
let mut adjacency = vec![Vec::new(); node_count];
for left in 0..node_count {
for right in left + 1..node_count {
stats.visibility_checks += 1;
if !segment_legal(grid, nodes[left], nodes[right]) {
continue;
}
let weight = nodes[left].distance_to(nodes[right]);
adjacency[left].push((right, weight));
adjacency[right].push((left, weight));
stats.accepted_undirected_edges += 1;
}
}
adjacency
}
pub(crate) fn adjacency_to_csr(adjacency: &[Vec<(usize, f64)>]) -> VisibilityGraphCsr {
let node_count = adjacency.len();
let mut offsets = Vec::with_capacity(node_count + 1);
let mut neighbors = Vec::new();
let mut weights = Vec::new();
offsets.push(0);
for edges in adjacency {
for &(neighbor, weight) in edges {
neighbors.push(neighbor as u32);
weights.push(weight);
}
offsets.push(neighbors.len() as u32);
}
VisibilityGraphCsr {
offsets,
neighbors,
weights,
}
}
pub(crate) fn build_visibility_csr(
grid: &Grid,
nodes: &[Point2],
stats: &mut VisibilityGraphBuildStats,
) -> VisibilityGraphCsr {
let adjacency = build_visibility_adjacency(grid, nodes, segment_is_legal, stats);
adjacency_to_csr(&adjacency)
}
pub(crate) fn run_dijkstra_csr_with_overlay(
csr: &VisibilityGraphCsr,
prepared_count: usize,
overlay_edges: &[Vec<(usize, f64)>],
start_index: usize,
goal_index: usize,
stats: &mut DijkstraSearchStats,
watch: &BudgetWatch,
) -> Result<Option<Vec<Option<usize>>>, BudgetExhausted> {
let node_count = overlay_edges.len();
debug_assert!(prepared_count <= node_count);
let mut dist = vec![f64::INFINITY; node_count];
let mut predecessors = vec![None; node_count];
let mut settled = vec![false; node_count];
dist[start_index] = 0.0;
let mut frontier = BinaryHeap::new();
frontier.push(QueueEntry {
node: start_index,
cost: 0.0,
});
stats.pushes += 1;
stats.frontier_peak = stats.frontier_peak.max(frontier.len());
while let Some(current) = frontier.pop() {
if settled[current.node] {
stats.stale_pops += 1;
continue;
}
if current.cost > dist[current.node] + 1e-15 {
stats.stale_pops += 1;
continue;
}
settled[current.node] = true;
stats.settled_nodes += 1;
if current.node == goal_index {
return Ok(Some(predecessors));
}
watch.check(stats.settled_nodes)?;
relax_neighbors(
current.node,
csr,
prepared_count,
overlay_edges,
&mut dist,
&mut predecessors,
&mut settled,
&mut frontier,
stats,
);
}
Ok(None)
}
pub(crate) fn run_dijkstra(
adjacency: &[Vec<(usize, f64)>],
start_index: usize,
goal_index: usize,
stats: &mut DijkstraSearchStats,
watch: &BudgetWatch,
) -> Result<Option<Vec<Option<usize>>>, BudgetExhausted> {
let node_count = adjacency.len();
let mut dist = vec![f64::INFINITY; node_count];
let mut predecessors = vec![None; node_count];
let mut settled = vec![false; node_count];
dist[start_index] = 0.0;
let mut frontier = BinaryHeap::new();
frontier.push(QueueEntry {
node: start_index,
cost: 0.0,
});
stats.pushes += 1;
stats.frontier_peak = stats.frontier_peak.max(frontier.len());
while let Some(current) = frontier.pop() {
if settled[current.node] {
stats.stale_pops += 1;
continue;
}
if current.cost > dist[current.node] + 1e-15 {
stats.stale_pops += 1;
continue;
}
settled[current.node] = true;
stats.settled_nodes += 1;
if current.node == goal_index {
return Ok(Some(predecessors));
}
watch.check(stats.settled_nodes)?;
for &(neighbor, weight) in &adjacency[current.node] {
relax_edge(
current.node,
neighbor,
weight,
&mut dist,
&mut predecessors,
&settled,
&mut frontier,
stats,
);
}
}
Ok(None)
}
#[allow(clippy::too_many_arguments)]
fn relax_neighbors(
node: usize,
csr: &VisibilityGraphCsr,
prepared_count: usize,
overlay_edges: &[Vec<(usize, f64)>],
dist: &mut [f64],
predecessors: &mut [Option<usize>],
settled: &mut [bool],
frontier: &mut BinaryHeap<QueueEntry>,
stats: &mut DijkstraSearchStats,
) {
if node < prepared_count {
for (neighbor, weight) in csr.neighbors_of(node) {
relax_edge(
node,
neighbor,
weight,
dist,
predecessors,
settled,
frontier,
stats,
);
}
}
for &(neighbor, weight) in &overlay_edges[node] {
relax_edge(
node,
neighbor,
weight,
dist,
predecessors,
settled,
frontier,
stats,
);
}
}
#[allow(clippy::too_many_arguments)]
fn relax_edge(
node: usize,
neighbor: usize,
weight: f64,
dist: &mut [f64],
predecessors: &mut [Option<usize>],
settled: &[bool],
frontier: &mut BinaryHeap<QueueEntry>,
stats: &mut DijkstraSearchStats,
) {
if settled[neighbor] {
return;
}
let candidate = dist[node] + weight;
let better = candidate < dist[neighbor]
|| (approximately_equal(candidate, dist[neighbor])
&& better_predecessor(node, predecessors[neighbor]));
if better {
dist[neighbor] = candidate;
predecessors[neighbor] = Some(node);
stats.relaxations += 1;
frontier.push(QueueEntry {
node: neighbor,
cost: candidate,
});
stats.pushes += 1;
stats.frontier_peak = stats.frontier_peak.max(frontier.len());
}
}
pub(crate) fn reconstruct_path(
nodes: &[Point2],
predecessors: &[Option<usize>],
goal_index: usize,
) -> Vec<Point2> {
let mut path = vec![nodes[goal_index]];
let mut current = goal_index;
while let Some(previous) = predecessors[current] {
path.push(nodes[previous]);
if previous == current {
break;
}
current = previous;
}
path.reverse();
path
}
pub(crate) fn elide_collinear_with_predicate(
grid: &Grid,
points: &mut Vec<Point2>,
segment_legal: fn(&Grid, Point2, Point2) -> bool,
) {
if points.len() < 3 {
return;
}
let mut simplified = Vec::with_capacity(points.len());
simplified.push(points[0]);
for index in 1..points.len() - 1 {
let prev = simplified[simplified.len() - 1];
let current = points[index];
let next = points[index + 1];
if !are_collinear(prev, current, next) {
simplified.push(current);
}
}
simplified.push(*points.last().expect("non-empty path"));
if simplified
.windows(2)
.all(|pair| segment_legal(grid, pair[0], pair[1]))
{
*points = simplified;
}
}
pub(crate) fn elide_collinear_points(grid: &Grid, points: &mut Vec<Point2>) {
elide_collinear_with_predicate(grid, points, segment_is_legal);
}
pub(crate) fn dedup_points(points: &mut Vec<Point2>) {
points.sort_by(|left, right| {
left.x
.partial_cmp(&right.x)
.unwrap_or(Ordering::Equal)
.then_with(|| left.y.partial_cmp(&right.y).unwrap_or(Ordering::Equal))
});
points.dedup_by(|left, right| points_equal(*left, *right));
}
#[must_use]
pub(crate) fn points_equal(left: Point2, right: Point2) -> bool {
approximately_equal(left.x, right.x) && approximately_equal(left.y, right.y)
}
pub(crate) fn node_index(nodes: &[Point2], point: Point2) -> Option<usize> {
nodes.iter().position(|node| points_equal(*node, point))
}
pub(crate) fn path_endpoints_match_request(
path: &crate::any_angle::AnyAnglePath,
request: AnyAngleSearchRequest,
) -> bool {
path.points().first() == Some(&request.start) && path.points().last() == Some(&request.goal)
}
pub(crate) fn add_undirected_edge(
adjacency: &mut [Vec<(usize, f64)>],
left: usize,
right: usize,
weight: f64,
) {
if left == right {
return;
}
if !adjacency[left]
.iter()
.any(|(neighbor, _)| *neighbor == right)
{
adjacency[left].push((right, weight));
}
if !adjacency[right]
.iter()
.any(|(neighbor, _)| *neighbor == left)
{
adjacency[right].push((left, weight));
}
}
fn are_collinear(a: Point2, b: Point2, c: Point2) -> bool {
let abx = b.x - a.x;
let aby = b.y - a.y;
let bcx = c.x - b.x;
let bcy = c.y - b.y;
(abx * bcy - aby * bcx).abs() <= 1e-12
}
fn better_predecessor(candidate: usize, current: Option<usize>) -> bool {
match current {
None => true,
Some(existing) => candidate < existing,
}
}
#[derive(Debug, PartialEq)]
struct QueueEntry {
node: usize,
cost: f64,
}
impl Eq for QueueEntry {}
impl Ord for QueueEntry {
fn cmp(&self, other: &Self) -> Ordering {
other
.cost
.partial_cmp(&self.cost)
.unwrap_or(Ordering::Equal)
.then_with(|| self.node.cmp(&other.node))
}
}
impl PartialOrd for QueueEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}