mirror of
https://github.com/cfpwastaken/planetiler-openmaptiles.git
synced 2026-02-04 20:41:09 +00:00
Upgrade planetiler-basemap to be compatible with OpenMapTiles 3.13 (#49)
Applying changes to layers from [OpenMapTiles 3.13 release](https://github.com/openmaptiles/openmaptiles/releases/tag/v3.13) (https://github.com/openmaptiles/openmaptiles/compare/v3.12.2...v3.13), minus transportation network connectivity improvements - those will be a separate change.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
@@ -36,6 +36,7 @@ See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for deta
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullOrEmpty;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
@@ -67,13 +68,15 @@ public class AerodromeLabel implements
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmAerodromeLabelPoint element, FeatureCollector features) {
|
||||
String clazz = classLookup.getOrElse(element.source(), FieldValues.CLASS_OTHER);
|
||||
boolean important = !nullOrEmpty(element.iata()) && FieldValues.CLASS_INTERNATIONAL.equals(clazz);
|
||||
features.centroid(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setMinZoom(10)
|
||||
.setMinZoom(important ? 8 : 10)
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.putAttrs(Utils.elevationTags(element.ele()))
|
||||
.setAttr(Fields.IATA, nullIfEmpty(element.iata()))
|
||||
.setAttr(Fields.ICAO, nullIfEmpty(element.icao()))
|
||||
.setAttr(Fields.CLASS, classLookup.getOrElse(element.source(), FieldValues.CLASS_OTHER));
|
||||
.setAttr(Fields.CLASS, clazz);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
@@ -168,7 +168,9 @@ public class Boundary implements
|
||||
: 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;
|
||||
yield minZoom != null && minZoom <= 7 ? new BoundaryInfo(4, 1, 4) :
|
||||
minZoom != null && minZoom <= 7.7 ? new BoundaryInfo(4, 4, 4) :
|
||||
null;
|
||||
}
|
||||
default -> null;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
@@ -152,14 +152,14 @@ public class Landcover implements
|
||||
long numPoints = num.longValue();
|
||||
if (zoom >= 10) {
|
||||
if (WOOD_OR_FOREST.contains(subclass) && numPoints < 300) {
|
||||
attrs.put(tempGroupKey, numPoints < 50 ? "<50" : "<300");
|
||||
attrs.put(tempGroupKey, "<300");
|
||||
toMerge.add(item);
|
||||
} else { // don't merge
|
||||
result.add(item);
|
||||
}
|
||||
} else if (zoom == 9) {
|
||||
if (WOOD_OR_FOREST.contains(subclass)) {
|
||||
attrs.put(tempGroupKey, numPoints < 50 ? "<50" : numPoints < 300 ? "<300" : ">300");
|
||||
attrs.put(tempGroupKey, numPoints < 300 ? "<300" : ">300");
|
||||
toMerge.add(item);
|
||||
} else { // don't merge
|
||||
result.add(item);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
@@ -101,6 +101,9 @@ public class Landuse implements
|
||||
nullIfEmpty(element.waterway())
|
||||
);
|
||||
if (clazz != null) {
|
||||
if ("grave_yard".equals(clazz)) {
|
||||
clazz = FieldValues.CLASS_CEMETERY;
|
||||
}
|
||||
features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
.setMinPixelSizeOverrides(MIN_PIXEL_SIZE_THRESHOLDS)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
@@ -48,12 +48,18 @@ import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.prep.PreparedGeometry;
|
||||
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for mountain peak label points in the {@code mountain_peak} layer from
|
||||
@@ -63,8 +69,10 @@ import org.locationtech.jts.geom.Point;
|
||||
* mountain_peak sql files</a>.
|
||||
*/
|
||||
public class MountainPeak implements
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
OpenMapTilesSchema.MountainPeak,
|
||||
Tables.OsmPeakPoint.Handler,
|
||||
Tables.OsmMountainLinestring.Handler,
|
||||
BasemapProfile.FeaturePostProcessor {
|
||||
|
||||
/*
|
||||
@@ -73,20 +81,40 @@ public class MountainPeak implements
|
||||
* label density by only taking the top 5 most important mountain peaks within each 100x100px
|
||||
* square.
|
||||
*/
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(TransportationName.class);
|
||||
|
||||
private final Translations translations;
|
||||
private final Stats stats;
|
||||
// keep track of areas that prefer feet to meters to set customary_ft=1 (just U.S.)
|
||||
private PreparedGeometry unitedStates = null;
|
||||
private final AtomicBoolean loggedNoUS = new AtomicBoolean(false);
|
||||
|
||||
public MountainPeak(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.translations = translations;
|
||||
this.stats = stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) {
|
||||
if ("ne_10m_admin_0_countries".equals(table) && feature.hasTag("iso_a2", "US")) {
|
||||
// multiple threads call this method concurrently, US polygon *should* only be found
|
||||
// once, but just to be safe synchronize updates to that field
|
||||
synchronized (this) {
|
||||
try {
|
||||
Geometry boundary = feature.polygon();
|
||||
unitedStates = PreparedGeometryFactory.prepare(boundary);
|
||||
} catch (GeometryException e) {
|
||||
LOGGER.error("Failed to get United States Polygon for mountain_peak layer: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmPeakPoint element, FeatureCollector features) {
|
||||
Integer meters = Parse.parseIntSubstring(element.ele());
|
||||
if (meters != null && Math.abs(meters) < 10_000) {
|
||||
features.point(LAYER_NAME)
|
||||
var feature = features.point(LAYER_NAME)
|
||||
.setAttr(Fields.CLASS, element.source().getTag("natural"))
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.putAttrs(elevationTags(meters))
|
||||
@@ -101,9 +129,46 @@ public class MountainPeak implements
|
||||
// in adjacent tiles. postProcess() will remove anything outside the desired buffer.
|
||||
.setBufferPixels(100)
|
||||
.setPointLabelGridSizeAndLimit(13, 100, 5);
|
||||
|
||||
if (peakInAreaUsingFeet(element)) {
|
||||
feature.setAttr(Fields.CUSTOMARY_FT, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmMountainLinestring element, FeatureCollector features) {
|
||||
// TODO rank is approximate to sort important/named ridges before others, should switch to labelgrid for linestrings later
|
||||
int rank = 3 -
|
||||
(nullIfEmpty(element.wikipedia()) != null ? 1 : 0) -
|
||||
(nullIfEmpty(element.name()) != null ? 1 : 0);
|
||||
features.line(LAYER_NAME)
|
||||
.setAttr(Fields.CLASS, element.source().getTag("natural"))
|
||||
.setAttr(Fields.RANK, rank)
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setSortKey(rank)
|
||||
.setMinZoom(13)
|
||||
.setBufferPixels(100);
|
||||
}
|
||||
|
||||
/** Returns true if {@code element} is a point in an area where feet are used insead of meters (the US). */
|
||||
private boolean peakInAreaUsingFeet(Tables.OsmPeakPoint element) {
|
||||
if (unitedStates == null) {
|
||||
if (!loggedNoUS.get() && loggedNoUS.compareAndSet(false, true)) {
|
||||
LOGGER.warn("No US polygon for inferring mountain_peak customary_ft tag");
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Geometry wayGeometry = element.source().worldGeometry();
|
||||
return unitedStates.intersects(wayGeometry);
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_mountain_peak_us_test",
|
||||
"Unable to test mountain_peak against US polygon: " + element.source().id());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
LongIntMap groupCounts = new LongIntHashMap();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
@@ -42,6 +42,7 @@ import static com.onthegomap.planetiler.collection.FeatureGroup.SORT_KEY_BITS;
|
||||
import com.carrotsearch.hppc.LongIntHashMap;
|
||||
import com.carrotsearch.hppc.LongIntMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
@@ -101,10 +102,10 @@ public class Park implements
|
||||
);
|
||||
|
||||
// park shape
|
||||
features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
var outline = features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttrWithMinzoom(Fields.CLASS, clazz, 5)
|
||||
.setMinPixelSize(2)
|
||||
.setMinZoom(6);
|
||||
.setMinZoom(4);
|
||||
|
||||
// park name label point (if it has one)
|
||||
if (element.name() != null) {
|
||||
@@ -112,8 +113,13 @@ public class Park implements
|
||||
double area = element.source().area();
|
||||
int minzoom = getMinZoomForArea(area);
|
||||
|
||||
features.centroid(LAYER_NAME).setBufferPixels(256)
|
||||
var names = LanguageUtils.getNamesWithoutTranslations(element.source().tags());
|
||||
|
||||
outline.putAttrsWithMinzoom(names, 5);
|
||||
|
||||
features.pointOnSurface(LAYER_NAME).setBufferPixels(256)
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
.putAttrs(names)
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setPointLabelGridPixelSize(14, 100)
|
||||
.setSortKey(SortKey
|
||||
@@ -132,12 +138,12 @@ public class Park implements
|
||||
// sql filter: area > 70000*2^(20-zoom_level)
|
||||
// simplifies to: zoom_level > 20 - log(area / 70000) / log(2)
|
||||
int minzoom = (int) Math.floor(20 - Math.log(area / WORLD_AREA_FOR_70K_SQUARE_METERS) / LOG2);
|
||||
minzoom = Math.min(14, Math.max(6, minzoom));
|
||||
minzoom = Math.min(14, Math.max(5, minzoom));
|
||||
return minzoom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) throws GeometryException {
|
||||
// infer the "rank" attribute from point ordering within each label grid square
|
||||
LongIntMap counts = new LongIntHashMap();
|
||||
for (VectorTile.Feature feature : items) {
|
||||
@@ -147,6 +153,9 @@ public class Park implements
|
||||
counts.put(feature.group(), count);
|
||||
}
|
||||
}
|
||||
if (zoom <= 4) {
|
||||
items = FeatureMerge.mergeOverlappingPolygons(items, 0);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
@@ -171,7 +171,7 @@ public class Place implements
|
||||
case "ne_10m_admin_1_states_provinces" -> {
|
||||
Double scalerank = Parse.parseDoubleOrNull(feature.getTag("scalerank"));
|
||||
Double labelrank = Parse.parseDoubleOrNull(feature.getTag("labelrank"));
|
||||
if (scalerank != null && scalerank <= 3 && labelrank != null && labelrank <= 2) {
|
||||
if (scalerank != null && scalerank <= 6 && labelrank != null && labelrank <= 7) {
|
||||
states.put(feature.worldGeometry(), new NaturalEarthRegion(
|
||||
feature.getString("name"), 6,
|
||||
scalerank,
|
||||
@@ -265,7 +265,7 @@ public class Place implements
|
||||
|
||||
features.point(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(names)
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_STATE)
|
||||
.setAttr(Fields.CLASS, element.place())
|
||||
.setAttr(Fields.RANK, rank)
|
||||
.setMinZoom(2)
|
||||
.setSortKey(rank);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
@@ -36,8 +36,8 @@ See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for deta
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIf;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfLong;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullOrEmpty;
|
||||
import static java.util.Map.entry;
|
||||
|
||||
@@ -170,7 +170,7 @@ public class Poi implements
|
||||
output.setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, poiClass)
|
||||
.setAttr(Fields.SUBCLASS, subclass)
|
||||
.setAttr(Fields.LAYER, nullIf(element.layer(), 0))
|
||||
.setAttr(Fields.LAYER, nullIfLong(element.layer(), 0))
|
||||
.setAttr(Fields.LEVEL, Parse.parseLongOrNull(element.source().getTag("level")))
|
||||
.setAttr(Fields.INDOOR, element.indoor() ? 1 : null)
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
@@ -36,6 +36,10 @@ See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for deta
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.*;
|
||||
import static com.onthegomap.planetiler.util.MemoryEstimator.CLASS_HEADER_BYTES;
|
||||
import static com.onthegomap.planetiler.util.MemoryEstimator.POINTER_BYTES;
|
||||
import static com.onthegomap.planetiler.util.MemoryEstimator.estimateSize;
|
||||
import static java.util.Map.entry;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
@@ -45,15 +49,32 @@ import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmElement;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmReader;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.MemoryEstimator;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import com.onthegomap.planetiler.util.ZoomFunction;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
import org.locationtech.jts.geom.prep.PreparedGeometry;
|
||||
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for roads, shipways, railroads, and paths in the {@code transportation}
|
||||
@@ -69,7 +90,9 @@ public class Transportation implements
|
||||
Tables.OsmRailwayLinestring.Handler,
|
||||
Tables.OsmShipwayLinestring.Handler,
|
||||
Tables.OsmHighwayPolygon.Handler,
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
BasemapProfile.FeaturePostProcessor,
|
||||
BasemapProfile.OsmRelationPreprocessor,
|
||||
BasemapProfile.IgnoreWikidata {
|
||||
|
||||
/*
|
||||
@@ -78,6 +101,8 @@ public class Transportation implements
|
||||
* layer includes names, but less detailed attributes.
|
||||
*/
|
||||
|
||||
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 MultiExpression.Index<String> classMapping = FieldMappings.Class.index();
|
||||
private static final Set<String> RAILWAY_RAIL_VALUES = Set.of(
|
||||
FieldValues.SUBCLASS_RAIL,
|
||||
@@ -108,11 +133,24 @@ public class Transportation implements
|
||||
"paved", "asphalt", "cobblestone", "concrete", "concrete:lanes", "concrete:plates", "metal",
|
||||
"paving_stones", "sett", "unhewn_cobblestone", "wood"
|
||||
);
|
||||
private static final Set<String> ACCESS_NO_VALUES = Set.of(
|
||||
"private", "no"
|
||||
);
|
||||
private static final ZoomFunction.MeterToPixelThresholds MIN_LENGTH = ZoomFunction.meterThresholds()
|
||||
.put(7, 50)
|
||||
.put(6, 100)
|
||||
.put(5, 500)
|
||||
.put(4, 1_000);
|
||||
// ORDER BY network_type, network, LENGTH(ref), ref)
|
||||
private static final Comparator<RouteRelation> RELATION_ORDERING = Comparator
|
||||
.<RouteRelation>comparingInt(
|
||||
r -> r.networkType() != null ? r.networkType.ordinal() : Integer.MAX_VALUE)
|
||||
.thenComparing(routeRelation -> coalesce(routeRelation.network(), ""))
|
||||
.thenComparingInt(r -> r.ref().length())
|
||||
.thenComparing(RouteRelation::ref);
|
||||
private final AtomicBoolean loggedNoGb = new AtomicBoolean(false);
|
||||
private final boolean z13Paths;
|
||||
private PreparedGeometry greatBritain = null;
|
||||
private final Map<String, Integer> MINZOOMS;
|
||||
private final Stats stats;
|
||||
private final PlanetilerConfig config;
|
||||
@@ -120,30 +158,54 @@ public class Transportation implements
|
||||
public Transportation(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.config = config;
|
||||
this.stats = stats;
|
||||
boolean z13Paths = config.arguments().getBoolean(
|
||||
z13Paths = config.arguments().getBoolean(
|
||||
"transportation_z13_paths",
|
||||
"transportation(_name) layer: show paths on z13",
|
||||
true
|
||||
"transportation(_name) layer: show all paths on z13",
|
||||
false
|
||||
);
|
||||
MINZOOMS = Map.of(
|
||||
FieldValues.CLASS_TRACK, 14,
|
||||
FieldValues.CLASS_PATH, z13Paths ? 13 : 14,
|
||||
FieldValues.CLASS_MINOR, 13,
|
||||
FieldValues.CLASS_RACEWAY, 12,
|
||||
FieldValues.CLASS_TERTIARY, 11,
|
||||
FieldValues.CLASS_SECONDARY, 9,
|
||||
FieldValues.CLASS_PRIMARY, 7,
|
||||
FieldValues.CLASS_TRUNK, 5,
|
||||
FieldValues.CLASS_MOTORWAY, 4
|
||||
MINZOOMS = Map.ofEntries(
|
||||
entry(FieldValues.CLASS_PATH, z13Paths ? 13 : 14),
|
||||
entry(FieldValues.CLASS_TRACK, 14),
|
||||
entry(FieldValues.CLASS_SERVICE, 13),
|
||||
entry(FieldValues.CLASS_MINOR, 13),
|
||||
entry(FieldValues.CLASS_RACEWAY, 12),
|
||||
entry(FieldValues.CLASS_TERTIARY, 11),
|
||||
entry(FieldValues.CLASS_BUSWAY, 11),
|
||||
entry(FieldValues.CLASS_SECONDARY, 9),
|
||||
entry(FieldValues.CLASS_PRIMARY, 7),
|
||||
entry(FieldValues.CLASS_TRUNK, 5),
|
||||
entry(FieldValues.CLASS_MOTORWAY, 4)
|
||||
);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a value for {@code surface} tag constrained to a small set of known values from raw OSM data. */
|
||||
private static String surface(String value) {
|
||||
return value == null ? null : SURFACE_PAVED_VALUES.contains(value) ? FieldValues.SURFACE_PAVED :
|
||||
SURFACE_UNPAVED_VALUES.contains(value) ? FieldValues.SURFACE_UNPAVED : null;
|
||||
}
|
||||
|
||||
/** Returns a value for {@code access} tag constrained to a small set of known values from raw OSM data. */
|
||||
private static String access(String value) {
|
||||
return value == null ? null : ACCESS_NO_VALUES.contains(value) ? "no" : null;
|
||||
}
|
||||
|
||||
/** Returns a value for {@code service} tag constrained to a small set of known values from raw OSM data. */
|
||||
private static String service(String value) {
|
||||
return (value == null || !SERVICE_VALUES.contains(value)) ? null : value;
|
||||
@@ -171,68 +233,160 @@ public class Transportation implements
|
||||
return "footway".equals(highway) || "steps".equals(highway);
|
||||
}
|
||||
|
||||
static boolean isLink(String highway) {
|
||||
return highway != null && highway.endsWith("_link");
|
||||
}
|
||||
|
||||
private static boolean isResidentialOrUnclassified(String highway) {
|
||||
return "residential".equals(highway) || "unclassified".equals(highway);
|
||||
}
|
||||
|
||||
private static boolean isDrivewayOrParkingAisle(String service) {
|
||||
return FieldValues.SERVICE_PARKING_AISLE.equals(service) || FieldValues.SERVICE_DRIVEWAY.equals(service);
|
||||
}
|
||||
|
||||
private static boolean isBridgeOrPier(String manMade) {
|
||||
return "bridge".equals(manMade) || "pier".equals(manMade);
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
final String name;
|
||||
|
||||
RouteNetwork(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OsmRelationInfo> preprocessOsmRelation(OsmElement.Relation relation) {
|
||||
if (relation.hasTag("route", "road", "hiking")) {
|
||||
RouteNetwork networkType = null;
|
||||
String network = relation.getString("network");
|
||||
String ref = relation.getString("ref");
|
||||
|
||||
if ("US:I".equals(network)) {
|
||||
networkType = RouteNetwork.US_INTERSTATE;
|
||||
} else if ("US:US".equals(network)) {
|
||||
networkType = RouteNetwork.US_HIGHWAY;
|
||||
} else if (network != null && network.length() == 5 && network.startsWith("US:")) {
|
||||
networkType = RouteNetwork.US_STATE;
|
||||
} else if (network != null && network.startsWith("CA:transcanada")) {
|
||||
networkType = RouteNetwork.CA_TRANSCANADA;
|
||||
}
|
||||
|
||||
int rank = switch (coalesce(network, "")) {
|
||||
case "iwn", "nwn", "rwn" -> 1;
|
||||
case "lwn" -> 2;
|
||||
default -> (relation.hasTag("osmc:symbol") || relation.hasTag("colour")) ? 2 : 3;
|
||||
};
|
||||
|
||||
if (networkType != null || rank < 3) {
|
||||
return List.of(new RouteRelation(coalesce(ref, ""), network, networkType, (byte) rank, relation.id()));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<RouteRelation> getRouteRelations(Tables.OsmHighwayLinestring element) {
|
||||
String ref = element.ref();
|
||||
List<OsmReader.RelationMember<RouteRelation>> relations = element.source().relationInfo(RouteRelation.class);
|
||||
List<RouteRelation> result = new ArrayList<>(relations.size() + 1);
|
||||
for (var relationMember : relations) {
|
||||
var relation = relationMember.relation();
|
||||
// avoid duplicates - list should be very small and usually only one
|
||||
if (!result.contains(relation)) {
|
||||
result.add(relation);
|
||||
}
|
||||
}
|
||||
if (ref != null) {
|
||||
// GB doesn't use regular relations like everywhere else, so if we are
|
||||
// in GB then use a naming convention instead.
|
||||
Matcher refMatcher = GREAT_BRITAIN_REF_NETWORK_PATTERN.matcher(ref);
|
||||
if (refMatcher.find()) {
|
||||
if (greatBritain == null) {
|
||||
if (!loggedNoGb.get() && loggedNoGb.compareAndSet(false, true)) {
|
||||
LOGGER.warn("No GB polygon for inferring route network types");
|
||||
}
|
||||
} else {
|
||||
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));
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_transportation_name_gb_test",
|
||||
"Unable to test highway against GB route network: " + element.source().id());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Collections.sort(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RouteRelation getRouteRelation(Tables.OsmHighwayLinestring element) {
|
||||
List<RouteRelation> all = getRouteRelations(element);
|
||||
return all.isEmpty() ? null : all.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmHighwayLinestring element, FeatureCollector features) {
|
||||
if (element.isArea()) {
|
||||
return;
|
||||
}
|
||||
|
||||
RouteRelation routeRelation = getRouteRelation(element);
|
||||
RouteNetwork networkType = routeRelation != null ? routeRelation.networkType : null;
|
||||
|
||||
String highway = element.highway();
|
||||
String highwayClass = highwayClass(element.highway(), element.publicTransport(), element.construction(),
|
||||
element.manMade());
|
||||
String service = service(element.service());
|
||||
if (highwayClass != null) {
|
||||
int minzoom;
|
||||
if ("pier".equals(element.manMade())) {
|
||||
try {
|
||||
if (element.source().worldGeometry() instanceof LineString lineString && lineString.isClosed()) {
|
||||
// ignore this because it's a polygon
|
||||
return;
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_transportation_pier",
|
||||
"Unable to decode pier geometry for " + element.source().id());
|
||||
return;
|
||||
}
|
||||
minzoom = 13;
|
||||
} else if (isResidentialOrUnclassified(highway)) {
|
||||
minzoom = 12;
|
||||
} else {
|
||||
String baseClass = highwayClass.replace("_construction", "");
|
||||
minzoom = MINZOOMS.getOrDefault(baseClass, 12);
|
||||
if (isPierPolygon(element)) {
|
||||
return;
|
||||
}
|
||||
boolean highwayIsLink = coalesce(highway, "").endsWith("_link");
|
||||
int minzoom = getMinzoom(element, highwayClass);
|
||||
|
||||
if (highwayIsLink) {
|
||||
minzoom = Math.max(minzoom, 9);
|
||||
}
|
||||
|
||||
boolean highwayRamp = highwayIsLink || "steps".equals(highway);
|
||||
int rampAboveZ12 = (highwayRamp || element.isRamp()) ? 1 : 0;
|
||||
int rampBelowZ12 = highwayRamp ? 1 : 0;
|
||||
boolean highwayRamp = isLink(highway);
|
||||
Integer rampAboveZ12 = (highwayRamp || element.isRamp()) ? 1 : null;
|
||||
Integer rampBelowZ12 = highwayRamp ? 1 : null;
|
||||
|
||||
FeatureCollector.Feature feature = features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
// main attributes at all zoom levels (used for grouping <= z8)
|
||||
.setAttr(Fields.CLASS, highwayClass)
|
||||
.setAttr(Fields.SUBCLASS, highwaySubclass(highwayClass, element.publicTransport(), highway))
|
||||
.setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()))
|
||||
// rest at z9+
|
||||
.setAttrWithMinzoom(Fields.SERVICE, service(element.service()), 12)
|
||||
.setAttrWithMinzoom(Fields.ONEWAY, element.isOneway(), 12)
|
||||
.setAttr(Fields.RAMP, minzoom >= 12 ? rampAboveZ12 :
|
||||
((ZoomFunction<Integer>) z -> z < 9 ? null : z >= 12 ? rampAboveZ12 : rampBelowZ12))
|
||||
.setAttrWithMinzoom(Fields.LAYER, nullIf(element.layer(), 0), 9)
|
||||
.setAttr(Fields.NETWORK, networkType != null ? networkType.name : null)
|
||||
// z8+
|
||||
.setAttrWithMinzoom(Fields.EXPRESSWAY, element.expressway() && !"motorway".equals(highway) ? 1 : null, 8)
|
||||
// z9+
|
||||
.setAttrWithMinzoom(Fields.LAYER, nullIfLong(element.layer(), 0), 9)
|
||||
.setAttrWithMinzoom(Fields.BICYCLE, nullIfEmpty(element.bicycle()), 9)
|
||||
.setAttrWithMinzoom(Fields.FOOT, nullIfEmpty(element.foot()), 9)
|
||||
.setAttrWithMinzoom(Fields.HORSE, nullIfEmpty(element.horse()), 9)
|
||||
.setAttrWithMinzoom(Fields.MTB_SCALE, nullIfEmpty(element.mtbScale()), 9)
|
||||
.setAttrWithMinzoom(Fields.ACCESS, access(element.access()), 9)
|
||||
.setAttrWithMinzoom(Fields.TOLL, element.toll() ? 1 : null, 9)
|
||||
// sometimes z9+, sometimes z12+
|
||||
.setAttr(Fields.RAMP, minzoom >= 12 ? rampAboveZ12 :
|
||||
((ZoomFunction<Integer>) z -> z < 9 ? null : z >= 12 ? rampAboveZ12 : rampBelowZ12))
|
||||
// z12+
|
||||
.setAttrWithMinzoom(Fields.SERVICE, service, 12)
|
||||
.setAttrWithMinzoom(Fields.ONEWAY, nullIfInt(element.isOneway(), 0), 12)
|
||||
.setAttrWithMinzoom(Fields.SURFACE, surface(element.surface()), 12)
|
||||
.setMinPixelSize(0) // merge during post-processing, then limit by size
|
||||
.setSortKey(element.zOrder())
|
||||
@@ -246,6 +400,53 @@ public class Transportation implements
|
||||
}
|
||||
}
|
||||
|
||||
int getMinzoom(Tables.OsmHighwayLinestring element, String highwayClass) {
|
||||
List<RouteRelation> routeRelations = getRouteRelations(element);
|
||||
int routeRank = 3;
|
||||
for (var rel : routeRelations) {
|
||||
if (rel.intRank() < routeRank) {
|
||||
routeRank = rel.intRank();
|
||||
}
|
||||
}
|
||||
String highway = element.highway();
|
||||
|
||||
int minzoom;
|
||||
if ("pier".equals(element.manMade())) {
|
||||
minzoom = 13;
|
||||
} else if (isResidentialOrUnclassified(highway)) {
|
||||
minzoom = 12;
|
||||
} else {
|
||||
String baseClass = highwayClass.replace("_construction", "");
|
||||
minzoom = switch (baseClass) {
|
||||
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;
|
||||
default -> MINZOOMS.get(baseClass);
|
||||
};
|
||||
}
|
||||
|
||||
if (isLink(highway)) {
|
||||
minzoom = Math.max(minzoom, 9);
|
||||
}
|
||||
return minzoom;
|
||||
}
|
||||
|
||||
private boolean isPierPolygon(Tables.OsmHighwayLinestring element) {
|
||||
if ("pier".equals(element.manMade())) {
|
||||
try {
|
||||
if (element.source().worldGeometry() instanceof LineString lineString && lineString.isClosed()) {
|
||||
// ignore this because it's a polygon
|
||||
return true;
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_transportation_pier",
|
||||
"Unable to decode pier geometry for " + element.source().id());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmRailwayLinestring element, FeatureCollector features) {
|
||||
String railway = element.railway();
|
||||
@@ -268,10 +469,10 @@ public class Transportation implements
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
.setAttr(Fields.SUBCLASS, railway)
|
||||
.setAttr(Fields.SERVICE, service(service))
|
||||
.setAttr(Fields.ONEWAY, element.isOneway())
|
||||
.setAttr(Fields.RAMP, element.isRamp() ? 1 : 0)
|
||||
.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, nullIf(element.layer(), 0), 9)
|
||||
.setAttrWithMinzoom(Fields.LAYER, nullIfLong(element.layer(), 0), 9)
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinPixelSize(0) // merge during post-processing, then limit by size
|
||||
.setMinZoom(minzoom);
|
||||
@@ -284,10 +485,10 @@ public class Transportation implements
|
||||
.setAttr(Fields.CLASS, "aerialway")
|
||||
.setAttr(Fields.SUBCLASS, element.aerialway())
|
||||
.setAttr(Fields.SERVICE, service(element.service()))
|
||||
.setAttr(Fields.ONEWAY, element.isOneway())
|
||||
.setAttr(Fields.RAMP, element.isRamp() ? 1 : 0)
|
||||
.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, nullIf(element.layer(), 0))
|
||||
.setAttr(Fields.LAYER, nullIfLong(element.layer(), 0))
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinPixelSize(0) // merge during post-processing, then limit by size
|
||||
.setMinZoom(12);
|
||||
@@ -299,10 +500,10 @@ public class Transportation implements
|
||||
.setAttr(Fields.CLASS, element.shipway()) // "ferry"
|
||||
// no subclass
|
||||
.setAttr(Fields.SERVICE, service(element.service()))
|
||||
.setAttr(Fields.ONEWAY, element.isOneway())
|
||||
.setAttr(Fields.RAMP, element.isRamp() ? 1 : 0)
|
||||
.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, nullIf(element.layer(), 0))
|
||||
.setAttr(Fields.LAYER, nullIfLong(element.layer(), 0))
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinPixelSize(0) // merge during post-processing, then limit by size
|
||||
.setMinZoom(11);
|
||||
@@ -320,7 +521,7 @@ public class Transportation implements
|
||||
.setAttr(Fields.CLASS, highwayClass)
|
||||
.setAttr(Fields.SUBCLASS, highwaySubclass(highwayClass, element.publicTransport(), element.highway()))
|
||||
.setAttr(Fields.BRUNNEL, brunnel("bridge".equals(manMade), false, false))
|
||||
.setAttr(Fields.LAYER, nullIf(element.layer(), 0))
|
||||
.setAttr(Fields.LAYER, nullIfLong(element.layer(), 0))
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinZoom(13);
|
||||
}
|
||||
@@ -330,7 +531,37 @@ public class Transportation implements
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
double tolerance = config.tolerance(zoom);
|
||||
double minLength = coalesce(MIN_LENGTH.apply(zoom), config.minFeatureSize(zoom)).doubleValue();
|
||||
double minLength = coalesce(MIN_LENGTH.apply(zoom), 0).doubleValue();
|
||||
// TODO preserve direction for one-way?
|
||||
return FeatureMerge.mergeLineStrings(items, minLength, tolerance, BUFFER_SIZE);
|
||||
}
|
||||
|
||||
/** Information extracted from route relations to use when processing ways in that relation. */
|
||||
record RouteRelation(
|
||||
String ref,
|
||||
String network,
|
||||
RouteNetwork networkType,
|
||||
byte rank,
|
||||
@Override long id
|
||||
) implements OsmRelationInfo, Comparable<RouteRelation> {
|
||||
|
||||
@Override
|
||||
public long estimateMemoryUsageBytes() {
|
||||
return CLASS_HEADER_BYTES +
|
||||
MemoryEstimator.estimateSize(rank) +
|
||||
POINTER_BYTES + estimateSize(ref) +
|
||||
POINTER_BYTES + estimateSize(network) +
|
||||
POINTER_BYTES + // networkType
|
||||
MemoryEstimator.estimateSizeLong(id);
|
||||
}
|
||||
|
||||
public int intRank() {
|
||||
return rank;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(RouteRelation o) {
|
||||
return RELATION_ORDERING.compare(this, o);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
@@ -38,45 +38,30 @@ package com.onthegomap.planetiler.basemap.layers;
|
||||
import static com.onthegomap.planetiler.basemap.layers.Transportation.highwayClass;
|
||||
import static com.onthegomap.planetiler.basemap.layers.Transportation.highwaySubclass;
|
||||
import static com.onthegomap.planetiler.basemap.layers.Transportation.isFootwayOrSteps;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.brunnel;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIf;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
import static com.onthegomap.planetiler.util.MemoryEstimator.CLASS_HEADER_BYTES;
|
||||
import static com.onthegomap.planetiler.util.MemoryEstimator.POINTER_BYTES;
|
||||
import static com.onthegomap.planetiler.util.MemoryEstimator.estimateSize;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.*;
|
||||
|
||||
import com.carrotsearch.hppc.LongArrayList;
|
||||
import com.carrotsearch.hppc.LongByteHashMap;
|
||||
import com.carrotsearch.hppc.LongByteMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.ForwardingProfile;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmElement;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmReader;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.MemoryEstimator;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import com.onthegomap.planetiler.util.ZoomFunction;
|
||||
import java.util.Comparator;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.prep.PreparedGeometry;
|
||||
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for road, shipway, rail, and path names in the {@code
|
||||
@@ -87,14 +72,17 @@ import org.slf4j.LoggerFactory;
|
||||
*/
|
||||
public class TransportationName implements
|
||||
OpenMapTilesSchema.TransportationName,
|
||||
Tables.OsmHighwayPoint.Handler,
|
||||
Tables.OsmHighwayLinestring.Handler,
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
Tables.OsmAerialwayLinestring.Handler,
|
||||
Tables.OsmShipwayLinestring.Handler,
|
||||
BasemapProfile.FeaturePostProcessor,
|
||||
BasemapProfile.OsmRelationPreprocessor,
|
||||
BasemapProfile.IgnoreWikidata {
|
||||
BasemapProfile.IgnoreWikidata,
|
||||
ForwardingProfile.OsmNodePreprocessor,
|
||||
ForwardingProfile.OsmWayPreprocessor {
|
||||
|
||||
/*
|
||||
* Generate road names from OSM data. Route network and ref are copied
|
||||
* Generate road names from OSM data. Route networkType and ref are copied
|
||||
* from relations that roads are a part of - except in Great Britain which
|
||||
* uses a naming convention instead of relations.
|
||||
*
|
||||
@@ -113,8 +101,6 @@ public class TransportationName implements
|
||||
private static final String LINK_TEMP_KEY = "__islink";
|
||||
private static final String RELATION_ID_TEMP_KEY = "__relid";
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(TransportationName.class);
|
||||
private static final Pattern GREAT_BRITAIN_REF_NETWORK_PATTERN = Pattern.compile("^[AM][0-9AM()]+");
|
||||
private static final ZoomFunction.MeterToPixelThresholds MIN_LENGTH = ZoomFunction.meterThresholds()
|
||||
.put(6, 20_000)
|
||||
.put(7, 20_000)
|
||||
@@ -122,23 +108,23 @@ public class TransportationName implements
|
||||
.put(9, 8_000)
|
||||
.put(10, 8_000)
|
||||
.put(11, 8_000);
|
||||
private static final Comparator<RouteRelation> RELATION_ORDERING = Comparator
|
||||
.<RouteRelation>comparingInt(r -> r.network.ordinal())
|
||||
// TODO also compare network string?
|
||||
.thenComparingInt(r -> r.ref.length())
|
||||
.thenComparing(RouteRelation::ref);
|
||||
private final Map<String, Integer> MINZOOMS;
|
||||
private static final List<String> CONCURRENT_ROUTE_KEYS = List.of(
|
||||
Fields.ROUTE_1,
|
||||
Fields.ROUTE_2,
|
||||
Fields.ROUTE_3,
|
||||
Fields.ROUTE_4,
|
||||
Fields.ROUTE_5,
|
||||
Fields.ROUTE_6
|
||||
);
|
||||
private final boolean brunnel;
|
||||
private final boolean sizeForShield;
|
||||
private final boolean limitMerge;
|
||||
private final Stats stats;
|
||||
private final PlanetilerConfig config;
|
||||
private final AtomicBoolean loggedNoGb = new AtomicBoolean(false);
|
||||
private PreparedGeometry greatBritain = null;
|
||||
private Transportation transportation;
|
||||
private final LongByteMap motorwayJunctionHighwayClasses = new LongByteHashMap();
|
||||
|
||||
public TransportationName(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.config = config;
|
||||
this.stats = stats;
|
||||
this.brunnel = config.arguments().getBoolean(
|
||||
"transportation_name_brunnel",
|
||||
"transportation_name layer: set to false to omit brunnel and help merge long highways",
|
||||
@@ -154,71 +140,72 @@ public class TransportationName implements
|
||||
"transportation_name layer: limit merge so we don't combine different relations to help merge long highways",
|
||||
false
|
||||
);
|
||||
boolean z13Paths = config.arguments().getBoolean(
|
||||
"transportation_z13_paths",
|
||||
"transportation(_name) layer: show paths on z13",
|
||||
true
|
||||
);
|
||||
MINZOOMS = Map.of(
|
||||
FieldValues.CLASS_TRACK, 14,
|
||||
FieldValues.CLASS_PATH, z13Paths ? 13 : 14,
|
||||
FieldValues.CLASS_MINOR, 13,
|
||||
FieldValues.CLASS_TRUNK, 8,
|
||||
FieldValues.CLASS_MOTORWAY, 6
|
||||
// default: 12
|
||||
);
|
||||
}
|
||||
|
||||
public void needsTransportationLayer(Transportation transportation) {
|
||||
this.transportation = transportation;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void preprocessOsmNode(OsmElement.Node node) {
|
||||
if (node.hasTag("highway", "motorway_junction")) {
|
||||
motorwayJunctionHighwayClasses.put(node.id(), HighwayClass.UNKNOWN.value);
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
public void preprocessOsmWay(OsmElement.Way way) {
|
||||
String highway = way.getString("highway");
|
||||
if (highway != null) {
|
||||
HighwayClass cls = HighwayClass.from(highway);
|
||||
if (cls != HighwayClass.UNKNOWN) {
|
||||
LongArrayList nodes = way.nodes();
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
long node = nodes.get(i);
|
||||
if (motorwayJunctionHighwayClasses.containsKey(node)) {
|
||||
byte oldValue = motorwayJunctionHighwayClasses.get(node);
|
||||
byte newValue = cls.value;
|
||||
if (newValue > oldValue) {
|
||||
motorwayJunctionHighwayClasses.put(node, newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OsmRelationInfo> preprocessOsmRelation(OsmElement.Relation relation) {
|
||||
if (relation.hasTag("route", "road")) {
|
||||
RouteNetwork networkType = null;
|
||||
String network = relation.getString("network");
|
||||
String ref = relation.getString("ref");
|
||||
public void process(Tables.OsmHighwayPoint element, FeatureCollector features) {
|
||||
long id = element.source().id();
|
||||
byte value = motorwayJunctionHighwayClasses.getOrDefault(id, (byte) -1);
|
||||
if (value > 0) {
|
||||
HighwayClass cls = HighwayClass.from(value);
|
||||
if (cls != HighwayClass.UNKNOWN) {
|
||||
String subclass = FieldValues.SUBCLASS_JUNCTION;
|
||||
String ref = element.ref();
|
||||
|
||||
if ("US:I".equals(network)) {
|
||||
networkType = RouteNetwork.US_INTERSTATE;
|
||||
} else if ("US:US".equals(network)) {
|
||||
networkType = RouteNetwork.US_HIGHWAY;
|
||||
} else if (network != null && network.length() == 5 && network.startsWith("US:")) {
|
||||
networkType = RouteNetwork.US_STATE;
|
||||
} else if (network != null && network.startsWith("CA:transcanada")) {
|
||||
networkType = RouteNetwork.CA_TRANSCANADA;
|
||||
}
|
||||
|
||||
if (networkType != null) {
|
||||
return List.of(new RouteRelation(coalesce(ref, ""), networkType, relation.id()));
|
||||
features.point(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(LanguageUtils.getNamesWithoutTranslations(element.source().tags()))
|
||||
.setAttr(Fields.REF, ref)
|
||||
.setAttr(Fields.REF_LENGTH, ref != null ? ref.length() : null)
|
||||
.setAttr(Fields.CLASS, highwayClass(cls.highwayValue, null, null, null))
|
||||
.setAttr(Fields.SUBCLASS, subclass)
|
||||
.setAttr(Fields.LAYER, nullIfLong(element.layer(), 0))
|
||||
.setSortKeyDescending(element.zOrder())
|
||||
.setMinZoom(10);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmHighwayLinestring element, FeatureCollector features) {
|
||||
List<OsmReader.RelationMember<RouteRelation>> relations = element.source()
|
||||
.relationInfo(RouteRelation.class);
|
||||
|
||||
String ref = element.ref();
|
||||
RouteRelation relation = getRouteRelation(element, relations, ref);
|
||||
if (relation != null && nullIfEmpty(relation.ref) != null) {
|
||||
ref = relation.ref;
|
||||
List<Transportation.RouteRelation> relations = transportation.getRouteRelations(element);
|
||||
Transportation.RouteRelation relation = relations.isEmpty() ? null : relations.get(0);
|
||||
if (relation != null && nullIfEmpty(relation.ref()) != null) {
|
||||
ref = relation.ref();
|
||||
}
|
||||
|
||||
String name = nullIfEmpty(element.name());
|
||||
@@ -230,13 +217,15 @@ public class TransportationName implements
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isLink = Transportation.isLink(highway);
|
||||
String baseClass = highwayClass.replace("_construction", "");
|
||||
|
||||
int minzoom = MINZOOMS.getOrDefault(baseClass, 12);
|
||||
boolean isLink = highway.endsWith("_link");
|
||||
if (isLink) {
|
||||
minzoom = Math.max(13, minzoom);
|
||||
}
|
||||
int minzoom = FieldValues.CLASS_TRUNK.equals(baseClass) ? 8 :
|
||||
FieldValues.CLASS_MOTORWAY.equals(baseClass) ? 6 :
|
||||
isLink ? 13 : 12; // fallback - get from line minzoom, but floor at 12
|
||||
|
||||
// inherit min zoom threshold from visible road, and ensure we never show a label on a road that's not visible yet.
|
||||
minzoom = Math.max(minzoom, transportation.getMinzoom(element, highwayClass));
|
||||
|
||||
FeatureCollector.Feature feature = features.line(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
@@ -246,13 +235,21 @@ public class TransportationName implements
|
||||
.setAttr(Fields.REF, ref)
|
||||
.setAttr(Fields.REF_LENGTH, ref != null ? ref.length() : null)
|
||||
.setAttr(Fields.NETWORK,
|
||||
(relation != null && relation.network != null) ? relation.network.name : ref != null ? "road" : null)
|
||||
(relation != null && relation.networkType() != null) ? relation.networkType().name
|
||||
: !nullOrEmpty(ref) ? "road" : null)
|
||||
.setAttr(Fields.CLASS, highwayClass)
|
||||
.setAttr(Fields.SUBCLASS, highwaySubclass(highwayClass, null, highway))
|
||||
.setMinPixelSize(0)
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinZoom(minzoom);
|
||||
|
||||
// populate route_1, route_2, ... tags
|
||||
for (int i = 0; i < Math.min(CONCURRENT_ROUTE_KEYS.size(), relations.size()); i++) {
|
||||
Transportation.RouteRelation routeRelation = relations.get(i);
|
||||
feature.setAttr(CONCURRENT_ROUTE_KEYS.get(i), routeRelation.network() == null ? null :
|
||||
routeRelation.network() + "=" + coalesce(routeRelation.ref(), ""));
|
||||
}
|
||||
|
||||
if (brunnel) {
|
||||
feature.setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()));
|
||||
}
|
||||
@@ -265,48 +262,44 @@ public class TransportationName implements
|
||||
if (limitMerge) {
|
||||
feature
|
||||
.setAttr(LINK_TEMP_KEY, isLink ? 1 : 0)
|
||||
.setAttr(RELATION_ID_TEMP_KEY, relation == null ? null : relation.id);
|
||||
.setAttr(RELATION_ID_TEMP_KEY, relation == null ? null : relation.id());
|
||||
}
|
||||
|
||||
if (isFootwayOrSteps(highway)) {
|
||||
feature
|
||||
.setAttrWithMinzoom(Fields.LAYER, nullIf(element.layer(), 0), 12)
|
||||
.setAttrWithMinzoom(Fields.LAYER, nullIfLong(element.layer(), 0), 12)
|
||||
.setAttrWithMinzoom(Fields.LEVEL, Parse.parseLongOrNull(element.source().getTag("level")), 12)
|
||||
.setAttrWithMinzoom(Fields.INDOOR, element.indoor() ? 1 : null, 12);
|
||||
}
|
||||
}
|
||||
|
||||
private RouteRelation getRouteRelation(Tables.OsmHighwayLinestring element,
|
||||
List<OsmReader.RelationMember<RouteRelation>> relations, String ref) {
|
||||
RouteRelation relation = relations.stream()
|
||||
.map(OsmReader.RelationMember::relation)
|
||||
.min(RELATION_ORDERING)
|
||||
.orElse(null);
|
||||
if (relation == null && ref != null) {
|
||||
// GB doesn't use regular relations like everywhere else, so if we are
|
||||
// in GB then use a naming convention instead.
|
||||
Matcher refMatcher = GREAT_BRITAIN_REF_NETWORK_PATTERN.matcher(ref);
|
||||
if (refMatcher.find()) {
|
||||
if (greatBritain == null) {
|
||||
if (!loggedNoGb.get() && loggedNoGb.compareAndSet(false, true)) {
|
||||
LOGGER.warn("No GB polygon for inferring route network types");
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Geometry wayGeometry = element.source().worldGeometry();
|
||||
if (greatBritain.intersects(wayGeometry)) {
|
||||
RouteNetwork networkType =
|
||||
"motorway".equals(element.highway()) ? RouteNetwork.GB_MOTORWAY : RouteNetwork.GB_TRUNK;
|
||||
relation = new RouteRelation(refMatcher.group(), networkType, 0);
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_transportation_name_gb_test",
|
||||
"Unable to test highway against GB route network: " + element.source().id());
|
||||
}
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void process(Tables.OsmAerialwayLinestring element, FeatureCollector features) {
|
||||
if (!nullOrEmpty(element.name())) {
|
||||
features.line(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setBufferPixelOverrides(MIN_LENGTH)
|
||||
.putAttrs(LanguageUtils.getNamesWithoutTranslations(element.source().tags()))
|
||||
.setAttr(Fields.CLASS, "aerialway")
|
||||
.setAttr(Fields.SUBCLASS, element.aerialway())
|
||||
.setMinPixelSize(0)
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinZoom(12);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmShipwayLinestring element, FeatureCollector features) {
|
||||
if (!nullOrEmpty(element.name())) {
|
||||
features.line(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setBufferPixelOverrides(MIN_LENGTH)
|
||||
.putAttrs(LanguageUtils.getNamesWithoutTranslations(element.source().tags()))
|
||||
.setAttr(Fields.CLASS, element.shipway())
|
||||
.setMinPixelSize(0)
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinZoom(12);
|
||||
}
|
||||
return relation;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -341,35 +334,38 @@ public class TransportationName implements
|
||||
name instanceof String str ? str.length() * 6 : Double.MAX_VALUE;
|
||||
}
|
||||
|
||||
private enum RouteNetwork {
|
||||
private enum HighwayClass {
|
||||
MOTORWAY("motorway", 6),
|
||||
TRUNK("trunk", 5),
|
||||
PRIMARY("primary", 4),
|
||||
SECONDARY("secondary", 3),
|
||||
TERTIARY("tertiary", 2),
|
||||
UNCLASSIFIED("unclassified", 1),
|
||||
UNKNOWN("", 0);
|
||||
|
||||
US_INTERSTATE("us-interstate"),
|
||||
US_HIGHWAY("us-highway"),
|
||||
US_STATE("us-state"),
|
||||
CA_TRANSCANADA("ca-transcanada"),
|
||||
GB_MOTORWAY("gb-motorway"),
|
||||
GB_TRUNK("gb-trunk");
|
||||
private static final Map<String, HighwayClass> indexByString = new HashMap<>();
|
||||
private static final Map<Byte, HighwayClass> indexByByte = new HashMap<>();
|
||||
final byte value;
|
||||
final String highwayValue;
|
||||
|
||||
final String name;
|
||||
|
||||
RouteNetwork(String name) {
|
||||
this.name = name;
|
||||
HighwayClass(String highwayValue, int id) {
|
||||
this.highwayValue = highwayValue;
|
||||
this.value = (byte) id;
|
||||
}
|
||||
}
|
||||
|
||||
/** Information extracted from route relations to use when processing ways in that relation. */
|
||||
private static record RouteRelation(
|
||||
String ref,
|
||||
RouteNetwork network,
|
||||
@Override long id
|
||||
) implements OsmRelationInfo {
|
||||
static {
|
||||
Arrays.stream(values()).forEach(cls -> {
|
||||
indexByString.put(cls.highwayValue, cls);
|
||||
indexByByte.put(cls.value, cls);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public long estimateMemoryUsageBytes() {
|
||||
return CLASS_HEADER_BYTES +
|
||||
POINTER_BYTES + estimateSize(ref) +
|
||||
POINTER_BYTES + // network
|
||||
MemoryEstimator.estimateSizeLong(id);
|
||||
static HighwayClass from(String highway) {
|
||||
return indexByString.getOrDefault(highway, UNKNOWN);
|
||||
}
|
||||
|
||||
static HighwayClass from(byte value) {
|
||||
return indexByByte.getOrDefault(value, UNKNOWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
@@ -103,13 +103,15 @@ public class Water implements
|
||||
@Override
|
||||
public void process(Tables.OsmWaterPolygon element, FeatureCollector features) {
|
||||
if (!"bay".equals(element.natural())) {
|
||||
String clazz = "riverbank".equals(element.waterway()) ? FieldValues.CLASS_RIVER :
|
||||
classMapping.getOrElse(element.source(), FieldValues.CLASS_LAKE);
|
||||
features.polygon(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setMinPixelSizeBelowZoom(11, 2)
|
||||
.setMinZoom(6)
|
||||
.setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0)
|
||||
.setAttrWithMinzoom(Fields.BRUNNEL, Utils.brunnel(element.isBridge(), element.isTunnel()), 12)
|
||||
.setAttr(Fields.CLASS, classMapping.getOrElse(element.source(), FieldValues.CLASS_RIVER));
|
||||
.setAttr(Fields.CLASS, clazz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
@@ -37,6 +37,9 @@ package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.carrotsearch.hppc.LongObjectHashMap;
|
||||
import com.google.common.util.concurrent.AtomicDouble;
|
||||
import com.graphhopper.coll.GHLongObjectHashMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
@@ -46,7 +49,11 @@ import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.basemap.util.Utils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmElement;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmReader;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import com.onthegomap.planetiler.util.ZoomFunction;
|
||||
@@ -63,7 +70,9 @@ public class Waterway implements
|
||||
OpenMapTilesSchema.Waterway,
|
||||
Tables.OsmWaterwayLinestring.Handler,
|
||||
BasemapProfile.FeaturePostProcessor,
|
||||
BasemapProfile.NaturalEarthProcessor {
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
BasemapProfile.OsmRelationPreprocessor,
|
||||
BasemapProfile.OsmAllProcessor {
|
||||
|
||||
/*
|
||||
* Uses Natural Earth at lower zoom-levels and OpenStreetMap at higher zoom levels.
|
||||
@@ -76,14 +85,6 @@ public class Waterway implements
|
||||
* short segment of it goes through this tile.
|
||||
*/
|
||||
|
||||
private final Translations translations;
|
||||
private final PlanetilerConfig config;
|
||||
|
||||
public Waterway(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.config = config;
|
||||
this.translations = translations;
|
||||
}
|
||||
|
||||
private static final Map<String, Integer> CLASS_MINZOOM = Map.of(
|
||||
"river", 12,
|
||||
"canal", 12,
|
||||
@@ -92,12 +93,29 @@ public class Waterway implements
|
||||
"drain", 13,
|
||||
"ditch", 13
|
||||
);
|
||||
private static final String TEMP_REL_ID_ADDR = "_relid";
|
||||
|
||||
private final Translations translations;
|
||||
private final PlanetilerConfig config;
|
||||
private final Stats stats;
|
||||
private final LongObjectHashMap<AtomicDouble> riverRelationLengths = new GHLongObjectHashMap<>();
|
||||
|
||||
public Waterway(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.config = config;
|
||||
this.translations = translations;
|
||||
this.stats = stats;
|
||||
}
|
||||
|
||||
private static final ZoomFunction.MeterToPixelThresholds MIN_PIXEL_LENGTHS = ZoomFunction.meterThresholds()
|
||||
.put(6, 500_000)
|
||||
.put(7, 400_000)
|
||||
.put(8, 300_000)
|
||||
.put(9, 8_000)
|
||||
.put(10, 4_000)
|
||||
.put(11, 1_000);
|
||||
|
||||
// zoom-level 3-5 come from natural earth
|
||||
|
||||
@Override
|
||||
public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) {
|
||||
if (feature.hasTag("featurecla", "River")) {
|
||||
@@ -105,7 +123,6 @@ public class Waterway implements
|
||||
ZoomRange zoom = switch (table) {
|
||||
case "ne_110m_rivers_lake_centerlines" -> new ZoomRange(3, 3);
|
||||
case "ne_50m_rivers_lake_centerlines" -> new ZoomRange(4, 5);
|
||||
case "ne_10m_rivers_lake_centerlines" -> new ZoomRange(6, 8);
|
||||
default -> null;
|
||||
};
|
||||
if (zoom != null) {
|
||||
@@ -117,6 +134,52 @@ public class Waterway implements
|
||||
}
|
||||
}
|
||||
|
||||
// zoom-level 6-8 come from OSM river relations
|
||||
|
||||
private record WaterwayRelation(
|
||||
long id,
|
||||
Map<String, Object> names
|
||||
) implements OsmRelationInfo {}
|
||||
|
||||
@Override
|
||||
public List<OsmRelationInfo> preprocessOsmRelation(OsmElement.Relation relation) {
|
||||
if (relation.hasTag("waterway", "river") && !Utils.nullOrEmpty(relation.getString("name"))) {
|
||||
riverRelationLengths.put(relation.id(), new AtomicDouble());
|
||||
return List.of(new WaterwayRelation(relation.id(), LanguageUtils.getNames(relation.tags(), translations)));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processAllOsm(SourceFeature feature, FeatureCollector features) {
|
||||
List<OsmReader.RelationMember<WaterwayRelation>> waterways = feature.relationInfo(WaterwayRelation.class);
|
||||
if (waterways != null && !waterways.isEmpty() && feature.canBeLine()) {
|
||||
for (var waterway : waterways) {
|
||||
String role = waterway.role();
|
||||
if (Utils.nullOrEmpty(role) || "main_stream".equals(role)) {
|
||||
long relId = waterway.relation().id();
|
||||
try {
|
||||
AtomicDouble counter = riverRelationLengths.get(relId);
|
||||
if (counter != null) {
|
||||
counter.addAndGet(feature.length());
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "waterway_decode", "Unable to get waterway length for " + feature.id());
|
||||
}
|
||||
features.line(LAYER_NAME)
|
||||
.setAttr(TEMP_REL_ID_ADDR, relId)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_RIVER)
|
||||
.putAttrs(waterway.relation().names())
|
||||
.setZoomRange(6, 8)
|
||||
.setMinPixelSize(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// zoom-level 9+ come from OSM river ways
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmWaterwayLinestring element, FeatureCollector features) {
|
||||
String waterway = element.waterway();
|
||||
@@ -137,7 +200,22 @@ public class Waterway implements
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
if (zoom >= 9 && zoom <= 11) {
|
||||
if (zoom >= 6 && zoom <= 8) {
|
||||
// remove ways for river relations if relation is not long enough
|
||||
double minSizeAtZoom = MIN_PIXEL_LENGTHS.apply(zoom).doubleValue() / Math.pow(2, zoom) / 256d;
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
Object relIdObj = items.get(i).attrs().remove(TEMP_REL_ID_ADDR);
|
||||
if (relIdObj instanceof Long relId && riverRelationLengths.get(relId).get() < minSizeAtZoom) {
|
||||
items.set(i, null);
|
||||
}
|
||||
}
|
||||
return FeatureMerge.mergeLineStrings(
|
||||
items,
|
||||
config.minFeatureSize(zoom),
|
||||
config.tolerance(zoom),
|
||||
BUFFER_SIZE
|
||||
);
|
||||
} else if (zoom >= 9 && zoom <= 11) {
|
||||
return FeatureMerge.mergeLineStrings(
|
||||
items,
|
||||
MIN_PIXEL_LENGTHS.apply(zoom).doubleValue(),
|
||||
|
||||
Reference in New Issue
Block a user