mirror of
https://github.com/cfpwastaken/planetiler-openmaptiles.git
synced 2026-02-04 04:21:08 +00:00
Make planetiler-openmaptiles runnable as a standalone project (#19)
This commit is contained in:
83
src/main/java/org/openmaptiles/layers/AerodromeLabel.java
Normal file
83
src/main/java/org/openmaptiles/layers/AerodromeLabel.java
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
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.util.Utils.nullIfEmpty;
|
||||
import static org.openmaptiles.util.Utils.nullOrEmpty;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import org.openmaptiles.generated.OpenMapTilesSchema;
|
||||
import org.openmaptiles.generated.Tables;
|
||||
import org.openmaptiles.util.OmtLanguageUtils;
|
||||
import org.openmaptiles.util.Utils;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements in the {@code aerodrome_label} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/aerodrome_label">OpenMapTiles
|
||||
* aerodrome_layer sql files</a>.
|
||||
*/
|
||||
public class AerodromeLabel implements
|
||||
OpenMapTilesSchema.AerodromeLabel,
|
||||
Tables.OsmAerodromeLabelPoint.Handler {
|
||||
|
||||
private final MultiExpression.Index<String> classLookup;
|
||||
private final Translations translations;
|
||||
|
||||
public AerodromeLabel(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.classLookup = FieldMappings.Class.index();
|
||||
this.translations = translations;
|
||||
}
|
||||
|
||||
@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(important ? 8 : 10)
|
||||
.putAttrs(OmtLanguageUtils.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, clazz);
|
||||
}
|
||||
}
|
||||
83
src/main/java/org/openmaptiles/layers/Aeroway.java
Normal file
83
src/main/java/org/openmaptiles/layers/Aeroway.java
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
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 com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import org.openmaptiles.generated.OpenMapTilesSchema;
|
||||
import org.openmaptiles.generated.Tables;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements in the {@code aeroway} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/aeroway">OpenMapTiles aeroway sql files</a>.
|
||||
*/
|
||||
public class Aeroway implements
|
||||
OpenMapTilesSchema.Aeroway,
|
||||
Tables.OsmAerowayLinestring.Handler,
|
||||
Tables.OsmAerowayPolygon.Handler,
|
||||
Tables.OsmAerowayPoint.Handler {
|
||||
|
||||
public Aeroway(Translations translations, PlanetilerConfig config, Stats stats) {}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmAerowayPolygon element, FeatureCollector features) {
|
||||
features.polygon(LAYER_NAME)
|
||||
.setMinZoom(10)
|
||||
.setMinPixelSize(2)
|
||||
.setAttr(Fields.CLASS, element.aeroway())
|
||||
.setAttr(Fields.REF, element.ref());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmAerowayLinestring element, FeatureCollector features) {
|
||||
features.line(LAYER_NAME)
|
||||
.setMinZoom(10)
|
||||
.setAttr(Fields.CLASS, element.aeroway())
|
||||
.setAttr(Fields.REF, element.ref());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmAerowayPoint element, FeatureCollector features) {
|
||||
features.point(LAYER_NAME)
|
||||
.setMinZoom(14)
|
||||
.setAttr(Fields.CLASS, element.aeroway())
|
||||
.setAttr(Fields.REF, element.ref());
|
||||
}
|
||||
}
|
||||
469
src/main/java/org/openmaptiles/layers/Boundary.java
Normal file
469
src/main/java/org/openmaptiles/layers/Boundary.java
Normal file
@@ -0,0 +1,469 @@
|
||||
/*
|
||||
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 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.stream.Collectors.counting;
|
||||
import static java.util.stream.Collectors.groupingBy;
|
||||
|
||||
import com.carrotsearch.hppc.LongObjectMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.collection.Hppc;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.reader.SimpleFeature;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmElement;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Format;
|
||||
import com.onthegomap.planetiler.util.MemoryEstimator;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.TopologyException;
|
||||
import org.locationtech.jts.geom.prep.PreparedGeometry;
|
||||
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
|
||||
import org.locationtech.jts.operation.linemerge.LineMerger;
|
||||
import org.locationtech.jts.operation.polygonize.Polygonizer;
|
||||
import org.openmaptiles.OpenMapTilesProfile;
|
||||
import org.openmaptiles.generated.OpenMapTilesSchema;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for country, state, and town boundaries in the {@code boundary} layer
|
||||
* from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/boundary">OpenMapTiles boundary sql
|
||||
* files</a>.
|
||||
*/
|
||||
public class Boundary implements
|
||||
OpenMapTilesSchema.Boundary,
|
||||
OpenMapTilesProfile.NaturalEarthProcessor,
|
||||
OpenMapTilesProfile.OsmRelationPreprocessor,
|
||||
OpenMapTilesProfile.OsmAllProcessor,
|
||||
OpenMapTilesProfile.FeaturePostProcessor,
|
||||
OpenMapTilesProfile.FinishHandler {
|
||||
|
||||
/*
|
||||
* Uses natural earth at lower zoom levels and OpenStreetMap at higher zoom levels.
|
||||
*
|
||||
* For OpenStreetMap data at higher zoom levels:
|
||||
* 1) Preprocess relations on the first pass to extract info for relations where
|
||||
* type=boundary and boundary=administrative and store the admin_level for
|
||||
* later.
|
||||
* 2) When processing individual ways, take the minimum (most important) admin
|
||||
* level of every relation they are a part of and use that as the admin level
|
||||
* for the way.
|
||||
* 3) If boundary_country_names argument is true and the way is part of a country
|
||||
* (admin_level=2) boundary, then hold onto it for later
|
||||
* 4) When we finish processing the OSM source, build country polygons from the
|
||||
* saved ways and use that to determine which country is on the left and right
|
||||
* side of each way, then emit the way with ADM0_L and ADM0_R keys set.
|
||||
* 5) Before emitting boundary lines, merge linestrings with the same tags.
|
||||
*/
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(Boundary.class);
|
||||
private static final double COUNTRY_TEST_OFFSET = GeoUtils.metersToPixelAtEquator(0, 10) / 256d;
|
||||
private final Stats stats;
|
||||
private final boolean addCountryNames;
|
||||
// may be updated concurrently by multiple threads
|
||||
private final Map<Long, String> regionNames = new ConcurrentHashMap<>();
|
||||
// need to synchronize updates to these shared data structures:
|
||||
private final Map<Long, List<Geometry>> regionGeometries = new HashMap<>();
|
||||
private final Map<CountryBoundaryComponent, List<Geometry>> boundariesToMerge = new HashMap<>();
|
||||
private final PlanetilerConfig config;
|
||||
|
||||
public Boundary(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.config = config;
|
||||
this.addCountryNames = config.arguments().getBoolean(
|
||||
"boundary_country_names",
|
||||
"boundary layer: add left/right codes of neighboring countries",
|
||||
true
|
||||
);
|
||||
this.stats = stats;
|
||||
}
|
||||
|
||||
private static boolean isDisputed(Map<String, Object> tags) {
|
||||
return Parse.bool(tags.get("disputed")) ||
|
||||
Parse.bool(tags.get("dispute")) ||
|
||||
"dispute".equals(tags.get("border_status")) ||
|
||||
tags.containsKey("disputed_by") ||
|
||||
tags.containsKey("claimed_by");
|
||||
}
|
||||
|
||||
private static String editName(String name) {
|
||||
return name == null ? null : name.replace(" at ", "")
|
||||
.replaceAll("\\s+", "")
|
||||
.replace("Extentof", "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
regionGeometries.clear();
|
||||
boundariesToMerge.clear();
|
||||
regionNames.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) {
|
||||
boolean disputed = feature.getString("featurecla", "").startsWith("Disputed");
|
||||
record BoundaryInfo(int adminLevel, int minzoom, int maxzoom) {}
|
||||
BoundaryInfo info = switch (table) {
|
||||
case "ne_110m_admin_0_boundary_lines_land" -> new BoundaryInfo(2, 0, 0);
|
||||
case "ne_50m_admin_0_boundary_lines_land" -> new BoundaryInfo(2, 1, 3);
|
||||
case "ne_10m_admin_0_boundary_lines_land" -> feature.hasTag("featurecla", "Lease Limit") ? null :
|
||||
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) :
|
||||
minZoom != null && minZoom <= 7.7 ? new BoundaryInfo(4, 4, 4) :
|
||||
null;
|
||||
}
|
||||
default -> null;
|
||||
};
|
||||
if (info != null) {
|
||||
features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setZoomRange(info.minzoom, info.maxzoom)
|
||||
.setMinPixelSizeAtAllZooms(0)
|
||||
.setAttr(Fields.ADMIN_LEVEL, info.adminLevel)
|
||||
.setAttr(Fields.MARITIME, 0)
|
||||
.setAttr(Fields.DISPUTED, disputed ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OsmRelationInfo> preprocessOsmRelation(OsmElement.Relation relation) {
|
||||
if (relation.hasTag("type", "boundary") &&
|
||||
relation.hasTag("admin_level") &&
|
||||
relation.hasTag("boundary", "administrative")) {
|
||||
Integer adminLevelValue = Parse.parseRoundInt(relation.getTag("admin_level"));
|
||||
String code = relation.getString("ISO3166-1:alpha3");
|
||||
if (adminLevelValue != null && adminLevelValue >= 2 && adminLevelValue <= 10) {
|
||||
boolean disputed = isDisputed(relation.tags());
|
||||
if (code != null) {
|
||||
regionNames.put(relation.id(), code);
|
||||
}
|
||||
return List.of(new BoundaryRelation(
|
||||
relation.id(),
|
||||
adminLevelValue,
|
||||
disputed,
|
||||
relation.getString("name"),
|
||||
disputed ? relation.getString("claimed_by") : null,
|
||||
code
|
||||
));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processAllOsm(SourceFeature feature, FeatureCollector features) {
|
||||
if (!feature.canBeLine()) {
|
||||
return;
|
||||
}
|
||||
var relationInfos = feature.relationInfo(BoundaryRelation.class);
|
||||
if (!relationInfos.isEmpty()) {
|
||||
int minAdminLevel = Integer.MAX_VALUE;
|
||||
String disputedName = null, claimedBy = null;
|
||||
Set<Long> regionIds = new HashSet<>();
|
||||
boolean disputed = false;
|
||||
// aggregate all borders this way is a part of - take the lowest
|
||||
// admin level, and assume it is disputed if any relation is disputed.
|
||||
for (var info : relationInfos) {
|
||||
BoundaryRelation rel = info.relation();
|
||||
disputed |= rel.disputed;
|
||||
if (rel.adminLevel < minAdminLevel) {
|
||||
minAdminLevel = rel.adminLevel;
|
||||
}
|
||||
if (rel.disputed) {
|
||||
disputedName = disputedName == null ? rel.name : disputedName;
|
||||
claimedBy = claimedBy == null ? rel.claimedBy : claimedBy;
|
||||
}
|
||||
if (minAdminLevel == 2 && regionNames.containsKey(info.relation().id)) {
|
||||
regionIds.add(info.relation().id);
|
||||
}
|
||||
}
|
||||
|
||||
if (minAdminLevel <= 10) {
|
||||
boolean wayIsDisputed = isDisputed(feature.tags());
|
||||
disputed |= wayIsDisputed;
|
||||
if (wayIsDisputed) {
|
||||
disputedName = disputedName == null ? feature.getString("name") : disputedName;
|
||||
claimedBy = claimedBy == null ? feature.getString("claimed_by") : claimedBy;
|
||||
}
|
||||
boolean maritime = feature.getBoolean("maritime") ||
|
||||
feature.hasTag("natural", "coastline") ||
|
||||
feature.hasTag("boundary_type", "maritime");
|
||||
int minzoom =
|
||||
(maritime && minAdminLevel == 2) ? 4 :
|
||||
minAdminLevel <= 4 ? 5 :
|
||||
minAdminLevel <= 6 ? 9 :
|
||||
minAdminLevel <= 8 ? 11 : 12;
|
||||
if (addCountryNames && !regionIds.isEmpty()) {
|
||||
// save for later
|
||||
try {
|
||||
CountryBoundaryComponent component = new CountryBoundaryComponent(
|
||||
minAdminLevel,
|
||||
disputed,
|
||||
maritime,
|
||||
minzoom,
|
||||
feature.line(),
|
||||
regionIds,
|
||||
claimedBy,
|
||||
disputedName
|
||||
);
|
||||
// multiple threads may update this concurrently
|
||||
synchronized (this) {
|
||||
boundariesToMerge.computeIfAbsent(component.groupingKey(), key -> new ArrayList<>()).add(component.line);
|
||||
for (var info : relationInfos) {
|
||||
var rel = info.relation();
|
||||
if (rel.adminLevel <= 2) {
|
||||
regionGeometries.computeIfAbsent(rel.id, id -> new ArrayList<>()).add(component.line);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
LOGGER.warn("Cannot extract boundary line from " + feature);
|
||||
}
|
||||
} else {
|
||||
features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.ADMIN_LEVEL, minAdminLevel)
|
||||
.setAttr(Fields.DISPUTED, disputed ? 1 : 0)
|
||||
.setAttr(Fields.MARITIME, maritime ? 1 : 0)
|
||||
.setMinPixelSizeAtAllZooms(0)
|
||||
.setMinZoom(minzoom)
|
||||
.setAttr(Fields.CLAIMED_BY, claimedBy)
|
||||
.setAttr(Fields.DISPUTED_NAME, editName(disputedName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish(String sourceName, FeatureCollector.Factory featureCollectors,
|
||||
Consumer<FeatureCollector.Feature> emit) {
|
||||
if (OpenMapTilesProfile.OSM_SOURCE.equals(sourceName)) {
|
||||
var timer = stats.startStage("boundaries");
|
||||
LongObjectMap<PreparedGeometry> countryBoundaries = prepareRegionPolygons();
|
||||
|
||||
for (var entry : boundariesToMerge.entrySet()) {
|
||||
CountryBoundaryComponent key = entry.getKey();
|
||||
LineMerger merger = new LineMerger();
|
||||
for (Geometry geom : entry.getValue()) {
|
||||
merger.add(geom);
|
||||
}
|
||||
entry.getValue().clear();
|
||||
for (Object merged : merger.getMergedLineStrings()) {
|
||||
if (merged instanceof LineString lineString) {
|
||||
BorderingRegions borderingRegions = getBorderingRegions(countryBoundaries, key.regions, lineString);
|
||||
|
||||
var features = featureCollectors.get(SimpleFeature.fromWorldGeometry(lineString));
|
||||
features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.ADMIN_LEVEL, key.adminLevel)
|
||||
.setAttr(Fields.DISPUTED, key.disputed ? 1 : 0)
|
||||
.setAttr(Fields.MARITIME, key.maritime ? 1 : 0)
|
||||
.setAttr(Fields.CLAIMED_BY, key.claimedBy)
|
||||
.setAttr(Fields.DISPUTED_NAME, key.disputed ? editName(key.name) : null)
|
||||
.setAttr(Fields.ADM0_L, borderingRegions.left == null ? null : regionNames.get(borderingRegions.left))
|
||||
.setAttr(Fields.ADM0_R, borderingRegions.right == null ? null : regionNames.get(borderingRegions.right))
|
||||
.setMinPixelSizeAtAllZooms(0)
|
||||
.setMinZoom(key.minzoom);
|
||||
for (var feature : features) {
|
||||
emit.accept(feature);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
timer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
double minLength = config.minFeatureSize(zoom);
|
||||
double tolerance = config.tolerance(zoom);
|
||||
return FeatureMerge.mergeLineStrings(items, attrs -> minLength, tolerance, BUFFER_SIZE);
|
||||
}
|
||||
|
||||
/** Returns the left and right country for {@code lineString}. */
|
||||
private BorderingRegions getBorderingRegions(
|
||||
LongObjectMap<PreparedGeometry> countryBoundaries,
|
||||
Set<Long> allRegions,
|
||||
LineString lineString
|
||||
) {
|
||||
Set<Long> validRegions = allRegions.stream()
|
||||
.filter(countryBoundaries::containsKey)
|
||||
.collect(Collectors.toSet());
|
||||
if (validRegions.isEmpty()) {
|
||||
return BorderingRegions.empty();
|
||||
}
|
||||
List<Long> rights = new ArrayList<>();
|
||||
List<Long> lefts = new ArrayList<>();
|
||||
int steps = 10;
|
||||
for (int i = 0; i < steps; i++) {
|
||||
double ratio = (double) (i + 1) / (steps + 2);
|
||||
Point right = GeoUtils.pointAlongOffset(lineString, ratio, COUNTRY_TEST_OFFSET);
|
||||
Point left = GeoUtils.pointAlongOffset(lineString, ratio, -COUNTRY_TEST_OFFSET);
|
||||
for (Long regionId : validRegions) {
|
||||
PreparedGeometry geom = countryBoundaries.get(regionId);
|
||||
if (geom != null) {
|
||||
if (geom.contains(right)) {
|
||||
rights.add(regionId);
|
||||
} else if (geom.contains(left)) {
|
||||
lefts.add(regionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var right = mode(rights);
|
||||
if (right != null) {
|
||||
lefts.removeAll(List.of(right));
|
||||
}
|
||||
var left = mode(lefts);
|
||||
|
||||
if (left == null && right == null) {
|
||||
Coordinate point = GeoUtils.worldToLatLonCoords(GeoUtils.pointAlongOffset(lineString, 0.5, 0)).getCoordinate();
|
||||
LOGGER.warn("no left or right country for border between OSM country relations: %s around %s"
|
||||
.formatted(
|
||||
validRegions,
|
||||
Format.osmDebugUrl(10, point)
|
||||
));
|
||||
}
|
||||
|
||||
return new BorderingRegions(left, right);
|
||||
}
|
||||
|
||||
/** Returns a map from region ID to prepared geometry optimized for {@code contains} queries. */
|
||||
private LongObjectMap<PreparedGeometry> prepareRegionPolygons() {
|
||||
LOGGER.info("Creating polygons for " + regionGeometries.size() + " boundaries");
|
||||
LongObjectMap<PreparedGeometry> countryBoundaries = Hppc.newLongObjectHashMap();
|
||||
for (var entry : regionGeometries.entrySet()) {
|
||||
Long regionId = entry.getKey();
|
||||
Polygonizer polygonizer = new Polygonizer();
|
||||
polygonizer.add(entry.getValue());
|
||||
try {
|
||||
Geometry combined = polygonizer.getGeometry().union();
|
||||
if (combined.isEmpty()) {
|
||||
LOGGER.warn("Unable to form closed polygon for OSM relation " + regionId + " (likely missing edges)");
|
||||
} else {
|
||||
countryBoundaries.put(regionId, PreparedGeometryFactory.prepare(combined));
|
||||
}
|
||||
} catch (TopologyException e) {
|
||||
LOGGER
|
||||
.warn("Unable to build boundary polygon for OSM relation " + regionId + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
LOGGER.info("Finished creating " + countryBoundaries.size() + " country polygons");
|
||||
return countryBoundaries;
|
||||
}
|
||||
|
||||
/** Returns most frequently-occurring element in {@code list}. */
|
||||
private static Long mode(List<Long> list) {
|
||||
return list.stream()
|
||||
.collect(groupingBy(Function.identity(), counting())).entrySet().stream()
|
||||
.max(Map.Entry.comparingByValue())
|
||||
.map(Map.Entry::getKey)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private record BorderingRegions(Long left, Long right) {
|
||||
|
||||
public static BorderingRegions empty() {
|
||||
return new BorderingRegions(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal set of information extracted from a boundary relation to be used when processing each way in that relation.
|
||||
*/
|
||||
private record BoundaryRelation(
|
||||
long id,
|
||||
int adminLevel,
|
||||
boolean disputed,
|
||||
String name,
|
||||
String claimedBy,
|
||||
String iso3166alpha3
|
||||
) implements OsmRelationInfo {
|
||||
|
||||
@Override
|
||||
public long estimateMemoryUsageBytes() {
|
||||
return CLASS_HEADER_BYTES + MemoryEstimator.estimateSizeLong(id) + MemoryEstimator.estimateSizeInt(adminLevel) +
|
||||
estimateSize(disputed) + POINTER_BYTES + estimateSize(name) + POINTER_BYTES + estimateSize(claimedBy) +
|
||||
POINTER_BYTES + estimateSize(iso3166alpha3);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Information to hold onto from processing a way in a boundary relation to determine the left/right region ID later.
|
||||
*/
|
||||
private record CountryBoundaryComponent(
|
||||
int adminLevel,
|
||||
boolean disputed,
|
||||
boolean maritime,
|
||||
int minzoom,
|
||||
Geometry line,
|
||||
Set<Long> regions,
|
||||
String claimedBy,
|
||||
String name
|
||||
) {
|
||||
|
||||
CountryBoundaryComponent groupingKey() {
|
||||
return new CountryBoundaryComponent(adminLevel, disputed, maritime, minzoom, null, regions, claimedBy, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
191
src/main/java/org/openmaptiles/layers/Building.java
Normal file
191
src/main/java/org/openmaptiles/layers/Building.java
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
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 com.onthegomap.planetiler.util.MemoryEstimator.CLASS_HEADER_BYTES;
|
||||
import static com.onthegomap.planetiler.util.Parse.parseDoubleOrNull;
|
||||
import static java.util.Map.entry;
|
||||
import static org.openmaptiles.util.Utils.coalesce;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmElement;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.MemoryEstimator;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import org.openmaptiles.OpenMapTilesProfile;
|
||||
import org.openmaptiles.generated.OpenMapTilesSchema;
|
||||
import org.openmaptiles.generated.Tables;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for buildings in the {@code building} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/building">OpenMapTiles building sql
|
||||
* files</a>.
|
||||
*/
|
||||
public class Building implements
|
||||
OpenMapTilesSchema.Building,
|
||||
Tables.OsmBuildingPolygon.Handler,
|
||||
OpenMapTilesProfile.FeaturePostProcessor,
|
||||
OpenMapTilesProfile.OsmRelationPreprocessor {
|
||||
|
||||
/*
|
||||
* Emit all buildings from OSM data at z14.
|
||||
*
|
||||
* At z13, emit all buildings at process-time, but then at tile render-time,
|
||||
* merge buildings that are overlapping or almost touching into combined
|
||||
* buildings so that entire city blocks show up as a single building polygon.
|
||||
*
|
||||
* THIS IS VERY EXPENSIVE! Merging buildings at z13 adds about 50% to the
|
||||
* total map generation time. To disable it, set building_merge_z13 argument
|
||||
* to false.
|
||||
*/
|
||||
|
||||
private static final Map<String, String> MATERIAL_COLORS = Map.ofEntries(
|
||||
entry("cement_block", "#6a7880"),
|
||||
entry("brick", "#bd8161"),
|
||||
entry("plaster", "#dadbdb"),
|
||||
entry("wood", "#d48741"),
|
||||
entry("concrete", "#d3c2b0"),
|
||||
entry("metal", "#b7b1a6"),
|
||||
entry("stone", "#b4a995"),
|
||||
entry("mud", "#9d8b75"),
|
||||
entry("steel", "#b7b1a6"), // same as metal
|
||||
entry("glass", "#5a81a0"),
|
||||
entry("traditional", "#bd8161"), // same as brick
|
||||
entry("masonry", "#bd8161"), // same as brick
|
||||
entry("Brick", "#bd8161"), // same as brick
|
||||
entry("tin", "#b7b1a6"), // same as metal
|
||||
entry("timber_framing", "#b3b0a9"),
|
||||
entry("sandstone", "#b4a995"), // same as stone
|
||||
entry("clay", "#9d8b75") // same as mud
|
||||
);
|
||||
private final boolean mergeZ13Buildings;
|
||||
|
||||
public Building(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.mergeZ13Buildings = config.arguments().getBoolean(
|
||||
"building_merge_z13",
|
||||
"building layer: merge nearby buildings at z13",
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OsmRelationInfo> preprocessOsmRelation(OsmElement.Relation relation) {
|
||||
if (relation.hasTag("type", "building")) {
|
||||
return List.of(new BuildingRelationInfo(relation.id()));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmBuildingPolygon element, FeatureCollector features) {
|
||||
Boolean hide3d = null;
|
||||
var relations = element.source().relationInfo(BuildingRelationInfo.class);
|
||||
for (var relation : relations) {
|
||||
if ("outline".equals(relation.role())) {
|
||||
hide3d = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
String color = element.colour();
|
||||
if (color == null && element.material() != null) {
|
||||
color = MATERIAL_COLORS.get(element.material());
|
||||
}
|
||||
if (color != null) {
|
||||
color = color.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
Double height = coalesce(
|
||||
parseDoubleOrNull(element.height()),
|
||||
parseDoubleOrNull(element.buildingheight())
|
||||
);
|
||||
Double minHeight = coalesce(
|
||||
parseDoubleOrNull(element.minHeight()),
|
||||
parseDoubleOrNull(element.buildingminHeight())
|
||||
);
|
||||
Double levels = coalesce(
|
||||
parseDoubleOrNull(element.levels()),
|
||||
parseDoubleOrNull(element.buildinglevels())
|
||||
);
|
||||
Double minLevels = coalesce(
|
||||
parseDoubleOrNull(element.minLevel()),
|
||||
parseDoubleOrNull(element.buildingminLevel())
|
||||
);
|
||||
|
||||
int renderHeight = (int) Math.ceil(height != null ? height : levels != null ? (levels * 3.66) : 5);
|
||||
int renderMinHeight = (int) Math.floor(minHeight != null ? minHeight : minLevels != null ? (minLevels * 3.66) : 0);
|
||||
|
||||
if (renderHeight < 3660 && renderMinHeight < 3660) {
|
||||
var feature = features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setMinZoom(13)
|
||||
.setMinPixelSize(2)
|
||||
.setAttrWithMinzoom(Fields.RENDER_HEIGHT, renderHeight, 14)
|
||||
.setAttrWithMinzoom(Fields.RENDER_MIN_HEIGHT, renderMinHeight, 14)
|
||||
.setAttrWithMinzoom(Fields.COLOUR, color, 14)
|
||||
.setAttrWithMinzoom(Fields.HIDE_3D, hide3d, 14)
|
||||
.setSortKey(renderHeight);
|
||||
if (mergeZ13Buildings) {
|
||||
feature
|
||||
.setMinPixelSize(0.1)
|
||||
.setPixelTolerance(0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom,
|
||||
List<VectorTile.Feature> items) throws GeometryException {
|
||||
return (mergeZ13Buildings && zoom == 13) ? FeatureMerge.mergeNearbyPolygons(items, 4, 4, 0.5, 0.5) : items;
|
||||
}
|
||||
|
||||
private record BuildingRelationInfo(long id) implements OsmRelationInfo {
|
||||
|
||||
@Override
|
||||
public long estimateMemoryUsageBytes() {
|
||||
return CLASS_HEADER_BYTES + MemoryEstimator.estimateSizeLong(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/main/java/org/openmaptiles/layers/Housenumber.java
Normal file
65
src/main/java/org/openmaptiles/layers/Housenumber.java
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
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 com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import org.openmaptiles.generated.OpenMapTilesSchema;
|
||||
import org.openmaptiles.generated.Tables;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements in the {@code housenumber} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/housenumber">OpenMapTiles housenumber sql
|
||||
* files</a>.
|
||||
*/
|
||||
public class Housenumber implements
|
||||
OpenMapTilesSchema.Housenumber,
|
||||
Tables.OsmHousenumberPoint.Handler {
|
||||
|
||||
public Housenumber(Translations translations, PlanetilerConfig config, Stats stats) {}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmHousenumberPoint element, FeatureCollector features) {
|
||||
features.centroidIfConvex(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.HOUSENUMBER, element.housenumber())
|
||||
.setMinZoom(14);
|
||||
}
|
||||
}
|
||||
183
src/main/java/org/openmaptiles/layers/Landcover.java
Normal file
183
src/main/java/org/openmaptiles/layers/Landcover.java
Normal file
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
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 com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import com.onthegomap.planetiler.util.ZoomFunction;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.openmaptiles.OpenMapTilesProfile;
|
||||
import org.openmaptiles.generated.OpenMapTilesSchema;
|
||||
import org.openmaptiles.generated.Tables;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for natural land cover polygons like ice, sand, and forest in the
|
||||
* {@code landcover} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/landcover">OpenMapTiles landcover sql
|
||||
* files</a>.
|
||||
*/
|
||||
public class Landcover implements
|
||||
OpenMapTilesSchema.Landcover,
|
||||
OpenMapTilesProfile.NaturalEarthProcessor,
|
||||
Tables.OsmLandcoverPolygon.Handler,
|
||||
OpenMapTilesProfile.FeaturePostProcessor {
|
||||
|
||||
/*
|
||||
* Large ice areas come from natural earth and the rest come from OpenStreetMap at higher zoom
|
||||
* levels. At render-time, postProcess() merges polygons into larger connected area based
|
||||
* on the number of points in the original area. Since postProcess() only has visibility into
|
||||
* features on a single tile, process() needs to pass the number of points the original feature
|
||||
* had through using a temporary "_numpoints" attribute.
|
||||
*/
|
||||
|
||||
public static final ZoomFunction<Number> MIN_PIXEL_SIZE_THRESHOLDS = ZoomFunction.fromMaxZoomThresholds(Map.of(
|
||||
13, 8,
|
||||
10, 4,
|
||||
9, 2
|
||||
));
|
||||
private static final String TEMP_NUM_POINTS_ATTR = "_numpoints";
|
||||
private static final Set<String> WOOD_OR_FOREST = Set.of(
|
||||
FieldValues.SUBCLASS_WOOD,
|
||||
FieldValues.SUBCLASS_FOREST
|
||||
);
|
||||
private final MultiExpression.Index<String> classMapping;
|
||||
|
||||
public Landcover(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.classMapping = FieldMappings.Class.index();
|
||||
}
|
||||
|
||||
private String getClassFromSubclass(String subclass) {
|
||||
return subclass == null ? null : classMapping.getOrElse(Map.of(Fields.SUBCLASS, subclass), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processNaturalEarth(String table, SourceFeature feature,
|
||||
FeatureCollector features) {
|
||||
record LandcoverInfo(String subclass, int minzoom, int maxzoom) {}
|
||||
LandcoverInfo info = switch (table) {
|
||||
case "ne_110m_glaciated_areas" -> new LandcoverInfo(FieldValues.SUBCLASS_GLACIER, 0, 1);
|
||||
case "ne_50m_glaciated_areas" -> new LandcoverInfo(FieldValues.SUBCLASS_GLACIER, 2, 4);
|
||||
case "ne_10m_glaciated_areas" -> new LandcoverInfo(FieldValues.SUBCLASS_GLACIER, 5, 6);
|
||||
case "ne_50m_antarctic_ice_shelves_polys" -> new LandcoverInfo("ice_shelf", 2, 4);
|
||||
case "ne_10m_antarctic_ice_shelves_polys" -> new LandcoverInfo("ice_shelf", 5, 6);
|
||||
default -> null;
|
||||
};
|
||||
if (info != null) {
|
||||
String clazz = getClassFromSubclass(info.subclass);
|
||||
if (clazz != null) {
|
||||
features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
.setAttr(Fields.SUBCLASS, info.subclass)
|
||||
.setZoomRange(info.minzoom, info.maxzoom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmLandcoverPolygon element, FeatureCollector features) {
|
||||
String subclass = element.subclass();
|
||||
String clazz = getClassFromSubclass(subclass);
|
||||
if (clazz != null) {
|
||||
features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setMinPixelSizeOverrides(MIN_PIXEL_SIZE_THRESHOLDS)
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
.setAttr(Fields.SUBCLASS, subclass)
|
||||
.setNumPointsAttr(TEMP_NUM_POINTS_ATTR)
|
||||
.setMinZoom(7);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) throws GeometryException {
|
||||
if (zoom < 7 || zoom > 13) {
|
||||
for (var item : items) {
|
||||
item.attrs().remove(TEMP_NUM_POINTS_ATTR);
|
||||
}
|
||||
return items;
|
||||
} else { // z7-13
|
||||
// merging only merges polygons with the same attributes, so use this temporary key
|
||||
// to separate features into layers that will be merged separately
|
||||
String tempGroupKey = "_group";
|
||||
List<VectorTile.Feature> result = new ArrayList<>();
|
||||
List<VectorTile.Feature> toMerge = new ArrayList<>();
|
||||
for (var item : items) {
|
||||
Map<String, Object> attrs = item.attrs();
|
||||
Object numPointsObj = attrs.remove(TEMP_NUM_POINTS_ATTR);
|
||||
Object subclassObj = attrs.get(Fields.SUBCLASS);
|
||||
if (numPointsObj instanceof Number num && subclassObj instanceof String subclass) {
|
||||
long numPoints = num.longValue();
|
||||
if (zoom >= 10) {
|
||||
if (WOOD_OR_FOREST.contains(subclass) && numPoints < 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 < 300 ? "<300" : ">300");
|
||||
toMerge.add(item);
|
||||
} else { // don't merge
|
||||
result.add(item);
|
||||
}
|
||||
} else { // zoom between 7 and 8
|
||||
toMerge.add(item);
|
||||
}
|
||||
} else {
|
||||
result.add(item);
|
||||
}
|
||||
}
|
||||
var merged = FeatureMerge.mergeOverlappingPolygons(toMerge, 4);
|
||||
for (var item : merged) {
|
||||
item.attrs().remove(tempGroupKey);
|
||||
}
|
||||
result.addAll(merged);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
112
src/main/java/org/openmaptiles/layers/Landuse.java
Normal file
112
src/main/java/org/openmaptiles/layers/Landuse.java
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
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.util.Utils.coalesce;
|
||||
import static org.openmaptiles.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
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 com.onthegomap.planetiler.util.ZoomFunction;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.openmaptiles.OpenMapTilesProfile;
|
||||
import org.openmaptiles.generated.OpenMapTilesSchema;
|
||||
import org.openmaptiles.generated.Tables;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for man-made land use polygons like cemeteries, zoos, and hospitals in
|
||||
* the {@code landuse} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/landuse">OpenMapTiles landuse sql files</a>.
|
||||
*/
|
||||
public class Landuse implements
|
||||
OpenMapTilesSchema.Landuse,
|
||||
OpenMapTilesProfile.NaturalEarthProcessor,
|
||||
Tables.OsmLandusePolygon.Handler {
|
||||
|
||||
private static final ZoomFunction<Number> MIN_PIXEL_SIZE_THRESHOLDS = ZoomFunction.fromMaxZoomThresholds(Map.of(
|
||||
13, 4,
|
||||
7, 2,
|
||||
6, 1
|
||||
));
|
||||
private static final Set<String> Z6_CLASSES = Set.of(
|
||||
FieldValues.CLASS_RESIDENTIAL,
|
||||
FieldValues.CLASS_SUBURB,
|
||||
FieldValues.CLASS_QUARTER,
|
||||
FieldValues.CLASS_NEIGHBOURHOOD
|
||||
);
|
||||
|
||||
public Landuse(Translations translations, PlanetilerConfig config, Stats stats) {}
|
||||
|
||||
@Override
|
||||
public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) {
|
||||
if ("ne_50m_urban_areas".equals(table)) {
|
||||
Double scalerank = Parse.parseDoubleOrNull(feature.getTag("scalerank"));
|
||||
if (scalerank != null && scalerank <= 2) {
|
||||
features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_RESIDENTIAL)
|
||||
.setZoomRange(4, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmLandusePolygon element, FeatureCollector features) {
|
||||
String clazz = coalesce(
|
||||
nullIfEmpty(element.landuse()),
|
||||
nullIfEmpty(element.amenity()),
|
||||
nullIfEmpty(element.leisure()),
|
||||
nullIfEmpty(element.tourism()),
|
||||
nullIfEmpty(element.place()),
|
||||
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)
|
||||
.setMinZoom(Z6_CLASSES.contains(clazz) ? 6 : 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
203
src/main/java/org/openmaptiles/layers/MountainPeak.java
Normal file
203
src/main/java/org/openmaptiles/layers/MountainPeak.java
Normal file
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
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.util.Utils.elevationTags;
|
||||
import static org.openmaptiles.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.carrotsearch.hppc.LongIntMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.collection.Hppc;
|
||||
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.openmaptiles.OpenMapTilesProfile;
|
||||
import org.openmaptiles.generated.OpenMapTilesSchema;
|
||||
import org.openmaptiles.generated.Tables;
|
||||
import org.openmaptiles.util.OmtLanguageUtils;
|
||||
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
|
||||
* source features.
|
||||
* <p>
|
||||
* This class is ported to Java from
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/mountain_peak">OpenMapTiles mountain_peak
|
||||
* sql files</a>.
|
||||
*/
|
||||
public class MountainPeak implements
|
||||
OpenMapTilesProfile.NaturalEarthProcessor,
|
||||
OpenMapTilesSchema.MountainPeak,
|
||||
Tables.OsmPeakPoint.Handler,
|
||||
Tables.OsmMountainLinestring.Handler,
|
||||
OpenMapTilesProfile.FeaturePostProcessor {
|
||||
|
||||
/*
|
||||
* Mountain peaks come from OpenStreetMap data and are ranked by importance (based on if they
|
||||
* have a name or wikipedia page) then by elevation. Uses the "label grid" feature to limit
|
||||
* label density by only taking the top 5 most important mountain peaks within each 100x100px
|
||||
* square.
|
||||
*/
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(MountainPeak.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) {
|
||||
Double meters = Parse.meters(element.ele());
|
||||
if (meters != null && Math.abs(meters) < 10_000) {
|
||||
var feature = features.point(LAYER_NAME)
|
||||
.setAttr(Fields.CLASS, element.source().getTag("natural"))
|
||||
.putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations))
|
||||
.putAttrs(elevationTags(meters))
|
||||
.setSortKeyDescending(
|
||||
meters.intValue() +
|
||||
(nullIfEmpty(element.wikipedia()) != null ? 10_000 : 0) +
|
||||
(nullIfEmpty(element.name()) != null ? 10_000 : 0)
|
||||
)
|
||||
.setMinZoom(7)
|
||||
// need to use a larger buffer size to allow enough points through to not cut off
|
||||
// any label grid squares which could lead to inconsistent label ranks for a feature
|
||||
// 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(OmtLanguageUtils.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 = Hppc.newLongIntHashMap();
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
VectorTile.Feature feature = items.get(i);
|
||||
int gridrank = groupCounts.getOrDefault(feature.group(), 1);
|
||||
groupCounts.put(feature.group(), gridrank + 1);
|
||||
// now that we have accurate ranks, remove anything outside the desired buffer
|
||||
if (!insideTileBuffer(feature)) {
|
||||
items.set(i, null);
|
||||
} else if (!feature.attrs().containsKey(Fields.RANK)) {
|
||||
feature.attrs().put(Fields.RANK, gridrank);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private static boolean insideTileBuffer(double xOrY) {
|
||||
return xOrY >= -BUFFER_SIZE && xOrY <= 256 + BUFFER_SIZE;
|
||||
}
|
||||
|
||||
private boolean insideTileBuffer(VectorTile.Feature feature) {
|
||||
try {
|
||||
Geometry geom = feature.geometry().decode();
|
||||
return !(geom instanceof Point point) || (insideTileBuffer(point.getX()) && insideTileBuffer(point.getY()));
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "mountain_peak_decode_point", "Error decoding mountain peak point: " + feature.attrs());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
161
src/main/java/org/openmaptiles/layers/Park.java
Normal file
161
src/main/java/org/openmaptiles/layers/Park.java
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
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 com.onthegomap.planetiler.collection.FeatureGroup.SORT_KEY_BITS;
|
||||
import static org.openmaptiles.util.Utils.coalesce;
|
||||
import static org.openmaptiles.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.carrotsearch.hppc.LongIntMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.collection.Hppc;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.geo.GeometryType;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.SortKey;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
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 designated parks polygons and their label points in the {@code
|
||||
* park} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/park">OpenMapTiles park sql files</a>.
|
||||
*/
|
||||
public class Park implements
|
||||
OpenMapTilesSchema.Park,
|
||||
Tables.OsmParkPolygon.Handler,
|
||||
OpenMapTilesProfile.FeaturePostProcessor {
|
||||
|
||||
// constants for packing the minimum zoom ordering of park labels into the sort-key field
|
||||
private static final int PARK_NATIONAL_PARK_BOOST = 1 << (SORT_KEY_BITS - 1);
|
||||
private static final int PARK_WIKIPEDIA_BOOST = 1 << (SORT_KEY_BITS - 2);
|
||||
|
||||
// constants for determining the minimum zoom level for a park label based on its area
|
||||
private static final double WORLD_AREA_FOR_70K_SQUARE_METERS =
|
||||
Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2);
|
||||
private static final double LOG2 = Math.log(2);
|
||||
private static final int PARK_AREA_RANGE = 1 << (SORT_KEY_BITS - 3);
|
||||
private static final double SMALLEST_PARK_WORLD_AREA = Math.pow(4, -26); // 2^14 tiles, 2^12 pixels per tile
|
||||
|
||||
private final Translations translations;
|
||||
private final Stats stats;
|
||||
|
||||
public Park(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.stats = stats;
|
||||
this.translations = translations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmParkPolygon element, FeatureCollector features) {
|
||||
String protectionTitle = element.protectionTitle();
|
||||
if (protectionTitle != null) {
|
||||
protectionTitle = protectionTitle.replace(' ', '_').toLowerCase(Locale.ROOT);
|
||||
}
|
||||
String clazz = coalesce(
|
||||
nullIfEmpty(protectionTitle),
|
||||
nullIfEmpty(element.boundary()),
|
||||
nullIfEmpty(element.leisure())
|
||||
);
|
||||
|
||||
// park shape
|
||||
var outline = features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttrWithMinzoom(Fields.CLASS, clazz, 5)
|
||||
.setMinPixelSize(2)
|
||||
.setMinZoom(4);
|
||||
|
||||
// park name label point (if it has one)
|
||||
if (element.name() != null) {
|
||||
try {
|
||||
double area = element.source().area();
|
||||
int minzoom = getMinZoomForArea(area);
|
||||
|
||||
var names = OmtLanguageUtils.getNamesWithoutTranslations(element.source().tags());
|
||||
|
||||
outline.putAttrsWithMinzoom(names, 5);
|
||||
|
||||
features.pointOnSurface(LAYER_NAME).setBufferPixels(256)
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
.putAttrs(names)
|
||||
.putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setPointLabelGridPixelSize(14, 100)
|
||||
.setSortKey(SortKey
|
||||
.orderByTruesFirst("national_park".equals(clazz))
|
||||
.thenByTruesFirst(element.source().hasTag("wikipedia") || element.source().hasTag("wikidata"))
|
||||
.thenByLog(area, 1d, SMALLEST_PARK_WORLD_AREA, 1 << (SORT_KEY_BITS - 2) - 1)
|
||||
.get()
|
||||
).setMinZoom(minzoom);
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_park_area", "Unable to get park area for " + element.source().id());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getMinZoomForArea(double area) {
|
||||
// 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(5, minzoom));
|
||||
return minzoom;
|
||||
}
|
||||
|
||||
@Override
|
||||
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 = Hppc.newLongIntHashMap();
|
||||
for (VectorTile.Feature feature : items) {
|
||||
if (feature.geometry().geomType() == GeometryType.POINT && feature.hasGroup()) {
|
||||
int count = counts.getOrDefault(feature.group(), 0) + 1;
|
||||
feature.attrs().put("rank", count);
|
||||
counts.put(feature.group(), count);
|
||||
}
|
||||
}
|
||||
if (zoom <= 4) {
|
||||
items = FeatureMerge.mergeOverlappingPolygons(items, 0);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
426
src/main/java/org/openmaptiles/layers/Place.java
Normal file
426
src/main/java/org/openmaptiles/layers/Place.java
Normal file
@@ -0,0 +1,426 @@
|
||||
/*
|
||||
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 com.onthegomap.planetiler.collection.FeatureGroup.SORT_KEY_BITS;
|
||||
import static org.openmaptiles.util.Utils.coalesce;
|
||||
import static org.openmaptiles.util.Utils.nullIfEmpty;
|
||||
import static org.openmaptiles.util.Utils.nullOrEmpty;
|
||||
|
||||
import com.carrotsearch.hppc.LongIntMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.collection.Hppc;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.geo.PointIndex;
|
||||
import com.onthegomap.planetiler.geo.PolygonIndex;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.SortKey;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import com.onthegomap.planetiler.util.ZoomFunction;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.DoubleStream;
|
||||
import java.util.stream.Stream;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.openmaptiles.OpenMapTilesProfile;
|
||||
import org.openmaptiles.generated.OpenMapTilesSchema;
|
||||
import org.openmaptiles.generated.Tables;
|
||||
import org.openmaptiles.util.OmtLanguageUtils;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating label points for populated places like continents, countries, cities, and towns in
|
||||
* the {@code place} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/place">OpenMapTiles place sql files</a>.
|
||||
*/
|
||||
public class Place implements
|
||||
OpenMapTilesSchema.Place,
|
||||
OpenMapTilesProfile.NaturalEarthProcessor,
|
||||
Tables.OsmContinentPoint.Handler,
|
||||
Tables.OsmCountryPoint.Handler,
|
||||
Tables.OsmStatePoint.Handler,
|
||||
Tables.OsmIslandPoint.Handler,
|
||||
Tables.OsmIslandPolygon.Handler,
|
||||
Tables.OsmCityPoint.Handler,
|
||||
OpenMapTilesProfile.FeaturePostProcessor {
|
||||
|
||||
/*
|
||||
* Place labels locations and names come from OpenStreetMap, but we also join with natural
|
||||
* earth state/country geographic areas and city point labels to give a hint for what rank
|
||||
* and minimum zoom level to use for those points.
|
||||
*/
|
||||
|
||||
private static final TreeMap<Double, Integer> ISLAND_AREA_RANKS = new TreeMap<>(Map.of(
|
||||
Double.MAX_VALUE, 3,
|
||||
squareMetersToWorldArea(40_000_000), 4,
|
||||
squareMetersToWorldArea(15_000_000), 5,
|
||||
squareMetersToWorldArea(1_000_000), 6
|
||||
));
|
||||
private static final double MIN_ISLAND_WORLD_AREA = Math.pow(4, -26); // 2^14 tiles, 2^12 pixels per tile
|
||||
private static final double CITY_JOIN_DISTANCE = GeoUtils.metersToPixelAtEquator(0, 50_000) / 256d;
|
||||
// constants for packing place label precedence into the sort-key field
|
||||
private static final double MAX_CITY_POPULATION = 100_000_000d;
|
||||
private static final Set<String> MAJOR_CITY_PLACES = Set.of("city", "town", "village");
|
||||
private static final ZoomFunction<Number> LABEL_GRID_LIMITS = ZoomFunction.fromMaxZoomThresholds(Map.of(
|
||||
8, 4,
|
||||
9, 8,
|
||||
10, 12,
|
||||
12, 14
|
||||
), 0);
|
||||
private final Translations translations;
|
||||
private final Stats stats;
|
||||
// spatial indexes for joining natural earth place labels with their corresponding points
|
||||
// from openstreetmap
|
||||
private PolygonIndex<NaturalEarthRegion> countries = PolygonIndex.create();
|
||||
private PolygonIndex<NaturalEarthRegion> states = PolygonIndex.create();
|
||||
private PointIndex<NaturalEarthPoint> cities = PointIndex.create();
|
||||
|
||||
public Place(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.translations = translations;
|
||||
this.stats = stats;
|
||||
}
|
||||
|
||||
/** Returns the portion of the world that {@code squareMeters} covers where 1 is the entire planet. */
|
||||
private static double squareMetersToWorldArea(double squareMeters) {
|
||||
double oneSideMeters = Math.sqrt(squareMeters);
|
||||
double oneSideWorld = GeoUtils.metersToPixelAtEquator(0, oneSideMeters) / 256d;
|
||||
return Math.pow(oneSideWorld, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs place precedence ordering ({@code rank asc, place asc, population desc, name.length asc}) into an integer for
|
||||
* the sort-key field.
|
||||
*/
|
||||
static int getSortKey(Integer rank, PlaceType place, long population, String name) {
|
||||
return SortKey
|
||||
// ORDER BY "rank" ASC NULLS LAST,
|
||||
.orderByInt(rank == null ? 15 : rank, 0, 15) // 4 bits
|
||||
// place ASC NULLS LAST,
|
||||
.thenByInt(place == null ? 15 : place.ordinal(), 0, 15) // 4 bits
|
||||
// population DESC NULLS LAST,
|
||||
.thenByLog(population, MAX_CITY_POPULATION, 1, 1 << (SORT_KEY_BITS - 13) - 1)
|
||||
// length(name) ASC
|
||||
.thenByInt(name == null ? 0 : name.length(), 0, 31) // 5 bits
|
||||
.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
countries = null;
|
||||
states = null;
|
||||
cities = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) {
|
||||
// store data from natural earth to help with ranks and min zoom levels when actually
|
||||
// emitting features from openstreetmap data.
|
||||
try {
|
||||
switch (table) {
|
||||
case "ne_10m_admin_0_countries" -> countries.put(feature.worldGeometry(), new NaturalEarthRegion(
|
||||
feature.getString("name"), 6,
|
||||
feature.getLong("scalerank"),
|
||||
feature.getLong("labelrank")
|
||||
));
|
||||
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 <= 6 && labelrank != null && labelrank <= 7) {
|
||||
states.put(feature.worldGeometry(), new NaturalEarthRegion(
|
||||
feature.getString("name"), 6,
|
||||
scalerank,
|
||||
labelrank,
|
||||
feature.getLong("datarank")
|
||||
));
|
||||
}
|
||||
}
|
||||
case "ne_10m_populated_places" -> cities.put(feature.worldGeometry(), new NaturalEarthPoint(
|
||||
feature.getString("name"),
|
||||
feature.getString("wikidataid"),
|
||||
(int) feature.getLong("scalerank"),
|
||||
Stream.of("name", "namealt", "meganame", "gn_ascii", "nameascii").map(feature::getString)
|
||||
.filter(Objects::nonNull)
|
||||
.map(s -> s.toLowerCase(Locale.ROOT))
|
||||
.collect(Collectors.toSet())
|
||||
));
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_place_ne",
|
||||
"Error getting geometry for natural earth feature " + table + " " + feature.getTag("ogc_fid"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmContinentPoint element, FeatureCollector features) {
|
||||
if (!nullOrEmpty(element.name())) {
|
||||
features.point(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_CONTINENT)
|
||||
.setAttr(Fields.RANK, 1)
|
||||
.putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setZoomRange(0, 3);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmCountryPoint element, FeatureCollector features) {
|
||||
if (nullOrEmpty(element.name())) {
|
||||
return;
|
||||
}
|
||||
String isoA2 = coalesce(
|
||||
nullIfEmpty(element.countryCodeIso31661Alpha2()),
|
||||
nullIfEmpty(element.iso31661Alpha2()),
|
||||
nullIfEmpty(element.iso31661())
|
||||
);
|
||||
if (isoA2 == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// set country rank to 6, unless there is a match in natural earth that indicates it
|
||||
// should be lower
|
||||
int rank = 7;
|
||||
NaturalEarthRegion country = countries.get(element.source().worldGeometry().getCentroid());
|
||||
var names = OmtLanguageUtils.getNames(element.source().tags(), translations);
|
||||
|
||||
if (country != null) {
|
||||
if (nullOrEmpty(names.get(Fields.NAME_EN))) {
|
||||
names.put(Fields.NAME_EN, country.name);
|
||||
}
|
||||
rank = country.rank;
|
||||
}
|
||||
|
||||
rank = Math.max(1, Math.min(6, rank));
|
||||
|
||||
features.point(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(names)
|
||||
.setAttr(Fields.ISO_A2, isoA2)
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_COUNTRY)
|
||||
.setAttr(Fields.RANK, rank)
|
||||
.setMinZoom(rank - 1)
|
||||
.setSortKey(rank);
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_place_country",
|
||||
"Unable to get point for OSM country " + element.source().id());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmStatePoint element, FeatureCollector features) {
|
||||
try {
|
||||
// want the containing (not nearest) state polygon since we pre-filter the states in the polygon index
|
||||
// use natural earth to filter out any spurious states, and to set the rank field
|
||||
NaturalEarthRegion state = states.getOnlyContaining(element.source().worldGeometry().getCentroid());
|
||||
if (state != null) {
|
||||
var names = OmtLanguageUtils.getNames(element.source().tags(), translations);
|
||||
if (nullOrEmpty(names.get(Fields.NAME_EN))) {
|
||||
names.put(Fields.NAME_EN, state.name);
|
||||
}
|
||||
int rank = Math.min(6, Math.max(1, state.rank));
|
||||
|
||||
features.point(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(names)
|
||||
.setAttr(Fields.CLASS, element.place())
|
||||
.setAttr(Fields.RANK, rank)
|
||||
.setMinZoom(2)
|
||||
.setSortKey(rank);
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_place_state",
|
||||
"Unable to get point for OSM state " + element.source().id());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmIslandPolygon element, FeatureCollector features) {
|
||||
try {
|
||||
double area = element.source().area();
|
||||
int rank = ISLAND_AREA_RANKS.ceilingEntry(area).getValue();
|
||||
int minzoom = rank <= 3 ? 8 : rank <= 4 ? 9 : 10;
|
||||
|
||||
features.pointOnSurface(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setAttr(Fields.CLASS, "island")
|
||||
.setAttr(Fields.RANK, rank)
|
||||
.setMinZoom(minzoom)
|
||||
.setSortKey(SortKey.orderByLog(area, 1d, MIN_ISLAND_WORLD_AREA).get());
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_place_island_poly",
|
||||
"Unable to get point for OSM island polygon " + element.source().id());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmIslandPoint element, FeatureCollector features) {
|
||||
features.point(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setAttr(Fields.CLASS, "island")
|
||||
.setAttr(Fields.RANK, 7)
|
||||
.setMinZoom(12);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmCityPoint element, FeatureCollector features) {
|
||||
Integer rank = null;
|
||||
if (MAJOR_CITY_PLACES.contains(element.place())) {
|
||||
// only for major cities, attempt to find a nearby natural earth label with a similar
|
||||
// name and use that to set a rank from OSM that causes the label to be shown at lower
|
||||
// zoom levels
|
||||
try {
|
||||
Point point = element.source().worldGeometry().getCentroid();
|
||||
List<NaturalEarthPoint> neCities = cities.getWithin(point, CITY_JOIN_DISTANCE);
|
||||
String rawName = coalesce(element.name(), "");
|
||||
String name = coalesce(rawName, "").toLowerCase(Locale.ROOT);
|
||||
String nameEn = coalesce(element.nameEn(), "").toLowerCase(Locale.ROOT);
|
||||
String normalizedName = StringUtils.stripAccents(rawName);
|
||||
String wikidata = element.source().getString("wikidata", "");
|
||||
for (var neCity : neCities) {
|
||||
if (wikidata.equals(neCity.wikidata) ||
|
||||
neCity.names.contains(name) ||
|
||||
neCity.names.contains(nameEn) ||
|
||||
normalizedName.equals(neCity.name)) {
|
||||
rank = neCity.scaleRank <= 5 ? neCity.scaleRank + 1 : neCity.scaleRank;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_place_city",
|
||||
"Unable to get point for OSM city " + element.source().id());
|
||||
}
|
||||
}
|
||||
|
||||
String capital = element.capital();
|
||||
|
||||
PlaceType placeType = PlaceType.forName(element.place());
|
||||
|
||||
int minzoom = rank != null && rank == 1 ? 2 :
|
||||
rank != null && rank <= 8 ? Math.max(3, rank - 1) :
|
||||
placeType.ordinal() <= PlaceType.TOWN.ordinal() ? 7 :
|
||||
placeType.ordinal() <= PlaceType.VILLAGE.ordinal() ? 8 :
|
||||
placeType.ordinal() <= PlaceType.SUBURB.ordinal() ? 11 : 14;
|
||||
|
||||
var feature = features.point(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setAttr(Fields.CLASS, element.place())
|
||||
.setAttr(Fields.RANK, rank)
|
||||
.setMinZoom(minzoom)
|
||||
.setSortKey(getSortKey(rank, placeType, element.population(), element.name()))
|
||||
.setPointLabelGridPixelSize(12, 128);
|
||||
|
||||
if (rank == null) {
|
||||
feature.setPointLabelGridLimit(LABEL_GRID_LIMITS);
|
||||
}
|
||||
|
||||
if ("2".equals(capital) || "yes".equals(capital)) {
|
||||
feature.setAttr(Fields.CAPITAL, 2);
|
||||
} else if ("4".equals(capital)) {
|
||||
feature.setAttr(Fields.CAPITAL, 4);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
// infer the rank field from ordering of the place labels with each label grid square
|
||||
LongIntMap groupCounts = Hppc.newLongIntHashMap();
|
||||
for (VectorTile.Feature feature : items) {
|
||||
int gridrank = groupCounts.getOrDefault(feature.group(), 1);
|
||||
groupCounts.put(feature.group(), gridrank + 1);
|
||||
if (!feature.attrs().containsKey(Fields.RANK)) {
|
||||
feature.attrs().put(Fields.RANK, 10 + gridrank);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/** Ordering defines the precedence of place classes. */
|
||||
enum PlaceType {
|
||||
CITY("city"),
|
||||
TOWN("town"),
|
||||
VILLAGE("village"),
|
||||
HAMLET("hamlet"),
|
||||
SUBURB("suburb"),
|
||||
QUARTER("quarter"),
|
||||
NEIGHBORHOOD("neighbourhood"),
|
||||
ISOLATED_DWELLING("isolated_dwelling"),
|
||||
UNKNOWN("unknown");
|
||||
|
||||
private static final Map<String, PlaceType> byName = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (PlaceType place : values()) {
|
||||
byName.put(place.name, place);
|
||||
}
|
||||
}
|
||||
|
||||
private final String name;
|
||||
|
||||
PlaceType(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public static PlaceType forName(String name) {
|
||||
return byName.getOrDefault(name, UNKNOWN);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Information extracted from a natural earth geographic region that will be inspected when joining with OpenStreetMap
|
||||
* data.
|
||||
*/
|
||||
private record NaturalEarthRegion(String name, int rank) {
|
||||
|
||||
NaturalEarthRegion(String name, int maxRank, double... ranks) {
|
||||
this(name, (int) Math.ceil(DoubleStream.of(ranks).average().orElse(maxRank)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Information extracted from a natural earth place label that will be inspected when joining with OpenStreetMap data.
|
||||
*/
|
||||
private record NaturalEarthPoint(String name, String wikidata, int scaleRank, Set<String> names) {}
|
||||
}
|
||||
194
src/main/java/org/openmaptiles/layers/Poi.java
Normal file
194
src/main/java/org/openmaptiles/layers/Poi.java
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
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 java.util.Map.entry;
|
||||
import static org.openmaptiles.util.Utils.coalesce;
|
||||
import static org.openmaptiles.util.Utils.nullIfEmpty;
|
||||
import static org.openmaptiles.util.Utils.nullIfLong;
|
||||
import static org.openmaptiles.util.Utils.nullOrEmpty;
|
||||
|
||||
import com.carrotsearch.hppc.LongIntMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
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.expression.MultiExpression;
|
||||
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.Map;
|
||||
import org.openmaptiles.generated.OpenMapTilesSchema;
|
||||
import org.openmaptiles.generated.Tables;
|
||||
import org.openmaptiles.util.OmtLanguageUtils;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for things like shops, parks, and schools in the {@code poi} layer from
|
||||
* source features.
|
||||
* <p>
|
||||
* This class is ported to Java from
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/poi">OpenMapTiles poi sql files</a>.
|
||||
*/
|
||||
public class Poi implements
|
||||
OpenMapTilesSchema.Poi,
|
||||
Tables.OsmPoiPoint.Handler,
|
||||
Tables.OsmPoiPolygon.Handler,
|
||||
ForwardingProfile.FeaturePostProcessor {
|
||||
|
||||
/*
|
||||
* process() creates the raw POI feature from OSM elements and postProcess()
|
||||
* assigns the feature rank from order in the tile at render-time.
|
||||
*/
|
||||
|
||||
private static final Map<String, Integer> CLASS_RANKS = Map.ofEntries(
|
||||
entry(FieldValues.CLASS_HOSPITAL, 20),
|
||||
entry(FieldValues.CLASS_RAILWAY, 40),
|
||||
entry(FieldValues.CLASS_BUS, 50),
|
||||
entry(FieldValues.CLASS_ATTRACTION, 70),
|
||||
entry(FieldValues.CLASS_HARBOR, 75),
|
||||
entry(FieldValues.CLASS_COLLEGE, 80),
|
||||
entry(FieldValues.CLASS_SCHOOL, 85),
|
||||
entry(FieldValues.CLASS_STADIUM, 90),
|
||||
entry("zoo", 95),
|
||||
entry(FieldValues.CLASS_TOWN_HALL, 100),
|
||||
entry(FieldValues.CLASS_CAMPSITE, 110),
|
||||
entry(FieldValues.CLASS_CEMETERY, 115),
|
||||
entry(FieldValues.CLASS_PARK, 120),
|
||||
entry(FieldValues.CLASS_LIBRARY, 130),
|
||||
entry("police", 135),
|
||||
entry(FieldValues.CLASS_POST, 140),
|
||||
entry(FieldValues.CLASS_GOLF, 150),
|
||||
entry(FieldValues.CLASS_SHOP, 400),
|
||||
entry(FieldValues.CLASS_GROCERY, 500),
|
||||
entry(FieldValues.CLASS_FAST_FOOD, 600),
|
||||
entry(FieldValues.CLASS_CLOTHING_STORE, 700),
|
||||
entry(FieldValues.CLASS_BAR, 800)
|
||||
);
|
||||
private final MultiExpression.Index<String> classMapping;
|
||||
private final Translations translations;
|
||||
|
||||
public Poi(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.classMapping = FieldMappings.Class.index();
|
||||
this.translations = translations;
|
||||
}
|
||||
|
||||
static int poiClassRank(String clazz) {
|
||||
return CLASS_RANKS.getOrDefault(clazz, 1_000);
|
||||
}
|
||||
|
||||
private String poiClass(String subclass, String mappingKey) {
|
||||
subclass = coalesce(subclass, "");
|
||||
return classMapping.getOrElse(Map.of(
|
||||
"subclass", subclass,
|
||||
"mapping_key", coalesce(mappingKey, "")
|
||||
), subclass);
|
||||
}
|
||||
|
||||
private int minzoom(String subclass, String mappingKey) {
|
||||
boolean lowZoom = ("station".equals(subclass) && "railway".equals(mappingKey)) ||
|
||||
"halt".equals(subclass) || "ferry_terminal".equals(subclass);
|
||||
return lowZoom ? 12 : 14;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmPoiPoint element, FeatureCollector features) {
|
||||
// TODO handle uic_ref => agg_stop
|
||||
setupPoiFeature(element, features.point(LAYER_NAME));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmPoiPolygon element, FeatureCollector features) {
|
||||
setupPoiFeature(element, features.centroidIfConvex(LAYER_NAME));
|
||||
}
|
||||
|
||||
private <T extends Tables.WithSubclass & Tables.WithStation & Tables.WithFunicular & Tables.WithSport & Tables.WithInformation & Tables.WithReligion & Tables.WithMappingKey & Tables.WithName & Tables.WithIndoor & Tables.WithLayer & Tables.WithSource & Tables.WithOperator & Tables.WithNetwork> void setupPoiFeature(
|
||||
T element, FeatureCollector.Feature output) {
|
||||
String rawSubclass = element.subclass();
|
||||
if ("station".equals(rawSubclass) && "subway".equals(element.station())) {
|
||||
rawSubclass = "subway";
|
||||
}
|
||||
if ("station".equals(rawSubclass) && "yes".equals(element.funicular())) {
|
||||
rawSubclass = "halt";
|
||||
}
|
||||
|
||||
// ATM names fall back to operator, or else network
|
||||
String name = element.name();
|
||||
var tags = element.source().tags();
|
||||
if ("atm".equals(rawSubclass) && nullOrEmpty(name)) {
|
||||
name = coalesce(nullIfEmpty(element.operator()), nullIfEmpty(element.network()));
|
||||
if (name != null) {
|
||||
tags.put("name", name);
|
||||
}
|
||||
}
|
||||
|
||||
String subclass = switch (rawSubclass) {
|
||||
case "information" -> nullIfEmpty(element.information());
|
||||
case "place_of_worship" -> nullIfEmpty(element.religion());
|
||||
case "pitch" -> nullIfEmpty(element.sport());
|
||||
default -> rawSubclass;
|
||||
};
|
||||
String poiClass = poiClass(rawSubclass, element.mappingKey());
|
||||
int poiClassRank = poiClassRank(poiClass);
|
||||
int rankOrder = poiClassRank + ((nullOrEmpty(name)) ? 2000 : 0);
|
||||
|
||||
output.setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, poiClass)
|
||||
.setAttr(Fields.SUBCLASS, subclass)
|
||||
.setAttr(Fields.LAYER, nullIfLong(element.layer(), 0))
|
||||
.setAttr(Fields.LEVEL, Parse.parseLongOrNull(element.source().getTag("level")))
|
||||
.setAttr(Fields.INDOOR, element.indoor() ? 1 : null)
|
||||
.putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setPointLabelGridPixelSize(14, 64)
|
||||
.setSortKey(rankOrder)
|
||||
.setMinZoom(minzoom(element.subclass(), element.mappingKey()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
// infer the "rank" field from the order of features within each label grid square
|
||||
LongIntMap groupCounts = Hppc.newLongIntHashMap();
|
||||
for (VectorTile.Feature feature : items) {
|
||||
int gridrank = groupCounts.getOrDefault(feature.group(), 1);
|
||||
groupCounts.put(feature.group(), gridrank + 1);
|
||||
if (!feature.attrs().containsKey(Fields.RANK)) {
|
||||
feature.attrs().put(Fields.RANK, gridrank);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
569
src/main/java/org/openmaptiles/layers/Transportation.java
Normal file
569
src/main/java/org/openmaptiles/layers/Transportation.java
Normal file
@@ -0,0 +1,569 @@
|
||||
/*
|
||||
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 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 static org.openmaptiles.util.Utils.*;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
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.openmaptiles.OpenMapTilesProfile;
|
||||
import org.openmaptiles.generated.OpenMapTilesSchema;
|
||||
import org.openmaptiles.generated.Tables;
|
||||
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}
|
||||
* layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/transportation">OpenMapTiles transportation
|
||||
* sql files</a>.
|
||||
*/
|
||||
public class Transportation implements
|
||||
OpenMapTilesSchema.Transportation,
|
||||
Tables.OsmAerialwayLinestring.Handler,
|
||||
Tables.OsmHighwayLinestring.Handler,
|
||||
Tables.OsmRailwayLinestring.Handler,
|
||||
Tables.OsmShipwayLinestring.Handler,
|
||||
Tables.OsmHighwayPolygon.Handler,
|
||||
OpenMapTilesProfile.NaturalEarthProcessor,
|
||||
OpenMapTilesProfile.FeaturePostProcessor,
|
||||
OpenMapTilesProfile.OsmRelationPreprocessor,
|
||||
OpenMapTilesProfile.IgnoreWikidata {
|
||||
|
||||
/*
|
||||
* Generates the shape for roads, trails, ferries, railways with detailed
|
||||
* attributes for rendering, but not any names. The transportation_name
|
||||
* 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,
|
||||
FieldValues.SUBCLASS_NARROW_GAUGE,
|
||||
FieldValues.SUBCLASS_PRESERVED,
|
||||
FieldValues.SUBCLASS_FUNICULAR
|
||||
);
|
||||
private static final Set<String> RAILWAY_TRANSIT_VALUES = Set.of(
|
||||
FieldValues.SUBCLASS_SUBWAY,
|
||||
FieldValues.SUBCLASS_LIGHT_RAIL,
|
||||
FieldValues.SUBCLASS_MONORAIL,
|
||||
FieldValues.SUBCLASS_TRAM
|
||||
);
|
||||
private static final Set<String> SERVICE_VALUES = Set.of(
|
||||
FieldValues.SERVICE_SPUR,
|
||||
FieldValues.SERVICE_YARD,
|
||||
FieldValues.SERVICE_SIDING,
|
||||
FieldValues.SERVICE_CROSSOVER,
|
||||
FieldValues.SERVICE_DRIVEWAY,
|
||||
FieldValues.SERVICE_ALLEY,
|
||||
FieldValues.SERVICE_PARKING_AISLE
|
||||
);
|
||||
private static final Set<String> SURFACE_UNPAVED_VALUES = Set.of(
|
||||
"unpaved", "compacted", "dirt", "earth", "fine_gravel", "grass", "grass_paver", "gravel", "gravel_turf", "ground",
|
||||
"ice", "mud", "pebblestone", "salt", "sand", "snow", "woodchips"
|
||||
);
|
||||
private static final Set<String> SURFACE_PAVED_VALUES = Set.of(
|
||||
"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;
|
||||
|
||||
public Transportation(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.config = config;
|
||||
this.stats = stats;
|
||||
z13Paths = config.arguments().getBoolean(
|
||||
"transportation_z13_paths",
|
||||
"transportation(_name) layer: show all paths on z13",
|
||||
false
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
private static String railwayClass(String value) {
|
||||
return value == null ? null :
|
||||
RAILWAY_RAIL_VALUES.contains(value) ? "rail" :
|
||||
RAILWAY_TRANSIT_VALUES.contains(value) ? "transit" : null;
|
||||
}
|
||||
|
||||
static String highwayClass(String highway, String publicTransport, String construction, String manMade) {
|
||||
return (!nullOrEmpty(highway) || !nullOrEmpty(publicTransport)) ? classMapping.getOrElse(Map.of(
|
||||
"highway", coalesce(highway, ""),
|
||||
"public_transport", coalesce(publicTransport, ""),
|
||||
"construction", coalesce(construction, "")
|
||||
), null) : isBridgeOrPier(manMade) ? manMade : null;
|
||||
}
|
||||
|
||||
static String highwaySubclass(String highwayClass, String publicTransport, String highway) {
|
||||
return FieldValues.CLASS_PATH.equals(highwayClass) ? coalesce(nullIfEmpty(publicTransport), highway) : null;
|
||||
}
|
||||
|
||||
static boolean isFootwayOrSteps(String highway) {
|
||||
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 (network != 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) {
|
||||
if (isPierPolygon(element)) {
|
||||
return;
|
||||
}
|
||||
int minzoom = getMinzoom(element, highwayClass);
|
||||
|
||||
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()))
|
||||
.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())
|
||||
.setMinZoom(minzoom);
|
||||
|
||||
if (isFootwayOrSteps(highway)) {
|
||||
feature
|
||||
.setAttr(Fields.LEVEL, Parse.parseLongOrNull(element.source().getTag("level")))
|
||||
.setAttr(Fields.INDOOR, element.indoor() ? 1 : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
String clazz = railwayClass(railway);
|
||||
if (clazz != null) {
|
||||
String service = nullIfEmpty(element.service());
|
||||
int minzoom;
|
||||
if (service != null) {
|
||||
minzoom = 14;
|
||||
} else if (FieldValues.SUBCLASS_RAIL.equals(railway)) {
|
||||
minzoom = "main".equals(element.usage()) ? 8 : 10;
|
||||
} else if (FieldValues.SUBCLASS_NARROW_GAUGE.equals(railway)) {
|
||||
minzoom = 10;
|
||||
} else if (FieldValues.SUBCLASS_LIGHT_RAIL.equals(railway)) {
|
||||
minzoom = 11;
|
||||
} else {
|
||||
minzoom = 14;
|
||||
}
|
||||
features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
.setAttr(Fields.SUBCLASS, railway)
|
||||
.setAttr(Fields.SERVICE, service(service))
|
||||
.setAttr(Fields.ONEWAY, nullIfInt(element.isOneway(), 0))
|
||||
.setAttr(Fields.RAMP, element.isRamp() ? 1L : null)
|
||||
.setAttrWithMinzoom(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()), 10)
|
||||
.setAttrWithMinzoom(Fields.LAYER, nullIfLong(element.layer(), 0), 9)
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinPixelSize(0) // merge during post-processing, then limit by size
|
||||
.setMinZoom(minzoom);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmAerialwayLinestring element, FeatureCollector features) {
|
||||
features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, "aerialway")
|
||||
.setAttr(Fields.SUBCLASS, element.aerialway())
|
||||
.setAttr(Fields.SERVICE, service(element.service()))
|
||||
.setAttr(Fields.ONEWAY, nullIfInt(element.isOneway(), 0))
|
||||
.setAttr(Fields.RAMP, element.isRamp() ? 1L : null)
|
||||
.setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()))
|
||||
.setAttr(Fields.LAYER, nullIfLong(element.layer(), 0))
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinPixelSize(0) // merge during post-processing, then limit by size
|
||||
.setMinZoom(12);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmShipwayLinestring element, FeatureCollector features) {
|
||||
features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, element.shipway()) // "ferry"
|
||||
// no subclass
|
||||
.setAttr(Fields.SERVICE, service(element.service()))
|
||||
.setAttr(Fields.ONEWAY, nullIfInt(element.isOneway(), 0))
|
||||
.setAttr(Fields.RAMP, element.isRamp() ? 1L : null)
|
||||
.setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()))
|
||||
.setAttr(Fields.LAYER, nullIfLong(element.layer(), 0))
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinPixelSize(0) // merge during post-processing, then limit by size
|
||||
.setMinZoom(11);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmHighwayPolygon element, FeatureCollector features) {
|
||||
String manMade = element.manMade();
|
||||
if (isBridgeOrPier(manMade) ||
|
||||
// only allow closed ways where area=yes, and multipolygons
|
||||
// and ignore underground pedestrian areas
|
||||
(!element.source().canBeLine() && element.layer() >= 0)) {
|
||||
String highwayClass = highwayClass(element.highway(), element.publicTransport(), null, element.manMade());
|
||||
if (highwayClass != null) {
|
||||
features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.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, nullIfLong(element.layer(), 0))
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinZoom(13);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
132
src/main/java/org/openmaptiles/layers/Water.java
Normal file
132
src/main/java/org/openmaptiles/layers/Water.java
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
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 com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.ForwardingProfile;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.List;
|
||||
import org.openmaptiles.OpenMapTilesProfile;
|
||||
import org.openmaptiles.generated.OpenMapTilesSchema;
|
||||
import org.openmaptiles.generated.Tables;
|
||||
import org.openmaptiles.util.Utils;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for oceans and lakes in the {@code water} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/water">OpenMapTiles water sql files</a>.
|
||||
*/
|
||||
public class Water implements
|
||||
OpenMapTilesSchema.Water,
|
||||
Tables.OsmWaterPolygon.Handler,
|
||||
OpenMapTilesProfile.NaturalEarthProcessor,
|
||||
OpenMapTilesProfile.OsmWaterPolygonProcessor,
|
||||
ForwardingProfile.FeaturePostProcessor {
|
||||
|
||||
/*
|
||||
* At low zoom levels, use natural earth for oceans and major lakes, and at high zoom levels
|
||||
* use OpenStreetMap data. OpenStreetMap data contains smaller bodies of water, but not
|
||||
* large ocean polygons. For oceans, use https://osmdata.openstreetmap.de/data/water-polygons.html
|
||||
* which infers ocean polygons by preprocessing all coastline elements.
|
||||
*/
|
||||
|
||||
private final MultiExpression.Index<String> classMapping;
|
||||
private final PlanetilerConfig config;
|
||||
|
||||
public Water(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.classMapping = FieldMappings.Class.index();
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) {
|
||||
record WaterInfo(int minZoom, int maxZoom, String clazz) {}
|
||||
WaterInfo info = switch (table) {
|
||||
case "ne_110m_ocean" -> new WaterInfo(0, 1, FieldValues.CLASS_OCEAN);
|
||||
case "ne_50m_ocean" -> new WaterInfo(2, 4, FieldValues.CLASS_OCEAN);
|
||||
case "ne_10m_ocean" -> new WaterInfo(5, 5, FieldValues.CLASS_OCEAN);
|
||||
|
||||
// TODO: get OSM ID from low-zoom natural earth lakes
|
||||
case "ne_110m_lakes" -> new WaterInfo(0, 1, FieldValues.CLASS_LAKE);
|
||||
case "ne_50m_lakes" -> new WaterInfo(2, 3, FieldValues.CLASS_LAKE);
|
||||
case "ne_10m_lakes" -> new WaterInfo(4, 5, FieldValues.CLASS_LAKE);
|
||||
default -> null;
|
||||
};
|
||||
if (info != null) {
|
||||
features.polygon(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setZoomRange(info.minZoom, info.maxZoom)
|
||||
.setAttr(Fields.CLASS, info.clazz);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processOsmWater(SourceFeature feature, FeatureCollector features) {
|
||||
features.polygon(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_OCEAN)
|
||||
.setMinZoom(6);
|
||||
}
|
||||
|
||||
@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.ID, element.source().id())
|
||||
.setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0)
|
||||
.setAttrWithMinzoom(Fields.BRUNNEL, Utils.brunnel(element.isBridge(), element.isTunnel()), 12)
|
||||
.setAttr(Fields.CLASS, clazz);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) throws GeometryException {
|
||||
return items.size() > 1 ? FeatureMerge.mergeOverlappingPolygons(items, config.minFeatureSize(zoom)) : items;
|
||||
}
|
||||
}
|
||||
202
src/main/java/org/openmaptiles/layers/WaterName.java
Normal file
202
src/main/java/org/openmaptiles/layers/WaterName.java
Normal file
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
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.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.carrotsearch.hppc.LongObjectMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.collection.Hppc;
|
||||
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.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentSkipListMap;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.openmaptiles.OpenMapTilesProfile;
|
||||
import org.openmaptiles.generated.OpenMapTilesSchema;
|
||||
import org.openmaptiles.generated.Tables;
|
||||
import org.openmaptiles.util.OmtLanguageUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for ocean and lake names in the {@code water_name} layer from source
|
||||
* features.
|
||||
* <p>
|
||||
* This class is ported to Java from
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/water_name">OpenMapTiles water_name sql
|
||||
* files</a>.
|
||||
*/
|
||||
public class WaterName implements
|
||||
OpenMapTilesSchema.WaterName,
|
||||
Tables.OsmMarinePoint.Handler,
|
||||
Tables.OsmWaterPolygon.Handler,
|
||||
OpenMapTilesProfile.NaturalEarthProcessor,
|
||||
OpenMapTilesProfile.LakeCenterlineProcessor {
|
||||
|
||||
/*
|
||||
* Labels for lakes and oceans come primarily from OpenStreetMap data, but we also join
|
||||
* with the lake centerlines source to get linestring geometries for prominent lakes.
|
||||
* We also join with natural earth to make certain important lake/ocean labels visible
|
||||
* at lower zoom levels.
|
||||
*/
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WaterName.class);
|
||||
private static final double WORLD_AREA_FOR_70K_SQUARE_METERS =
|
||||
Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2);
|
||||
private static final double LOG2 = Math.log(2);
|
||||
private final Translations translations;
|
||||
// need to synchronize updates from multiple threads
|
||||
private final LongObjectMap<Geometry> lakeCenterlines = Hppc.newLongObjectHashMap();
|
||||
// may be updated concurrently by multiple threads
|
||||
private final ConcurrentSkipListMap<String, Integer> importantMarinePoints = new ConcurrentSkipListMap<>();
|
||||
private final Stats stats;
|
||||
|
||||
public WaterName(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.translations = translations;
|
||||
this.stats = stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
lakeCenterlines.release();
|
||||
importantMarinePoints.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processLakeCenterline(SourceFeature feature, FeatureCollector features) {
|
||||
// TODO pull lake centerline computation into planetiler?
|
||||
long osmId = Math.abs(feature.getLong("OSM_ID"));
|
||||
if (osmId == 0L) {
|
||||
LOGGER.warn("Bad lake centerline. Tags: {}", feature.tags());
|
||||
} else {
|
||||
try {
|
||||
// multiple threads call this concurrently
|
||||
synchronized (this) {
|
||||
// if we already have a centerline for this OSM_ID, then merge the existing one with this one
|
||||
var newGeometry = feature.worldGeometry();
|
||||
var oldGeometry = lakeCenterlines.get(osmId);
|
||||
if (oldGeometry != null) {
|
||||
newGeometry = GeoUtils.combine(oldGeometry, newGeometry);
|
||||
}
|
||||
lakeCenterlines.put(osmId, newGeometry);
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_water_name_lakeline", "Bad lake centerline: " + feature);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) {
|
||||
// use natural earth named polygons just as a source of name to zoom-level mappings for later
|
||||
if ("ne_10m_geography_marine_polys".equals(table)) {
|
||||
String name = feature.getString("name");
|
||||
Integer scalerank = Parse.parseIntOrNull(feature.getTag("scalerank"));
|
||||
if (name != null && scalerank != null) {
|
||||
name = name.replaceAll("\\s+", " ").trim().toLowerCase();
|
||||
importantMarinePoints.put(name, scalerank);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmMarinePoint element, FeatureCollector features) {
|
||||
if (!element.name().isBlank()) {
|
||||
String place = element.place();
|
||||
var source = element.source();
|
||||
// use name from OSM, but get min zoom from natural earth based on fuzzy name match...
|
||||
Integer rank = Parse.parseIntOrNull(source.getTag("rank"));
|
||||
String name = element.name().toLowerCase();
|
||||
Integer nerank;
|
||||
if ((nerank = importantMarinePoints.get(name)) != null) {
|
||||
rank = nerank;
|
||||
} else if ((nerank = importantMarinePoints.get(source.getString("name:en", "").toLowerCase())) != null) {
|
||||
rank = nerank;
|
||||
} else if ((nerank = importantMarinePoints.get(source.getString("name:es", "").toLowerCase())) != null) {
|
||||
rank = nerank;
|
||||
} else {
|
||||
Map.Entry<String, Integer> next = importantMarinePoints.ceilingEntry(name);
|
||||
if (next != null && next.getKey().startsWith(name)) {
|
||||
rank = next.getValue();
|
||||
}
|
||||
}
|
||||
int minZoom = "ocean".equals(place) ? 0 : rank != null ? rank : 8;
|
||||
features.point(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(OmtLanguageUtils.getNames(source.tags(), translations))
|
||||
.setAttr(Fields.CLASS, place)
|
||||
.setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0)
|
||||
.setMinZoom(minZoom);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmWaterPolygon element, FeatureCollector features) {
|
||||
if (nullIfEmpty(element.name()) != null) {
|
||||
try {
|
||||
Geometry centerlineGeometry = lakeCenterlines.get(element.source().id());
|
||||
FeatureCollector.Feature feature;
|
||||
int minzoom = 9;
|
||||
if (centerlineGeometry != null) {
|
||||
// prefer lake centerline if it exists
|
||||
feature = features.geometry(LAYER_NAME, centerlineGeometry)
|
||||
.setMinPixelSizeBelowZoom(13, 6d * element.name().length());
|
||||
} else {
|
||||
// otherwise just use a label point inside the lake
|
||||
feature = features.pointOnSurface(LAYER_NAME);
|
||||
Geometry geometry = element.source().worldGeometry();
|
||||
double area = geometry.getArea();
|
||||
minzoom = (int) Math.floor(20 - Math.log(area / WORLD_AREA_FOR_70K_SQUARE_METERS) / LOG2);
|
||||
minzoom = Math.min(14, Math.max(9, minzoom));
|
||||
}
|
||||
feature
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_LAKE)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0)
|
||||
.setMinZoom(minzoom);
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_water_polygon", "Unable to get geometry for water polygon " + element.source().id());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
231
src/main/java/org/openmaptiles/layers/Waterway.java
Normal file
231
src/main/java/org/openmaptiles/layers/Waterway.java
Normal file
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
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.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.carrotsearch.hppc.LongObjectHashMap;
|
||||
import com.google.common.util.concurrent.AtomicDouble;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.collection.Hppc;
|
||||
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;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.openmaptiles.OpenMapTilesProfile;
|
||||
import org.openmaptiles.generated.OpenMapTilesSchema;
|
||||
import org.openmaptiles.generated.Tables;
|
||||
import org.openmaptiles.util.OmtLanguageUtils;
|
||||
import org.openmaptiles.util.Utils;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating river map elements in the {@code waterway} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/waterway">OpenMapTiles waterway sql
|
||||
* files</a>.
|
||||
*/
|
||||
public class Waterway implements
|
||||
OpenMapTilesSchema.Waterway,
|
||||
Tables.OsmWaterwayLinestring.Handler,
|
||||
OpenMapTilesProfile.FeaturePostProcessor,
|
||||
OpenMapTilesProfile.NaturalEarthProcessor,
|
||||
OpenMapTilesProfile.OsmRelationPreprocessor,
|
||||
OpenMapTilesProfile.OsmAllProcessor {
|
||||
|
||||
/*
|
||||
* Uses Natural Earth at lower zoom-levels and OpenStreetMap at higher zoom levels.
|
||||
*
|
||||
* For OpenStreetMap, attempts to merge disconnected linestrings with the same name
|
||||
* at lower zoom levels so that clients can more easily render the name. We also
|
||||
* limit their length at merge-time which only has visibilty into that feature in a
|
||||
* single tile, so at render-time we need to allow through features far enough outside
|
||||
* the tile boundary enough to not accidentally filter out a long river only because a
|
||||
* short segment of it goes through this tile.
|
||||
*/
|
||||
|
||||
private static final Map<String, Integer> CLASS_MINZOOM = Map.of(
|
||||
"river", 12,
|
||||
"canal", 12,
|
||||
|
||||
"stream", 13,
|
||||
"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 = Hppc.newLongObjectHashMap();
|
||||
|
||||
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")) {
|
||||
record ZoomRange(int min, int max) {}
|
||||
ZoomRange zoom = switch (table) {
|
||||
case "ne_110m_rivers_lake_centerlines" -> new ZoomRange(3, 3);
|
||||
case "ne_50m_rivers_lake_centerlines" -> new ZoomRange(4, 5);
|
||||
default -> null;
|
||||
};
|
||||
if (zoom != null) {
|
||||
features.line(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_RIVER)
|
||||
.setZoomRange(zoom.min, zoom.max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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"))) {
|
||||
synchronized (riverRelationLengths) {
|
||||
riverRelationLengths.put(relation.id(), new AtomicDouble());
|
||||
}
|
||||
return List.of(new WaterwayRelation(relation.id(), OmtLanguageUtils.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();
|
||||
String name = nullIfEmpty(element.name());
|
||||
boolean important = "river".equals(waterway) && name != null;
|
||||
int minzoom = important ? 9 : CLASS_MINZOOM.getOrDefault(element.waterway(), 14);
|
||||
features.line(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, element.waterway())
|
||||
.putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setMinZoom(minzoom)
|
||||
// details only at higher zoom levels so that named rivers can be merged more aggressively
|
||||
.setAttrWithMinzoom(Fields.BRUNNEL, Utils.brunnel(element.isBridge(), element.isTunnel()), 12)
|
||||
.setAttrWithMinzoom(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0, 12)
|
||||
// at lower zoom levels, we'll merge linestrings and limit length/clip afterwards
|
||||
.setBufferPixelOverrides(MIN_PIXEL_LENGTHS).setMinPixelSizeBelowZoom(11, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
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(),
|
||||
config.tolerance(zoom),
|
||||
BUFFER_SIZE
|
||||
);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user