diff --git a/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkOsmRead.java b/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkOsmRead.java index a3a703f2..091975c9 100644 --- a/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkOsmRead.java +++ b/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkOsmRead.java @@ -15,10 +15,12 @@ import java.nio.file.Path; public class BenchmarkOsmRead { public static void main(String[] args) throws IOException { - OsmInputFile file = new OsmInputFile(Path.of("data/sources/northeast.osm.pbf"), true); var profile = new Profile.NullProfile(); var stats = Stats.inMemory(); - var config = PlanetilerConfig.from(Arguments.of()); + var parsedArgs = Arguments.fromArgsOrConfigFile(args); + var config = PlanetilerConfig.from(parsedArgs); + var path = parsedArgs.inputFile("osm_path", "path to osm file", Path.of("data/sources/northeast.osm.pbf")); + OsmInputFile file = new OsmInputFile(path, config.osmLazyReads()); while (true) { Timer timer = Timer.start(); diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/config/PlanetilerConfig.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/config/PlanetilerConfig.java index 026b20df..1f20fc20 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/config/PlanetilerConfig.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/config/PlanetilerConfig.java @@ -43,7 +43,8 @@ public record PlanetilerConfig( double simplifyToleranceBelowMaxZoom, boolean osmLazyReads, boolean compactDb, - boolean skipFilledTiles + boolean skipFilledTiles, + int tileWarningSizeBytes ) { public static final int MIN_MINZOOM = 0; @@ -148,7 +149,10 @@ public record PlanetilerConfig( true), arguments.getBoolean("skip_filled_tiles", "Skip writing tiles containing only polygon fills to the output", - false) + false), + (int) (arguments.getDouble("tile_warning_size_mb", + "Maximum size in megabytes of a tile to emit a warning about", + 1d) * 1024 * 1024) ); } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/mbtiles/Mbtiles.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/mbtiles/Mbtiles.java index 8438755e..eb80dd9c 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/mbtiles/Mbtiles.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/mbtiles/Mbtiles.java @@ -756,13 +756,17 @@ public final class Mbtiles implements Closeable { public Metadata setMetadata(String name, Object value) { if (value != null) { - LOGGER.debug("Set mbtiles metadata: {}={}", name, value); + String stringValue = value.toString(); + LOGGER.debug("Set mbtiles metadata: {}={}", name, + stringValue.length() > 1_000 ? + (stringValue.substring(0, 1_000) + "... " + (stringValue.length() - 1_000) + " more characters") : + stringValue); try ( PreparedStatement statement = connection.prepareStatement( "INSERT INTO " + METADATA_TABLE + " (" + METADATA_COL_NAME + "," + METADATA_COL_VALUE + ") VALUES(?, ?);") ) { statement.setString(1, name); - statement.setString(2, value.toString()); + statement.setString(2, stringValue); statement.execute(); } catch (SQLException throwables) { LOGGER.error("Error setting metadata " + name + "=" + value, throwables); diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/mbtiles/MbtilesWriter.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/mbtiles/MbtilesWriter.java index e7dc807a..31b52e40 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/mbtiles/MbtilesWriter.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/mbtiles/MbtilesWriter.java @@ -283,7 +283,7 @@ public class MbtilesWriter { } else { encoded = en.encode(); bytes = gzip(encoded); - if (encoded.length > 1_000_000) { + if (encoded.length > config.tileWarningSizeBytes()) { LOGGER.warn("{} {}kb uncompressed", tileFeatures.tileCoord(), encoded.length / 1024); diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/OsmElement.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/OsmElement.java index 7e14a9cd..7bf59242 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/OsmElement.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/OsmElement.java @@ -20,6 +20,8 @@ public interface OsmElement extends WithTags { /** OSM element ID */ long id(); + Info info(); + int cost(); enum Type { @@ -31,12 +33,13 @@ public interface OsmElement extends WithTags { /** An un-handled element read from the .osm.pbf file (i.e. file header). */ record Other( @Override long id, - @Override Map tags + @Override Map tags, + @Override Info info ) implements OsmElement { @Override public int cost() { - return 1 + tags.size(); + return 1 + tags.size() + (info == null ? 0 : Info.COST); } } @@ -48,6 +51,7 @@ public interface OsmElement extends WithTags { private final Map tags; private final double lat; private final double lon; + private final Info info; // bailed out of a record to make encodedLocation lazy since it is fairly expensive to compute private long encodedLocation = MISSING_LOCATION; @@ -55,16 +59,27 @@ public interface OsmElement extends WithTags { long id, Map tags, double lat, - double lon + double lon, + Info info ) { this.id = id; this.tags = tags; this.lat = lat; this.lon = lon; + this.info = info; } public Node(long id, double lat, double lon) { - this(id, new HashMap<>(), lat, lon); + this(id, new HashMap<>(), lat, lon, null); + } + + public Node( + long id, + Map tags, + double lat, + double lon + ) { + this(id, tags, lat, lon, null); } @Override @@ -72,6 +87,11 @@ public interface OsmElement extends WithTags { return id; } + @Override + public Info info() { + return info; + } + @Override public Map tags() { return tags; @@ -94,7 +114,7 @@ public interface OsmElement extends WithTags { @Override public int cost() { - return 1 + tags.size(); + return 1 + tags.size() + (info == null ? 0 : Info.COST); } @Override @@ -109,12 +129,13 @@ public interface OsmElement extends WithTags { return this.id == that.id && Objects.equals(this.tags, that.tags) && Double.doubleToLongBits(this.lat) == Double.doubleToLongBits(that.lat) && - Double.doubleToLongBits(this.lon) == Double.doubleToLongBits(that.lon); + Double.doubleToLongBits(this.lon) == Double.doubleToLongBits(that.lon) && + Objects.equals(this.info, that.info); } @Override public int hashCode() { - return Objects.hash(id, tags, lat, lon); + return Objects.hash(id, tags, lat, lon, info); } @Override @@ -123,7 +144,8 @@ public interface OsmElement extends WithTags { "id=" + id + ", " + "tags=" + tags + ", " + "lat=" + lat + ", " + - "lon=" + lon + ']'; + "lon=" + lon + ", " + + "info=" + info + ']'; } } @@ -132,16 +154,21 @@ public interface OsmElement extends WithTags { record Way( @Override long id, @Override Map tags, - LongArrayList nodes + LongArrayList nodes, + @Override Info info ) implements OsmElement { public Way(long id) { - this(id, new HashMap<>(), new LongArrayList(5)); + this(id, new HashMap<>(), new LongArrayList(5), null); + } + + public Way(long id, Map tags, LongArrayList nodes) { + this(id, tags, nodes, null); } @Override public int cost() { - return 1 + tags.size() + nodes.size(); + return 1 + tags.size() + nodes.size() + (info == null ? 0 : Info.COST); } } @@ -149,11 +176,16 @@ public interface OsmElement extends WithTags { record Relation( @Override long id, @Override Map tags, - List members + List members, + @Override Info info ) implements OsmElement { public Relation(long id) { - this(id, new HashMap<>(), new ArrayList<>()); + this(id, new HashMap<>(), new ArrayList<>(), null); + } + + public Relation(long id, Map tags, List members) { + this(id, tags, members, null); } public Relation { @@ -164,7 +196,7 @@ public interface OsmElement extends WithTags { @Override public int cost() { - return 1 + tags.size() + members.size() * 3; + return 1 + tags.size() + members.size() * 3 + (info == null ? 0 : Info.COST); } /** @@ -176,4 +208,8 @@ public interface OsmElement extends WithTags { String role ) {} } + + record Info(long changeset, long timestamp, int userId, int version, String user) { + private static final int COST = 2; + } } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/OsmReader.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/OsmReader.java index 2ea7e6f8..92ee62b1 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/OsmReader.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/OsmReader.java @@ -621,17 +621,20 @@ public class OsmReader implements Closeable, MemoryEstimator.HasEstimate { * A source feature generated from OSM elements. Inferring the geometry can be expensive, so each subclass is * constructed with the inputs necessary to create the geometry, but the geometry is constructed lazily on read. */ - private abstract class OsmFeature extends SourceFeature { + private abstract class OsmFeature extends SourceFeature implements OsmSourceFeature { + private final OsmElement originalElement; private final boolean polygon; private final boolean line; private final boolean point; private Geometry latLonGeom; private Geometry worldGeom; + public OsmFeature(OsmElement elem, boolean point, boolean line, boolean polygon, List> relationInfo) { super(elem.tags(), name, null, relationInfo, elem.id()); + this.originalElement = elem; this.point = point; this.line = line; this.polygon = polygon; @@ -663,6 +666,11 @@ public class OsmReader implements Closeable, MemoryEstimator.HasEstimate { public boolean canBePolygon() { return polygon; } + + @Override + public OsmElement originalElement() { + return originalElement; + } } /** A {@link Point} created from an OSM node. */ diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/OsmSourceFeature.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/OsmSourceFeature.java new file mode 100644 index 00000000..cf893913 --- /dev/null +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/OsmSourceFeature.java @@ -0,0 +1,5 @@ +package com.onthegomap.planetiler.reader.osm; + +public interface OsmSourceFeature { + OsmElement originalElement(); +} diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/PbfDecoder.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/PbfDecoder.java index bceafdae..9663c321 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/PbfDecoder.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/PbfDecoder.java @@ -149,7 +149,8 @@ public class PbfDecoder implements Iterable { node.getId(), buildTags(node.getKeysCount(), node::getKeys, node::getVals), fieldDecoder.decodeLatitude(node.getLat()), - fieldDecoder.decodeLongitude(node.getLon()) + fieldDecoder.decodeLongitude(node.getLon()), + parseInfo(node.getInfo()) ); } } @@ -198,7 +199,8 @@ public class PbfDecoder implements Iterable { return new OsmElement.Relation( relation.getId(), buildTags(relation.getKeysCount(), relation::getKeys, relation::getVals), - members + members, + parseInfo(relation.getInfo()) ); } } @@ -241,27 +243,40 @@ public class PbfDecoder implements Iterable { return new OsmElement.Way( way.getId(), buildTags(way.getKeysCount(), way::getKeys, way::getVals), - wayNodesList + wayNodesList, + parseInfo(way.getInfo()) ); } } + private OsmElement.Info parseInfo(Osmformat.Info info) { + return info == null ? null : new OsmElement.Info( + info.getChangeset(), + info.getTimestamp(), + info.getUid(), + info.getVersion(), + fieldDecoder.decodeString(info.getUserSid()) + ); + } + private class DenseNodeIterator implements Iterator { final Osmformat.DenseNodes nodes; - long nodeId; - long latitude; - long longitude; - int i; - int kvIndex; + final Osmformat.DenseInfo denseInfo; + long nodeId = 0; + long latitude = 0; + long longitude = 0; + int i = 0; + int kvIndex = 0; + // info + long timestamp = 0; + long changeset = 0; + int uid = 0; + int userSid = 0; public DenseNodeIterator(Osmformat.DenseNodes nodes) { this.nodes = nodes; - nodeId = 0; - latitude = 0; - longitude = 0; - i = 0; - kvIndex = 0; + this.denseInfo = nodes.getDenseinfo(); } @@ -279,6 +294,16 @@ public class PbfDecoder implements Iterable { nodeId += nodes.getId(i); latitude += nodes.getLat(i); longitude += nodes.getLon(i); + int version = 0; + + if (denseInfo != null) { + version = denseInfo.getVersion(i); + timestamp += denseInfo.getTimestamp(i); + changeset += denseInfo.getChangeset(i); + uid += denseInfo.getUid(i); + userSid += denseInfo.getUserSid(i); + } + i++; // Build the tags. The key and value string indexes are sequential @@ -304,7 +329,14 @@ public class PbfDecoder implements Iterable { nodeId, tags == null ? Collections.emptyMap() : tags, ((double) latitude) / 10000000, - ((double) longitude) / 10000000 + ((double) longitude) / 10000000, + denseInfo == null ? null : new OsmElement.Info( + changeset, + timestamp, + uid, + version, + fieldDecoder.decodeString(userSid) + ) ); } } diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/reader/osm/OsmInputFileTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/reader/osm/OsmInputFileTest.java index c6ca2278..a56b5462 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/reader/osm/OsmInputFileTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/reader/osm/OsmInputFileTest.java @@ -24,7 +24,7 @@ class OsmInputFileTest { private final OsmElement.Node expectedNode = new OsmElement.Node(1737114566L, Map.of( "highway", "crossing", "crossing", "zebra" - ), 43.7409723, 7.4303278); + ), 43.7409723, 7.4303278, new OsmElement.Info(0, 1600807207, 0, 5, "")); private final OsmElement.Way expectedWay = new OsmElement.Way(4097656L, Map.of( "name", "Avenue Princesse Alice", "lanes", "2", @@ -35,7 +35,7 @@ class OsmInputFileTest { ), LongArrayList.from( 21912089L, 7265761724L, 1079750744L, 2104793864L, 6340961560L, 1110560507L, 21912093L, 6340961559L, 21912095L, 7265762803L, 2104793866L, 6340961561L, 5603088200L, 6340961562L, 21912097L, 21912099L - )); + ), new OsmElement.Info(0, 1583398246, 0, 13, "")); private final OsmElement.Relation expectedRel = new OsmElement.Relation(7360630L, Map.of( "local_ref", "Saint-Roman", "public_transport:version", "2", @@ -51,7 +51,7 @@ class OsmInputFileTest { new OsmElement.Relation.Member(OsmElement.Type.NODE, 3465728159L, "stop"), new OsmElement.Relation.Member(OsmElement.Type.NODE, 4939122068L, "platform"), new OsmElement.Relation.Member(OsmElement.Type.NODE, 3805333988L, "stop") - )); + ), new OsmElement.Info(0, 1586074405, 0, 2, "")); private final Envelope expectedBounds = new Envelope(7.409205, 7.448637, 43.72335, 43.75169); @Test diff --git a/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java b/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java index aa8b3380..36ee7180 100644 --- a/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java +++ b/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java @@ -1,11 +1,14 @@ package com.onthegomap.planetiler; +import static java.util.Map.entry; + import com.onthegomap.planetiler.basemap.BasemapMain; import com.onthegomap.planetiler.basemap.util.VerifyMonaco; import com.onthegomap.planetiler.benchmarks.BasemapMapping; import com.onthegomap.planetiler.benchmarks.LongLongMapBench; import com.onthegomap.planetiler.custommap.ConfiguredMapMain; import com.onthegomap.planetiler.examples.BikeRouteOverlay; +import com.onthegomap.planetiler.examples.OsmQaTiles; import com.onthegomap.planetiler.examples.ToiletsOverlay; import com.onthegomap.planetiler.examples.ToiletsOverlayLowLevelApi; import com.onthegomap.planetiler.mbtiles.Verify; @@ -20,17 +23,19 @@ import java.util.Map; public class Main { private static final EntryPoint DEFAULT_TASK = BasemapMain::main; - private static final Map ENTRY_POINTS = Map.of( - "generate-basemap", BasemapMain::main, - "generate-custom", ConfiguredMapMain::main, - "basemap", BasemapMain::main, - "example-bikeroutes", BikeRouteOverlay::main, - "example-toilets", ToiletsOverlay::main, - "example-toilets-lowlevel", ToiletsOverlayLowLevelApi::main, - "benchmark-mapping", BasemapMapping::main, - "benchmark-longlongmap", LongLongMapBench::main, - "verify-mbtiles", Verify::main, - "verify-monaco", VerifyMonaco::main + private static final Map ENTRY_POINTS = Map.ofEntries( + entry("generate-basemap", BasemapMain::main), + entry("generate-custom", ConfiguredMapMain::main), + entry("basemap", BasemapMain::main), + entry("example-bikeroutes", BikeRouteOverlay::main), + entry("example-toilets", ToiletsOverlay::main), + entry("example-toilets-lowlevel", ToiletsOverlayLowLevelApi::main), + entry("example-qa", OsmQaTiles::main), + entry("osm-qa", OsmQaTiles::main), + entry("benchmark-mapping", BasemapMapping::main), + entry("benchmark-longlongmap", LongLongMapBench::main), + entry("verify-mbtiles", Verify::main), + entry("verify-monaco", VerifyMonaco::main) ); public static void main(String[] args) throws Exception { diff --git a/planetiler-examples/src/main/java/com/onthegomap/planetiler/examples/OsmQaTiles.java b/planetiler-examples/src/main/java/com/onthegomap/planetiler/examples/OsmQaTiles.java new file mode 100644 index 00000000..44df7001 --- /dev/null +++ b/planetiler-examples/src/main/java/com/onthegomap/planetiler/examples/OsmQaTiles.java @@ -0,0 +1,107 @@ +package com.onthegomap.planetiler.examples; + +import com.onthegomap.planetiler.FeatureCollector; +import com.onthegomap.planetiler.Planetiler; +import com.onthegomap.planetiler.Profile; +import com.onthegomap.planetiler.config.Arguments; +import com.onthegomap.planetiler.reader.SourceFeature; +import com.onthegomap.planetiler.reader.osm.OsmElement; +import com.onthegomap.planetiler.reader.osm.OsmSourceFeature; +import java.nio.file.Path; + +/** + * Generates tiles with a raw copy of OSM data in a single "osm" layer at one zoom level, similar to + * OSM QA Tiles. + *

+ * Nodes are mapped to points and ways are mapped to polygons or linestrings, and multipolygon relations are mapped to + * polygons. Each output feature contains all key/value tags from the input feature, plus these extra attributes: + *

    + *
  • {@code @type}: node, way, or relation
  • + *
  • {@code @id}: OSM element ID
  • + *
  • {@code @changeset}: Changeset that last modified the element
  • + *
  • {@code @timestamp}: Timestamp at which the element was last modified
  • + *
  • {@code @version}: Version number of the OSM element
  • + *
  • {@code @uid}: User ID that last modified the element
  • + *
  • {@code @user}: User name that last modified the element
  • + *
+ *

+ * To run this example: + *

    + *
  1. build the examples: {@code mvn clean package}
  2. + *
  3. then run this example: + * {@code java -cp target/*-fatjar.jar com.onthegomap.planetiler.examples.OsmQaTiles --area=monaco --download}
  4. + *
  5. then run the demo tileserver: {@code tileserver-gl-light data/output.mbtiles}
  6. + *
  7. and view the output at localhost:8080
  8. + *
+ */ +public class OsmQaTiles implements Profile { + + public static void main(String[] args) throws Exception { + run(Arguments.fromArgsOrConfigFile(args)); + } + + static void run(Arguments inArgs) throws Exception { + int zoom = inArgs.getInteger("zoom", "zoom level to generate tiles at", 12); + var args = inArgs.orElse(Arguments.of( + "minzoom", zoom, + "maxzoom", zoom, + "tile_warning_size_mb", 100 + )); + String area = args.getString("area", "geofabrik area to download", "monaco"); + Planetiler.create(args) + .setProfile(new OsmQaTiles()) + .addOsmSource("osm", + Path.of("data", "sources", area + ".osm.pbf"), + "planet".equalsIgnoreCase(area) ? "aws:latest" : ("geofabrik:" + area) + ) + .overwriteOutput("mbtiles", Path.of("data", "qa.mbtiles")) + .run(); + } + + @Override + public void processFeature(SourceFeature sourceFeature, FeatureCollector features) { + if (!sourceFeature.tags().isEmpty() && sourceFeature instanceof OsmSourceFeature osmFeature) { + var feature = sourceFeature.canBePolygon() ? features.polygon("osm") : + sourceFeature.canBeLine() ? features.line("osm") : + sourceFeature.isPoint() ? features.point("osm") : + null; + if (feature != null) { + var element = osmFeature.originalElement(); + feature + .setMinPixelSize(0) + .setPixelTolerance(0) + .setBufferPixels(0); + for (var entry : sourceFeature.tags().entrySet()) { + feature.setAttr(entry.getKey(), entry.getValue()); + } + feature + .setAttr("@id", sourceFeature.id()) + .setAttr("@type", element instanceof OsmElement.Node ? "node" : + element instanceof OsmElement.Way ? "way" : + element instanceof OsmElement.Relation ? "relation" : null + ); + var info = element.info(); + if (info != null) { + feature + .setAttr("@version", info.version() == 0 ? null : info.version()) + .setAttr("@timestamp", info.timestamp() == 0L ? null : info.timestamp()) + .setAttr("@changeset", info.changeset() == 0L ? null : info.changeset()) + .setAttr("@uid", info.userId() == 0 ? null : info.userId()) + .setAttr("@user", info.user() == null || info.user().isBlank() ? null : info.user()); + } + } + } + } + + @Override + public String name() { + return "osm qa"; + } + + @Override + public String attribution() { + return """ + © OpenStreetMap contributors + """.trim(); + } +} diff --git a/planetiler-examples/src/test/java/com/onthegomap/planetiler/examples/OsmQaTilesTest.java b/planetiler-examples/src/test/java/com/onthegomap/planetiler/examples/OsmQaTilesTest.java new file mode 100644 index 00000000..a1b62eaa --- /dev/null +++ b/planetiler-examples/src/test/java/com/onthegomap/planetiler/examples/OsmQaTilesTest.java @@ -0,0 +1,138 @@ +package com.onthegomap.planetiler.examples; + +import static com.onthegomap.planetiler.TestUtils.assertContains; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.onthegomap.planetiler.TestUtils; +import com.onthegomap.planetiler.config.Arguments; +import com.onthegomap.planetiler.geo.GeoUtils; +import com.onthegomap.planetiler.mbtiles.Mbtiles; +import com.onthegomap.planetiler.reader.SourceFeature; +import com.onthegomap.planetiler.reader.osm.OsmElement; +import com.onthegomap.planetiler.reader.osm.OsmSourceFeature; +import java.nio.file.Path; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; + +class OsmQaTilesTest { + + private final OsmQaTiles profile = new OsmQaTiles(); + + @Test + void testNode() { + Map tags = Map.of("key", "value"); + class TestNode extends SourceFeature implements OsmSourceFeature { + TestNode() { + super(tags, "source", "layer", null, 0); + } + + @Override + public Geometry latLonGeometry() { + return null; + } + + @Override + public Geometry worldGeometry() { + return TestUtils.newPoint( + 0.5, 0.5 + ); + } + + @Override + public boolean isPoint() { + return true; + } + + @Override + public boolean canBePolygon() { + return false; + } + + @Override + public boolean canBeLine() { + return false; + } + + @Override + public OsmElement originalElement() { + return new OsmElement.Node(123, tags, 1, 1, new OsmElement.Info(1, 2, 3, 4, "user")); + } + } + + var node = new TestNode(); + var mapFeatures = TestUtils.processSourceFeature(node, profile); + + // verify output attributes + assertEquals(1, mapFeatures.size()); + var feature = mapFeatures.get(0); + assertEquals("osm", feature.getLayer()); + assertEquals(Map.of( + "key", "value", + "@changeset", 1L, + "@timestamp", 2L, + "@id", 0L, + "@type", "node", + "@uid", 3, + "@user", "user", + "@version", 4 + ), feature.getAttrsAtZoom(12)); + assertEquals(0, feature.getBufferPixelsAtZoom(12), 1e-2); + } + + @Test + void integrationTest(@TempDir Path tmpDir) throws Exception { + Path dbPath = tmpDir.resolve("output.mbtiles"); + OsmQaTiles.run(Arguments.of( + // Override input source locations + "osm_path", TestUtils.pathToResource("monaco-latest.osm.pbf"), + // Override temp dir location + "tmp", tmpDir.toString(), + // Override output location + "mbtiles", dbPath.toString() + )); + try (Mbtiles mbtiles = Mbtiles.newReadOnlyDatabase(dbPath)) { + Map metadata = mbtiles.metadata().getAll(); + assertEquals("osm qa", metadata.get("name")); + assertContains("openstreetmap.org/copyright", metadata.get("attribution")); + + TestUtils + .assertNumFeatures(mbtiles, "osm", 12, Map.of( + "bus", "yes", + "name", "Crémaillère", + "public_transport", "stop_position", + "@type", "node", + "@version", 4L, + "@id", 1699777833L + ), GeoUtils.WORLD_LAT_LON_BOUNDS, 1, Point.class); + TestUtils + .assertNumFeatures(mbtiles, "osm", 12, Map.of( + "boundary", "administrative", + "admin_level", "10", + "name", "Monte-Carlo", + "wikipedia", "fr:Monte-Carlo", + "ISO3166-2", "MC-MC", + "type", "boundary", + "wikidata", "Q45240", + "@type", "relation", + "@version", 9L, + "@id", 5986438L + ), GeoUtils.WORLD_LAT_LON_BOUNDS, 1, Polygon.class); + TestUtils + .assertNumFeatures(mbtiles, "osm", 12, Map.of( + "name", "Avenue de la Costa", + "maxspeed", "50", + "lit", "yes", + "surface", "asphalt", + "lanes", "2", + "@type", "way", + "@version", 5L, + "@id", 166009791L + ), GeoUtils.WORLD_LAT_LON_BOUNDS, 1, LineString.class); + } + } +}