diff --git a/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkLineMerge.java b/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkLineMerge.java index b468dd18ce..7979937162 100644 --- a/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkLineMerge.java +++ b/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkLineMerge.java @@ -2,86 +2,111 @@ import com.onthegomap.planetiler.geo.GeoUtils; import com.onthegomap.planetiler.util.Format; +import com.onthegomap.planetiler.util.Gzip; import com.onthegomap.planetiler.util.LoopLineMerger; +import com.onthegomap.planetiler.util.Try; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.MathContext; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Random; import java.util.function.Function; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateXY; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKBReader; import org.locationtech.jts.operation.linemerge.LineMerger; public class BenchmarkLineMerge { + private static int numLines; - public static void main(String[] args) { + public static void main(String[] args) throws Exception { for (int i = 0; i < 10; i++) { - System.err.println( - " JTS line merger (/s):\t" + - timeJts(10, 10) + "\t" + - timeJts(10, 100) + "\t" + - timeJts(1000, 10) + "\t" + - timeJts(1000, 1000)); - System.err.println( - "loop line merger (/s):\t" + - timeLoop(10, 10) + "\t" + - timeLoop(10, 100) + "\t" + - timeLoop(1000, 10)); - // TODO currently this does not finish: - // + "\t" + timeLoop(1000, 1000)); - System.err.println(); + time(" JTS", geom -> { + var lm = new LineMerger(); + 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(); + }); + System.out.println(numLines); + numLines = 0; } } - private static String timeLoop(int lines, int parts) { - return time(lines, parts, geom -> { - var merger = new LoopLineMerger(); - merger.add(geom); - merger.setLoopMinLength(0.1); - merger.setMinLength(0.1); - return merger.getMergedLineStrings(); - }); + private static void time(String name, Function> fn) throws Exception { + System.err.println(String.join("\t", + name, + timeMillis(() -> fn.apply(read("mergelines_200433_lines.wkb.gz"))), + timeMillis(() -> fn.apply(read("mergelines_239823_lines.wkb.gz"))), + "(/s):", + timePerSec(() -> fn.apply(read("mergelines_1759_point_line.wkb.gz"))), + timePerSec(() -> fn.apply(makeLines(50, 2))), + timePerSec(() -> fn.apply(makeLines(10, 10))), + timePerSec(() -> fn.apply(makeLines(2, 50))) + )); } - private static String timeJts(int lines, int parts) { - return time(lines, parts, geom -> { - var merger = new LineMerger(); - merger.add(geom); - return merger.getMergedLineStrings(); - }); - } - - private static String time(int lines, int parts, Function> fn) { - Geometry multiLinestring = makeLines(lines, parts); + private static String timePerSec(Try.SupplierThatThrows> fn) throws Exception { long start = System.nanoTime(); long end = start + Duration.ofSeconds(1).toNanos(); int num = 0; for (; System.nanoTime() < end;) { - fn.apply(multiLinestring); + numLines += fn.get().size(); num++; } return Format.defaultInstance() .numeric(Math.round(num * 1d / ((System.nanoTime() - start) * 1d / Duration.ofSeconds(1).toNanos())), true); } + private static String timeMillis(Try.SupplierThatThrows> fn) throws Exception { + long start = System.nanoTime(); + long end = start + Duration.ofSeconds(1).toNanos(); + int num = 0; + for (; System.nanoTime() < end;) { + numLines += fn.get().size(); + num++; + } + // equivalent of toPrecision(3) + long nanosPer = (System.nanoTime() - start) / num; + var bd = new BigDecimal(nanosPer, new MathContext(3)); + return Format.padRight(Duration.ofNanos(bd.longValue()).toString().replace("PT", ""), 6); + } + + + private static Geometry read(String fileName) throws IOException, ParseException { + var path = Path.of("planetiler-core", "src", "test", "resources", "mergelines", fileName); + byte[] bytes = Gzip.gunzip(Files.readAllBytes(path)); + return new WKBReader().read(bytes); + } + private static Geometry makeLines(int lines, int parts) { List result = new ArrayList<>(); - var random = new Random(); + double idx = 0; for (int i = 0; i < lines; i++) { Coordinate[] coords = new Coordinate[parts]; - double startX = random.nextInt(100); - double startY = random.nextInt(100); - double endX = random.nextInt(100); - double endY = random.nextInt(100); for (int j = 0; j < parts; j++) { - coords[j] = new CoordinateXY( - j * (endX - startX) / parts + startX, - j * (endY - startY) / parts + startY - ); + coords[j] = new CoordinateXY(idx, idx); + idx += 0.5; } result.add(GeoUtils.JTS_FACTORY.createLineString(coords)); } diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/LoopLineMergerTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/LoopLineMergerTest.java index 6d3bd9fe0e..13c969af4d 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/LoopLineMergerTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/LoopLineMergerTest.java @@ -1,11 +1,21 @@ package com.onthegomap.planetiler.util; + import static com.onthegomap.planetiler.TestUtils.newLineString; import static com.onthegomap.planetiler.TestUtils.newPoint; import static org.junit.jupiter.api.Assertions.assertEquals; +import com.onthegomap.planetiler.TestUtils; +import com.onthegomap.planetiler.geo.GeoUtils; +import java.io.IOException; +import java.nio.file.Files; import java.util.List; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.PrecisionModel; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKBReader; class LoopLineMergerTest { @@ -162,11 +172,7 @@ void testMerge() { .setLoopMinLength(100); /** - * 10 20 30 40 - * 10 o--o--o - * \ | - * \| - * 20 o--o + * 10 20 30 40 10 o--o--o \ | \| 20 o--o */ merger.add(newLineString( 10, 10, @@ -197,11 +203,7 @@ void testMerge() { .setLoopMinLength(0.001); /** - * 10 20 30 40 - * 10 o--o--o - * \ | - * \| - * 20 o--o + * 10 20 30 40 10 o--o--o \ | \| 20 o--o */ merger.add(newLineString( 10, 10, @@ -250,7 +252,7 @@ void testMerge() { merger.getMergedLineStrings() ); - // removes first short stubs, merges again, and only then + // removes first short stubs, merges again, and only then // removes non-stubs that are too short // stub = linestring that has at least one disconnected end merger = new LoopLineMerger() @@ -258,10 +260,7 @@ void testMerge() { .setLoopMinLength(-1); /** - * 0 20 30 50 - * 0 o----o--o----o - * | | - * 10 o o + * 0 20 30 50 0 o----o--o----o | | 10 o o */ merger.add(newLineString(0, 0, 20, 0)); merger.add(newLineString(20, 0, 30, 0)); @@ -308,11 +307,8 @@ void testHasPointAppearingMoreThanTwice() { void testFindAllPaths() { // finds all paths and orders them by length var merger = new LoopLineMerger(); - /** 10 20 30 - * 10 o-----o - * |\ | - * | \ | - * 20 o--o--o + /** + * 10 20 30 10 o-----o |\ | | \ | 20 o--o--o */ merger.add(newLineString(10, 10, 30, 10)); merger.add(newLineString(10, 10, 10, 20)); @@ -344,5 +340,32 @@ void testFindAllPaths() { allPaths.get(2) ); } - + + + @ParameterizedTest + @CsvSource({ + "mergelines_1759_point_line.wkb.gz,0,4", + "mergelines_1759_point_line.wkb.gz,1,2", + + "mergelines_200433_lines.wkb.gz,0,35160", + "mergelines_200433_lines.wkb.gz,0.1,22403", + "mergelines_200433_lines.wkb.gz,1,1516", + + "mergelines_239823_lines.wkb.gz,0,19632", + "mergelines_239823_lines.wkb.gz,0.1,13861", + "mergelines_239823_lines.wkb.gz,1,1585", + }) + void testOnRealWorldData(String file, double minLengths, int expected) + throws IOException, ParseException { + // finds all paths and orders them by length + Geometry geom = new WKBReader(GeoUtils.JTS_FACTORY).read( + Gzip.gunzip(Files.readAllBytes(TestUtils.pathToResource("mergelines").resolve(file)))); + var merger = new LoopLineMerger(); + merger.setMinLength(minLengths); + merger.setLoopMinLength(minLengths); + merger.add(geom); + merger.getMergedLineStrings(); + assertEquals(expected, merger.getMergedLineStrings().size()); + } + } diff --git a/planetiler-core/src/test/resources/mergelines/mergelines_1759_point_line.wkb.gz b/planetiler-core/src/test/resources/mergelines/mergelines_1759_point_line.wkb.gz new file mode 100644 index 0000000000..8813e1c180 Binary files /dev/null and b/planetiler-core/src/test/resources/mergelines/mergelines_1759_point_line.wkb.gz differ diff --git a/planetiler-core/src/test/resources/mergelines/mergelines_200433_lines.wkb.gz b/planetiler-core/src/test/resources/mergelines/mergelines_200433_lines.wkb.gz new file mode 100644 index 0000000000..eb0ee1fd58 Binary files /dev/null and b/planetiler-core/src/test/resources/mergelines/mergelines_200433_lines.wkb.gz differ diff --git a/planetiler-core/src/test/resources/mergelines/mergelines_239823_lines.wkb.gz b/planetiler-core/src/test/resources/mergelines/mergelines_239823_lines.wkb.gz new file mode 100644 index 0000000000..5a863f85c5 Binary files /dev/null and b/planetiler-core/src/test/resources/mergelines/mergelines_239823_lines.wkb.gz differ