Skip to main content

map_tile_toolkit/
mosaic.rs

1//! Stateful reassembly of sliced tiles back into whole features — the accumulating inverse of
2//! [`SlicerAll`](crate::SlicerAll).
3//!
4//! Feed [`Mosaic::add`] each tile's runs in its local frame; the mosaic rebases them into the global
5//! frame (`local + tile·extent`) and links geometry across borders by shared **directed edges**.
6//! [`add`](Mosaic::add) rejects a tile that is inconsistent with those already present — naming the
7//! ones it conflicts with, and leaving the mosaic unchanged — in two ways:
8//!
9//! - **Payload:** slicing duplicates a border-crossing segment identically (position **and** payload)
10//!   in every tile that carries it, so a shared edge whose vertices *disagree* is a conflict. (A single
11//!   position may be shared by several features — edges only collide when their whole segment does — so
12//!   unrelated features meeting at a point never conflict.)
13//! - **Membership:** every tile is complete in its own *core* (the cell it owns), so an edge with an
14//!   endpoint in a present tile's core must be carried by that tile. A present tile that owns an
15//!   endpoint but lacks the edge — a line spanning into a neighbor the neighbor never corroborates,
16//!   or two tiles disagreeing at a shared junction — is a conflict. A buffer only widens the overlap
17//!   two tiles share, and the whole overlap must match; any *disagreement* there surfaces here too,
18//!   because the differing vertex lands in some tile's core. Only a pure *omission* of redundant
19//!   overlap geometry goes unflagged — and that is harmless, since the core tile still supplies the
20//!   edge and duplicates collapse on reassembly. So the mosaic needs only the `extent`, never the
21//!   buffer.
22//!
23//! [`iter_features`](Mosaic::iter_features) re-chains the distinct edges into maximal polylines
24//! (`stitch`) — the same reconstruction a from-scratch merge of all tiles would produce.
25
26use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
27
28use geo_types::Coord;
29
30use crate::TileError;
31use crate::tile::{TileId, tile_of};
32use crate::vertex::Vertex;
33
34/// A global directed edge's two vertices (identical across every tile that carries it) and the set of
35/// tiles that do.
36#[derive(Debug, Clone)]
37struct Edge<V> {
38    a: V,
39    b: V,
40    tiles: BTreeSet<TileId>,
41}
42
43/// A directed edge keyed by its endpoint positions.
44type EdgeKey = (Coord<i32>, Coord<i32>);
45
46/// Reassembles tiled features back into whole features as tiles are [added](Self::add).
47///
48/// Generic over the [`Vertex`] type `V` (defaults to [`Coord<i32>`]). Needs only the `extent` the
49/// tiles were sliced at — never the buffer (see the module docs). A payload-carrying vertex (e.g.
50/// [`Measured`](crate::Measured)) additionally makes payload [`Conflict`](TileError::Conflict)s
51/// meaningful.
52#[derive(Debug, Clone)]
53pub struct Mosaic<V: Vertex = Coord<i32>> {
54    extent: u32,
55    /// Global directed-edge index, keyed by endpoint positions. Every tile at a key holds the same
56    /// vertices (conflicts are rejected on `add`).
57    edges: HashMap<EdgeKey, Edge<V>>,
58    /// The edge keys each tile contributed, for `O(tile)` purge and self-exclusion when re-adding.
59    tile_edges: BTreeMap<TileId, Vec<EdgeKey>>,
60}
61
62impl<V: Vertex> Mosaic<V> {
63    /// Create an empty mosaic for tiles sliced at `extent`.
64    ///
65    /// # Errors
66    ///
67    /// [`TileError::InvalidExtent`] if `extent` is `0` or greater than `i32::MAX`.
68    pub fn new(extent: u32) -> Result<Self, TileError> {
69        if extent == 0 || extent > i32::MAX.cast_unsigned() {
70            return Err(TileError::InvalidExtent);
71        }
72        Ok(Self {
73            extent,
74            edges: HashMap::new(),
75            tile_edges: BTreeMap::new(),
76        })
77    }
78
79    /// The extent every tile is sliced at.
80    #[must_use]
81    pub fn extent(&self) -> u32 {
82        self.extent
83    }
84
85    /// Add `tile`'s runs (in its local frame) to the mosaic. `runs` is anything sliceable to `[V]`
86    /// (e.g. `&[&[V]]` collected from a tile's features). Re-adding a tile replaces its contents. A
87    /// tile with no ≥2-vertex run contributes nothing.
88    ///
89    /// # Errors
90    ///
91    /// - [`TileError::Conflict`] if the tile disagrees with already-added tiles on a shared border
92    ///   segment. The mosaic is unchanged.
93    /// - [`TileError::Overflow`] if a vertex overflows `i32` when rebased into the global frame.
94    #[cfg_attr(feature = "hotpath", hotpath::measure)]
95    pub fn add<L: AsRef<[V]>>(&mut self, tile: TileId, runs: &[L]) -> Result<(), TileError> {
96        // `tile · extent` fits `i64`: both factors are within `i32`, so the product is below `2^62`.
97        let e = i64::from(self.extent);
98        let off_x = i64::from(tile.x) * e;
99        let off_y = i64::from(tile.y) * e;
100        // Rebase to the global frame and collect this tile's distinct directed edges. Nothing is
101        // mutated yet, so an overflow (or a later conflict) leaves the mosaic untouched.
102        let mut new_edges: HashMap<EdgeKey, (V, V)> = HashMap::new();
103        for run in runs {
104            let g = rebase(run.as_ref(), off_x, off_y)?;
105            for w in g.windows(2) {
106                let key = (w[0].position(), w[1].position());
107                if key.0 != key.1 {
108                    new_edges.entry(key).or_insert((w[0], w[1]));
109                }
110            }
111        }
112        // Conflict scan against *other* tiles (this tile's own prior data is replaced, not compared).
113        let mut conflicts = BTreeSet::new();
114        // (a) Payload conflict: another tile carries the same edge (identical positions) but different
115        // vertices — only a payload (e.g. an M value) can differ, since the key is the positions.
116        for (key, (a, b)) in &new_edges {
117            if let Some(existing) = self.edges.get(key)
118                && !(existing.a == *a && existing.b == *b)
119            {
120                conflicts.extend(existing.tiles.iter().copied().filter(|&t| t != tile));
121            }
122        }
123        // (b) Membership conflict: a tile is complete in its own core, so every edge with an endpoint
124        // in a present tile's core must be carried by that tile. A present endpoint-tile that lacks the
125        // edge means the tiles came from inconsistent data. Checked both ways so detection is
126        // order-independent. (Any *disagreement* in a buffer overlap surfaces here too — the differing
127        // vertex lands in some core — so only a harmless omission of a redundant overlap edge slips.)
128        let ext = self.extent.cast_signed();
129        // This tile must carry every already-present edge that names it as an endpoint-tile.
130        for (key, existing) in &self.edges {
131            if (tile_of(key.0, ext) == tile || tile_of(key.1, ext) == tile)
132                && !new_edges.contains_key(key)
133            {
134                conflicts.extend(existing.tiles.iter().copied().filter(|&t| t != tile));
135            }
136        }
137        // Every already-present endpoint-tile of one of this tile's edges must carry that edge.
138        for key in new_edges.keys() {
139            for endpoint in [tile_of(key.0, ext), tile_of(key.1, ext)] {
140                if endpoint != tile
141                    && self.tile_edges.contains_key(&endpoint)
142                    && !self
143                        .edges
144                        .get(key)
145                        .is_some_and(|e| e.tiles.contains(&endpoint))
146                {
147                    conflicts.insert(endpoint);
148                }
149            }
150        }
151        if !conflicts.is_empty() {
152            return Err(TileError::Conflict(conflicts.into_iter().collect()));
153        }
154        // Commit: replace any prior contents for this tile, then index the new edges.
155        self.purge(tile);
156        let mut keys = Vec::with_capacity(new_edges.len());
157        for (key, (a, b)) in new_edges {
158            self.edges
159                .entry(key)
160                .or_insert_with(|| Edge {
161                    a,
162                    b,
163                    tiles: BTreeSet::new(),
164                })
165                .tiles
166                .insert(tile);
167            keys.push(key);
168        }
169        if !keys.is_empty() {
170            self.tile_edges.insert(tile, keys);
171        }
172        Ok(())
173    }
174
175    /// Remove `tile` and all geometry it contributed. Returns whether the tile was present.
176    pub fn purge(&mut self, tile: TileId) -> bool {
177        let Some(keys) = self.tile_edges.remove(&tile) else {
178            return false;
179        };
180        for key in keys {
181            if let Some(edge) = self.edges.get_mut(&key) {
182                edge.tiles.remove(&tile);
183                if edge.tiles.is_empty() {
184                    self.edges.remove(&key);
185                }
186            }
187        }
188        true
189    }
190
191    /// Iterate every reassembled feature — geometry stitched across tile borders into maximal
192    /// polylines, in the global frame. A single iterator over the whole mosaic; deterministic order.
193    pub fn iter_features(&self) -> impl Iterator<Item = Vec<V>> {
194        let mut ordered: Vec<&Edge<V>> = self.edges.values().collect();
195        ordered.sort_unstable_by_key(|edge| {
196            let (a, b) = (edge.a.position(), edge.b.position());
197            (a.x, a.y, b.x, b.y)
198        });
199        let runs: Vec<Vec<V>> = ordered.iter().map(|edge| vec![edge.a, edge.b]).collect();
200        stitch(&runs).into_iter()
201    }
202
203    /// Whether `tile` is currently in the mosaic.
204    #[must_use]
205    pub fn contains(&self, tile: TileId) -> bool {
206        self.tile_edges.contains_key(&tile)
207    }
208
209    /// The number of tiles currently in the mosaic.
210    #[must_use]
211    pub fn len(&self) -> usize {
212        self.tile_edges.len()
213    }
214
215    /// Whether no tile has been added yet.
216    #[must_use]
217    pub fn is_empty(&self) -> bool {
218        self.tile_edges.is_empty()
219    }
220
221    /// Discard every tile, keeping the extent.
222    pub fn clear(&mut self) {
223        self.edges.clear();
224        self.tile_edges.clear();
225    }
226}
227
228/// Rebase every vertex of `run` by `(off_x, off_y)` in `i64` (the offset is `tile · extent`, below
229/// `2^62`, and a vertex adds at most another `i32`, so the sum stays within `i64`), range-checking
230/// each result back into `i32`.
231fn rebase<V: Vertex>(run: &[V], off_x: i64, off_y: i64) -> Result<Vec<V>, TileError> {
232    run.iter()
233        .map(|&v| {
234            let p = v.position();
235            Ok(v.with_position(Coord {
236                x: i32::try_from(i64::from(p.x) + off_x).map_err(|_| TileError::Overflow)?,
237                y: i32::try_from(i64::from(p.y) + off_y).map_err(|_| TileError::Overflow)?,
238            }))
239        })
240        .collect()
241}
242
243/// Rejoin overlapping runs into maximal polylines via their **directed edges** (consecutive vertex
244/// pairs, keyed by position): a border-crossing segment is the same edge in both tiles, so keeping the
245/// distinct edges drops every duplicate, and following each vertex to its outgoing edge re-chains them.
246///
247/// Order-independent and deterministic (edges/positions keep first-seen order). A simple polyline
248/// reconstructs exactly; where the geometry revisits a position, some covering chain is produced
249/// (deterministic but arbitrary).
250fn stitch<V: Vertex>(runs: &[Vec<V>]) -> Vec<Vec<V>> {
251    // Distinct directed edges, in first-seen order (dedup by endpoint positions).
252    let mut seen: HashSet<(Coord<i32>, Coord<i32>)> = HashSet::new();
253    let mut edges: Vec<(V, V)> = Vec::new();
254    for run in runs {
255        for w in run.windows(2) {
256            if seen.insert((w[0].position(), w[1].position())) {
257                edges.push((w[0], w[1]));
258            }
259        }
260    }
261    if edges.is_empty() {
262        return Vec::new();
263    }
264
265    // Adjacency: each start position → its outgoing edge indices (FIFO = first-seen). Track in-degree
266    // and first-seen position order so chain starts are found deterministically.
267    let mut out: HashMap<Coord<i32>, VecDeque<usize>> = HashMap::new();
268    let mut indeg: HashMap<Coord<i32>, usize> = HashMap::new();
269    let mut points: Vec<Coord<i32>> = Vec::new();
270    let mut known: HashSet<Coord<i32>> = HashSet::new();
271    for (i, (p, q)) in edges.iter().enumerate() {
272        let (pp, qp) = (p.position(), q.position());
273        out.entry(pp).or_default().push_back(i);
274        *indeg.entry(qp).or_insert(0) += 1;
275        indeg.entry(pp).or_insert(0);
276        for pt in [pp, qp] {
277            if known.insert(pt) {
278                points.push(pt);
279            }
280        }
281    }
282
283    let mut used = vec![false; edges.len()];
284    let mut chains: Vec<Vec<V>> = Vec::new();
285
286    // Phase 1: start a chain at each path source — a position with more outgoing than incoming edges
287    // accounts for `outdeg − indeg` chain starts (the endpoints of open polylines).
288    for &p in &points {
289        let outdeg = out.get(&p).map_or(0, VecDeque::len);
290        let ind = indeg.get(&p).copied().unwrap_or(0);
291        for _ in ind..outdeg {
292            if let Some(first) = next_unused(&mut out, p, &used) {
293                chains.push(build_chain(first, &edges, &mut used, &mut out));
294            }
295        }
296    }
297    // Phase 2: whatever edges remain form closed loops; start each at its first unused edge.
298    for i in 0..edges.len() {
299        if !used[i] {
300            chains.push(build_chain(i, &edges, &mut used, &mut out));
301        }
302    }
303    chains
304}
305
306/// Pop and return the next not-yet-used outgoing edge index from position `p`, discarding used ones
307/// from the front. `None` when `p` has no remaining unused outgoing edge. Used by [`stitch`].
308fn next_unused(
309    out: &mut HashMap<Coord<i32>, VecDeque<usize>>,
310    p: Coord<i32>,
311    used: &[bool],
312) -> Option<usize> {
313    let dq = out.get_mut(&p)?;
314    while let Some(&i) = dq.front() {
315        if used[i] {
316            dq.pop_front();
317        } else {
318            return dq.pop_front();
319        }
320    }
321    None
322}
323
324/// Follow outgoing edges from edge `first`, consuming each, until a position has no unused outgoing
325/// edge; return the vertex chain `[start, …]`. Used by [`stitch`].
326fn build_chain<V: Vertex>(
327    first: usize,
328    edges: &[(V, V)],
329    used: &mut [bool],
330    out: &mut HashMap<Coord<i32>, VecDeque<usize>>,
331) -> Vec<V> {
332    let mut chain = vec![edges[first].0];
333    let mut cur = first;
334    loop {
335        used[cur] = true;
336        let q = edges[cur].1;
337        chain.push(q);
338        match next_unused(out, q.position(), used) {
339            Some(next) => cur = next,
340            None => break,
341        }
342    }
343    chain
344}