mirror of
https://github.com/cfpwastaken/planetiler-openmaptiles.git
synced 2026-02-04 20:41:09 +00:00
Make planetiler-openmaptiles runnable as a standalone project (#19)
This commit is contained in:
398
src/main/java/org/openmaptiles/layers/TransportationName.java
Normal file
398
src/main/java/org/openmaptiles/layers/TransportationName.java
Normal file
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package org.openmaptiles.layers;
|
||||
|
||||
import static org.openmaptiles.layers.Transportation.highwayClass;
|
||||
import static org.openmaptiles.layers.Transportation.highwaySubclass;
|
||||
import static org.openmaptiles.layers.Transportation.isFootwayOrSteps;
|
||||
import static org.openmaptiles.util.Utils.*;
|
||||
|
||||
import com.carrotsearch.hppc.LongArrayList;
|
||||
import com.carrotsearch.hppc.LongByteMap;
|
||||
import com.carrotsearch.hppc.LongHashSet;
|
||||
import com.carrotsearch.hppc.LongSet;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.ForwardingProfile;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.collection.Hppc;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmElement;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import com.onthegomap.planetiler.util.ZoomFunction;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import org.openmaptiles.OpenMapTilesProfile;
|
||||
import org.openmaptiles.generated.OpenMapTilesSchema;
|
||||
import org.openmaptiles.generated.Tables;
|
||||
import org.openmaptiles.util.OmtLanguageUtils;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for road, shipway, rail, and path names in the {@code
|
||||
* transportation_name} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/transportation_name">OpenMapTiles
|
||||
* transportation_name sql files</a>.
|
||||
*/
|
||||
public class TransportationName implements
|
||||
OpenMapTilesSchema.TransportationName,
|
||||
Tables.OsmHighwayPoint.Handler,
|
||||
Tables.OsmHighwayLinestring.Handler,
|
||||
Tables.OsmAerialwayLinestring.Handler,
|
||||
Tables.OsmShipwayLinestring.Handler,
|
||||
OpenMapTilesProfile.FeaturePostProcessor,
|
||||
OpenMapTilesProfile.IgnoreWikidata,
|
||||
ForwardingProfile.OsmNodePreprocessor,
|
||||
ForwardingProfile.OsmWayPreprocessor {
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* The goal is to make name linestrings as long as possible to give clients
|
||||
* the best chance of showing road names at different zoom levels, so do not
|
||||
* limit linestrings by length at process time and merge them at tile
|
||||
* render-time.
|
||||
*
|
||||
* Any 3-way nodes and intersections break line merging so set the
|
||||
* transportation_name_limit_merge argument to true to add temporary
|
||||
* "is link" and "relation" keys to prevent opposite directions of a
|
||||
* divided highway or on/off ramps from getting merged for main highways.
|
||||
*/
|
||||
|
||||
// extra temp key used to group on/off-ramps separately from main highways
|
||||
private static final String LINK_TEMP_KEY = "__islink";
|
||||
private static final String RELATION_ID_TEMP_KEY = "__relid";
|
||||
|
||||
private static final ZoomFunction.MeterToPixelThresholds MIN_LENGTH = ZoomFunction.meterThresholds()
|
||||
.put(6, 20_000)
|
||||
.put(7, 20_000)
|
||||
.put(8, 14_000)
|
||||
.put(9, 8_000)
|
||||
.put(10, 8_000)
|
||||
.put(11, 8_000);
|
||||
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 PlanetilerConfig config;
|
||||
private final boolean minorRefs;
|
||||
private Transportation transportation;
|
||||
private final LongByteMap motorwayJunctionHighwayClasses = Hppc.newLongByteHashMap();
|
||||
private final LongSet motorwayJunctionNodes = new LongHashSet();
|
||||
|
||||
public TransportationName(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.config = config;
|
||||
this.brunnel = config.arguments().getBoolean(
|
||||
"transportation_name_brunnel",
|
||||
"transportation_name layer: set to false to omit brunnel and help merge long highways",
|
||||
false
|
||||
);
|
||||
this.sizeForShield = config.arguments().getBoolean(
|
||||
"transportation_name_size_for_shield",
|
||||
"transportation_name layer: allow road names on shorter segments (ie. they will have a shield)",
|
||||
false
|
||||
);
|
||||
this.limitMerge = config.arguments().getBoolean(
|
||||
"transportation_name_limit_merge",
|
||||
"transportation_name layer: limit merge so we don't combine different relations to help merge long highways",
|
||||
false
|
||||
);
|
||||
this.minorRefs = config.arguments().getBoolean(
|
||||
"transportation_name_minor_refs",
|
||||
"transportation_name layer: include name and refs from minor road networks if not present on a way",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
public void needsTransportationLayer(Transportation transportation) {
|
||||
this.transportation = transportation;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void preprocessOsmNode(OsmElement.Node node) {
|
||||
if (node.hasTag("highway", "motorway_junction")) {
|
||||
synchronized (motorwayJunctionNodes) {
|
||||
motorwayJunctionNodes.add(node.id());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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 (motorwayJunctionNodes.contains(node)) {
|
||||
synchronized (motorwayJunctionHighwayClasses) {
|
||||
byte oldValue = motorwayJunctionHighwayClasses.getOrDefault(node, HighwayClass.UNKNOWN.value);
|
||||
byte newValue = cls.value;
|
||||
if (newValue > oldValue) {
|
||||
motorwayJunctionHighwayClasses.put(node, newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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();
|
||||
|
||||
features.point(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(OmtLanguageUtils.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmHighwayLinestring element, FeatureCollector features) {
|
||||
String ref = element.ref();
|
||||
List<Transportation.RouteRelation> relations = transportation.getRouteRelations(element);
|
||||
Transportation.RouteRelation firstRelationWithNetwork = relations.stream()
|
||||
.filter(rel -> rel.networkType() != null)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (firstRelationWithNetwork != null && !nullOrEmpty(firstRelationWithNetwork.ref())) {
|
||||
ref = firstRelationWithNetwork.ref();
|
||||
}
|
||||
|
||||
// if transportation_name_minor_refs flag set and we don't have a ref yet, then pull it from any network
|
||||
if (nullOrEmpty(ref) && minorRefs && !relations.isEmpty()) {
|
||||
ref = relations.stream()
|
||||
.map(r -> r.ref())
|
||||
.filter(r -> !nullOrEmpty(r))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
String name = nullIfEmpty(element.name());
|
||||
ref = nullIfEmpty(ref);
|
||||
String highway = nullIfEmpty(element.highway());
|
||||
|
||||
String highwayClass = highwayClass(element.highway(), null, element.construction(), element.manMade());
|
||||
if (element.isArea() || highway == null || highwayClass == null || (name == null && ref == null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isLink = Transportation.isLink(highway);
|
||||
String baseClass = highwayClass.replace("_construction", "");
|
||||
|
||||
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)
|
||||
.setBufferPixelOverrides(MIN_LENGTH)
|
||||
// TODO abbreviate road names - can't port osml10n because it is AGPL
|
||||
.putAttrs(OmtLanguageUtils.getNamesWithoutTranslations(element.source().tags()))
|
||||
.setAttr(Fields.REF, ref)
|
||||
.setAttr(Fields.REF_LENGTH, ref != null ? ref.length() : null)
|
||||
.setAttr(Fields.NETWORK,
|
||||
firstRelationWithNetwork != null ? firstRelationWithNetwork.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()));
|
||||
}
|
||||
|
||||
/*
|
||||
* to help group roads into longer segments, add temporary tags to limit which segments get grouped together. Since
|
||||
* a divided highway typically has a separate relation for each direction, this ends up keeping segments going
|
||||
* opposite directions group getting grouped together and confusing the line merging process
|
||||
*/
|
||||
if (limitMerge) {
|
||||
feature
|
||||
.setAttr(LINK_TEMP_KEY, isLink ? 1 : 0)
|
||||
.setAttr(RELATION_ID_TEMP_KEY, firstRelationWithNetwork == null ? null : firstRelationWithNetwork.id());
|
||||
}
|
||||
|
||||
if (isFootwayOrSteps(highway)) {
|
||||
feature
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmAerialwayLinestring element, FeatureCollector features) {
|
||||
if (!nullOrEmpty(element.name())) {
|
||||
features.line(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setBufferPixelOverrides(MIN_LENGTH)
|
||||
.putAttrs(OmtLanguageUtils.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(OmtLanguageUtils.getNamesWithoutTranslations(element.source().tags()))
|
||||
.setAttr(Fields.CLASS, element.shipway())
|
||||
.setMinPixelSize(0)
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinZoom(12);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
double tolerance = config.tolerance(zoom);
|
||||
double minLength = coalesce(MIN_LENGTH.apply(zoom), 0).doubleValue();
|
||||
// TODO tolerances:
|
||||
// z6: (tolerance: 500)
|
||||
// z7: (tolerance: 200)
|
||||
// z8: (tolerance: 120)
|
||||
// z9-11: (tolerance: 50)
|
||||
Function<Map<String, Object>, Double> lengthLimitCalculator =
|
||||
zoom >= 14 ? (p -> 0d) :
|
||||
minLength > 0 ? (p -> minLength) :
|
||||
this::getMinLengthForName;
|
||||
var result = FeatureMerge.mergeLineStrings(items, lengthLimitCalculator, tolerance, BUFFER_SIZE);
|
||||
if (limitMerge) {
|
||||
// remove temp keys that were just used to improve line merging
|
||||
for (var feature : result) {
|
||||
feature.attrs().remove(LINK_TEMP_KEY);
|
||||
feature.attrs().remove(RELATION_ID_TEMP_KEY);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Returns the minimum pixel length that a name will fit into. */
|
||||
private double getMinLengthForName(Map<String, Object> attrs) {
|
||||
Object ref = attrs.get(Fields.REF);
|
||||
Object name = coalesce(attrs.get(Fields.NAME), ref);
|
||||
return (sizeForShield && ref instanceof String) ? 6 :
|
||||
name instanceof String str ? str.length() * 6 : Double.MAX_VALUE;
|
||||
}
|
||||
|
||||
private enum HighwayClass {
|
||||
MOTORWAY("motorway", 6),
|
||||
TRUNK("trunk", 5),
|
||||
PRIMARY("primary", 4),
|
||||
SECONDARY("secondary", 3),
|
||||
TERTIARY("tertiary", 2),
|
||||
UNCLASSIFIED("unclassified", 1),
|
||||
UNKNOWN("", 0);
|
||||
|
||||
private static final Map<String, HighwayClass> indexByString = new HashMap<>();
|
||||
private static final Map<Byte, HighwayClass> indexByByte = new HashMap<>();
|
||||
final byte value;
|
||||
final String highwayValue;
|
||||
|
||||
HighwayClass(String highwayValue, int id) {
|
||||
this.highwayValue = highwayValue;
|
||||
this.value = (byte) id;
|
||||
}
|
||||
|
||||
static {
|
||||
Arrays.stream(values()).forEach(cls -> {
|
||||
indexByString.put(cls.highwayValue, cls);
|
||||
indexByByte.put(cls.value, cls);
|
||||
});
|
||||
}
|
||||
|
||||
static HighwayClass from(String highway) {
|
||||
return indexByString.getOrDefault(highway, UNKNOWN);
|
||||
}
|
||||
|
||||
static HighwayClass from(byte value) {
|
||||
return indexByByte.getOrDefault(value, UNKNOWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user