diff --git a/core/src/main/java/com/onthegomap/flatmap/FeatureCollector.java b/core/src/main/java/com/onthegomap/flatmap/FeatureCollector.java index 61687f52..030a0cd2 100644 --- a/core/src/main/java/com/onthegomap/flatmap/FeatureCollector.java +++ b/core/src/main/java/com/onthegomap/flatmap/FeatureCollector.java @@ -35,7 +35,7 @@ public class FeatureCollector implements Iterable { } public Feature geometry(String layer, Geometry geometry) { - Feature feature = new Feature(layer, geometry); + Feature feature = new Feature(layer, geometry, source.id()); output.add(feature); return feature; } @@ -49,7 +49,7 @@ public class FeatureCollector implements Iterable { } catch (GeometryException e) { stats.dataError("feature_point_" + e.stat()); LOGGER.warn("Error getting point geometry for " + source + ": " + e.getMessage()); - return new Feature(layer, EMPTY_GEOM); + return new Feature(layer, EMPTY_GEOM, source.id()); } } @@ -59,7 +59,7 @@ public class FeatureCollector implements Iterable { } catch (GeometryException e) { stats.dataError("feature_centroid_" + e.stat()); LOGGER.warn("Error getting centroid for " + source + ": " + e.getMessage()); - return new Feature(layer, EMPTY_GEOM); + return new Feature(layer, EMPTY_GEOM, source.id()); } } @@ -69,7 +69,7 @@ public class FeatureCollector implements Iterable { } catch (GeometryException e) { stats.dataError("feature_line_" + e.stat()); LOGGER.warn("Error constructing line for " + source + ": " + e.getMessage()); - return new Feature(layer, EMPTY_GEOM); + return new Feature(layer, EMPTY_GEOM, source.id()); } } @@ -79,7 +79,7 @@ public class FeatureCollector implements Iterable { } catch (GeometryException e) { stats.dataError("feature_polygon_" + e.stat()); LOGGER.warn("Error constructing polygon for " + source + ": " + e.getMessage()); - return new Feature(layer, EMPTY_GEOM); + return new Feature(layer, EMPTY_GEOM, source.id()); } } @@ -89,7 +89,7 @@ public class FeatureCollector implements Iterable { } catch (GeometryException e) { stats.dataError("feature_validated_polygon_" + e.stat()); LOGGER.warn("Error constructing validated polygon for " + source + ": " + e.getMessage()); - return new Feature(layer, EMPTY_GEOM); + return new Feature(layer, EMPTY_GEOM, source.id()); } } @@ -99,7 +99,7 @@ public class FeatureCollector implements Iterable { } catch (GeometryException e) { stats.dataError("feature_point_on_surface_" + e.stat()); LOGGER.warn("Error constructing point on surface for " + source + ": " + e.getMessage()); - return new Feature(layer, EMPTY_GEOM); + return new Feature(layer, EMPTY_GEOM, source.id()); } } @@ -118,6 +118,7 @@ public class FeatureCollector implements Iterable { private final Geometry geom; private final Map attrs = new TreeMap<>(); private final GeometryType geometryType; + private final long sourceId; private int zOrder; private int minzoom = config.minzoom(); private int maxzoom = config.maxzoom(); @@ -134,11 +135,16 @@ public class FeatureCollector implements Iterable { private double pixelToleranceAtMaxZoom = 256d / 4096; private ZoomFunction pixelTolerance = null; - private Feature(String layer, Geometry geom) { + private Feature(String layer, Geometry geom, long sourceId) { this.layer = layer; this.geom = geom; this.zOrder = 0; this.geometryType = GeometryType.valueOf(geom); + this.sourceId = sourceId; + } + + public long sourceId() { + return sourceId; } public int getZorder() { diff --git a/core/src/main/java/com/onthegomap/flatmap/Parse.java b/core/src/main/java/com/onthegomap/flatmap/Parse.java index 98e84dae..8c711d28 100644 --- a/core/src/main/java/com/onthegomap/flatmap/Parse.java +++ b/core/src/main/java/com/onthegomap/flatmap/Parse.java @@ -109,4 +109,18 @@ public class Parse { (Parse.boolInt(tags.get("bridge")) * 10L); return Math.abs(z) < 10_000 ? (int) z : 0; } + + public static Double parseDoubleOrNull(Object value) { + if (value instanceof Number num) { + return num.doubleValue(); + } + if (value == null) { + return null; + } + try { + return Double.parseDouble(value.toString()); + } catch (NumberFormatException e) { + return null; + } + } } diff --git a/core/src/main/java/com/onthegomap/flatmap/Profile.java b/core/src/main/java/com/onthegomap/flatmap/Profile.java index bf8af09f..aedc4b98 100644 --- a/core/src/main/java/com/onthegomap/flatmap/Profile.java +++ b/core/src/main/java/com/onthegomap/flatmap/Profile.java @@ -5,6 +5,7 @@ import com.graphhopper.reader.ReaderRelation; import com.onthegomap.flatmap.geo.GeometryException; import com.onthegomap.flatmap.read.OpenStreetMapReader; import java.util.List; +import java.util.function.Consumer; public interface Profile { @@ -44,6 +45,10 @@ public interface Profile { return true; } + default void finish(String sourceName, FeatureCollector.Factory featureCollectors, + Consumer next) { + } + class NullProfile implements Profile { @Override diff --git a/core/src/main/java/com/onthegomap/flatmap/SourceFeature.java b/core/src/main/java/com/onthegomap/flatmap/SourceFeature.java index 6659cf00..768f3a30 100644 --- a/core/src/main/java/com/onthegomap/flatmap/SourceFeature.java +++ b/core/src/main/java/com/onthegomap/flatmap/SourceFeature.java @@ -14,11 +14,11 @@ public abstract class SourceFeature { private final Map properties; private final String source; private final String sourceLayer; - private final List> relationInfos; + private final List> relationInfos; private final long id; protected SourceFeature(Map properties, String source, String sourceLayer, - List> relationInfos, long id) { + List> relationInfos, long id) { this.properties = properties; this.source = source; this.sourceLayer = sourceLayer; diff --git a/core/src/main/java/com/onthegomap/flatmap/geo/GeoUtils.java b/core/src/main/java/com/onthegomap/flatmap/geo/GeoUtils.java index 84a8f3ac..1cf3fb15 100644 --- a/core/src/main/java/com/onthegomap/flatmap/geo/GeoUtils.java +++ b/core/src/main/java/com/onthegomap/flatmap/geo/GeoUtils.java @@ -134,7 +134,7 @@ public class GeoUtils { } public static double decodeWorldX(long encoded) { - return ((double) (encoded >> 32)) / QUANTIZED_WORLD_SIZE; + return ((double) (encoded >>> 32)) / QUANTIZED_WORLD_SIZE; } public static double getZoomFromLonLatBounds(Envelope envelope) { diff --git a/core/src/main/java/com/onthegomap/flatmap/read/OpenStreetMapReader.java b/core/src/main/java/com/onthegomap/flatmap/read/OpenStreetMapReader.java index 554c67c4..e9f48931 100644 --- a/core/src/main/java/com/onthegomap/flatmap/read/OpenStreetMapReader.java +++ b/core/src/main/java/com/onthegomap/flatmap/read/OpenStreetMapReader.java @@ -37,6 +37,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateList; import org.locationtech.jts.geom.CoordinateSequence; @@ -169,12 +170,7 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima ReaderElement readerElement; var featureCollectors = new FeatureCollector.Factory(config, stats); NodeLocationProvider nodeCache = newNodeGeometryCache(); - var encoder = writer.newRenderedFeatureEncoder(); - FeatureRenderer renderer = new FeatureRenderer( - config, - rendered -> next.accept(encoder.apply(rendered)), - stats - ); + FeatureRenderer renderer = getFeatureRenderer(writer, config, next); while ((readerElement = prev.get()) != null) { SourceFeature feature = null; if (readerElement instanceof ReaderNode node) { @@ -220,9 +216,23 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima .addTopologyStats(topology); topology.awaitAndLog(logger, config.logInterval()); + + profile.finish(name, + new FeatureCollector.Factory(config, stats), + getFeatureRenderer(writer, config, writer)); timer.stop(); } + private FeatureRenderer getFeatureRenderer(FeatureGroup writer, CommonParams config, + Consumer next) { + var encoder = writer.newRenderedFeatureEncoder(); + return new FeatureRenderer( + config, + rendered -> next.accept(encoder.apply(rendered)), + stats + ); + } + SourceFeature processRelationPass2(ReaderRelation rel, NodeLocationProvider nodeCache) { return rel.hasTag("type", "multipolygon") ? new MultipolygonSourceFeature(rel, nodeCache) : null; } @@ -237,7 +247,7 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima boolean closed = nodes.size() > 1 && nodes.get(0) == nodes.get(nodes.size() - 1); String area = way.getTag("area"); LongArrayList relationIds = wayToRelations.get(way.getId()); - List> rels = null; + List> rels = null; if (!relationIds.isEmpty()) { rels = new ArrayList<>(relationIds.size()); for (int r = 0; r < relationIds.size(); r++) { @@ -332,7 +342,7 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima final boolean point; public ProxyFeature(ReaderElement elem, boolean point, boolean line, boolean polygon, - List> relationInfo) { + List> relationInfo) { super(ReaderElementUtils.getProperties(elem), name, null, relationInfo, elem.getId()); this.point = point; this.line = line; @@ -407,7 +417,7 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima private final LongArrayList nodeIds; public WaySourceFeature(ReaderWay way, boolean closed, String area, NodeLocationProvider nodeCache, - List> relationInfo) { + List> relationInfo) { super(way, false, (!closed || !"yes".equals(area)) && way.getNodes().size() >= 2, (closed && !"no".equals(area)) && way.getNodes().size() >= 4, diff --git a/core/src/main/java/com/onthegomap/flatmap/read/OsmMultipolygon.java b/core/src/main/java/com/onthegomap/flatmap/read/OsmMultipolygon.java index ab7404eb..73c4af64 100644 --- a/core/src/main/java/com/onthegomap/flatmap/read/OsmMultipolygon.java +++ b/core/src/main/java/com/onthegomap/flatmap/read/OsmMultipolygon.java @@ -15,8 +15,10 @@ package com.onthegomap.flatmap.read; import com.carrotsearch.hppc.LongArrayList; import com.carrotsearch.hppc.LongObjectMap; +import com.carrotsearch.hppc.ObjectIntMap; import com.carrotsearch.hppc.cursors.LongObjectCursor; import com.graphhopper.coll.GHLongObjectHashMap; +import com.graphhopper.coll.GHObjectIntHashMap; import com.onthegomap.flatmap.geo.GeoUtils; import com.onthegomap.flatmap.geo.GeometryException; import java.util.ArrayList; @@ -35,7 +37,7 @@ import org.locationtech.jts.geom.prep.PreparedPolygon; * This class is ported to Java from https://github.com/omniscale/imposm3/blob/master/geom/multipolygon.go and * https://github.com/omniscale/imposm3/blob/master/geom/ring.go */ -class OsmMultipolygon { +public class OsmMultipolygon { private static final double MIN_CLOSE_RING_GAP = 0.1 / GeoUtils.WORLD_CIRCUMFERENCE_METERS; private static final Comparator BY_AREA_DESCENDING = Comparator.comparingDouble(ring -> -ring.area); @@ -68,10 +70,39 @@ class OsmMultipolygon { } } + public static Geometry build(List rings) throws GeometryException { + ObjectIntMap coordToId = new GHObjectIntHashMap<>(); + List idToCoord = new ArrayList<>(); + int id = 0; + List idRings = new ArrayList<>(rings.size()); + for (CoordinateSequence coords : rings) { + LongArrayList idRing = new LongArrayList(coords.size()); + idRings.add(idRing); + for (Coordinate coord : coords.toCoordinateArray()) { + if (!coordToId.containsKey(coord)) { + coordToId.put(coord, id); + idToCoord.add(coord); + id++; + } + idRing.add(coordToId.get(coord)); + } + } + return build(idRings, lookupId -> idToCoord.get((int) lookupId), 0, MIN_CLOSE_RING_GAP); + } + public static Geometry build( List rings, OpenStreetMapReader.NodeLocationProvider nodeCache, long osmId + ) throws GeometryException { + return build(rings, nodeCache, osmId, MIN_CLOSE_RING_GAP); + } + + public static Geometry build( + List rings, + OpenStreetMapReader.NodeLocationProvider nodeCache, + long osmId, + double minGap ) throws GeometryException { try { if (rings.size() == 0) { @@ -83,7 +114,7 @@ class OsmMultipolygon { for (LongArrayList segment : idSegments) { int size = segment.size(); long firstId = segment.get(0), lastId = segment.get(size - 1); - if (firstId == lastId || tryClose(segment, nodeCache)) { + if (firstId == lastId || tryClose(segment, nodeCache, minGap)) { CoordinateSequence coordinates = nodeCache.getWayGeometry(segment); Polygon poly = GeoUtils.JTS_FACTORY.createPolygon(coordinates); polygons.add(new Ring(poly)); @@ -142,12 +173,13 @@ class OsmMultipolygon { return shells; } - private static boolean tryClose(LongArrayList segment, OpenStreetMapReader.NodeLocationProvider nodeCache) { + private static boolean tryClose(LongArrayList segment, OpenStreetMapReader.NodeLocationProvider nodeCache, + double minGap) { int size = segment.size(); long firstId = segment.get(0); Coordinate firstCoord = nodeCache.getCoordinate(firstId); Coordinate lastCoord = nodeCache.getCoordinate(segment.get(size - 1)); - if (firstCoord.distance(lastCoord) <= MIN_CLOSE_RING_GAP) { + if (firstCoord.distance(lastCoord) <= minGap) { segment.set(size - 1, firstId); return true; } diff --git a/core/src/main/java/com/onthegomap/flatmap/read/Reader.java b/core/src/main/java/com/onthegomap/flatmap/read/Reader.java index 70988f24..0a4d6568 100644 --- a/core/src/main/java/com/onthegomap/flatmap/read/Reader.java +++ b/core/src/main/java/com/onthegomap/flatmap/read/Reader.java @@ -12,6 +12,8 @@ import com.onthegomap.flatmap.render.FeatureRenderer; import com.onthegomap.flatmap.worker.Topology; import java.io.Closeable; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import org.jetbrains.annotations.NotNull; import org.locationtech.jts.geom.Envelope; public abstract class Reader implements Closeable { @@ -40,12 +42,7 @@ public abstract class Reader implements Closeable { .addWorker("process", threads, (prev, next) -> { SourceFeature sourceFeature; var featureCollectors = new FeatureCollector.Factory(config, stats); - var encoder = writer.newRenderedFeatureEncoder(); - FeatureRenderer renderer = new FeatureRenderer( - config, - rendered -> next.accept(encoder.apply(rendered)), - stats - ); + FeatureRenderer renderer = getFeatureRenderer(writer, config, next); while ((sourceFeature = prev.get()) != null) { featuresRead.incrementAndGet(); FeatureCollector features = featureCollectors.get(sourceFeature); @@ -71,9 +68,25 @@ public abstract class Reader implements Closeable { .addTopologyStats(topology); topology.awaitAndLog(loggers, config.logInterval()); + + profile.finish(sourceName, + new FeatureCollector.Factory(config, stats), + getFeatureRenderer(writer, config, writer) + ); timer.stop(); } + @NotNull + private FeatureRenderer getFeatureRenderer(FeatureGroup writer, CommonParams config, + Consumer next) { + var encoder = writer.newRenderedFeatureEncoder(); + return new FeatureRenderer( + config, + rendered -> next.accept(encoder.apply(rendered)), + stats + ); + } + public abstract long getCount(); public abstract Topology.SourceStep read(); diff --git a/core/src/main/java/com/onthegomap/flatmap/read/ReaderFeature.java b/core/src/main/java/com/onthegomap/flatmap/read/ReaderFeature.java index 7ea08df9..8ab1b3be 100644 --- a/core/src/main/java/com/onthegomap/flatmap/read/ReaderFeature.java +++ b/core/src/main/java/com/onthegomap/flatmap/read/ReaderFeature.java @@ -3,6 +3,7 @@ package com.onthegomap.flatmap.read; import com.onthegomap.flatmap.SourceFeature; import com.onthegomap.flatmap.geo.GeoUtils; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import org.locationtech.jts.geom.Geometry; @@ -21,15 +22,20 @@ public class ReaderFeature extends SourceFeature { public ReaderFeature(Geometry latLonGeometry, Map properties, String source, String sourceLayer, long id) { - super(properties, source, sourceLayer, null, id); - this.latLonGeometry = latLonGeometry; - this.properties = properties; + this(latLonGeometry, properties, source, sourceLayer, id, null); } public ReaderFeature(Geometry latLonGeometry, int numProperties, String source, String sourceLayer, long id) { this(latLonGeometry, new HashMap<>(numProperties), source, sourceLayer, id); } + public ReaderFeature(Geometry latLonGeometry, Map properties, String source, String sourceLayer, + long id, List> relations) { + super(properties, source, sourceLayer, relations, id); + this.latLonGeometry = latLonGeometry; + this.properties = properties; + } + @Override public Geometry latLonGeometry() { return latLonGeometry; diff --git a/core/src/main/java/com/onthegomap/flatmap/render/FeatureRenderer.java b/core/src/main/java/com/onthegomap/flatmap/render/FeatureRenderer.java index b2c8dd43..8b281e07 100644 --- a/core/src/main/java/com/onthegomap/flatmap/render/FeatureRenderer.java +++ b/core/src/main/java/com/onthegomap/flatmap/render/FeatureRenderer.java @@ -88,7 +88,7 @@ public class FeatureRenderer implements Consumer { double buffer = feature.getBufferPixelsAtZoom(zoom) / 256; int tilesAtZoom = 1 << zoom; TileExtents.ForZoom extents = config.extents().getForZoom(zoom); - TiledGeometry tiled = TiledGeometry.slicePointsIntoTiles(extents, buffer, zoom, coords); + TiledGeometry tiled = TiledGeometry.slicePointsIntoTiles(extents, buffer, zoom, coords, feature.sourceId()); RenderedFeature.Group groupInfo = null; if (hasLabelGrid && coords.length == 1) { @@ -163,7 +163,7 @@ public class FeatureRenderer implements Consumer { List> groups = CoordinateSequenceExtractor.extractGroups(geom, minSize); double buffer = feature.getBufferPixelsAtZoom(z) / 256; TileExtents.ForZoom extents = config.extents().getForZoom(z); - TiledGeometry sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents); + TiledGeometry sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents, feature.sourceId()); writeTileFeatures(z, id, feature, sliced); } diff --git a/core/src/main/java/com/onthegomap/flatmap/render/TiledGeometry.java b/core/src/main/java/com/onthegomap/flatmap/render/TiledGeometry.java index 62253073..ba40eab5 100644 --- a/core/src/main/java/com/onthegomap/flatmap/render/TiledGeometry.java +++ b/core/src/main/java/com/onthegomap/flatmap/render/TiledGeometry.java @@ -46,6 +46,7 @@ class TiledGeometry { private static final double NEIGHBOR_BUFFER_EPS = 0.1d / 4096; private final Map>> tileContents = new HashMap<>(); + private final long id; private Map filledRanges = null; private final TileExtents.ForZoom extents; private final double buffer; @@ -54,7 +55,8 @@ class TiledGeometry { private final boolean area; private final int max; - private TiledGeometry(TileExtents.ForZoom extents, double buffer, int z, boolean area) { + private TiledGeometry(TileExtents.ForZoom extents, double buffer, int z, boolean area, long id) { + this.id = id; this.extents = extents; this.buffer = buffer; // make sure we inspect neighboring tiles when a line runs along an edge @@ -65,8 +67,8 @@ class TiledGeometry { } public static TiledGeometry slicePointsIntoTiles(TileExtents.ForZoom extents, double buffer, int z, - Coordinate[] coords) { - TiledGeometry result = new TiledGeometry(extents, buffer, z, false); + Coordinate[] coords, long id) { + TiledGeometry result = new TiledGeometry(extents, buffer, z, false, id); for (Coordinate coord : coords) { result.slicePoint(coord); } @@ -107,9 +109,9 @@ class TiledGeometry { } public static TiledGeometry sliceIntoTiles(List> groups, double buffer, boolean area, int z, - TileExtents.ForZoom extents) { + TileExtents.ForZoom extents, long id) { int worldExtent = 1 << z; - TiledGeometry result = new TiledGeometry(extents, buffer, z, area); + TiledGeometry result = new TiledGeometry(extents, buffer, z, area, id); EnumSet wrapResult = result.sliceWorldCopy(groups, 0); if (wrapResult.contains(Direction.RIGHT)) { result.sliceWorldCopy(groups, -worldExtent); @@ -176,7 +178,7 @@ class TiledGeometry { boolean outer = i == 0; IntObjectMap> xSlices = sliceX(segment); if (z >= 6 && xSlices.size() >= Math.pow(2, z) - 1) { - LOGGER.warn("Feature crosses world at z" + z + ": " + xSlices.size()); + LOGGER.warn("Feature " + id + " crosses world at z" + z + ": " + xSlices.size()); } for (IntObjectCursor> xCursor : xSlices) { int x = xCursor.key + xOffset; diff --git a/core/src/test/java/com/onthegomap/flatmap/FlatMapTest.java b/core/src/test/java/com/onthegomap/flatmap/FlatMapTest.java index d95f1bf5..9328ef8f 100644 --- a/core/src/test/java/com/onthegomap/flatmap/FlatMapTest.java +++ b/core/src/test/java/com/onthegomap/flatmap/FlatMapTest.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -151,6 +152,18 @@ public class FlatMapTest { ); } + private FlatMapResults runWithReaderFeaturesProfile( + Map args, + List features, + Profile profileToUse + ) throws Exception { + return run( + args, + (featureGroup, profile, config) -> processReaderFeatures(featureGroup, profile, config, features), + profileToUse + ); + } + private FlatMapResults runWithOsmElements( Map args, List features, @@ -163,6 +176,18 @@ public class FlatMapTest { ); } + private FlatMapResults runWithOsmElements( + Map args, + List features, + Profile profileToUse + ) throws Exception { + return run( + args, + (featureGroup, profile, config) -> processOsmFeatures(featureGroup, profile, config, features), + profileToUse + ); + } + private FlatMapResults runWithOsmElements( Map args, List features, @@ -1042,6 +1067,114 @@ public class FlatMapTest { )), sortListValues(results.tiles)); } + @Test + public void testReaderProfileFinish() throws Exception { + double y = 0.5 + Z14_WIDTH / 2; + double lat = GeoUtils.getWorldLat(y); + + double x1 = 0.5 + Z14_WIDTH / 4; + double lng1 = GeoUtils.getWorldLon(x1); + double lng2 = GeoUtils.getWorldLon(x1 + Z14_WIDTH * 10d / 256); + + var results = runWithReaderFeaturesProfile( + Map.of("threads", "1"), + List.of( + newReaderFeature(newPoint(lng1, lat), Map.of("a", 1, "b", 2)), + newReaderFeature(newPoint(lng2, lat), Map.of("a", 3, "b", 4)) + ), + new Profile.NullProfile() { + private final List featureList = Collections.synchronizedList(new ArrayList<>()); + + @Override + public void processFeature(SourceFeature in, FeatureCollector features) { + featureList.add(in); + } + + @Override + public void finish(String name, FeatureCollector.Factory featureCollectors, + Consumer next) { + if ("test".equals(name)) { + for (SourceFeature in : featureList) { + var features = featureCollectors.get(in); + features.point("layer") + .setZoomRange(13, 14) + .inheritFromSource("a"); + for (var feature : features) { + next.accept(feature); + } + } + } + } + } + ); + + assertSubmap(sortListValues(Map.of( + TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of( + feature(newPoint(64, 128), Map.of("a", 1L)), + feature(newPoint(74, 128), Map.of("a", 3L)) + ), + TileCoord.ofXYZ(Z13_TILES / 2, Z13_TILES / 2, 13), List.of( + // merge 32->37 and 37->42 since they have same attrs + feature(newPoint(32, 64), Map.of("a", 1L)), + feature(newPoint(37, 64), Map.of("a", 3L)) + ) + )), sortListValues(results.tiles)); + } + + @Test + public void testOsmProfileFinish() throws Exception { + double y = 0.5 + Z14_WIDTH / 2; + double lat = GeoUtils.getWorldLat(y); + + double x1 = 0.5 + Z14_WIDTH / 4; + double lng1 = GeoUtils.getWorldLon(x1); + double lng2 = GeoUtils.getWorldLon(x1 + Z14_WIDTH * 10d / 256); + + var results = runWithOsmElements( + Map.of("threads", "1"), + List.of( + with(new ReaderNode(1, lat, lng1), t -> t.setTag("a", 1)), + with(new ReaderNode(2, lat, lng2), t -> t.setTag("a", 3)) + ), + new Profile.NullProfile() { + private final List featureList = Collections.synchronizedList(new ArrayList<>()); + + @Override + public void processFeature(SourceFeature in, FeatureCollector features) { + featureList.add(in); + } + + @Override + public void finish(String name, FeatureCollector.Factory featureCollectors, + Consumer next) { + if ("osm".equals(name)) { + for (SourceFeature in : featureList) { + var features = featureCollectors.get(in); + features.point("layer") + .setZoomRange(13, 14) + .inheritFromSource("a"); + for (var feature : features) { + next.accept(feature); + } + } + } + } + } + ); + + assertSubmap(sortListValues(Map.of( + TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of( + feature(newPoint(64, 128), Map.of("a", 1L)), + feature(newPoint(74, 128), Map.of("a", 3L)) + ), + TileCoord.ofXYZ(Z13_TILES / 2, Z13_TILES / 2, 13), List.of( + // merge 32->37 and 37->42 since they have same attrs + feature(newPoint(32, 64), Map.of("a", 1L)), + feature(newPoint(37, 64), Map.of("a", 3L)) + ) + )), sortListValues(results.tiles)); + } + private , V extends List> Map sortListValues(Map input) { Map> result = new TreeMap<>(); for (var entry : input.entrySet()) { diff --git a/core/src/test/java/com/onthegomap/flatmap/geo/GeoUtilsTest.java b/core/src/test/java/com/onthegomap/flatmap/geo/GeoUtilsTest.java index 5c33d609..807e4c71 100644 --- a/core/src/test/java/com/onthegomap/flatmap/geo/GeoUtilsTest.java +++ b/core/src/test/java/com/onthegomap/flatmap/geo/GeoUtilsTest.java @@ -17,6 +17,7 @@ public class GeoUtilsTest { @CsvSource({ "0,0, 0.5,0.5", "0, -180, 0, 0.5", + "0, 180, 1, 0.5", "0, " + (180 - 1e-7) + ", 1, 0.5", "45, 0, 0.5, 0.359725", "-45, 0, 0.5, " + (1 - 0.359725) diff --git a/core/src/test/java/com/onthegomap/flatmap/read/OsmMultipolygonTest.java b/core/src/test/java/com/onthegomap/flatmap/read/OsmMultipolygonTest.java index 0d5fe910..4ea758a0 100644 --- a/core/src/test/java/com/onthegomap/flatmap/read/OsmMultipolygonTest.java +++ b/core/src/test/java/com/onthegomap/flatmap/read/OsmMultipolygonTest.java @@ -164,6 +164,15 @@ public class OsmMultipolygonTest { ); } + @Test + public void testBuildMultipolygonFromGeometries() throws GeometryException { + Geometry actual = OsmMultipolygon.build(List.of( + newLineString(0.2, 0.2, 0.4, 0.2, 0.4, 0.4).getCoordinateSequence(), + newLineString(0.4, 0.4, 0.2, 0.4, 0.2, 0.2).getCoordinateSequence() + )); + assertSameNormalizedFeature(rectangle(0.2, 0.4), actual); + } + @Test public void testThrowWhenNoClosed() { var node1 = node(0.5, 0.5); diff --git a/openmaptiles/pom.xml b/openmaptiles/pom.xml index 90664622..f5d9064c 100644 --- a/openmaptiles/pom.xml +++ b/openmaptiles/pom.xml @@ -23,6 +23,11 @@ snakeyaml 1.29 + + org.commonmark + commonmark + 0.17.2 + com.ibm.icu icu4j diff --git a/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/Generate.java b/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/Generate.java index f293ed70..f574ea66 100644 --- a/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/Generate.java +++ b/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/Generate.java @@ -26,6 +26,9 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Stream; import org.apache.commons.text.StringEscapeUtils; +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yaml.snakeyaml.LoaderOptions; @@ -343,6 +346,7 @@ public class Generate { import com.onthegomap.flatmap.Translations; import java.util.List; import java.util.Map; + import java.util.Set; public class OpenMapTilesSchema { public static final String NAME = %s; @@ -415,6 +419,10 @@ public class Generate { .formatted(name.toUpperCase(Locale.ROOT) + "_" + v.toUpperCase(Locale.ROOT).replace('-', '_'), quote(v))) .collect(joining("\n")).indent(2).strip() .indent(4)); + fieldValues.append("public static final Set %s = Set.of(%s);".formatted( + name.toUpperCase(Locale.ROOT) + "_VALUES", + values.stream().map(Generate::quote).collect(joining(", ")) + ).indent(4)); } if (valuesNode != null && valuesNode.isObject()) { @@ -524,8 +532,12 @@ public class Generate { return result; } + private static final Parser parser = Parser.builder().build(); + private static final HtmlRenderer renderer = HtmlRenderer.builder().build(); + private static String escapeJavadoc(String description) { - return description.replaceAll("[\n\r*\\s]+", " "); + Node document = parser.parse(description); + return renderer.render(document).replaceAll("[\n\r*\\s]+", " "); } private static String getFieldDescription(JsonNode value) { diff --git a/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/OpenMapTilesMain.java b/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/OpenMapTilesMain.java index bfd07f9e..4ad12fda 100644 --- a/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/OpenMapTilesMain.java +++ b/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/OpenMapTilesMain.java @@ -27,8 +27,8 @@ public class OpenMapTilesMain { .setProfile(createProfileWithWikidataTranslations(runner)) .addShapefileSource("EPSG:3857", OpenMapTilesProfile.LAKE_CENTERLINE_SOURCE, sourcesDir.resolve("lake_centerline.shp.zip")) - .addShapefileSource(OpenMapTilesProfile.WATER_POLYGON_SOURCE, - sourcesDir.resolve("water-polygons-split-3857.zip")) +// .addShapefileSource(OpenMapTilesProfile.WATER_POLYGON_SOURCE, +// sourcesDir.resolve("water-polygons-split-3857.zip")) .addNaturalEarthSource(OpenMapTilesProfile.NATURAL_EARTH_SOURCE, sourcesDir.resolve("natural_earth_vector.sqlite.zip")) .addOsmSource(OpenMapTilesProfile.OSM_SOURCE, sourcesDir.resolve(fallbackOsmFile)) diff --git a/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/OpenMapTilesProfile.java b/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/OpenMapTilesProfile.java index 24b2dca9..25bbcc63 100644 --- a/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/OpenMapTilesProfile.java +++ b/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/OpenMapTilesProfile.java @@ -23,9 +23,14 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class OpenMapTilesProfile implements Profile { + private static final Logger LOGGER = LoggerFactory.getLogger(OpenMapTilesProfile.class); + public static final String LAKE_CENTERLINE_SOURCE = "lake_centerlines"; public static final String WATER_POLYGON_SOURCE = "water_polygons"; public static final String NATURAL_EARTH_SOURCE = "natural_earth"; @@ -41,6 +46,8 @@ public class OpenMapTilesProfile implements Profile { private final List osmWaterProcessors; private final List lakeCenterlineProcessors; private final List osmAllProcessors; + private final List osmRelationPreprocessors; + private final List finishHandlers; private MultiExpression.MultiExpressionIndex indexForType(String type) { return Tables.MAPPINGS @@ -73,6 +80,8 @@ public class OpenMapTilesProfile implements Profile { lakeCenterlineProcessors = new ArrayList<>(); naturalEarthProcessors = new ArrayList<>(); osmWaterProcessors = new ArrayList<>(); + osmRelationPreprocessors = new ArrayList<>(); + finishHandlers = new ArrayList<>(); for (Layer layer : layers) { if (layer instanceof FeaturePostProcessor postProcessor) { postProcessors.put(layer.name(), postProcessor); @@ -89,6 +98,12 @@ public class OpenMapTilesProfile implements Profile { if (layer instanceof NaturalEarthProcessor processor) { naturalEarthProcessors.add(processor); } + if (layer instanceof OsmRelationPreprocessor processor) { + osmRelationPreprocessors.add(processor); + } + if (layer instanceof FinishHandler processor) { + finishHandlers.add(processor); + } } } @@ -110,7 +125,19 @@ public class OpenMapTilesProfile implements Profile { @Override public List preprocessOsmRelation(ReaderRelation relation) { - return null; + List result = null; + for (int i = 0; i < osmRelationPreprocessors.size(); i++) { + List thisResult = osmRelationPreprocessors.get(i) + .preprocessOsmRelation(relation); + if (thisResult != null) { + if (result == null) { + result = new ArrayList<>(thisResult); + } else { + result.addAll(thisResult); + } + } + } + return result; } @Override @@ -166,6 +193,14 @@ public class OpenMapTilesProfile implements Profile { return result == null ? List.of() : result; } + @Override + public void finish(String sourceName, FeatureCollector.Factory featureCollectors, + Consumer next) { + for (var handler : finishHandlers) { + handler.finish(sourceName, featureCollectors, next); + } + } + public interface NaturalEarthProcessor { void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features); @@ -186,6 +221,17 @@ public class OpenMapTilesProfile implements Profile { void processAllOsm(SourceFeature feature, FeatureCollector features); } + public interface FinishHandler { + + void finish(String sourceName, FeatureCollector.Factory featureCollectors, + Consumer next); + } + + public interface OsmRelationPreprocessor { + + List preprocessOsmRelation(ReaderRelation relation); + } + public interface FeaturePostProcessor { List postProcess(int zoom, List items) diff --git a/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/generated/OpenMapTilesSchema.java b/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/generated/OpenMapTilesSchema.java index f67e3123..ce381269 100644 --- a/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/generated/OpenMapTilesSchema.java +++ b/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/generated/OpenMapTilesSchema.java @@ -13,6 +13,7 @@ import com.onthegomap.flatmap.openmaptiles.Layer; import com.onthegomap.flatmap.openmaptiles.MultiExpression; import java.util.List; import java.util.Map; +import java.util.Set; public class OpenMapTilesSchema { @@ -49,12 +50,12 @@ public class OpenMapTilesSchema { } /** - * Water polygons representing oceans and lakes. Covered watered areas are excluded (`covered=yes`). On low zoom - * levels all water originates from Natural Earth. To get a more correct display of the south pole you should also - * style the covering ice shelves over the water. On higher zoom levels water polygons from - * [OpenStreetMapData](http://osmdata.openstreetmap.de/) are used. The polygons are split into many smaller polygons - * to improve rendering performance. This however can lead to less rendering options in clients since these boundaries - * show up. So you might not be able to use border styling for ocean water features. + *

Water polygons representing oceans and lakes. Covered watered areas are excluded (covered=yes). On + * low zoom levels all water originates from Natural Earth. To get a more correct display of the south pole you should + * also style the covering ice shelves over the water. On higher zoom levels water polygons from OpenStreetMapData are used. The polygons are split into many smaller + * polygons to improve rendering performance. This however can lead to less rendering options in clients since these + * boundaries show up. So you might not be able to use border styling for ocean water features.

*/ public interface Water extends Layer { @@ -69,9 +70,9 @@ public class OpenMapTilesSchema { final class Fields { /** - * All water polygons from [OpenStreetMapData](http://osmdata.openstreetmap.de/) have the class `ocean`. Water - * bodies are classified as `lake` or `river` for water bodies with the [`waterway`](http://wiki.openstreetmap.org/wiki/Key:waterway) - * tag. + *

All water polygons from OpenStreetMapData have the class + * ocean. Water bodies are classified as lake or river for water bodies + * with the waterway tag.

*

* allowed values: *

    @@ -84,7 +85,8 @@ public class OpenMapTilesSchema { public static final String CLASS = "class"; /** - * Mark with `1` if it is an [intermittent](http://wiki.openstreetmap.org/wiki/Key:intermittent) water polygon. + *

    Mark with 1 if it is an intermittent + * water polygon.

    *

    * allowed values: *

      @@ -95,7 +97,7 @@ public class OpenMapTilesSchema { public static final String INTERMITTENT = "intermittent"; /** - * Identifies the type of crossing as either a bridge or a tunnel. + *

      Identifies the type of crossing as either a bridge or a tunnel.

      *

      * allowed values: *

        @@ -112,8 +114,10 @@ public class OpenMapTilesSchema { public static final String CLASS_DOCK = "dock"; public static final String CLASS_RIVER = "river"; public static final String CLASS_OCEAN = "ocean"; + public static final Set CLASS_VALUES = Set.of("lake", "dock", "river", "ocean"); public static final String BRUNNEL_BRIDGE = "bridge"; public static final String BRUNNEL_TUNNEL = "tunnel"; + public static final Set BRUNNEL_VALUES = Set.of("bridge", "tunnel"); } final class FieldMappings { @@ -125,11 +129,11 @@ public class OpenMapTilesSchema { } /** - * OpenStreetMap [waterways](https://wiki.openstreetmap.org/wiki/Waterways) for higher zoom levels (z9 and more) and - * Natural Earth rivers and lake centerlines for low zoom levels (z3 - z8). Linestrings without a name or which are - * too short are filtered out at low zoom levels. Till z11 there is `river` class only, in z12 there is also `canal` - * generated, starting z13 there is no generalization according to `class` field applied. Waterways do not have a - * `subclass` field. + *

        OpenStreetMap waterways for higher zoom levels (z9 + * and more) and Natural Earth rivers and lake centerlines for low zoom levels (z3 - z8). Linestrings without a name + * or which are too short are filtered out at low zoom levels. Till z11 there is river class only, in z12 + * there is also canal generated, starting z13 there is no generalization according to class + * field applied. Waterways do not have a subclass field.

        */ public interface Waterway extends Layer { @@ -144,21 +148,22 @@ public class OpenMapTilesSchema { final class Fields { /** - * The OSM [`name`](http://wiki.openstreetmap.org/wiki/Key:name) value of the waterway. The `name` field may be - * empty for NaturalEarth data or at lower zoom levels. + *

        The OSM name value of the waterway. + * The name field may be empty for NaturalEarth data or at lower zoom levels.

        */ public static final String NAME = "name"; /** - * English name `name:en` if available, otherwise `name`. + *

        English name name:en if available, otherwise name.

        */ public static final String NAME_EN = "name_en"; /** - * German name `name:de` if available, otherwise `name` or `name:en`. + *

        German name name:de if available, otherwise name or name:en.

        */ public static final String NAME_DE = "name_de"; /** - * The original value of the [`waterway`](http://wiki.openstreetmap.org/wiki/Key:waterway) tag. + *

        The original value of the waterway + * tag.

        *

        * allowed values: *

          @@ -172,7 +177,7 @@ public class OpenMapTilesSchema { public static final String CLASS = "class"; /** - * Mark whether way is a tunnel or bridge. + *

          Mark whether way is a tunnel or bridge.

          *

          * allowed values: *

            @@ -183,7 +188,8 @@ public class OpenMapTilesSchema { public static final String BRUNNEL = "brunnel"; /** - * Mark with `1` if it is an [intermittent](http://wiki.openstreetmap.org/wiki/Key:intermittent) waterway. + *

            Mark with 1 if it is an intermittent + * waterway.

            *

            * allowed values: *

              @@ -201,8 +207,10 @@ public class OpenMapTilesSchema { public static final String CLASS_CANAL = "canal"; public static final String CLASS_DRAIN = "drain"; public static final String CLASS_DITCH = "ditch"; + public static final Set CLASS_VALUES = Set.of("stream", "river", "canal", "drain", "ditch"); public static final String BRUNNEL_BRIDGE = "bridge"; public static final String BRUNNEL_TUNNEL = "tunnel"; + public static final Set BRUNNEL_VALUES = Set.of("bridge", "tunnel"); } final class FieldMappings { @@ -211,10 +219,10 @@ public class OpenMapTilesSchema { } /** - * Landcover is used to describe the physical material at the surface of the earth. At lower zoom levels this is from - * Natural Earth data for glaciers and ice shelves and at higher zoom levels the landcover is [implied by OSM - * tags](http://wiki.openstreetmap.org/wiki/Landcover). The most common use case for this layer is to style wood - * (`class=wood`) and grass (`class=grass`) areas. + *

              Landcover is used to describe the physical material at the surface of the earth. At lower zoom levels this is + * from Natural Earth data for glaciers and ice shelves and at higher zoom levels the landcover is implied by OSM tags. The most common use case for this + * layer is to style wood (class=wood) and grass (class=grass) areas.

              */ public interface Landcover extends Layer { @@ -229,7 +237,7 @@ public class OpenMapTilesSchema { final class Fields { /** - * Use the class to assign natural colors for landcover . + *

              Use the class to assign natural colors for landcover.

              *

              * allowed values: *

                @@ -245,9 +253,11 @@ public class OpenMapTilesSchema { public static final String CLASS = "class"; /** - * Use subclass to do more precise styling. Original value of either the [`natural`](http://wiki.openstreetmap.org/wiki/Key:natural), - * [`landuse`](http://wiki.openstreetmap.org/wiki/Key:landuse), [`leisure`](http://wiki.openstreetmap.org/wiki/Key:leisure), - * or [`wetland`](http://wiki.openstreetmap.org/wiki/Key:wetland) tag. + *

                Use subclass to do more precise styling. Original value of either the natural, landuse, leisure, or wetland tag.

                *

                * allowed values: *

                  @@ -301,6 +311,8 @@ public class OpenMapTilesSchema { public static final String CLASS_GRASS = "grass"; public static final String CLASS_WETLAND = "wetland"; public static final String CLASS_SAND = "sand"; + public static final Set CLASS_VALUES = Set + .of("farmland", "ice", "wood", "rock", "grass", "wetland", "sand"); public static final String SUBCLASS_ALLOTMENTS = "allotments"; public static final String SUBCLASS_BARE_ROCK = "bare_rock"; public static final String SUBCLASS_BEACH = "beach"; @@ -337,6 +349,11 @@ public class OpenMapTilesSchema { public static final String SUBCLASS_WET_MEADOW = "wet_meadow"; public static final String SUBCLASS_WETLAND = "wetland"; public static final String SUBCLASS_WOOD = "wood"; + public static final Set SUBCLASS_VALUES = Set + .of("allotments", "bare_rock", "beach", "bog", "dune", "scrub", "farm", "farmland", "fell", "forest", "garden", + "glacier", "grass", "grassland", "golf_course", "heath", "mangrove", "marsh", "meadow", "orchard", "park", + "plant_nursery", "recreation_ground", "reedbed", "saltern", "saltmarsh", "sand", "scree", "swamp", + "tidalflat", "tundra", "village_green", "vineyard", "wet_meadow", "wetland", "wood"); } final class FieldMappings { @@ -354,8 +371,8 @@ public class OpenMapTilesSchema { } /** - * Landuse is used to describe use of land by humans. At lower zoom levels this is from Natural Earth data for - * residential (urban) areas and at higher zoom levels mostly OSM `landuse` tags. + *

                  Landuse is used to describe use of land by humans. At lower zoom levels this is from Natural Earth data for + * residential (urban) areas and at higher zoom levels mostly OSM landuse tags.

                  */ public interface Landuse extends Layer { @@ -370,11 +387,13 @@ public class OpenMapTilesSchema { final class Fields { /** - * Use the class to assign special colors to areas. Original value of either the - * [`landuse`](http://wiki.openstreetmap.org/wiki/Key:landuse), [`amenity`](http://wiki.openstreetmap.org/wiki/Key:amenity), - * [`leisure`](http://wiki.openstreetmap.org/wiki/Key:leisure), [`tourism`](http://wiki.openstreetmap.org/wiki/Key:tourism), - * [`place`](http://wiki.openstreetmap.org/wiki/Key:place) or [`waterway`](http://wiki.openstreetmap.org/wiki/Key:waterway) - * tag. + *

                  Use the class to assign special colors to areas. Original value of either the landuse, amenity, leisure, tourism, place or waterway tag.

                  *

                  * allowed values: *

                    @@ -435,6 +454,10 @@ public class OpenMapTilesSchema { public static final String CLASS_QUARTER = "quarter"; public static final String CLASS_NEIGHBOURHOOD = "neighbourhood"; public static final String CLASS_DAM = "dam"; + public static final Set CLASS_VALUES = Set + .of("railway", "cemetery", "military", "residential", "commercial", "industrial", "garages", "retail", + "bus_station", "school", "university", "kindergarten", "college", "library", "hospital", "stadium", "pitch", + "playground", "track", "theme_park", "zoo", "suburb", "quarter", "neighbourhood", "dam"); } final class FieldMappings { @@ -443,7 +466,7 @@ public class OpenMapTilesSchema { } /** - * [Natural peaks](http://wiki.openstreetmap.org/wiki/Tag:natural%3Dpeak) + *

                    Natural peaks

                    */ public interface MountainPeak extends Layer { @@ -458,20 +481,20 @@ public class OpenMapTilesSchema { final class Fields { /** - * The OSM [`name`](http://wiki.openstreetmap.org/wiki/Key:name) value of the peak. + *

                    The OSM name value of the peak.

                    */ public static final String NAME = "name"; /** - * English name `name:en` if available, otherwise `name`. + *

                    English name name:en if available, otherwise name.

                    */ public static final String NAME_EN = "name_en"; /** - * German name `name:de` if available, otherwise `name` or `name:en`. + *

                    German name name:de if available, otherwise name or name:en.

                    */ public static final String NAME_DE = "name_de"; /** - * Use the class to differentiate between mountain peak and volcano. + *

                    Use the class to differentiate between mountain peak and volcano.

                    *

                    * allowed values: *

                      @@ -481,15 +504,15 @@ public class OpenMapTilesSchema { */ public static final String CLASS = "class"; /** - * Elevation (`ele`) in meters. + *

                      Elevation (ele) in meters.

                      */ public static final String ELE = "ele"; /** - * Elevation (`ele`) in feets. + *

                      Elevation (ele) in feets.

                      */ public static final String ELE_FT = "ele_ft"; /** - * Rank of the peak within one tile (starting at 1 that is the most important peak). + *

                      Rank of the peak within one tile (starting at 1 that is the most important peak).

                      */ public static final String RANK = "rank"; } @@ -498,6 +521,7 @@ public class OpenMapTilesSchema { public static final String CLASS_PEAK = "peak"; public static final String CLASS_VOLCANO = "volcano"; + public static final Set CLASS_VALUES = Set.of("peak", "volcano"); } final class FieldMappings { @@ -506,9 +530,9 @@ public class OpenMapTilesSchema { } /** - * The park layer contains parks from OpenStreetMap tagged with [`boundary=national_park`](http://wiki.openstreetmap.org/wiki/Tag:boundary%3Dnational_park), - * [`boundary=protected_area`](http://wiki.openstreetmap.org/wiki/Tag:boundary%3Dprotected_area), or - * [`leisure=nature_reserve`](http://wiki.openstreetmap.org/wiki/Tag:leisure%3Dnature_reserve). + *

                      The park layer contains parks from OpenStreetMap tagged with boundary=national_park, + * boundary=protected_area, + * or leisure=nature_reserve.

                      */ public interface Park extends Layer { @@ -523,28 +547,32 @@ public class OpenMapTilesSchema { final class Fields { /** - * Use the class to differentiate between different parks. The class for `boundary=protected_area` parks is the - * lower-case of the [`protection_title`](http://wiki.openstreetmap.org/wiki/key:protection_title) value with - * blanks replaced by `_`. `national_park` is the class of `protection_title=National Park` and - * `boundary=national_park`. `nature_reserve` is the class of `protection_title=Nature Reserve` and - * `leisure=nature_reserve`. The class for other [`protection_title`](http://wiki.openstreetmap.org/wiki/key:protection_title) - * values is similarly assigned. + *

                      Use the class to differentiate between different parks. The class for + * boundary=protected_area parks is the lower-case of the protection_title + * value with blanks replaced by _. national_park is the class of + * protection_title=National Park and boundary=national_park. + * nature_reserve is the class of protection_title=Nature Reserve and + * leisure=nature_reserve. The class for other protection_title + * values is similarly assigned.

                      */ public static final String CLASS = "class"; /** - * The OSM [`name`](http://wiki.openstreetmap.org/wiki/Key:name) value of the park (point features only). + *

                      The OSM name value of the park + * (point + * features only).

                      */ public static final String NAME = "name"; /** - * English name `name:en` if available, otherwise `name` (point features only). + *

                      English name name:en if available, otherwise name (point features only).

                      */ public static final String NAME_EN = "name_en"; /** - * German name `name:de` if available, otherwise `name` or `name:en` (point features only). + *

                      German name name:de if available, otherwise name or name:en (point + * features only).

                      */ public static final String NAME_DE = "name_de"; /** - * Rank of the park within one tile, starting at 1 that is the most important park (point features only). + *

                      Rank of the park within one tile, starting at 1 that is the most important park (point features only).

                      */ public static final String RANK = "rank"; } @@ -559,11 +587,11 @@ public class OpenMapTilesSchema { } /** - * Contains administrative boundaries as linestrings. Until z4 [Natural Earth data](http://www.naturalearthdata.com/downloads/) - * is used after which OSM boundaries ([`boundary=administrative`](http://wiki.openstreetmap.org/wiki/Tag:boundary%3Dadministrative)) - * are present from z5 to z14 (also for maritime boundaries with `admin_level <= 2` at z4). OSM data contains several - * [`admin_level`](http://wiki.openstreetmap.org/wiki/Tag:boundary%3Dadministrative#admin_level) but for most styles - * it makes sense to just style `admin_level=2` and `admin_level=4`. + *

                      Contains administrative boundaries as linestrings. Until z4 Natural + * Earth data is used after which OSM boundaries (boundary=administrative) + * are present from z5 to z14 (also for maritime boundaries with admin_level <= 2 at z4). OSM data + * contains several admin_level + * but for most styles it makes sense to just style admin_level=2 and admin_level=4.

                      */ public interface Boundary extends Layer { @@ -578,22 +606,23 @@ public class OpenMapTilesSchema { final class Fields { /** - * OSM [admin_level](http://wiki.openstreetmap.org/wiki/Tag:boundary%3Dadministrative#admin_level) indicating the - * level of importance of this boundary. The `admin_level` corresponds to the lowest `admin_level` the line - * participates in. At low zoom levels the Natural Earth boundaries are mapped to the equivalent admin levels. + *

                      OSM admin_level + * indicating the level of importance of this boundary. The admin_level corresponds to the lowest + * admin_level the line participates in. At low zoom levels the Natural Earth boundaries are mapped + * to the equivalent admin levels.

                      */ public static final String ADMIN_LEVEL = "admin_level"; /** - * State name on the left of the border. For country boundaries only (`admin_level = 2`). + *

                      State name on the left of the border. For country boundaries only (admin_level = 2).

                      */ public static final String ADM0_L = "adm0_l"; /** - * State name on the right of the border. For country boundaries only (`admin_level = 2`). + *

                      State name on the right of the border. For country boundaries only (admin_level = 2).

                      */ public static final String ADM0_R = "adm0_r"; /** - * Mark with `1` if the border is disputed. + *

                      Mark with 1 if the border is disputed.

                      *

                      * allowed values: *

                        @@ -604,8 +633,8 @@ public class OpenMapTilesSchema { public static final String DISPUTED = "disputed"; /** - * Field containing name of the disputed area (extracted from border relation in OSM, without spaces). For country - * boundaries only (`admin_level = 2`). Value examples from Asian OSM pbf extract + *

                        Field containing name of the disputed area (extracted from border relation in OSM, without spaces). For + * country boundaries only (admin_level = 2). Value examples from Asian OSM pbf extract

                        *

                        * allowed values: *

                          @@ -624,12 +653,14 @@ public class OpenMapTilesSchema { */ public static final String DISPUTED_NAME = "disputed_name"; /** - * ISO2 code of country, which wants to see the boundary line. For country boundaries only (`admin_level = 2`). + *

                          ISO2 code of country, which wants to see the boundary line. For country boundaries only (admin_level + * = + * 2).

                          */ public static final String CLAIMED_BY = "claimed_by"; /** - * Mark with `1` if it is a maritime border. + *

                          Mark with 1 if it is a maritime border.

                          *

                          * allowed values: *

                            @@ -653,6 +684,9 @@ public class OpenMapTilesSchema { public static final String DISPUTED_NAME_PAKISTANICLAIM = "PakistaniClaim"; public static final String DISPUTED_NAME_SAMDUVALLEYS = "SamduValleys"; public static final String DISPUTED_NAME_TIRPANIVALLEYS = "TirpaniValleys"; + public static final Set DISPUTED_NAME_VALUES = Set + .of("AbuMusaIsland", "BaraHotiiValleys", "ChineseClaim", "Crimea", "Demchok", "Dokdo", "IndianClaim-North", + "IndianClaimwesternKashmir", "PakistaniClaim", "SamduValleys", "TirpaniValleys"); } final class FieldMappings { @@ -661,8 +695,9 @@ public class OpenMapTilesSchema { } /** - * Aeroway polygons based of OpenStreetMap [aeroways](http://wiki.openstreetmap.org/wiki/Aeroways). Airport buildings - * are contained in the building layer but all other airport related polygons can be found in the aeroway layer. + *

                            Aeroway polygons based of OpenStreetMap aeroways. + * Airport buildings are contained in the building layer but all other airport related polygons can + * be found in the aeroway layer.

                            */ public interface Aeroway extends Layer { @@ -677,12 +712,14 @@ public class OpenMapTilesSchema { final class Fields { /** - * The OSM [`ref`](http://wiki.openstreetmap.org/wiki/Key:ref) tag of the runway/taxiway. + *

                            The OSM ref tag of the + * runway/taxiway.

                            */ public static final String REF = "ref"; /** - * The original value of [`aeroway`](http://wiki.openstreetmap.org/wiki/Key:aeroway) or `area:aeroway` tag. + *

                            The original value of aeroway or + * area:aeroway tag.

                            *

                            * allowed values: *

                              @@ -707,6 +744,8 @@ public class OpenMapTilesSchema { public static final String CLASS_TAXIWAY = "taxiway"; public static final String CLASS_APRON = "apron"; public static final String CLASS_GATE = "gate"; + public static final Set CLASS_VALUES = Set + .of("aerodrome", "heliport", "runway", "helipad", "taxiway", "apron", "gate"); } final class FieldMappings { @@ -715,10 +754,11 @@ public class OpenMapTilesSchema { } /** - * transportation contains roads, railways, aerial ways, and shipping lines. This layer is directly derived from the - * OSM road hierarchy. At lower zoom levels major highways from Natural Earth are used. It contains all roads from - * motorways to primary, secondary and tertiary roads to residential roads and foot paths. Styling the roads is the - * most essential part of the map. The `transportation` layer also contains polygons for features like plazas. + *

                              transportation contains roads, railways, aerial ways, and shipping lines. This layer is + * directly derived from the OSM road hierarchy. At lower zoom levels major highways from Natural Earth are used. It + * contains all roads from motorways to primary, secondary and tertiary roads to residential roads and foot paths. + * Styling the roads is the most essential part of the map. The transportation layer also contains + * polygons for features like plazas.

                              */ public interface Transportation extends Layer { @@ -733,11 +773,14 @@ public class OpenMapTilesSchema { final class Fields { /** - * Distinguish between more and less important roads or railways and roads under construction. Class is derived - * from the value of the [`highway`](http://wiki.openstreetmap.org/wiki/Key:highway), - * [`construction`](http://wiki.openstreetmap.org/wiki/Key:construction), [`railway`](http://wiki.openstreetmap.org/wiki/Key:railway), - * [`aerialway`](http://wiki.openstreetmap.org/wiki/Key:aerialway), [`route`](http://wiki.openstreetmap.org/wiki/Key:route) - * tag (for shipping ways), or [`man_made`](http://wiki.openstreetmap.org/wiki/Key:route). + *

                              Distinguish between more and less important roads or railways and roads under construction. Class is + * derived + * from the value of the highway, construction, railway, aerialway, route tag (for shipping ways), or man_made.

                              *

                              * allowed values: *

                                @@ -766,10 +809,11 @@ public class OpenMapTilesSchema { public static final String CLASS = "class"; /** - * Distinguish more specific classes of railway and path: Subclass is value of the - * [`railway`](http://wiki.openstreetmap.org/wiki/Key:railway), [`highway`](http://wiki.openstreetmap.org/wiki/Key:highway) - * (for paths), or [`public_transport`](http://wiki.openstreetmap.org/wiki/Key:public_transport) (for platforms) - * tag. + *

                                Distinguish more specific classes of railway and path: Subclass is value of the railway, highway (for paths), or public_transport (for + * platforms) tag.

                                *

                                * allowed values: *

                                  @@ -794,7 +838,7 @@ public class OpenMapTilesSchema { public static final String SUBCLASS = "subclass"; /** - * Mark whether way is a tunnel or bridge. + *

                                  Mark whether way is a tunnel or bridge.

                                  *

                                  * allowed values: *

                                    @@ -806,8 +850,8 @@ public class OpenMapTilesSchema { public static final String BRUNNEL = "brunnel"; /** - * Mark with `1` whether way is a oneway in the direction of the way, with `-1` whether way is a oneway in the - * opposite direction of the way or not a oneway with `0`. + *

                                    Mark with 1 whether way is a oneway in the direction of the way, with -1 whether + * way is a oneway in the opposite direction of the way or not a oneway with 0.

                                    *

                                    * allowed values: *

                                      @@ -819,7 +863,7 @@ public class OpenMapTilesSchema { public static final String ONEWAY = "oneway"; /** - * Mark with `1` whether way is a ramp (link or steps) or not with `0`. + *

                                      Mark with 1 whether way is a ramp (link or steps) or not with 0.

                                      *

                                      * allowed values: *

                                        @@ -830,7 +874,8 @@ public class OpenMapTilesSchema { public static final String RAMP = "ramp"; /** - * Original value of the [`service`](http://wiki.openstreetmap.org/wiki/Key:service) tag. + *

                                        Original value of the service + * tag.

                                        *

                                        * allowed values: *

                                          @@ -845,18 +890,19 @@ public class OpenMapTilesSchema { */ public static final String SERVICE = "service"; /** - * Original value of the [`layer`](http://wiki.openstreetmap.org/wiki/Key:layer) tag. + *

                                          Original value of the layer + * tag.

                                          */ public static final String LAYER = "layer"; /** - * Experimental feature! Filled only for steps and footways. Original value of the - * [`level`](http://wiki.openstreetmap.org/wiki/Key:level) tag. + *

                                          Experimental feature! Filled only for steps and footways. Original value of the level tag.

                                          */ public static final String LEVEL = "level"; /** - * Experimental feature! Filled only for steps and footways. Original value of the - * [`indoor`](http://wiki.openstreetmap.org/wiki/Key:indoor) tag. + *

                                          Experimental feature! Filled only for steps and footways. Original value of the indoor tag.

                                          *

                                          * allowed values: *

                                            @@ -865,27 +911,32 @@ public class OpenMapTilesSchema { */ public static final String INDOOR = "indoor"; /** - * Original value of the [`bicycle`](http://wiki.openstreetmap.org/wiki/Key:bicycle) tag (highways only). + *

                                            Original value of the bicycle tag + * (highways only).

                                            */ public static final String BICYCLE = "bicycle"; /** - * Original value of the [`foot`](http://wiki.openstreetmap.org/wiki/Key:foot) tag (highways only). + *

                                            Original value of the foot tag + * (highways only).

                                            */ public static final String FOOT = "foot"; /** - * Original value of the [`horse`](http://wiki.openstreetmap.org/wiki/Key:horse) tag (highways only). + *

                                            Original value of the horse tag + * (highways only).

                                            */ public static final String HORSE = "horse"; /** - * Original value of the [`mtb:scale`](http://wiki.openstreetmap.org/wiki/Key:mtb:scale) tag (highways only). + *

                                            Original value of the mtb:scale + * tag (highways only).

                                            */ public static final String MTB_SCALE = "mtb_scale"; /** - * Values of [`surface`](https://wiki.openstreetmap.org/wiki/Key:surface) tag devided into 2 groups `paved` - * (paved, asphalt, cobblestone, concrete, concrete:lanes, concrete:plates, metal, paving_stones, sett, - * unhewn_cobblestone, wood) and `unpaved` (unpaved, compacted, dirt, earth, fine_gravel, grass, grass_paver, - * gravel, gravel_turf, ground, ice, mud, pebblestone, salt, sand, snow, woodchips). + *

                                            Values of surface tag devided + * into 2 groups paved (paved, asphalt, cobblestone, concrete, concrete:lanes, concrete:plates, + * metal, paving_stones, sett, unhewn_cobblestone, wood) and unpaved (unpaved, compacted, dirt, + * earth, fine_gravel, grass, grass_paver, gravel, gravel_turf, ground, ice, mud, pebblestone, salt, sand, snow, + * woodchips).

                                            *

                                            * allowed values: *

                                              @@ -918,6 +969,11 @@ public class OpenMapTilesSchema { public static final String CLASS_SERVICE_CONSTRUCTION = "service_construction"; public static final String CLASS_TRACK_CONSTRUCTION = "track_construction"; public static final String CLASS_RACEWAY_CONSTRUCTION = "raceway_construction"; + public static final Set CLASS_VALUES = Set + .of("motorway", "trunk", "primary", "secondary", "tertiary", "minor", "path", "service", "track", "raceway", + "motorway_construction", "trunk_construction", "primary_construction", "secondary_construction", + "tertiary_construction", "minor_construction", "path_construction", "service_construction", + "track_construction", "raceway_construction"); public static final String SUBCLASS_RAIL = "rail"; public static final String SUBCLASS_NARROW_GAUGE = "narrow_gauge"; public static final String SUBCLASS_PRESERVED = "preserved"; @@ -934,9 +990,13 @@ public class OpenMapTilesSchema { public static final String SUBCLASS_BRIDLEWAY = "bridleway"; public static final String SUBCLASS_CORRIDOR = "corridor"; public static final String SUBCLASS_PLATFORM = "platform"; + public static final Set SUBCLASS_VALUES = Set + .of("rail", "narrow_gauge", "preserved", "funicular", "subway", "light_rail", "monorail", "tram", "pedestrian", + "path", "footway", "cycleway", "steps", "bridleway", "corridor", "platform"); public static final String BRUNNEL_BRIDGE = "bridge"; public static final String BRUNNEL_TUNNEL = "tunnel"; public static final String BRUNNEL_FORD = "ford"; + public static final Set BRUNNEL_VALUES = Set.of("bridge", "tunnel", "ford"); public static final String SERVICE_SPUR = "spur"; public static final String SERVICE_YARD = "yard"; public static final String SERVICE_SIDING = "siding"; @@ -944,8 +1004,11 @@ public class OpenMapTilesSchema { public static final String SERVICE_DRIVEWAY = "driveway"; public static final String SERVICE_ALLEY = "alley"; public static final String SERVICE_PARKING_AISLE = "parking_aisle"; + public static final Set SERVICE_VALUES = Set + .of("spur", "yard", "siding", "crossover", "driveway", "alley", "parking_aisle"); public static final String SURFACE_PAVED = "paved"; public static final String SURFACE_UNPAVED = "unpaved"; + public static final Set SURFACE_VALUES = Set.of("paved", "unpaved"); } final class FieldMappings { @@ -984,9 +1047,10 @@ public class OpenMapTilesSchema { } /** - * All [OSM Buildings](http://wiki.openstreetmap.org/wiki/Buildings). All building tags are imported ([`building= - * `](http://wiki.openstreetmap.org/wiki/Key:building)). The buildings are not yet ready for 3D rendering support and - * any help to improve this is welcomed. + *

                                              All OSM Buildings. All building tags are imported + * (building= ). The buildings are not yet + * ready for 3D rendering support and any help to improve this is welcomed.

                                              */ public interface Building extends Layer { @@ -1001,22 +1065,24 @@ public class OpenMapTilesSchema { final class Fields { /** - * An approximated height from levels and height of the building or building:part after the method of Paul Norman - * in [OSM Clear](https://github.com/ClearTables/osm-clear). For future 3D rendering of buildings. + *

                                              An approximated height from levels and height of the building or building:part after the method of Paul + * Norman in OSM Clear. For future 3D rendering of + * buildings.

                                              */ public static final String RENDER_HEIGHT = "render_height"; /** - * An approximated height from levels and height of the bottom of the building or building:part after the method - * of Paul Norman in [OSM Clear](https://github.com/ClearTables/osm-clear). For future 3D rendering of buildings. + *

                                              An approximated height from levels and height of the bottom of the building or building:part after the + * method of Paul Norman in OSM Clear. For future 3D + * rendering of buildings.

                                              */ public static final String RENDER_MIN_HEIGHT = "render_min_height"; /** - * Colour + *

                                              Colour

                                              */ public static final String COLOUR = "colour"; /** - * If True, building (part) should not be rendered in 3D. Currently, [building - * outlines](https://wiki.openstreetmap.org/wiki/Simple_3D_buildings) are marked as hide_3d. + *

                                              If True, building (part) should not be rendered in 3D. Currently, building + * outlines are marked as hide_3d.

                                              */ public static final String HIDE_3D = "hide_3d"; } @@ -1031,8 +1097,8 @@ public class OpenMapTilesSchema { } /** - * Lake center lines for labelling lake bodies. This is based of the [osm-lakelines](https://github.com/lukasmartinelli/osm-lakelines) - * project which derives nice centerlines from OSM water bodies. Only the most important lakes contain labels. + *

                                              Lake center lines for labelling lake bodies. This is based of the osm-lakelines + * project which derives nice centerlines from OSM water bodies. Only the most important lakes contain labels.

                                              */ public interface WaterName extends Layer { @@ -1047,20 +1113,22 @@ public class OpenMapTilesSchema { final class Fields { /** - * The OSM [`name`](http://wiki.openstreetmap.org/wiki/Key:name) value of the water body. + *

                                              The OSM name value of the water + * body.

                                              */ public static final String NAME = "name"; /** - * English name `name:en` if available, otherwise `name`. + *

                                              English name name:en if available, otherwise name.

                                              */ public static final String NAME_EN = "name_en"; /** - * German name `name:de` if available, otherwise `name` or `name:en`. + *

                                              German name name:de if available, otherwise name or name:en.

                                              */ public static final String NAME_DE = "name_de"; /** - * At the moment only `lake` since no ocean parts are labelled. Reserved for future use . + *

                                              At the moment only lake since no ocean parts are labelled. Reserved for future + * use.

                                              *

                                              * allowed values: *

                                                @@ -1070,7 +1138,8 @@ public class OpenMapTilesSchema { public static final String CLASS = "class"; /** - * Mark with `1` if it is an [intermittent](http://wiki.openstreetmap.org/wiki/Key:intermittent) lake. + *

                                                Mark with 1 if it is an intermittent + * lake.

                                                *

                                                * allowed values: *

                                                  @@ -1084,6 +1153,7 @@ public class OpenMapTilesSchema { final class FieldValues { public static final String CLASS_LAKE = "lake"; + public static final Set CLASS_VALUES = Set.of("lake"); } final class FieldMappings { @@ -1092,10 +1162,10 @@ public class OpenMapTilesSchema { } /** - * This is the layer for labelling the highways. Only highways that are named `name= ` and are long enough to place - * text upon appear. The OSM roads are stitched together if they contain the same name to have better label placement - * than having many small linestrings. For motorways you should use the `ref` field to label them while for other - * roads you should use `name`. + *

                                                  This is the layer for labelling the highways. Only highways that are named name= and are long + * enough to place text upon appear. The OSM roads are stitched together if they contain the same name to have better + * label placement than having many small linestrings. For motorways you should use the ref field to + * label them while for other roads you should use name.

                                                  */ public interface TransportationName extends Layer { @@ -1110,31 +1180,36 @@ public class OpenMapTilesSchema { final class Fields { /** - * The OSM [`name`](http://wiki.openstreetmap.org/wiki/Highways#Names_and_references) value of the highway. + *

                                                  The OSM name + * value of the highway.

                                                  */ public static final String NAME = "name"; /** - * English name `name:en` if available, otherwise `name`. + *

                                                  English name name:en if available, otherwise name.

                                                  */ public static final String NAME_EN = "name_en"; /** - * German name `name:de` if available, otherwise `name` or `name:en`. + *

                                                  German name name:de if available, otherwise name or name:en.

                                                  */ public static final String NAME_DE = "name_de"; /** - * The OSM [`ref`](http://wiki.openstreetmap.org/wiki/Key:ref) tag of the motorway or its network. + *

                                                  The OSM ref tag of the motorway or + * its + * network.

                                                  */ public static final String REF = "ref"; /** - * Length of the `ref` field. Useful for having a shield icon as background for labeling motorways. + *

                                                  Length of the ref field. Useful for having a shield icon as background for labeling + * motorways.

                                                  */ public static final String REF_LENGTH = "ref_length"; /** - * The network type derived mainly from [`network`](http://wiki.openstreetmap.org/wiki/Key:network) tag of the - * road. See more info about [`us- `](http://wiki.openstreetmap.org/wiki/Road_signs_in_the_United_States), - * [`ca-transcanada`](https://en.wikipedia.org/wiki/Trans-Canada_Highway), or [`gb- - * `](http://wiki.openstreetmap.org/wiki/United_Kingdom_Tagging_Guidelines#UK_roads). + *

                                                  The network type derived mainly from network + * tag of the road. See more info about us- + * , ca-transcanada, or + * gb- + * .

                                                  *

                                                  * allowed values: *

                                                    @@ -1150,7 +1225,7 @@ public class OpenMapTilesSchema { public static final String NETWORK = "network"; /** - * Distinguish between more and less important roads and roads under construction. + *

                                                    Distinguish between more and less important roads and roads under construction.

                                                    *

                                                    * allowed values: *

                                                      @@ -1181,8 +1256,8 @@ public class OpenMapTilesSchema { public static final String CLASS = "class"; /** - * Distinguish more specific classes of path: Subclass is value of the [`highway`](http://wiki.openstreetmap.org/wiki/Key:highway) - * (for paths). + *

                                                      Distinguish more specific classes of path: Subclass is value of the highway + * (for paths).

                                                      *

                                                      * allowed values: *

                                                        @@ -1199,7 +1274,7 @@ public class OpenMapTilesSchema { public static final String SUBCLASS = "subclass"; /** - * Mark whether way is a bridge, a tunnel or a ford. + *

                                                        Mark whether way is a bridge, a tunnel or a ford.

                                                        *

                                                        * allowed values: *

                                                          @@ -1210,19 +1285,19 @@ public class OpenMapTilesSchema { */ public static final String BRUNNEL = "brunnel"; /** - * Experimental feature! Filled only for steps and footways. Original value of - * [`level`](http://wiki.openstreetmap.org/wiki/Key:level) tag. + *

                                                          Experimental feature! Filled only for steps and footways. Original value of level tag.

                                                          */ public static final String LEVEL = "level"; /** - * Experimental feature! Filled only for steps and footways. Original value of - * [`layer`](http://wiki.openstreetmap.org/wiki/Key:layer) tag. + *

                                                          Experimental feature! Filled only for steps and footways. Original value of layer tag.

                                                          */ public static final String LAYER = "layer"; /** - * Experimental feature! Filled only for steps and footways. Original value of - * [`indoor`](http://wiki.openstreetmap.org/wiki/Key:indoor) tag. + *

                                                          Experimental feature! Filled only for steps and footways. Original value of indoor tag.

                                                          *

                                                          * allowed values: *

                                                            @@ -1241,6 +1316,8 @@ public class OpenMapTilesSchema { public static final String NETWORK_GB_MOTORWAY = "gb-motorway"; public static final String NETWORK_GB_TRUNK = "gb-trunk"; public static final String NETWORK_ROAD = "road"; + public static final Set NETWORK_VALUES = Set + .of("us-interstate", "us-highway", "us-state", "ca-transcanada", "gb-motorway", "gb-trunk", "road"); public static final String CLASS_MOTORWAY = "motorway"; public static final String CLASS_TRUNK = "trunk"; public static final String CLASS_PRIMARY = "primary"; @@ -1263,6 +1340,11 @@ public class OpenMapTilesSchema { public static final String CLASS_RACEWAY_CONSTRUCTION = "raceway_construction"; public static final String CLASS_RAIL = "rail"; public static final String CLASS_TRANSIT = "transit"; + public static final Set CLASS_VALUES = Set + .of("motorway", "trunk", "primary", "secondary", "tertiary", "minor", "service", "track", "path", "raceway", + "motorway_construction", "trunk_construction", "primary_construction", "secondary_construction", + "tertiary_construction", "minor_construction", "service_construction", "track_construction", + "path_construction", "raceway_construction", "rail", "transit"); public static final String SUBCLASS_PEDESTRIAN = "pedestrian"; public static final String SUBCLASS_PATH = "path"; public static final String SUBCLASS_FOOTWAY = "footway"; @@ -1271,9 +1353,12 @@ public class OpenMapTilesSchema { public static final String SUBCLASS_BRIDLEWAY = "bridleway"; public static final String SUBCLASS_CORRIDOR = "corridor"; public static final String SUBCLASS_PLATFORM = "platform"; + public static final Set SUBCLASS_VALUES = Set + .of("pedestrian", "path", "footway", "cycleway", "steps", "bridleway", "corridor", "platform"); public static final String BRUNNEL_BRIDGE = "bridge"; public static final String BRUNNEL_TUNNEL = "tunnel"; public static final String BRUNNEL_FORD = "ford"; + public static final Set BRUNNEL_VALUES = Set.of("bridge", "tunnel", "ford"); } final class FieldMappings { @@ -1282,10 +1367,11 @@ public class OpenMapTilesSchema { } /** - * The place layer consists out of [countries](http://wiki.openstreetmap.org/wiki/Tag:place%3Dcountry), - * [states](http://wiki.openstreetmap.org/wiki/Tag:place%3Dstate) and [cities](http://wiki.openstreetmap.org/wiki/Key:place). - * Apart from the roads this is also one of the more important layers to create a beautiful map. We suggest you use - * different font styles and sizes to create a text hierarchy. + *

                                                            The place layer consists out of countries, + * states and cities. Apart from the roads this is also one of the more + * important layers to create a beautiful map. We suggest you use different font styles and sizes to create a text + * hierarchy.

                                                            */ public interface Place extends Layer { @@ -1300,21 +1386,21 @@ public class OpenMapTilesSchema { final class Fields { /** - * The OSM [`name`](http://wiki.openstreetmap.org/wiki/Key:name) value of the POI. + *

                                                            The OSM name value of the POI.

                                                            */ public static final String NAME = "name"; /** - * English name `name:en` if available, otherwise `name`. + *

                                                            English name name:en if available, otherwise name.

                                                            */ public static final String NAME_EN = "name_en"; /** - * German name `name:de` if available, otherwise `name` or `name:en`. + *

                                                            German name name:de if available, otherwise name or name:en.

                                                            */ public static final String NAME_DE = "name_de"; /** - * The capital field marks the [`admin_level`](http://wiki.openstreetmap.org/wiki/Tag:boundary%3Dadministrative#admin_level) - * of the boundary the place is a capital of. + *

                                                            The capital field marks the admin_level + * of the boundary the place is a capital of.

                                                            *

                                                            * allowed values: *

                                                              @@ -1325,9 +1411,10 @@ public class OpenMapTilesSchema { public static final String CAPITAL = "capital"; /** - * Original value of the [`place`](http://wiki.openstreetmap.org/wiki/Key:place) tag. Distinguish between - * continents, countries, states and places like settlements or smaller entities. Use class to separately style - * the different places and build a text hierarchy according to their importance. + *

                                                              Original value of the place tag. + * Distinguish between continents, countries, states and places like settlements or smaller entities. Use + * class to separately style the different places and build a text hierarchy according to their + * importance.

                                                              *

                                                              * allowed values: *

                                                                @@ -1346,19 +1433,22 @@ public class OpenMapTilesSchema { */ public static final String CLASS = "class"; /** - * Two-letter country code [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2). Available only - * for `class=country`. Original value of the [`country_code_iso3166_1_alpha_2`](http://wiki.openstreetmap.org/wiki/Tag:place%3Dcountry) - * tag. + *

                                                                Two-letter country code ISO 3166-1 alpha-2. + * Available only for class=country. Original value of the country_code_iso3166_1_alpha_2 + * tag.

                                                                */ public static final String ISO_A2 = "iso_a2"; /** - * Countries, states and the most important cities all have a rank to boost their importance on the map. The rank - * field for counries and states ranges from `1` to `6` while the rank field for cities ranges from `1` to `10` - * for the most important cities and continues from `10` serially based on the local importance of the city - * (derived from population and city class). You can use the rank to limit density of labels or improve the text - * hierarchy. The rank value is a combination of the Natural Earth `scalerank`, `labelrank` and `datarank` values - * for countries and states and for cities consists out of a shifted Natural Earth `scalerank` combined with a - * local rank within a grid for cities that do not have a Natural Earth `scalerank`. + *

                                                                Countries, states and the most important cities all have a rank to boost their importance + * on the map. The rank field for counries and states ranges from 1 to + * 6 while the rank field for cities ranges from 1 to 10 + * for the most important cities and continues from 10 serially based on the local importance of the + * city (derived from population and city class). You can use the rank to limit density of labels + * or improve the text hierarchy. The rank value is a combination of the Natural Earth scalerank, + * labelrank and datarank values for countries and states and for cities consists out + * of + * a shifted Natural Earth scalerank combined with a local rank within a grid for cities that do not + * have a Natural Earth scalerank.

                                                                */ public static final String RANK = "rank"; } @@ -1376,6 +1466,9 @@ public class OpenMapTilesSchema { public static final String CLASS_QUARTER = "quarter"; public static final String CLASS_NEIGHBOURHOOD = "neighbourhood"; public static final String CLASS_ISOLATED_DWELLING = "isolated_dwelling"; + public static final Set CLASS_VALUES = Set + .of("continent", "country", "state", "city", "town", "village", "hamlet", "suburb", "quarter", "neighbourhood", + "isolated_dwelling"); } final class FieldMappings { @@ -1384,8 +1477,10 @@ public class OpenMapTilesSchema { } /** - * Everything in OpenStreetMap which contains a `addr:housenumber` tag useful for labelling housenumbers on a map. - * This adds significant size to z14 . For buildings the centroid of the building is used as housenumber. + *

                                                                Everything in OpenStreetMap which contains a addr:housenumber tag useful for labelling + * housenumbers + * on a map. This adds significant size to z14. For buildings the centroid of the building is used as + * housenumber.

                                                                */ public interface Housenumber extends Layer { @@ -1400,7 +1495,8 @@ public class OpenMapTilesSchema { final class Fields { /** - * Value of the [`addr:housenumber`](http://wiki.openstreetmap.org/wiki/Key:addr) tag. + *

                                                                Value of the addr:housenumber + * tag.

                                                                */ public static final String HOUSENUMBER = "housenumber"; } @@ -1415,8 +1511,8 @@ public class OpenMapTilesSchema { } /** - * [Points of interests](http://wiki.openstreetmap.org/wiki/Points_of_interest) containing a of a variety of - * OpenStreetMap tags. Mostly contains amenities, sport, shop and tourist POIs. + *

                                                                Points of interests containing a of a + * variety of OpenStreetMap tags. Mostly contains amenities, sport, shop and tourist POIs.

                                                                */ public interface Poi extends Layer { @@ -1431,22 +1527,23 @@ public class OpenMapTilesSchema { final class Fields { /** - * The OSM [`name`](http://wiki.openstreetmap.org/wiki/Key:name) value of the POI. + *

                                                                The OSM name value of the POI.

                                                                */ public static final String NAME = "name"; /** - * English name `name:en` if available, otherwise `name`. + *

                                                                English name name:en if available, otherwise name.

                                                                */ public static final String NAME_EN = "name_en"; /** - * German name `name:de` if available, otherwise `name` or `name:en`. + *

                                                                German name name:de if available, otherwise name or name:en.

                                                                */ public static final String NAME_DE = "name_de"; /** - * More general classes of POIs. If there is no more general `class` for the `subclass` this field will contain - * the same value as `subclass`. But for example for schools you only need to style the class `school` to filter - * the subclasses `school` and `kindergarten`. Or use the class `shop` to style all shops. + *

                                                                More general classes of POIs. If there is no more general class for the subclass + * this field will contain the same value as subclass. But for example for schools you only need to + * style the class school to filter the subclasses school and kindergarten. + * Or use the class shop to style all shops.

                                                                *

                                                                * allowed values: *

                                                                  @@ -1487,29 +1584,38 @@ public class OpenMapTilesSchema { */ public static final String CLASS = "class"; /** - * Original value of either the [`amenity`](http://wiki.openstreetmap.org/wiki/Key:amenity), - * [`barrier`](http://wiki.openstreetmap.org/wiki/Key:barrier), [`historic`](http://wiki.openstreetmap.org/wiki/Key:historic), - * [`information`](http://wiki.openstreetmap.org/wiki/Key:information), [`landuse`](http://wiki.openstreetmap.org/wiki/Key:landuse), - * [`leisure`](http://wiki.openstreetmap.org/wiki/Key:leisure), [`railway`](http://wiki.openstreetmap.org/wiki/Key:railway), - * [`shop`](http://wiki.openstreetmap.org/wiki/Key:shop), [`sport`](http://wiki.openstreetmap.org/wiki/Key:sport), - * [`station`](http://wiki.openstreetmap.org/wiki/Key:station), [`religion`](http://wiki.openstreetmap.org/wiki/Key:religion), - * [`tourism`](http://wiki.openstreetmap.org/wiki/Key:tourism), [`aerialway`](http://wiki.openstreetmap.org/wiki/Key:aerialway), - * [`building`](http://wiki.openstreetmap.org/wiki/Key:building), [`highway`](http://wiki.openstreetmap.org/wiki/Key:highway) - * or [`waterway`](http://wiki.openstreetmap.org/wiki/Key:waterway) tag. Use this to do more precise styling. + *

                                                                  Original value of either the amenity, + * barrier, historic, information, landuse, leisure, railway, shop, sport, station, religion, tourism, aerialway, building, highway or waterway tag. Use this to do more + * precise styling.

                                                                  */ public static final String SUBCLASS = "subclass"; /** - * The POIs are ranked ascending according to their importance within a grid. The `rank` value shows the local - * relative importance of a POI within it's cell in the grid. This can be used to reduce label density at z14 . - * Since all POIs already need to be contained at z14 you can use `less than rank=10` epxression to limit POIs. At - * some point like z17 you can show all POIs. + *

                                                                  The POIs are ranked ascending according to their importance within a grid. The rank value + * shows + * the local relative importance of a POI within it's cell in the grid. This can be used to reduce label density + * at z14. Since all POIs already need to be contained at z14 you can use less than + * rank=10 epxression to limit POIs. At some point like z17 you can show all POIs.

                                                                  */ public static final String RANK = "rank"; /** - * Experimental feature! Indicates main platform of public transport stops (buses, trams, and subways). Grouping - * of platforms is implemented using [`uic_ref`](http://wiki.openstreetmap.org/wiki/Key:uic_ref) tag that is not - * used worldwide. + *

                                                                  Experimental feature! Indicates main platform of public transport stops (buses, trams, and subways). + * Grouping of platforms is implemented using uic_ref + * tag that is not used worldwide.

                                                                  *

                                                                  * allowed values: *

                                                                    @@ -1518,16 +1624,16 @@ public class OpenMapTilesSchema { */ public static final String AGG_STOP = "agg_stop"; /** - * Original value of [`level`](http://wiki.openstreetmap.org/wiki/Key:level) tag. + *

                                                                    Original value of level tag.

                                                                    */ public static final String LEVEL = "level"; /** - * Original value of [`layer`](http://wiki.openstreetmap.org/wiki/Key:layer) tag. + *

                                                                    Original value of layer tag.

                                                                    */ public static final String LAYER = "layer"; /** - * Original value of [`indoor`](http://wiki.openstreetmap.org/wiki/Key:indoor) tag. + *

                                                                    Original value of indoor tag.

                                                                    *

                                                                    * allowed values: *

                                                                      @@ -1572,6 +1678,11 @@ public class OpenMapTilesSchema { public static final String CLASS_CLOTHING_STORE = "clothing_store"; public static final String CLASS_SWIMMING = "swimming"; public static final String CLASS_CASTLE = "castle"; + public static final Set CLASS_VALUES = Set + .of("shop", "town_hall", "golf", "fast_food", "park", "bus", "railway", "aerialway", "entrance", "campsite", + "laundry", "grocery", "library", "college", "lodging", "ice_cream", "post", "cafe", "school", "alcohol_shop", + "bar", "harbor", "car", "hospital", "cemetery", "attraction", "beer", "music", "stadium", "art_gallery", + "clothing_store", "swimming", "castle"); } final class FieldMappings { @@ -1621,7 +1732,7 @@ public class OpenMapTilesSchema { } /** - * [Aerodrome labels](http://wiki.openstreetmap.org/wiki/Tag:aeroway%3Daerodrome) + *

                                                                      Aerodrome labels

                                                                      */ public interface AerodromeLabel extends Layer { @@ -1636,21 +1747,23 @@ public class OpenMapTilesSchema { final class Fields { /** - * The OSM [`name`](http://wiki.openstreetmap.org/wiki/Key:name) value of the aerodrome. + *

                                                                      The OSM name value of the + * aerodrome.

                                                                      */ public static final String NAME = "name"; /** - * English name `name:en` if available, otherwise `name`. + *

                                                                      English name name:en if available, otherwise name.

                                                                      */ public static final String NAME_EN = "name_en"; /** - * German name `name:de` if available, otherwise `name` or `name:en`. + *

                                                                      German name name:de if available, otherwise name or name:en.

                                                                      */ public static final String NAME_DE = "name_de"; /** - * Distinguish between more and less important aerodromes. Class is derived from the value of - * [`aerodrome`](http://wiki.openstreetmap.org/wiki/Proposed_features/Aerodrome) and `aerodrome:type` tags. + *

                                                                      Distinguish between more and less important aerodromes. Class is derived from the value of aerodrome and + * aerodrome:type tags.

                                                                      *

                                                                      * allowed values: *

                                                                        @@ -1664,19 +1777,19 @@ public class OpenMapTilesSchema { */ public static final String CLASS = "class"; /** - * 3-character code issued by the IATA. + *

                                                                        3-character code issued by the IATA.

                                                                        */ public static final String IATA = "iata"; /** - * 4-letter code issued by the ICAO. + *

                                                                        4-letter code issued by the ICAO.

                                                                        */ public static final String ICAO = "icao"; /** - * Elevation (`ele`) in meters. + *

                                                                        Elevation (ele) in meters.

                                                                        */ public static final String ELE = "ele"; /** - * Elevation (`ele`) in feets. + *

                                                                        Elevation (ele) in feets.

                                                                        */ public static final String ELE_FT = "ele_ft"; } @@ -1689,6 +1802,8 @@ public class OpenMapTilesSchema { public static final String CLASS_MILITARY = "military"; public static final String CLASS_PRIVATE = "private"; public static final String CLASS_OTHER = "other"; + public static final Set CLASS_VALUES = Set + .of("international", "public", "regional", "military", "private", "other"); } final class FieldMappings { diff --git a/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/layers/Boundary.java b/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/layers/Boundary.java index a9cf3b8b..8091b8a8 100644 --- a/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/layers/Boundary.java +++ b/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/layers/Boundary.java @@ -1,14 +1,344 @@ package com.onthegomap.flatmap.openmaptiles.layers; -import com.onthegomap.flatmap.Arguments; -import com.onthegomap.flatmap.Translations; -import com.onthegomap.flatmap.monitoring.Stats; -import com.onthegomap.flatmap.openmaptiles.generated.OpenMapTilesSchema; +import static com.onthegomap.flatmap.geo.GeoUtils.JTS_FACTORY; -public class Boundary implements OpenMapTilesSchema.Boundary { +import com.carrotsearch.hppc.LongObjectMap; +import com.graphhopper.coll.GHLongObjectHashMap; +import com.graphhopper.reader.ReaderElementUtils; +import com.graphhopper.reader.ReaderRelation; +import com.onthegomap.flatmap.Arguments; +import com.onthegomap.flatmap.FeatureCollector; +import com.onthegomap.flatmap.FeatureMerge; +import com.onthegomap.flatmap.MemoryEstimator; +import com.onthegomap.flatmap.Parse; +import com.onthegomap.flatmap.SourceFeature; +import com.onthegomap.flatmap.Translations; +import com.onthegomap.flatmap.VectorTileEncoder; +import com.onthegomap.flatmap.geo.GeoUtils; +import com.onthegomap.flatmap.geo.GeometryException; +import com.onthegomap.flatmap.monitoring.Stats; +import com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile; +import com.onthegomap.flatmap.openmaptiles.generated.OpenMapTilesSchema; +import com.onthegomap.flatmap.read.OpenStreetMapReader; +import com.onthegomap.flatmap.read.OsmMultipolygon; +import com.onthegomap.flatmap.read.ReaderFeature; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryComponentFilter; +import org.locationtech.jts.geom.LineSegment; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; +import org.locationtech.jts.operation.linemerge.LineMerger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Boundary implements + OpenMapTilesSchema.Boundary, + OpenMapTilesProfile.NaturalEarthProcessor, + OpenMapTilesProfile.OsmRelationPreprocessor, + OpenMapTilesProfile.OsmAllProcessor, + OpenMapTilesProfile.FeaturePostProcessor, + OpenMapTilesProfile.FinishHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(Boundary.class); + private static final double COUNTRY_TEST_OFFSET = GeoUtils.metersToPixelAtEquator(0, 100) / 256d; + private final Map regionNames = new HashMap<>(); + private final Map> regionGeometries = new HashMap<>(); + private final Map> boundariesToMerge = new HashMap<>(); + private final Stats stats; public Boundary(Translations translations, Arguments args, Stats stats) { + this.stats = stats; } - // TODO implement + private static boolean isDisputed(Map tags) { + return Parse.bool(tags.get("disputed")) || + Parse.bool(tags.get("dispute")) || + "dispute".equals(tags.get("border_status")) || + tags.containsKey("disputed_by") || + tags.containsKey("claimed_by"); + } + + private static String editName(String name) { + return name == null ? null : name.replace(" at ", "") + .replaceAll("\\s+", "") + .replace("Extentof", ""); + } + + @Override + public void release() { + regionGeometries.clear(); + boundariesToMerge.clear(); + regionNames.clear(); + } + + @Override + public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) { + boolean disputed = feature.getString("featurecla", "").startsWith("Disputed"); + record BoundaryInfo(int adminLevel, int minzoom, int maxzoom) {} + BoundaryInfo info = switch (table) { + case "ne_110m_admin_0_boundary_lines_land" -> new BoundaryInfo(2, 0, 0); + case "ne_50m_admin_0_boundary_lines_land" -> new BoundaryInfo(2, 1, 3); + case "ne_10m_admin_0_boundary_lines_land" -> feature.hasTag("featurecla", "Lease Limit") ? null + : new BoundaryInfo(2, 4, 4); + case "ne_10m_admin_1_states_provinces_lines" -> { + Double minZoom = Parse.parseDoubleOrNull(feature.getTag("min_zoom")); + yield minZoom != null && minZoom <= 7 ? new BoundaryInfo(4, 1, 4) : null; + } + default -> null; + }; + if (info != null) { + features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE) + .setZoomRange(info.minzoom, info.maxzoom) + .setMinPixelSizeAtAllZooms(0) + .setAttr(Fields.ADMIN_LEVEL, info.adminLevel) + .setAttr(Fields.MARITIME, 0) + .setAttr(Fields.DISPUTED, disputed ? 1 : 0); + } + } + + @Override + public List postProcess(int zoom, List items) + throws GeometryException { + double tolerance = zoom >= 14 ? 256d / 4096d : 0.1; + return FeatureMerge.mergeLineStrings(items, 1, tolerance, BUFFER_SIZE); + } + + @Override + public void finish(String sourceName, FeatureCollector.Factory featureCollectors, + Consumer next) { + if (OpenMapTilesProfile.OSM_SOURCE.equals(sourceName)) { + var timer = stats.startTimer("boundaries"); + LOGGER.info("[boundaries] Creating polygons for " + regionGeometries.size() + " boundaries"); + LongObjectMap countryBoundaries = new GHLongObjectHashMap<>(); + for (var entry : regionGeometries.entrySet()) { + Long countryCode = entry.getKey(); + List seqs = new ArrayList<>(); + for (Geometry geometry : entry.getValue()) { + geometry.apply((GeometryComponentFilter) geom -> { + if (geom instanceof LineString lineString) { + seqs.add(lineString.getCoordinateSequence()); + } + }); + } + try { + countryBoundaries.put(countryCode, PreparedGeometryFactory.prepare( + GeoUtils.fixPolygon( + OsmMultipolygon.build(seqs) + ) + )); + } catch (GeometryException e) { + LOGGER.warn("[boundaries] Unable to build boundary polygon for " + countryCode + ": " + e.getMessage()); + } + } + LOGGER.info("[boundaries] Finished creating polygons"); + + long number = 0; + for (var entry : boundariesToMerge.entrySet()) { + number++; + CountryBoundaryComponent key = entry.getKey(); + LineMerger merger = new LineMerger(); + for (Geometry geom : entry.getValue()) { + merger.add(geom); + } + entry.getValue().clear(); + for (Object merged : merger.getMergedLineStrings()) { + if (merged instanceof LineString lineString) { + Long rightCountry = null, leftCountry = null; + int numPoints = lineString.getNumPoints(); + int middle = Math.max(0, Math.min(numPoints - 2, numPoints / 2)); + Coordinate a = lineString.getCoordinateN(middle); + Coordinate b = lineString.getCoordinateN(middle + 1); + LineSegment segment = new LineSegment(a, b); + Point right = JTS_FACTORY.createPoint(segment.pointAlongOffset(0.5, COUNTRY_TEST_OFFSET)); + Point left = JTS_FACTORY.createPoint(segment.pointAlongOffset(0.5, -COUNTRY_TEST_OFFSET)); + for (Long regionId : key.regions) { + PreparedGeometry geom = countryBoundaries.get(regionId); + if (geom != null) { + if (geom.contains(right)) { + rightCountry = regionId; + } else if (geom.contains(left)) { + leftCountry = regionId; + } + } + } + + if (leftCountry == null && rightCountry == null) { + LOGGER.warn("[boundaries] no left or right country for " + key); + } + + var features = featureCollectors.get(new ReaderFeature( + GeoUtils.worldToLatLonCoords(lineString), + Map.of(), + number + )); + features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE) + .setAttr(Fields.ADMIN_LEVEL, key.adminLevel) + .setAttr(Fields.DISPUTED, key.disputed ? 1 : 0) + .setAttr(Fields.MARITIME, key.maritime ? 1 : 0) + .setAttr(Fields.CLAIMED_BY, key.claimedBy) + .setAttr(Fields.DISPUTED_NAME, key.disputed ? editName(key.name) : null) + .setAttr(Fields.ADM0_L, regionNames.get(leftCountry)) + .setAttr(Fields.ADM0_R, regionNames.get(rightCountry)) + .setMinPixelSizeAtAllZooms(0) + .setZoomRange(key.minzoom, 14); + for (var feature : features) { + next.accept(feature); + } + } + } + } + timer.stop(); + } + } + + @Override + public List preprocessOsmRelation(ReaderRelation relation) { + String typeTag = relation.getTag("type"); + if ("boundary".equals(typeTag) && relation.hasTag("admin_level") && relation.hasTag("boundary", "administrative")) { + Integer adminLevelValue = Parse.parseIntSubstring(relation.getTag("admin_level")); + String code = relation.getTag("ISO3166-1:alpha3"); + if (adminLevelValue != null && adminLevelValue >= 2 && adminLevelValue <= 2) { + boolean disputed = isDisputed(ReaderElementUtils.getProperties(relation)); + if (code != null) { + synchronized (regionNames) { + regionNames.put(relation.getId(), code); + } + } + return List.of(new BoundaryRelation( + relation.getId(), + adminLevelValue, + disputed, + relation.getTag("name"), + disputed ? relation.getTag("claimed_by") : null, + code + )); + } + } + return null; + } + + @Override + public void processAllOsm(SourceFeature feature, FeatureCollector features) { + if (!feature.canBeLine()) { + return; + } + var relationInfos = feature.relationInfo(BoundaryRelation.class); + if (!relationInfos.isEmpty()) { + int minAdminLevel = Integer.MAX_VALUE; + String disputedName = null, claimedBy = null; + Set regionIds = new HashSet<>(); + boolean disputed = false; + for (var info : relationInfos) { + BoundaryRelation rel = info.relation(); + disputed |= rel.disputed; + if (rel.adminLevel < minAdminLevel) { + minAdminLevel = rel.adminLevel; + } + if (rel.disputed) { + disputedName = disputedName == null ? rel.name : disputedName; + claimedBy = claimedBy == null ? rel.claimedBy : claimedBy; + } + if (minAdminLevel == 2 && regionNames.containsKey(info.relation().id)) { + regionIds.add(info.relation().id); + } + } + + if (minAdminLevel <= 10) { + boolean wayIsDisputed = isDisputed(feature.properties()); + disputed |= wayIsDisputed; + if (wayIsDisputed) { + disputedName = disputedName == null ? feature.getString("name") : disputedName; + claimedBy = claimedBy == null ? feature.getString("claimed_by") : claimedBy; + } + boolean maritime = feature.getBoolean("maritime") || + feature.hasTag("natural", "coastline") || + feature.hasTag("boundary_type", "maritime"); + int minzoom = + (maritime && minAdminLevel == 2) ? 4 : + minAdminLevel <= 4 ? 5 : + minAdminLevel <= 6 ? 9 : + minAdminLevel <= 8 ? 11 : 12; + if (!regionIds.isEmpty()) { + // save for later + try { + CountryBoundaryComponent component = new CountryBoundaryComponent( + minAdminLevel, + disputed, + maritime, + minzoom, + feature.line(), + regionIds, + claimedBy, + disputedName + ); + synchronized (regionGeometries) { + boundariesToMerge.computeIfAbsent(component.groupingKey(), key -> new ArrayList<>()).add(component.line); + for (var info : relationInfos) { + var rel = info.relation(); + if (rel.adminLevel <= 2) { + regionGeometries.computeIfAbsent(rel.id, id -> new ArrayList<>()).add(component.line); + } + } + } + } catch (GeometryException e) { + LOGGER.warn("Cannot extract boundary line from " + feature); + } + } else { + features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE) + .setAttr(Fields.ADMIN_LEVEL, minAdminLevel) + .setAttr(Fields.DISPUTED, disputed ? 1 : 0) + .setAttr(Fields.MARITIME, maritime ? 1 : 0) + .setMinPixelSizeAtAllZooms(0) + .setZoomRange(minzoom, 14) + .setAttr(Fields.CLAIMED_BY, claimedBy) + .setAttr(Fields.DISPUTED_NAME, editName(disputedName)); + } + } + } + } + + private static record BoundaryRelation( + long id, + int adminLevel, + boolean disputed, + String name, + String claimedBy, + String iso3166alpha3 + ) implements OpenStreetMapReader.RelationInfo { + + @Override + public long estimateMemoryUsageBytes() { + return 29 + 8 + MemoryEstimator.size(name) + + 8 + MemoryEstimator.size(claimedBy) + + 8 + MemoryEstimator.size(iso3166alpha3); + } + } + + private static record CountryBoundaryComponent( + int adminLevel, + boolean disputed, + boolean maritime, + int minzoom, + Geometry line, + Set regions, + String claimedBy, + String name + ) { + + CountryBoundaryComponent groupingKey() { + return new CountryBoundaryComponent(adminLevel, disputed, maritime, minzoom, null, regions, claimedBy, name); + } + + } } diff --git a/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/OpenMaptilesProfileTest.java b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/OpenMaptilesProfileTest.java index 5b5100b1..74b15ba4 100644 --- a/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/OpenMaptilesProfileTest.java +++ b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/OpenMaptilesProfileTest.java @@ -1,859 +1,23 @@ package com.onthegomap.flatmap.openmaptiles; -import static com.onthegomap.flatmap.TestUtils.assertSubmap; -import static com.onthegomap.flatmap.TestUtils.newLineString; -import static com.onthegomap.flatmap.TestUtils.newPoint; -import static com.onthegomap.flatmap.TestUtils.rectangle; -import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.LAKE_CENTERLINE_SOURCE; -import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.NATURAL_EARTH_SOURCE; -import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.OSM_SOURCE; -import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.WATER_POLYGON_SOURCE; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.DynamicTest.dynamicTest; import com.graphhopper.reader.ReaderNode; import com.onthegomap.flatmap.Arguments; -import com.onthegomap.flatmap.CommonParams; -import com.onthegomap.flatmap.FeatureCollector; -import com.onthegomap.flatmap.SourceFeature; -import com.onthegomap.flatmap.TestUtils; import com.onthegomap.flatmap.Translations; -import com.onthegomap.flatmap.VectorTileEncoder; import com.onthegomap.flatmap.Wikidata; -import com.onthegomap.flatmap.geo.GeoUtils; -import com.onthegomap.flatmap.geo.GeometryException; import com.onthegomap.flatmap.monitoring.Stats; -import com.onthegomap.flatmap.openmaptiles.layers.MountainPeak; -import com.onthegomap.flatmap.openmaptiles.layers.Waterway; -import com.onthegomap.flatmap.read.ReaderFeature; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.stream.StreamSupport; -import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestFactory; public class OpenMaptilesProfileTest { private final Wikidata.WikidataTranslations wikidataTranslations = new Wikidata.WikidataTranslations(); private final Translations translations = Translations.defaultProvider(List.of("en", "es", "de")) .addTranslationProvider(wikidataTranslations); - - private final CommonParams params = CommonParams.defaults(); private final OpenMapTilesProfile profile = new OpenMapTilesProfile(translations, Arguments.of(), new Stats.InMemory()); - private final Stats stats = new Stats.InMemory(); - private final FeatureCollector.Factory featureCollectorFactory = new FeatureCollector.Factory(params, stats); - - private static void assertFeatures(int zoom, List> expected, FeatureCollector actual) { - List actualList = StreamSupport.stream(actual.spliterator(), false).toList(); - assertEquals(expected.size(), actualList.size(), "size"); - for (int i = 0; i < expected.size(); i++) { - assertSubmap(expected.get(i), TestUtils.toMap(actualList.get(i), zoom)); - } - } - - @TestFactory - public List mountainPeakProcessing() { - wikidataTranslations.put(123, "es", "es wd name"); - return List.of( - dynamicTest("happy path", () -> { - var peak = process(pointFeature(Map.of( - "natural", "peak", - "name", "test", - "ele", "100", - "wikidata", "Q123" - ))); - assertFeatures(14, List.of(Map.of( - "class", "peak", - "ele", 100, - "ele_ft", 328, - - "_layer", "mountain_peak", - "_type", "point", - "_minzoom", 7, - "_maxzoom", 14, - "_buffer", 64d - )), peak); - assertFeatures(14, List.of(Map.of( - "name:latin", "test", - "name", "test", - "name:es", "es wd name" - )), peak); - }), - - dynamicTest("labelgrid", () -> { - var peak = process(pointFeature(Map.of( - "natural", "peak", - "ele", "100" - ))); - assertFeatures(14, List.of(Map.of( - "_labelgrid_limit", 0 - )), peak); - assertFeatures(13, List.of(Map.of( - "_labelgrid_limit", 5, - "_labelgrid_size", 100d - )), peak); - }), - - dynamicTest("volcano", () -> - assertFeatures(14, List.of(Map.of( - "class", "volcano" - )), process(pointFeature(Map.of( - "natural", "volcano", - "ele", "100" - ))))), - - dynamicTest("no elevation", () -> - assertFeatures(14, List.of(), process(pointFeature(Map.of( - "natural", "volcano" - ))))), - - dynamicTest("bogus elevation", () -> - assertFeatures(14, List.of(), process(pointFeature(Map.of( - "natural", "volcano", - "ele", "11000" - ))))), - - dynamicTest("ignore lines", () -> - assertFeatures(14, List.of(), process(lineFeature(Map.of( - "natural", "peak", - "name", "name", - "ele", "100" - ))))), - - dynamicTest("zorder", () -> { - assertFeatures(14, List.of(Map.of( - "_zorder", 100 - )), process(pointFeature(Map.of( - "natural", "peak", - "ele", "100" - )))); - assertFeatures(14, List.of(Map.of( - "_zorder", 10100 - )), process(pointFeature(Map.of( - "natural", "peak", - "name", "name", - "ele", "100" - )))); - assertFeatures(14, List.of(Map.of( - "_zorder", 20100 - )), process(pointFeature(Map.of( - "natural", "peak", - "name", "name", - "wikipedia", "wikilink", - "ele", "100" - )))); - }) - ); - } - - @Test - public void testMountainPeakPostProcessing() throws GeometryException { - assertEquals(List.of(), profile.postProcessLayerFeatures(MountainPeak.LAYER_NAME, 13, List.of())); - - assertEquals(List.of(pointFeature( - MountainPeak.LAYER_NAME, - Map.of("rank", 1), - 1 - )), profile.postProcessLayerFeatures(MountainPeak.LAYER_NAME, 13, List.of(pointFeature( - MountainPeak.LAYER_NAME, - Map.of(), - 1 - )))); - - assertEquals(List.of( - pointFeature( - MountainPeak.LAYER_NAME, - Map.of("rank", 2, "name", "a"), - 1 - ), pointFeature( - MountainPeak.LAYER_NAME, - Map.of("rank", 1, "name", "b"), - 1 - ), pointFeature( - MountainPeak.LAYER_NAME, - Map.of("rank", 1, "name", "c"), - 2 - ) - ), profile.postProcessLayerFeatures(MountainPeak.LAYER_NAME, 13, List.of( - pointFeature( - MountainPeak.LAYER_NAME, - Map.of("name", "a"), - 1 - ), - pointFeature( - MountainPeak.LAYER_NAME, - Map.of("name", "b"), - 1 - ), - pointFeature( - MountainPeak.LAYER_NAME, - Map.of("name", "c"), - 2 - ) - ))); - } - - @TestFactory - public List aerodromeLabel() { - wikidataTranslations.put(123, "es", "es wd name"); - return List.of( - dynamicTest("happy path point", () -> { - assertFeatures(14, List.of(Map.of( - "class", "international", - "ele", 100, - "ele_ft", 328, - "name", "osm name", - "name:es", "es wd name", - - "_layer", "aerodrome_label", - "_type", "point", - "_minzoom", 10, - "_maxzoom", 14, - "_buffer", 64d - )), process(pointFeature(Map.of( - "aeroway", "aerodrome", - "name", "osm name", - "wikidata", "Q123", - "ele", "100", - "aerodrome", "international", - "iata", "123", - "icao", "1234" - )))); - }), - - dynamicTest("international", () -> { - assertFeatures(14, List.of(Map.of( - "class", "international", - "_layer", "aerodrome_label" - )), process(pointFeature(Map.of( - "aeroway", "aerodrome", - "aerodrome_type", "international" - )))); - }), - - dynamicTest("public", () -> { - assertFeatures(14, List.of(Map.of( - "class", "public", - "_layer", "aerodrome_label" - )), process(pointFeature(Map.of( - "aeroway", "aerodrome", - "aerodrome_type", "public airport" - )))); - assertFeatures(14, List.of(Map.of( - "class", "public", - "_layer", "aerodrome_label" - )), process(pointFeature(Map.of( - "aeroway", "aerodrome", - "aerodrome_type", "civil" - )))); - }), - - dynamicTest("military", () -> { - assertFeatures(14, List.of(Map.of( - "class", "military", - "_layer", "aerodrome_label" - )), process(pointFeature(Map.of( - "aeroway", "aerodrome", - "aerodrome_type", "military airport" - )))); - assertFeatures(14, List.of(Map.of( - "class", "military", - "_layer", "aerodrome_label" - )), process(pointFeature(Map.of( - "aeroway", "aerodrome", - "military", "airfield" - )))); - }), - - dynamicTest("private", () -> { - assertFeatures(14, List.of(Map.of( - "class", "private", - "_layer", "aerodrome_label" - )), process(pointFeature(Map.of( - "aeroway", "aerodrome", - "aerodrome_type", "private" - )))); - assertFeatures(14, List.of(Map.of( - "class", "private", - "_layer", "aerodrome_label" - )), process(pointFeature(Map.of( - "aeroway", "aerodrome", - "aerodrome", "private" - )))); - }), - - dynamicTest("other", () -> { - assertFeatures(14, List.of(Map.of( - "class", "other", - "_layer", "aerodrome_label" - )), process(pointFeature(Map.of( - "aeroway", "aerodrome" - )))); - }), - - dynamicTest("ignore non-points", () -> { - assertFeatures(14, List.of(), process(lineFeature(Map.of( - "aeroway", "aerodrome" - )))); - }) - ); - } - - @Test - public void aerowayGate() { - assertFeatures(14, List.of(Map.of( - "class", "gate", - "ref", "123", - - "_layer", "aeroway", - "_type", "point", - "_minzoom", 14, - "_maxzoom", 14, - "_buffer", 4d - )), process(pointFeature(Map.of( - "aeroway", "gate", - "ref", "123" - )))); - assertFeatures(14, List.of(), process(lineFeature(Map.of( - "aeroway", "gate" - )))); - assertFeatures(14, List.of(), process(polygonFeature(Map.of( - "aeroway", "gate" - )))); - } - - @Test - public void aerowayLine() { - assertFeatures(14, List.of(Map.of( - "class", "runway", - "ref", "123", - - "_layer", "aeroway", - "_type", "line", - "_minzoom", 10, - "_maxzoom", 14, - "_buffer", 4d - )), process(lineFeature(Map.of( - "aeroway", "runway", - "ref", "123" - )))); - assertFeatures(14, List.of(), process(pointFeature(Map.of( - "aeroway", "runway" - )))); - } - - @Test - public void aerowayPolygon() { - assertFeatures(14, List.of(Map.of( - "class", "runway", - "ref", "123", - - "_layer", "aeroway", - "_type", "polygon", - "_minzoom", 10, - "_maxzoom", 14, - "_buffer", 4d - )), process(polygonFeature(Map.of( - "aeroway", "runway", - "ref", "123" - )))); - assertFeatures(14, List.of(Map.of( - "class", "runway", - "ref", "123", - "_layer", "aeroway", - "_type", "polygon" - )), process(polygonFeature(Map.of( - "area:aeroway", "runway", - "ref", "123" - )))); - assertFeatures(14, List.of(Map.of( - "class", "heliport", - "ref", "123", - "_layer", "aeroway", - "_type", "polygon" - )), process(polygonFeature(Map.of( - "aeroway", "heliport", - "ref", "123" - )))); - assertFeatures(14, List.of(), process(lineFeature(Map.of( - "aeroway", "heliport" - )))); - assertFeatures(14, List.of(), process(pointFeature(Map.of( - "aeroway", "heliport" - )))); - } - - @Test - public void testWaterwayImportantRiverProcess() { - var charlesRiver = process(lineFeature(Map.of( - "waterway", "river", - "name", "charles river", - "name:es", "es name" - ))); - assertFeatures(14, List.of(Map.of( - "class", "river", - "name", "charles river", - "name:es", "es name", - "intermittent", 0, - - "_layer", "waterway", - "_type", "line", - "_minzoom", 9, - "_maxzoom", 14, - "_buffer", 4d - )), charlesRiver); - assertFeatures(11, List.of(Map.of( - "class", "river", - "name", "charles river", - "name:es", "es name", - "intermittent", "", - "_buffer", 13.082664546679323 - )), charlesRiver); - assertFeatures(10, List.of(Map.of( - "class", "river", - "_buffer", 26.165329093358647 - )), charlesRiver); - assertFeatures(9, List.of(Map.of( - "class", "river", - "_buffer", 26.165329093358647 - )), charlesRiver); - } - - @Test - public void testWaterwayImportantRiverPostProcess() throws GeometryException { - var line1 = new VectorTileEncoder.Feature( - Waterway.LAYER_NAME, - 1, - VectorTileEncoder.encodeGeometry(newLineString(0, 0, 10, 0)), - Map.of("name", "river"), - 0 - ); - var line2 = new VectorTileEncoder.Feature( - Waterway.LAYER_NAME, - 1, - VectorTileEncoder.encodeGeometry(newLineString(10, 0, 20, 0)), - Map.of("name", "river"), - 0 - ); - var connected = new VectorTileEncoder.Feature( - Waterway.LAYER_NAME, - 1, - VectorTileEncoder.encodeGeometry(newLineString(00, 0, 20, 0)), - Map.of("name", "river"), - 0 - ); - - assertEquals( - List.of(), - profile.postProcessLayerFeatures(Waterway.LAYER_NAME, 11, List.of()) - ); - assertEquals( - List.of(line1, line2), - profile.postProcessLayerFeatures(Waterway.LAYER_NAME, 12, List.of(line1, line2)) - ); - assertEquals( - List.of(connected), - profile.postProcessLayerFeatures(Waterway.LAYER_NAME, 11, List.of(line1, line2)) - ); - } - - @Test - public void testWaterwaySmaller() { - // river with no name is not important - assertFeatures(14, List.of(Map.of( - "class", "river", - "brunnel", "bridge", - - "_layer", "waterway", - "_type", "line", - "_minzoom", 12 - )), process(lineFeature(Map.of( - "waterway", "river", - "bridge", "1" - )))); - - assertFeatures(14, List.of(Map.of( - "class", "canal", - "_layer", "waterway", - "_type", "line", - "_minzoom", 12 - )), process(lineFeature(Map.of( - "waterway", "canal", - "name", "name" - )))); - - assertFeatures(14, List.of(Map.of( - "class", "stream", - "_layer", "waterway", - "_type", "line", - "_minzoom", 13 - )), process(lineFeature(Map.of( - "waterway", "stream", - "name", "name" - )))); - } - - @Test - public void testWaterwayNaturalEarth() { - assertFeatures(3, List.of(Map.of( - "class", "river", - "name", "", - "intermittent", "", - - "_layer", "waterway", - "_type", "line", - "_minzoom", 3, - "_maxzoom", 3 - )), process(new ReaderFeature( - newLineString(0, 0, 1, 1), - Map.of( - "featurecla", "River", - "name", "name" - ), - NATURAL_EARTH_SOURCE, - "ne_110m_rivers_lake_centerlines", - 0 - ))); - - assertFeatures(6, List.of(Map.of( - "class", "river", - "intermittent", "", - - "_layer", "waterway", - "_type", "line", - "_minzoom", 4, - "_maxzoom", 5 - )), process(new ReaderFeature( - newLineString(0, 0, 1, 1), - Map.of( - "featurecla", "River", - "name", "name" - ), - NATURAL_EARTH_SOURCE, - "ne_50m_rivers_lake_centerlines", - 0 - ))); - - assertFeatures(6, List.of(Map.of( - "class", "river", - "intermittent", "", - - "_layer", "waterway", - "_type", "line", - "_minzoom", 6, - "_maxzoom", 8 - )), process(new ReaderFeature( - newLineString(0, 0, 1, 1), - Map.of( - "featurecla", "River", - "name", "name" - ), - NATURAL_EARTH_SOURCE, - "ne_10m_rivers_lake_centerlines", - 0 - ))); - } - - @Test - public void testWaterNaturalEarth() { - assertFeatures(0, List.of(Map.of( - "class", "lake", - "intermittent", "", - "_layer", "water", - "_type", "polygon", - "_minzoom", 0 - )), process(new ReaderFeature( - rectangle(0, 10), - Map.of(), - NATURAL_EARTH_SOURCE, - "ne_110m_lakes", - 0 - ))); - - assertFeatures(0, List.of(Map.of( - "class", "ocean", - "intermittent", "", - "_layer", "water", - "_type", "polygon", - "_minzoom", 0 - )), process(new ReaderFeature( - rectangle(0, 10), - Map.of(), - NATURAL_EARTH_SOURCE, - "ne_110m_ocean", - 0 - ))); - - assertFeatures(6, List.of(Map.of( - "class", "lake", - "_layer", "water", - "_type", "polygon", - "_maxzoom", 5 - )), process(new ReaderFeature( - rectangle(0, 10), - Map.of(), - NATURAL_EARTH_SOURCE, - "ne_10m_lakes", - 0 - ))); - - assertFeatures(6, List.of(Map.of( - "class", "ocean", - "_layer", "water", - "_type", "polygon", - "_maxzoom", 5 - )), process(new ReaderFeature( - rectangle(0, 10), - Map.of(), - NATURAL_EARTH_SOURCE, - "ne_10m_ocean", - 0 - ))); - } - - @Test - public void testWaterOsmWaterPolygon() { - assertFeatures(0, List.of(Map.of( - "class", "ocean", - "intermittent", "", - "_layer", "water", - "_type", "polygon", - "_minzoom", 6, - "_maxzoom", 14 - )), process(new ReaderFeature( - rectangle(0, 10), - Map.of(), - WATER_POLYGON_SOURCE, - null, - 0 - ))); - } - - @Test - public void testWater() { - assertFeatures(14, List.of(Map.of( - "class", "lake", - "_layer", "water", - "_type", "polygon", - "_minzoom", 6, - "_maxzoom", 14 - )), process(polygonFeature(Map.of( - "natural", "water", - "water", "reservoir" - )))); - assertFeatures(14, List.of(Map.of( - "class", "lake", - - "_layer", "water", - "_type", "polygon", - "_minzoom", 6, - "_maxzoom", 14 - )), process(polygonFeature(Map.of( - "leisure", "swimming_pool" - )))); - assertFeatures(14, List.of(), process(polygonFeature(Map.of( - "natural", "bay" - )))); - assertFeatures(14, List.of(Map.of()), process(polygonFeature(Map.of( - "natural", "water" - )))); - assertFeatures(14, List.of(), process(polygonFeature(Map.of( - "natural", "water", - "covered", "yes" - )))); - assertFeatures(14, List.of(Map.of( - "class", "river", - "brunnel", "bridge", - "intermittent", 1, - - "_layer", "water", - "_type", "polygon", - "_minzoom", 6, - "_maxzoom", 14 - )), process(polygonFeature(Map.of( - "waterway", "stream", - "bridge", "1", - "intermittent", "1" - )))); - assertFeatures(11, List.of(Map.of( - "class", "lake", - "brunnel", "", - "intermittent", 0, - - "_layer", "water", - "_type", "polygon", - "_minzoom", 6, - "_maxzoom", 14, - "_minpixelsize", 2d - )), process(polygonFeature(Map.of( - "landuse", "salt_pond", - "bridge", "1" - )))); - } - - @Test - public void testWaterNamePoint() { - assertFeatures(11, List.of(Map.of( - "_layer", "water" - ), Map.of( - "class", "lake", - "name", "waterway", - "name:es", "waterway es", - "intermittent", 1, - - "_layer", "water_name", - "_type", "point", - "_minzoom", 9, - "_maxzoom", 14 - )), process(polygonFeatureWithArea(1, Map.of( - "name", "waterway", - "name:es", "waterway es", - "natural", "water", - "water", "pond", - "intermittent", "1" - )))); - double z11area = Math.pow((GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d), 2) * Math.pow(2, 20 - 11); - assertFeatures(10, List.of(Map.of( - "_layer", "water" - ), Map.of( - "_layer", "water_name", - "_type", "point", - "_minzoom", 11, - "_maxzoom", 14 - )), process(polygonFeatureWithArea(z11area, Map.of( - "name", "waterway", - "natural", "water", - "water", "pond" - )))); - } - - @Test - public void testWaterNameLakeline() { - assertFeatures(11, List.of(), process(new ReaderFeature( - newLineString(0, 0, 1, 1), - new HashMap<>(Map.of( - "OSM_ID", -10 - )), - LAKE_CENTERLINE_SOURCE, - null, - 0 - ))); - assertFeatures(10, List.of(Map.of( - "_layer", "water" - ), Map.of( - "name", "waterway", - "name:es", "waterway es", - - "_layer", "water_name", - "_type", "line", - "_geom", new TestUtils.NormGeometry(GeoUtils.latLonToWorldCoords(newLineString(0, 0, 1, 1))), - "_minzoom", 9, - "_maxzoom", 14, - "_minpixelsize", "waterway".length() * 6d - )), process(new ReaderFeature( - GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), - new HashMap<>(Map.of( - "name", "waterway", - "name:es", "waterway es", - "natural", "water", - "water", "pond" - )), - OSM_SOURCE, - null, - 10 - ))); - } - - @Test - public void testMarinePoint() { - assertFeatures(11, List.of(), process(new ReaderFeature( - newLineString(0, 0, 1, 1), - new HashMap<>(Map.of( - "scalerank", 10, - "name", "pacific ocean" - )), - NATURAL_EARTH_SOURCE, - "ne_10m_geography_marine_polys", - 0 - ))); - - // name match - use scale rank from NE - assertFeatures(10, List.of(Map.of( - "name", "Pacific", - "name:es", "Pacific es", - "_layer", "water_name", - "_type", "point", - "_minzoom", 10, - "_maxzoom", 14 - )), process(pointFeature(Map.of( - "rank", 9, - "name", "Pacific", - "name:es", "Pacific es", - "place", "sea" - )))); - - // name match but ocean - use min zoom=0 - assertFeatures(10, List.of(Map.of( - "_layer", "water_name", - "_type", "point", - "_minzoom", 0, - "_maxzoom", 14 - )), process(pointFeature(Map.of( - "rank", 9, - "name", "Pacific", - "place", "ocean" - )))); - - // no name match - use OSM rank - assertFeatures(10, List.of(Map.of( - "_layer", "water_name", - "_type", "point", - "_minzoom", 9, - "_maxzoom", 14 - )), process(pointFeature(Map.of( - "rank", 9, - "name", "Atlantic", - "place", "sea" - )))); - - // no rank at all, default to 8 - assertFeatures(10, List.of(Map.of( - "_layer", "water_name", - "_type", "point", - "_minzoom", 8, - "_maxzoom", 14 - )), process(pointFeature(Map.of( - "name", "Atlantic", - "place", "sea" - )))); - } - - @Test - public void testHousenumber() { - assertFeatures(14, List.of(Map.of( - "_layer", "housenumber", - "_type", "point", - "_minzoom", 14, - "_maxzoom", 14, - "_buffer", 8d - )), process(pointFeature(Map.of( - "addr:housenumber", "10" - )))); - assertFeatures(15, List.of(Map.of( - "_layer", "housenumber", - "_type", "point", - "_minzoom", 14, - "_maxzoom", 14, - "_buffer", 8d - )), process(polygonFeature(Map.of( - "addr:housenumber", "10" - )))); - } @Test public void testCaresAboutWikidata() { @@ -864,54 +28,4 @@ public class OpenMaptilesProfileTest { node.setTag("aeroway", "other"); assertFalse(profile.caresAboutWikidataTranslation(node)); } - - private VectorTileEncoder.Feature pointFeature(String layer, Map map, int group) { - return new VectorTileEncoder.Feature( - layer, - 1, - VectorTileEncoder.encodeGeometry(newPoint(0, 0)), - new HashMap<>(map), - group - ); - } - - private FeatureCollector process(SourceFeature feature) { - var collector = featureCollectorFactory.get(feature); - profile.processFeature(feature, collector); - return collector; - } - - private SourceFeature pointFeature(Map props) { - return new ReaderFeature( - newPoint(0, 0), - new HashMap<>(props), - OSM_SOURCE, - null, - 0 - ); - } - - private SourceFeature lineFeature(Map props) { - return new ReaderFeature( - newLineString(0, 0, 1, 1), - new HashMap<>(props), - OSM_SOURCE, - null, - 0 - ); - } - - private SourceFeature polygonFeatureWithArea(double area, Map props) { - return new ReaderFeature( - GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(area))), - new HashMap<>(props), - OSM_SOURCE, - null, - 0 - ); - } - - private SourceFeature polygonFeature(Map props) { - return polygonFeatureWithArea(1, props); - } } diff --git a/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/AerodromeLabelTest.java b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/AerodromeLabelTest.java new file mode 100644 index 00000000..7123f2d3 --- /dev/null +++ b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/AerodromeLabelTest.java @@ -0,0 +1,118 @@ +package com.onthegomap.flatmap.openmaptiles.layers; + +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +public class AerodromeLabelTest extends BaseLayerTest { + + + @TestFactory + public List aerodromeLabel() { + wikidataTranslations.put(123, "es", "es wd name"); + return List.of( + dynamicTest("happy path point", () -> { + assertFeatures(14, List.of(Map.of( + "class", "international", + "ele", 100, + "ele_ft", 328, + "name", "osm name", + "name:es", "es wd name", + + "_layer", "aerodrome_label", + "_type", "point", + "_minzoom", 10, + "_maxzoom", 14, + "_buffer", 64d + )), process(pointFeature(Map.of( + "aeroway", "aerodrome", + "name", "osm name", + "wikidata", "Q123", + "ele", "100", + "aerodrome", "international", + "iata", "123", + "icao", "1234" + )))); + }), + + dynamicTest("international", () -> { + assertFeatures(14, List.of(Map.of( + "class", "international", + "_layer", "aerodrome_label" + )), process(pointFeature(Map.of( + "aeroway", "aerodrome", + "aerodrome_type", "international" + )))); + }), + + dynamicTest("public", () -> { + assertFeatures(14, List.of(Map.of( + "class", "public", + "_layer", "aerodrome_label" + )), process(pointFeature(Map.of( + "aeroway", "aerodrome", + "aerodrome_type", "public airport" + )))); + assertFeatures(14, List.of(Map.of( + "class", "public", + "_layer", "aerodrome_label" + )), process(pointFeature(Map.of( + "aeroway", "aerodrome", + "aerodrome_type", "civil" + )))); + }), + + dynamicTest("military", () -> { + assertFeatures(14, List.of(Map.of( + "class", "military", + "_layer", "aerodrome_label" + )), process(pointFeature(Map.of( + "aeroway", "aerodrome", + "aerodrome_type", "military airport" + )))); + assertFeatures(14, List.of(Map.of( + "class", "military", + "_layer", "aerodrome_label" + )), process(pointFeature(Map.of( + "aeroway", "aerodrome", + "military", "airfield" + )))); + }), + + dynamicTest("private", () -> { + assertFeatures(14, List.of(Map.of( + "class", "private", + "_layer", "aerodrome_label" + )), process(pointFeature(Map.of( + "aeroway", "aerodrome", + "aerodrome_type", "private" + )))); + assertFeatures(14, List.of(Map.of( + "class", "private", + "_layer", "aerodrome_label" + )), process(pointFeature(Map.of( + "aeroway", "aerodrome", + "aerodrome", "private" + )))); + }), + + dynamicTest("other", () -> { + assertFeatures(14, List.of(Map.of( + "class", "other", + "_layer", "aerodrome_label" + )), process(pointFeature(Map.of( + "aeroway", "aerodrome" + )))); + }), + + dynamicTest("ignore non-points", () -> { + assertFeatures(14, List.of(), process(lineFeature(Map.of( + "aeroway", "aerodrome" + )))); + }) + ); + } +} diff --git a/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/AerowayTest.java b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/AerowayTest.java new file mode 100644 index 00000000..602f1bd2 --- /dev/null +++ b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/AerowayTest.java @@ -0,0 +1,92 @@ +package com.onthegomap.flatmap.openmaptiles.layers; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class AerowayTest extends BaseLayerTest { + + @Test + public void aerowayGate() { + assertFeatures(14, List.of(Map.of( + "class", "gate", + "ref", "123", + + "_layer", "aeroway", + "_type", "point", + "_minzoom", 14, + "_maxzoom", 14, + "_buffer", 4d + )), process(pointFeature(Map.of( + "aeroway", "gate", + "ref", "123" + )))); + assertFeatures(14, List.of(), process(lineFeature(Map.of( + "aeroway", "gate" + )))); + assertFeatures(14, List.of(), process(polygonFeature(Map.of( + "aeroway", "gate" + )))); + } + + @Test + public void aerowayLine() { + assertFeatures(14, List.of(Map.of( + "class", "runway", + "ref", "123", + + "_layer", "aeroway", + "_type", "line", + "_minzoom", 10, + "_maxzoom", 14, + "_buffer", 4d + )), process(lineFeature(Map.of( + "aeroway", "runway", + "ref", "123" + )))); + assertFeatures(14, List.of(), process(pointFeature(Map.of( + "aeroway", "runway" + )))); + } + + @Test + public void aerowayPolygon() { + assertFeatures(14, List.of(Map.of( + "class", "runway", + "ref", "123", + + "_layer", "aeroway", + "_type", "polygon", + "_minzoom", 10, + "_maxzoom", 14, + "_buffer", 4d + )), process(polygonFeature(Map.of( + "aeroway", "runway", + "ref", "123" + )))); + assertFeatures(14, List.of(Map.of( + "class", "runway", + "ref", "123", + "_layer", "aeroway", + "_type", "polygon" + )), process(polygonFeature(Map.of( + "area:aeroway", "runway", + "ref", "123" + )))); + assertFeatures(14, List.of(Map.of( + "class", "heliport", + "ref", "123", + "_layer", "aeroway", + "_type", "polygon" + )), process(polygonFeature(Map.of( + "aeroway", "heliport", + "ref", "123" + )))); + assertFeatures(14, List.of(), process(lineFeature(Map.of( + "aeroway", "heliport" + )))); + assertFeatures(14, List.of(), process(pointFeature(Map.of( + "aeroway", "heliport" + )))); + } +} diff --git a/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/BaseLayerTest.java b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/BaseLayerTest.java new file mode 100644 index 00000000..0357910c --- /dev/null +++ b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/BaseLayerTest.java @@ -0,0 +1,125 @@ +package com.onthegomap.flatmap.openmaptiles.layers; + +import static com.onthegomap.flatmap.TestUtils.assertSubmap; +import static com.onthegomap.flatmap.TestUtils.newLineString; +import static com.onthegomap.flatmap.TestUtils.newPoint; +import static com.onthegomap.flatmap.TestUtils.rectangle; +import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.OSM_SOURCE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import com.onthegomap.flatmap.Arguments; +import com.onthegomap.flatmap.CommonParams; +import com.onthegomap.flatmap.FeatureCollector; +import com.onthegomap.flatmap.SourceFeature; +import com.onthegomap.flatmap.TestUtils; +import com.onthegomap.flatmap.Translations; +import com.onthegomap.flatmap.VectorTileEncoder; +import com.onthegomap.flatmap.Wikidata; +import com.onthegomap.flatmap.geo.GeoUtils; +import com.onthegomap.flatmap.monitoring.Stats; +import com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile; +import com.onthegomap.flatmap.read.ReaderFeature; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.StreamSupport; + +public abstract class BaseLayerTest { + + final Wikidata.WikidataTranslations wikidataTranslations = new Wikidata.WikidataTranslations(); + final Translations translations = Translations.defaultProvider(List.of("en", "es", "de")) + .addTranslationProvider(wikidataTranslations); + + final CommonParams params = CommonParams.defaults(); + final OpenMapTilesProfile profile = new OpenMapTilesProfile(translations, Arguments.of(), + new Stats.InMemory()); + final Stats stats = new Stats.InMemory(); + final FeatureCollector.Factory featureCollectorFactory = new FeatureCollector.Factory(params, stats); + + static void assertFeatures(int zoom, List> expected, Iterable actual) { + List actualList = StreamSupport.stream(actual.spliterator(), false).toList(); + assertEquals(expected.size(), actualList.size(), "size"); + for (int i = 0; i < expected.size(); i++) { + assertSubmap(expected.get(i), TestUtils.toMap(actualList.get(i), zoom)); + } + } + + VectorTileEncoder.Feature pointFeature(String layer, Map map, int group) { + return new VectorTileEncoder.Feature( + layer, + 1, + VectorTileEncoder.encodeGeometry(newPoint(0, 0)), + new HashMap<>(map), + group + ); + } + + FeatureCollector process(SourceFeature feature) { + var collector = featureCollectorFactory.get(feature); + profile.processFeature(feature, collector); + return collector; + } + + void assertCoversZoomRange(int minzoom, int maxzoom, String layer, FeatureCollector... featureCollectors) { + Map[] zooms = new Map[Math.max(15, maxzoom + 1)]; + for (var features : featureCollectors) { + for (var feature : features) { + if (feature.getLayer().equals(layer)) { + for (int zoom = feature.getMinZoom(); zoom <= feature.getMaxZoom(); zoom++) { + Map map = TestUtils.toMap(feature, zoom); + if (zooms[zoom] != null) { + fail("Multiple features at z" + zoom + ":\n" + zooms[zoom] + "\n" + map); + } + zooms[zoom] = map; + } + } + } + } + for (int zoom = 0; zoom <= 14; zoom++) { + if (zoom < minzoom || zoom > maxzoom) { + if (zooms[zoom] != null) { + fail("Expected nothing at z" + zoom + " but found: " + zooms[zoom]); + } + } else { + if (zooms[zoom] == null) { + fail("No feature at z" + zoom); + } + } + } + } + + SourceFeature pointFeature(Map props) { + return new ReaderFeature( + newPoint(0, 0), + new HashMap<>(props), + OSM_SOURCE, + null, + 0 + ); + } + + SourceFeature lineFeature(Map props) { + return new ReaderFeature( + newLineString(0, 0, 1, 1), + new HashMap<>(props), + OSM_SOURCE, + null, + 0 + ); + } + + SourceFeature polygonFeatureWithArea(double area, Map props) { + return new ReaderFeature( + GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(area))), + new HashMap<>(props), + OSM_SOURCE, + null, + 0 + ); + } + + SourceFeature polygonFeature(Map props) { + return polygonFeatureWithArea(1, props); + } +} diff --git a/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/BoundaryTest.java b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/BoundaryTest.java new file mode 100644 index 00000000..0c322ebe --- /dev/null +++ b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/BoundaryTest.java @@ -0,0 +1,603 @@ +package com.onthegomap.flatmap.openmaptiles.layers; + +import static com.onthegomap.flatmap.TestUtils.newLineString; +import static com.onthegomap.flatmap.TestUtils.rectangle; +import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.NATURAL_EARTH_SOURCE; +import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.OSM_SOURCE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.graphhopper.reader.ReaderRelation; +import com.onthegomap.flatmap.FeatureCollector; +import com.onthegomap.flatmap.VectorTileEncoder; +import com.onthegomap.flatmap.geo.GeoUtils; +import com.onthegomap.flatmap.geo.GeometryException; +import com.onthegomap.flatmap.read.OpenStreetMapReader; +import com.onthegomap.flatmap.read.ReaderFeature; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +public class BoundaryTest extends BaseLayerTest { + + @Test + public void testNaturalEarthCountryBoundaries() { + assertCoversZoomRange( + 0, 4, "boundary", + process(new ReaderFeature( + newLineString(0, 0, 1, 1), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_110m_admin_0_boundary_lines_land", + 0 + )), + process(new ReaderFeature( + newLineString(0, 0, 1, 1), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_50m_admin_0_boundary_lines_land", + 1 + )), + process(new ReaderFeature( + newLineString(0, 0, 1, 1), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_boundary_lines_land", + 2 + )) + ); + + assertFeatures(0, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "disputed", 0, + "maritime", 0, + "admin_level", 2, + + "_minzoom", 0, + "_buffer", 4d + )), process(new ReaderFeature( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "International boundary (verify)" + ), + NATURAL_EARTH_SOURCE, + "ne_110m_admin_0_boundary_lines_land", + 0 + ))); + + assertFeatures(0, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "disputed", 1, + "maritime", 0, + "admin_level", 2, + "_buffer", 4d + )), process(new ReaderFeature( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "Disputed (please verify)" + ), + NATURAL_EARTH_SOURCE, + "ne_110m_admin_0_boundary_lines_land", + 0 + ))); + + assertFeatures(0, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "admin_level", 2 + )), process(new ReaderFeature( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "International boundary (verify)" + ), + NATURAL_EARTH_SOURCE, + "ne_50m_admin_0_boundary_lines_land", + 0 + ))); + + assertFeatures(0, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "admin_level", 2 + )), process(new ReaderFeature( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "International boundary (verify)" + ), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_boundary_lines_land", + 0 + ))); + + assertFeatures(0, List.of(), process(new ReaderFeature( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "Lease Limit" + ), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_boundary_lines_land", + 0 + ))); + } + + @Test + public void testNaturalEarthStateBoundaries() { + assertFeatures(0, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "disputed", 0, + "maritime", 0, + "admin_level", 4, + + "_minzoom", 1, + "_maxzoom", 4, + "_buffer", 4d + )), process(new ReaderFeature( + newLineString(0, 0, 1, 1), + Map.of( + "min_zoom", 7d + ), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_1_states_provinces_lines", + 0 + ))); + + assertFeatures(0, List.of(), process(new ReaderFeature( + newLineString(0, 0, 1, 1), + Map.of( + "min_zoom", 7.1d + ), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_1_states_provinces_lines", + 0 + ))); + + assertFeatures(0, List.of(), process(new ReaderFeature( + newLineString(0, 0, 1, 1), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_1_states_provinces_lines", + 0 + ))); + } + + @Test + public void testMergesDisconnectedLineFeatures() throws GeometryException { + var line1 = new VectorTileEncoder.Feature( + Boundary.LAYER_NAME, + 1, + VectorTileEncoder.encodeGeometry(newLineString(0, 0, 10, 0)), + Map.of("admin_level", 2), + 0 + ); + var line2 = new VectorTileEncoder.Feature( + Boundary.LAYER_NAME, + 1, + VectorTileEncoder.encodeGeometry(newLineString(10, 0, 20, 0)), + Map.of("admin_level", 2), + 0 + ); + var connected = new VectorTileEncoder.Feature( + Boundary.LAYER_NAME, + 1, + VectorTileEncoder.encodeGeometry(newLineString(00, 0, 20, 0)), + Map.of("admin_level", 2), + 0 + ); + + assertEquals( + List.of(connected), + profile.postProcessLayerFeatures(Boundary.LAYER_NAME, 14, List.of(line1, line2)) + ); + assertEquals( + List.of(connected), + profile.postProcessLayerFeatures(Boundary.LAYER_NAME, 13, List.of(line1, line2)) + ); + } + + @Test + public void testOsmTownBoundary() { + var relation = new ReaderRelation(1); + relation.setTag("type", "boundary"); + relation.setTag("admin_level", "10"); + relation.setTag("boundary", "administrative"); + + assertFeatures(14, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "disputed", 0, + "maritime", 0, + "admin_level", 10, + + "_minzoom", 12, + "_maxzoom", 14, + "_buffer", 4d, + "_minpixelsize", 0d + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation), + Map.of()))); + } + + @Test + public void testOsmBoundaryTakesMinAdminLevel() { + var relation1 = new ReaderRelation(1); + relation1.setTag("type", "boundary"); + relation1.setTag("admin_level", "10"); + relation1.setTag("name", "Town"); + relation1.setTag("boundary", "administrative"); + var relation2 = new ReaderRelation(2); + relation2.setTag("type", "boundary"); + relation2.setTag("admin_level", "4"); + relation2.setTag("name", "State"); + relation2.setTag("boundary", "administrative"); + + assertFeatures(14, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "disputed", 0, + "maritime", 0, + "admin_level", 4 + )), process(lineFeatureWithRelation( + Stream.concat( + profile.preprocessOsmRelation(relation2).stream(), + profile.preprocessOsmRelation(relation1).stream() + ).toList(), + Map.of()))); + } + + @Test + public void testOsmBoundarySetsMaritimeFromWay() { + var relation1 = new ReaderRelation(1); + relation1.setTag("type", "boundary"); + relation1.setTag("admin_level", "10"); + relation1.setTag("boundary", "administrative"); + + assertFeatures(14, List.of(Map.of( + "maritime", 1 + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation1), + Map.of( + "maritime", "yes" + )) + )); + assertFeatures(14, List.of(Map.of( + "maritime", 1 + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation1), + Map.of( + "natural", "coastline" + )) + )); + assertFeatures(14, List.of(Map.of( + "maritime", 1 + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation1), + Map.of( + "boundary_type", "maritime" + )) + )); + } + + @Test + public void testIgnoresProtectedAreas() { + var relation1 = new ReaderRelation(1); + relation1.setTag("type", "boundary"); + relation1.setTag("admin_level", "10"); + relation1.setTag("boundary", "protected_area"); + + assertNull(profile.preprocessOsmRelation(relation1)); + } + + @Test + public void testIgnoresProtectedAdminLevelOver10() { + var relation1 = new ReaderRelation(1); + relation1.setTag("type", "boundary"); + relation1.setTag("admin_level", "11"); + relation1.setTag("boundary", "administrative"); + + assertNull(profile.preprocessOsmRelation(relation1)); + } + + @Test + public void testOsmBoundaryDisputed() { + var relation = new ReaderRelation(1); + relation.setTag("type", "boundary"); + relation.setTag("admin_level", "5"); + relation.setTag("boundary", "administrative"); + relation.setTag("disputed", "yes"); + relation.setTag("name", "Border A - B"); + relation.setTag("claimed_by", "A"); + assertFeatures(14, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "disputed_name", "BorderA-B", + "claimed_by", "A", + + "disputed", 1, + "maritime", 0, + "admin_level", 5 + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation), + Map.of()) + )); + } + + @Test + public void testOsmBoundaryDisputedFromWay() { + var relation = new ReaderRelation(1); + relation.setTag("type", "boundary"); + relation.setTag("admin_level", "5"); + relation.setTag("boundary", "administrative"); + + assertFeatures(14, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + + "disputed", 1, + "maritime", 0, + "admin_level", 5 + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation), + Map.of( + "disputed", "yes" + )) + )); + + assertFeatures(14, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + + "disputed", 1, + "maritime", 0, + "admin_level", 5, + "claimed_by", "A", + "disputed_name", "AB" + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation), + Map.of( + "disputed", "yes", + "claimed_by", "A", + "name", "AB" + )) + )); + } + + @Test + public void testCountryBoundaryEmittedIfNoName() { + var relation = new ReaderRelation(1); + relation.setTag("type", "boundary"); + relation.setTag("admin_level", "2"); + relation.setTag("boundary", "administrative"); + + assertFeatures(14, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + + "disputed", 0, + "maritime", 0, + "admin_level", 2 + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation), + Map.of()) + )); + } + + @Test + public void testCountryLeftRightName() { + var country1 = new ReaderRelation(1); + country1.setTag("type", "boundary"); + country1.setTag("admin_level", "2"); + country1.setTag("boundary", "administrative"); + country1.setTag("ISO3166-1:alpha3", "C1"); + var country2 = new ReaderRelation(2); + country2.setTag("type", "boundary"); + country2.setTag("admin_level", "2"); + country2.setTag("boundary", "administrative"); + country2.setTag("ISO3166-1:alpha3", "C2"); + + // shared edge + assertFeatures(14, List.of(), process(new ReaderFeature( + newLineString(0, 0, 0, 10), + Map.of(), + OSM_SOURCE, + null, + 3, + Stream.concat( + profile.preprocessOsmRelation(country1).stream(), + profile.preprocessOsmRelation(country2).stream() + ).map(r -> new OpenStreetMapReader.RelationMember<>("", r)).toList() + ) + )); + + // other 2 edges of country 1 + assertFeatures(14, List.of(), process(new ReaderFeature( + newLineString(0, 0, 5, 10), + Map.of(), + OSM_SOURCE, + null, + 4, + profile.preprocessOsmRelation(country1).stream().map(r -> new OpenStreetMapReader.RelationMember<>("", r)) + .toList() + ) + )); + assertFeatures(14, List.of(), process(new ReaderFeature( + newLineString(0, 10, 5, 10), + Map.of(), + OSM_SOURCE, + null, + 4, + profile.preprocessOsmRelation(country1).stream().map(r -> new OpenStreetMapReader.RelationMember<>("", r)) + .toList() + ) + )); + + // other 2 edges of country 2 + assertFeatures(14, List.of(), process(new ReaderFeature( + newLineString(0, 0, -5, 10), + Map.of(), + OSM_SOURCE, + null, + 4, + profile.preprocessOsmRelation(country2).stream().map(r -> new OpenStreetMapReader.RelationMember<>("", r)) + .toList() + ) + )); + assertFeatures(14, List.of(), process(new ReaderFeature( + newLineString(0, 10, -5, 10), + Map.of(), + OSM_SOURCE, + null, + 4, + profile.preprocessOsmRelation(country2).stream().map(r -> new OpenStreetMapReader.RelationMember<>("", r)) + .toList() + ) + )); + + List features = new ArrayList<>(); + profile.finish(OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add); + assertEquals(3, features.size()); + + // ensure shared edge has country labels on right sides + var sharedEdge = features.stream() + .filter(c -> c.getAttrsAtZoom(0).containsKey("adm0_l") && c.getAttrsAtZoom(0).containsKey("adm0_r")).findFirst() + .get(); + if (sharedEdge.getGeometry().getCoordinate().y == 0.5) { // going up + assertEquals("C1", sharedEdge.getAttrsAtZoom(0).get("adm0_r")); + assertEquals("C2", sharedEdge.getAttrsAtZoom(0).get("adm0_l")); + } else { // going down + assertEquals("C2", sharedEdge.getAttrsAtZoom(0).get("adm0_r")); + assertEquals("C1", sharedEdge.getAttrsAtZoom(0).get("adm0_l")); + } + var c1 = features.stream() + .filter(c -> c.getGeometry().getEnvelopeInternal().getMaxX() > 0.5).findFirst() + .get(); + if (c1.getGeometry().getCoordinate().y == 0.5) { // going up + assertEquals("C1", c1.getAttrsAtZoom(0).get("adm0_l")); + } else { // going down + assertEquals("C1", c1.getAttrsAtZoom(0).get("adm0_r")); + } + var c2 = features.stream() + .filter(c -> c.getGeometry().getEnvelopeInternal().getMinX() < 0.5).findFirst() + .get(); + if (c2.getGeometry().getCoordinate().y == 0.5) { // going up + assertEquals("C2", c2.getAttrsAtZoom(0).get("adm0_r")); + } else { // going down + assertEquals("C2", c2.getAttrsAtZoom(0).get("adm0_l")); + } + } + + @Test + public void testCountryBoundaryNotClosed() { + var country1 = new ReaderRelation(1); + country1.setTag("type", "boundary"); + country1.setTag("admin_level", "2"); + country1.setTag("boundary", "administrative"); + country1.setTag("ISO3166-1:alpha3", "C1"); + + // shared edge + assertFeatures(14, List.of(), process(new ReaderFeature( + newLineString(0, 0, 0, 10, 5, 5), + Map.of(), + OSM_SOURCE, + null, + 3, + profile.preprocessOsmRelation(country1).stream().map(r -> new OpenStreetMapReader.RelationMember<>("", r)) + .toList() + ))); + + List features = new ArrayList<>(); + profile.finish(OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add); + assertFeatures(0, List.of(Map.of( + "adm0_r", "", + "adm0_l", "", + "maritime", 0, + "disputed", 0, + "admin_level", 2, + + "_layer", "boundary" + )), features); + } + + @Test + public void testNestedCountry() throws GeometryException { + var country1 = new ReaderRelation(1); + country1.setTag("type", "boundary"); + country1.setTag("admin_level", "2"); + country1.setTag("boundary", "administrative"); + country1.setTag("ISO3166-1:alpha3", "C1"); + + assertFeatures(14, List.of(), process(new ReaderFeature( + GeoUtils.polygonToLineString(rectangle(0, 10)), + Map.of(), + OSM_SOURCE, + null, + 3, + profile.preprocessOsmRelation(country1).stream().map(r -> new OpenStreetMapReader.RelationMember<>("", r)) + .toList() + ))); + assertFeatures(14, List.of(), process(new ReaderFeature( + GeoUtils.polygonToLineString(rectangle(1, 9)), + Map.of(), + OSM_SOURCE, + null, + 3, + profile.preprocessOsmRelation(country1).stream().map(r -> new OpenStreetMapReader.RelationMember<>("", r)) + .toList() + ))); + + List features = new ArrayList<>(); + profile.finish(OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add); + assertFeatures(0, List.of(Map.of( + "adm0_l", "C1", + "adm0_r", "" + ), Map.of( + "adm0_r", "C1", + "adm0_l", "" + )), features); + } + + @Test + public void testDontLabelBadPolygon() throws GeometryException { + var country1 = new ReaderRelation(1); + country1.setTag("type", "boundary"); + country1.setTag("admin_level", "2"); + country1.setTag("boundary", "administrative"); + country1.setTag("ISO3166-1:alpha3", "C1"); + + assertFeatures(14, List.of(), process(new ReaderFeature( + GeoUtils.worldToLatLonCoords(newLineString(0, 0, 10, 0, 10, 10, 2, 10, 2, -2)), + Map.of(), + OSM_SOURCE, + null, + 3, + profile.preprocessOsmRelation(country1).stream().map(r -> new OpenStreetMapReader.RelationMember<>("", r)) + .toList() + ))); + + List features = new ArrayList<>(); + profile.finish(OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add); + assertFeatures(0, List.of(Map.of( + "adm0_l", "", + "adm0_r", "" + )), features); + } + + @NotNull + private ReaderFeature lineFeatureWithRelation(List relationInfos, + Map map) { + return new ReaderFeature( + newLineString(0, 0, 1, 1), + map, + OSM_SOURCE, + null, + 0, + (relationInfos == null ? List.of() : relationInfos).stream() + .map(r -> new OpenStreetMapReader.RelationMember<>("", r)).toList() + ); + } +} diff --git a/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/HousenumberTest.java b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/HousenumberTest.java new file mode 100644 index 00000000..5e654d93 --- /dev/null +++ b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/HousenumberTest.java @@ -0,0 +1,30 @@ +package com.onthegomap.flatmap.openmaptiles.layers; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class HousenumberTest extends BaseLayerTest { + + @Test + public void testHousenumber() { + assertFeatures(14, List.of(Map.of( + "_layer", "housenumber", + "_type", "point", + "_minzoom", 14, + "_maxzoom", 14, + "_buffer", 8d + )), process(pointFeature(Map.of( + "addr:housenumber", "10" + )))); + assertFeatures(14, List.of(Map.of( + "_layer", "housenumber", + "_type", "point", + "_minzoom", 14, + "_maxzoom", 14, + "_buffer", 8d + )), process(polygonFeature(Map.of( + "addr:housenumber", "10" + )))); + } +} diff --git a/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/MountainPeakTest.java b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/MountainPeakTest.java new file mode 100644 index 00000000..be108ddc --- /dev/null +++ b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/MountainPeakTest.java @@ -0,0 +1,156 @@ +package com.onthegomap.flatmap.openmaptiles.layers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +import com.onthegomap.flatmap.geo.GeometryException; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; + +public class MountainPeakTest extends BaseLayerTest { + + @TestFactory + public List mountainPeakProcessing() { + wikidataTranslations.put(123, "es", "es wd name"); + return List.of( + dynamicTest("happy path", () -> { + var peak = process(pointFeature(Map.of( + "natural", "peak", + "name", "test", + "ele", "100", + "wikidata", "Q123" + ))); + assertFeatures(14, List.of(Map.of( + "class", "peak", + "ele", 100, + "ele_ft", 328, + + "_layer", "mountain_peak", + "_type", "point", + "_minzoom", 7, + "_maxzoom", 14, + "_buffer", 64d + )), peak); + assertFeatures(14, List.of(Map.of( + "name:latin", "test", + "name", "test", + "name:es", "es wd name" + )), peak); + }), + + dynamicTest("labelgrid", () -> { + var peak = process(pointFeature(Map.of( + "natural", "peak", + "ele", "100" + ))); + assertFeatures(14, List.of(Map.of( + "_labelgrid_limit", 0 + )), peak); + assertFeatures(13, List.of(Map.of( + "_labelgrid_limit", 5, + "_labelgrid_size", 100d + )), peak); + }), + + dynamicTest("volcano", () -> + assertFeatures(14, List.of(Map.of( + "class", "volcano" + )), process(pointFeature(Map.of( + "natural", "volcano", + "ele", "100" + ))))), + + dynamicTest("no elevation", () -> + assertFeatures(14, List.of(), process(pointFeature(Map.of( + "natural", "volcano" + ))))), + + dynamicTest("bogus elevation", () -> + assertFeatures(14, List.of(), process(pointFeature(Map.of( + "natural", "volcano", + "ele", "11000" + ))))), + + dynamicTest("ignore lines", () -> + assertFeatures(14, List.of(), process(lineFeature(Map.of( + "natural", "peak", + "name", "name", + "ele", "100" + ))))), + + dynamicTest("zorder", () -> { + assertFeatures(14, List.of(Map.of( + "_zorder", 100 + )), process(pointFeature(Map.of( + "natural", "peak", + "ele", "100" + )))); + assertFeatures(14, List.of(Map.of( + "_zorder", 10100 + )), process(pointFeature(Map.of( + "natural", "peak", + "name", "name", + "ele", "100" + )))); + assertFeatures(14, List.of(Map.of( + "_zorder", 20100 + )), process(pointFeature(Map.of( + "natural", "peak", + "name", "name", + "wikipedia", "wikilink", + "ele", "100" + )))); + }) + ); + } + + @Test + public void testMountainPeakPostProcessing() throws GeometryException { + assertEquals(List.of(), profile.postProcessLayerFeatures(MountainPeak.LAYER_NAME, 13, List.of())); + + assertEquals(List.of(pointFeature( + MountainPeak.LAYER_NAME, + Map.of("rank", 1), + 1 + )), profile.postProcessLayerFeatures(MountainPeak.LAYER_NAME, 13, List.of(pointFeature( + MountainPeak.LAYER_NAME, + Map.of(), + 1 + )))); + + assertEquals(List.of( + pointFeature( + MountainPeak.LAYER_NAME, + Map.of("rank", 2, "name", "a"), + 1 + ), pointFeature( + MountainPeak.LAYER_NAME, + Map.of("rank", 1, "name", "b"), + 1 + ), pointFeature( + MountainPeak.LAYER_NAME, + Map.of("rank", 1, "name", "c"), + 2 + ) + ), profile.postProcessLayerFeatures(MountainPeak.LAYER_NAME, 13, List.of( + pointFeature( + MountainPeak.LAYER_NAME, + Map.of("name", "a"), + 1 + ), + pointFeature( + MountainPeak.LAYER_NAME, + Map.of("name", "b"), + 1 + ), + pointFeature( + MountainPeak.LAYER_NAME, + Map.of("name", "c"), + 2 + ) + ))); + } +} diff --git a/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/WaterNameTest.java b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/WaterNameTest.java new file mode 100644 index 00000000..d8394688 --- /dev/null +++ b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/WaterNameTest.java @@ -0,0 +1,155 @@ +package com.onthegomap.flatmap.openmaptiles.layers; + +import static com.onthegomap.flatmap.TestUtils.newLineString; +import static com.onthegomap.flatmap.TestUtils.rectangle; +import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.LAKE_CENTERLINE_SOURCE; +import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.NATURAL_EARTH_SOURCE; +import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.OSM_SOURCE; + +import com.onthegomap.flatmap.TestUtils; +import com.onthegomap.flatmap.geo.GeoUtils; +import com.onthegomap.flatmap.read.ReaderFeature; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class WaterNameTest extends BaseLayerTest { + + @Test + public void testWaterNamePoint() { + assertFeatures(11, List.of(Map.of( + "_layer", "water" + ), Map.of( + "class", "lake", + "name", "waterway", + "name:es", "waterway es", + "intermittent", 1, + + "_layer", "water_name", + "_type", "point", + "_minzoom", 9, + "_maxzoom", 14 + )), process(polygonFeatureWithArea(1, Map.of( + "name", "waterway", + "name:es", "waterway es", + "natural", "water", + "water", "pond", + "intermittent", "1" + )))); + double z11area = Math.pow((GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d), 2) * Math.pow(2, 20 - 11); + assertFeatures(10, List.of(Map.of( + "_layer", "water" + ), Map.of( + "_layer", "water_name", + "_type", "point", + "_minzoom", 11, + "_maxzoom", 14 + )), process(polygonFeatureWithArea(z11area, Map.of( + "name", "waterway", + "natural", "water", + "water", "pond" + )))); + } + + @Test + public void testWaterNameLakeline() { + assertFeatures(11, List.of(), process(new ReaderFeature( + newLineString(0, 0, 1, 1), + new HashMap<>(Map.of( + "OSM_ID", -10 + )), + LAKE_CENTERLINE_SOURCE, + null, + 0 + ))); + assertFeatures(10, List.of(Map.of( + "_layer", "water" + ), Map.of( + "name", "waterway", + "name:es", "waterway es", + + "_layer", "water_name", + "_type", "line", + "_geom", new TestUtils.NormGeometry(GeoUtils.latLonToWorldCoords(newLineString(0, 0, 1, 1))), + "_minzoom", 9, + "_maxzoom", 14, + "_minpixelsize", "waterway".length() * 6d + )), process(new ReaderFeature( + GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), + new HashMap<>(Map.of( + "name", "waterway", + "name:es", "waterway es", + "natural", "water", + "water", "pond" + )), + OSM_SOURCE, + null, + 10 + ))); + } + + @Test + public void testMarinePoint() { + assertFeatures(11, List.of(), process(new ReaderFeature( + newLineString(0, 0, 1, 1), + new HashMap<>(Map.of( + "scalerank", 10, + "name", "pacific ocean" + )), + NATURAL_EARTH_SOURCE, + "ne_10m_geography_marine_polys", + 0 + ))); + + // name match - use scale rank from NE + assertFeatures(10, List.of(Map.of( + "name", "Pacific", + "name:es", "Pacific es", + "_layer", "water_name", + "_type", "point", + "_minzoom", 10, + "_maxzoom", 14 + )), process(pointFeature(Map.of( + "rank", 9, + "name", "Pacific", + "name:es", "Pacific es", + "place", "sea" + )))); + + // name match but ocean - use min zoom=0 + assertFeatures(10, List.of(Map.of( + "_layer", "water_name", + "_type", "point", + "_minzoom", 0, + "_maxzoom", 14 + )), process(pointFeature(Map.of( + "rank", 9, + "name", "Pacific", + "place", "ocean" + )))); + + // no name match - use OSM rank + assertFeatures(10, List.of(Map.of( + "_layer", "water_name", + "_type", "point", + "_minzoom", 9, + "_maxzoom", 14 + )), process(pointFeature(Map.of( + "rank", 9, + "name", "Atlantic", + "place", "sea" + )))); + + // no rank at all, default to 8 + assertFeatures(10, List.of(Map.of( + "_layer", "water_name", + "_type", "point", + "_minzoom", 8, + "_maxzoom", 14 + )), process(pointFeature(Map.of( + "name", "Atlantic", + "place", "sea" + )))); + } +} diff --git a/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/WaterTest.java b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/WaterTest.java new file mode 100644 index 00000000..389bfcb8 --- /dev/null +++ b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/WaterTest.java @@ -0,0 +1,223 @@ +package com.onthegomap.flatmap.openmaptiles.layers; + +import static com.onthegomap.flatmap.TestUtils.rectangle; +import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.NATURAL_EARTH_SOURCE; +import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.OSM_SOURCE; +import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.WATER_POLYGON_SOURCE; + +import com.onthegomap.flatmap.read.ReaderFeature; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class WaterTest extends BaseLayerTest { + + + @Test + public void testWaterNaturalEarth() { + assertFeatures(0, List.of(Map.of( + "class", "lake", + "intermittent", "", + "_layer", "water", + "_type", "polygon", + "_minzoom", 0 + )), process(new ReaderFeature( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_110m_lakes", + 0 + ))); + + assertFeatures(0, List.of(Map.of( + "class", "ocean", + "intermittent", "", + "_layer", "water", + "_type", "polygon", + "_minzoom", 0 + )), process(new ReaderFeature( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_110m_ocean", + 0 + ))); + + assertFeatures(6, List.of(Map.of( + "class", "lake", + "_layer", "water", + "_type", "polygon", + "_maxzoom", 5 + )), process(new ReaderFeature( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_10m_lakes", + 0 + ))); + + assertFeatures(6, List.of(Map.of( + "class", "ocean", + "_layer", "water", + "_type", "polygon", + "_maxzoom", 5 + )), process(new ReaderFeature( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_10m_ocean", + 0 + ))); + } + + @Test + public void testWaterOsmWaterPolygon() { + assertFeatures(0, List.of(Map.of( + "class", "ocean", + "intermittent", "", + "_layer", "water", + "_type", "polygon", + "_minzoom", 6, + "_maxzoom", 14 + )), process(new ReaderFeature( + rectangle(0, 10), + Map.of(), + WATER_POLYGON_SOURCE, + null, + 0 + ))); + } + + @Test + public void testWater() { + assertFeatures(14, List.of(Map.of( + "class", "lake", + "_layer", "water", + "_type", "polygon", + "_minzoom", 6, + "_maxzoom", 14 + )), process(polygonFeature(Map.of( + "natural", "water", + "water", "reservoir" + )))); + assertFeatures(14, List.of(Map.of( + "class", "lake", + + "_layer", "water", + "_type", "polygon", + "_minzoom", 6, + "_maxzoom", 14 + )), process(polygonFeature(Map.of( + "leisure", "swimming_pool" + )))); + assertFeatures(14, List.of(), process(polygonFeature(Map.of( + "natural", "bay" + )))); + assertFeatures(14, List.of(Map.of()), process(polygonFeature(Map.of( + "natural", "water" + )))); + assertFeatures(14, List.of(), process(polygonFeature(Map.of( + "natural", "water", + "covered", "yes" + )))); + assertFeatures(14, List.of(Map.of( + "class", "river", + "brunnel", "bridge", + "intermittent", 1, + + "_layer", "water", + "_type", "polygon", + "_minzoom", 6, + "_maxzoom", 14 + )), process(polygonFeature(Map.of( + "waterway", "stream", + "bridge", "1", + "intermittent", "1" + )))); + assertFeatures(11, List.of(Map.of( + "class", "lake", + "brunnel", "", + "intermittent", 0, + + "_layer", "water", + "_type", "polygon", + "_minzoom", 6, + "_maxzoom", 14, + "_minpixelsize", 2d + )), process(polygonFeature(Map.of( + "landuse", "salt_pond", + "bridge", "1" + )))); + } + + @Test + public void testOceanZoomLevels() { + assertCoversZoomRange(0, 14, "water", + process(new ReaderFeature( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_110m_ocean", + 0 + )), + process(new ReaderFeature( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_50m_ocean", + 0 + )), + process(new ReaderFeature( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_10m_ocean", + 0 + )), + process(new ReaderFeature( + rectangle(0, 10), + Map.of(), + WATER_POLYGON_SOURCE, + null, + 0 + )) + ); + } + + @Test + public void testLakeZoomLevels() { + assertCoversZoomRange(0, 14, "water", + process(new ReaderFeature( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_110m_lakes", + 0 + )), + process(new ReaderFeature( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_50m_lakes", + 0 + )), + process(new ReaderFeature( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_10m_lakes", + 0 + )), + process(new ReaderFeature( + rectangle(0, 10), + Map.of( + "natural", "water", + "water", "reservoir" + ), + OSM_SOURCE, + null, + 0 + )) + ); + } +} diff --git a/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/WaterwayTest.java b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/WaterwayTest.java new file mode 100644 index 00000000..ed92a47b --- /dev/null +++ b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/WaterwayTest.java @@ -0,0 +1,186 @@ +package com.onthegomap.flatmap.openmaptiles.layers; + +import static com.onthegomap.flatmap.TestUtils.newLineString; +import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.NATURAL_EARTH_SOURCE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.onthegomap.flatmap.VectorTileEncoder; +import com.onthegomap.flatmap.geo.GeometryException; +import com.onthegomap.flatmap.read.ReaderFeature; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class WaterwayTest extends BaseLayerTest { + + @Test + public void testWaterwayImportantRiverProcess() { + var charlesRiver = process(lineFeature(Map.of( + "waterway", "river", + "name", "charles river", + "name:es", "es name" + ))); + assertFeatures(14, List.of(Map.of( + "class", "river", + "name", "charles river", + "name:es", "es name", + "intermittent", 0, + + "_layer", "waterway", + "_type", "line", + "_minzoom", 9, + "_maxzoom", 14, + "_buffer", 4d + )), charlesRiver); + assertFeatures(11, List.of(Map.of( + "class", "river", + "name", "charles river", + "name:es", "es name", + "intermittent", "", + "_buffer", 13.082664546679323 + )), charlesRiver); + assertFeatures(10, List.of(Map.of( + "class", "river", + "_buffer", 26.165329093358647 + )), charlesRiver); + assertFeatures(9, List.of(Map.of( + "class", "river", + "_buffer", 26.165329093358647 + )), charlesRiver); + } + + @Test + public void testWaterwayImportantRiverPostProcess() throws GeometryException { + var line1 = new VectorTileEncoder.Feature( + Waterway.LAYER_NAME, + 1, + VectorTileEncoder.encodeGeometry(newLineString(0, 0, 10, 0)), + Map.of("name", "river"), + 0 + ); + var line2 = new VectorTileEncoder.Feature( + Waterway.LAYER_NAME, + 1, + VectorTileEncoder.encodeGeometry(newLineString(10, 0, 20, 0)), + Map.of("name", "river"), + 0 + ); + var connected = new VectorTileEncoder.Feature( + Waterway.LAYER_NAME, + 1, + VectorTileEncoder.encodeGeometry(newLineString(00, 0, 20, 0)), + Map.of("name", "river"), + 0 + ); + + assertEquals( + List.of(), + profile.postProcessLayerFeatures(Waterway.LAYER_NAME, 11, List.of()) + ); + assertEquals( + List.of(line1, line2), + profile.postProcessLayerFeatures(Waterway.LAYER_NAME, 12, List.of(line1, line2)) + ); + assertEquals( + List.of(connected), + profile.postProcessLayerFeatures(Waterway.LAYER_NAME, 11, List.of(line1, line2)) + ); + } + + @Test + public void testWaterwaySmaller() { + // river with no name is not important + assertFeatures(14, List.of(Map.of( + "class", "river", + "brunnel", "bridge", + + "_layer", "waterway", + "_type", "line", + "_minzoom", 12 + )), process(lineFeature(Map.of( + "waterway", "river", + "bridge", "1" + )))); + + assertFeatures(14, List.of(Map.of( + "class", "canal", + "_layer", "waterway", + "_type", "line", + "_minzoom", 12 + )), process(lineFeature(Map.of( + "waterway", "canal", + "name", "name" + )))); + + assertFeatures(14, List.of(Map.of( + "class", "stream", + "_layer", "waterway", + "_type", "line", + "_minzoom", 13 + )), process(lineFeature(Map.of( + "waterway", "stream", + "name", "name" + )))); + } + + @Test + public void testWaterwayNaturalEarth() { + assertFeatures(3, List.of(Map.of( + "class", "river", + "name", "", + "intermittent", "", + + "_layer", "waterway", + "_type", "line", + "_minzoom", 3, + "_maxzoom", 3 + )), process(new ReaderFeature( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "River", + "name", "name" + ), + NATURAL_EARTH_SOURCE, + "ne_110m_rivers_lake_centerlines", + 0 + ))); + + assertFeatures(6, List.of(Map.of( + "class", "river", + "intermittent", "", + + "_layer", "waterway", + "_type", "line", + "_minzoom", 4, + "_maxzoom", 5 + )), process(new ReaderFeature( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "River", + "name", "name" + ), + NATURAL_EARTH_SOURCE, + "ne_50m_rivers_lake_centerlines", + 0 + ))); + + assertFeatures(6, List.of(Map.of( + "class", "river", + "intermittent", "", + + "_layer", "waterway", + "_type", "line", + "_minzoom", 6, + "_maxzoom", 8 + )), process(new ReaderFeature( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "River", + "name", "name" + ), + NATURAL_EARTH_SOURCE, + "ne_10m_rivers_lake_centerlines", + 0 + ))); + } +}