Skip to main content

topology_checker_core/
merge.rs

1use crate::{
2    Error, Layer,
3    engine::{
4        cluster::{Cluster, distance},
5        index,
6        line_graph::projection,
7    },
8    model::Invalid,
9};
10use geo::{BoundingRect, Coord, Geometry, LineString};
11use std::collections::BTreeMap;
12
13/// Report a line utility input by position. Inputs receive sequential FIDs, so
14/// only the coordinate check can fail.
15fn line_error(invalid: Invalid) -> Error {
16    match invalid {
17        Invalid::NonFinite(fid) | Invalid::DuplicateFid(fid) => Error::NonFiniteLine {
18            index: usize::try_from(fid).unwrap_or_default(),
19        },
20    }
21}
22
23/// Merge original line parts through degree-two endpoint joins, preserving interior junctions.
24///
25/// This independent Rust implementation uses a chain pass followed by a cycle pass.
26/// Each original part is visited once. Index construction and contact queries are separate
27/// from the linear traversal. The input coordinates are never modified in place.
28/// Tolerance uses planar input coordinate units. Consecutive duplicate coordinates
29/// are removed; degenerate parts are skipped. Output order follows the earliest
30/// contributing part. Direction follows the majority of original part directions,
31/// with ties resolved by the earliest part. Output carries no feature provenance.
32///
33/// # Errors
34/// Returns [`Error::Tolerance`] for negative or non-finite tolerance and
35/// [`Error::NonFiniteLine`] for non-finite input coordinates.
36pub fn merge_lines(
37    lines: impl IntoIterator<Item = LineString>,
38    tolerance: f64,
39) -> Result<Vec<LineString>, Error> {
40    if !tolerance.is_finite() || tolerance < 0. {
41        return Err(Error::Tolerance(tolerance));
42    }
43    let layer = Layer::from_geometries("lines", lines.into_iter().map(Geometry::LineString));
44    let parts = layer.parts().map_err(line_error)?;
45    let lines = parts.lines.iter().map(|p| &p.geometry).collect::<Vec<_>>();
46    let mut cluster = Cluster::new(tolerance);
47    let edges = lines
48        .iter()
49        .map(|l| [cluster.insert(l.0[0]), cluster.insert(l.0[l.0.len() - 1])])
50        .collect::<Vec<_>>();
51    let mut incident = vec![Vec::new(); cluster.coords.len()];
52    for (i, edge) in edges.iter().enumerate() {
53        incident[edge[0]].push((i, true));
54        incident[edge[1]].push((i, false));
55    }
56    let segments = lines
57        .iter()
58        .enumerate()
59        .flat_map(|(part, l)| {
60            l.lines()
61                .enumerate()
62                .map(move |(position, line)| (part, position, line))
63        })
64        .collect::<Vec<_>>();
65    let tree = index::build(
66        segments
67            .iter()
68            .enumerate()
69            .map(|(i, (_, _, line))| (i, line.bounding_rect())),
70    );
71    let mut blocked = vec![false; incident.len()];
72    for (part, line) in lines.iter().enumerate() {
73        for (end, &point) in [line.0[0], line.0[line.0.len() - 1]].iter().enumerate() {
74            let node = edges[part][end];
75            for item in tree.locate_in_envelope_intersecting(&index::at(point, tolerance)) {
76                let (owner, position, segment) = segments[item.data];
77                let (_, projected) = projection(segment, point);
78                if distance(point, projected) > tolerance {
79                    continue;
80                }
81                // A true endpoint contact belongs in the endpoint graph. Interior contacts
82                // block merging, including a vertex between two segments of a part.
83                let at_start = position == 0 && edges[owner][0] == node;
84                let at_end = position + 2 == lines[owner].0.len() && edges[owner][1] == node;
85                if !at_start && !at_end {
86                    blocked[node] = true;
87                }
88            }
89        }
90    }
91    let mut visited = vec![false; edges.len()];
92    let mut outputs = Vec::new();
93    let walk = |start: usize, forward: bool, visited: &mut [bool]| {
94        let mut coords = Vec::new();
95        let mut current = (start, forward);
96        let mut forwards = 0;
97        let mut backwards = 0;
98        let mut minimum = start;
99        let mut minimum_forward = forward;
100        loop {
101            let (id, direction) = current;
102            if visited[id] {
103                break;
104            }
105            visited[id] = true;
106            if id < minimum {
107                minimum = id;
108                minimum_forward = direction;
109            }
110            if direction {
111                forwards += 1;
112            } else {
113                backwards += 1;
114            }
115            let first = edges[id][usize::from(!direction)];
116            let last = edges[id][usize::from(direction)];
117            let part = &lines[id].0;
118            // Reserve the whole append so endpoint pushes cannot trigger a second growth.
119            coords.reserve(part.len() - usize::from(!coords.is_empty()));
120            if coords.is_empty() {
121                coords.push(cluster.coords[first]);
122            }
123            let interior = &part[1..part.len() - 1];
124            if direction {
125                coords.extend(interior.iter().copied());
126            } else {
127                coords.extend(interior.iter().rev().copied());
128            }
129            coords.push(cluster.coords[last]);
130            if blocked[last] || incident[last].len() != 2 {
131                break;
132            }
133            let Some(next) = incident[last].iter().copied().find(|(id, _)| !visited[*id]) else {
134                break;
135            };
136            current = next;
137        }
138        if backwards > forwards || (backwards == forwards && !minimum_forward) {
139            coords.reverse();
140        }
141        coords.dedup();
142        (minimum, LineString(coords))
143    };
144    for (node, uses) in incident.iter().enumerate() {
145        if uses.len() == 2 && !blocked[node] {
146            continue;
147        }
148        for &(id, forward) in uses {
149            if !visited[id] {
150                outputs.push(walk(id, forward, &mut visited));
151            }
152        }
153    }
154    for id in 0..edges.len() {
155        if !visited[id] {
156            outputs.push(walk(id, true, &mut visited));
157        }
158    }
159    outputs.sort_by_key(|(id, _)| *id);
160    Ok(outputs
161        .into_iter()
162        .map(|(_, line)| line)
163        .filter(|l| l.0.len() >= 2)
164        .collect())
165}
166
167/// Explode nondegenerate parts into individual segments, preserving duplicate uses.
168/// Consecutive duplicate coordinates are removed; remaining segments retain input
169/// part order and direction. Output coordinates use the original planar units and
170/// carry no feature provenance.
171///
172/// # Errors
173/// Returns [`Error::NonFiniteLine`] for non-finite input coordinates.
174pub fn explode_lines(
175    lines: impl IntoIterator<Item = LineString>,
176) -> Result<Vec<LineString>, Error> {
177    let layer = Layer::from_geometries("lines", lines.into_iter().map(Geometry::LineString));
178    Ok(layer
179        .parts()
180        .map_err(line_error)?
181        .lines
182        .iter()
183        .flat_map(|p| p.geometry.lines())
184        .map(LineString::from)
185        .collect())
186}
187
188/// Walk a subset of noded edges. Overlap geometry is merged independently per feature pair.
189pub(crate) fn merge_edges(coords: &[Coord], edges: &[[usize; 2]]) -> Vec<LineString> {
190    let mut incident: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
191    for (id, edge) in edges.iter().enumerate() {
192        for &node in edge {
193            incident.entry(node).or_default().push(id);
194        }
195    }
196    let mut visited = vec![false; edges.len()];
197    let mut output = Vec::new();
198    let walk = |start: usize, node: usize, visited: &mut [bool]| {
199        let mut current = start;
200        let mut node = node;
201        let mut path = vec![coords[node]];
202        loop {
203            if visited[current] {
204                break;
205            }
206            visited[current] = true;
207            let edge = edges[current];
208            node = if edge[0] == node { edge[1] } else { edge[0] };
209            path.push(coords[node]);
210            let uses = &incident[&node];
211            if uses.len() != 2 {
212                break;
213            }
214            let Some(next) = uses.iter().copied().find(|id| !visited[*id]) else {
215                break;
216            };
217            current = next;
218        }
219        if let (Some(first), Some(last)) = (path.first(), path.last())
220            && (last.x.total_cmp(&first.x).then(last.y.total_cmp(&first.y))).is_lt()
221        {
222            path.reverse();
223        }
224        LineString(path)
225    };
226    for (&node, uses) in &incident {
227        if uses.len() != 2 {
228            for &id in uses {
229                if !visited[id] {
230                    output.push(walk(id, node, &mut visited));
231                }
232            }
233        }
234    }
235    for id in 0..edges.len() {
236        if !visited[id] {
237            output.push(walk(id, edges[id][0], &mut visited));
238        }
239    }
240    output
241}