planetiler/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/layers/TransportationName.java

313 wiersze
12 KiB
Java

package com.onthegomap.flatmap.openmaptiles.layers;
import static com.onthegomap.flatmap.openmaptiles.Utils.brunnel;
import static com.onthegomap.flatmap.openmaptiles.Utils.coalesce;
import static com.onthegomap.flatmap.openmaptiles.Utils.nullIf;
import static com.onthegomap.flatmap.openmaptiles.Utils.nullIfEmpty;
import static com.onthegomap.flatmap.openmaptiles.layers.Transportation.highwayClass;
import static com.onthegomap.flatmap.openmaptiles.layers.Transportation.highwaySubclass;
import static com.onthegomap.flatmap.openmaptiles.layers.Transportation.isFootwayOrSteps;
import com.graphhopper.reader.ReaderRelation;
import com.onthegomap.flatmap.Arguments;
import com.onthegomap.flatmap.FeatureCollector;
import com.onthegomap.flatmap.FeatureMerge;
import com.onthegomap.flatmap.MemoryEstimator;
import com.onthegomap.flatmap.Parse;
import com.onthegomap.flatmap.SourceFeature;
import com.onthegomap.flatmap.Translations;
import com.onthegomap.flatmap.VectorTileEncoder;
import com.onthegomap.flatmap.ZoomFunction;
import com.onthegomap.flatmap.geo.GeoUtils;
import com.onthegomap.flatmap.geo.GeometryException;
import com.onthegomap.flatmap.monitoring.Stats;
import com.onthegomap.flatmap.openmaptiles.LanguageUtils;
import com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile;
import com.onthegomap.flatmap.openmaptiles.generated.OpenMapTilesSchema;
import com.onthegomap.flatmap.openmaptiles.generated.Tables;
import com.onthegomap.flatmap.read.OpenStreetMapReader;
import java.util.Comparator;
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.jetbrains.annotations.Nullable;
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;
public class TransportationName implements
OpenMapTilesSchema.TransportationName,
Tables.OsmHighwayLinestring.Handler,
OpenMapTilesProfile.NaturalEarthProcessor,
OpenMapTilesProfile.FeaturePostProcessor,
OpenMapTilesProfile.OsmRelationPreprocessor,
OpenMapTilesProfile.IgnoreWikidata {
// 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 Logger LOGGER = LoggerFactory.getLogger(TransportationName.class);
private static final Pattern GREAT_BRITAIN_REF_NETWORK_PATTERN = Pattern.compile("^[AM][0-9AM()]+");
private final Map<String, Integer> MINZOOMS;
private static final ZoomFunction.MeterThresholds 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 double PIXEL = 256d / 4096d;
private final boolean brunnel;
private final boolean sizeForShield;
private final boolean limitMerge;
private final boolean z13Paths;
private final Stats stats;
private PreparedGeometry greatBritain = null;
private AtomicBoolean loggedNoGb = new AtomicBoolean(false);
public TransportationName(Translations translations, Arguments args, Stats stats) {
this.stats = stats;
this.brunnel = args.get(
"transportation_name_brunnel",
"transportation_name layer: set to false to omit brunnel and help merge long highways",
true
);
this.sizeForShield = args.get(
"transportation_name_size_for_shield",
"transportation_name layer: allow road names on shorter segments (ie. they will have a shield)",
false
);
this.limitMerge = args.get(
"transportation_name_limit_merge",
"transportation_name layer: limit merge so we don't combine different relations to help merge long highways",
false
);
this.z13Paths = args.get(
"transportation_z13_paths",
"transportation(_name) layer: show paths on z13",
false
);
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
);
}
@Override
public void processNaturalEarth(String table, SourceFeature feature,
FeatureCollector features) {
if ("ne_10m_admin_0_countries".equals(table) && feature.hasTag("iso_a2", "GB")) {
try {
synchronized (this) {
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);
}
}
}
@Override
public List<OpenStreetMapReader.RelationInfo> preprocessOsmRelation(ReaderRelation relation) {
if (relation.hasTag("route", "road")) {
RouteNetwork networkType = null;
String network = relation.getTag("network");
String name = relation.getTag("name");
String ref = relation.getTag("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(ref, networkType, relation.getId()));
}
}
return null;
}
@Override
public void process(Tables.OsmHighwayLinestring element, FeatureCollector features) {
List<OpenStreetMapReader.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;
}
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;
}
String baseClass = highwayClass.replace("_construction", "");
int minzoom = MINZOOMS.getOrDefault(baseClass, 12);
boolean isLink = highway.endsWith("_link");
if (isLink) {
minzoom = Math.max(13, minzoom);
}
FeatureCollector.Feature feature = features.line(LAYER_NAME)
.setBufferPixels(BUFFER_SIZE)
.setBufferPixelOverrides(MIN_LENGTH)
// TODO abbreviate road names
.setAttrs(LanguageUtils.getNamesWithoutTranslations(element.source().properties()))
.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)
.setAttr(Fields.CLASS, highwayClass)
.setAttr(Fields.SUBCLASS, highwaySubclass(highwayClass, null, highway))
.setMinPixelSize(0)
.setZorder(element.zOrder())
.setZoomRange(minzoom, 14);
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, relation == null ? null : relation.id);
}
if (isFootwayOrSteps(highway)) {
feature
.setAttrWithMinzoom(Fields.LAYER, nullIf(element.layer(), 0), 12)
.setAttrWithMinzoom(Fields.LEVEL, Parse.parseLongOrNull(element.source().getTag("level")), 12)
.setAttrWithMinzoom(Fields.INDOOR, element.indoor() ? 1 : null, 12);
}
}
@Nullable
private RouteRelation getRouteRelation(Tables.OsmHighwayLinestring element,
List<OpenStreetMapReader.RelationMember<RouteRelation>> relations, String ref) {
RouteRelation relation = relations.stream()
.map(OpenStreetMapReader.RelationMember::relation)
.min(RELATION_ORDERING)
.orElse(null);
if (relation == null && ref != null) {
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());
}
}
}
}
return relation;
}
@Override
public List<VectorTileEncoder.Feature> postProcess(int zoom,
List<VectorTileEncoder.Feature> items) throws GeometryException {
double tolerance = zoom >= 14 ? PIXEL : 0.1;
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) {
for (var feature : result) {
feature.attrs().remove(LINK_TEMP_KEY);
feature.attrs().remove(RELATION_ID_TEMP_KEY);
}
}
return result;
}
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 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;
}
}
private static record RouteRelation(
String ref,
RouteNetwork network,
long id
) implements OpenStreetMapReader.RelationInfo {
@Override
public long estimateMemoryUsageBytes() {
return 24 +
8 + // network pointer
8 + // ref pointer
8 + // id
MemoryEstimator.size(ref);
}
}
private static final Comparator<RouteRelation> RELATION_ORDERING = Comparator
.<RouteRelation>comparingInt(r -> r.network.ordinal())
// TODO also compare network string?
.thenComparingInt(r -> r.ref == null ? 0 : r.ref.length())
.thenComparing(r -> r.ref);
}