axiolid_route/
forced.rs

1//! The shortest walk forced through a region (#196).
2//!
3//! A walk from an origin to a target that visits a point `p` is at least
4//! `d_origin(p) + d_targets(p)` long, and the best such walk is exactly
5//! that long. So the shortest walk that enters a polygon has length
6//!
7//! ```text
8//! W = min over p in the polygon (and the free space) of  d_origin(p) + d_targets(p).
9//! ```
10//!
11//! If `W` exceeds the shortest walk overall, no shortest walk touches the
12//! polygon; if `W` equals it, one does.
13//!
14//! # The bracket
15//!
16//! The same branch and bound as [`crate::farthest_point`], turned round to
17//! find a minimum. Both distances are 1-Lipschitz along segments in the free
18//! space, so their sum `f` is 2-Lipschitz: on a triangle of the free space
19//! with an anchor `a`,
20//!
21//! ```text
22//! min over T of f  >=  f(a) - 2 (greatest distance from a to a corner of T).
23//! ```
24//!
25//! That bound is weak where `f` is least along a whole stretch of path, as
26//! it is on every straight run of the shortest walk through the polygon: it
27//! would need cells as small as the tolerance all along the run. So a cell
28//! is also bounded through the maps themselves. A distance at `y` is
29//! `|y - v| + D(v)` for some graph vertex `v` that `y` sees, so for any
30//! vertices `u` of one map and `v` of the other,
31//!
32//! ```text
33//! f(y) >= D(u) + D'(v) + max(|u - v|, dist(T, u) + dist(T, v)),
34//! ```
35//!
36//! and the least of these over the vertices the cell might see bounds `f`
37//! on the cell. A vertex is left out only when it certainly sees no point
38//! of the cell: one obstacle edge crosses every segment from it to the
39//! cell, or the cell lies strictly inside a solid corner at the vertex. On a cell the walk passes straight
40//! through, the pair of vertices it runs between gives `|u - v|` exactly.
41//!
42//! Every value of `f` at a point of the polygon is an upper bound on `W`,
43//! and no walk from an origin to a target is shorter than the shortest one,
44//! `L`, so `W >= L` bounds every cell from below as well. Cells are split,
45//! the one with the least lower bound first, and dropped once their lower
46//! bound reaches the best upper one. Rounding widens the result as in
47//! [`crate::farthest_point`].
48//!
49//! # Over weighted maps
50//!
51//! [`weighted_forced_walk`] runs the same search over two
52//! [`WeightedMap`]s (#198), the walk's cost for its length. The sum of two
53//! weighted distances falls at most twice as fast as the steepest factor
54//! in a cell. The pair bound runs over the maps' nodes -- vertices and the
55//! intervals along cost edges -- with their lower bounds: a walk from a
56//! point `y` of the cell is straight up to its first node, and on a cell
57//! no cost edge meets, that piece costs the cell's factor `F` a metre, so
58//!
59//! ```text
60//! f(y) >= L(u) + L'(v) + F max(dist(u, v), dist(T, u) + dist(T, v)).
61//! ```
62//!
63//! Each map only brackets its distance, so the result is never narrower
64//! than the two maps' brackets summed at the witness; the search stops
65//! once it is within the tolerance of that.
66
67use core::cmp::Ordering;
68use std::collections::{BinaryHeap, HashMap};
69
70use axiolid_contracts::Sign;
71use axiolid_core::Point2;
72use axiolid_overlay::Polygon;
73
74use crate::map::{
75    admissible, free_triangles, free_triangles_in, in_polygon, in_triangle, meets_triangle, outside,
76};
77use crate::{
78    crosses, side, validate_region, within, DistanceMap, FarthestError, LengthInterval, RouteError,
79    WeightedMap, MAX_CELLS,
80};
81
82/// The shortest walk from an origin to a target that enters a polygon.
83#[derive(Debug, Clone, Copy, PartialEq)]
84#[non_exhaustive]
85pub struct ForcedWalk {
86    /// Contains the length of the shortest walk that enters the polygon.
87    /// Both ends are infinite when no walk from an origin to a target
88    /// reaches the polygon.
89    pub length: LengthInterval,
90    /// The shortest walk from any origin to any target, ignoring the
91    /// polygon: `length.lower` is never below it, up to rounding. Infinite
92    /// when no target is reachable from an origin.
93    pub shortest: f64,
94    /// A point of the polygon, in the free space, through which a walk of
95    /// at most `length.upper` passes; `None` when no walk reaches the
96    /// polygon.
97    pub witness: Option<Point2>,
98    /// Whether the interval is no wider than the tolerance asked for. When
99    /// the cell budget runs out first it is still sound, only wider.
100    pub converged: bool,
101    /// Cells examined.
102    pub cells: usize,
103}
104
105/// The shortest walk from an origin of `from` to a target of `to` that
106/// enters `through`, bracketed to within `tolerance`.
107///
108/// `from` is a distance map whose targets are the walk's origins, `to` one
109/// whose targets are its destinations; both must cover the same region and
110/// barriers.
111///
112/// # Errors
113///
114/// [`FarthestError::MismatchedMaps`] when the maps cover different free
115/// space; otherwise as [`crate::farthest_point`], except that a part of the
116/// polygon no walk reaches is not an error: it only contributes nothing.
117pub fn forced_walk(
118    from: &DistanceMap,
119    to: &DistanceMap,
120    through: &Polygon,
121    tolerance: f64,
122) -> Result<ForcedWalk, FarthestError> {
123    forced_walk_within(from, to, through, tolerance, MAX_CELLS)
124}
125
126/// A cell: a triangle inside the free-space triangle `root`, with the best
127/// anchor known for it and the lower bound that gives.
128#[derive(Debug, Clone, Copy)]
129struct Cell {
130    corners: [Point2; 3],
131    root: usize,
132    anchor: (Point2, f64),
133    lower: f64,
134    depth: u32,
135    order: usize,
136}
137
138impl PartialEq for Cell {
139    fn eq(&self, other: &Self) -> bool {
140        self.cmp(other) == Ordering::Equal
141    }
142}
143
144impl Eq for Cell {}
145
146impl PartialOrd for Cell {
147    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
148        Some(self.cmp(other))
149    }
150}
151
152impl Ord for Cell {
153    /// Least lower bound first (the heap is a max-heap); the earlier cell
154    /// on a tie.
155    fn cmp(&self, other: &Self) -> Ordering {
156        other
157            .lower
158            .total_cmp(&self.lower)
159            .then(other.order.cmp(&self.order))
160    }
161}
162
163/// [`forced_walk`] with a caller-chosen cell budget.
164///
165/// # Errors
166///
167/// As [`forced_walk`].
168pub fn forced_walk_within(
169    from: &DistanceMap,
170    to: &DistanceMap,
171    through: &Polygon,
172    tolerance: f64,
173    max_cells: usize,
174) -> Result<ForcedWalk, FarthestError> {
175    if !(tolerance.is_finite() && tolerance >= 0.0) {
176        return Err(FarthestError::InvalidTolerance);
177    }
178    if !from.same_space(to) {
179        return Err(FarthestError::MismatchedMaps);
180    }
181    validate_region(core::slice::from_ref(through), &[])?;
182    let roots = free_triangles(from)?;
183    // Each sum carries the roundings of both maps' lengths.
184    let relative = (from.hops() as f64 + to.hops() as f64 + 16.0) * 2.0 * f64::EPSILON;
185    let scale = from
186        .nodes()
187        .iter()
188        .chain(through.outer.points.iter())
189        .fold(0.0f64, |m, p| m.max(p.x.abs()).max(p.y.abs()));
190    // As for the farthest point, doubled: two distances move with a point.
191    let slack = |depth: u32, radius: f64| {
192        2.0 * (f64::from(depth + 4) * 4.0 * f64::EPSILON * scale + 4.0 * f64::EPSILON * radius)
193    };
194    // The shortest walk overall: no walk through the polygon beats it.
195    let mut shortest = f64::INFINITY;
196    for (&origin, &weight) in from.sites().iter().zip(from.weights()) {
197        if let Some(d) = to.at(origin)? {
198            shortest = shortest.min(weight + d);
199        }
200    }
201    let floor = shortest * (1.0 - relative);
202    let vertices = |map: &DistanceMap| -> Result<Vec<Vertex>, RouteError> {
203        map.nodes()
204            .iter()
205            .copied()
206            .zip(map.vertex_distances())
207            .filter(|(_, d)| d.is_finite())
208            .map(|(at, distance)| {
209                Ok(Vertex {
210                    at,
211                    distance,
212                    solid: solid_corner(map.region(), map.obstacles(), at)?,
213                })
214            })
215            .collect()
216    };
217    let mut search = Search {
218        from,
219        to,
220        through,
221        from_vertices: vertices(from)?,
222        to_vertices: vertices(to)?,
223        roots: &roots,
224        evaluated: HashMap::new(),
225        upper: None,
226    };
227    let mut heap = BinaryHeap::new();
228    let mut cells = 0usize;
229    let mut met = false;
230    for (root, corners) in roots.iter().enumerate() {
231        if outside(through, corners)? {
232            continue;
233        }
234        met = true;
235        cells += 1;
236        match search.cell(*corners, root, None, 0, cells, floor, &slack)? {
237            Anchored::Cell(cell) => heap.push(cell),
238            // No walk reaches this part of the free space.
239            Anchored::Unreached => {}
240            Anchored::None => return Err(FarthestError::Triangulation),
241        }
242    }
243    if !met {
244        return Err(FarthestError::Empty);
245    }
246    let best = |search: &Search| search.upper.map_or(f64::INFINITY, |(d, _)| d);
247    while let Some(top) = heap.peek() {
248        if top.lower >= best(&search) - tolerance || cells >= max_cells {
249            break;
250        }
251        let cell = heap.pop().expect("peeked");
252        let [a, b, c] = cell.corners;
253        let mid = |p: Point2, q: Point2| Point2::new(0.5 * p.x + 0.5 * q.x, 0.5 * p.y + 0.5 * q.y);
254        let (ab, bc, ca) = (mid(a, b), mid(b, c), mid(c, a));
255        for corners in [[a, ab, ca], [ab, b, bc], [ca, bc, c], [ab, bc, ca]] {
256            if outside(through, &corners)? {
257                continue;
258            }
259            cells += 1;
260            let child = search.cell(
261                corners,
262                cell.root,
263                Some(cell.anchor),
264                cell.depth + 1,
265                cells,
266                floor,
267                &slack,
268            )?;
269            if let Anchored::Cell(child) = child {
270                if child.lower < best(&search) {
271                    heap.push(child);
272                }
273            }
274        }
275    }
276    let Some((upper, witness)) = search.upper else {
277        if heap.is_empty() {
278            // Every part of the polygon in the free space is unreached.
279            return Ok(ForcedWalk {
280                length: LengthInterval {
281                    lower: f64::INFINITY,
282                    upper: f64::INFINITY,
283                },
284                shortest,
285                witness: None,
286                converged: true,
287                cells,
288            });
289        }
290        let lower = heap.peek().map_or(floor, |c| c.lower.max(floor));
291        return Ok(ForcedWalk {
292            length: LengthInterval {
293                lower: lower * (1.0 - relative),
294                upper: f64::INFINITY,
295            },
296            shortest,
297            witness: None,
298            converged: false,
299            cells,
300        });
301    };
302    let lower = heap.peek().map_or(upper, |c| c.lower.min(upper)).max(floor);
303    Ok(ForcedWalk {
304        length: LengthInterval {
305            lower: (lower * (1.0 - relative)).min(upper),
306            upper: upper * (1.0 + relative),
307        },
308        shortest,
309        witness: Some(witness),
310        converged: upper - lower <= tolerance,
311        cells,
312    })
313}
314
315enum Anchored {
316    Cell(Cell),
317    /// The cell's root triangle is unreached from an origin or a target.
318    Unreached,
319    /// No anchor lies in the cell (never for a root, unless the triangle
320    /// holds no representable interior point).
321    None,
322}
323
324struct Search<'a> {
325    from: &'a DistanceMap,
326    to: &'a DistanceMap,
327    through: &'a Polygon,
328    /// Graph vertices a target is reached from.
329    from_vertices: Vec<Vertex>,
330    to_vertices: Vec<Vertex>,
331    roots: &'a [[Point2; 3]],
332    evaluated: HashMap<(u64, u64), Option<f64>>,
333    /// The least walk length at a point of the polygon, and the point.
334    upper: Option<(f64, Point2)>,
335}
336
337impl Search<'_> {
338    /// The least of `D(u) + D'(v) + max(|u - v|, dist(T, u) + dist(T, v))`
339    /// over the vertices `u` of `from` and `v` of `to` that the cell might
340    /// see; infinite when it sees none of one map's.
341    fn pair_bound(&self, t: &[Point2; 3]) -> Result<f64, RouteError> {
342        let origins = candidates(&self.from_vertices, t, self.from.obstacles())?;
343        let targets = candidates(&self.to_vertices, t, self.to.obstacles())?;
344        let Some(&(least, ..)) = targets.first() else {
345            return Ok(f64::INFINITY);
346        };
347        let mut best = f64::INFINITY;
348        for &(s, u, du, gu) in &origins {
349            if s + least >= best {
350                break;
351            }
352            for &(r, v, dv, gv) in &targets {
353                if s + r >= best {
354                    break;
355                }
356                best = best.min(du + dv + (u - v).length().max(gu + gv));
357            }
358        }
359        Ok(best)
360    }
361
362    /// `d_origin(p) + d_targets(p)`, or `None` when either is unreachable.
363    fn walk(&mut self, p: Point2) -> Result<Option<f64>, FarthestError> {
364        let key = (p.x.to_bits(), p.y.to_bits());
365        if let Some(d) = self.evaluated.get(&key) {
366            return Ok(*d);
367        }
368        let d = match (self.from.at(p)?, self.to.at(p)?) {
369            (Some(a), Some(b)) => Some(a + b),
370            _ => None,
371        };
372        self.evaluated.insert(key, d);
373        Ok(d)
374    }
375
376    #[allow(clippy::too_many_arguments)]
377    fn cell(
378        &mut self,
379        corners: [Point2; 3],
380        root: usize,
381        inherited: Option<(Point2, f64)>,
382        depth: u32,
383        order: usize,
384        floor: f64,
385        slack: &dyn Fn(u32, f64) -> f64,
386    ) -> Result<Anchored, FarthestError> {
387        let centroid = Point2::new(
388            (corners[0].x + corners[1].x + corners[2].x) / 3.0,
389            (corners[0].y + corners[1].y + corners[2].y) / 3.0,
390        );
391        let radius = |a: Point2| {
392            corners
393                .iter()
394                .map(|c| (*c - a).length())
395                .fold(0.0, f64::max)
396        };
397        let mut best: Option<(f64, (Point2, f64))> =
398            inherited.map(|(a, f)| (lipschitz_lower(f, radius(a)), (a, f)));
399        for a in [corners[0], corners[1], corners[2], centroid] {
400            if !admissible(self.from, &self.roots[root], a)? {
401                continue;
402            }
403            // Each map reaches all of a free-space triangle or none of it.
404            let Some(f) = self.walk(a)? else {
405                return Ok(Anchored::Unreached);
406            };
407            if crate::map::in_polygon(self.through, a)? && self.upper.is_none_or(|(u, _)| f < u) {
408                self.upper = Some((f, a));
409            }
410            let bound = lipschitz_lower(f, radius(a));
411            if best.is_none_or(|(b, _)| bound > b) {
412                best = Some((bound, (a, f)));
413            }
414        }
415        let pairs = self.pair_bound(&corners)?;
416        Ok(match best {
417            Some((bound, anchor)) => Anchored::Cell(Cell {
418                corners,
419                root,
420                anchor,
421                lower: (bound.max(pairs) - slack(depth, radius(anchor.0))).max(floor),
422                depth,
423                order,
424            }),
425            None => Anchored::None,
426        })
427    }
428}
429
430/// The vertices a cell might see, as `(dist + D, vertex, D, dist)` in
431/// increasing order, where `dist` is the vertex's distance to the cell. A
432/// vertex is left out only when one obstacle edge properly crosses the
433/// segment from it to every corner of the cell, which then blocks it from
434/// every point of the cell.
435fn candidates(
436    vertices: &[Vertex],
437    t: &[Point2; 3],
438    obstacles: &[(Point2, Point2)],
439) -> Result<Vec<(f64, Point2, f64, f64)>, RouteError> {
440    let mut out = Vec::new();
441    'vertex: for vertex in vertices {
442        let (v, d) = (vertex.at, vertex.distance);
443        if let Some((a, b)) = vertex.solid {
444            // Strictly inside the convex wedge from `v` between `a` and `b`.
445            let inside = |c: Point2| -> Result<bool, RouteError> {
446                let (ab, ac) = (side(v, a, b)?, side(v, a, c)?);
447                let (ba, bc) = (side(v, b, a)?, side(v, b, c)?);
448                Ok(ac != Sign::Zero && ac == ab && bc != Sign::Zero && bc == ba)
449            };
450            if inside(t[0])? && inside(t[1])? && inside(t[2])? {
451                continue 'vertex;
452            }
453        }
454        for &(p, q) in obstacles {
455            if crosses(v, t[0], p, q)? && crosses(v, t[1], p, q)? && crosses(v, t[2], p, q)? {
456                continue 'vertex;
457            }
458        }
459        let g = triangle_distance(t, v)?;
460        out.push((g + d, v, d, g));
461    }
462    out.sort_by(|a, b| a.0.total_cmp(&b.0));
463    Ok(out)
464}
465
466/// The least `f` can be within `radius` of a point where it is `f`: both
467/// distances are 1-Lipschitz, so their sum falls at most twice as fast.
468fn lipschitz_lower(f: f64, radius: f64) -> f64 {
469    f - 2.0 * radius
470}
471
472/// A graph vertex a target is reached from.
473struct Vertex {
474    at: Point2,
475    distance: f64,
476    /// Neighbours `a`, `b` on its ring such that the open convex wedge from
477    /// the vertex between them lies outside the free space: the inside of
478    /// a hole at a convex corner, or the outside of the region at a reflex
479    /// corner. Nothing strictly inside that wedge is seen from the vertex.
480    solid: Option<(Point2, Point2)>,
481}
482
483/// The solid convex wedge at `v`, when `v` is a corner of exactly one ring
484/// and no barrier, and the wedge less than a half turn lies outside the
485/// free space. Decided exactly.
486fn solid_corner(
487    region: &[Polygon],
488    obstacles: &[(Point2, Point2)],
489    v: Point2,
490) -> Result<Option<(Point2, Point2)>, RouteError> {
491    if obstacles.iter().filter(|(p, q)| *p == v || *q == v).count() != 2 {
492        return Ok(None);
493    }
494    for polygon in region {
495        for (hole, ring) in
496            core::iter::once((false, &polygon.outer)).chain(polygon.holes.iter().map(|h| (true, h)))
497        {
498            let n = ring.points.len();
499            let Some(i) = ring.points.iter().position(|p| *p == v) else {
500                continue;
501            };
502            let (prev, next) = (ring.points[(i + n - 1) % n], ring.points[(i + 1) % n]);
503            let turn = side(prev, v, next)?;
504            if turn == Sign::Zero {
505                return Ok(None);
506            }
507            // The ring's orientation, from its lowest-leftmost corner,
508            // which is convex.
509            let k = (0..n)
510                .min_by(|&a, &b| {
511                    let (p, q) = (ring.points[a], ring.points[b]);
512                    p.y.total_cmp(&q.y).then(p.x.total_cmp(&q.x))
513                })
514                .expect("a ring has corners");
515            let orientation = side(
516                ring.points[(k + n - 1) % n],
517                ring.points[k],
518                ring.points[(k + 1) % n],
519            )?;
520            // The ring's inside is the small wedge at a corner that turns
521            // with the ring; solid means inside a hole, outside the outer.
522            let small_is_inside = turn == orientation;
523            return Ok((small_is_inside == hole).then_some((prev, next)));
524        }
525    }
526    Ok(None)
527}
528
529/// The distance from `v` to the closed triangle: zero inside it, decided
530/// exactly, else the least distance to an edge.
531fn triangle_distance(t: &[Point2; 3], v: Point2) -> Result<f64, RouteError> {
532    if meets_triangle(v, v, t)? {
533        return Ok(0.0);
534    }
535    Ok((0..3)
536        .map(|i| segment_distance(t[i], t[(i + 1) % 3], v))
537        .fold(f64::INFINITY, f64::min))
538}
539
540fn segment_distance(a: Point2, b: Point2, v: Point2) -> f64 {
541    let d = b - a;
542    let length2 = d.dot(d);
543    let s = if length2 > 0.0 {
544        ((v - a).dot(d) / length2).clamp(0.0, 1.0)
545    } else {
546        0.0
547    };
548    (v - (a + d * s)).length()
549}
550
551/// The cheapest walk from an origin to a target that enters a polygon,
552/// over weighted maps.
553#[derive(Debug, Clone, Copy, PartialEq)]
554#[non_exhaustive]
555pub struct WeightedForcedWalk {
556    /// Contains the cost of the cheapest walk that enters the polygon.
557    /// Both ends are infinite when no walk from an origin to a target
558    /// reaches the polygon.
559    pub cost: LengthInterval,
560    /// Contains the cost of the cheapest walk from any origin to any
561    /// target, ignoring the polygon: `cost` is never below its lower end.
562    /// Infinite when no target is reachable from an origin.
563    pub shortest: LengthInterval,
564    /// A point of the polygon, in the free space, through which a walk
565    /// costing at most `cost.upper` passes; `None` when no walk reaches
566    /// the polygon.
567    pub witness: Option<Point2>,
568    /// Whether `cost` is no wider than the tolerance asked for plus the
569    /// maps' own brackets summed at the witness. When the cell budget runs
570    /// out first it is still sound, only wider.
571    pub converged: bool,
572    /// Cells examined.
573    pub cells: usize,
574}
575
576/// The cheapest walk from an origin of `from` to a target of `to` that
577/// enters `through`, where both maps weight travel by the same cost
578/// regions (#198): [`forced_walk`] for [`WeightedMap`]s. Origins' start
579/// weights (see [`crate::weighted_distance_map_seeded`]) count.
580///
581/// The cost is bracketed to within `tolerance` plus what the maps' own
582/// brackets allow at the witness.
583///
584/// # Errors
585///
586/// [`FarthestError::MismatchedMaps`] when the maps cover different free
587/// space or weight it differently; otherwise as [`forced_walk`].
588pub fn weighted_forced_walk(
589    from: &WeightedMap,
590    to: &WeightedMap,
591    through: &Polygon,
592    tolerance: f64,
593) -> Result<WeightedForcedWalk, FarthestError> {
594    weighted_forced_walk_within(from, to, through, tolerance, MAX_CELLS)
595}
596
597/// A node of a weighted map for the pair bound: its span, the least lower
598/// bound from it, and the solid wedge at a vertex.
599struct Span {
600    a: Point2,
601    b: Point2,
602    least: f64,
603    solid: Option<(Point2, Point2)>,
604}
605
606/// [`weighted_forced_walk`] with a caller-chosen cell budget.
607///
608/// # Errors
609///
610/// As [`weighted_forced_walk`].
611pub fn weighted_forced_walk_within(
612    from: &WeightedMap,
613    to: &WeightedMap,
614    through: &Polygon,
615    tolerance: f64,
616    max_cells: usize,
617) -> Result<WeightedForcedWalk, FarthestError> {
618    if !(tolerance.is_finite() && tolerance >= 0.0) {
619        return Err(FarthestError::InvalidTolerance);
620    }
621    if !from.same_space(to) {
622        return Err(FarthestError::MismatchedMaps);
623    }
624    validate_region(core::slice::from_ref(through), &[])?;
625    let roots = free_triangles_in(from.region(), from.obstacles())?;
626    let steep: Vec<f64> = roots
627        .iter()
628        .map(|t| from.steepest(t))
629        .collect::<Result<_, _>>()?;
630    // The maps' bounds are rounded outward already; their sums once more.
631    let relative = 16.0 * 2.0 * f64::EPSILON;
632    let scale = from
633        .region()
634        .iter()
635        .flat_map(|p| p.outer.points.iter())
636        .chain(through.outer.points.iter())
637        .fold(0.0f64, |m, p| m.max(p.x.abs()).max(p.y.abs()));
638    let slack = |depth: u32, radius: f64, k: f64| {
639        2.0 * k * (f64::from(depth + 4) * 4.0 * f64::EPSILON * scale + 4.0 * f64::EPSILON * radius)
640    };
641    let mut shortest = LengthInterval {
642        lower: f64::INFINITY,
643        upper: f64::INFINITY,
644    };
645    for (origin, weight) in from.seeded() {
646        if let Some((lo, hi)) = to.bracket(origin)? {
647            shortest.lower = shortest.lower.min(weight + lo);
648            shortest.upper = shortest.upper.min(weight + hi);
649        }
650    }
651    let floor = shortest.lower * (1.0 - relative);
652    let spans = |map: &WeightedMap| -> Result<Vec<Span>, RouteError> {
653        map.spans()
654            .into_iter()
655            .map(|((a, b), least)| {
656                Ok(Span {
657                    a,
658                    b,
659                    least,
660                    solid: if a == b {
661                        solid_corner(map.region(), map.obstacles(), a)?
662                    } else {
663                        None
664                    },
665                })
666            })
667            .collect()
668    };
669    let (from_spans, to_spans) = (spans(from)?, spans(to)?);
670    let mut evaluated: HashMap<(u64, u64), Option<(f64, f64)>> = HashMap::new();
671    // The least upper bound at a point of the polygon, the point, and the
672    // width of the maps' brackets there.
673    let mut upper: Option<(f64, Point2, f64)> = None;
674    let mut cell = |corners: [Point2; 3],
675                    root: usize,
676                    inherited: Option<(Point2, f64)>,
677                    depth: u32,
678                    order: usize,
679                    upper: &mut Option<(f64, Point2, f64)>|
680     -> Result<Anchored, FarthestError> {
681        let centroid = Point2::new(
682            (corners[0].x + corners[1].x + corners[2].x) / 3.0,
683            (corners[0].y + corners[1].y + corners[2].y) / 3.0,
684        );
685        let radius = |a: Point2| {
686            corners
687                .iter()
688                .map(|c| (*c - a).length())
689                .fold(0.0, f64::max)
690        };
691        let k = steep[root];
692        let mut best: Option<(f64, (Point2, f64))> =
693            inherited.map(|(a, f)| (weighted_lipschitz_lower(f, radius(a), k), (a, f)));
694        for a in [corners[0], corners[1], corners[2], centroid] {
695            if !in_triangle(&roots[root], a)? {
696                continue;
697            }
698            if from.walls().iter().try_fold(false, |m, &(p, q)| {
699                Ok::<_, RouteError>(m || (within(p, q, a) && side(p, q, a)? == Sign::Zero))
700            })? {
701                continue;
702            }
703            let key = (a.x.to_bits(), a.y.to_bits());
704            let bracket = match evaluated.get(&key) {
705                Some(b) => *b,
706                None => {
707                    let b = match (from.bracket(a)?, to.bracket(a)?) {
708                        (Some((l1, h1)), Some((l2, h2))) => Some((l1 + l2, h1 + h2)),
709                        _ => None,
710                    };
711                    evaluated.insert(key, b);
712                    b
713                }
714            };
715            // Each map reaches all of a free-space triangle or none of it.
716            let Some((lo, hi)) = bracket else {
717                return Ok(Anchored::Unreached);
718            };
719            if in_polygon(through, a)? && upper.is_none_or(|(u, ..)| hi < u) {
720                *upper = Some((hi, a, hi - lo));
721            }
722            let bound = weighted_lipschitz_lower(lo, radius(a), k);
723            if best.is_none_or(|(b, _)| bound > b) {
724                best = Some((bound, (a, lo)));
725            }
726        }
727        let pairs = weighted_pair_bound(from, &from_spans, &to_spans, &corners)?;
728        Ok(match best {
729            Some((bound, anchor)) => Anchored::Cell(Cell {
730                corners,
731                root,
732                anchor,
733                lower: (bound.max(pairs) - slack(depth, radius(anchor.0), k)).max(floor),
734                depth,
735                order,
736            }),
737            None => Anchored::None,
738        })
739    };
740    let mut heap = BinaryHeap::new();
741    let mut cells = 0usize;
742    let mut met = false;
743    for (root, corners) in roots.iter().enumerate() {
744        if outside(through, corners)? {
745            continue;
746        }
747        met = true;
748        cells += 1;
749        match cell(*corners, root, None, 0, cells, &mut upper)? {
750            Anchored::Cell(c) => heap.push(c),
751            Anchored::Unreached => {}
752            Anchored::None => return Err(FarthestError::Triangulation),
753        }
754    }
755    if !met {
756        return Err(FarthestError::Empty);
757    }
758    // Stop once no cell can come within the tolerance, widened by the
759    // maps' own brackets at the best point: no splitting narrows those.
760    let goal = |upper: &Option<(f64, Point2, f64)>| {
761        upper.map_or(f64::INFINITY, |(u, _, width)| u - tolerance - width)
762    };
763    while let Some(top) = heap.peek() {
764        if top.lower >= goal(&upper) || cells >= max_cells {
765            break;
766        }
767        let parent = heap.pop().expect("peeked");
768        let [a, b, c] = parent.corners;
769        let mid = |p: Point2, q: Point2| Point2::new(0.5 * p.x + 0.5 * q.x, 0.5 * p.y + 0.5 * q.y);
770        let (ab, bc, ca) = (mid(a, b), mid(b, c), mid(c, a));
771        for corners in [[a, ab, ca], [ab, b, bc], [ca, bc, c], [ab, bc, ca]] {
772            if outside(through, &corners)? {
773                continue;
774            }
775            cells += 1;
776            let child = cell(
777                corners,
778                parent.root,
779                Some(parent.anchor),
780                parent.depth + 1,
781                cells,
782                &mut upper,
783            )?;
784            if let Anchored::Cell(child) = child {
785                if child.lower < upper.map_or(f64::INFINITY, |(u, ..)| u) {
786                    heap.push(child);
787                }
788            }
789        }
790    }
791    let Some((hi, witness, width)) = upper else {
792        let lower = if heap.is_empty() {
793            f64::INFINITY
794        } else {
795            heap.peek().map_or(floor, |c| c.lower.max(floor)) * (1.0 - relative)
796        };
797        return Ok(WeightedForcedWalk {
798            cost: LengthInterval {
799                lower,
800                upper: f64::INFINITY,
801            },
802            shortest,
803            witness: None,
804            converged: heap.is_empty(),
805            cells,
806        });
807    };
808    let lower = heap.peek().map_or(hi, |c| c.lower.min(hi)).max(floor);
809    Ok(WeightedForcedWalk {
810        cost: LengthInterval {
811            lower: (lower * (1.0 - relative)).min(hi),
812            upper: hi * (1.0 + relative),
813        },
814        shortest,
815        witness: Some(witness),
816        converged: hi - lower <= tolerance + width,
817        cells,
818    })
819}
820
821/// The least the sum of two weighted distances can be within `radius` of a
822/// point where it is `f`, on a cell whose steepest factor is `k`: each
823/// falls at most `k` a metre.
824fn weighted_lipschitz_lower(f: f64, radius: f64, k: f64) -> f64 {
825    f - 2.0 * k * radius
826}
827
828/// The pair bound on the cell `t` (see [`span_pair_bound`]), at the
829/// factor every point of the cell has, or 1 where a cost edge meets it.
830fn weighted_pair_bound(
831    map: &WeightedMap,
832    origins: &[Span],
833    targets: &[Span],
834    t: &[Point2; 3],
835) -> Result<f64, RouteError> {
836    let factor = map.inside_factor(t)?;
837    span_pair_bound(origins, targets, t, factor, map.obstacles())
838}
839
840/// The least of `L(u) + L'(v) + F max(dist(u, v), dist(T, u) + dist(T, v))`
841/// over the nodes `u` of one map and `v` of the other that the cell `t`
842/// might see, `F` the factor of every point of the cell; infinite when it
843/// sees none of one map's. A node is left out as in [`candidates`].
844fn span_pair_bound(
845    origins: &[Span],
846    targets: &[Span],
847    t: &[Point2; 3],
848    factor: f64,
849    obstacles: &[(Point2, Point2)],
850) -> Result<f64, RouteError> {
851    let origins = span_candidates(origins, t, obstacles)?;
852    let targets = span_candidates(targets, t, obstacles)?;
853    let Some(&(least, ..)) = targets.first() else {
854        return Ok(f64::INFINITY);
855    };
856    let mut best = f64::INFINITY;
857    for &(s, u, du, gu) in &origins {
858        // The factor is at least 1: `s + r` bounds every later pair.
859        if s + least >= best {
860            break;
861        }
862        for &(r, v, dv, gv) in &targets {
863            if s + r >= best {
864                break;
865            }
866            let apart = crate::weighted::span_distance((u.a, u.b), (v.a, v.b))?;
867            best = best.min(du + dv + factor * apart.max(gu + gv));
868        }
869    }
870    Ok(best)
871}
872
873/// The spans a cell might see, as `(dist + L, span, L, dist)` in
874/// increasing order, where `dist` is the span's distance to the cell. A
875/// span is left out only when one obstacle edge properly crosses the
876/// segment from each of its ends to every corner of the cell, or, for a
877/// vertex, the cell lies strictly inside its solid wedge.
878fn span_candidates<'s>(
879    spans: &'s [Span],
880    t: &[Point2; 3],
881    obstacles: &[(Point2, Point2)],
882) -> Result<Vec<(f64, &'s Span, f64, f64)>, RouteError> {
883    let mut out = Vec::new();
884    'span: for span in spans {
885        if let Some((a, b)) = span.solid {
886            let v = span.a;
887            let inside = |c: Point2| -> Result<bool, RouteError> {
888                let (ab, ac) = (side(v, a, b)?, side(v, a, c)?);
889                let (ba, bc) = (side(v, b, a)?, side(v, b, c)?);
890                Ok(ac != Sign::Zero && ac == ab && bc != Sign::Zero && bc == ba)
891            };
892            if inside(t[0])? && inside(t[1])? && inside(t[2])? {
893                continue 'span;
894            }
895        }
896        for &(p, q) in obstacles {
897            let mut all = true;
898            for end in [span.a, span.b] {
899                for c in t {
900                    if !crosses(end, *c, p, q)? {
901                        all = false;
902                    }
903                }
904            }
905            if all {
906                continue 'span;
907            }
908        }
909        let g = if span.a == span.b {
910            triangle_distance(t, span.a)?
911        } else if meets_triangle(span.a, span.b, t)? {
912            0.0
913        } else {
914            let mut g = f64::INFINITY;
915            for i in 0..3 {
916                let (c, d) = (t[i], t[(i + 1) % 3]);
917                g = g
918                    .min(segment_distance(c, d, span.a))
919                    .min(segment_distance(c, d, span.b))
920                    .min(segment_distance(span.a, span.b, c));
921            }
922            g
923        };
924        out.push((g + span.least, span, span.least, g));
925    }
926    out.sort_by(|a, b| a.0.total_cmp(&b.0));
927    Ok(out)
928}
929
930#[cfg(test)]
931mod tests {
932    use super::*;
933    use crate::distance_map;
934    use axiolid_overlay::Ring;
935
936    fn p(x: f64, y: f64) -> Point2 {
937        Point2::new(x, y)
938    }
939
940    fn ring(points: &[(f64, f64)]) -> Ring {
941        Ring {
942            points: points.iter().map(|(x, y)| p(*x, *y)).collect(),
943        }
944    }
945
946    fn two_holes() -> Polygon {
947        Polygon {
948            outer: ring(&[(0.0, 0.0), (12.0, 0.0), (12.0, 8.0), (0.0, 8.0)]),
949            holes: vec![
950                ring(&[(2.0, 2.0), (5.0, 2.0), (5.0, 6.0), (2.0, 6.0)]),
951                ring(&[(7.0, 2.0), (10.0, 2.0), (10.0, 6.0), (7.0, 6.0)]),
952            ],
953        }
954    }
955
956    fn vertices(map: &DistanceMap) -> Vec<Vertex> {
957        map.nodes()
958            .iter()
959            .copied()
960            .zip(map.vertex_distances())
961            .filter(|(_, d)| d.is_finite())
962            .map(|(at, distance)| Vertex {
963                at,
964                distance,
965                solid: solid_corner(map.region(), map.obstacles(), at).unwrap(),
966            })
967            .collect()
968    }
969
970    /// `f(y) = 2 |y|` when origin and target coincide: it falls at twice
971    /// the rate of either distance, and the bound must allow for that.
972    #[test]
973    fn the_lipschitz_bound_allows_both_distances_to_fall_together() {
974        let room = [Polygon {
975            outer: ring(&[(-10.0, -10.0), (10.0, -10.0), (10.0, 10.0), (-10.0, 10.0)]),
976            holes: Vec::new(),
977        }];
978        let map = distance_map(&room, &[], &[p(0.0, 0.0)]).unwrap();
979        let a = p(5.0, 0.0);
980        let f = 2.0 * map.at(a).unwrap().unwrap();
981        // The nearest point within radius 1 of `a` to the origin.
982        let y = p(4.0, 0.0);
983        let least = 2.0 * map.at(y).unwrap().unwrap();
984        assert!(lipschitz_lower(f, 1.0) <= least);
985        assert_eq!(lipschitz_lower(f, 1.0), least);
986    }
987
988    /// The pair search stops early only when no later pair can win: its
989    /// result is the brute-force least over every pair of candidates.
990    #[test]
991    fn the_pair_bound_is_the_least_over_every_pair() {
992        let region = [two_holes()];
993        let from = distance_map(&region, &[], &[p(1.0, 1.0), p(6.0, 7.5)]).unwrap();
994        let to = distance_map(&region, &[], &[p(11.0, 7.0), p(6.0, 0.5)]).unwrap();
995        let search = Search {
996            from: &from,
997            to: &to,
998            through: &region[0],
999            from_vertices: vertices(&from),
1000            to_vertices: vertices(&to),
1001            roots: &[],
1002            evaluated: HashMap::new(),
1003            upper: None,
1004        };
1005        let mut checked = 0;
1006        for i in 0..24 {
1007            for j in 0..16 {
1008                let (x, y) = (0.25 + 0.5 * f64::from(i), 0.25 + 0.5 * f64::from(j));
1009                let t = [p(x, y), p(x + 0.4, y), p(x, y + 0.4)];
1010                let origins = candidates(&search.from_vertices, &t, from.obstacles()).unwrap();
1011                let targets = candidates(&search.to_vertices, &t, to.obstacles()).unwrap();
1012                let mut brute = f64::INFINITY;
1013                for &(_, u, du, gu) in &origins {
1014                    for &(_, v, dv, gv) in &targets {
1015                        brute = brute.min(du + dv + (u - v).length().max(gu + gv));
1016                    }
1017                }
1018                assert_eq!(search.pair_bound(&t).unwrap(), brute, "{t:?}");
1019                checked += 1;
1020            }
1021        }
1022        assert_eq!(checked, 384);
1023    }
1024
1025    fn weighted_spans(map: &WeightedMap) -> Vec<Span> {
1026        map.spans()
1027            .into_iter()
1028            .map(|((a, b), least)| Span {
1029                a,
1030                b,
1031                least,
1032                solid: if a == b {
1033                    solid_corner(map.region(), map.obstacles(), a).unwrap()
1034                } else {
1035                    None
1036                },
1037            })
1038            .collect()
1039    }
1040
1041    /// In a room costing 3 all through, from one point to itself,
1042    /// `f(y) = 6 |y|`: it falls at twice the factor, and the bound must
1043    /// allow for that.
1044    #[test]
1045    fn the_weighted_lipschitz_bound_allows_the_steepest_fall() {
1046        let room = [Polygon {
1047            outer: ring(&[(-10.0, -10.0), (10.0, -10.0), (10.0, 10.0), (-10.0, 10.0)]),
1048            holes: Vec::new(),
1049        }];
1050        let costly = [crate::CostRegion::new(room[0].clone(), 3.0)];
1051        let map = crate::weighted_distance_map(&room, &[], &[p(0.0, 0.0)], &costly, 1.0).unwrap();
1052        // The distances are exact, 15 and 12, and the map holds them.
1053        for (at, d) in [(p(5.0, 0.0), 15.0), (p(4.0, 0.0), 12.0)] {
1054            let (lo, hi) = map.bracket(at).unwrap().unwrap();
1055            assert!(lo <= d && d <= hi && hi - lo < 1e-9, "[{lo}, {hi}]");
1056        }
1057        let k = map
1058            .steepest(&[p(4.0, 0.0), p(5.0, 0.0), p(5.0, 1.0)])
1059            .unwrap();
1060        assert_eq!(k, 3.0);
1061        assert_eq!(weighted_lipschitz_lower(30.0, 1.0, k), 24.0);
1062    }
1063
1064    /// No cell's pair bound exceeds a walk through a point of the cell --
1065    /// along a wall a cost region lies beyond, where the cost region
1066    /// meets every cell and costs nothing, and across a cost square's
1067    /// edges.
1068    #[test]
1069    fn the_weighted_pair_bound_is_below_every_walk_through_the_cell() {
1070        let room = [Polygon {
1071            outer: ring(&[(0.0, 0.0), (10.0, 0.0), (10.0, 4.0), (0.0, 4.0)]),
1072            holes: Vec::new(),
1073        }];
1074        let costs = [
1075            crate::CostRegion::new(
1076                Polygon {
1077                    outer: ring(&[(0.0, -1.0), (10.0, -1.0), (10.0, 0.0), (0.0, 0.0)]),
1078                    holes: Vec::new(),
1079                },
1080                3.0,
1081            ),
1082            crate::CostRegion::new(
1083                Polygon {
1084                    outer: ring(&[(4.0, 1.5), (6.0, 1.5), (6.0, 3.5), (4.0, 3.5)]),
1085                    holes: Vec::new(),
1086                },
1087                3.0,
1088            ),
1089        ];
1090        let map = |t: Point2| crate::weighted_distance_map(&room, &[], &[t], &costs, 0.5).unwrap();
1091        let (from, to) = (map(p(1.0, 2.0)), map(p(9.0, 2.0)));
1092        let (fs, ts) = (weighted_spans(&from), weighted_spans(&to));
1093        let mut checked = 0;
1094        for i in 0..20 {
1095            for j in 0..8 {
1096                let (x, y) = (0.5 * f64::from(i), 0.5 * f64::from(j));
1097                let t = [p(x, y), p(x + 0.5, y), p(x, y + 0.5)];
1098                let bound = weighted_pair_bound(&from, &fs, &ts, &t).unwrap();
1099                for c in t {
1100                    let walk =
1101                        from.bracket(c).unwrap().unwrap().1 + to.bracket(c).unwrap().unwrap().1;
1102                    assert!(bound <= walk + 1e-9, "{t:?}: {bound} above {walk} at {c:?}");
1103                }
1104                checked += 1;
1105            }
1106        }
1107        assert_eq!(checked, 160);
1108        // Along the bottom wall the cells meet the region beyond it.
1109        let t = [p(2.0, 0.0), p(2.5, 0.0), p(2.0, 0.5)];
1110        assert_eq!(from.inside_factor(&t).unwrap(), 1.0);
1111        assert_eq!(from.steepest(&t).unwrap(), 3.0);
1112        // Inside the square, clear of its edges, its factor; across an
1113        // edge, 1.
1114        assert_eq!(
1115            from.inside_factor(&[p(4.5, 2.0), p(5.0, 2.0), p(4.5, 2.5)])
1116                .unwrap(),
1117            3.0
1118        );
1119        assert_eq!(
1120            from.inside_factor(&[p(3.5, 2.0), p(4.5, 2.0), p(3.5, 2.5)])
1121                .unwrap(),
1122            1.0
1123        );
1124    }
1125
1126    /// A vertex is hidden only from a cell it sees no point of.
1127    #[test]
1128    fn a_vertex_is_hidden_only_from_the_whole_cell() {
1129        let region = [two_holes()];
1130        let map = distance_map(&region, &[], &[p(1.0, 1.0)]).unwrap();
1131        let find = |at: Point2| {
1132            vertices(&map)
1133                .into_iter()
1134                .find(|v| v.at == at)
1135                .expect("a vertex")
1136        };
1137        let hidden = |v: &Vertex, t: [Point2; 3]| {
1138            !candidates(core::slice::from_ref(v), &t, map.obstacles())
1139                .unwrap()
1140                .iter()
1141                .any(|c| c.1 == v.at)
1142        };
1143        // The origin, behind the first hole from a cell right of it.
1144        let origin = find(p(1.0, 1.0));
1145        let behind = [p(6.0, 4.5), p(6.5, 4.5), p(6.0, 5.0)];
1146        assert!(hidden(&origin, behind));
1147        // One corner of this cell pokes out below the hole's shadow.
1148        let peeking = [p(6.0, 4.5), p(6.5, 1.5), p(6.0, 5.0)];
1149        assert!(!hidden(&origin, peeking));
1150        // The hole's corner (5, 2) sees nothing strictly inside the hole's
1151        // corner wedge -- the cell above-left of it, inside the hole's
1152        // interior directions -- but does see a cell with one corner out.
1153        let corner = find(p(5.0, 2.0));
1154        assert!(corner.solid.is_some());
1155        let inside = [p(4.5, 2.5), p(4.8, 2.5), p(4.5, 2.8)];
1156        assert!(hidden(&corner, inside));
1157        let straddling = [p(4.5, 2.5), p(5.5, 2.5), p(4.5, 2.8)];
1158        assert!(!hidden(&corner, straddling));
1159        // The room's convex corner has no solid wedge.
1160        assert!(find(p(0.0, 0.0)).solid.is_none());
1161    }
1162}