Skip to content

Commit

Permalink
remove short stub edges using a priority queue
Browse files Browse the repository at this point in the history
  • Loading branch information
msbarry committed Nov 18, 2024
1 parent a7cf4f6 commit 267dd75
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,31 +33,23 @@ public static void main(String[] args) throws Exception {
lm.add(geom);
return lm.getMergedLineStrings();
});
time(" loop(0)", geom -> {
var lm = new LoopLineMerger();
lm.setLoopMinLength(0);
lm.setMinLength(0);
lm.add(geom);
return lm.getMergedLineStrings();
});
time(" loop(0.1)", geom -> {
var lm = new LoopLineMerger();
lm.setLoopMinLength(0.1);
lm.setMinLength(0.1);
lm.add(geom);
return lm.getMergedLineStrings();
});
time("loop(20.0)", geom -> {
var lm = new LoopLineMerger();
lm.setLoopMinLength(20.0);
lm.setMinLength(0.1);
lm.add(geom);
return lm.getMergedLineStrings();
});
time(" loop(0)", geom -> loopMerger(0).add(geom).getMergedLineStrings());
time(" loop(0.1)", geom -> loopMerger(0.1).add(geom).getMergedLineStrings());
time("loop(20.0)", geom -> loopMerger(20).add(geom).getMergedLineStrings());
}
System.err.println(numLines);
}

private static LoopLineMerger loopMerger(double minLength) {
var lm = new LoopLineMerger();
lm.setMinLength(minLength);
lm.setStubMinLength(minLength);
lm.setLoopMinLength(minLength);
lm.setTolerance(1);
lm.setMergeStrokes(true);
return lm;
}

