Skip to content

Commit

Permalink
Progressive stub removal, angle merge after simplification (#18)
Browse files Browse the repository at this point in the history
* Progressive stub removal, angle merge after simplification

* Review fixes
  • Loading branch information
wipfli authored Nov 11, 2024
1 parent 5f98e85 commit c123593
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ public static List<VectorTile.Feature> mergeLineStrings(List<VectorTile.Feature>
List<LineString> outputSegments = new ArrayList<>();
var i = 0;
for (Object merged : merger.getMergedLineStrings()) {
if (merged instanceof LineString line && line.getLength() >= lengthLimit) {
if (merged instanceof LineString line) {
// TODO remove debug features comment
// Map<String, Object> attrs = new HashMap<>();
// attrs.put("idx", i++);
Expand All @@ -216,10 +216,23 @@ public static List<VectorTile.Feature> mergeLineStrings(List<VectorTile.Feature>
}
}
}

merger = new LoopLineMerger();
for (var outputSegment : outputSegments) {
merger.add(outputSegment);
}
outputSegments = merger.getMergedByAngle();

if (!outputSegments.isEmpty()) {
outputSegments = sortByHilbertIndex(outputSegments);
Geometry newGeometry = GeoUtils.combineLineStrings(outputSegments);
result.add(feature1.copyWithNewGeometry(newGeometry));
// i = 0;
// for (var outputSegment : outputSegments) {
// Map<String, Object> attrs = new HashMap<>();
// attrs.put("idx", ++i);
// result.add(feature1.copyWithNewGeometry(outputSegment).copyWithExtraAttrs(attrs));
// }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import org.locationtech.jts.algorithm.Angle;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateXY;
import org.locationtech.jts.geom.Geometry;
Expand All @@ -15,6 +16,7 @@
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.PrecisionModel;


public class LoopLineMerger {
final List<LineString> input = new ArrayList<>();
private final List<Node> output = new ArrayList<>();
Expand Down Expand Up @@ -75,16 +77,48 @@ private void merge() {
if (node.getEdges().size() == 2) {
Edge a = node.getEdges().getFirst();
Edge b = node.getEdges().get(1);
node.getEdges().clear();
List<Coordinate> coordinates = new ArrayList<>();
coordinates.addAll(a.coordinates.reversed());
coordinates.addAll(b.coordinates.subList(1, b.coordinates.size()));
Edge c = new Edge(a.to, b.to, coordinates, a.length + b.length);
a.to.removeEdge(a.reversed);
b.to.removeEdge(b.reversed);
a.to.addEdge(c);
if (a.to != b.to) {
b.to.addEdge(c.reversed);
mergeTwoEdges(a, b);
}
}
}

private void mergeTwoEdges(Edge a, Edge b) {
assert a.to == b.from;
a.to.getEdges().remove(a);
b.from.getEdges().remove(b);
List<Coordinate> coordinates = new ArrayList<>();
coordinates.addAll(a.coordinates.reversed());
coordinates.addAll(b.coordinates.subList(1, b.coordinates.size()));
Edge c = new Edge(a.to, b.to, coordinates, a.length + b.length);
a.to.removeEdge(a.reversed);
b.to.removeEdge(b.reversed);
a.to.addEdge(c);
if (a.to != b.to) {
b.to.addEdge(c.reversed);
}
}

private void mergeByAngle() {
for (var node : output) {
List<Edge> edges = List.copyOf(node.getEdges());
if (edges.size() >= 3) {
record AngledPair(Edge a, Edge b, double angle) {}
List<AngledPair> angledPairs = new ArrayList<>();
for (var i = 0; i < edges.size(); ++i) {
for (var j = i + 1; j < edges.size(); ++j) {
double angle = edges.get(i).angleTo(edges.get(j));
angledPairs.add(new AngledPair(edges.get(i), edges.get(j), angle));
}
}
angledPairs.sort(Comparator.comparingDouble(angledPair -> angledPair.angle));
List<Edge> merged = new ArrayList<>();
for (var angledPair : angledPairs.reversed()) {
if (merged.contains(angledPair.a) || merged.contains(angledPair.b)) {
continue;
}
mergeTwoEdges(angledPair.a, angledPair.b);
merged.add(angledPair.a);
merged.add(angledPair.b);
}
}
}
Expand Down Expand Up @@ -152,10 +186,10 @@ record Candidate(Node node, double cost, double heuristic) {}
return Double.POSITIVE_INFINITY;
}

private void removeShortStubEdges() {
private void removeShortStubEdges(double stubMinLength) {
for (var node : output) {
for (var edge : List.copyOf(node.getEdges())) {
if (edge.length < minLength &&
if (edge.length < stubMinLength &&
(edge.from.getEdges().size() == 1 || edge.to.getEdges().size() == 1 || edge.from == edge.to)) {
edge.remove();
}
Expand Down Expand Up @@ -185,12 +219,37 @@ public List<LineString> getMergedLineStrings() {
}

if (minLength > 0.0) {
removeShortStubEdges();
merge();
removeShortEdges();
double step = 1.0 / precisionModel.getScale();
for (double stubMinLength = 0.0; stubMinLength < minLength; stubMinLength += step) {
removeShortStubEdges(stubMinLength);
merge();
}
removeShortStubEdges(minLength);
merge();
// minLength = 10 * 0.0625;
// removeShortEdges();
// merge();
}

List<LineString> result = new ArrayList<>();

for (var node : output) {
for (var edge : node.getEdges()) {
if (edge.main) {
result.add(factory.createLineString(edge.coordinates.toArray(Coordinate[]::new)));
}
}
}

return result;
}

public List<LineString> getMergedByAngle() {
List<List<Coordinate>> edges = nodeLines(input);
buildNodes(edges);

mergeByAngle();

List<LineString> result = new ArrayList<>();

for (var node : output) {
Expand Down Expand Up @@ -360,6 +419,16 @@ public void remove() {
to.removeEdge(reversed);
}

double angleTo(Edge other) {
assert from.equals(other.from);
assert coordinates.size() >= 2;

double angle = Angle.angle(coordinates.get(0), coordinates.get(1));
double angleOther = Angle.angle(other.coordinates.get(0), other.coordinates.get(1));

return Math.abs(Angle.normalize(angle - angleOther));
}

@Override
public String toString() {
return "Edge{" + from.id + "->" + to.id + (main ? "" : "(R)") + ": [" + coordinates.getFirst() + ".." +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.junit.jupiter.params.provider.CsvSource;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKBReader;
import org.locationtech.jts.operation.linemerge.LineMerger;
Expand Down Expand Up @@ -83,6 +84,24 @@ void testSplitLinestringsBeforeMerging() {
);
}

@Test
void testProgressiveStubRemoval() {
var merger = new LoopLineMerger()
.setMinLength(4)
.setPrecisionModel(new PrecisionModel(-1.0));

merger.add(newLineString(0, 0, 5, 0)); // stub length 5
merger.add(newLineString(5, 0, 6, 0)); // mid piece
merger.add(newLineString(6, 0, 8, 0)); // stub length 2
merger.add(newLineString(5, 0, 5, 1)); // stub length 1
merger.add(newLineString(6, 0, 6, 1)); // stub length 1

assertEquals(
List.of(newLineString(8, 0, 6, 0, 5, 0, 0, 0)),
merger.getMergedLineStrings()
);
}

@Test
void testRoundCoordinatesBeforeMerging() {
var merger = new LoopLineMerger()
Expand Down Expand Up @@ -130,6 +149,19 @@ void testRemoveSmallLoops() {
);
}

@Test
void testRemoveSelfClosingLoops() {
var merger = new LoopLineMerger()
.setMinLength(-1)
.setLoopMinLength(10);

merger.add(newLineString(1, 0, 1, 1, 1, 2, 0, 2, 0, 1, 1, 1, 2, 1));
assertEquals(
List.of(newLineString(1, 0, 1, 1, 2, 1)),
merger.getMergedLineStrings()
);
}

@Test
void testDoNotRemoveLargeLoops() {
var merger = new LoopLineMerger()
Expand Down

0 comments on commit c123593

Please sign in to comment.