emit, Integer aggStop) {
+ try {
+ var features = featureCollectors.get(SimpleFeature.fromWorldGeometry(element.source().worldGeometry()));
+ setupPoiFeature(element, features.point(LAYER_NAME), aggStop);
+ for (var feature : features) {
+ emit.accept(feature);
+ }
+ } catch (GeometryException e) {
+ e.log(stats, "agg_stop_geometry_2",
+ "Error getting geometry for the stop " + element.source().id() + " (agg_stop)");
+ }
+ }
+
+ /**
+ * We've put aside some stops for {@code agg_stop} processing and we do that processing here.
+ *
+ * The main point is to group together stops with same {@code uid_ref} and then order them first based on subclass
+ * (see {@code AGG_STOP_ORDER}) and then based on distance from centroid (calculated from all the stops). The first
+ * one gets {@code agg_stop=1}, the rest will be "normal" (e.g. no {@code agg_stop} attribute).
+ *
+ * ref: poi_stop_agg.sql
+ */
+ @Override
+ public void finish(String sourceName, FeatureCollector.Factory featureCollectors,
+ Consumer emit) {
+ if (OpenMapTilesProfile.OSM_SOURCE.equals(sourceName)) {
+ var timer = stats.startStage("agg_stop");
+ LOGGER.info("Processing {} agg_stop sets", aggStops.size());
+
+ // possible TODO: run in parallel?
+ for (var aggStopSet : aggStops.values()) {
+ if (aggStopSet.size() == 1) {
+ processAggStop(aggStopSet.get(0), featureCollectors, emit, 1);
+ continue;
+ }
+ try {
+ // find first based on subclass
+ var firstSubclass = aggStopSet.stream().sorted(BY_SUBCLASS).toArray(Tables.OsmPoiPoint[]::new)[0].subclass();
+ var topAggStops =
+ aggStopSet.stream().filter(s -> firstSubclass.equals(s.subclass())).toArray(Tables.OsmPoiPoint[]::new);
+ if (topAggStops.length <= 2) {
+ // one found => straightforward: flag it and emit
+ // two found => both will be same distance from centroid: pick first one, flag it and emit
+ processAggStop(topAggStops[0], featureCollectors, emit, 1);
+ // and emit the rest
+ aggStopSet.stream()
+ .filter(s -> !topAggStops[0].equals(s))
+ .forEach(s -> processAggStop(s, featureCollectors, emit, null));
+ continue;
+ }
+
+ // easy cases handled, now we need also centroid
+ List aggStopPoints = new ArrayList<>(aggStopSet.size());
+ for (var aggStop : aggStopSet) {
+ aggStopPoints.add(aggStop.source().worldGeometry().getCentroid());
+ }
+ var aggStopCentroid = GeoUtils.combinePoints(aggStopPoints).getCentroid();
+
+ // find nearest
+ double minDistance = Double.MAX_VALUE;
+ Tables.OsmPoiPoint nearest = null;
+ for (var aggStop : topAggStops) {
+ double distance = aggStopCentroid.distance(aggStop.source().worldGeometry());
+ if (distance < minDistance) {
+ minDistance = distance;
+ nearest = aggStop;
+ }
+ }
+
+ // emit nearest
+ if (nearest != null) {
+ processAggStop(nearest, featureCollectors, emit, 1);
+ } else {
+ stats.dataError("agg_stop_nearest");
+ LOGGER.warn("Failed to find nearest stop for agg_stop UIC ref. {}", aggStopSet.get(0).uicRef());
+ }
+
+ // emit the rest
+ final var alreadyDone = nearest;
+ aggStopSet.stream()
+ .filter(s -> !s.equals(alreadyDone))
+ .forEach(s -> processAggStop(s, featureCollectors, emit, null));
+ } catch (GeometryException e) {
+ e.log(stats, "agg_stop_geometry_1",
+ "Error getting geometry for some of the stops with UIC ref. " + aggStopSet.get(0).uicRef() + " (agg_stop)");
+ // we're not able to calculate agg_stop, so simply dump them as-is
+ aggStopSet
+ .forEach(s -> processAggStop(s, featureCollectors, emit, null));
+ }
+ }
+
+ timer.stop();
+ }
}
@Override
public void process(Tables.OsmPoiPolygon element, FeatureCollector features) {
- setupPoiFeature(element, features.centroidIfConvex(LAYER_NAME));
+ setupPoiFeature(element, features.centroidIfConvex(LAYER_NAME), null);
}
private void setupPoiFeature(
- T element, FeatureCollector.Feature output) {
+ T element, FeatureCollector.Feature output, Integer aggStop) {
String rawSubclass = element.subclass();
if ("station".equals(rawSubclass) && "subway".equals(element.station())) {
rawSubclass = "subway";
@@ -178,16 +328,29 @@ private classMapping = FieldMappings.Class.index();
private static final Set RAILWAY_RAIL_VALUES = Set.of(
FieldValues.SUBCLASS_RAIL,
@@ -132,11 +134,22 @@ public class Transportation implements
);
private static final Set SURFACE_PAVED_VALUES = Set.of(
"paved", "asphalt", "cobblestone", "concrete", "concrete:lanes", "concrete:plates", "metal",
- "paving_stones", "sett", "unhewn_cobblestone", "wood"
+ "paving_stones", "sett", "unhewn_cobblestone", "wood", "grade1"
);
private static final Set ACCESS_NO_VALUES = Set.of(
"private", "no"
);
+ private static final Set TRUNK_AS_MOTORWAY_BY_NETWORK = Set.of(
+ RouteNetwork.CA_TRANSCANADA.toString(),
+ RouteNetwork.CA_PROVINCIAL_ARTERIAL.toString(),
+ RouteNetwork.US_INTERSTATE.toString()
+ );
+ private static final Set CA_AB_PRIMARY_AS_ARTERIAL_BY_REF = Set.of(
+ "2", "3", "4"
+ );
+ private static final Set CA_BC_AS_ARTERIAL_BY_REF = Set.of(
+ "3", "5", "99"
+ );
private static final ZoomFunction.MeterToPixelThresholds MIN_LENGTH = ZoomFunction.meterThresholds()
.put(7, 50)
.put(6, 100)
@@ -152,11 +165,13 @@ public class Transportation implements
private static final Set ONEWAY_VALUES = Set.of(-1, 1);
private static final String LIMIT_MERGE_TAG = "__limit_merge";
private final AtomicBoolean loggedNoGb = new AtomicBoolean(false);
+ private final AtomicBoolean loggedNoIreland = new AtomicBoolean(false);
private final boolean z13Paths;
private final Map MINZOOMS;
private final Stats stats;
private final PlanetilerConfig config;
private PreparedGeometry greatBritain = null;
+ private PreparedGeometry ireland = null;
public Transportation(Translations translations, PlanetilerConfig config, Stats stats) {
this.config = config;
@@ -239,9 +254,12 @@ private static boolean isBridgeOrPier(String manMade) {
@Override
public void processNaturalEarth(String table, SourceFeature feature,
FeatureCollector features) {
- if ("ne_10m_admin_0_countries".equals(table) && feature.hasTag("iso_a2", "GB")) {
- // multiple threads call this method concurrently, GB polygon *should* only be found
- // once, but just to be safe synchronize updates to that field
+ if (!"ne_10m_admin_0_countries".equals(table)) {
+ return;
+ }
+ // multiple threads call this method concurrently, GB (or IE) polygon *should* only be found
+ // once, but just to be safe synchronize updates to that field
+ if (feature.hasTag("iso_a2", "GB")) {
synchronized (this) {
try {
Geometry boundary = feature.polygon().buffer(GeoUtils.metersToPixelAtEquator(0, 10_000) / 256d);
@@ -250,6 +268,15 @@ public void processNaturalEarth(String table, SourceFeature feature,
LOGGER.error("Failed to get Great Britain Polygon: " + e);
}
}
+ } else if (feature.hasTag("iso_a2", "IE")) {
+ synchronized (this) {
+ try {
+ Geometry boundary = feature.polygon().buffer(GeoUtils.metersToPixelAtEquator(0, 10_000) / 256d);
+ ireland = PreparedGeometryFactory.prepare(boundary);
+ } catch (GeometryException e) {
+ LOGGER.error("Failed to get Great Britain Polygon: " + e);
+ }
+ }
}
}
@@ -268,6 +295,26 @@ public List preprocessOsmRelation(OsmElement.Relation relation)
networkType = RouteNetwork.US_STATE;
} else if (network != null && network.startsWith("CA:transcanada")) {
networkType = RouteNetwork.CA_TRANSCANADA;
+ } else if ("CA:QC:A".equals(network)) {
+ networkType = RouteNetwork.CA_PROVINCIAL_ARTERIAL;
+ } else if ("CA:ON:primary".equals(network)) {
+ if (ref != null && ref.length() == 3 && ref.startsWith("4")) {
+ networkType = RouteNetwork.CA_PROVINCIAL_ARTERIAL;
+ } else if ("QEW".equals(ref)) {
+ networkType = RouteNetwork.CA_PROVINCIAL_ARTERIAL;
+ } else {
+ networkType = RouteNetwork.CA_PROVINCIAL;
+ }
+ } else if ("CA:MB:PTH".equals(network) && "75".equals(ref)) {
+ networkType = RouteNetwork.CA_PROVINCIAL_ARTERIAL;
+ } else if ("CA:AB:primary".equals(network) && ref != null && CA_AB_PRIMARY_AS_ARTERIAL_BY_REF.contains(ref)) {
+ networkType = RouteNetwork.CA_PROVINCIAL_ARTERIAL;
+ } else if ("CA:BC".equals(network) && ref != null && CA_BC_AS_ARTERIAL_BY_REF.contains(ref)) {
+ networkType = RouteNetwork.CA_PROVINCIAL_ARTERIAL;
+ } else if (network != null && ((network.length() == 5 && network.startsWith("CA:")) ||
+ (network.length() >= 6 && network.startsWith("CA:") && network.charAt(5) == ':'))) {
+ // in SQL: LIKE 'CA:__' OR network LIKE 'CA:__:%'; but wanted to avoid regexp hence more ugly
+ networkType = RouteNetwork.CA_PROVINCIAL;
}
int rank = switch (coalesce(network, "")) {
@@ -307,10 +354,26 @@ List getRouteRelations(Tables.OsmHighwayLinestring element) {
try {
Geometry wayGeometry = element.source().worldGeometry();
if (greatBritain.intersects(wayGeometry)) {
- Transportation.RouteNetwork networkType =
- "motorway".equals(element.highway()) ? Transportation.RouteNetwork.GB_MOTORWAY :
- Transportation.RouteNetwork.GB_TRUNK;
- String network = "motorway".equals(element.highway()) ? "omt-gb-motorway" : "omt-gb-trunk";
+ Transportation.RouteNetwork networkType;
+ String network;
+ switch (element.highway()) {
+ case "motorway" -> {
+ networkType = Transportation.RouteNetwork.GB_MOTORWAY;
+ network = "omt-gb-motorway";
+ }
+ case "trunk" -> {
+ networkType = RouteNetwork.GB_TRUNK;
+ network = "omt-gb-trunk";
+ }
+ case "primary", "secondary" -> {
+ networkType = RouteNetwork.GB_PRIMARY;
+ network = "omt-gb-primary";
+ }
+ default -> {
+ networkType = null;
+ network = null;
+ }
+ }
result.add(new RouteRelation(refMatcher.group(), network, networkType, (byte) -1,
0));
}
@@ -320,6 +383,43 @@ List getRouteRelations(Tables.OsmHighwayLinestring element) {
}
}
}
+ // Similarly Ireland.
+ refMatcher = IRELAND_REF_NETWORK_PATTERN.matcher(ref);
+ if (refMatcher.find()) {
+ if (ireland == null) {
+ if (!loggedNoIreland.get() && loggedNoIreland.compareAndSet(false, true)) {
+ LOGGER.warn("No IE polygon for inferring route network types");
+ }
+ } else {
+ try {
+ Geometry wayGeometry = element.source().worldGeometry();
+ if (ireland.intersects(wayGeometry)) {
+ Transportation.RouteNetwork networkType;
+ String network;
+ String highway = coalesce(element.highway(), "");
+ switch (highway) {
+ case "motorway" -> {
+ networkType = Transportation.RouteNetwork.IE_MOTORWAY;
+ network = "omt-ie-motorway";
+ }
+ case "trunk", "primary" -> {
+ networkType = RouteNetwork.IE_NATIONAL;
+ network = "omt-ie-national";
+ }
+ default -> {
+ networkType = RouteNetwork.IE_REGIONAL;
+ network = "omt-ie-regional";
+ }
+ }
+ result.add(new RouteRelation(refMatcher.group(), network, networkType, (byte) -1,
+ 0));
+ }
+ } catch (GeometryException e) {
+ e.log(stats, "omt_transportation_name_ie_test",
+ "Unable to test highway against IE route network: " + element.source().id());
+ }
+ }
+ }
}
Collections.sort(result);
return result;
@@ -381,7 +481,7 @@ public void process(Tables.OsmHighwayLinestring element, FeatureCollector featur
// z12+
.setAttrWithMinzoom(Fields.SERVICE, service, 12)
.setAttrWithMinzoom(Fields.ONEWAY, nullIfInt(element.isOneway(), 0), 12)
- .setAttrWithMinzoom(Fields.SURFACE, surface(element.surface()), 12)
+ .setAttrWithMinzoom(Fields.SURFACE, surface(coalesce(element.surface(), element.tracktype())), 12)
.setMinPixelSize(0) // merge during post-processing, then limit by size
.setSortKey(element.zOrder())
.setMinZoom(minzoom);
@@ -415,6 +515,15 @@ int getMinzoom(Tables.OsmHighwayLinestring element, String highwayClass) {
case FieldValues.CLASS_SERVICE -> isDrivewayOrParkingAisle(service(element.service())) ? 14 : 13;
case FieldValues.CLASS_TRACK, FieldValues.CLASS_PATH -> routeRank == 1 ? 12 :
(z13Paths || !nullOrEmpty(element.name()) || routeRank <= 2 || !nullOrEmpty(element.sacScale())) ? 13 : 14;
+ case FieldValues.CLASS_TRUNK -> {
+ // trunks in some networks to have same min. zoom as highway = "motorway"
+ String clazz = routeRelations.stream()
+ .map(RouteRelation::networkType)
+ .filter(Objects::nonNull)
+ .map(Enum::toString)
+ .anyMatch(TRUNK_AS_MOTORWAY_BY_NETWORK::contains) ? FieldValues.CLASS_MOTORWAY : FieldValues.CLASS_TRUNK;
+ yield MINZOOMS.getOrDefault(clazz, Integer.MAX_VALUE);
+ }
default -> MINZOOMS.getOrDefault(baseClass, Integer.MAX_VALUE);
};
}
@@ -552,8 +661,14 @@ enum RouteNetwork {
US_HIGHWAY("us-highway"),
US_STATE("us-state"),
CA_TRANSCANADA("ca-transcanada"),
+ CA_PROVINCIAL_ARTERIAL("ca-provincial-arterial"),
+ CA_PROVINCIAL("ca-provincial"),
GB_MOTORWAY("gb-motorway"),
- GB_TRUNK("gb-trunk");
+ GB_TRUNK("gb-trunk"),
+ GB_PRIMARY("gb-primary"),
+ IE_MOTORWAY("ie-motorway"),
+ IE_NATIONAL("ie-national"),
+ IE_REGIONAL("ie-regional");
final String name;
diff --git a/src/main/java/org/openmaptiles/layers/WaterName.java b/src/main/java/org/openmaptiles/layers/WaterName.java
index 2bc19fc9..18621048 100644
--- a/src/main/java/org/openmaptiles/layers/WaterName.java
+++ b/src/main/java/org/openmaptiles/layers/WaterName.java
@@ -35,6 +35,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
*/
package org.openmaptiles.layers;
+import static org.openmaptiles.util.Utils.coalesce;
import static org.openmaptiles.util.Utils.nullIfEmpty;
import com.carrotsearch.hppc.LongObjectMap;
@@ -48,6 +49,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
import com.onthegomap.planetiler.util.Parse;
import com.onthegomap.planetiler.util.Translations;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.ConcurrentSkipListMap;
import org.locationtech.jts.geom.Geometry;
import org.openmaptiles.OpenMapTilesProfile;
@@ -80,9 +82,8 @@ public class WaterName implements
*/
private static final Logger LOGGER = LoggerFactory.getLogger(WaterName.class);
- private static final double WORLD_AREA_FOR_70K_SQUARE_METERS =
- Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2);
private static final double LOG2 = Math.log(2);
+ private static final Set SEA_OR_OCEAN_PLACE = Set.of("sea", "ocean");
private final Translations translations;
// need to synchronize updates from multiple threads
private final LongObjectMap lakeCenterlines = Hppc.newLongObjectHashMap();
@@ -141,7 +142,10 @@ public void processNaturalEarth(String table, SourceFeature feature, FeatureColl
@Override
public void process(Tables.OsmMarinePoint element, FeatureCollector features) {
if (!element.name().isBlank()) {
- String place = element.place();
+ String clazz = coalesce(
+ nullIfEmpty(element.natural()),
+ nullIfEmpty(element.place())
+ );
var source = element.source();
// use name from OSM, but get min zoom from natural earth based on fuzzy name match...
Integer rank = Parse.parseIntOrNull(source.getTag("rank"));
@@ -159,11 +163,20 @@ public void process(Tables.OsmMarinePoint element, FeatureCollector features) {
rank = next.getValue();
}
}
- int minZoom = "ocean".equals(place) ? 0 : rank != null ? rank : 8;
+ int minZoom;
+ if ("ocean".equals(element.place())) {
+ minZoom = 0;
+ } else if (rank != null) {
+ minZoom = rank;
+ } else if ("bay".equals(element.natural())) {
+ minZoom = 13;
+ } else {
+ minZoom = 8;
+ }
features.point(LAYER_NAME)
.setBufferPixels(BUFFER_SIZE)
.putAttrs(OmtLanguageUtils.getNames(source.tags(), translations))
- .setAttr(Fields.CLASS, place)
+ .setAttr(Fields.CLASS, clazz)
.setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0)
.setMinZoom(minZoom);
}
@@ -176,6 +189,16 @@ public void process(Tables.OsmWaterPolygon element, FeatureCollector features) {
Geometry centerlineGeometry = lakeCenterlines.get(element.source().id());
FeatureCollector.Feature feature;
int minzoom = 9;
+ String place = element.place();
+ String clazz;
+ if ("bay".equals(element.natural())) {
+ clazz = FieldValues.CLASS_BAY;
+ } else if ("sea".equals(place)) {
+ clazz = FieldValues.CLASS_SEA;
+ } else {
+ clazz = FieldValues.CLASS_LAKE;
+ minzoom = 3;
+ }
if (centerlineGeometry != null) {
// prefer lake centerline if it exists
feature = features.geometry(LAYER_NAME, centerlineGeometry)
@@ -183,13 +206,15 @@ public void process(Tables.OsmWaterPolygon element, FeatureCollector features) {
} else {
// otherwise just use a label point inside the lake
feature = features.pointOnSurface(LAYER_NAME);
- Geometry geometry = element.source().worldGeometry();
- double area = geometry.getArea();
- minzoom = (int) Math.floor(20 - Math.log(area / WORLD_AREA_FOR_70K_SQUARE_METERS) / LOG2);
- minzoom = Math.min(14, Math.max(9, minzoom));
+ double area = element.source().area();
+ if (place != null && SEA_OR_OCEAN_PLACE.contains(place)) {
+ minzoom = 0;
+ } else {
+ minzoom = areaToMinZoom(area);
+ }
}
feature
- .setAttr(Fields.CLASS, FieldValues.CLASS_LAKE)
+ .setAttr(Fields.CLASS, clazz)
.setBufferPixels(BUFFER_SIZE)
.putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations))
.setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0)
@@ -199,4 +224,17 @@ public void process(Tables.OsmWaterPolygon element, FeatureCollector features) {
}
}
}
+
+ public static int areaToMinZoom(double areaWorld) {
+ double oneSideWorld = Math.sqrt(areaWorld);
+ // full(-er) formula (along with comments) is in WaterNameTest.testAreaToMinZoom(), here is simplified reverse of that
+ double zoom = -(Math.log(oneSideWorld) / LOG2) - 1;
+
+ // Say Z13.01 means bellow threshold, Z13.00 is exactly threshold, Z12.99 is over threshold,
+ // hence Z13.01 and Z13.00 will be rounded to Z14 and Z12.99 to Z13 (e.g. `floor() + 1`).
+ // And to accommodate for some precision errors (observed for Z9-Z11) we do also `- 0.1e-11`.
+ int result = (int) Math.floor(zoom - 0.1e-11) + 1;
+
+ return Math.min(14, Math.max(3, result));
+ }
}
diff --git a/src/test/java/org/openmaptiles/layers/PoiTest.java b/src/test/java/org/openmaptiles/layers/PoiTest.java
index 11a9a705..f4e10ced 100644
--- a/src/test/java/org/openmaptiles/layers/PoiTest.java
+++ b/src/test/java/org/openmaptiles/layers/PoiTest.java
@@ -1,13 +1,19 @@
package org.openmaptiles.layers;
+import static com.onthegomap.planetiler.TestUtils.newPoint;
+
+import com.onthegomap.planetiler.FeatureCollector;
import com.onthegomap.planetiler.geo.GeometryException;
+import com.onthegomap.planetiler.reader.SimpleFeature;
import com.onthegomap.planetiler.reader.SourceFeature;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
+import org.openmaptiles.OpenMapTilesProfile;
class PoiTest extends AbstractLayerTest {
@@ -63,6 +69,159 @@ void testSubway(boolean area) {
))));
}
+ private List testAggStops(List sourceFeatures) {
+ sourceFeatures.forEach(this::process);
+
+ List features = new ArrayList<>();
+ profile.finish(OpenMapTilesProfile.OSM_SOURCE, featureCollectorFactory, features::add);
+
+ return features;
+ }
+
+ @Test
+ void testAggStopJustOne() {
+ var result = testAggStops(List.of(pointFeature(Map.of(
+ "highway", "bus_stop",
+ "name", "station",
+ "uic_ref", "1"
+ ))));
+ assertFeatures(14, List.of(Map.of(
+ "_layer", "poi",
+ "class", "bus",
+ "subclass", "bus_stop",
+ "agg_stop", 1,
+ "_minzoom", 14
+ )), result);
+ }
+
+ @Test
+ void testAggStopTwoWithSameSubclass() {
+ var result = testAggStops(List.of(
+ pointFeature(Map.of(
+ "railway", "tram_stop",
+ "name", "station 1",
+ "uic_ref", "1"
+ )),
+ pointFeature(Map.of(
+ "railway", "tram_stop",
+ "name", "station 2",
+ "uic_ref", "1"
+ ))
+ ));
+ assertFeatures(14, List.of(
+ Map.of(
+ "_layer", "poi",
+ "name", "station 1",
+ "class", "railway",
+ "subclass", "tram_stop",
+ "agg_stop", 1,
+ "_minzoom", 14
+ ),
+ Map.of(
+ "_layer", "poi",
+ "name", "station 2",
+ "class", "railway",
+ "subclass", "tram_stop",
+ "agg_stop", "",
+ "_minzoom", 14
+ )
+ ), result);
+ }
+
+ @Test
+ void testAggStopThreeWithMixedSubclass() {
+ var result = testAggStops(List.of(
+ pointFeature(Map.of(
+ "highway", "bus_stop",
+ "name", "station 1",
+ "uic_ref", "1"
+ )),
+ pointFeature(Map.of(
+ "highway", "bus_stop",
+ "name", "station 2",
+ "uic_ref", "1"
+ )),
+ pointFeature(Map.of(
+ "railway", "tram_stop",
+ "name", "station 3",
+ "uic_ref", "1"
+ ))
+ ));
+ assertFeatures(14, List.of(
+ Map.of(
+ "_layer", "poi",
+ "name", "station 3",
+ "class", "railway",
+ "subclass", "tram_stop",
+ "agg_stop", 1,
+ "_minzoom", 14
+ ),
+ Map.of(
+ "_layer", "poi",
+ "name", "station 1",
+ "class", "bus",
+ "subclass", "bus_stop",
+ "agg_stop", "",
+ "_minzoom", 14
+ ),
+ Map.of(
+ "_layer", "poi",
+ "name", "station 2",
+ "class", "bus",
+ "subclass", "bus_stop",
+ "agg_stop", "",
+ "_minzoom", 14
+ )
+ ), result);
+ }
+
+ @Test
+ void testAggStopThreeWithSameSubclass() {
+ var result = testAggStops(List.of(
+ SimpleFeature.create(newPoint(0, 0), Map.of(
+ "highway", "bus_stop",
+ "name", "station 1",
+ "uic_ref", "1"
+ ), OpenMapTilesProfile.OSM_SOURCE, null, 0),
+ SimpleFeature.create(newPoint(1, 0), Map.of(
+ "highway", "bus_stop",
+ "name", "station 2",
+ "uic_ref", "1"
+ ), OpenMapTilesProfile.OSM_SOURCE, null, 1),
+ SimpleFeature.create(newPoint(2, 0), Map.of(
+ "highway", "bus_stop",
+ "name", "station 3",
+ "uic_ref", "1"
+ ), OpenMapTilesProfile.OSM_SOURCE, null, 2)
+ ));
+ assertFeatures(14, List.of(
+ Map.of(
+ "_layer", "poi",
+ "name", "station 2",
+ "class", "bus",
+ "subclass", "bus_stop",
+ "agg_stop", 1,
+ "_minzoom", 14
+ ),
+ Map.of(
+ "_layer", "poi",
+ "name", "station 1",
+ "class", "bus",
+ "subclass", "bus_stop",
+ "agg_stop", "",
+ "_minzoom", 14
+ ),
+ Map.of(
+ "_layer", "poi",
+ "name", "station 3",
+ "class", "bus",
+ "subclass", "bus_stop",
+ "agg_stop", "",
+ "_minzoom", 14
+ )
+ ), result);
+ }
+
@ParameterizedTest
@ValueSource(booleans = {false, true})
void testPlaceOfWorshipFromReligionTag(boolean area) {
@@ -274,4 +433,56 @@ void testParcelLockerCornerCase() {
"ref", "Corner Case"
))));
}
+
+ private record TestEntry(
+ SourceFeature feature,
+ int expectedZoom
+ ) {}
+
+ private void createUniAreaForMinZoomTest(List testEntries, double side, int expectedZoom, String name) {
+ double area = Math.pow(side, 2);
+ var feature = polygonFeatureWithArea(area, Map.of(
+ "name", name,
+ "amenity", "university"
+ ));
+ testEntries.add(new TestEntry(
+ feature,
+ Math.min(14, Math.max(10, expectedZoom))
+ ));
+ }
+
+ @Test
+ void testUniAreaToMinZoom() throws GeometryException {
+ // threshold is 1/10 of tile area, hence ...
+ // ... side is 1/sqrt(10) tile side: from pixels to world coord, for say Z14 ...
+ //final double PORTION_OF_TILE_SIDE = (256d / Math.sqrt(10)) / Math.pow(2d, 14d + 8d);
+ // ... and then for some lower zoom:
+ //double testAreaSide = PORTION_OF_TILE_SIDE * Math.pow(2, 14 - zoom);
+ // all this then simplified to `testAreaSide` calculation bellow
+
+ final double SQRT10 = Math.sqrt(10);
+ final List testEntries = new ArrayList<>();
+ for (int zoom = 14; zoom >= 0; zoom--) {
+ double testAreaSide = Math.pow(2, -zoom) / SQRT10;
+
+ // slightly bellow the threshold
+ createUniAreaForMinZoomTest(testEntries, testAreaSide * 0.999, zoom + 1, "uni-");
+ // precisely at the threshold
+ createUniAreaForMinZoomTest(testEntries, testAreaSide, zoom, "uni=");
+ // slightly over the threshold
+ createUniAreaForMinZoomTest(testEntries, testAreaSide * 1.001, zoom, "uni+");
+ }
+
+ for (var entry : testEntries) {
+ assertFeatures(14, List.of(Map.of(
+ "_layer", "landuse",
+ "class", "university"
+ ), Map.of(
+ "_layer", "poi",
+ "_type", "point",
+ "_minzoom", entry.expectedZoom,
+ "_maxzoom", 14
+ )), process(entry.feature));
+ }
+ }
}
diff --git a/src/test/java/org/openmaptiles/layers/TransportationTest.java b/src/test/java/org/openmaptiles/layers/TransportationTest.java
index 5f1e6636..943c10aa 100644
--- a/src/test/java/org/openmaptiles/layers/TransportationTest.java
+++ b/src/test/java/org/openmaptiles/layers/TransportationTest.java
@@ -3,6 +3,7 @@
import static com.onthegomap.planetiler.TestUtils.newLineString;
import static com.onthegomap.planetiler.TestUtils.newPoint;
import static com.onthegomap.planetiler.TestUtils.rectangle;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import com.onthegomap.planetiler.FeatureCollector;
import com.onthegomap.planetiler.config.Arguments;
@@ -15,6 +16,7 @@
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@@ -909,6 +911,316 @@ void testTransCanadaHighway() {
)), features);
}
+ @Test
+ void testTransCanadaTrunk() {
+ var rel = new OsmElement.Relation(1);
+ rel.setTag("type", "route");
+ rel.setTag("route", "road");
+ rel.setTag("network", "CA:transcanada:namedRoute");
+
+ FeatureCollector features = process(lineFeatureWithRelation(
+ profile.preprocessOsmRelation(rel),
+ Map.of(
+ "highway", "trunk"
+ )));
+
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "trunk",
+ "_minzoom", 4
+ )), features);
+ }
+
+ @Test
+ void testTransCanadaProvincialCaQcA() {
+ var rel = new OsmElement.Relation(1);
+ rel.setTag("type", "route");
+ rel.setTag("route", "road");
+ rel.setTag("network", "CA:QC:A");
+
+ FeatureCollector features = process(lineFeatureWithRelation(
+ profile.preprocessOsmRelation(rel),
+ Map.of(
+ "highway", "trunk"
+ )));
+
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "trunk",
+ "network", "ca-provincial-arterial",
+ "_minzoom", 4
+ )), features);
+ }
+
+ @Test
+ void testTransCanadaProvincialCaOnPrimaryRef4xx() {
+ var rel = new OsmElement.Relation(1);
+ rel.setTag("type", "route");
+ rel.setTag("route", "road");
+ rel.setTag("network", "CA:ON:primary");
+ rel.setTag("ref", "420");
+
+ FeatureCollector features = process(lineFeatureWithRelation(
+ profile.preprocessOsmRelation(rel),
+ Map.of(
+ "highway", "trunk"
+ )));
+
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "trunk",
+ "network", "ca-provincial-arterial",
+ "_minzoom", 4
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "trunk",
+ "ref", "420",
+ "network", "ca-provincial-arterial"
+ )), features);
+ }
+
+ @Test
+ void testTransCanadaProvincialCaOnPrimaryRefQew() {
+ var rel = new OsmElement.Relation(1);
+ rel.setTag("type", "route");
+ rel.setTag("route", "road");
+ rel.setTag("network", "CA:ON:primary");
+ rel.setTag("ref", "QEW");
+
+ FeatureCollector features = process(lineFeatureWithRelation(
+ profile.preprocessOsmRelation(rel),
+ Map.of(
+ "highway", "trunk"
+ )));
+
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "trunk",
+ "network", "ca-provincial-arterial",
+ "_minzoom", 4
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "trunk",
+ "ref", "QEW",
+ "network", "ca-provincial-arterial"
+ )), features);
+ }
+
+ @Test
+ void testTransCanadaProvincialCaOnPrimaryRefOther() {
+ var rel = new OsmElement.Relation(1);
+ rel.setTag("type", "route");
+ rel.setTag("route", "road");
+ rel.setTag("network", "CA:ON:primary");
+ rel.setTag("ref", "85");
+
+ FeatureCollector features = process(lineFeatureWithRelation(
+ profile.preprocessOsmRelation(rel),
+ Map.of(
+ "highway", "trunk"
+ )));
+
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "trunk",
+ "network", "ca-provincial",
+ "_minzoom", 5
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "trunk",
+ "ref", "85",
+ "network", "ca-provincial"
+ )), features);
+ }
+
+ @Test
+ void testTransCanadaProvincialCaMbPthRef75() {
+ var rel = new OsmElement.Relation(1);
+ rel.setTag("type", "route");
+ rel.setTag("route", "road");
+ rel.setTag("network", "CA:MB:PTH");
+ rel.setTag("ref", "75");
+
+ FeatureCollector features = process(lineFeatureWithRelation(
+ profile.preprocessOsmRelation(rel),
+ Map.of(
+ "highway", "trunk"
+ )));
+
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "trunk",
+ "network", "ca-provincial-arterial",
+ "_minzoom", 4
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "trunk",
+ "ref", "75",
+ "network", "ca-provincial-arterial"
+ )), features);
+ }
+
+ @Test
+ void testTransCanadaProvincialCaMbPthRefOther() {
+ var rel = new OsmElement.Relation(1);
+ rel.setTag("type", "route");
+ rel.setTag("route", "road");
+ rel.setTag("network", "CA:MB:PTH");
+ rel.setTag("ref", "77");
+
+ FeatureCollector features = process(lineFeatureWithRelation(
+ profile.preprocessOsmRelation(rel),
+ Map.of(
+ "highway", "trunk"
+ )));
+
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "trunk",
+ "network", "ca-provincial",
+ "_minzoom", 5
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "trunk",
+ "ref", "77",
+ "network", "ca-provincial"
+ )), features);
+ }
+
+ @Test
+ void testTransCanadaProvincialCaAbPrimaryRef3() {
+ var rel = new OsmElement.Relation(1);
+ rel.setTag("type", "route");
+ rel.setTag("route", "road");
+ rel.setTag("network", "CA:AB:primary");
+ rel.setTag("ref", "3");
+
+ FeatureCollector features = process(lineFeatureWithRelation(
+ profile.preprocessOsmRelation(rel),
+ Map.of(
+ "highway", "trunk"
+ )));
+
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "trunk",
+ "network", "ca-provincial-arterial",
+ "_minzoom", 4
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "trunk",
+ "ref", "3",
+ "network", "ca-provincial-arterial"
+ )), features);
+ }
+
+ @Test
+ void testTransCanadaProvincialCaAbPrimaryRefOther() {
+ var rel = new OsmElement.Relation(1);
+ rel.setTag("type", "route");
+ rel.setTag("route", "road");
+ rel.setTag("network", "CA:AB:primary");
+ rel.setTag("ref", "10");
+
+ FeatureCollector features = process(lineFeatureWithRelation(
+ profile.preprocessOsmRelation(rel),
+ Map.of(
+ "highway", "trunk"
+ )));
+
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "trunk",
+ "network", "ca-provincial",
+ "_minzoom", 5
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "trunk",
+ "ref", "10",
+ "network", "ca-provincial"
+ )), features);
+ }
+
+ @Test
+ void testTransCanadaProvincialCaBcRef3() {
+ var rel = new OsmElement.Relation(1);
+ rel.setTag("type", "route");
+ rel.setTag("route", "road");
+ rel.setTag("network", "CA:BC");
+ rel.setTag("ref", "3");
+
+ FeatureCollector features = process(lineFeatureWithRelation(
+ profile.preprocessOsmRelation(rel),
+ Map.of(
+ "highway", "trunk"
+ )));
+
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "trunk",
+ "network", "ca-provincial-arterial",
+ "_minzoom", 4
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "trunk",
+ "ref", "3",
+ "network", "ca-provincial-arterial"
+ )), features);
+ }
+
+ @Test
+ void testTransCanadaProvincialCaBcRefOther() {
+ var rel = new OsmElement.Relation(1);
+ rel.setTag("type", "route");
+ rel.setTag("route", "road");
+ rel.setTag("network", "CA:BC");
+ rel.setTag("ref", "10");
+
+ FeatureCollector features = process(lineFeatureWithRelation(
+ profile.preprocessOsmRelation(rel),
+ Map.of(
+ "highway", "trunk"
+ )));
+
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "trunk",
+ "network", "ca-provincial",
+ "_minzoom", 5
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "trunk",
+ "ref", "10",
+ "network", "ca-provincial"
+ )), features);
+ }
+
+ @Test
+ void testTransCanadaProvincialCaOther() {
+ var rel = new OsmElement.Relation(1);
+ rel.setTag("type", "route");
+ rel.setTag("route", "road");
+ rel.setTag("network", "CA:yellowhead");
+
+ FeatureCollector features = process(lineFeatureWithRelation(
+ profile.preprocessOsmRelation(rel),
+ Map.of(
+ "highway", "trunk"
+ )));
+
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "trunk",
+ "_minzoom", 5
+ )), features);
+ boolean caProvPresent = StreamSupport.stream(features.spliterator(), false)
+ .flatMap(f -> f.getAttrsAtZoom(13).entrySet().stream())
+ .filter(e -> "network".equals(e.getKey()))
+ .map(Map.Entry::getValue)
+ .anyMatch(v -> "ca-provincial".equals(v) || "ca-provincial-arterial".equals(v));
+ assertFalse(caProvPresent, "ca-provincial present");
+ }
+
@Test
void testGreatBritainHighway() {
process(SimpleFeature.create(
@@ -972,6 +1284,307 @@ void testGreatBritainHighway() {
)));
}
+ @Test
+ void testGreatBritainTrunk() {
+ process(SimpleFeature.create(
+ rectangle(0, 0.1),
+ Map.of("iso_a2", "GB"),
+ OpenMapTilesProfile.NATURAL_EARTH_SOURCE,
+ "ne_10m_admin_0_countries",
+ 0
+ ));
+
+ // in GB
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "trunk",
+ "_minzoom", 5
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "trunk",
+ "ref", "A272",
+ "ref_length", 4,
+ "network", "gb-trunk",
+ "_minzoom", 8
+ )), process(SimpleFeature.create(
+ newLineString(0, 0, 1, 1),
+ Map.of(
+ "highway", "trunk",
+ "ref", "A272"
+ ),
+ OpenMapTilesProfile.OSM_SOURCE,
+ null,
+ 0
+ )));
+ }
+
+ @Test
+ void testGreatBritainPrimary() {
+ process(SimpleFeature.create(
+ rectangle(0, 0.1),
+ Map.of("iso_a2", "GB"),
+ OpenMapTilesProfile.NATURAL_EARTH_SOURCE,
+ "ne_10m_admin_0_countries",
+ 0
+ ));
+
+ // in GB
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "primary",
+ "_minzoom", 7
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "primary",
+ "ref", "A598",
+ "ref_length", 4,
+ "network", "gb-primary",
+ "_minzoom", 12
+ )), process(SimpleFeature.create(
+ newLineString(0, 0, 1, 1),
+ Map.of(
+ "highway", "primary",
+ "ref", "A598"
+ ),
+ OpenMapTilesProfile.OSM_SOURCE,
+ null,
+ 0
+ )));
+ }
+
+ @Test
+ void testGreatBritainSecondary() {
+ process(SimpleFeature.create(
+ rectangle(0, 0.1),
+ Map.of("iso_a2", "GB"),
+ OpenMapTilesProfile.NATURAL_EARTH_SOURCE,
+ "ne_10m_admin_0_countries",
+ 0
+ ));
+
+ // in GB
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "secondary",
+ "_minzoom", 9
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "secondary",
+ "ref", "B4558",
+ "ref_length", 5,
+ "network", "gb-primary",
+ "_minzoom", 12
+ )), process(SimpleFeature.create(
+ newLineString(0, 0, 1, 1),
+ Map.of(
+ "highway", "secondary",
+ "ref", "B4558"
+ ),
+ OpenMapTilesProfile.OSM_SOURCE,
+ null,
+ 0
+ )));
+ }
+
+ @Test
+ void testGreatBritainTertiary() {
+ process(SimpleFeature.create(
+ rectangle(0, 0.1),
+ Map.of("iso_a2", "GB"),
+ OpenMapTilesProfile.NATURAL_EARTH_SOURCE,
+ "ne_10m_admin_0_countries",
+ 0
+ ));
+
+ // in GB
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "tertiary",
+ "_minzoom", 11
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "tertiary",
+ "ref", "B4086",
+ "ref_length", 5,
+ "network", "road",
+ "_minzoom", 12
+ )), process(SimpleFeature.create(
+ newLineString(0, 0, 1, 1),
+ Map.of(
+ "highway", "tertiary",
+ "ref", "B4086"
+ ),
+ OpenMapTilesProfile.OSM_SOURCE,
+ null,
+ 0
+ )));
+ }
+
+ @Test
+ void testIrelandHighway() {
+ process(SimpleFeature.create(
+ rectangle(0, 0.1),
+ Map.of("iso_a2", "IE"),
+ OpenMapTilesProfile.NATURAL_EARTH_SOURCE,
+ "ne_10m_admin_0_countries",
+ 0
+ ));
+
+ // in IE
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "motorway",
+ "oneway", 1,
+ "ramp", "",
+ "_minzoom", 4
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "motorway",
+ "ref", "M18",
+ "ref_length", 3,
+ "network", "ie-motorway",
+ "_minzoom", 6
+ )), process(SimpleFeature.create(
+ newLineString(0, 0, 1, 1),
+ Map.of(
+ "highway", "motorway",
+ "oneway", "yes",
+ "ref", "M18"
+ ),
+ OpenMapTilesProfile.OSM_SOURCE,
+ null,
+ 0
+ )));
+
+ // not in IE
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "motorway",
+ "oneway", 1,
+ "ramp", "",
+ "_minzoom", 4
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "motorway",
+ "ref", "M18",
+ "ref_length", 3,
+ "network", "road",
+ "_minzoom", 6
+ )), process(SimpleFeature.create(
+ newLineString(1, 0, 0, 1),
+ Map.of(
+ "highway", "motorway",
+ "oneway", "yes",
+ "ref", "M18"
+ ),
+ OpenMapTilesProfile.OSM_SOURCE,
+ null,
+ 0
+ )));
+ }
+
+ @Test
+ void testIrelandTrunk() {
+ process(SimpleFeature.create(
+ rectangle(0, 0.1),
+ Map.of("iso_a2", "IE"),
+ OpenMapTilesProfile.NATURAL_EARTH_SOURCE,
+ "ne_10m_admin_0_countries",
+ 0
+ ));
+
+ // in IE
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "trunk",
+ "_minzoom", 5
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "trunk",
+ "ref", "N8",
+ "ref_length", 2,
+ "network", "ie-national",
+ "_minzoom", 8
+ )), process(SimpleFeature.create(
+ newLineString(0, 0, 1, 1),
+ Map.of(
+ "highway", "trunk",
+ "ref", "N8"
+ ),
+ OpenMapTilesProfile.OSM_SOURCE,
+ null,
+ 0
+ )));
+ }
+
+ @Test
+ void testIrelandPrimary() {
+ process(SimpleFeature.create(
+ rectangle(0, 0.1),
+ Map.of("iso_a2", "IE"),
+ OpenMapTilesProfile.NATURAL_EARTH_SOURCE,
+ "ne_10m_admin_0_countries",
+ 0
+ ));
+
+ // in IE
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "primary",
+ "_minzoom", 7
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "primary",
+ "ref", "N59",
+ "ref_length", 3,
+ "network", "ie-national",
+ "_minzoom", 12
+ )), process(SimpleFeature.create(
+ newLineString(0, 0, 1, 1),
+ Map.of(
+ "highway", "primary",
+ "ref", "N59"
+ ),
+ OpenMapTilesProfile.OSM_SOURCE,
+ null,
+ 0
+ )));
+ }
+
+ @Test
+ void testIrelandSecondary() {
+ process(SimpleFeature.create(
+ rectangle(0, 0.1),
+ Map.of("iso_a2", "IE"),
+ OpenMapTilesProfile.NATURAL_EARTH_SOURCE,
+ "ne_10m_admin_0_countries",
+ 0
+ ));
+
+ // in IE
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "secondary",
+ "_minzoom", 9
+ ), Map.of(
+ "_layer", "transportation_name",
+ "class", "secondary",
+ "ref", "R813",
+ "ref_length", 4,
+ "network", "ie-regional",
+ "_minzoom", 12
+ )), process(SimpleFeature.create(
+ newLineString(0, 0, 1, 1),
+ Map.of(
+ "highway", "secondary",
+ "ref", "R813"
+ ),
+ OpenMapTilesProfile.OSM_SOURCE,
+ null,
+ 0
+ )));
+ }
+
@Test
void testMergesDisconnectedRoadNameFeatures() throws GeometryException {
testMergesLinestrings(Map.of("class", "motorway"), TransportationName.LAYER_NAME, 10, 14);
@@ -1315,4 +1928,28 @@ void testIssue86() {
"service", "driveway"
))));
}
+
+ @Test
+ void testGrade1SurfacePath() {
+ assertFeatures(14, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "track",
+ "surface", "paved"
+ )), process(lineFeature(Map.of(
+ "surface", "grade1",
+ "highway", "track"
+ ))));
+ }
+
+ @Test
+ void testGrade1TracktypePath() {
+ assertFeatures(14, List.of(Map.of(
+ "_layer", "transportation",
+ "class", "track",
+ "surface", "paved"
+ )), process(lineFeature(Map.of(
+ "tracktype", "grade1",
+ "highway", "track"
+ ))));
+ }
}
diff --git a/src/test/java/org/openmaptiles/layers/WaterNameTest.java b/src/test/java/org/openmaptiles/layers/WaterNameTest.java
index 54e0b00d..fee396b1 100644
--- a/src/test/java/org/openmaptiles/layers/WaterNameTest.java
+++ b/src/test/java/org/openmaptiles/layers/WaterNameTest.java
@@ -5,7 +5,10 @@
import com.onthegomap.planetiler.TestUtils;
import com.onthegomap.planetiler.geo.GeoUtils;
+import com.onthegomap.planetiler.geo.GeometryException;
import com.onthegomap.planetiler.reader.SimpleFeature;
+import com.onthegomap.planetiler.reader.SourceFeature;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -27,7 +30,7 @@ void testWaterNamePoint() {
"_layer", "water_name",
"_type", "point",
- "_minzoom", 9,
+ "_minzoom", 3,
"_maxzoom", 14
)), process(polygonFeatureWithArea(1, Map.of(
"name", "waterway",
@@ -36,7 +39,8 @@ void testWaterNamePoint() {
"water", "pond",
"intermittent", "1"
))));
- double z11area = Math.pow((GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d), 2) * Math.pow(2, 20 - 11);
+ // 1/4 th of tile area is the threshold, 1/4 = 0.25 => area->side:0.25->0.5 => slightly bigger -> 0.51
+ double z11area = Math.pow(0.51d / Math.pow(2, 11), 2);
assertFeatures(10, List.of(Map.of(
"_layer", "water"
), Map.of(
@@ -51,6 +55,23 @@ void testWaterNamePoint() {
))));
}
+ // https://zelonewolf.github.io/openstreetmap-americana/#map=13/41.43989/-71.5716
+ @Test
+ void testWordenPondNamePoint() {
+ assertFeatures(10, List.of(Map.of(
+ "_layer", "water"
+ ), Map.of(
+ "_layer", "water_name",
+ "_type", "point",
+ "_minzoom", 13,
+ "_maxzoom", 14
+ )), process(polygonFeatureWithArea(4.930387948170328E-9, Map.of(
+ "name", "waterway",
+ "natural", "water",
+ "water", "pond"
+ ))));
+ }
+
@Test
void testWaterNameLakeline() {
assertFeatures(11, List.of(), process(SimpleFeature.create(
@@ -71,7 +92,7 @@ void testWaterNameLakeline() {
"_layer", "water_name",
"_type", "line",
"_geom", new TestUtils.NormGeometry(GeoUtils.latLonToWorldCoords(newLineString(0, 0, 1, 1))),
- "_minzoom", 9,
+ "_minzoom", 3,
"_maxzoom", 14,
"_minpixelsize", "waterway".length() * 6d
)), process(SimpleFeature.create(
@@ -121,7 +142,7 @@ void testWaterNameMultipleLakelines() {
newLineString(0, 0, 1, 1),
newLineString(2, 2, 3, 3)
}))),
- "_minzoom", 9,
+ "_minzoom", 3,
"_maxzoom", 14,
"_minpixelsize", "waterway".length() * 6d
)), process(SimpleFeature.create(
@@ -138,6 +159,40 @@ void testWaterNameMultipleLakelines() {
)));
}
+ @Test
+ void testWaterNameBay() {
+ assertFeatures(11, List.of(), process(SimpleFeature.create(
+ newLineString(0, 0, 1, 1),
+ new HashMap<>(Map.of(
+ "OSM_ID", -10
+ )),
+ OpenMapTilesProfile.LAKE_CENTERLINE_SOURCE,
+ null,
+ 0
+ )));
+ assertFeatures(10, List.of(Map.of(
+ "name", "bay",
+ "name:es", "bay es",
+
+ "_layer", "water_name",
+ "_type", "line",
+ "_geom", new TestUtils.NormGeometry(GeoUtils.latLonToWorldCoords(newLineString(0, 0, 1, 1))),
+ "_minzoom", 9,
+ "_maxzoom", 14,
+ "_minpixelsize", "bay".length() * 6d
+ )), process(SimpleFeature.create(
+ GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))),
+ new HashMap<>(Map.of(
+ "name", "bay",
+ "name:es", "bay es",
+ "natural", "bay"
+ )),
+ OpenMapTilesProfile.OSM_SOURCE,
+ null,
+ 10
+ )));
+ }
+
@Test
void testMarinePoint() {
assertFeatures(11, List.of(), process(SimpleFeature.create(
@@ -201,4 +256,54 @@ void testMarinePoint() {
"place", "sea"
))));
}
+
+ private record TestEntry(
+ SourceFeature feature,
+ int expectedZoom
+ ) {}
+
+ private void createAreaForMinZoomTest(List testEntries, double side, int expectedZoom, String name) {
+ double area = Math.pow(side, 2);
+ var feature = polygonFeatureWithArea(area, Map.of(
+ "name", name,
+ "natural", "water"
+ ));
+ testEntries.add(new TestEntry(
+ feature,
+ Math.min(14, Math.max(3, expectedZoom))
+ ));
+ }
+
+ @Test
+ void testAreaToMinZoom() throws GeometryException {
+ // threshold is 1/4 of tile area, hence ...
+ // ... side is 1/2 tile side: from pixels to world coord, for say Z14 ...
+ //final double HALF_OF_TILE_SIDE = 128d / Math.pow(2d, 14d + 8d);
+ // ... and then for some lower zoom:
+ //double testAreaSide = HALF_OF_TILE_SIDE * Math.pow(2, 14 - zoom);
+ // all this then simplified to `testAreaSide` calculation bellow
+
+ final List testEntries = new ArrayList<>();
+ for (int zoom = 14; zoom >= 0; zoom--) {
+ double testAreaSide = Math.pow(2, -zoom - 1);
+
+ // slightly bellow the threshold
+ createAreaForMinZoomTest(testEntries, testAreaSide * 0.999, zoom + 1, "waterway-");
+ // precisely at the threshold
+ createAreaForMinZoomTest(testEntries, testAreaSide, zoom, "waterway=");
+ // slightly over the threshold
+ createAreaForMinZoomTest(testEntries, testAreaSide * 1.001, zoom, "waterway+");
+ }
+
+ for (var entry : testEntries) {
+ assertFeatures(10, List.of(Map.of(
+ "_layer", "water"
+ ), Map.of(
+ "_layer", "water_name",
+ "_type", "point",
+ "_minzoom", entry.expectedZoom,
+ "_maxzoom", 14
+ )), process(entry.feature));
+ }
+ }
}