private static void time(String name, FunctionThatThrows<Geometry, Collection<LineString>> fn) throws Exception {
System.err.println(String.join("\t",
name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ public static List<VectorTile.Feature> mergeLineStrings(List<VectorTile.Feature>
.setTolerance(tolerance)
.setMergeStrokes(true)
.setMinLength(lengthLimit)
.setLoopMinLength(lengthLimit);
.setLoopMinLength(lengthLimit)
.setStubMinLength(0.5);
for (VectorTile.Feature feature : groupedFeatures) {
try {
merger.add(feature.geometry().decode());
Expand All @@ -200,7 +201,7 @@ public static List<VectorTile.Feature> mergeLineStrings(List<VectorTile.Feature>
outputSegments.add(line);
}
}

if (!outputSegments.isEmpty()) {
outputSegments = sortByHilbertIndex(outputSegments);
Geometry newGeometry = GeoUtils.combineLineStrings(outputSegments);
Expand All @@ -218,7 +219,8 @@ public static List<VectorTile.Feature> mergeLineStrings(List<VectorTile.Feature>
}

public static List<VectorTile.Feature> mergeLineStringsAllParams(List<VectorTile.Feature> features,
Function<Map<String, Object>, Double> lengthLimitCalculator, double tolerance, double buffer, boolean resimplify, double loopMinLength, double stubMinLength) {
Function<Map<String, Object>, Double> lengthLimitCalculator, double tolerance, double buffer, boolean resimplify,
double loopMinLength, double stubMinLength) {
List<VectorTile.Feature> result = new ArrayList<>(features.size());
var groupedByAttrs = groupByAttrs(features, result, GeometryType.LINE);
for (List<VectorTile.Feature> groupedFeatures : groupedByAttrs) {
Expand Down Expand Up @@ -263,7 +265,7 @@ public static List<VectorTile.Feature> mergeLineStringsAllParams(List<VectorTile
outputSegments.add(line);
}
}

if (!outputSegments.isEmpty()) {
outputSegments = sortByHilbertIndex(outputSegments);
Geometry newGeometry = GeoUtils.combineLineStrings(outputSegments);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,25 +62,31 @@ public LoopLineMerger setMergeStrokes(boolean mergeStrokes) {
return this;
}

public void add(Geometry geometry) {
public LoopLineMerger add(Geometry geometry) {
geometry.apply((GeometryComponentFilter) component -> {
if (component instanceof LineString lineString) {
input.add(lineString);
}
});
return this;
}

private void degreeTwoMerge() {
for (var node : output) {
if (node.getEdges().size() == 2) {
Edge a = node.getEdges().getFirst();
Edge b = node.getEdges().get(1);
mergeTwoEdges(node, a, b);
}
degreeTwoMerge(node);
}
}

private void mergeTwoEdges(Node node, Edge a, Edge b) {
private Edge degreeTwoMerge(Node node) {
if (node.getEdges().size() == 2) {
Edge a = node.getEdges().getFirst();
Edge b = node.getEdges().get(1);
return mergeTwoEdges(node, a, b);
}
return null;
}

private Edge mergeTwoEdges(Node node, Edge a, Edge b) {
node.getEdges().remove(a);
node.getEdges().remove(b);
List<Coordinate> coordinates = new ArrayList<>();
Expand All @@ -93,6 +99,7 @@ private void mergeTwoEdges(Node node, Edge a, Edge b) {
if (a.to != b.to) {
b.to.addEdge(c.reversed);
}
return c;
}

private void strokeMerge() {
Expand Down Expand Up @@ -134,7 +141,7 @@ record HasLoop(Edge edge, double distance) {}
}
for (var other : node.getEdges()) {
double distance = other.length +
shortestDistance(other.to, current.to, current.from, loopMinLength - other.length);
shortestDistanceAStar(other.to, current.to, current.from, loopMinLength - other.length);
if (distance <= loopMinLength) {
loops.add(new HasLoop(other, distance));
}
Expand All @@ -151,30 +158,30 @@ record HasLoop(Edge edge, double distance) {}
}
}

private double shortestDistance(Node start, Node end, Node exclude, double maxLength) {
private double shortestDistanceAStar(Node start, Node end, Node exclude, double maxLength) {
Map<Integer, Double> bestDistance = new HashMap<>();
record Candidate(Node node, double cost, double heuristic) {}
PriorityQueue<Candidate> frontier = new PriorityQueue<>(Comparator.comparingDouble(Candidate::heuristic));
record Candidate(Node node, double length, double minTotalLength) {}
PriorityQueue<Candidate> frontier = new PriorityQueue<>(Comparator.comparingDouble(Candidate::minTotalLength));
if (exclude != start) {
frontier.offer(new Candidate(start, 0, start.distance(end)));
}
while (!frontier.isEmpty()) {
Candidate candidate = frontier.poll();
Node current = candidate.node;
if (current == end) {
return candidate.cost;
return candidate.length;
}

for (var edge : current.getEdges()) {
var neighbor = edge.to;
if (neighbor != exclude) {
double newDist = candidate.cost + edge.length;
double newDist = candidate.length + edge.length;
double prev = bestDistance.getOrDefault(neighbor.id, Double.POSITIVE_INFINITY);
if (newDist < prev) {
bestDistance.put(neighbor.id, newDist);
double heuristic = newDist + neighbor.distance(end);
if (heuristic <= maxLength) {
frontier.offer(new Candidate(neighbor, newDist, heuristic));
double minTotalLength = newDist + neighbor.distance(end);
if (minTotalLength <= maxLength) {
frontier.offer(new Candidate(neighbor, newDist, minTotalLength));
}
}
}
Expand All @@ -183,15 +190,30 @@ record Candidate(Node node, double cost, double heuristic) {}
return Double.POSITIVE_INFINITY;
}

private void removeShortStubEdges(double stubMinLength) {
private void removeShortStubEdges() {
PriorityQueue<Edge> toRemove = new PriorityQueue<>(Comparator.comparingDouble(Edge::length));
for (var node : output) {
for (var edge : List.copyOf(node.getEdges())) {
if (edge.length < stubMinLength &&
(edge.from.getEdges().size() == 1 || edge.to.getEdges().size() == 1 || edge.from == edge.to)) {
edge.remove();
for (var edge : node.getEdges()) {
if (isShortStubEdge(edge)) {
toRemove.offer(edge);
}
}
}
while (!toRemove.isEmpty()) {
var edge = toRemove.poll();
edge.remove();
if (degreeTwoMerge(edge.from) instanceof Edge merged && isShortStubEdge(merged)) {
toRemove.offer(merged);
}
if (degreeTwoMerge(edge.to) instanceof Edge merged && isShortStubEdge(merged)) {
toRemove.offer(merged);
}
}
}

private boolean isShortStubEdge(Edge edge) {
return edge != null && edge.main && edge.length < stubMinLength &&
(edge.from.getEdges().size() == 1 || edge.to.getEdges().size() == 1 || edge.from == edge.to);
}

private void removeShortEdges() {
Expand Down Expand Up @@ -245,13 +267,8 @@ public List<LineString> getMergedLineStrings() {
}

if (stubMinLength > 0.0) {
double step = 1.0 / precisionModel.getScale();
for (double current = step; current < stubMinLength; current += step) {
removeShortStubEdges(current);
degreeTwoMerge();
}
removeShortStubEdges(stubMinLength);
degreeTwoMerge();
removeShortStubEdges();
// removeShortStubEdges does degreeTwoMerge internally
}

if (tolerance >= 0.0) {
Expand Down Expand Up @@ -284,18 +301,9 @@ public List<LineString> getMergedLineStrings() {
private double length(List<Coordinate> edge) {
Coordinate last = null;
double length = 0;
// we only care about the length of lines if they are < the limit
// so stop counting once we exceed it
double maxLengthToTrack = Math.max(minLength, loopMinLength);
if (maxLengthToTrack <= 0) {
return Double.POSITIVE_INFINITY;
}
for (Coordinate coord : edge) {
if (last != null) {
length += last.distance(coord);
if (length > maxLengthToTrack) {
return Double.POSITIVE_INFINITY;
}
}
last = coord;
}
Expand Down Expand Up @@ -369,9 +377,9 @@ private List<List<Coordinate>> nodeLines(List<LineString> input) {
return result;
}

class Node {
int id = nodes++;
List<Edge> edge = new ArrayList<>();
private class Node {
final int id = nodes++;
final List<Edge> edge = new ArrayList<>();

void addEdge(Edge edge) {
for (Edge other : this.edge) {
Expand Down Expand Up @@ -405,15 +413,16 @@ public double distance(Node end) {
}
}

class Edge {
private class Edge {

final int id;
final Node from;
final Node to;
final double length;
final boolean main;

public int id;
Node from;
Node to;
double length;
List<Coordinate> coordinates;
boolean main;
Edge reversed;
List<Coordinate> coordinates;


private Edge(Node from, Node to, List<Coordinate> coordinateSequence, double length) {
Expand All @@ -437,10 +446,6 @@ public void remove() {
to.removeEdge(reversed);
}

public void setCoordinates(List<Coordinate> coordinates) {
this.coordinates = new ArrayList<>(coordinates);
}

double angleTo(Edge other) {
assert from.equals(other.from);
assert coordinates.size() >= 2;
Expand All @@ -451,6 +456,10 @@ public void setCoordinates(List<Coordinate> coordinates) {
return Math.abs(Angle.normalize(angle - angleOther));
}

double length() {
return length;
}

public void simplify() {
coordinates = DouglasPeuckerSimplifier.simplify(coordinates, tolerance, false);
if (reversed != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@ void testOnRealWorldData(String file, double minLengths, int expected)
var merger = new LoopLineMerger();
merger.setMinLength(minLengths);
merger.setLoopMinLength(minLengths);
merger.setStubMinLength(minLengths);
merger.setMergeStrokes(true);
merger.add(geom);
var merged = merger.getMergedLineStrings();
Set<List<Coordinate>> lines = new HashSet<>();
Expand Down

0 comments on commit 267dd75

Please sign in to comment.