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());
+
+ for (var aggStopSet : aggStops.values()) {
+ if (aggStopSet.size() == 1) {
+ processAggStop(aggStopSet.getFirst(), featureCollectors, emit, 1);
+ continue;
+ }
+
+ Tables.OsmPoiPoint nearest = null;
+ try {
+ // find most important stops based on subclass
+ var firstSubclass = aggStopSet.stream().min(BY_SUBCLASS).get().subclass();
+ var topAggStops =
+ aggStopSet.stream().filter(s -> firstSubclass.equals(s.subclass())).toArray(Tables.OsmPoiPoint[]::new);
+
+ // calculate the centroid and ...
+ List aggStopPoints = new ArrayList<>(aggStopSet.size());
+ for (var aggStop : aggStopSet) {
+ aggStopPoints.add(aggStop.source().worldGeometry().getCentroid());
+ }
+ var aggStopCentroid = GeoUtils.combinePoints(aggStopPoints).getCentroid();
+
+ // ... find one stop nearest to the centroid
+ double minDistance = Double.MAX_VALUE;
+ for (var aggStop : topAggStops) {
+ double distance = aggStopCentroid.distance(aggStop.source().worldGeometry());
+ if (distance < minDistance) {
+ minDistance = distance;
+ nearest = aggStop;
+ }
+ }
+ } catch (GeometryException e) {
+ e.log(stats, "agg_stop_geometry_1",
+ "Error getting geometry for some of the stops with UIC ref. " + aggStopSet.getFirst().uicRef() +
+ " (agg_stop)");
+ // we're not able to calculate agg_stop, so simply dump the stops as they are
+ nearest = null;
+ }
+
+ // now emit the stops
+ final Tables.OsmPoiPoint nearestFinal = nearest; // final needed for lambda
+ aggStopSet
+ .forEach(s -> processAggStop(s, featureCollectors, emit, s == nearestFinal ? 1 : 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";
@@ -157,7 +277,7 @@ public class Poi implements
}
// Parcel locker without name: use either brand or operator and add ref if present
- if ("parcel_locker".equals(rawSubclass) && nullOrEmpty(name)) {
+ if (BRAND_OPERATOR_REF_SUBCLASSES.contains(rawSubclass) && nullOrEmpty(name)) {
name = coalesce(nullIfEmpty(element.brand()), nullIfEmpty(element.operator()));
String ref = nullIfEmpty(element.ref());
if (ref != null) {
@@ -178,16 +298,24 @@ public class Poi implements
int poiClassRank = poiClassRank(poiClass);
int rankOrder = poiClassRank + ((nullOrEmpty(name)) ? 2000 : 0);
+ int minzoom = minzoom(element.subclass(), element.mappingKey());
+ if (UNIVERSITY_POI_SUBCLASSES.contains(rawSubclass)) {
+ // universities that are at least 10% of a tile may appear from Z10
+ output.setMinPixelSizeBelowZoom(13, 80); // 80x80px is ~10% of a 256x256px tile
+ minzoom = 10;
+ }
+
output.setBufferPixels(BUFFER_SIZE)
.setAttr(Fields.CLASS, poiClass)
.setAttr(Fields.SUBCLASS, subclass)
.setAttr(Fields.LAYER, nullIfLong(element.layer(), 0))
.setAttr(Fields.LEVEL, Parse.parseLongOrNull(element.source().getTag("level")))
.setAttr(Fields.INDOOR, element.indoor() ? 1 : null)
+ .setAttr(Fields.AGG_STOP, aggStop)
.putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations))
.setPointLabelGridPixelSize(14, 64)
.setSortKey(rankOrder)
- .setMinZoom(minzoom(element.subclass(), element.mappingKey()));
+ .setMinZoom(minzoom);
}
@Override
diff --git a/src/main/java/org/openmaptiles/layers/Transportation.java b/src/main/java/org/openmaptiles/layers/Transportation.java
index a8cc8e2..4aa898e 100644
--- a/src/main/java/org/openmaptiles/layers/Transportation.java
+++ b/src/main/java/org/openmaptiles/layers/Transportation.java
@@ -62,6 +62,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
@@ -103,7 +104,8 @@ public class Transportation implements
*/
private static final Logger LOGGER = LoggerFactory.getLogger(Transportation.class);
- private static final Pattern GREAT_BRITAIN_REF_NETWORK_PATTERN = Pattern.compile("^[AM][0-9AM()]+");
+ private static final Pattern GREAT_BRITAIN_REF_NETWORK_PATTERN = Pattern.compile("^[ABM][0-9ABM()]+");
+ private static final Pattern IRELAND_REF_NETWORK_PATTERN = Pattern.compile("^[MNRL][0-9]+");
private static final MultiExpression.Index 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,
+ RouteNetwork.CA_PROVINCIAL_ARTERIAL,
+ RouteNetwork.US_INTERSTATE
+ );
+ 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)
@@ -150,13 +163,15 @@ public class Transportation implements
.thenComparingInt(r -> r.ref().length())
.thenComparing(RouteRelation::ref);
private static final Set ONEWAY_VALUES = Set.of(-1, 1);
+ private final Map MINZOOMS;
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,16 +254,32 @@ public class Transportation implements
@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
- synchronized (this) {
- try {
- Geometry boundary = feature.polygon().buffer(GeoUtils.metersToPixelAtEquator(0, 10_000) / 256d);
- greatBritain = PreparedGeometryFactory.prepare(boundary);
- } catch (GeometryException e) {
- LOGGER.error("Failed to get Great Britain Polygon: " + e);
+ 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")) {
+ try {
+ var prepared = PreparedGeometryFactory.prepare(
+ feature.polygon().buffer(GeoUtils.metersToPixelAtEquator(0, 10_000) / 256d)
+ );
+ synchronized (this) {
+ greatBritain = prepared;
}
+ } catch (GeometryException e) {
+ LOGGER.error("Failed to get Great Britain Polygon: " + e);
+ }
+ } else if (feature.hasTag("iso_a2", "IE")) {
+ try {
+ var prepared = PreparedGeometryFactory.prepare(
+ feature.polygon().buffer(GeoUtils.metersToPixelAtEquator(0, 10_000) / 256d)
+ );
+ synchronized (this) {
+ ireland = prepared;
+ }
+ } catch (GeometryException e) {
+ LOGGER.error("Failed to get Ireland Polygon: " + e);
}
}
}
@@ -268,6 +299,26 @@ public class Transportation implements
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,12 +358,15 @@ public class Transportation implements
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";
- result.add(new RouteRelation(refMatcher.group(), network, networkType, (byte) -1,
- 0));
+ Transportation.RouteNetwork networkType = switch (element.highway()) {
+ case "motorway" -> Transportation.RouteNetwork.GB_MOTORWAY;
+ case "trunk" -> RouteNetwork.GB_TRUNK;
+ case "primary", "secondary" -> RouteNetwork.GB_PRIMARY;
+ default -> null;
+ };
+ result.add(new RouteRelation(refMatcher.group(),
+ networkType == null ? null : networkType.network,
+ networkType, (byte) -1, 0));
}
} catch (GeometryException e) {
e.log(stats, "omt_transportation_name_gb_test",
@@ -320,6 +374,31 @@ public class Transportation implements
}
}
}
+ // 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)) {
+ String highway = coalesce(element.highway(), "");
+ Transportation.RouteNetwork networkType = switch (highway) {
+ case "motorway" -> Transportation.RouteNetwork.IE_MOTORWAY;
+ case "trunk", "primary" -> RouteNetwork.IE_NATIONAL;
+ default -> RouteNetwork.IE_REGIONAL;
+ };
+ result.add(new RouteRelation(refMatcher.group(), networkType.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;
@@ -362,13 +441,11 @@ public class Transportation implements
.setAttr(Fields.CLASS, highwayClass)
.setAttr(Fields.SUBCLASS, highwaySubclass(highwayClass, element.publicTransport(), highway))
.setAttr(Fields.NETWORK, networkType != null ? networkType.name : null)
- // TODO: including brunnel at low zooms leads to some large 300-400+kb z4-7 tiles, instead
- // we should only set brunnel if the line is above a certain length
- .setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()))
+ .setAttrWithMinSize(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()), 4, 4, 12)
// z8+
.setAttrWithMinzoom(Fields.EXPRESSWAY, element.expressway() && !"motorway".equals(highway) ? 1 : null, 8)
// z9+
- .setAttrWithMinzoom(Fields.LAYER, nullIfLong(element.layer(), 0), 9)
+ .setAttrWithMinSize(Fields.LAYER, nullIfLong(element.layer(), 0), 4, 9, 12)
.setAttrWithMinzoom(Fields.BICYCLE, nullIfEmpty(element.bicycle()), 9)
.setAttrWithMinzoom(Fields.FOOT, nullIfEmpty(element.foot()), 9)
.setAttrWithMinzoom(Fields.HORSE, nullIfEmpty(element.horse()), 9)
@@ -381,7 +458,7 @@ public class Transportation implements
// 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 +492,14 @@ public class Transportation implements
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)
+ .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);
};
}
@@ -463,7 +548,6 @@ public class Transportation implements
.setAttr(Fields.CLASS, clazz)
.setAttr(Fields.SUBCLASS, railway)
.setAttr(Fields.SERVICE, service(service))
- .setAttr(Fields.ONEWAY, nullIfInt(element.isOneway(), 0))
.setAttr(Fields.RAMP, element.isRamp() ? 1L : null)
.setAttrWithMinzoom(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()), 10)
.setAttrWithMinzoom(Fields.LAYER, nullIfLong(element.layer(), 0), 9)
@@ -494,13 +578,13 @@ public class Transportation implements
.setAttr(Fields.CLASS, element.shipway()) // "ferry"
// no subclass
.setAttr(Fields.SERVICE, service(element.service()))
- .setAttr(Fields.ONEWAY, nullIfInt(element.isOneway(), 0))
.setAttr(Fields.RAMP, element.isRamp() ? 1L : null)
.setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()))
.setAttr(Fields.LAYER, nullIfLong(element.layer(), 0))
.setSortKey(element.zOrder())
.setMinPixelSize(0) // merge during post-processing, then limit by size
- .setMinZoom(11);
+ .setMinZoom(4)
+ .setMinPixelSizeBelowZoom(10, 32); // `sql_filter: ST_Length(...)` used in OpenMapTiles translates to 32px
}
@Override
@@ -548,17 +632,25 @@ public class Transportation implements
enum RouteNetwork {
- US_INTERSTATE("us-interstate"),
- US_HIGHWAY("us-highway"),
- US_STATE("us-state"),
- CA_TRANSCANADA("ca-transcanada"),
- GB_MOTORWAY("gb-motorway"),
- GB_TRUNK("gb-trunk");
+ US_INTERSTATE("us-interstate", null),
+ US_HIGHWAY("us-highway", null),
+ US_STATE("us-state", null),
+ CA_TRANSCANADA("ca-transcanada", null),
+ CA_PROVINCIAL_ARTERIAL("ca-provincial-arterial", null),
+ CA_PROVINCIAL("ca-provincial", null),
+ GB_MOTORWAY("gb-motorway", "omt-gb-motorway"),
+ GB_TRUNK("gb-trunk", "omt-gb-trunk"),
+ GB_PRIMARY("gb-primary", "omt-gb-primary"),
+ IE_MOTORWAY("ie-motorway", "omt-ie-motorway"),
+ IE_NATIONAL("ie-national", "omt-ie-national"),
+ IE_REGIONAL("ie-regional", "omt-ie-regional");
final String name;
+ final String network;
- RouteNetwork(String name) {
+ RouteNetwork(String name, String network) {
this.name = name;
+ this.network = network;
}
}
diff --git a/src/main/java/org/openmaptiles/layers/TransportationName.java b/src/main/java/org/openmaptiles/layers/TransportationName.java
index 27372a3..4e8fefd 100644
--- a/src/main/java/org/openmaptiles/layers/TransportationName.java
+++ b/src/main/java/org/openmaptiles/layers/TransportationName.java
@@ -111,8 +111,8 @@ public class TransportationName implements
.put(7, 20_000)
.put(8, 14_000)
.put(9, 8_000)
- .put(10, 8_000)
- .put(11, 8_000);
+ .put(10, 4_000)
+ .put(11, 2_000);
private final boolean brunnel;
private final boolean sizeForShield;
private final boolean limitMerge;
@@ -279,7 +279,9 @@ public class TransportationName implements
}
if (brunnel) {
- feature.setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()));
+ // from OMT: "Drop brunnel if length of way < 2% of tile width (less than 3 pixels)"
+ feature.setAttrWithMinSize(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()),
+ 3, 4, 12);
}
/*
diff --git a/src/main/java/org/openmaptiles/layers/WaterName.java b/src/main/java/org/openmaptiles/layers/WaterName.java
index 2bc19fc..cebaeff 100644
--- a/src/main/java/org/openmaptiles/layers/WaterName.java
+++ b/src/main/java/org/openmaptiles/layers/WaterName.java
@@ -35,6 +35,7 @@ See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for deta
*/
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 @@ import com.onthegomap.planetiler.stats.Stats;
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,7 @@ 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 +141,10 @@ public class WaterName implements
@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 +162,25 @@ public class WaterName implements
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) {
+ // FIXME: While this looks like matching properly stuff in https://github.com/openmaptiles/openmaptiles/pull/1457/files#diff-201daa1c61c99073fe3280d440c9feca5ed2236b251ad454caa14cc203f952d1R74 ,
+ // it includes not just https://www.openstreetmap.org/relation/13360255 but also https://www.openstreetmap.org/node/1385157299 (and some others).
+ // Hence check how that OpenMapTiles code works for "James Bay" and:
+ // a) if same as here then, fix there and then here
+ // b) if OK (while here NOK), fix only here
+ 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);
}
@@ -172,31 +189,35 @@ public class WaterName implements
@Override
public void process(Tables.OsmWaterPolygon element, FeatureCollector features) {
if (nullIfEmpty(element.name()) != null) {
- try {
- Geometry centerlineGeometry = lakeCenterlines.get(element.source().id());
- FeatureCollector.Feature feature;
- int minzoom = 9;
- if (centerlineGeometry != null) {
- // prefer lake centerline if it exists
- feature = features.geometry(LAYER_NAME, centerlineGeometry)
- .setMinPixelSizeBelowZoom(13, 6d * element.name().length());
- } 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));
- }
- feature
- .setAttr(Fields.CLASS, FieldValues.CLASS_LAKE)
- .setBufferPixels(BUFFER_SIZE)
- .putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations))
- .setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0)
- .setMinZoom(minzoom);
- } catch (GeometryException e) {
- e.log(stats, "omt_water_polygon", "Unable to get geometry for water polygon " + element.source().id());
+ 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)
+ .setMinPixelSizeBelowZoom(13, 6d * element.name().length());
+ } else {
+ // otherwise just use a label point inside the lake
+ feature = features.pointOnSurface(LAYER_NAME)
+ .setMinZoom(place != null && SEA_OR_OCEAN_PLACE.contains(place) ? 0 : 3)
+ .setMinPixelSize(128); // tiles are 256x256, so 128x128 is 1/4 of a tile
+ }
+ feature
+ .setAttr(Fields.CLASS, clazz)
+ .setBufferPixels(BUFFER_SIZE)
+ .putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations))
+ .setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0)
+ .setMinZoom(minzoom);
}
}
}
diff --git a/src/main/java/org/openmaptiles/util/Utils.java b/src/main/java/org/openmaptiles/util/Utils.java
index de9f4bf..993ca09 100644
--- a/src/main/java/org/openmaptiles/util/Utils.java
+++ b/src/main/java/org/openmaptiles/util/Utils.java
@@ -74,5 +74,4 @@ public class Utils {
public static String brunnel(boolean isBridge, boolean isTunnel, boolean isFord) {
return isBridge ? "bridge" : isTunnel ? "tunnel" : isFord ? "ford" : null;
}
-
}
diff --git a/src/test/java/org/openmaptiles/OpenMapTilesTest.java b/src/test/java/org/openmaptiles/OpenMapTilesTest.java
index 44d428d..187fccf 100644
--- a/src/test/java/org/openmaptiles/OpenMapTilesTest.java
+++ b/src/test/java/org/openmaptiles/OpenMapTilesTest.java
@@ -137,7 +137,7 @@ class OpenMapTilesTest {
assertFeatureNear(mbtiles, "housenumber", Map.of(
"housenumber", "27"
), 7.42117, 43.73652, 14, 14);
- assertNumFeatures("housenumber", Map.of(), 14, 274, Point.class);
+ assertNumFeatures("housenumber", Map.of(), 14, 231, Point.class);
}
@Test
diff --git a/src/test/java/org/openmaptiles/layers/AbstractLayerTest.java b/src/test/java/org/openmaptiles/layers/AbstractLayerTest.java
index 05e94d9..e3f649e 100644
--- a/src/test/java/org/openmaptiles/layers/AbstractLayerTest.java
+++ b/src/test/java/org/openmaptiles/layers/AbstractLayerTest.java
@@ -137,6 +137,16 @@ public abstract class AbstractLayerTest {
);
}
+ SourceFeature lineFeatureWithLength(double length, Map props) {
+ return SimpleFeature.create(
+ GeoUtils.worldToLatLonCoords(newLineString(0, 0, 0, length)),
+ new HashMap<>(props),
+ OpenMapTilesProfile.OSM_SOURCE,
+ null,
+ 0
+ );
+ }
+
SourceFeature closedWayFeature(Map props) {
return SimpleFeature.createFakeOsmFeature(
newLineString(0, 0, 1, 0, 1, 1, 0, 1, 0, 0),
diff --git a/src/test/java/org/openmaptiles/layers/HousenumberTest.java b/src/test/java/org/openmaptiles/layers/HousenumberTest.java
index 6ead5e9..2fa3129 100644
--- a/src/test/java/org/openmaptiles/layers/HousenumberTest.java
+++ b/src/test/java/org/openmaptiles/layers/HousenumberTest.java
@@ -1,8 +1,14 @@
package org.openmaptiles.layers;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.onthegomap.planetiler.geo.GeometryException;
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.CsvSource;
class HousenumberTest extends AbstractLayerTest {
@@ -27,4 +33,127 @@ class HousenumberTest extends AbstractLayerTest {
"addr:housenumber", "10"
))));
}
+
+ @ParameterizedTest
+ @CsvSource({
+ "1, 1",
+ "1;1a;2;2/b;20;3, 1–3",
+ "1;1a;2;2/b;20;3;, 1–3",
+ "1;2;20;3, 1–20",
+ "1;2;20;3;, 1–20",
+ ";, ;",
+ ";;, ;;",
+ "2712;935803935803, 2712–935803935803",
+ })
+ void testDisplayHousenumber(String outlier, String expected) {
+ assertEquals(expected, Housenumber.displayHousenumber(outlier));
+ }
+
+ @Test
+ void testTempAttrs() {
+ assertFeatures(14, List.of(Map.of(
+ "_has_name", Boolean.TRUE,
+ "_partition", "streetX765/6"
+ )), process(polygonFeature(Map.of(
+ "addr:housenumber", "765/6",
+ "addr:block_number", "X",
+ "addr:street", "street",
+ "name", "name"
+ ))));
+ }
+
+ @Test
+ void testNonduplicateHousenumber() throws GeometryException {
+ var layerName = Housenumber.LAYER_NAME;
+ var hn1 = pointFeature(
+ layerName,
+ Map.of(
+ "housenumber", "764/2",
+ "_partition", "764/2"
+ ),
+ 1
+ );
+ var hn2 = pointFeature(
+ layerName,
+ Map.of(
+ "housenumber", "765/6",
+ "_partition", "765/6"
+ ),
+ 1
+ );
+
+ Assertions.assertEquals(
+ 2,
+ profile.postProcessLayerFeatures(layerName, 14, List.of(hn1, hn2)).size()
+ );
+ }
+
+ @Test
+ void testNonduplicateStreet() throws GeometryException {
+ var layerName = Housenumber.LAYER_NAME;
+ var housenumber = "765/6";
+ var hn1 = pointFeature(
+ layerName,
+ Map.of(
+ "housenumber", housenumber,
+ "_partition", "street 1" + housenumber
+ ),
+ 1
+ );
+ var hn2 = pointFeature(
+ layerName,
+ Map.of(
+ "housenumber", housenumber,
+ "_partition", "street 2" + housenumber
+ ),
+ 1
+ );
+
+ var result = profile.postProcessLayerFeatures(layerName, 14, List.of(hn1, hn2));
+
+ Assertions.assertEquals(
+ 1, // same housenumber => two points merged into one multipoint
+ result.size()
+ );
+ Assertions.assertEquals(
+ 5, // two point in multipoint => 5 commands
+ result.getFirst().geometry().commands().length);
+ }
+
+ @Test
+ void testDuplicateHousenumber() throws GeometryException {
+ var layerName = Housenumber.LAYER_NAME;
+ var housenumber = "765/6";
+ var hn1 = pointFeature(
+ layerName,
+ Map.of(
+ "housenumber", housenumber + " (no name)",
+ "_has_name", false,
+ "_partition", housenumber
+ ),
+ 1
+ );
+ var hn2 = pointFeature(
+ layerName,
+ Map.of(
+ "housenumber", housenumber + " (with name)",
+ "_has_name", true,
+ "_partition", housenumber
+ ),
+ 1
+ );
+
+ var result = profile.postProcessLayerFeatures(layerName, 14, List.of(hn1, hn2));
+
+ Assertions.assertEquals(List.of(
+ pointFeature(
+ layerName,
+ Map.of("housenumber", "765/6 (no name)"),
+ 1
+ )
+ ), result);
+ Assertions.assertEquals(
+ 3, // only one point in multipoint => 3 commands
+ result.getFirst().geometry().commands().length);
+ }
}
diff --git a/src/test/java/org/openmaptiles/layers/ParkTest.java b/src/test/java/org/openmaptiles/layers/ParkTest.java
index a393b21..2383a13 100644
--- a/src/test/java/org/openmaptiles/layers/ParkTest.java
+++ b/src/test/java/org/openmaptiles/layers/ParkTest.java
@@ -45,6 +45,30 @@ class ParkTest extends AbstractLayerTest {
))));
}
+ @Test
+ void testAbotiginalLand() {
+ assertFeatures(13, List.of(Map.of(
+ "_layer", "park",
+ "_type", "polygon",
+ "class", "aboriginal_lands",
+ "name", "Hualapai Tribe",
+ "_minpixelsize", 2d,
+ "_minzoom", 4,
+ "_maxzoom", 14
+ ), Map.of(
+ "_layer", "park",
+ "_type", "point",
+ "class", "aboriginal_lands",
+ "name", "Hualapai Tribe",
+ "_minzoom", 5,
+ "_maxzoom", 14
+ )), process(polygonFeature(Map.of(
+ "boundary", "aboriginal_lands",
+ "name", "Hualapai Tribe",
+ "protection_title", "National Park"
+ ))));
+ }
+
@Test
void testSmallerPark() {
double z11area = Math.pow((GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d), 2) * Math.pow(2, 20 - 11);
diff --git a/src/test/java/org/openmaptiles/layers/PoiTest.java b/src/test/java/org/openmaptiles/layers/PoiTest.java
index 11a9a70..9027cf5 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,167 @@ class PoiTest extends AbstractLayerTest {
))));
}
+ 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",
+ "name:es", "test 1",
+ "uic_ref", "1"
+ )),
+ pointFeature(Map.of(
+ "railway", "tram_stop",
+ "name", "station",
+ "name:es", "test 2",
+ "uic_ref", "1"
+ ))
+ ));
+ assertFeatures(14, List.of(
+ Map.of(
+ "_layer", "poi",
+ "name:es", "test 1",
+ "class", "railway",
+ "subclass", "tram_stop",
+ "agg_stop", 1,
+ "_minzoom", 14
+ ),
+ Map.of(
+ "_layer", "poi",
+ "name:es", "test 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",
+ "name:es", "test 1",
+ "uic_ref", "1"
+ )),
+ pointFeature(Map.of(
+ "highway", "bus_stop",
+ "name", "station",
+ "name:es", "test 2",
+ "uic_ref", "1"
+ )),
+ pointFeature(Map.of(
+ "railway", "tram_stop",
+ "name", "station",
+ "name:es", "test 3",
+ "uic_ref", "1"
+ ))
+ ));
+ assertFeatures(14, List.of(
+ Map.of(
+ "_layer", "poi",
+ "name:es", "test 1",
+ "class", "bus",
+ "subclass", "bus_stop",
+ "agg_stop", "",
+ "_minzoom", 14
+ ),
+ Map.of(
+ "_layer", "poi",
+ "name:es", "test 2",
+ "class", "bus",
+ "subclass", "bus_stop",
+ "agg_stop", "",
+ "_minzoom", 14
+ ),
+ Map.of(
+ "_layer", "poi",
+ "name:es", "test 3",
+ "class", "railway",
+ "subclass", "tram_stop",
+ "agg_stop", 1,
+ "_minzoom", 14
+ )
+ ), result);
+ }
+
+ @Test
+ void testAggStopThreeWithSameSubclass() {
+ var result = testAggStops(List.of(
+ SimpleFeature.create(newPoint(0, 0), Map.of(
+ "highway", "bus_stop",
+ "name", "station",
+ "name:es", "test 1",
+ "uic_ref", "1"
+ ), OpenMapTilesProfile.OSM_SOURCE, null, 0),
+ SimpleFeature.create(newPoint(1, 0), Map.of(
+ "highway", "bus_stop",
+ "name", "station",
+ "name:es", "test 2",
+ "uic_ref", "1"
+ ), OpenMapTilesProfile.OSM_SOURCE, null, 1),
+ SimpleFeature.create(newPoint(2, 0), Map.of(
+ "highway", "bus_stop",
+ "name", "station",
+ "name:es", "test 3",
+ "uic_ref", "1"
+ ), OpenMapTilesProfile.OSM_SOURCE, null, 2)
+ ));
+ assertFeatures(14, List.of(
+ Map.of(
+ "_layer", "poi",
+ "name:es", "test 1",
+ "class", "bus",
+ "subclass", "bus_stop",
+ "agg_stop", "",
+ "_minzoom", 14
+ ),
+ Map.of(
+ "_layer", "poi",
+ "name:es", "test 2",
+ "class", "bus",
+ "subclass", "bus_stop",
+ "agg_stop", 1,
+ "_minzoom", 14
+ ),
+ Map.of(
+ "_layer", "poi",
+ "name:es", "test 3",
+ "class", "bus",
+ "subclass", "bus_stop",
+ "agg_stop", "",
+ "_minzoom", 14
+ )
+ ), result);
+ }
+
@ParameterizedTest
@ValueSource(booleans = {false, true})
void testPlaceOfWorshipFromReligionTag(boolean area) {
@@ -185,7 +352,7 @@ class PoiTest extends AbstractLayerTest {
void testEmbassy() {
assertFeatures(7, List.of(Map.of(
"_layer", "poi",
- "class", "diplomatic",
+ "class", "office",
"subclass", "diplomatic",
"name", "The Embassy"
)), process(pointFeature(Map.of(
@@ -274,4 +441,28 @@ class PoiTest extends AbstractLayerTest {
"ref", "Corner Case"
))));
}
+
+ @Test
+ void testChargingStation() {
+ List