mirror of
https://github.com/cfpwastaken/planetiler-openmaptiles.git
synced 2026-02-04 12:31:10 +00:00
Change name to Planetiler (#40)
* change name from flatmap to planetiler * bump version to 0.2-SNAPSHOT
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package com.onthegomap.planetiler.basemap;
|
||||
|
||||
import com.onthegomap.planetiler.Planetiler;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Main entrypoint for generating a map using the basemap schema.
|
||||
*/
|
||||
public class BasemapMain {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
run(Arguments.fromArgsOrConfigFile(args));
|
||||
}
|
||||
|
||||
static void run(Arguments arguments) throws Exception {
|
||||
Path dataDir = Path.of("data");
|
||||
Path sourcesDir = dataDir.resolve("sources");
|
||||
// use --area=... argument, AREA=... env var or area=... in config to set the region of the world to use
|
||||
// will be ignored if osm_path or osm_url are set
|
||||
String area = arguments.getString(
|
||||
"area",
|
||||
"name of the extract to download if osm_url/osm_path not specified (i.e. 'monaco' 'rhode island' 'australia' or 'planet')",
|
||||
"monaco"
|
||||
);
|
||||
|
||||
Planetiler.create(arguments)
|
||||
.setDefaultLanguages(OpenMapTilesSchema.LANGUAGES)
|
||||
.fetchWikidataNameTranslations(sourcesDir.resolve("wikidata_names.json"))
|
||||
// defer creation of the profile because it depends on data from the runner
|
||||
.setProfile(BasemapProfile::new)
|
||||
// override any of these with arguments: --osm_path=... or --osm_url=...
|
||||
// or OSM_PATH=... OSM_URL=... environmental argument
|
||||
// or osm_path=... osm_url=... in a config file
|
||||
.addShapefileSource("EPSG:3857", BasemapProfile.LAKE_CENTERLINE_SOURCE,
|
||||
sourcesDir.resolve("lake_centerline.shp.zip"),
|
||||
"https://github.com/lukasmartinelli/osm-lakelines/releases/download/v0.9/lake_centerline.shp.zip")
|
||||
.addShapefileSource(BasemapProfile.WATER_POLYGON_SOURCE,
|
||||
sourcesDir.resolve("water-polygons-split-3857.zip"),
|
||||
"https://osmdata.openstreetmap.de/download/water-polygons-split-3857.zip")
|
||||
.addNaturalEarthSource(BasemapProfile.NATURAL_EARTH_SOURCE,
|
||||
sourcesDir.resolve("natural_earth_vector.sqlite.zip"),
|
||||
"https://naciscdn.org/naturalearth/packages/natural_earth_vector.sqlite.zip")
|
||||
.addOsmSource(BasemapProfile.OSM_SOURCE,
|
||||
sourcesDir.resolve(area.replaceAll("[^a-zA-Z]+", "_") + ".osm.pbf"),
|
||||
"planet".equalsIgnoreCase(area) ? ("aws:latest") : ("geofabrik:" + area))
|
||||
// override with --mbtiles=... argument or MBTILES=... env var or mbtiles=... in a config file
|
||||
.setOutput("mbtiles", dataDir.resolve("output.mbtiles"))
|
||||
.run();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package com.onthegomap.planetiler.basemap;
|
||||
|
||||
import static com.onthegomap.planetiler.geo.GeoUtils.EMPTY_LINE;
|
||||
import static com.onthegomap.planetiler.geo.GeoUtils.EMPTY_POINT;
|
||||
import static com.onthegomap.planetiler.geo.GeoUtils.EMPTY_POLYGON;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.ForwardingProfile;
|
||||
import com.onthegomap.planetiler.Planetiler;
|
||||
import com.onthegomap.planetiler.Profile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.reader.SimpleFeature;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmElement;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Delegates the logic for generating a map to individual implementations in the {@code layers} package.
|
||||
* <p>
|
||||
* Layer implementations extend these interfaces to subscribe to elements from different sources:
|
||||
* <ul>
|
||||
* <li>{@link LakeCenterlineProcessor}</li>
|
||||
* <li>{@link NaturalEarthProcessor}</li>
|
||||
* <li>{@link OsmWaterPolygonProcessor}</li>
|
||||
* <li>{@link OsmAllProcessor} to process every OSM feature</li>
|
||||
* <li>{@link OsmRelationPreprocessor} to process every OSM relation during first pass through OSM file</li>
|
||||
* <li>A {@link Tables.RowHandler} implementation in {@code Tables.java} to process input features filtered and parsed
|
||||
* according to the imposm3 mappings defined in the OpenMapTiles schema. Each element corresponds to a row in the
|
||||
* table that imposm3 would have generated, with generated methods for accessing the data that would have been in each
|
||||
* column</li>
|
||||
* </ul>
|
||||
* Layers can also subscribe to notifications when we finished processing an input source by implementing
|
||||
* {@link FinishHandler} or post-process features in that layer before rendering the output tile by implementing
|
||||
* {@link FeaturePostProcessor}.
|
||||
*/
|
||||
public class BasemapProfile extends ForwardingProfile {
|
||||
|
||||
// IDs used in stats and logs for each input source, as well as argument/config file overrides to source locations
|
||||
public static final String LAKE_CENTERLINE_SOURCE = "lake_centerlines";
|
||||
public static final String WATER_POLYGON_SOURCE = "water_polygons";
|
||||
public static final String NATURAL_EARTH_SOURCE = "natural_earth";
|
||||
public static final String OSM_SOURCE = "osm";
|
||||
/** Index to efficiently find the imposm3 "table row" constructor from an OSM element based on its tags. */
|
||||
private final MultiExpression.Index<RowDispatch> osmMappings;
|
||||
/** Index variant that filters out any table only used by layers that implement IgnoreWikidata class. */
|
||||
private final MultiExpression.Index<Boolean> wikidataMappings;
|
||||
|
||||
public BasemapProfile(Planetiler runner) {
|
||||
this(runner.translations(), runner.config(), runner.stats());
|
||||
}
|
||||
|
||||
public BasemapProfile(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
List<String> onlyLayers = config.arguments().getList("only_layers", "Include only certain layers", List.of());
|
||||
List<String> excludeLayers = config.arguments().getList("exclude_layers", "Exclude certain layers", List.of());
|
||||
|
||||
// register release/finish/feature postprocessor/osm relationship handler methods...
|
||||
List<Handler> layers = new ArrayList<>();
|
||||
for (Layer layer : OpenMapTilesSchema.createInstances(translations, config, stats)) {
|
||||
if ((onlyLayers.isEmpty() || onlyLayers.contains(layer.name())) && !excludeLayers.contains(layer.name())) {
|
||||
layers.add(layer);
|
||||
registerHandler(layer);
|
||||
}
|
||||
}
|
||||
|
||||
// register per-source input element handlers
|
||||
for (Handler handler : layers) {
|
||||
if (handler instanceof NaturalEarthProcessor processor) {
|
||||
registerSourceHandler(NATURAL_EARTH_SOURCE,
|
||||
(source, features) -> processor.processNaturalEarth(source.getSourceLayer(), source, features));
|
||||
}
|
||||
if (handler instanceof OsmWaterPolygonProcessor processor) {
|
||||
registerSourceHandler(WATER_POLYGON_SOURCE, processor::processOsmWater);
|
||||
}
|
||||
if (handler instanceof LakeCenterlineProcessor processor) {
|
||||
registerSourceHandler(LAKE_CENTERLINE_SOURCE, processor::processLakeCenterline);
|
||||
}
|
||||
if (handler instanceof OsmAllProcessor processor) {
|
||||
registerSourceHandler(OSM_SOURCE, processor::processAllOsm);
|
||||
}
|
||||
}
|
||||
|
||||
// pre-process layers to build efficient indexes for matching OSM elements based on matching expressions
|
||||
// Map from imposm3 table row class to the layers that implement its handler.
|
||||
var handlerMap = Tables.generateDispatchMap(layers);
|
||||
osmMappings = Tables.MAPPINGS
|
||||
.mapResults(constructor -> {
|
||||
var handlers = handlerMap.getOrDefault(constructor.rowClass(), List.of()).stream()
|
||||
.map(r -> {
|
||||
@SuppressWarnings("unchecked") var handler = (Tables.RowHandler<Tables.Row>) r.handler();
|
||||
return handler;
|
||||
})
|
||||
.toList();
|
||||
return new RowDispatch(constructor.create(), handlers);
|
||||
}).simplify().index();
|
||||
wikidataMappings = Tables.MAPPINGS
|
||||
.mapResults(constructor ->
|
||||
handlerMap.getOrDefault(constructor.rowClass(), List.of()).stream()
|
||||
.anyMatch(handler -> !IgnoreWikidata.class.isAssignableFrom(handler.handlerClass()))
|
||||
).filterResults(b -> b).simplify().index();
|
||||
|
||||
// register a handler for all OSM elements that forwards to imposm3 "table row" handler methods
|
||||
// based on efficient pre-processed index
|
||||
if (!osmMappings.isEmpty()) {
|
||||
registerSourceHandler(OSM_SOURCE, (source, features) -> {
|
||||
for (var match : getTableMatches(source)) {
|
||||
RowDispatch rowDispatch = match.match();
|
||||
var row = rowDispatch.constructor.create(source, match.keys().get(0));
|
||||
for (Tables.RowHandler<Tables.Row> handler : rowDispatch.handlers()) {
|
||||
handler.process(row, features);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the imposm3 table row constructors that match an input element's tags. */
|
||||
public List<MultiExpression.Match<RowDispatch>> getTableMatches(SourceFeature input) {
|
||||
return osmMappings.getMatchesWithTriggers(input);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean caresAboutWikidataTranslation(OsmElement elem) {
|
||||
var tags = elem.tags();
|
||||
if (elem instanceof OsmElement.Node) {
|
||||
return wikidataMappings.getOrElse(SimpleFeature.create(EMPTY_POINT, tags), false);
|
||||
} else if (elem instanceof OsmElement.Way) {
|
||||
return wikidataMappings.getOrElse(SimpleFeature.create(EMPTY_POLYGON, tags), false)
|
||||
|| wikidataMappings.getOrElse(SimpleFeature.create(EMPTY_LINE, tags), false);
|
||||
} else if (elem instanceof OsmElement.Relation) {
|
||||
return wikidataMappings.getOrElse(SimpleFeature.create(EMPTY_POLYGON, tags), false);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Pass-through constants generated from the OpenMapTiles vector schema
|
||||
*/
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return OpenMapTilesSchema.NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return OpenMapTilesSchema.DESCRIPTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String attribution() {
|
||||
return OpenMapTilesSchema.ATTRIBUTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String version() {
|
||||
return OpenMapTilesSchema.VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layers should implement this interface to subscribe to elements from <a href="https://www.naturalearthdata.com/">natural
|
||||
* earth</a>.
|
||||
*/
|
||||
public interface NaturalEarthProcessor {
|
||||
|
||||
/**
|
||||
* Process an element from {@code table} in the<a href="https://www.naturalearthdata.com/">natural earth
|
||||
* source</a>.
|
||||
*
|
||||
* @see Profile#processFeature(SourceFeature, FeatureCollector)
|
||||
*/
|
||||
void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layers should implement this interface to subscribe to elements from <a href="https://github.com/lukasmartinelli/osm-lakelines">OSM
|
||||
* lake centerlines source</a>.
|
||||
*/
|
||||
public interface LakeCenterlineProcessor {
|
||||
|
||||
/**
|
||||
* Process an element from the <a href="https://github.com/lukasmartinelli/osm-lakelines">OSM lake centerlines
|
||||
* source</a>
|
||||
*
|
||||
* @see Profile#processFeature(SourceFeature, FeatureCollector)
|
||||
*/
|
||||
void processLakeCenterline(SourceFeature feature, FeatureCollector features);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layers should implement this interface to subscribe to elements from <a href="https://osmdata.openstreetmap.de/data/water-polygons.html">OSM
|
||||
* water polygons source</a>.
|
||||
*/
|
||||
public interface OsmWaterPolygonProcessor {
|
||||
|
||||
/**
|
||||
* Process an element from the <a href="https://osmdata.openstreetmap.de/data/water-polygons.html">OSM water
|
||||
* polygons source</a>
|
||||
*
|
||||
* @see Profile#processFeature(SourceFeature, FeatureCollector)
|
||||
*/
|
||||
void processOsmWater(SourceFeature feature, FeatureCollector features);
|
||||
}
|
||||
|
||||
/** Layers should implement this interface to subscribe to every OSM element. */
|
||||
public interface OsmAllProcessor {
|
||||
|
||||
/**
|
||||
* Process an OSM element during the second pass through the OSM data file.
|
||||
*
|
||||
* @see Profile#processFeature(SourceFeature, FeatureCollector)
|
||||
*/
|
||||
void processAllOsm(SourceFeature feature, FeatureCollector features);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layers should implement to indicate they do not need wikidata name translations to avoid downloading more
|
||||
* translations than are needed.
|
||||
*/
|
||||
public interface IgnoreWikidata {}
|
||||
|
||||
private static record RowDispatch(
|
||||
Tables.Constructor constructor,
|
||||
List<Tables.RowHandler<Tables.Row>> handlers
|
||||
) {}
|
||||
}
|
||||
757
src/main/java/com/onthegomap/planetiler/basemap/Generate.java
Normal file
757
src/main/java/com/onthegomap/planetiler/basemap/Generate.java
Normal file
@@ -0,0 +1,757 @@
|
||||
package com.onthegomap.planetiler.basemap;
|
||||
|
||||
import static com.onthegomap.planetiler.expression.Expression.*;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.CaseFormat;
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.Expression;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.util.Downloader;
|
||||
import com.onthegomap.planetiler.util.FileUtils;
|
||||
import com.onthegomap.planetiler.util.Format;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
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.Stream;
|
||||
import org.commonmark.parser.Parser;
|
||||
import org.commonmark.renderer.html.HtmlRenderer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.yaml.snakeyaml.LoaderOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
/**
|
||||
* Generates code in the {@code generated} package from the OpenMapTiles schema crawled from a tag or branch in the <a
|
||||
* href="https://github.com/openmaptiles/openmaptiles">OpenMapTiles GitHub repo</a>.
|
||||
* <p>
|
||||
* {@code OpenMapTilesSchema.java} contains the output layer definitions (i.e. attributes and allowed values) so that
|
||||
* layer implementations in {@code layers} package can reference them instead of hard-coding.
|
||||
* <p>
|
||||
* {@code Tables.java} contains the <a href="https://github.com/omniscale/imposm3">imposm3</a> table definitions from
|
||||
* mapping.yaml files in the OpenMapTiles repo. Layers in the {@code layer} package can extend the {@code Handler}
|
||||
* nested class for a table definition to "subscribe" to OSM elements that imposm3 would put in that table.
|
||||
* <p>
|
||||
* To run use {@code ./scripts/regenerate-openmaptiles.sh}
|
||||
*/
|
||||
public class Generate {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(Generate.class);
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
private static final Yaml yaml;
|
||||
private static final String LINE_SEPARATOR = System.lineSeparator();
|
||||
private static final String GENERATED_FILE_HEADER = """
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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
|
||||
*/
|
||||
// AUTOGENERATED BY Generate.java -- DO NOT MODIFY
|
||||
""";
|
||||
private static final Parser parser = Parser.builder().build();
|
||||
private static final HtmlRenderer renderer = HtmlRenderer.builder().build();
|
||||
|
||||
static {
|
||||
// bump the default limit of 50
|
||||
var options = new LoaderOptions();
|
||||
options.setMaxAliasesForCollections(1_000);
|
||||
yaml = new Yaml(options);
|
||||
}
|
||||
|
||||
private static <T> T loadAndParseYaml(String url, PlanetilerConfig config, Class<T> clazz) throws IOException {
|
||||
LOGGER.info("reading " + url);
|
||||
try (var stream = Downloader.openStream(url, config)) {
|
||||
// Jackson yaml parsing does not handle anchors and references, so first parse the input
|
||||
// using SnakeYAML, then parse SnakeYAML's output using Jackson to get it into our records.
|
||||
Map<String, Object> parsed = yaml.load(stream);
|
||||
return mapper.convertValue(parsed, clazz);
|
||||
}
|
||||
}
|
||||
|
||||
static <T> T parseYaml(String string, Class<T> clazz) {
|
||||
// Jackson yaml parsing does not handle anchors and references, so first parse the input
|
||||
// using SnakeYAML, then parse SnakeYAML's output using Jackson to get it into our records.
|
||||
Map<String, Object> parsed = yaml.load(string);
|
||||
return mapper.convertValue(parsed, clazz);
|
||||
}
|
||||
|
||||
static JsonNode parseYaml(String string) {
|
||||
return string == null ? null : parseYaml(string, JsonNode.class);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
Arguments arguments = Arguments.fromArgsOrConfigFile(args);
|
||||
PlanetilerConfig planetilerConfig = PlanetilerConfig.from(arguments);
|
||||
String tag = arguments.getString("tag", "openmaptiles tag to use", "v3.12.2");
|
||||
String base = "https://raw.githubusercontent.com/openmaptiles/openmaptiles/" + tag + "/";
|
||||
|
||||
// start crawling from openmaptiles.yaml
|
||||
// then crawl schema from each layers/<layer>/<layer>.yaml file that it references
|
||||
// then crawl table definitions from each layers/<layer>/mapping.yaml file that the layer references
|
||||
String rootUrl = base + "openmaptiles.yaml";
|
||||
OpenmaptilesConfig config = loadAndParseYaml(rootUrl, planetilerConfig, OpenmaptilesConfig.class);
|
||||
|
||||
List<LayerConfig> layers = new ArrayList<>();
|
||||
Set<String> imposm3MappingFiles = new LinkedHashSet<>();
|
||||
for (String layerFile : config.tileset.layers) {
|
||||
String layerURL = base + layerFile;
|
||||
LayerConfig layer = loadAndParseYaml(layerURL, planetilerConfig, LayerConfig.class);
|
||||
layers.add(layer);
|
||||
for (Datasource datasource : layer.datasources) {
|
||||
if ("imposm3".equals(datasource.type)) {
|
||||
String mappingPath = Path.of(layerFile).resolveSibling(datasource.mapping_file).normalize().toString();
|
||||
imposm3MappingFiles.add(base + mappingPath);
|
||||
} else {
|
||||
LOGGER.warn("Unknown datasource type: " + datasource.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Imposm3Table> tables = new LinkedHashMap<>();
|
||||
for (String uri : imposm3MappingFiles) {
|
||||
Imposm3Mapping layer = loadAndParseYaml(uri, planetilerConfig, Imposm3Mapping.class);
|
||||
tables.putAll(layer.tables);
|
||||
}
|
||||
|
||||
String packageName = "com.onthegomap.planetiler.basemap.generated";
|
||||
String[] packageParts = packageName.split("\\.");
|
||||
Path output = Path.of("planetiler-basemap", "src", "main", "java")
|
||||
.resolve(Path.of(packageParts[0], Arrays.copyOfRange(packageParts, 1, packageParts.length)));
|
||||
|
||||
FileUtils.deleteDirectory(output);
|
||||
Files.createDirectories(output);
|
||||
|
||||
emitLayerSchemaDefinitions(config.tileset, layers, packageName, output, tag);
|
||||
emitTableDefinitions(tables, packageName, output, tag);
|
||||
LOGGER.info(
|
||||
"Done generating code in 'generated' package, now run IntelliJ 'Reformat Code' operation with 'Optimize imports' and 'Cleanup code' options selected.");
|
||||
}
|
||||
|
||||
/** Generates {@code OpenMapTilesSchema.java} */
|
||||
private static void emitLayerSchemaDefinitions(OpenmaptilesTileSet info, List<LayerConfig> layers, String packageName,
|
||||
Path output, String tag)
|
||||
throws IOException {
|
||||
StringBuilder schemaClass = new StringBuilder();
|
||||
schemaClass.append("""
|
||||
%s
|
||||
package %s;
|
||||
|
||||
import static com.onthegomap.planetiler.expression.Expression.*;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.basemap.Layer;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* All vector tile layer definitions, attributes, and allowed values generated from the
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/blob/%s/openmaptiles.yaml">OpenMapTiles vector tile schema %s</a>.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class OpenMapTilesSchema {
|
||||
public static final String NAME = %s;
|
||||
public static final String DESCRIPTION = %s;
|
||||
public static final String VERSION = %s;
|
||||
public static final String ATTRIBUTION = %s;
|
||||
public static final List<String> LANGUAGES = List.of(%s);
|
||||
|
||||
/** Returns a list of expected layer implementation instances from the {@code layers} package. */
|
||||
public static List<Layer> createInstances(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
return List.of(
|
||||
%s
|
||||
);
|
||||
}
|
||||
"""
|
||||
.formatted(
|
||||
GENERATED_FILE_HEADER,
|
||||
packageName,
|
||||
escapeJavadoc(tag),
|
||||
escapeJavadoc(tag),
|
||||
Format.quote(info.name),
|
||||
Format.quote(info.description),
|
||||
Format.quote(info.version),
|
||||
Format.quote(info.attribution),
|
||||
info.languages.stream().map(Format::quote).collect(joining(", ")),
|
||||
layers.stream()
|
||||
.map(
|
||||
l -> "new com.onthegomap.planetiler.basemap.layers.%s(translations, config, stats)"
|
||||
.formatted(lowerUnderscoreToUpperCamel(l.layer.id)))
|
||||
.collect(joining("," + LINE_SEPARATOR))
|
||||
.indent(6).trim()
|
||||
));
|
||||
for (var layer : layers) {
|
||||
String layerCode = generateCodeForLayer(tag, layer);
|
||||
schemaClass.append(layerCode);
|
||||
}
|
||||
|
||||
schemaClass.append("}");
|
||||
Files.writeString(output.resolve("OpenMapTilesSchema.java"), schemaClass);
|
||||
}
|
||||
|
||||
private static String generateCodeForLayer(String tag, LayerConfig layer) {
|
||||
String layerName = layer.layer.id;
|
||||
String className = lowerUnderscoreToUpperCamel(layerName);
|
||||
|
||||
StringBuilder fields = new StringBuilder();
|
||||
StringBuilder fieldValues = new StringBuilder();
|
||||
StringBuilder fieldMappings = new StringBuilder();
|
||||
|
||||
layer.layer.fields.forEach((name, value) -> {
|
||||
JsonNode valuesNode = value.get("values");
|
||||
List<String> valuesForComment = valuesNode == null ? List.of() : valuesNode.isArray() ?
|
||||
iterToList(valuesNode.elements()).stream().map(Objects::toString).toList() :
|
||||
iterToList(valuesNode.fieldNames());
|
||||
String javadocDescription = markdownToJavadoc(getFieldDescription(value));
|
||||
fields.append("""
|
||||
%s
|
||||
public static final String %s = %s;
|
||||
""".formatted(
|
||||
valuesForComment.isEmpty() ? "/** %s */".formatted(javadocDescription) : """
|
||||
|
||||
/**
|
||||
* %s
|
||||
* <p>
|
||||
* allowed values:
|
||||
* <ul>
|
||||
* %s
|
||||
* </ul>
|
||||
*/
|
||||
""".stripTrailing().formatted(javadocDescription,
|
||||
valuesForComment.stream().map(v -> "<li>" + v).collect(joining(LINE_SEPARATOR + " * "))),
|
||||
name.toUpperCase(Locale.ROOT),
|
||||
Format.quote(name)
|
||||
).indent(4));
|
||||
|
||||
List<String> values = valuesNode == null ? List.of() : valuesNode.isArray() ?
|
||||
iterToList(valuesNode.elements()).stream().filter(JsonNode::isTextual).map(JsonNode::textValue)
|
||||
.map(t -> t.replaceAll(" .*", "")).toList() :
|
||||
iterToList(valuesNode.fieldNames());
|
||||
if (values.size() > 0) {
|
||||
fieldValues.append(values.stream()
|
||||
.map(v -> "public static final String %s = %s;"
|
||||
.formatted(name.toUpperCase(Locale.ROOT) + "_" + v.toUpperCase(Locale.ROOT).replace('-', '_'),
|
||||
Format.quote(v)))
|
||||
.collect(joining(LINE_SEPARATOR)).indent(2).strip()
|
||||
.indent(4));
|
||||
fieldValues.append("public static final Set<String> %s = Set.of(%s);".formatted(
|
||||
name.toUpperCase(Locale.ROOT) + "_VALUES",
|
||||
values.stream().map(Format::quote).collect(joining(", "))
|
||||
).indent(4));
|
||||
}
|
||||
|
||||
if (valuesNode != null && valuesNode.isObject()) {
|
||||
MultiExpression<String> mapping = generateFieldMapping(valuesNode);
|
||||
fieldMappings.append(" public static final MultiExpression<String> %s = %s;%n"
|
||||
.formatted(lowerUnderscoreToUpperCamel(name), generateJavaCode(mapping)));
|
||||
}
|
||||
});
|
||||
|
||||
return """
|
||||
/**
|
||||
* %s
|
||||
*
|
||||
* Generated from <a href="https://github.com/openmaptiles/openmaptiles/blob/%s/layers/%s/%s.yaml">%s.yaml</a>
|
||||
*/
|
||||
public interface %s extends Layer {
|
||||
double BUFFER_SIZE = %s;
|
||||
String LAYER_NAME = %s;
|
||||
@Override
|
||||
default String name() {
|
||||
return LAYER_NAME;
|
||||
}
|
||||
/** Attribute names for map elements in the %s layer. */
|
||||
final class Fields {
|
||||
%s
|
||||
}
|
||||
/** Attribute values for map elements in the %s layer. */
|
||||
final class FieldValues {
|
||||
%s
|
||||
}
|
||||
/** Complex mappings to generate attribute values from OSM element tags in the %s layer. */
|
||||
final class FieldMappings {
|
||||
%s
|
||||
}
|
||||
}
|
||||
""".formatted(
|
||||
markdownToJavadoc(layer.layer.description),
|
||||
escapeJavadoc(tag),
|
||||
escapeJavadoc(layerName),
|
||||
escapeJavadoc(layerName),
|
||||
escapeJavadoc(layerName),
|
||||
className,
|
||||
layer.layer.buffer_size,
|
||||
Format.quote(layerName),
|
||||
escapeJavadoc(layerName),
|
||||
fields.toString().strip(),
|
||||
escapeJavadoc(layerName),
|
||||
fieldValues.toString().strip(),
|
||||
escapeJavadoc(layerName),
|
||||
fieldMappings.toString().strip()
|
||||
).indent(2);
|
||||
}
|
||||
|
||||
/** Generates {@code Tables.java} */
|
||||
private static void emitTableDefinitions(Map<String, Imposm3Table> tables, String packageName, Path output,
|
||||
String tag)
|
||||
throws IOException {
|
||||
StringBuilder tablesClass = new StringBuilder();
|
||||
tablesClass.append("""
|
||||
%s
|
||||
package %s;
|
||||
|
||||
import static com.onthegomap.planetiler.expression.Expression.*;
|
||||
|
||||
import com.onthegomap.planetiler.expression.Expression;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* OSM element parsers generated from the <a href="https://github.com/omniscale/imposm3">imposm3</a> table definitions
|
||||
* in the <a href="https://github.com/openmaptiles/openmaptiles/blob/%s/openmaptiles.yaml">OpenMapTiles vector tile schema</a>.
|
||||
*
|
||||
* These filter and parse the raw OSM key/value attribute pairs on tags into records with fields that match the
|
||||
* columns in the tables that imposm3 would generate. Layer implementations can "subscribe" to elements from each
|
||||
* "table" but implementing the table's {@code Handler} interface and use the element's typed API to access
|
||||
* attributes.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class Tables {
|
||||
/** A parsed OSM element that would appear in a "row" of the imposm3 table. */
|
||||
public interface Row {
|
||||
|
||||
/** Returns the original OSM element. */
|
||||
SourceFeature source();
|
||||
}
|
||||
|
||||
/** A functional interface that the constructor of a new table row can be coerced to. */
|
||||
@FunctionalInterface
|
||||
public interface Constructor {
|
||||
|
||||
Row create(SourceFeature source, String mappingKey);
|
||||
}
|
||||
|
||||
/** The {@code rowClass} of an imposm3 table row and its constructor coerced to a {@link Constructor}. */
|
||||
public static record RowClassAndConstructor(
|
||||
Class<? extends Row> rowClass,
|
||||
Constructor create
|
||||
) {}
|
||||
|
||||
/** A functional interface that the typed handler method that a layer implementation can be coerced to. */
|
||||
@FunctionalInterface
|
||||
public interface RowHandler<T extends Row> {
|
||||
|
||||
/** Process a typed element according to the profile. */
|
||||
void process(T element, FeatureCollector features);
|
||||
}
|
||||
|
||||
/** The {@code handlerClass} of a layer handler and it's {@code process} method coerced to a {@link RowHandler}. */
|
||||
public static record RowHandlerAndClass<T extends Row>(
|
||||
Class<?> handlerClass,
|
||||
RowHandler<T> handler
|
||||
) {}
|
||||
""".formatted(GENERATED_FILE_HEADER, packageName, escapeJavadoc(tag)));
|
||||
|
||||
List<String> classNames = new ArrayList<>();
|
||||
Map<String, String> fieldNameToType = new TreeMap<>();
|
||||
for (var entry : tables.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Imposm3Table table = entry.getValue();
|
||||
List<OsmTableField> fields = parseTableFields(table);
|
||||
for (var field : fields) {
|
||||
String existing = fieldNameToType.get(field.name);
|
||||
if (existing == null) {
|
||||
fieldNameToType.put(field.name, field.clazz);
|
||||
} else if (!existing.equals(field.clazz)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Field " + field.name + " has both " + existing + " and " + field.clazz + " types");
|
||||
}
|
||||
}
|
||||
Expression mappingExpression = parseImposm3MappingExpression(table);
|
||||
String mapping = """
|
||||
/** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */
|
||||
public static final Expression MAPPING = %s;
|
||||
""".formatted(
|
||||
mappingExpression
|
||||
);
|
||||
String tableName = "osm_" + key;
|
||||
String className = lowerUnderscoreToUpperCamel(tableName);
|
||||
if (!"relation_member".equals(table.type)) {
|
||||
classNames.add(className);
|
||||
|
||||
tablesClass.append("""
|
||||
/** An OSM element that would appear in the {@code %s} table generated by imposm3. */
|
||||
public static record %s(%s) implements Row, %s {
|
||||
public %s(SourceFeature source, String mappingKey) {
|
||||
this(%s);
|
||||
}
|
||||
%s
|
||||
/**
|
||||
* Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as
|
||||
* {@link %s}.
|
||||
*/
|
||||
public interface Handler {
|
||||
void process(%s element, FeatureCollector features);
|
||||
}
|
||||
}
|
||||
""".formatted(
|
||||
tableName,
|
||||
escapeJavadoc(className),
|
||||
fields.stream().map(c -> "@Override " + c.clazz + " " + lowerUnderscoreToLowerCamel(c.name))
|
||||
.collect(joining(", ")),
|
||||
fields.stream().map(c -> lowerUnderscoreToUpperCamel("with_" + c.name))
|
||||
.collect(joining(", ")),
|
||||
className,
|
||||
fields.stream().map(c -> c.extractCode).collect(joining(", ")),
|
||||
mapping,
|
||||
escapeJavadoc(className),
|
||||
className
|
||||
).indent(2));
|
||||
}
|
||||
}
|
||||
|
||||
tablesClass.append(fieldNameToType.entrySet().stream().map(e -> {
|
||||
String attrName = lowerUnderscoreToLowerCamel(e.getKey());
|
||||
String type = e.getValue();
|
||||
String interfaceName = lowerUnderscoreToUpperCamel("with_" + e.getKey());
|
||||
return """
|
||||
/** Rows with a %s %s attribute. */
|
||||
public interface %s {
|
||||
%s %s();
|
||||
}
|
||||
""".formatted(
|
||||
escapeJavadoc(type),
|
||||
escapeJavadoc(attrName),
|
||||
interfaceName,
|
||||
type,
|
||||
attrName);
|
||||
}).collect(joining(LINE_SEPARATOR)).indent(2));
|
||||
|
||||
tablesClass.append("""
|
||||
/** Index to efficiently choose which imposm3 "tables" an element should appear in based on its attributes. */
|
||||
public static final MultiExpression<RowClassAndConstructor> MAPPINGS = MultiExpression.of(List.of(
|
||||
%s
|
||||
));
|
||||
""".formatted(
|
||||
classNames.stream().map(
|
||||
className -> "MultiExpression.entry(new RowClassAndConstructor(%s.class, %s::new), %s.MAPPING)".formatted(
|
||||
className, className, className))
|
||||
.collect(joining("," + LINE_SEPARATOR)).indent(2).strip()
|
||||
).indent(2));
|
||||
|
||||
String handlerCondition = classNames.stream().map(className ->
|
||||
"""
|
||||
if (handler instanceof %s.Handler typedHandler) {
|
||||
result.computeIfAbsent(%s.class, cls -> new ArrayList<>()).add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process));
|
||||
}""".formatted(className, className)
|
||||
).collect(joining(LINE_SEPARATOR));
|
||||
tablesClass.append("""
|
||||
/**
|
||||
* Returns a map from imposm3 "table row" class to the layers that have a handler for it from a list of layer
|
||||
* implementations.
|
||||
*/
|
||||
public static Map<Class<? extends Row>, List<RowHandlerAndClass<?>>> generateDispatchMap(List<?> handlers) {
|
||||
Map<Class<? extends Row>, List<RowHandlerAndClass<?>>> result = new HashMap<>();
|
||||
for (var handler : handlers) {
|
||||
%s
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
""".formatted(handlerCondition.indent(6).trim()));
|
||||
Files.writeString(output.resolve("Tables.java"), tablesClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link Expression} that implements the same logic as the <a href="https://imposm.org/docs/imposm3/latest/mapping.html">Imposm3
|
||||
* Data Mapping</a> definition for a table.
|
||||
*/
|
||||
static Expression parseImposm3MappingExpression(Imposm3Table table) {
|
||||
if (table.type_mappings != null) {
|
||||
return or(
|
||||
table.type_mappings.entrySet().stream().map(entry ->
|
||||
parseImposm3MappingExpression(entry.getKey(), entry.getValue(), table.filters)
|
||||
).toList()
|
||||
).simplify();
|
||||
} else {
|
||||
return parseImposm3MappingExpression(table.type, table.mapping, table.filters);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link Expression} that implements the same logic as the <a href="https://imposm.org/docs/imposm3/latest/mapping.html#filters">Imposm3
|
||||
* Data Mapping filters</a> for a table.
|
||||
*/
|
||||
static Expression parseImposm3MappingExpression(String type, JsonNode mapping, Imposm3Filters filters) {
|
||||
return and(
|
||||
or(parseFieldMappingExpression(mapping).toList()),
|
||||
and(
|
||||
filters == null || filters.require == null ? List.of() : parseFieldMappingExpression(filters.require).toList()),
|
||||
not(or(
|
||||
filters == null || filters.reject == null ? List.of() : parseFieldMappingExpression(filters.reject).toList())),
|
||||
matchType(type.replaceAll("s$", ""))
|
||||
).simplify();
|
||||
}
|
||||
|
||||
private static List<OsmTableField> parseTableFields(Imposm3Table tableDefinition) {
|
||||
List<OsmTableField> result = new ArrayList<>();
|
||||
boolean relationMember = "relation_member".equals(tableDefinition.type);
|
||||
for (Imposm3Column col : tableDefinition.columns) {
|
||||
if (relationMember && col.from_member) {
|
||||
// layers process relation info that they need manually
|
||||
continue;
|
||||
}
|
||||
switch (col.type) {
|
||||
case "id", "validated_geometry", "area", "hstore_tags", "geometry" -> {
|
||||
// do nothing - already on source feature
|
||||
}
|
||||
case "member_id", "member_role", "member_type", "member_index" -> {
|
||||
// do nothing
|
||||
}
|
||||
case "mapping_key" -> result
|
||||
.add(new OsmTableField("String", col.name, "mappingKey"));
|
||||
case "mapping_value" -> result
|
||||
.add(new OsmTableField("String", col.name, "source.getString(mappingKey)"));
|
||||
case "string" -> result
|
||||
.add(new OsmTableField("String", col.name,
|
||||
"source.getString(\"%s\")".formatted(Objects.requireNonNull(col.key, col.toString()))));
|
||||
case "bool" -> result
|
||||
.add(new OsmTableField("boolean", col.name,
|
||||
"source.getBoolean(\"%s\")".formatted(Objects.requireNonNull(col.key, col.toString()))));
|
||||
case "integer" -> result
|
||||
.add(new OsmTableField("long", col.name,
|
||||
"source.getLong(\"%s\")".formatted(Objects.requireNonNull(col.key, col.toString()))));
|
||||
case "wayzorder" -> result.add(new OsmTableField("int", col.name, "source.getWayZorder()"));
|
||||
case "direction" -> result.add(new OsmTableField("int", col.name,
|
||||
"source.getDirection(\"%s\")".formatted(Objects.requireNonNull(col.key, col.toString()))));
|
||||
default -> throw new IllegalArgumentException("Unhandled column: " + col.type);
|
||||
}
|
||||
}
|
||||
result.add(new OsmTableField("SourceFeature", "source", "source"));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link MultiExpression} to efficiently determine the value for an output vector tile feature (i.e.
|
||||
* "class") based on the "field mapping" defined in the layer schema definition.
|
||||
*/
|
||||
static MultiExpression<String> generateFieldMapping(JsonNode valuesNode) {
|
||||
MultiExpression<String> mapping = MultiExpression.of(new ArrayList<>());
|
||||
valuesNode.fields().forEachRemaining(entry -> {
|
||||
String field = entry.getKey();
|
||||
JsonNode node = entry.getValue();
|
||||
Expression expression = or(parseFieldMappingExpression(node).toList()).simplify();
|
||||
if (!expression.equals(or()) && !expression.equals(and())) {
|
||||
mapping.expressions().add(MultiExpression.entry(field, expression));
|
||||
}
|
||||
});
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private static Stream<Expression> parseFieldMappingExpression(JsonNode node) {
|
||||
if (node.isObject()) {
|
||||
List<String> keys = iterToList(node.fieldNames());
|
||||
if (keys.contains("__AND__")) {
|
||||
if (keys.size() > 1) {
|
||||
throw new IllegalArgumentException("Cannot combine __AND__ with others");
|
||||
}
|
||||
return Stream.of(and(parseFieldMappingExpression(node.get("__AND__")).toList()));
|
||||
} else if (keys.contains("__OR__")) {
|
||||
if (keys.size() > 1) {
|
||||
throw new IllegalArgumentException("Cannot combine __OR__ with others");
|
||||
}
|
||||
return Stream.of(or(parseFieldMappingExpression(node.get("__OR__")).toList()));
|
||||
} else {
|
||||
return iterToList(node.fields()).stream().map(entry -> {
|
||||
String field = entry.getKey();
|
||||
List<String> value = toFlatList(entry.getValue()).map(JsonNode::textValue).filter(Objects::nonNull).toList();
|
||||
return value.isEmpty() || value.contains("__any__") ? matchField(field) : matchAny(field, value);
|
||||
});
|
||||
}
|
||||
} else if (node.isArray()) {
|
||||
return iterToList(node.elements()).stream().flatMap(Generate::parseFieldMappingExpression);
|
||||
} else if (node.isNull()) {
|
||||
return Stream.empty();
|
||||
} else {
|
||||
throw new IllegalArgumentException("parseExpression input not handled: " + node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a flattened list of all the elements in nested arrays from {@code node}.
|
||||
* <p>
|
||||
* For example: {@code [[[a, b], c], [d]} becomes {@code [a, b, c, d]}
|
||||
* <p>
|
||||
* And {@code a} becomes {@code [a]}
|
||||
*/
|
||||
private static Stream<JsonNode> toFlatList(JsonNode node) {
|
||||
return node.isArray() ? iterToList(node.elements()).stream().flatMap(Generate::toFlatList) : Stream.of(node);
|
||||
}
|
||||
|
||||
/** Returns java code that will recreate an {@link MultiExpression} identical to {@code mapping}. */
|
||||
private static String generateJavaCode(MultiExpression<String> mapping) {
|
||||
return "MultiExpression.of(List.of(" + mapping.expressions().stream()
|
||||
.map(s -> "MultiExpression.entry(%s, %s)".formatted(Format.quote(s.result()), s.expression()))
|
||||
.collect(joining(", ")) + "))";
|
||||
}
|
||||
|
||||
private static String lowerUnderscoreToLowerCamel(String name) {
|
||||
return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, name);
|
||||
}
|
||||
|
||||
private static String lowerUnderscoreToUpperCamel(String name) {
|
||||
return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, name);
|
||||
}
|
||||
|
||||
private static <T> List<T> iterToList(Iterator<T> iter) {
|
||||
List<T> result = new ArrayList<>();
|
||||
iter.forEachRemaining(result::add);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Renders {@code markdown} as HTML and returns comment text safe to insert in generated javadoc. */
|
||||
private static String markdownToJavadoc(String markdown) {
|
||||
return Stream.of(markdown.strip().split("[\r\n][\r\n]+"))
|
||||
.map(p -> parser.parse(p.strip()))
|
||||
.map(node -> escapeJavadoc(renderer.render(node)))
|
||||
.map(p -> p.replaceAll("(^<p>|</p>$)", "").strip())
|
||||
.collect(joining(LINE_SEPARATOR + "<p>" + LINE_SEPARATOR));
|
||||
}
|
||||
|
||||
/** Returns {@code comment} text safe to insert in generated javadoc. */
|
||||
private static String escapeJavadoc(String comment) {
|
||||
return comment.strip().replaceAll("[\n\r*\\s]+", " ");
|
||||
}
|
||||
|
||||
private static String getFieldDescription(JsonNode value) {
|
||||
if (value.isTextual()) {
|
||||
return value.textValue();
|
||||
} else {
|
||||
return value.get("description").textValue();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Models for deserializing yaml into:
|
||||
*/
|
||||
|
||||
private static record OpenmaptilesConfig(
|
||||
OpenmaptilesTileSet tileset
|
||||
) {}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static record OpenmaptilesTileSet(
|
||||
List<String> layers,
|
||||
String version,
|
||||
String attribution,
|
||||
String name,
|
||||
String description,
|
||||
List<String> languages
|
||||
) {}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static record LayerDetails(
|
||||
String id,
|
||||
String description,
|
||||
Map<String, JsonNode> fields,
|
||||
double buffer_size
|
||||
) {}
|
||||
|
||||
private static record Datasource(
|
||||
String type,
|
||||
String mapping_file
|
||||
) {}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static record LayerConfig(
|
||||
LayerDetails layer,
|
||||
List<Datasource> datasources
|
||||
) {}
|
||||
|
||||
private static record Imposm3Column(
|
||||
String type,
|
||||
String name,
|
||||
String key,
|
||||
boolean from_member
|
||||
) {}
|
||||
|
||||
static record Imposm3Filters(
|
||||
JsonNode reject,
|
||||
JsonNode require
|
||||
) {}
|
||||
|
||||
static record Imposm3Table(
|
||||
String type,
|
||||
@JsonProperty("_resolve_wikidata") boolean resolveWikidata,
|
||||
List<Imposm3Column> columns,
|
||||
Imposm3Filters filters,
|
||||
JsonNode mapping,
|
||||
Map<String, JsonNode> type_mappings
|
||||
) {}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static record Imposm3Mapping(
|
||||
Map<String, Imposm3Table> tables
|
||||
) {}
|
||||
|
||||
private static record OsmTableField(
|
||||
String clazz,
|
||||
String name,
|
||||
String extractCode
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.onthegomap.planetiler.basemap;
|
||||
|
||||
import com.onthegomap.planetiler.ForwardingProfile;
|
||||
|
||||
/** Interface for all vector tile layer implementations that {@link BasemapProfile} delegates to. */
|
||||
public interface Layer extends
|
||||
ForwardingProfile.Handler,
|
||||
ForwardingProfile.HandlerForLayer {}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.basemap.util.Utils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
features.centroid(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setMinZoom(10)
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.putAttrs(Utils.elevationTags(element.ele()))
|
||||
.setAttr(Fields.IATA, nullIfEmpty(element.iata()))
|
||||
.setAttr(Fields.ICAO, nullIfEmpty(element.icao()))
|
||||
.setAttr(Fields.CLASS, classLookup.getOrElse(element.source(), FieldValues.CLASS_OTHER));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.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.graphhopper.coll.GHLongObjectHashMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
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.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,
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
BasemapProfile.OsmRelationPreprocessor,
|
||||
BasemapProfile.OsmAllProcessor,
|
||||
BasemapProfile.FeaturePostProcessor,
|
||||
BasemapProfile.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) : 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 (BasemapProfile.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 = new GHLongObjectHashMap<>();
|
||||
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 static 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 static 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 static 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
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 com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
BasemapProfile.FeaturePostProcessor,
|
||||
BasemapProfile.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 static record BuildingRelationInfo(long id) implements OsmRelationInfo {
|
||||
|
||||
@Override
|
||||
public long estimateMemoryUsageBytes() {
|
||||
return CLASS_HEADER_BYTES + MemoryEstimator.estimateSizeLong(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
Tables.OsmLandcoverPolygon.Handler,
|
||||
BasemapProfile.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(WOOD_OR_FOREST.contains(subclass) ? 9 : 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, numPoints < 50 ? "<50" : "<300");
|
||||
toMerge.add(item);
|
||||
} else { // don't merge
|
||||
result.add(item);
|
||||
}
|
||||
} else if (zoom == 9) {
|
||||
if (WOOD_OR_FOREST.contains(subclass)) {
|
||||
attrs.put(tempGroupKey, numPoints < 50 ? "<50" : numPoints < 300 ? "<300" : ">300");
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
BasemapProfile.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) {
|
||||
features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
.setMinPixelSizeOverrides(MIN_PIXEL_SIZE_THRESHOLDS)
|
||||
.setMinZoom(Z6_CLASSES.contains(clazz) ? 6 : 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.elevationTags;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.carrotsearch.hppc.LongIntHashMap;
|
||||
import com.carrotsearch.hppc.LongIntMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.List;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
|
||||
/**
|
||||
* 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
|
||||
OpenMapTilesSchema.MountainPeak,
|
||||
Tables.OsmPeakPoint.Handler,
|
||||
BasemapProfile.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 final Translations translations;
|
||||
private final Stats stats;
|
||||
|
||||
public MountainPeak(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.translations = translations;
|
||||
this.stats = stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmPeakPoint element, FeatureCollector features) {
|
||||
Integer meters = Parse.parseIntSubstring(element.ele());
|
||||
if (meters != null && Math.abs(meters) < 10_000) {
|
||||
features.point(LAYER_NAME)
|
||||
.setAttr(Fields.CLASS, element.source().getTag("natural"))
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.putAttrs(elevationTags(meters))
|
||||
.setSortKeyDescending(
|
||||
meters +
|
||||
(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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
LongIntMap groupCounts = new LongIntHashMap();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
152
src/main/java/com/onthegomap/planetiler/basemap/layers/Park.java
Normal file
152
src/main/java/com/onthegomap/planetiler/basemap/layers/Park.java
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
import static com.onthegomap.planetiler.collection.FeatureGroup.SORT_KEY_BITS;
|
||||
|
||||
import com.carrotsearch.hppc.LongIntHashMap;
|
||||
import com.carrotsearch.hppc.LongIntMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.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;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
BasemapProfile.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
|
||||
features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
.setMinPixelSize(2)
|
||||
.setMinZoom(6);
|
||||
|
||||
// park name label point (if it has one)
|
||||
if (element.name() != null) {
|
||||
try {
|
||||
double area = element.source().area();
|
||||
int minzoom = getMinZoomForArea(area);
|
||||
|
||||
features.centroid(LAYER_NAME).setBufferPixels(256)
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
.putAttrs(LanguageUtils.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(6, minzoom));
|
||||
return minzoom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
// infer the "rank" attribute from point ordering within each label grid square
|
||||
LongIntMap counts = new LongIntHashMap();
|
||||
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);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullOrEmpty;
|
||||
import static com.onthegomap.planetiler.collection.FeatureGroup.SORT_KEY_BITS;
|
||||
|
||||
import com.carrotsearch.hppc.LongIntHashMap;
|
||||
import com.carrotsearch.hppc.LongIntMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.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;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
Tables.OsmContinentPoint.Handler,
|
||||
Tables.OsmCountryPoint.Handler,
|
||||
Tables.OsmStatePoint.Handler,
|
||||
Tables.OsmIslandPoint.Handler,
|
||||
Tables.OsmIslandPolygon.Handler,
|
||||
Tables.OsmCityPoint.Handler,
|
||||
BasemapProfile.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 <= 3 && labelrank != null && labelrank <= 2) {
|
||||
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(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_CONTINENT)
|
||||
.setAttr(Fields.RANK, 1)
|
||||
.putAttrs(LanguageUtils.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 = LanguageUtils.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.min(6, Math.max(1, 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 = LanguageUtils.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, FieldValues.CLASS_STATE)
|
||||
.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(LanguageUtils.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(LanguageUtils.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(LanguageUtils.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 = new LongIntHashMap();
|
||||
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 static 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 static record NaturalEarthPoint(String name, String wikidata, int scaleRank, Set<String> names) {}
|
||||
}
|
||||
|
||||
195
src/main/java/com/onthegomap/planetiler/basemap/layers/Poi.java
Normal file
195
src/main/java/com/onthegomap/planetiler/basemap/layers/Poi.java
Normal file
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIf;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullOrEmpty;
|
||||
import static java.util.Map.entry;
|
||||
|
||||
import com.carrotsearch.hppc.LongIntHashMap;
|
||||
import com.carrotsearch.hppc.LongIntMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.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;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
BasemapProfile.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>
|
||||
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";
|
||||
}
|
||||
|
||||
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(element.name())) ? 2000 : 0);
|
||||
|
||||
output.setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, poiClass)
|
||||
.setAttr(Fields.SUBCLASS, subclass)
|
||||
.setAttr(Fields.LAYER, nullIf(element.layer(), 0))
|
||||
.setAttr(Fields.LEVEL, Parse.parseLongOrNull(element.source().getTag("level")))
|
||||
.setAttr(Fields.INDOOR, element.indoor() ? 1 : null)
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.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 = new LongIntHashMap();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.*;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
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.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
BasemapProfile.FeaturePostProcessor,
|
||||
BasemapProfile.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 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 ZoomFunction.MeterToPixelThresholds MIN_LENGTH = ZoomFunction.meterThresholds()
|
||||
.put(7, 50)
|
||||
.put(6, 100)
|
||||
.put(5, 500)
|
||||
.put(4, 1_000);
|
||||
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;
|
||||
boolean z13Paths = config.arguments().getBoolean(
|
||||
"transportation_z13_paths",
|
||||
"transportation(_name) layer: show paths on z13",
|
||||
true
|
||||
);
|
||||
MINZOOMS = Map.of(
|
||||
FieldValues.CLASS_TRACK, 14,
|
||||
FieldValues.CLASS_PATH, z13Paths ? 13 : 14,
|
||||
FieldValues.CLASS_MINOR, 13,
|
||||
FieldValues.CLASS_RACEWAY, 12,
|
||||
FieldValues.CLASS_TERTIARY, 11,
|
||||
FieldValues.CLASS_SECONDARY, 9,
|
||||
FieldValues.CLASS_PRIMARY, 7,
|
||||
FieldValues.CLASS_TRUNK, 5,
|
||||
FieldValues.CLASS_MOTORWAY, 4
|
||||
);
|
||||
}
|
||||
|
||||
/** 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 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, "")
|
||||
), manMade) : manMade;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static boolean isResidentialOrUnclassified(String highway) {
|
||||
return "residential".equals(highway) || "unclassified".equals(highway);
|
||||
}
|
||||
|
||||
private static boolean isBridgeOrPier(String manMade) {
|
||||
return "bridge".equals(manMade) || "pier".equals(manMade);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmHighwayLinestring element, FeatureCollector features) {
|
||||
if (element.isArea()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String highway = element.highway();
|
||||
String highwayClass = highwayClass(element.highway(), element.publicTransport(), element.construction(),
|
||||
element.manMade());
|
||||
if (highwayClass != null) {
|
||||
int minzoom;
|
||||
if ("pier".equals(element.manMade())) {
|
||||
try {
|
||||
if (element.source().worldGeometry() instanceof LineString lineString && lineString.isClosed()) {
|
||||
// ignore this because it's a polygon
|
||||
return;
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_transportation_pier",
|
||||
"Unable to decode pier geometry for " + element.source().id());
|
||||
return;
|
||||
}
|
||||
minzoom = 13;
|
||||
} else if (isResidentialOrUnclassified(highway)) {
|
||||
minzoom = 12;
|
||||
} else {
|
||||
String baseClass = highwayClass.replace("_construction", "");
|
||||
minzoom = MINZOOMS.getOrDefault(baseClass, 12);
|
||||
}
|
||||
boolean highwayIsLink = coalesce(highway, "").endsWith("_link");
|
||||
|
||||
if (highwayIsLink) {
|
||||
minzoom = Math.max(minzoom, 9);
|
||||
}
|
||||
|
||||
boolean highwayRamp = highwayIsLink || "steps".equals(highway);
|
||||
int rampAboveZ12 = (highwayRamp || element.isRamp()) ? 1 : 0;
|
||||
int rampBelowZ12 = highwayRamp ? 1 : 0;
|
||||
|
||||
FeatureCollector.Feature feature = features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
// main attributes at all zoom levels (used for grouping <= z8)
|
||||
.setAttr(Fields.CLASS, highwayClass)
|
||||
.setAttr(Fields.SUBCLASS, highwaySubclass(highwayClass, element.publicTransport(), highway))
|
||||
.setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()))
|
||||
// rest at z9+
|
||||
.setAttrWithMinzoom(Fields.SERVICE, service(element.service()), 12)
|
||||
.setAttrWithMinzoom(Fields.ONEWAY, element.isOneway(), 12)
|
||||
.setAttr(Fields.RAMP, minzoom >= 12 ? rampAboveZ12 :
|
||||
((ZoomFunction<Integer>) z -> z < 9 ? null : z >= 12 ? rampAboveZ12 : rampBelowZ12))
|
||||
.setAttrWithMinzoom(Fields.LAYER, nullIf(element.layer(), 0), 9)
|
||||
.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.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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, element.isOneway())
|
||||
.setAttr(Fields.RAMP, element.isRamp() ? 1 : 0)
|
||||
.setAttrWithMinzoom(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()), 10)
|
||||
.setAttrWithMinzoom(Fields.LAYER, nullIf(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, element.isOneway())
|
||||
.setAttr(Fields.RAMP, element.isRamp() ? 1 : 0)
|
||||
.setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()))
|
||||
.setAttr(Fields.LAYER, nullIf(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, element.isOneway())
|
||||
.setAttr(Fields.RAMP, element.isRamp() ? 1 : 0)
|
||||
.setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()))
|
||||
.setAttr(Fields.LAYER, nullIf(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) ||
|
||||
// ignore underground pedestrian areas
|
||||
(element.isArea() && 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, nullIf(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), config.minFeatureSize(zoom)).doubleValue();
|
||||
return FeatureMerge.mergeLineStrings(items, minLength, tolerance, BUFFER_SIZE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.layers.Transportation.highwayClass;
|
||||
import static com.onthegomap.planetiler.basemap.layers.Transportation.highwaySubclass;
|
||||
import static com.onthegomap.planetiler.basemap.layers.Transportation.isFootwayOrSteps;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.brunnel;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIf;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
import static com.onthegomap.planetiler.util.MemoryEstimator.CLASS_HEADER_BYTES;
|
||||
import static com.onthegomap.planetiler.util.MemoryEstimator.POINTER_BYTES;
|
||||
import static com.onthegomap.planetiler.util.MemoryEstimator.estimateSize;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmElement;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmReader;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.MemoryEstimator;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import com.onthegomap.planetiler.util.ZoomFunction;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.prep.PreparedGeometry;
|
||||
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for road, shipway, rail, and path names in the {@code
|
||||
* 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.OsmHighwayLinestring.Handler,
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
BasemapProfile.FeaturePostProcessor,
|
||||
BasemapProfile.OsmRelationPreprocessor,
|
||||
BasemapProfile.IgnoreWikidata {
|
||||
|
||||
/*
|
||||
* Generate road names from OSM data. Route network 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 Logger LOGGER = LoggerFactory.getLogger(TransportationName.class);
|
||||
private static final Pattern GREAT_BRITAIN_REF_NETWORK_PATTERN = Pattern.compile("^[AM][0-9AM()]+");
|
||||
private static final ZoomFunction.MeterToPixelThresholds MIN_LENGTH = ZoomFunction.meterThresholds()
|
||||
.put(6, 20_000)
|
||||
.put(7, 20_000)
|
||||
.put(8, 14_000)
|
||||
.put(9, 8_000)
|
||||
.put(10, 8_000)
|
||||
.put(11, 8_000);
|
||||
private static final Comparator<RouteRelation> RELATION_ORDERING = Comparator
|
||||
.<RouteRelation>comparingInt(r -> r.network.ordinal())
|
||||
// TODO also compare network string?
|
||||
.thenComparingInt(r -> r.ref.length())
|
||||
.thenComparing(RouteRelation::ref);
|
||||
private final Map<String, Integer> MINZOOMS;
|
||||
private final boolean brunnel;
|
||||
private final boolean sizeForShield;
|
||||
private final boolean limitMerge;
|
||||
private final Stats stats;
|
||||
private final PlanetilerConfig config;
|
||||
private final AtomicBoolean loggedNoGb = new AtomicBoolean(false);
|
||||
private PreparedGeometry greatBritain = null;
|
||||
|
||||
public TransportationName(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.config = config;
|
||||
this.stats = stats;
|
||||
this.brunnel = config.arguments().getBoolean(
|
||||
"transportation_name_brunnel",
|
||||
"transportation_name layer: set to false to omit brunnel and help merge long highways",
|
||||
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
|
||||
);
|
||||
boolean z13Paths = config.arguments().getBoolean(
|
||||
"transportation_z13_paths",
|
||||
"transportation(_name) layer: show paths on z13",
|
||||
true
|
||||
);
|
||||
MINZOOMS = Map.of(
|
||||
FieldValues.CLASS_TRACK, 14,
|
||||
FieldValues.CLASS_PATH, z13Paths ? 13 : 14,
|
||||
FieldValues.CLASS_MINOR, 13,
|
||||
FieldValues.CLASS_TRUNK, 8,
|
||||
FieldValues.CLASS_MOTORWAY, 6
|
||||
// default: 12
|
||||
);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OsmRelationInfo> preprocessOsmRelation(OsmElement.Relation relation) {
|
||||
if (relation.hasTag("route", "road")) {
|
||||
RouteNetwork networkType = null;
|
||||
String network = relation.getString("network");
|
||||
String ref = relation.getString("ref");
|
||||
|
||||
if ("US:I".equals(network)) {
|
||||
networkType = RouteNetwork.US_INTERSTATE;
|
||||
} else if ("US:US".equals(network)) {
|
||||
networkType = RouteNetwork.US_HIGHWAY;
|
||||
} else if (network != null && network.length() == 5 && network.startsWith("US:")) {
|
||||
networkType = RouteNetwork.US_STATE;
|
||||
} else if (network != null && network.startsWith("CA:transcanada")) {
|
||||
networkType = RouteNetwork.CA_TRANSCANADA;
|
||||
}
|
||||
|
||||
if (networkType != null) {
|
||||
return List.of(new RouteRelation(coalesce(ref, ""), networkType, relation.id()));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmHighwayLinestring element, FeatureCollector features) {
|
||||
List<OsmReader.RelationMember<RouteRelation>> relations = element.source()
|
||||
.relationInfo(RouteRelation.class);
|
||||
|
||||
String ref = element.ref();
|
||||
RouteRelation relation = getRouteRelation(element, relations, ref);
|
||||
if (relation != null && nullIfEmpty(relation.ref) != null) {
|
||||
ref = relation.ref;
|
||||
}
|
||||
|
||||
String name = nullIfEmpty(element.name());
|
||||
ref = nullIfEmpty(ref);
|
||||
String highway = nullIfEmpty(element.highway());
|
||||
|
||||
String highwayClass = highwayClass(element.highway(), null, element.construction(), element.manMade());
|
||||
if (element.isArea() || highway == null || highwayClass == null || (name == null && ref == null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String baseClass = highwayClass.replace("_construction", "");
|
||||
|
||||
int minzoom = MINZOOMS.getOrDefault(baseClass, 12);
|
||||
boolean isLink = highway.endsWith("_link");
|
||||
if (isLink) {
|
||||
minzoom = Math.max(13, minzoom);
|
||||
}
|
||||
|
||||
FeatureCollector.Feature feature = features.line(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setBufferPixelOverrides(MIN_LENGTH)
|
||||
// TODO abbreviate road names - can't port osml10n because it is AGPL
|
||||
.putAttrs(LanguageUtils.getNamesWithoutTranslations(element.source().tags()))
|
||||
.setAttr(Fields.REF, ref)
|
||||
.setAttr(Fields.REF_LENGTH, ref != null ? ref.length() : null)
|
||||
.setAttr(Fields.NETWORK,
|
||||
(relation != null && relation.network != null) ? relation.network.name : ref != null ? "road" : null)
|
||||
.setAttr(Fields.CLASS, highwayClass)
|
||||
.setAttr(Fields.SUBCLASS, highwaySubclass(highwayClass, null, highway))
|
||||
.setMinPixelSize(0)
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinZoom(minzoom);
|
||||
|
||||
if (brunnel) {
|
||||
feature.setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()));
|
||||
}
|
||||
|
||||
/*
|
||||
* to help group roads into longer segments, add temporary tags to limit which segments get grouped together. Since
|
||||
* a divided highway typically has a separate relation for each direction, this ends up keeping segments going
|
||||
* opposite directions group getting grouped together and confusing the line merging process
|
||||
*/
|
||||
if (limitMerge) {
|
||||
feature
|
||||
.setAttr(LINK_TEMP_KEY, isLink ? 1 : 0)
|
||||
.setAttr(RELATION_ID_TEMP_KEY, relation == null ? null : relation.id);
|
||||
}
|
||||
|
||||
if (isFootwayOrSteps(highway)) {
|
||||
feature
|
||||
.setAttrWithMinzoom(Fields.LAYER, nullIf(element.layer(), 0), 12)
|
||||
.setAttrWithMinzoom(Fields.LEVEL, Parse.parseLongOrNull(element.source().getTag("level")), 12)
|
||||
.setAttrWithMinzoom(Fields.INDOOR, element.indoor() ? 1 : null, 12);
|
||||
}
|
||||
}
|
||||
|
||||
private RouteRelation getRouteRelation(Tables.OsmHighwayLinestring element,
|
||||
List<OsmReader.RelationMember<RouteRelation>> relations, String ref) {
|
||||
RouteRelation relation = relations.stream()
|
||||
.map(OsmReader.RelationMember::relation)
|
||||
.min(RELATION_ORDERING)
|
||||
.orElse(null);
|
||||
if (relation == null && ref != null) {
|
||||
// GB doesn't use regular relations like everywhere else, so if we are
|
||||
// in GB then use a naming convention instead.
|
||||
Matcher refMatcher = GREAT_BRITAIN_REF_NETWORK_PATTERN.matcher(ref);
|
||||
if (refMatcher.find()) {
|
||||
if (greatBritain == null) {
|
||||
if (!loggedNoGb.get() && loggedNoGb.compareAndSet(false, true)) {
|
||||
LOGGER.warn("No GB polygon for inferring route network types");
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Geometry wayGeometry = element.source().worldGeometry();
|
||||
if (greatBritain.intersects(wayGeometry)) {
|
||||
RouteNetwork networkType =
|
||||
"motorway".equals(element.highway()) ? RouteNetwork.GB_MOTORWAY : RouteNetwork.GB_TRUNK;
|
||||
relation = new RouteRelation(refMatcher.group(), networkType, 0);
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_transportation_name_gb_test",
|
||||
"Unable to test highway against GB route network: " + element.source().id());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return relation;
|
||||
}
|
||||
|
||||
@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 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;
|
||||
}
|
||||
}
|
||||
|
||||
/** Information extracted from route relations to use when processing ways in that relation. */
|
||||
private static record RouteRelation(
|
||||
String ref,
|
||||
RouteNetwork network,
|
||||
@Override long id
|
||||
) implements OsmRelationInfo {
|
||||
|
||||
@Override
|
||||
public long estimateMemoryUsageBytes() {
|
||||
return CLASS_HEADER_BYTES +
|
||||
POINTER_BYTES + estimateSize(ref) +
|
||||
POINTER_BYTES + // network
|
||||
MemoryEstimator.estimateSizeLong(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.Utils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
BasemapProfile.OsmWaterPolygonProcessor {
|
||||
|
||||
/*
|
||||
* 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;
|
||||
|
||||
public Water(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.classMapping = FieldMappings.Class.index();
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
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())) {
|
||||
features.polygon(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setMinPixelSizeBelowZoom(11, 2)
|
||||
.setMinZoom(6)
|
||||
.setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0)
|
||||
.setAttrWithMinzoom(Fields.BRUNNEL, Utils.brunnel(element.isBridge(), element.isTunnel()), 12)
|
||||
.setAttr(Fields.CLASS, classMapping.getOrElse(element.source(), FieldValues.CLASS_RIVER));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.carrotsearch.hppc.LongObjectMap;
|
||||
import com.graphhopper.coll.GHLongObjectHashMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.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.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,
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
BasemapProfile.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 = new GHLongObjectHashMap<>();
|
||||
// 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) {
|
||||
lakeCenterlines.put(osmId, feature.worldGeometry());
|
||||
}
|
||||
} 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(LanguageUtils.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, 6 * 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(LanguageUtils.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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.basemap.util.Utils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
BasemapProfile.FeaturePostProcessor,
|
||||
BasemapProfile.NaturalEarthProcessor {
|
||||
|
||||
/*
|
||||
* 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 final Translations translations;
|
||||
private final PlanetilerConfig config;
|
||||
|
||||
public Waterway(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.config = config;
|
||||
this.translations = translations;
|
||||
}
|
||||
|
||||
private static final Map<String, Integer> CLASS_MINZOOM = Map.of(
|
||||
"river", 12,
|
||||
"canal", 12,
|
||||
|
||||
"stream", 13,
|
||||
"drain", 13,
|
||||
"ditch", 13
|
||||
);
|
||||
|
||||
private static final ZoomFunction.MeterToPixelThresholds MIN_PIXEL_LENGTHS = ZoomFunction.meterThresholds()
|
||||
.put(9, 8_000)
|
||||
.put(10, 4_000)
|
||||
.put(11, 1_000);
|
||||
|
||||
@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);
|
||||
case "ne_10m_rivers_lake_centerlines" -> new ZoomRange(6, 8);
|
||||
default -> null;
|
||||
};
|
||||
if (zoom != null) {
|
||||
features.line(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_RIVER)
|
||||
.setZoomRange(zoom.min, zoom.max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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(LanguageUtils.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 >= 9 && zoom <= 11) {
|
||||
return FeatureMerge.mergeLineStrings(
|
||||
items,
|
||||
MIN_PIXEL_LENGTHS.apply(zoom).doubleValue(),
|
||||
config.tolerance(zoom),
|
||||
BUFFER_SIZE
|
||||
);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.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 com.onthegomap.planetiler.basemap.util;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Utilities to extract common name fields (name, name_en, name_de, name:latin, name:nonlatin, name_int) that the
|
||||
* OpenMapTiles schema uses across any map element with a name.
|
||||
* <p>
|
||||
* Ported from <a href="https://github.com/openmaptiles/openmaptiles-tools/blob/master/sql/zzz_language.sql">openmaptiles-tools</a>.
|
||||
*/
|
||||
public class LanguageUtils {
|
||||
|
||||
private static final Pattern NONLATIN = Pattern
|
||||
.compile("[^\\x{0000}-\\x{024f}\\x{1E00}-\\x{1EFF}\\x{0300}-\\x{036f}\\x{0259}]");
|
||||
private static final Pattern LETTER = Pattern.compile("[A-Za-zÀ-ÖØ-öø-ÿĀ-ɏ]+");
|
||||
private static final Pattern EMPTY_PARENS = Pattern.compile("(\\([ -.]*\\)|\\[[ -.]*])");
|
||||
private static final Pattern LEADING_TRAILING_JUNK = Pattern.compile("(^\\s*([./-]\\s*)*|(\\s+[./-])*\\s*$)");
|
||||
private static final Pattern WHITESPACE = Pattern.compile("\\s+");
|
||||
private static final Set<String> EN_DE_NAME_KEYS = Set.of("name:en", "name:de");
|
||||
|
||||
private static void putIfNotEmpty(Map<String, Object> dest, String key, Object value) {
|
||||
if (value != null && !value.equals("")) {
|
||||
dest.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static String string(Object obj) {
|
||||
return nullIfEmpty(obj == null ? null : obj.toString());
|
||||
}
|
||||
|
||||
static boolean containsOnlyLatinCharacters(String string) {
|
||||
return string != null && !NONLATIN.matcher(string).find();
|
||||
}
|
||||
|
||||
private static String transliteratedName(Map<String, Object> tags) {
|
||||
return Translations.transliterate(string(tags.get("name")));
|
||||
}
|
||||
|
||||
static String removeLatinCharacters(String name) {
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
var matcher = LETTER.matcher(name);
|
||||
if (matcher.find()) {
|
||||
String result = matcher.replaceAll("");
|
||||
// if the name was "<nonlatin text> (<latin description)"
|
||||
// or "<nonlatin text> - <latin description>"
|
||||
// then remove any of those extra characters now
|
||||
result = EMPTY_PARENS.matcher(result).replaceAll("");
|
||||
result = LEADING_TRAILING_JUNK.matcher(result).replaceAll("");
|
||||
return WHITESPACE.matcher(result).replaceAll(" ");
|
||||
}
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map with default name attributes (name, name_en, name_de, name:latin, name:nonlatin, name_int) that every
|
||||
* element should have, derived from name, int_name, name:en, and name:de tags on the input element.
|
||||
*
|
||||
* <ul>
|
||||
* <li>name is the original name value from the element</li>
|
||||
* <li>name_en is the original name:en value from the element, or name if missing</li>
|
||||
* <li>name_de is the original name:de value from the element, or name/ name_en if missing</li>
|
||||
* <li>name:latin is the first of name, int_name, or any name: attribute that contains only latin characters</li>
|
||||
* <li>name:nonlatin is any nonlatin part of name if present</li>
|
||||
* <li>name_int is the first of int_name name:en name:latin name</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static Map<String, Object> getNamesWithoutTranslations(Map<String, Object> tags) {
|
||||
return getNames(tags, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map with default name attributes that {@link #getNamesWithoutTranslations(Map)} adds, but also
|
||||
* translations for every language that {@code translations} is configured to handle.
|
||||
*/
|
||||
public static Map<String, Object> getNames(Map<String, Object> tags, Translations translations) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
String name = string(tags.get("name"));
|
||||
String intName = string(tags.get("int_name"));
|
||||
String nameEn = string(tags.get("name:en"));
|
||||
String nameDe = string(tags.get("name:de"));
|
||||
|
||||
boolean isLatin = containsOnlyLatinCharacters(name);
|
||||
String latin = isLatin ? name
|
||||
: Stream.concat(Stream.of(nameEn, intName, nameDe), getAllNameTranslationsBesidesEnglishAndGerman(tags))
|
||||
.filter(LanguageUtils::containsOnlyLatinCharacters)
|
||||
.findFirst().orElse(null);
|
||||
if (latin == null && translations != null && translations.getShouldTransliterate()) {
|
||||
latin = transliteratedName(tags);
|
||||
}
|
||||
String nonLatin = isLatin ? null : removeLatinCharacters(name);
|
||||
if (coalesce(nonLatin, "").equals(latin)) {
|
||||
nonLatin = null;
|
||||
}
|
||||
|
||||
putIfNotEmpty(result, "name", name);
|
||||
putIfNotEmpty(result, "name_en", coalesce(nameEn, name));
|
||||
putIfNotEmpty(result, "name_de", coalesce(nameDe, name, nameEn));
|
||||
putIfNotEmpty(result, "name:latin", latin);
|
||||
putIfNotEmpty(result, "name:nonlatin", nonLatin);
|
||||
putIfNotEmpty(result, "name_int", coalesce(
|
||||
intName,
|
||||
nameEn,
|
||||
latin,
|
||||
name
|
||||
));
|
||||
|
||||
if (translations != null) {
|
||||
translations.addTranslations(result, tags);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Stream<String> getAllNameTranslationsBesidesEnglishAndGerman(Map<String, Object> tags) {
|
||||
return tags.entrySet().stream()
|
||||
.filter(e -> {
|
||||
String key = e.getKey();
|
||||
return key.startsWith("name:") && !EN_DE_NAME_KEYS.contains(key);
|
||||
})
|
||||
.map(Map.Entry::getValue)
|
||||
.map(LanguageUtils::string);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.onthegomap.planetiler.basemap.util;
|
||||
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Common utilities for working with data and the OpenMapTiles schema in {@code layers} implementations.
|
||||
*/
|
||||
public class Utils {
|
||||
|
||||
public static <T> T coalesce(T a, T b) {
|
||||
return a != null ? a : b;
|
||||
}
|
||||
|
||||
public static <T> T coalesce(T a, T b, T c) {
|
||||
return a != null ? a : b != null ? b : c;
|
||||
}
|
||||
|
||||
public static <T> T coalesce(T a, T b, T c, T d) {
|
||||
return a != null ? a : b != null ? b : c != null ? c : d;
|
||||
}
|
||||
|
||||
public static <T> T coalesce(T a, T b, T c, T d, T e) {
|
||||
return a != null ? a : b != null ? b : c != null ? c : d != null ? d : e;
|
||||
}
|
||||
|
||||
public static <T> T coalesce(T a, T b, T c, T d, T e, T f) {
|
||||
return a != null ? a : b != null ? b : c != null ? c : d != null ? d : e != null ? e : f;
|
||||
}
|
||||
|
||||
/** Returns {@code a} or {@code nullValue} if {@code a} is null. */
|
||||
public static <T> T nullIf(T a, T nullValue) {
|
||||
return nullValue.equals(a) ? null : a;
|
||||
}
|
||||
|
||||
/** Returns {@code a}, or null if {@code a} is "". */
|
||||
public static String nullIfEmpty(String a) {
|
||||
return (a == null || a.isEmpty()) ? null : a;
|
||||
}
|
||||
|
||||
/** Returns true if {@code a} is null, or its {@link Object#toString()} value is "". */
|
||||
public static boolean nullOrEmpty(Object a) {
|
||||
return a == null || a.toString().isEmpty();
|
||||
}
|
||||
|
||||
/** Returns a map with {@code ele} (meters) and {ele_ft} attributes from an elevation in meters. */
|
||||
public static Map<String, Object> elevationTags(int meters) {
|
||||
return Map.of(
|
||||
"ele", meters,
|
||||
"ele_ft", (int) Math.round(meters * 3.2808399)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map with {@code ele} (meters) and {ele_ft} attributes from an elevation string in meters, if {@code
|
||||
* meters} can be parsed as a valid number.
|
||||
*/
|
||||
public static Map<String, Object> elevationTags(String meters) {
|
||||
Integer ele = Parse.parseIntSubstring(meters);
|
||||
return ele == null ? Map.of() : elevationTags(ele);
|
||||
}
|
||||
|
||||
/** Returns "bridge" or "tunnel" string used for "brunnel" attribute by OpenMapTiles schema. */
|
||||
public static String brunnel(boolean isBridge, boolean isTunnel) {
|
||||
return brunnel(isBridge, isTunnel, false);
|
||||
}
|
||||
|
||||
/** Returns "bridge" or "tunnel" or "ford" string used for "brunnel" attribute by OpenMapTiles schema. */
|
||||
public static String brunnel(boolean isBridge, boolean isTunnel, boolean isFord) {
|
||||
return isBridge ? "bridge" : isTunnel ? "tunnel" : isFord ? "ford" : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.onthegomap.planetiler.basemap.util;
|
||||
|
||||
import com.onthegomap.planetiler.mbtiles.Mbtiles;
|
||||
import com.onthegomap.planetiler.mbtiles.Verify;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import org.locationtech.jts.geom.Envelope;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
|
||||
/**
|
||||
* A utility to check the contents of an mbtiles file generated for Monaco.
|
||||
*/
|
||||
public class VerifyMonaco {
|
||||
|
||||
public static final Envelope MONACO_BOUNDS = new Envelope(7.40921, 7.44864, 43.72335, 43.75169);
|
||||
|
||||
/**
|
||||
* Returns a verification result with a basic set of checks against an openmaptiles map built from an extract for
|
||||
* Monaco.
|
||||
*/
|
||||
public static Verify verify(Mbtiles mbtiles) {
|
||||
Verify verify = Verify.verify(mbtiles);
|
||||
verify.checkMinFeatureCount(MONACO_BOUNDS, "building", Map.of(), 13, 14, 100, Polygon.class);
|
||||
verify.checkMinFeatureCount(MONACO_BOUNDS, "transportation", Map.of(), 10, 14, 5, LineString.class);
|
||||
verify.checkMinFeatureCount(MONACO_BOUNDS, "landcover", Map.of(
|
||||
"class", "grass",
|
||||
"subclass", "park"
|
||||
), 14, 10, Polygon.class);
|
||||
verify.checkMinFeatureCount(MONACO_BOUNDS, "water", Map.of("class", "ocean"), 0, 14, 1, Polygon.class);
|
||||
verify.checkMinFeatureCount(MONACO_BOUNDS, "place", Map.of("class", "country"), 2, 14, 1, Point.class);
|
||||
return verify;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
try (var mbtiles = Mbtiles.newReadOnlyDatabase(Path.of(args[0]))) {
|
||||
var result = verify(mbtiles);
|
||||
result.print();
|
||||
result.failIfErrors();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user