From 496d4f21eec0a123a16ee38a766089b346c4f841 Mon Sep 17 00:00:00 2001 From: Michael Barry Date: Thu, 23 Dec 2021 05:42:24 -0500 Subject: [PATCH] Change name to Planetiler (#40) * change name from flatmap to planetiler * bump version to 0.2-SNAPSHOT --- README.md | 58 + pom.xml | 57 + .../planetiler/basemap/BasemapMain.java | 52 + .../planetiler/basemap/BasemapProfile.java | 232 ++ .../planetiler/basemap/Generate.java | 757 +++++++ .../onthegomap/planetiler/basemap/Layer.java | 8 + .../basemap/generated/OpenMapTilesSchema.java | 1858 +++++++++++++++++ .../planetiler/basemap/generated/Tables.java | 1594 ++++++++++++++ .../basemap/layers/AerodromeLabel.java | 79 + .../planetiler/basemap/layers/Aeroway.java | 84 + .../planetiler/basemap/layers/Boundary.java | 470 +++++ .../planetiler/basemap/layers/Building.java | 192 ++ .../basemap/layers/Housenumber.java | 65 + .../planetiler/basemap/layers/Landcover.java | 182 ++ .../planetiler/basemap/layers/Landuse.java | 110 + .../basemap/layers/MountainPeak.java | 137 ++ .../planetiler/basemap/layers/Park.java | 152 ++ .../planetiler/basemap/layers/Place.java | 428 ++++ .../planetiler/basemap/layers/Poi.java | 195 ++ .../basemap/layers/Transportation.java | 336 +++ .../basemap/layers/TransportationName.java | 375 ++++ .../planetiler/basemap/layers/Water.java | 115 + .../planetiler/basemap/layers/WaterName.java | 195 ++ .../planetiler/basemap/layers/Waterway.java | 150 ++ .../basemap/util/LanguageUtils.java | 170 ++ .../planetiler/basemap/util/Utils.java | 73 + .../planetiler/basemap/util/VerifyMonaco.java | 44 + .../basemap/BasemapProfileTest.java | 38 + .../planetiler/basemap/BasemapTest.java | 225 ++ .../planetiler/basemap/GenerateTest.java | 227 ++ .../basemap/layers/AbstractLayerTest.java | 214 ++ .../basemap/layers/AerodromeLabelTest.java | 121 ++ .../basemap/layers/AerowayTest.java | 92 + .../basemap/layers/BoundaryTest.java | 622 ++++++ .../basemap/layers/BuildingTest.java | 171 ++ .../basemap/layers/HousenumberTest.java | 30 + .../basemap/layers/LandcoverTest.java | 201 ++ .../basemap/layers/LanduseTest.java | 82 + .../basemap/layers/MountainPeakTest.java | 222 ++ .../planetiler/basemap/layers/ParkTest.java | 135 ++ .../planetiler/basemap/layers/PlaceTest.java | 485 +++++ .../planetiler/basemap/layers/PoiTest.java | 184 ++ .../basemap/layers/TransportationTest.java | 721 +++++++ .../basemap/layers/WaterNameTest.java | 155 ++ .../planetiler/basemap/layers/WaterTest.java | 225 ++ .../basemap/layers/WaterwayTest.java | 186 ++ .../basemap/util/LanguageUtilsTest.java | 193 ++ .../basemap/util/VerifyMonacoTest.java | 62 + 48 files changed, 12759 insertions(+) create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/BasemapMain.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/BasemapProfile.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/Generate.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/Layer.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/generated/OpenMapTilesSchema.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/generated/Tables.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/layers/AerodromeLabel.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/layers/Aeroway.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/layers/Boundary.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/layers/Building.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/layers/Housenumber.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/layers/Landcover.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/layers/Landuse.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/layers/MountainPeak.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/layers/Park.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/layers/Place.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/layers/Poi.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/layers/Transportation.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/layers/TransportationName.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/layers/Water.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/layers/WaterName.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/layers/Waterway.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/util/LanguageUtils.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/util/Utils.java create mode 100644 src/main/java/com/onthegomap/planetiler/basemap/util/VerifyMonaco.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/BasemapProfileTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/BasemapTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/GenerateTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/layers/AbstractLayerTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/layers/AerodromeLabelTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/layers/AerowayTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/layers/BoundaryTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/layers/BuildingTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/layers/HousenumberTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/layers/LandcoverTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/layers/LanduseTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/layers/MountainPeakTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/layers/ParkTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/layers/PlaceTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/layers/PoiTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/layers/TransportationTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/layers/WaterNameTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/layers/WaterTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/layers/WaterwayTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/util/LanguageUtilsTest.java create mode 100644 src/test/java/com/onthegomap/planetiler/basemap/util/VerifyMonacoTest.java diff --git a/README.md b/README.md new file mode 100644 index 0000000..23dda83 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# Planetiler Basemap Profile + +This basemap profile is based on [OpenMapTiles](https://github.com/openmaptiles/openmaptiles) v3.12.2. +See [README.md](../README.md) in the parent directory for instructions on how to run. + +## Differences from OpenMapTiles + +- Road name abbreviations are not implemented yet in the `transportation_name` layer +- `agg_stop` tag not implemented yet in the `poi` layer +- Paths are visible at z13 and z14 in `transportation` and `transportation_name` layers instead of just z14 in + OpenMapTiles, to revert this behavior set `--transportation-z13-paths=false` +- `brunnel` tag is excluded from `transportation_name` layer to avoid breaking apart long `transportation_name` + lines, to revert this behavior set `--transportation-name-brunnel=true` + +## Code Layout + +[Generate.java](./src/main/java/com/onthegomap/planetiler/basemap/Generate.java) generates code in +the [generated](./src/main/java/com/onthegomap/planetiler/basemap/generated) package from an OpenMapTiles tag in GitHub: + +- [OpenMapTilesSchema](./src/main/java/com/onthegomap/planetiler/basemap/generated/OpenMapTilesSchema.java) + contains an interface for each layer with constants for the name, attributes, and allowed values for each tag in that + layer +- [Tables](./src/main/java/com/onthegomap/planetiler/basemap/generated/Tables.java) + contains a record for each table that OpenMapTiles [imposm3](https://github.com/omniscale/imposm3) configuration + generates (along with the tag-filtering expression) so layers can listen on instances of those records instead of + doing the tag filtering and parsing themselves + +The [layers](./src/main/java/com/onthegomap/planetiler/basemap/layers) package contains a port of the SQL logic to +generate each layer from OpenMapTiles. Layers define how source features (or parsed imposm3 table rows) map to vector +tile features, and logic for post-processing tile geometries. + +[BasemapProfile](./src/main/java/com/onthegomap/planetiler/basemap/BasemapProfile.java) dispatches source features to +layer handlers and merges the results. + +[BasemapMain](./src/main/java/com/onthegomap/planetiler/basemap/BasemapMain.java) is the main driver that registers +source data and output location. + +## Regenerating Code + +To run `Generate.java`, use [scripts/regenerate-openmaptiles.sh](../scripts/regenerate-openmaptiles.sh) script with the +OpenMapTiles release tag: + +```bash +./scripts/regenerate-openmaptiles.sh v3.12.2 +``` + +Then follow the instructions it prints for reformatting generated code. + +## License and Attribution + +OpenMapTiles code is licensed under the BSD 3-Clause License, which appears at the top of any file ported from +OpenMapTiles. + +The OpenMapTiles schema (or "look and feel") is licensed under [CC-BY 4.0](http://creativecommons.org/licenses/by/4.0/), +so any map derived from that schema +must [visibly credit OpenMapTiles](https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md#design-license-cc-by-40) +. It also uses OpenStreetMap data, so you +must [visibly credit OpenStreetMap contributors](https://www.openstreetmap.org/copyright). diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..78ab64a --- /dev/null +++ b/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + planetiler-basemap + + + com.onthegomap.planetiler + planetiler-parent + 0.2-SNAPSHOT + + + + + com.onthegomap.planetiler + planetiler-core + ${project.parent.version} + + + org.yaml + snakeyaml + 1.30 + + + org.commonmark + commonmark + 0.18.1 + + + + com.onthegomap.planetiler + planetiler-core + ${project.parent.version} + test-jar + test + + + + + + + io.github.zlika + reproducible-build-maven-plugin + + + + maven-deploy-plugin + + + true + + + + + diff --git a/src/main/java/com/onthegomap/planetiler/basemap/BasemapMain.java b/src/main/java/com/onthegomap/planetiler/basemap/BasemapMain.java new file mode 100644 index 0000000..087674e --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/BasemapMain.java @@ -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(); + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/BasemapProfile.java b/src/main/java/com/onthegomap/planetiler/basemap/BasemapProfile.java new file mode 100644 index 0000000..3cffa8c --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/BasemapProfile.java @@ -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. + *

+ * Layer implementations extend these interfaces to subscribe to elements from different sources: + *

+ * 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 osmMappings; + /** Index variant that filters out any table only used by layers that implement IgnoreWikidata class. */ + private final MultiExpression.Index wikidataMappings; + + public BasemapProfile(Planetiler runner) { + this(runner.translations(), runner.config(), runner.stats()); + } + + public BasemapProfile(Translations translations, PlanetilerConfig config, Stats stats) { + List onlyLayers = config.arguments().getList("only_layers", "Include only certain layers", List.of()); + List excludeLayers = config.arguments().getList("exclude_layers", "Exclude certain layers", List.of()); + + // register release/finish/feature postprocessor/osm relationship handler methods... + List 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) 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 handler : rowDispatch.handlers()) { + handler.process(row, features); + } + } + }); + } + } + + /** Returns the imposm3 table row constructors that match an input element's tags. */ + public List> 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 natural + * earth. + */ + public interface NaturalEarthProcessor { + + /** + * Process an element from {@code table} in thenatural earth + * source. + * + * @see Profile#processFeature(SourceFeature, FeatureCollector) + */ + void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features); + } + + /** + * Layers should implement this interface to subscribe to elements from OSM + * lake centerlines source. + */ + public interface LakeCenterlineProcessor { + + /** + * Process an element from the OSM lake centerlines + * source + * + * @see Profile#processFeature(SourceFeature, FeatureCollector) + */ + void processLakeCenterline(SourceFeature feature, FeatureCollector features); + } + + /** + * Layers should implement this interface to subscribe to elements from OSM + * water polygons source. + */ + public interface OsmWaterPolygonProcessor { + + /** + * Process an element from the OSM water + * polygons source + * + * @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> handlers + ) {} +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/Generate.java b/src/main/java/com/onthegomap/planetiler/basemap/Generate.java new file mode 100644 index 0000000..25b0a62 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/Generate.java @@ -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 OpenMapTiles GitHub repo. + *

+ * {@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. + *

+ * {@code Tables.java} contains the imposm3 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. + *

+ * 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 loadAndParseYaml(String url, PlanetilerConfig config, Class 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 parsed = yaml.load(stream); + return mapper.convertValue(parsed, clazz); + } + } + + static T parseYaml(String string, Class 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 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//.yaml file that it references + // then crawl table definitions from each layers//mapping.yaml file that the layer references + String rootUrl = base + "openmaptiles.yaml"; + OpenmaptilesConfig config = loadAndParseYaml(rootUrl, planetilerConfig, OpenmaptilesConfig.class); + + List layers = new ArrayList<>(); + Set 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 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 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 + * OpenMapTiles vector tile schema %s. + */ + @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 LANGUAGES = List.of(%s); + + /** Returns a list of expected layer implementation instances from the {@code layers} package. */ + public static List 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 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 + *

+ * allowed values: + *

    + * %s + *
+ */ + """.stripTrailing().formatted(javadocDescription, + valuesForComment.stream().map(v -> "
  • " + v).collect(joining(LINE_SEPARATOR + " * "))), + name.toUpperCase(Locale.ROOT), + Format.quote(name) + ).indent(4)); + + List 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 %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 mapping = generateFieldMapping(valuesNode); + fieldMappings.append(" public static final MultiExpression %s = %s;%n" + .formatted(lowerUnderscoreToUpperCamel(name), generateJavaCode(mapping))); + } + }); + + return """ + /** + * %s + * + * Generated from %s.yaml + */ + 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 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 imposm3 table definitions + * in the OpenMapTiles vector tile schema. + * + * 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 rowClass, + Constructor create + ) {} + + /** A functional interface that the typed handler method that a layer implementation can be coerced to. */ + @FunctionalInterface + public interface RowHandler { + + /** 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( + Class handlerClass, + RowHandler handler + ) {} + """.formatted(GENERATED_FILE_HEADER, packageName, escapeJavadoc(tag))); + + List classNames = new ArrayList<>(); + Map fieldNameToType = new TreeMap<>(); + for (var entry : tables.entrySet()) { + String key = entry.getKey(); + Imposm3Table table = entry.getValue(); + List 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 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, List>> generateDispatchMap(List handlers) { + Map, List>> 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 Imposm3 + * Data Mapping 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 Imposm3 + * Data Mapping filters 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 parseTableFields(Imposm3Table tableDefinition) { + List 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 generateFieldMapping(JsonNode valuesNode) { + MultiExpression 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 parseFieldMappingExpression(JsonNode node) { + if (node.isObject()) { + List 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 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}. + *

    + * For example: {@code [[[a, b], c], [d]} becomes {@code [a, b, c, d]} + *

    + * And {@code a} becomes {@code [a]} + */ + private static Stream 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 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 List iterToList(Iterator iter) { + List 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("(^

    |

    $)", "").strip()) + .collect(joining(LINE_SEPARATOR + "

    " + 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 layers, + String version, + String attribution, + String name, + String description, + List languages + ) {} + + @JsonIgnoreProperties(ignoreUnknown = true) + private static record LayerDetails( + String id, + String description, + Map fields, + double buffer_size + ) {} + + private static record Datasource( + String type, + String mapping_file + ) {} + + @JsonIgnoreProperties(ignoreUnknown = true) + private static record LayerConfig( + LayerDetails layer, + List 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 columns, + Imposm3Filters filters, + JsonNode mapping, + Map type_mappings + ) {} + + @JsonIgnoreProperties(ignoreUnknown = true) + private static record Imposm3Mapping( + Map tables + ) {} + + private static record OsmTableField( + String clazz, + String name, + String extractCode + ) {} +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/Layer.java b/src/main/java/com/onthegomap/planetiler/basemap/Layer.java new file mode 100644 index 0000000..2044659 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/Layer.java @@ -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 {} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/generated/OpenMapTilesSchema.java b/src/main/java/com/onthegomap/planetiler/basemap/generated/OpenMapTilesSchema.java new file mode 100644 index 0000000..8878a0e --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/generated/OpenMapTilesSchema.java @@ -0,0 +1,1858 @@ +/* +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 + +package com.onthegomap.planetiler.basemap.generated; + +import static com.onthegomap.planetiler.expression.Expression.FALSE; +import static com.onthegomap.planetiler.expression.Expression.and; +import static com.onthegomap.planetiler.expression.Expression.matchAny; +import static com.onthegomap.planetiler.expression.Expression.or; + +import com.onthegomap.planetiler.basemap.Layer; +import com.onthegomap.planetiler.config.PlanetilerConfig; +import com.onthegomap.planetiler.expression.MultiExpression; +import com.onthegomap.planetiler.stats.Stats; +import com.onthegomap.planetiler.util.Translations; +import java.util.List; +import java.util.Set; + +/** + * All vector tile layer definitions, attributes, and allowed values generated from the + * OpenMapTiles vector tile + * schema + * v3.12.2. + */ +@SuppressWarnings("unused") +public class OpenMapTilesSchema { + + public static final String NAME = "OpenMapTiles"; + public static final String DESCRIPTION = "A tileset showcasing all layers in OpenMapTiles. https://openmaptiles.org"; + public static final String VERSION = "3.12.1"; + public static final String ATTRIBUTION = "© OpenMapTiles © OpenStreetMap contributors"; + public static final List LANGUAGES = List.of("am", "ar", "az", "be", "bg", "br", "bs", "ca", "co", "cs", "cy", + "da", "de", "el", "en", "eo", "es", "et", "eu", "fi", "fr", "fy", "ga", "gd", "he", "hi", "hr", "hu", "hy", "id", + "is", "it", "ja", "ja_kana", "ja_rm", "ja-Latn", "ja-Hira", "ka", "kk", "kn", "ko", "ko-Latn", "ku", "la", "lb", + "lt", "lv", "mk", "mt", "ml", "nl", "no", "oc", "pl", "pt", "rm", "ro", "ru", "sk", "sl", "sq", "sr", "sr-Latn", + "sv", "ta", "te", "th", "tr", "uk", "zh"); + + /** Returns a list of expected layer implementation instances from the {@code layers} package. */ + public static List createInstances(Translations translations, PlanetilerConfig config, Stats stats) { + return List.of( + new com.onthegomap.planetiler.basemap.layers.Water(translations, config, stats), + new com.onthegomap.planetiler.basemap.layers.Waterway(translations, config, stats), + new com.onthegomap.planetiler.basemap.layers.Landcover(translations, config, stats), + new com.onthegomap.planetiler.basemap.layers.Landuse(translations, config, stats), + new com.onthegomap.planetiler.basemap.layers.MountainPeak(translations, config, stats), + new com.onthegomap.planetiler.basemap.layers.Park(translations, config, stats), + new com.onthegomap.planetiler.basemap.layers.Boundary(translations, config, stats), + new com.onthegomap.planetiler.basemap.layers.Aeroway(translations, config, stats), + new com.onthegomap.planetiler.basemap.layers.Transportation(translations, config, stats), + new com.onthegomap.planetiler.basemap.layers.Building(translations, config, stats), + new com.onthegomap.planetiler.basemap.layers.WaterName(translations, config, stats), + new com.onthegomap.planetiler.basemap.layers.TransportationName(translations, config, stats), + new com.onthegomap.planetiler.basemap.layers.Place(translations, config, stats), + new com.onthegomap.planetiler.basemap.layers.Housenumber(translations, config, stats), + new com.onthegomap.planetiler.basemap.layers.Poi(translations, config, stats), + new com.onthegomap.planetiler.basemap.layers.AerodromeLabel(translations, config, stats) + ); + } + + /** + * Water polygons representing oceans and lakes. Covered watered areas are excluded (covered=yes). On low + * zoom levels all water originates from Natural Earth. To get a more correct display of the south pole you should + * also style the covering ice shelves over the water. On higher zoom levels water polygons from OpenStreetMapData are used. The polygons are split into many smaller + * polygons to improve rendering performance. This however can lead to less rendering options in clients since these + * boundaries show up. So you might not be able to use border styling for ocean water features. + *

    + * Generated from water.yaml + */ + public interface Water extends Layer { + + double BUFFER_SIZE = 4.0; + String LAYER_NAME = "water"; + + @Override + default String name() { + return LAYER_NAME; + } + + /** Attribute names for map elements in the water layer. */ + final class Fields { + + /** + * All water polygons from OpenStreetMapData have the class + * ocean. Water bodies are classified as lake or river for water bodies + * with the waterway tag. + *

    + * allowed values: + *

      + *
    • lake + *
    • dock + *
    • river + *
    • ocean + *
    + */ + public static final String CLASS = "class"; + + /** + * Mark with 1 if it is an intermittent + * water polygon. + *

    + * allowed values: + *

      + *
    • 0 + *
    • 1 + *
    + */ + public static final String INTERMITTENT = "intermittent"; + + /** + * Identifies the type of crossing as either a bridge or a tunnel. + *

    + * allowed values: + *

      + *
    • "bridge" + *
    • "tunnel" + *
    + */ + public static final String BRUNNEL = "brunnel"; + } + + /** Attribute values for map elements in the water layer. */ + final class FieldValues { + + public static final String CLASS_LAKE = "lake"; + public static final String CLASS_DOCK = "dock"; + public static final String CLASS_RIVER = "river"; + public static final String CLASS_OCEAN = "ocean"; + public static final Set CLASS_VALUES = Set.of("lake", "dock", "river", "ocean"); + public static final String BRUNNEL_BRIDGE = "bridge"; + public static final String BRUNNEL_TUNNEL = "tunnel"; + public static final Set BRUNNEL_VALUES = Set.of("bridge", "tunnel"); + } + + /** Complex mappings to generate attribute values from OSM element tags in the water layer. */ + final class FieldMappings { + + public static final MultiExpression Class = MultiExpression.of( + List.of(MultiExpression.entry("lake", matchAny("waterway", "", "lake")), + MultiExpression.entry("dock", matchAny("waterway", "dock")), MultiExpression.entry("river", FALSE), + MultiExpression.entry("ocean", FALSE))); + } + } + + /** + * OpenStreetMap waterways for higher zoom levels (z9 and + * more) and Natural Earth rivers and lake centerlines for low zoom levels (z3 - z8). Linestrings without a name or + * which are too short are filtered out at low zoom levels. Till z11 there is river class only, in z12 + * there is also canal generated, starting z13 there is no generalization according to class + * field applied. Waterways do not have a subclass field. + *

    + * Generated from waterway.yaml + */ + public interface Waterway extends Layer { + + double BUFFER_SIZE = 4.0; + String LAYER_NAME = "waterway"; + + @Override + default String name() { + return LAYER_NAME; + } + + /** Attribute names for map elements in the waterway layer. */ + final class Fields { + + /** + * The OSM name value of the waterway. The + * name field may be empty for NaturalEarth data or at lower zoom levels. + */ + public static final String NAME = "name"; + /** English name name:en if available, otherwise name. */ + public static final String NAME_EN = "name_en"; + /** German name name:de if available, otherwise name or name:en. */ + public static final String NAME_DE = "name_de"; + + /** + * The original value of the waterway + * tag. + *

    + * allowed values: + *

      + *
    • "stream" + *
    • "river" + *
    • "canal" + *
    • "drain" + *
    • "ditch" + *
    + */ + public static final String CLASS = "class"; + + /** + * Mark whether way is a tunnel or bridge. + *

    + * allowed values: + *

      + *
    • "bridge" + *
    • "tunnel" + *
    + */ + public static final String BRUNNEL = "brunnel"; + + /** + * Mark with 1 if it is an intermittent + * waterway. + *

    + * allowed values: + *

      + *
    • 0 + *
    • 1 + *
    + */ + public static final String INTERMITTENT = "intermittent"; + } + + /** Attribute values for map elements in the waterway layer. */ + final class FieldValues { + + public static final String CLASS_STREAM = "stream"; + public static final String CLASS_RIVER = "river"; + public static final String CLASS_CANAL = "canal"; + public static final String CLASS_DRAIN = "drain"; + public static final String CLASS_DITCH = "ditch"; + public static final Set CLASS_VALUES = Set.of("stream", "river", "canal", "drain", "ditch"); + public static final String BRUNNEL_BRIDGE = "bridge"; + public static final String BRUNNEL_TUNNEL = "tunnel"; + public static final Set BRUNNEL_VALUES = Set.of("bridge", "tunnel"); + } + + /** Complex mappings to generate attribute values from OSM element tags in the waterway layer. */ + final class FieldMappings { + + } + } + + /** + * Landcover is used to describe the physical material at the surface of the earth. At lower zoom levels this is from + * Natural Earth data for glaciers and ice shelves and at higher zoom levels the landcover is implied by OSM tags. The most common use case for this + * layer is to style wood (class=wood) and grass (class=grass) areas. + *

    + * Generated from landcover.yaml + */ + public interface Landcover extends Layer { + + double BUFFER_SIZE = 4.0; + String LAYER_NAME = "landcover"; + + @Override + default String name() { + return LAYER_NAME; + } + + /** Attribute names for map elements in the landcover layer. */ + final class Fields { + + /** + * Use the class to assign natural colors for landcover. + *

    + * allowed values: + *

      + *
    • farmland + *
    • ice + *
    • wood + *
    • rock + *
    • grass + *
    • wetland + *
    • sand + *
    + */ + public static final String CLASS = "class"; + + /** + * Use subclass to do more precise styling. Original value of either the natural, landuse, leisure, or wetland tag. + *

    + * allowed values: + *

      + *
    • "allotments" + *
    • "bare_rock" + *
    • "beach" + *
    • "bog" + *
    • "dune" + *
    • "scrub" + *
    • "farm" + *
    • "farmland" + *
    • "fell" + *
    • "forest" + *
    • "garden" + *
    • "glacier" + *
    • "grass" + *
    • "grassland" + *
    • "golf_course" + *
    • "heath" + *
    • "mangrove" + *
    • "marsh" + *
    • "meadow" + *
    • "orchard" + *
    • "park" + *
    • "plant_nursery" + *
    • "recreation_ground" + *
    • "reedbed" + *
    • "saltern" + *
    • "saltmarsh" + *
    • "sand" + *
    • "scree" + *
    • "swamp" + *
    • "tidalflat" + *
    • "tundra" + *
    • "village_green" + *
    • "vineyard" + *
    • "wet_meadow" + *
    • "wetland" + *
    • "wood" + *
    + */ + public static final String SUBCLASS = "subclass"; + } + + /** Attribute values for map elements in the landcover layer. */ + final class FieldValues { + + public static final String CLASS_FARMLAND = "farmland"; + public static final String CLASS_ICE = "ice"; + public static final String CLASS_WOOD = "wood"; + public static final String CLASS_ROCK = "rock"; + public static final String CLASS_GRASS = "grass"; + public static final String CLASS_WETLAND = "wetland"; + public static final String CLASS_SAND = "sand"; + public static final Set CLASS_VALUES = Set.of("farmland", "ice", "wood", "rock", "grass", "wetland", + "sand"); + public static final String SUBCLASS_ALLOTMENTS = "allotments"; + public static final String SUBCLASS_BARE_ROCK = "bare_rock"; + public static final String SUBCLASS_BEACH = "beach"; + public static final String SUBCLASS_BOG = "bog"; + public static final String SUBCLASS_DUNE = "dune"; + public static final String SUBCLASS_SCRUB = "scrub"; + public static final String SUBCLASS_FARM = "farm"; + public static final String SUBCLASS_FARMLAND = "farmland"; + public static final String SUBCLASS_FELL = "fell"; + public static final String SUBCLASS_FOREST = "forest"; + public static final String SUBCLASS_GARDEN = "garden"; + public static final String SUBCLASS_GLACIER = "glacier"; + public static final String SUBCLASS_GRASS = "grass"; + public static final String SUBCLASS_GRASSLAND = "grassland"; + public static final String SUBCLASS_GOLF_COURSE = "golf_course"; + public static final String SUBCLASS_HEATH = "heath"; + public static final String SUBCLASS_MANGROVE = "mangrove"; + public static final String SUBCLASS_MARSH = "marsh"; + public static final String SUBCLASS_MEADOW = "meadow"; + public static final String SUBCLASS_ORCHARD = "orchard"; + public static final String SUBCLASS_PARK = "park"; + public static final String SUBCLASS_PLANT_NURSERY = "plant_nursery"; + public static final String SUBCLASS_RECREATION_GROUND = "recreation_ground"; + public static final String SUBCLASS_REEDBED = "reedbed"; + public static final String SUBCLASS_SALTERN = "saltern"; + public static final String SUBCLASS_SALTMARSH = "saltmarsh"; + public static final String SUBCLASS_SAND = "sand"; + public static final String SUBCLASS_SCREE = "scree"; + public static final String SUBCLASS_SWAMP = "swamp"; + public static final String SUBCLASS_TIDALFLAT = "tidalflat"; + public static final String SUBCLASS_TUNDRA = "tundra"; + public static final String SUBCLASS_VILLAGE_GREEN = "village_green"; + public static final String SUBCLASS_VINEYARD = "vineyard"; + public static final String SUBCLASS_WET_MEADOW = "wet_meadow"; + public static final String SUBCLASS_WETLAND = "wetland"; + public static final String SUBCLASS_WOOD = "wood"; + public static final Set SUBCLASS_VALUES = Set.of("allotments", "bare_rock", "beach", "bog", "dune", + "scrub", "farm", "farmland", "fell", "forest", "garden", "glacier", "grass", "grassland", "golf_course", + "heath", "mangrove", "marsh", "meadow", "orchard", "park", "plant_nursery", "recreation_ground", "reedbed", + "saltern", "saltmarsh", "sand", "scree", "swamp", "tidalflat", "tundra", "village_green", "vineyard", + "wet_meadow", "wetland", "wood"); + } + + /** Complex mappings to generate attribute values from OSM element tags in the landcover layer. */ + final class FieldMappings { + + public static final MultiExpression Class = MultiExpression.of(List.of(MultiExpression.entry("farmland", + matchAny("subclass", "farmland", "farm", "orchard", "vineyard", "plant_nursery")), + MultiExpression.entry("ice", matchAny("subclass", "glacier", "ice_shelf")), + MultiExpression.entry("wood", matchAny("subclass", "wood", "forest")), + MultiExpression.entry("rock", matchAny("subclass", "bare_rock", "scree")), MultiExpression.entry("grass", + matchAny("subclass", "fell", "grassland", "heath", "scrub", "tundra", "grass", "meadow", "allotments", "park", + "village_green", "recreation_ground", "garden", "golf_course")), MultiExpression.entry("wetland", + matchAny("subclass", "wetland", "bog", "swamp", "wet_meadow", "marsh", "reedbed", "saltern", "tidalflat", + "saltmarsh", "mangrove")), MultiExpression.entry("sand", matchAny("subclass", "beach", "sand", "dune")))); + } + } + + /** + * Landuse is used to describe use of land by humans. At lower zoom levels this is from Natural Earth data for + * residential (urban) areas and at higher zoom levels mostly OSM landuse tags. + *

    + * Generated from landuse.yaml + */ + public interface Landuse extends Layer { + + double BUFFER_SIZE = 4.0; + String LAYER_NAME = "landuse"; + + @Override + default String name() { + return LAYER_NAME; + } + + /** Attribute names for map elements in the landuse layer. */ + final class Fields { + + /** + * Use the class to assign special colors to areas. Original value of either the landuse, amenity, leisure, tourism, place or waterway tag. + *

    + * allowed values: + *

      + *
    • "railway" + *
    • "cemetery" + *
    • "military" + *
    • "residential" + *
    • "commercial" + *
    • "industrial" + *
    • "garages" + *
    • "retail" + *
    • "bus_station" + *
    • "school" + *
    • "university" + *
    • "kindergarten" + *
    • "college" + *
    • "library" + *
    • "hospital" + *
    • "stadium" + *
    • "pitch" + *
    • "playground" + *
    • "track" + *
    • "theme_park" + *
    • "zoo" + *
    • "suburb" + *
    • "quarter" + *
    • "neighbourhood" + *
    • "dam" + *
    + */ + public static final String CLASS = "class"; + } + + /** Attribute values for map elements in the landuse layer. */ + final class FieldValues { + + public static final String CLASS_RAILWAY = "railway"; + public static final String CLASS_CEMETERY = "cemetery"; + public static final String CLASS_MILITARY = "military"; + public static final String CLASS_RESIDENTIAL = "residential"; + public static final String CLASS_COMMERCIAL = "commercial"; + public static final String CLASS_INDUSTRIAL = "industrial"; + public static final String CLASS_GARAGES = "garages"; + public static final String CLASS_RETAIL = "retail"; + public static final String CLASS_BUS_STATION = "bus_station"; + public static final String CLASS_SCHOOL = "school"; + public static final String CLASS_UNIVERSITY = "university"; + public static final String CLASS_KINDERGARTEN = "kindergarten"; + public static final String CLASS_COLLEGE = "college"; + public static final String CLASS_LIBRARY = "library"; + public static final String CLASS_HOSPITAL = "hospital"; + public static final String CLASS_STADIUM = "stadium"; + public static final String CLASS_PITCH = "pitch"; + public static final String CLASS_PLAYGROUND = "playground"; + public static final String CLASS_TRACK = "track"; + public static final String CLASS_THEME_PARK = "theme_park"; + public static final String CLASS_ZOO = "zoo"; + public static final String CLASS_SUBURB = "suburb"; + public static final String CLASS_QUARTER = "quarter"; + public static final String CLASS_NEIGHBOURHOOD = "neighbourhood"; + public static final String CLASS_DAM = "dam"; + public static final Set CLASS_VALUES = Set.of("railway", "cemetery", "military", "residential", + "commercial", "industrial", "garages", "retail", "bus_station", "school", "university", "kindergarten", + "college", "library", "hospital", "stadium", "pitch", "playground", "track", "theme_park", "zoo", "suburb", + "quarter", "neighbourhood", "dam"); + } + + /** Complex mappings to generate attribute values from OSM element tags in the landuse layer. */ + final class FieldMappings { + + } + } + + /** + * Natural peaks + *

    + * Generated from mountain_peak.yaml + */ + public interface MountainPeak extends Layer { + + double BUFFER_SIZE = 64.0; + String LAYER_NAME = "mountain_peak"; + + @Override + default String name() { + return LAYER_NAME; + } + + /** Attribute names for map elements in the mountain_peak layer. */ + final class Fields { + + /** The OSM name value of the peak. */ + public static final String NAME = "name"; + /** English name name:en if available, otherwise name. */ + public static final String NAME_EN = "name_en"; + /** German name name:de if available, otherwise name or name:en. */ + public static final String NAME_DE = "name_de"; + + /** + * Use the class to differentiate between mountain peak and volcano. + *

    + * allowed values: + *

      + *
    • "peak" + *
    • "volcano" + *
    + */ + public static final String CLASS = "class"; + /** Elevation (ele) in meters. */ + public static final String ELE = "ele"; + /** Elevation (ele) in feets. */ + public static final String ELE_FT = "ele_ft"; + /** Rank of the peak within one tile (starting at 1 that is the most important peak). */ + public static final String RANK = "rank"; + } + + /** Attribute values for map elements in the mountain_peak layer. */ + final class FieldValues { + + public static final String CLASS_PEAK = "peak"; + public static final String CLASS_VOLCANO = "volcano"; + public static final Set CLASS_VALUES = Set.of("peak", "volcano"); + } + + /** Complex mappings to generate attribute values from OSM element tags in the mountain_peak layer. */ + final class FieldMappings { + + } + } + + /** + * The park layer contains parks from OpenStreetMap tagged with boundary=national_park, + * boundary=protected_area, + * or leisure=nature_reserve. + *

    + * Generated from park.yaml + */ + public interface Park extends Layer { + + double BUFFER_SIZE = 4.0; + String LAYER_NAME = "park"; + + @Override + default String name() { + return LAYER_NAME; + } + + /** Attribute names for map elements in the park layer. */ + final class Fields { + + /** + * Use the class to differentiate between different parks. The class for + * boundary=protected_area parks is the lower-case of the protection_title + * value with blanks replaced by _. national_park is the class of + * protection_title=National Park and boundary=national_park. + * nature_reserve is the class of protection_title=Nature Reserve and + * leisure=nature_reserve. The class for other protection_title + * values is similarly assigned. + */ + public static final String CLASS = "class"; + /** + * The OSM name value of the park (point + * features only). + */ + public static final String NAME = "name"; + /** English name name:en if available, otherwise name (point features only). */ + public static final String NAME_EN = "name_en"; + /** + * German name name:de if available, otherwise name or name:en (point + * features only). + */ + public static final String NAME_DE = "name_de"; + /** Rank of the park within one tile, starting at 1 that is the most important park (point features only). */ + public static final String RANK = "rank"; + } + + /** Attribute values for map elements in the park layer. */ + final class FieldValues { + + } + + /** Complex mappings to generate attribute values from OSM element tags in the park layer. */ + final class FieldMappings { + + } + } + + /** + * Contains administrative boundaries as linestrings. Until z4 Natural + * Earth data is used after which OSM boundaries (boundary=administrative) + * are present from z5 to z14 (also for maritime boundaries with admin_level <= 2 at z4). OSM data + * contains several admin_level + * but for most styles it makes sense to just style admin_level=2 and admin_level=4. + *

    + * Generated from boundary.yaml + */ + public interface Boundary extends Layer { + + double BUFFER_SIZE = 4.0; + String LAYER_NAME = "boundary"; + + @Override + default String name() { + return LAYER_NAME; + } + + /** Attribute names for map elements in the boundary layer. */ + final class Fields { + + /** + * OSM admin_level + * indicating the level of importance of this boundary. The admin_level corresponds to the lowest + * admin_level the line participates in. At low zoom levels the Natural Earth boundaries are mapped + * to the equivalent admin levels. + */ + public static final String ADMIN_LEVEL = "admin_level"; + /** State name on the left of the border. For country boundaries only (admin_level = 2). */ + public static final String ADM0_L = "adm0_l"; + /** State name on the right of the border. For country boundaries only (admin_level = 2). */ + public static final String ADM0_R = "adm0_r"; + + /** + * Mark with 1 if the border is disputed. + *

    + * allowed values: + *

      + *
    • 0 + *
    • 1 + *
    + */ + public static final String DISPUTED = "disputed"; + + /** + * Field containing name of the disputed area (extracted from border relation in OSM, without spaces). For country + * boundaries only (admin_level = 2). Value examples from Asian OSM pbf extract + *

    + * allowed values: + *

      + *
    • "AbuMusaIsland" + *
    • "BaraHotiiValleys" + *
    • "ChineseClaim" + *
    • "Crimea" + *
    • "Demchok" + *
    • "Dokdo" + *
    • "IndianClaim-North" + *
    • "IndianClaimwesternKashmir" + *
    • "PakistaniClaim" + *
    • "SamduValleys" + *
    • "TirpaniValleys" + *
    + */ + public static final String DISPUTED_NAME = "disputed_name"; + /** + * ISO2 code of country, which wants to see the boundary line. For country boundaries only (admin_level = + * 2). + */ + public static final String CLAIMED_BY = "claimed_by"; + + /** + * Mark with 1 if it is a maritime border. + *

    + * allowed values: + *

      + *
    • 0 + *
    • 1 + *
    + */ + public static final String MARITIME = "maritime"; + } + + /** Attribute values for map elements in the boundary layer. */ + final class FieldValues { + + public static final String DISPUTED_NAME_ABUMUSAISLAND = "AbuMusaIsland"; + public static final String DISPUTED_NAME_BARAHOTIIVALLEYS = "BaraHotiiValleys"; + public static final String DISPUTED_NAME_CHINESECLAIM = "ChineseClaim"; + public static final String DISPUTED_NAME_CRIMEA = "Crimea"; + public static final String DISPUTED_NAME_DEMCHOK = "Demchok"; + public static final String DISPUTED_NAME_DOKDO = "Dokdo"; + public static final String DISPUTED_NAME_INDIANCLAIM_NORTH = "IndianClaim-North"; + public static final String DISPUTED_NAME_INDIANCLAIMWESTERNKASHMIR = "IndianClaimwesternKashmir"; + public static final String DISPUTED_NAME_PAKISTANICLAIM = "PakistaniClaim"; + public static final String DISPUTED_NAME_SAMDUVALLEYS = "SamduValleys"; + public static final String DISPUTED_NAME_TIRPANIVALLEYS = "TirpaniValleys"; + public static final Set DISPUTED_NAME_VALUES = Set.of("AbuMusaIsland", "BaraHotiiValleys", "ChineseClaim", + "Crimea", "Demchok", "Dokdo", "IndianClaim-North", "IndianClaimwesternKashmir", "PakistaniClaim", + "SamduValleys", "TirpaniValleys"); + } + + /** Complex mappings to generate attribute values from OSM element tags in the boundary layer. */ + final class FieldMappings { + + } + } + + /** + * Aeroway polygons based of OpenStreetMap aeroways. Airport + * buildings are contained in the building layer but all other airport related polygons can be found + * in the aeroway layer. + *

    + * Generated from aeroway.yaml + */ + public interface Aeroway extends Layer { + + double BUFFER_SIZE = 4.0; + String LAYER_NAME = "aeroway"; + + @Override + default String name() { + return LAYER_NAME; + } + + /** Attribute names for map elements in the aeroway layer. */ + final class Fields { + + /** The OSM ref tag of the runway/taxiway. */ + public static final String REF = "ref"; + + /** + * The original value of aeroway or + * area:aeroway tag. + *

    + * allowed values: + *

      + *
    • "aerodrome" + *
    • "heliport" + *
    • "runway" + *
    • "helipad" + *
    • "taxiway" + *
    • "apron" + *
    • "gate" + *
    + */ + public static final String CLASS = "class"; + } + + /** Attribute values for map elements in the aeroway layer. */ + final class FieldValues { + + public static final String CLASS_AERODROME = "aerodrome"; + public static final String CLASS_HELIPORT = "heliport"; + public static final String CLASS_RUNWAY = "runway"; + public static final String CLASS_HELIPAD = "helipad"; + public static final String CLASS_TAXIWAY = "taxiway"; + public static final String CLASS_APRON = "apron"; + public static final String CLASS_GATE = "gate"; + public static final Set CLASS_VALUES = Set.of("aerodrome", "heliport", "runway", "helipad", "taxiway", + "apron", "gate"); + } + + /** Complex mappings to generate attribute values from OSM element tags in the aeroway layer. */ + final class FieldMappings { + + } + } + + /** + * transportation contains roads, railways, aerial ways, and shipping lines. This layer is directly + * derived from the OSM road hierarchy. At lower zoom levels major highways from Natural Earth are used. It contains + * all roads from motorways to primary, secondary and tertiary roads to residential roads and foot paths. Styling the + * roads is the most essential part of the map. The transportation layer also contains polygons for + * features like plazas. + *

    + * Generated from transportation.yaml + */ + public interface Transportation extends Layer { + + double BUFFER_SIZE = 4.0; + String LAYER_NAME = "transportation"; + + @Override + default String name() { + return LAYER_NAME; + } + + /** Attribute names for map elements in the transportation layer. */ + final class Fields { + + /** + * Distinguish between more and less important roads or railways and roads under construction. Class is derived + * from the value of the highway, construction, railway, aerialway, route tag (for shipping ways), or man_made. + *

    + * allowed values: + *

      + *
    • motorway + *
    • trunk + *
    • primary + *
    • secondary + *
    • tertiary + *
    • minor + *
    • path + *
    • service + *
    • track + *
    • raceway + *
    • motorway_construction + *
    • trunk_construction + *
    • primary_construction + *
    • secondary_construction + *
    • tertiary_construction + *
    • minor_construction + *
    • path_construction + *
    • service_construction + *
    • track_construction + *
    • raceway_construction + *
    + */ + public static final String CLASS = "class"; + + /** + * Distinguish more specific classes of railway and path: Subclass is value of the railway, highway (for paths), or public_transport (for + * platforms) tag. + *

    + * allowed values: + *

      + *
    • "rail" + *
    • "narrow_gauge" + *
    • "preserved" + *
    • "funicular" + *
    • "subway" + *
    • "light_rail" + *
    • "monorail" + *
    • "tram" + *
    • "pedestrian" + *
    • "path" + *
    • "footway" + *
    • "cycleway" + *
    • "steps" + *
    • "bridleway" + *
    • "corridor" + *
    • "platform" + *
    + */ + public static final String SUBCLASS = "subclass"; + + /** + * Mark whether way is a tunnel or bridge. + *

    + * allowed values: + *

      + *
    • "bridge" + *
    • "tunnel" + *
    • "ford" + *
    + */ + public static final String BRUNNEL = "brunnel"; + + /** + * Mark with 1 whether way is a oneway in the direction of the way, with -1 whether way + * is a oneway in the opposite direction of the way or not a oneway with 0. + *

    + * allowed values: + *

      + *
    • 0 + *
    • 1 + *
    • -1 + *
    + */ + public static final String ONEWAY = "oneway"; + + /** + * Mark with 1 whether way is a ramp (link or steps) or not with 0. + *

    + * allowed values: + *

      + *
    • 0 + *
    • 1 + *
    + */ + public static final String RAMP = "ramp"; + + /** + * Original value of the service tag. + *

    + * allowed values: + *

      + *
    • "spur" + *
    • "yard" + *
    • "siding" + *
    • "crossover" + *
    • "driveway" + *
    • "alley" + *
    • "parking_aisle" + *
    + */ + public static final String SERVICE = "service"; + /** Original value of the layer tag. */ + public static final String LAYER = "layer"; + /** + * Experimental feature! Filled only for steps and footways. Original value of the level tag. + */ + public static final String LEVEL = "level"; + + /** + * Experimental feature! Filled only for steps and footways. Original value of the indoor tag. + *

    + * allowed values: + *

      + *
    • 1 + *
    + */ + public static final String INDOOR = "indoor"; + /** + * Original value of the bicycle tag + * (highways only). + */ + public static final String BICYCLE = "bicycle"; + /** + * Original value of the foot tag (highways + * only). + */ + public static final String FOOT = "foot"; + /** + * Original value of the horse tag + * (highways only). + */ + public static final String HORSE = "horse"; + /** + * Original value of the mtb:scale tag + * (highways only). + */ + public static final String MTB_SCALE = "mtb_scale"; + + /** + * Values of surface tag devided into 2 + * groups paved (paved, asphalt, cobblestone, concrete, concrete:lanes, concrete:plates, metal, + * paving_stones, sett, unhewn_cobblestone, wood) and unpaved (unpaved, compacted, dirt, earth, + * fine_gravel, grass, grass_paver, gravel, gravel_turf, ground, ice, mud, pebblestone, salt, sand, snow, + * woodchips). + *

    + * allowed values: + *

      + *
    • "paved" + *
    • "unpaved" + *
    + */ + public static final String SURFACE = "surface"; + } + + /** Attribute values for map elements in the transportation layer. */ + final class FieldValues { + + public static final String CLASS_MOTORWAY = "motorway"; + public static final String CLASS_TRUNK = "trunk"; + public static final String CLASS_PRIMARY = "primary"; + public static final String CLASS_SECONDARY = "secondary"; + public static final String CLASS_TERTIARY = "tertiary"; + public static final String CLASS_MINOR = "minor"; + public static final String CLASS_PATH = "path"; + public static final String CLASS_SERVICE = "service"; + public static final String CLASS_TRACK = "track"; + public static final String CLASS_RACEWAY = "raceway"; + public static final String CLASS_MOTORWAY_CONSTRUCTION = "motorway_construction"; + public static final String CLASS_TRUNK_CONSTRUCTION = "trunk_construction"; + public static final String CLASS_PRIMARY_CONSTRUCTION = "primary_construction"; + public static final String CLASS_SECONDARY_CONSTRUCTION = "secondary_construction"; + public static final String CLASS_TERTIARY_CONSTRUCTION = "tertiary_construction"; + public static final String CLASS_MINOR_CONSTRUCTION = "minor_construction"; + public static final String CLASS_PATH_CONSTRUCTION = "path_construction"; + public static final String CLASS_SERVICE_CONSTRUCTION = "service_construction"; + public static final String CLASS_TRACK_CONSTRUCTION = "track_construction"; + public static final String CLASS_RACEWAY_CONSTRUCTION = "raceway_construction"; + public static final Set CLASS_VALUES = Set.of("motorway", "trunk", "primary", "secondary", "tertiary", + "minor", "path", "service", "track", "raceway", "motorway_construction", "trunk_construction", + "primary_construction", "secondary_construction", "tertiary_construction", "minor_construction", + "path_construction", "service_construction", "track_construction", "raceway_construction"); + public static final String SUBCLASS_RAIL = "rail"; + public static final String SUBCLASS_NARROW_GAUGE = "narrow_gauge"; + public static final String SUBCLASS_PRESERVED = "preserved"; + public static final String SUBCLASS_FUNICULAR = "funicular"; + public static final String SUBCLASS_SUBWAY = "subway"; + public static final String SUBCLASS_LIGHT_RAIL = "light_rail"; + public static final String SUBCLASS_MONORAIL = "monorail"; + public static final String SUBCLASS_TRAM = "tram"; + public static final String SUBCLASS_PEDESTRIAN = "pedestrian"; + public static final String SUBCLASS_PATH = "path"; + public static final String SUBCLASS_FOOTWAY = "footway"; + public static final String SUBCLASS_CYCLEWAY = "cycleway"; + public static final String SUBCLASS_STEPS = "steps"; + public static final String SUBCLASS_BRIDLEWAY = "bridleway"; + public static final String SUBCLASS_CORRIDOR = "corridor"; + public static final String SUBCLASS_PLATFORM = "platform"; + public static final Set SUBCLASS_VALUES = Set.of("rail", "narrow_gauge", "preserved", "funicular", + "subway", "light_rail", "monorail", "tram", "pedestrian", "path", "footway", "cycleway", "steps", "bridleway", + "corridor", "platform"); + public static final String BRUNNEL_BRIDGE = "bridge"; + public static final String BRUNNEL_TUNNEL = "tunnel"; + public static final String BRUNNEL_FORD = "ford"; + public static final Set BRUNNEL_VALUES = Set.of("bridge", "tunnel", "ford"); + public static final String SERVICE_SPUR = "spur"; + public static final String SERVICE_YARD = "yard"; + public static final String SERVICE_SIDING = "siding"; + public static final String SERVICE_CROSSOVER = "crossover"; + public static final String SERVICE_DRIVEWAY = "driveway"; + public static final String SERVICE_ALLEY = "alley"; + public static final String SERVICE_PARKING_AISLE = "parking_aisle"; + public static final Set SERVICE_VALUES = Set.of("spur", "yard", "siding", "crossover", "driveway", + "alley", "parking_aisle"); + public static final String SURFACE_PAVED = "paved"; + public static final String SURFACE_UNPAVED = "unpaved"; + public static final Set SURFACE_VALUES = Set.of("paved", "unpaved"); + } + + /** Complex mappings to generate attribute values from OSM element tags in the transportation layer. */ + final class FieldMappings { + + public static final MultiExpression Class = MultiExpression.of( + List.of(MultiExpression.entry("motorway", matchAny("highway", "motorway", "motorway_link")), + MultiExpression.entry("trunk", matchAny("highway", "trunk", "trunk_link")), + MultiExpression.entry("primary", matchAny("highway", "primary", "primary_link")), + MultiExpression.entry("secondary", matchAny("highway", "secondary", "secondary_link")), + MultiExpression.entry("tertiary", matchAny("highway", "tertiary", "tertiary_link")), + MultiExpression.entry("minor", matchAny("highway", "unclassified", "residential", "living_street", "road")), + MultiExpression.entry("path", + or(matchAny("highway", "pedestrian", "path", "footway", "cycleway", "steps", "bridleway", "corridor"), + matchAny("public_transport", "platform"))), + MultiExpression.entry("service", matchAny("highway", "service")), + MultiExpression.entry("track", matchAny("highway", "track")), + MultiExpression.entry("raceway", matchAny("highway", "raceway")), + MultiExpression.entry("motorway_construction", + and(matchAny("highway", "construction"), matchAny("construction", "motorway", "motorway_link"))), + MultiExpression.entry("trunk_construction", + and(matchAny("highway", "construction"), matchAny("construction", "trunk", "trunk_link"))), + MultiExpression.entry("primary_construction", + and(matchAny("highway", "construction"), matchAny("construction", "primary", "primary_link"))), + MultiExpression.entry("secondary_construction", + and(matchAny("highway", "construction"), matchAny("construction", "secondary", "secondary_link"))), + MultiExpression.entry("tertiary_construction", + and(matchAny("highway", "construction"), matchAny("construction", "tertiary", "tertiary_link"))), + MultiExpression.entry("minor_construction", and(matchAny("highway", "construction"), + matchAny("construction", "", "unclassified", "residential", "living_street", "road"))), + MultiExpression.entry("path_construction", and(matchAny("highway", "construction"), + or(matchAny("construction", "pedestrian", "path", "footway", "cycleway", "steps", "bridleway", "corridor"), + matchAny("public_transport", "platform")))), MultiExpression.entry("service_construction", + and(matchAny("highway", "construction"), matchAny("construction", "service"))), + MultiExpression.entry("track_construction", + and(matchAny("highway", "construction"), matchAny("construction", "track"))), + MultiExpression.entry("raceway_construction", + and(matchAny("highway", "construction"), matchAny("construction", "raceway"))))); + } + } + + /** + * All OSM Buildings. All building tags are imported (building= ). The buildings are not yet + * ready for 3D rendering support and any help to improve this is welcomed. + *

    + * Generated from building.yaml + */ + public interface Building extends Layer { + + double BUFFER_SIZE = 4.0; + String LAYER_NAME = "building"; + + @Override + default String name() { + return LAYER_NAME; + } + + /** Attribute names for map elements in the building layer. */ + final class Fields { + + /** + * An approximated height from levels and height of the building or building:part after the method of Paul Norman + * in OSM Clear. For future 3D rendering of buildings. + */ + public static final String RENDER_HEIGHT = "render_height"; + /** + * An approximated height from levels and height of the bottom of the building or building:part after the method + * of Paul Norman in OSM Clear. For future 3D rendering of + * buildings. + */ + public static final String RENDER_MIN_HEIGHT = "render_min_height"; + /** Colour */ + public static final String COLOUR = "colour"; + /** + * If True, building (part) should not be rendered in 3D. Currently, building + * outlines are marked as hide_3d. + */ + public static final String HIDE_3D = "hide_3d"; + } + + /** Attribute values for map elements in the building layer. */ + final class FieldValues { + + } + + /** Complex mappings to generate attribute values from OSM element tags in the building layer. */ + final class FieldMappings { + + } + } + + /** + * Lake center lines for labelling lake bodies. This is based of the osm-lakelines + * project which derives nice centerlines from OSM water bodies. Only the most important lakes contain labels. + *

    + * Generated from water_name.yaml + */ + public interface WaterName extends Layer { + + double BUFFER_SIZE = 256.0; + String LAYER_NAME = "water_name"; + + @Override + default String name() { + return LAYER_NAME; + } + + /** Attribute names for map elements in the water_name layer. */ + final class Fields { + + /** The OSM name value of the water body. */ + public static final String NAME = "name"; + /** English name name:en if available, otherwise name. */ + public static final String NAME_EN = "name_en"; + /** German name name:de if available, otherwise name or name:en. */ + public static final String NAME_DE = "name_de"; + + /** + * At the moment only lake since no ocean parts are labelled. Reserved for future use. + *

    + * allowed values: + *

      + *
    • "lake" + *
    + */ + public static final String CLASS = "class"; + + /** + * Mark with 1 if it is an intermittent + * lake. + *

    + * allowed values: + *

      + *
    • 0 + *
    • 1 + *
    + */ + public static final String INTERMITTENT = "intermittent"; + } + + /** Attribute values for map elements in the water_name layer. */ + final class FieldValues { + + public static final String CLASS_LAKE = "lake"; + public static final Set CLASS_VALUES = Set.of("lake"); + } + + /** Complex mappings to generate attribute values from OSM element tags in the water_name layer. */ + final class FieldMappings { + + } + } + + /** + * This is the layer for labelling the highways. Only highways that are named name= and are long enough + * to place text upon appear. The OSM roads are stitched together if they contain the same name to have better label + * placement than having many small linestrings. For motorways you should use the ref field to label them + * while for other roads you should use name. + *

    + * Generated from transportation_name.yaml + */ + public interface TransportationName extends Layer { + + double BUFFER_SIZE = 8.0; + String LAYER_NAME = "transportation_name"; + + @Override + default String name() { + return LAYER_NAME; + } + + /** Attribute names for map elements in the transportation_name layer. */ + final class Fields { + + /** + * The OSM name value + * of the highway. + */ + public static final String NAME = "name"; + /** English name name:en if available, otherwise name. */ + public static final String NAME_EN = "name_en"; + /** German name name:de if available, otherwise name or name:en. */ + public static final String NAME_DE = "name_de"; + /** + * The OSM ref tag of the motorway or its + * network. + */ + public static final String REF = "ref"; + /** Length of the ref field. Useful for having a shield icon as background for labeling motorways. */ + public static final String REF_LENGTH = "ref_length"; + + /** + * The network type derived mainly from network + * tag of the road. See more info about us- + * , ca-transcanada, or + * gb- . + *

    + * allowed values: + *

      + *
    • "us-interstate" + *
    • "us-highway" + *
    • "us-state" + *
    • "ca-transcanada" + *
    • "gb-motorway" + *
    • "gb-trunk" + *
    • "road (default)" + *
    + */ + public static final String NETWORK = "network"; + + /** + * Distinguish between more and less important roads and roads under construction. + *

    + * allowed values: + *

      + *
    • "motorway" + *
    • "trunk" + *
    • "primary" + *
    • "secondary" + *
    • "tertiary" + *
    • "minor" + *
    • "service" + *
    • "track" + *
    • "path" + *
    • "raceway" + *
    • "motorway_construction" + *
    • "trunk_construction" + *
    • "primary_construction" + *
    • "secondary_construction" + *
    • "tertiary_construction" + *
    • "minor_construction" + *
    • "service_construction" + *
    • "track_construction" + *
    • "path_construction" + *
    • "raceway_construction" + *
    • "rail" + *
    • "transit" + *
    + */ + public static final String CLASS = "class"; + + /** + * Distinguish more specific classes of path: Subclass is value of the highway + * (for paths). + *

    + * allowed values: + *

      + *
    • "pedestrian" + *
    • "path" + *
    • "footway" + *
    • "cycleway" + *
    • "steps" + *
    • "bridleway" + *
    • "corridor" + *
    • "platform" + *
    + */ + public static final String SUBCLASS = "subclass"; + + /** + * Mark whether way is a bridge, a tunnel or a ford. + *

    + * allowed values: + *

      + *
    • "bridge" + *
    • "tunnel" + *
    • "ford" + *
    + */ + public static final String BRUNNEL = "brunnel"; + /** + * Experimental feature! Filled only for steps and footways. Original value of level tag. + */ + public static final String LEVEL = "level"; + /** + * Experimental feature! Filled only for steps and footways. Original value of layer tag. + */ + public static final String LAYER = "layer"; + + /** + * Experimental feature! Filled only for steps and footways. Original value of indoor tag. + *

    + * allowed values: + *

      + *
    • 1 + *
    + */ + public static final String INDOOR = "indoor"; + } + + /** Attribute values for map elements in the transportation_name layer. */ + final class FieldValues { + + public static final String NETWORK_US_INTERSTATE = "us-interstate"; + public static final String NETWORK_US_HIGHWAY = "us-highway"; + public static final String NETWORK_US_STATE = "us-state"; + public static final String NETWORK_CA_TRANSCANADA = "ca-transcanada"; + public static final String NETWORK_GB_MOTORWAY = "gb-motorway"; + public static final String NETWORK_GB_TRUNK = "gb-trunk"; + public static final String NETWORK_ROAD = "road"; + public static final Set NETWORK_VALUES = Set.of("us-interstate", "us-highway", "us-state", + "ca-transcanada", "gb-motorway", "gb-trunk", "road"); + public static final String CLASS_MOTORWAY = "motorway"; + public static final String CLASS_TRUNK = "trunk"; + public static final String CLASS_PRIMARY = "primary"; + public static final String CLASS_SECONDARY = "secondary"; + public static final String CLASS_TERTIARY = "tertiary"; + public static final String CLASS_MINOR = "minor"; + public static final String CLASS_SERVICE = "service"; + public static final String CLASS_TRACK = "track"; + public static final String CLASS_PATH = "path"; + public static final String CLASS_RACEWAY = "raceway"; + public static final String CLASS_MOTORWAY_CONSTRUCTION = "motorway_construction"; + public static final String CLASS_TRUNK_CONSTRUCTION = "trunk_construction"; + public static final String CLASS_PRIMARY_CONSTRUCTION = "primary_construction"; + public static final String CLASS_SECONDARY_CONSTRUCTION = "secondary_construction"; + public static final String CLASS_TERTIARY_CONSTRUCTION = "tertiary_construction"; + public static final String CLASS_MINOR_CONSTRUCTION = "minor_construction"; + public static final String CLASS_SERVICE_CONSTRUCTION = "service_construction"; + public static final String CLASS_TRACK_CONSTRUCTION = "track_construction"; + public static final String CLASS_PATH_CONSTRUCTION = "path_construction"; + public static final String CLASS_RACEWAY_CONSTRUCTION = "raceway_construction"; + public static final String CLASS_RAIL = "rail"; + public static final String CLASS_TRANSIT = "transit"; + public static final Set CLASS_VALUES = Set.of("motorway", "trunk", "primary", "secondary", "tertiary", + "minor", "service", "track", "path", "raceway", "motorway_construction", "trunk_construction", + "primary_construction", "secondary_construction", "tertiary_construction", "minor_construction", + "service_construction", "track_construction", "path_construction", "raceway_construction", "rail", "transit"); + public static final String SUBCLASS_PEDESTRIAN = "pedestrian"; + public static final String SUBCLASS_PATH = "path"; + public static final String SUBCLASS_FOOTWAY = "footway"; + public static final String SUBCLASS_CYCLEWAY = "cycleway"; + public static final String SUBCLASS_STEPS = "steps"; + public static final String SUBCLASS_BRIDLEWAY = "bridleway"; + public static final String SUBCLASS_CORRIDOR = "corridor"; + public static final String SUBCLASS_PLATFORM = "platform"; + public static final Set SUBCLASS_VALUES = Set.of("pedestrian", "path", "footway", "cycleway", "steps", + "bridleway", "corridor", "platform"); + public static final String BRUNNEL_BRIDGE = "bridge"; + public static final String BRUNNEL_TUNNEL = "tunnel"; + public static final String BRUNNEL_FORD = "ford"; + public static final Set BRUNNEL_VALUES = Set.of("bridge", "tunnel", "ford"); + } + + /** Complex mappings to generate attribute values from OSM element tags in the transportation_name layer. */ + final class FieldMappings { + + } + } + + /** + * The place layer consists out of countries, states and cities. Apart from the roads this is also one of the more + * important layers to create a beautiful map. We suggest you use different font styles and sizes to create a text + * hierarchy. + *

    + * Generated from place.yaml + */ + public interface Place extends Layer { + + double BUFFER_SIZE = 256.0; + String LAYER_NAME = "place"; + + @Override + default String name() { + return LAYER_NAME; + } + + /** Attribute names for map elements in the place layer. */ + final class Fields { + + /** The OSM name value of the POI. */ + public static final String NAME = "name"; + /** English name name:en if available, otherwise name. */ + public static final String NAME_EN = "name_en"; + /** German name name:de if available, otherwise name or name:en. */ + public static final String NAME_DE = "name_de"; + + /** + * The capital field marks the admin_level + * of the boundary the place is a capital of. + *

    + * allowed values: + *

      + *
    • 2 + *
    • 4 + *
    + */ + public static final String CAPITAL = "capital"; + + /** + * Original value of the place tag. + * Distinguish between continents, countries, states and places like settlements or smaller entities. Use + * class to separately style the different places and build a text hierarchy according to their + * importance. + *

    + * allowed values: + *

      + *
    • "continent" + *
    • "country" + *
    • "state" + *
    • "city" + *
    • "town" + *
    • "village" + *
    • "hamlet" + *
    • "suburb" + *
    • "quarter" + *
    • "neighbourhood" + *
    • "isolated_dwelling" + *
    + */ + public static final String CLASS = "class"; + /** + * Two-letter country code ISO 3166-1 alpha-2. + * Available only for class=country. Original value of the country_code_iso3166_1_alpha_2 + * tag. + */ + public static final String ISO_A2 = "iso_a2"; + /** + * Countries, states and the most important cities all have a rank to boost their importance on + * the map. The rank field for counries and states ranges from 1 to 6 + * while the rank field for cities ranges from 1 to 10 for the most + * important cities and continues from 10 serially based on the local importance of the city (derived + * from population and city class). You can use the rank to limit density of labels or improve + * the text hierarchy. The rank value is a combination of the Natural Earth scalerank, + * labelrank and datarank values for countries and states and for cities consists out + * of + * a shifted Natural Earth scalerank combined with a local rank within a grid for cities that do not + * have a Natural Earth scalerank. + */ + public static final String RANK = "rank"; + } + + /** Attribute values for map elements in the place layer. */ + final class FieldValues { + + public static final String CLASS_CONTINENT = "continent"; + public static final String CLASS_COUNTRY = "country"; + public static final String CLASS_STATE = "state"; + public static final String CLASS_CITY = "city"; + public static final String CLASS_TOWN = "town"; + public static final String CLASS_VILLAGE = "village"; + public static final String CLASS_HAMLET = "hamlet"; + public static final String CLASS_SUBURB = "suburb"; + public static final String CLASS_QUARTER = "quarter"; + public static final String CLASS_NEIGHBOURHOOD = "neighbourhood"; + public static final String CLASS_ISOLATED_DWELLING = "isolated_dwelling"; + public static final Set CLASS_VALUES = Set.of("continent", "country", "state", "city", "town", "village", + "hamlet", "suburb", "quarter", "neighbourhood", "isolated_dwelling"); + } + + /** Complex mappings to generate attribute values from OSM element tags in the place layer. */ + final class FieldMappings { + + } + } + + /** + * Everything in OpenStreetMap which contains a addr:housenumber tag useful for labelling housenumbers on + * a map. This adds significant size to z14. For buildings the centroid of the building is used as + * housenumber. + *

    + * Generated from housenumber.yaml + */ + public interface Housenumber extends Layer { + + double BUFFER_SIZE = 8.0; + String LAYER_NAME = "housenumber"; + + @Override + default String name() { + return LAYER_NAME; + } + + /** Attribute names for map elements in the housenumber layer. */ + final class Fields { + + /** Value of the addr:housenumber tag. */ + public static final String HOUSENUMBER = "housenumber"; + } + + /** Attribute values for map elements in the housenumber layer. */ + final class FieldValues { + + } + + /** Complex mappings to generate attribute values from OSM element tags in the housenumber layer. */ + final class FieldMappings { + + } + } + + /** + * Points of interests containing a of a variety + * of OpenStreetMap tags. Mostly contains amenities, sport, shop and tourist POIs. + *

    + * Generated from poi.yaml + */ + public interface Poi extends Layer { + + double BUFFER_SIZE = 64.0; + String LAYER_NAME = "poi"; + + @Override + default String name() { + return LAYER_NAME; + } + + /** Attribute names for map elements in the poi layer. */ + final class Fields { + + /** The OSM name value of the POI. */ + public static final String NAME = "name"; + /** English name name:en if available, otherwise name. */ + public static final String NAME_EN = "name_en"; + /** German name name:de if available, otherwise name or name:en. */ + public static final String NAME_DE = "name_de"; + + /** + * More general classes of POIs. If there is no more general class for the subclass this + * field will contain the same value as subclass. But for example for schools you only need to style + * the class school to filter the subclasses school and kindergarten. Or + * use the class shop to style all shops. + *

    + * allowed values: + *

      + *
    • shop + *
    • town_hall + *
    • golf + *
    • fast_food + *
    • park + *
    • bus + *
    • railway + *
    • aerialway + *
    • entrance + *
    • campsite + *
    • laundry + *
    • grocery + *
    • library + *
    • college + *
    • lodging + *
    • ice_cream + *
    • post + *
    • cafe + *
    • school + *
    • alcohol_shop + *
    • bar + *
    • harbor + *
    • car + *
    • hospital + *
    • cemetery + *
    • attraction + *
    • beer + *
    • music + *
    • stadium + *
    • art_gallery + *
    • clothing_store + *
    • swimming + *
    • castle + *
    + */ + public static final String CLASS = "class"; + /** + * Original value of either the amenity, + * barrier, historic, information, landuse, leisure, railway, shop, sport, station, religion, tourism, aerialway, building, highway or waterway tag. Use this to do more + * precise styling. + */ + public static final String SUBCLASS = "subclass"; + /** + * The POIs are ranked ascending according to their importance within a grid. The rank value shows + * the local relative importance of a POI within it's cell in the grid. This can be used to reduce label density + * at z14. Since all POIs already need to be contained at z14 you can use less than + * rank=10 epxression to limit POIs. At some point like z17 you can show all POIs. + */ + public static final String RANK = "rank"; + + /** + * Experimental feature! Indicates main platform of public transport stops (buses, trams, and subways). Grouping + * of platforms is implemented using uic_ref + * tag that is not used worldwide. + *

    + * allowed values: + *

      + *
    • 1 + *
    + */ + public static final String AGG_STOP = "agg_stop"; + /** Original value of level tag. */ + public static final String LEVEL = "level"; + /** Original value of layer tag. */ + public static final String LAYER = "layer"; + + /** + * Original value of indoor tag. + *

    + * allowed values: + *

      + *
    • 1 + *
    + */ + public static final String INDOOR = "indoor"; + } + + /** Attribute values for map elements in the poi layer. */ + final class FieldValues { + + public static final String CLASS_SHOP = "shop"; + public static final String CLASS_TOWN_HALL = "town_hall"; + public static final String CLASS_GOLF = "golf"; + public static final String CLASS_FAST_FOOD = "fast_food"; + public static final String CLASS_PARK = "park"; + public static final String CLASS_BUS = "bus"; + public static final String CLASS_RAILWAY = "railway"; + public static final String CLASS_AERIALWAY = "aerialway"; + public static final String CLASS_ENTRANCE = "entrance"; + public static final String CLASS_CAMPSITE = "campsite"; + public static final String CLASS_LAUNDRY = "laundry"; + public static final String CLASS_GROCERY = "grocery"; + public static final String CLASS_LIBRARY = "library"; + public static final String CLASS_COLLEGE = "college"; + public static final String CLASS_LODGING = "lodging"; + public static final String CLASS_ICE_CREAM = "ice_cream"; + public static final String CLASS_POST = "post"; + public static final String CLASS_CAFE = "cafe"; + public static final String CLASS_SCHOOL = "school"; + public static final String CLASS_ALCOHOL_SHOP = "alcohol_shop"; + public static final String CLASS_BAR = "bar"; + public static final String CLASS_HARBOR = "harbor"; + public static final String CLASS_CAR = "car"; + public static final String CLASS_HOSPITAL = "hospital"; + public static final String CLASS_CEMETERY = "cemetery"; + public static final String CLASS_ATTRACTION = "attraction"; + public static final String CLASS_BEER = "beer"; + public static final String CLASS_MUSIC = "music"; + public static final String CLASS_STADIUM = "stadium"; + public static final String CLASS_ART_GALLERY = "art_gallery"; + public static final String CLASS_CLOTHING_STORE = "clothing_store"; + public static final String CLASS_SWIMMING = "swimming"; + public static final String CLASS_CASTLE = "castle"; + public static final Set CLASS_VALUES = Set.of("shop", "town_hall", "golf", "fast_food", "park", "bus", + "railway", "aerialway", "entrance", "campsite", "laundry", "grocery", "library", "college", "lodging", + "ice_cream", "post", "cafe", "school", "alcohol_shop", "bar", "harbor", "car", "hospital", "cemetery", + "attraction", "beer", "music", "stadium", "art_gallery", "clothing_store", "swimming", "castle"); + } + + /** Complex mappings to generate attribute values from OSM element tags in the poi layer. */ + final class FieldMappings { + + public static final MultiExpression Class = MultiExpression.of(List.of(MultiExpression.entry("shop", + matchAny("subclass", "accessories", "antiques", "beauty", "bed", "boutique", "camera", "carpet", "charity", + "chemist", "coffee", "computer", "convenience", "copyshop", "cosmetics", "garden_centre", "doityourself", + "erotic", "electronics", "fabric", "florist", "frozen_food", "furniture", "video_games", "video", "general", + "gift", "hardware", "hearing_aids", "hifi", "ice_cream", "interior_decoration", "jewelry", "kiosk", "lamps", + "mall", "massage", "motorcycle", "mobile_phone", "newsagent", "optician", "outdoor", "perfumery", "perfume", + "pet", "photo", "second_hand", "shoes", "sports", "stationery", "tailor", "tattoo", "ticket", "tobacco", + "toys", "travel_agency", "watches", "weapons", "wholesale")), MultiExpression.entry("town_hall", + matchAny("subclass", "townhall", "public_building", "courthouse", "community_centre")), + MultiExpression.entry("golf", matchAny("subclass", "golf", "golf_course", "miniature_golf")), + MultiExpression.entry("fast_food", matchAny("subclass", "fast_food", "food_court")), + MultiExpression.entry("park", matchAny("subclass", "park", "bbq")), + MultiExpression.entry("bus", matchAny("subclass", "bus_stop", "bus_station")), MultiExpression.entry("railway", + or(and(matchAny("subclass", "station"), matchAny("mapping_key", "railway")), + matchAny("subclass", "halt", "tram_stop", "subway"))), + MultiExpression.entry("aerialway", and(matchAny("subclass", "station"), matchAny("mapping_key", "aerialway"))), + MultiExpression.entry("entrance", matchAny("subclass", "subway_entrance", "train_station_entrance")), + MultiExpression.entry("campsite", matchAny("subclass", "camp_site", "caravan_site")), + MultiExpression.entry("laundry", matchAny("subclass", "laundry", "dry_cleaning")), + MultiExpression.entry("grocery", + matchAny("subclass", "supermarket", "deli", "delicatessen", "department_store", "greengrocer", + "marketplace")), MultiExpression.entry("library", matchAny("subclass", "books", "library")), + MultiExpression.entry("college", matchAny("subclass", "university", "college")), + MultiExpression.entry("lodging", + matchAny("subclass", "hotel", "motel", "bed_and_breakfast", "guest_house", "hostel", "chalet", "alpine_hut", + "dormitory")), MultiExpression.entry("ice_cream", matchAny("subclass", "chocolate", "confectionery")), + MultiExpression.entry("post", matchAny("subclass", "post_box", "post_office")), + MultiExpression.entry("cafe", matchAny("subclass", "cafe")), + MultiExpression.entry("school", matchAny("subclass", "school", "kindergarten")), + MultiExpression.entry("alcohol_shop", matchAny("subclass", "alcohol", "beverages", "wine")), + MultiExpression.entry("bar", matchAny("subclass", "bar", "nightclub")), + MultiExpression.entry("harbor", matchAny("subclass", "marina", "dock")), + MultiExpression.entry("car", matchAny("subclass", "car", "car_repair", "car_parts", "taxi")), + MultiExpression.entry("hospital", matchAny("subclass", "hospital", "nursing_home", "clinic")), + MultiExpression.entry("cemetery", matchAny("subclass", "grave_yard", "cemetery")), + MultiExpression.entry("attraction", matchAny("subclass", "attraction", "viewpoint")), + MultiExpression.entry("beer", matchAny("subclass", "biergarten", "pub")), + MultiExpression.entry("music", matchAny("subclass", "music", "musical_instrument")), + MultiExpression.entry("stadium", matchAny("subclass", "american_football", "stadium", "soccer")), + MultiExpression.entry("art_gallery", matchAny("subclass", "art", "artwork", "gallery", "arts_centre")), + MultiExpression.entry("clothing_store", matchAny("subclass", "bag", "clothes")), + MultiExpression.entry("swimming", matchAny("subclass", "swimming_area", "swimming")), + MultiExpression.entry("castle", matchAny("subclass", "castle", "ruins")))); + } + } + + /** + * Aerodrome labels + *

    + * Generated from aerodrome_label.yaml + */ + public interface AerodromeLabel extends Layer { + + double BUFFER_SIZE = 64.0; + String LAYER_NAME = "aerodrome_label"; + + @Override + default String name() { + return LAYER_NAME; + } + + /** Attribute names for map elements in the aerodrome_label layer. */ + final class Fields { + + /** The OSM name value of the aerodrome. */ + public static final String NAME = "name"; + /** English name name:en if available, otherwise name. */ + public static final String NAME_EN = "name_en"; + /** German name name:de if available, otherwise name or name:en. */ + public static final String NAME_DE = "name_de"; + + /** + * Distinguish between more and less important aerodromes. Class is derived from the value of aerodrome and + * aerodrome:type tags. + *

    + * allowed values: + *

      + *
    • international + *
    • public + *
    • regional + *
    • military + *
    • private + *
    • other + *
    + */ + public static final String CLASS = "class"; + /** 3-character code issued by the IATA. */ + public static final String IATA = "iata"; + /** 4-letter code issued by the ICAO. */ + public static final String ICAO = "icao"; + /** Elevation (ele) in meters. */ + public static final String ELE = "ele"; + /** Elevation (ele) in feets. */ + public static final String ELE_FT = "ele_ft"; + } + + /** Attribute values for map elements in the aerodrome_label layer. */ + final class FieldValues { + + public static final String CLASS_INTERNATIONAL = "international"; + public static final String CLASS_PUBLIC = "public"; + public static final String CLASS_REGIONAL = "regional"; + public static final String CLASS_MILITARY = "military"; + public static final String CLASS_PRIVATE = "private"; + public static final String CLASS_OTHER = "other"; + public static final Set CLASS_VALUES = Set.of("international", "public", "regional", "military", + "private", "other"); + } + + /** Complex mappings to generate attribute values from OSM element tags in the aerodrome_label layer. */ + final class FieldMappings { + + public static final MultiExpression Class = MultiExpression.of(List.of( + MultiExpression.entry("international", + or(matchAny("aerodrome", "international"), matchAny("aerodrome_type", "international"))), + MultiExpression.entry("public", + or(matchAny("aerodrome", "public"), matchAny("aerodrome_type", "%public%", "civil"))), + MultiExpression.entry("regional", + or(matchAny("aerodrome", "regional"), matchAny("aerodrome_type", "regional"))), + MultiExpression.entry("military", + or(matchAny("aerodrome", "military"), matchAny("aerodrome_type", "%military%"), + matchAny("military", "airfield"))), + MultiExpression.entry("private", or(matchAny("aerodrome", "private"), matchAny("aerodrome_type", "private"))), + MultiExpression.entry("other", FALSE))); + } + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/generated/Tables.java b/src/main/java/com/onthegomap/planetiler/basemap/generated/Tables.java new file mode 100644 index 0000000..c242959 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/generated/Tables.java @@ -0,0 +1,1594 @@ +/* +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 + +package com.onthegomap.planetiler.basemap.generated; + +import static com.onthegomap.planetiler.expression.Expression.*; + +import com.onthegomap.planetiler.FeatureCollector; +import com.onthegomap.planetiler.expression.Expression; +import com.onthegomap.planetiler.expression.MultiExpression; +import com.onthegomap.planetiler.reader.SourceFeature; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * OSM element parsers generated from the imposm3 table definitions + * in the OpenMapTiles vector tile + * schema. + *

    + * 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 rowClass, + Constructor create + ) {} + + /** A functional interface that the typed handler method that a layer implementation can be coerced to. */ + @FunctionalInterface + public interface RowHandler { + + /** 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( + Class handlerClass, + RowHandler handler + ) {} + + /** An OSM element that would appear in the {@code osm_water_polygon} table generated by imposm3. */ + public static record OsmWaterPolygon( + @Override String name, @Override String nameEn, @Override String nameDe, @Override String natural, + @Override String landuse, @Override String waterway, @Override boolean isIntermittent, @Override boolean isTunnel, + @Override boolean isBridge, @Override SourceFeature source + ) implements Row, WithName, WithNameEn, WithNameDe, WithNatural, WithLanduse, WithWaterway, WithIsIntermittent, + WithIsTunnel, WithIsBridge, WithSource { + + public OsmWaterPolygon(SourceFeature source, String mappingKey) { + this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), + source.getString("natural"), source.getString("landuse"), source.getString("waterway"), + source.getBoolean("intermittent"), source.getBoolean("tunnel"), source.getBoolean("bridge"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and( + or(matchAny("landuse", "reservoir", "basin", "salt_pond"), matchAny("leisure", "swimming_pool"), + matchAny("natural", "water", "bay"), + matchAny("waterway", "river", "riverbank", "stream", "canal", "drain", "ditch", "dock")), + not(matchAny("covered", "yes")), matchType("polygon")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmWaterPolygon}. + */ + public interface Handler { + + void process(OsmWaterPolygon element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_waterway_linestring} table generated by imposm3. */ + public static record OsmWaterwayLinestring( + @Override String waterway, @Override String name, @Override String nameEn, @Override String nameDe, + @Override boolean isTunnel, @Override boolean isBridge, @Override boolean isIntermittent, + @Override SourceFeature source + ) implements Row, WithWaterway, WithName, WithNameEn, WithNameDe, WithIsTunnel, WithIsBridge, WithIsIntermittent, + WithSource { + + public OsmWaterwayLinestring(SourceFeature source, String mappingKey) { + this(source.getString("waterway"), source.getString("name"), source.getString("name:en"), + source.getString("name:de"), source.getBoolean("tunnel"), source.getBoolean("bridge"), + source.getBoolean("intermittent"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(matchAny("waterway", "stream", "river", "canal", "drain", "ditch"), + matchType("linestring")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmWaterwayLinestring}. + */ + public interface Handler { + + void process(OsmWaterwayLinestring element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_landcover_polygon} table generated by imposm3. */ + public static record OsmLandcoverPolygon( + @Override String subclass, @Override String mappingKey, @Override SourceFeature source + ) implements Row, WithSubclass, WithMappingKey, WithSource { + + public OsmLandcoverPolygon(SourceFeature source, String mappingKey) { + this(source.getString(mappingKey), mappingKey, source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(or( + matchAny("landuse", "allotments", "farm", "farmland", "orchard", "plant_nursery", "vineyard", "grass", + "grassland", "meadow", "forest", "village_green", "recreation_ground", "park"), + matchAny("natural", "wood", "wetland", "fell", "grassland", "heath", "scrub", "tundra", "glacier", "bare_rock", + "scree", "beach", "sand", "dune"), matchAny("leisure", "park", "garden", "golf_course"), + matchAny("wetland", "bog", "swamp", "wet_meadow", "marsh", "reedbed", "saltern", "tidalflat", "saltmarsh", + "mangrove")), matchType("polygon")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmLandcoverPolygon}. + */ + public interface Handler { + + void process(OsmLandcoverPolygon element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_landuse_polygon} table generated by imposm3. */ + public static record OsmLandusePolygon( + @Override String landuse, @Override String amenity, @Override String leisure, @Override String tourism, + @Override String place, @Override String waterway, @Override SourceFeature source + ) implements Row, WithLanduse, WithAmenity, WithLeisure, WithTourism, WithPlace, WithWaterway, WithSource { + + public OsmLandusePolygon(SourceFeature source, String mappingKey) { + this(source.getString("landuse"), source.getString("amenity"), source.getString("leisure"), + source.getString("tourism"), source.getString("place"), source.getString("waterway"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(or( + matchAny("landuse", "railway", "cemetery", "military", "residential", "commercial", "industrial", "garages", + "retail"), + matchAny("amenity", "bus_station", "school", "university", "kindergarten", "college", "library", "hospital"), + matchAny("leisure", "stadium", "pitch", "playground", "track"), matchAny("tourism", "theme_park", "zoo"), + matchAny("place", "suburb", "quarter", "neighbourhood"), matchAny("waterway", "dam")), matchType("polygon")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmLandusePolygon}. + */ + public interface Handler { + + void process(OsmLandusePolygon element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_peak_point} table generated by imposm3. */ + public static record OsmPeakPoint( + @Override String name, @Override String nameEn, @Override String nameDe, @Override String ele, + @Override String wikipedia, @Override SourceFeature source + ) implements Row, WithName, WithNameEn, WithNameDe, WithEle, WithWikipedia, WithSource { + + public OsmPeakPoint(SourceFeature source, String mappingKey) { + this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), source.getString("ele"), + source.getString("wikipedia"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(matchAny("natural", "peak", "volcano"), matchType("point")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmPeakPoint}. + */ + public interface Handler { + + void process(OsmPeakPoint element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_park_polygon} table generated by imposm3. */ + public static record OsmParkPolygon( + @Override String name, @Override String nameEn, @Override String nameDe, @Override String landuse, + @Override String leisure, @Override String boundary, @Override String protectionTitle, + @Override SourceFeature source + ) implements Row, WithName, WithNameEn, WithNameDe, WithLanduse, WithLeisure, WithBoundary, WithProtectionTitle, + WithSource { + + public OsmParkPolygon(SourceFeature source, String mappingKey) { + this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), + source.getString("landuse"), source.getString("leisure"), source.getString("boundary"), + source.getString("protection_title"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and( + or(matchAny("leisure", "nature_reserve"), matchAny("boundary", "national_park", "protected_area")), + matchType("polygon")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmParkPolygon}. + */ + public interface Handler { + + void process(OsmParkPolygon element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_aeroway_polygon} table generated by imposm3. */ + public static record OsmAerowayPolygon( + @Override String ref, @Override String aeroway, @Override SourceFeature source + ) implements Row, WithRef, WithAeroway, WithSource { + + public OsmAerowayPolygon(SourceFeature source, String mappingKey) { + this(source.getString("ref"), source.getString(mappingKey), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and( + or(matchAny("aeroway", "aerodrome", "heliport", "runway", "helipad", "taxiway", "apron"), + matchAny("area:aeroway", "aerodrome", "heliport", "runway", "helipad", "taxiway", "apron")), + matchType("polygon")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmAerowayPolygon}. + */ + public interface Handler { + + void process(OsmAerowayPolygon element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_aeroway_linestring} table generated by imposm3. */ + public static record OsmAerowayLinestring( + @Override String ref, @Override String aeroway, @Override SourceFeature source + ) implements Row, WithRef, WithAeroway, WithSource { + + public OsmAerowayLinestring(SourceFeature source, String mappingKey) { + this(source.getString("ref"), source.getString("aeroway"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(matchAny("aeroway", "runway", "taxiway"), matchType("linestring")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmAerowayLinestring}. + */ + public interface Handler { + + void process(OsmAerowayLinestring element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_aeroway_point} table generated by imposm3. */ + public static record OsmAerowayPoint( + @Override String ref, @Override String aeroway, @Override SourceFeature source + ) implements Row, WithRef, WithAeroway, WithSource { + + public OsmAerowayPoint(SourceFeature source, String mappingKey) { + this(source.getString("ref"), source.getString("aeroway"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(matchAny("aeroway", "gate"), matchType("point")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmAerowayPoint}. + */ + public interface Handler { + + void process(OsmAerowayPoint element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_highway_linestring} table generated by imposm3. */ + public static record OsmHighwayLinestring( + @Override String highway, @Override String construction, @Override String ref, @Override String network, + @Override int zOrder, @Override long layer, @Override long level, @Override boolean indoor, @Override String name, + @Override String nameEn, @Override String nameDe, @Override String shortName, @Override boolean isTunnel, + @Override boolean isBridge, @Override boolean isRamp, @Override boolean isFord, @Override int isOneway, + @Override boolean isArea, @Override String service, @Override String usage, @Override String publicTransport, + @Override String manMade, @Override String bicycle, @Override String foot, @Override String horse, + @Override String mtbScale, @Override String surface, @Override SourceFeature source + ) implements Row, WithHighway, WithConstruction, WithRef, WithNetwork, WithZOrder, WithLayer, WithLevel, WithIndoor, + WithName, WithNameEn, WithNameDe, WithShortName, WithIsTunnel, WithIsBridge, WithIsRamp, WithIsFord, WithIsOneway, + WithIsArea, WithService, WithUsage, WithPublicTransport, WithManMade, WithBicycle, WithFoot, WithHorse, + WithMtbScale, WithSurface, WithSource { + + public OsmHighwayLinestring(SourceFeature source, String mappingKey) { + this(source.getString("highway"), source.getString("construction"), source.getString("ref"), + source.getString("network"), source.getWayZorder(), source.getLong("layer"), source.getLong("level"), + source.getBoolean("indoor"), source.getString("name"), source.getString("name:en"), source.getString("name:de"), + source.getString("short_name"), source.getBoolean("tunnel"), source.getBoolean("bridge"), + source.getBoolean("ramp"), source.getBoolean("ford"), source.getDirection("oneway"), source.getBoolean("area"), + source.getString("service"), source.getString("usage"), source.getString("public_transport"), + source.getString("man_made"), source.getString("bicycle"), source.getString("foot"), source.getString("horse"), + source.getString("mtb:scale"), source.getString("surface"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(or( + matchAny("highway", "motorway", "motorway_link", "trunk", "trunk_link", "primary", "primary_link", "secondary", + "secondary_link", "tertiary", "tertiary_link", "unclassified", "residential", "living_street", "road", + "pedestrian", "path", "footway", "cycleway", "steps", "bridleway", "corridor", "service", "track", "raceway", + "construction"), matchAny("public_transport", "platform"), matchAny("man_made", "pier")), + matchType("linestring")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmHighwayLinestring}. + */ + public interface Handler { + + void process(OsmHighwayLinestring element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_railway_linestring} table generated by imposm3. */ + public static record OsmRailwayLinestring( + @Override String railway, @Override String ref, @Override String network, @Override int zOrder, + @Override long layer, @Override long level, @Override boolean indoor, @Override String name, + @Override String nameEn, @Override String nameDe, @Override String shortName, @Override boolean isTunnel, + @Override boolean isBridge, @Override boolean isRamp, @Override boolean isFord, @Override int isOneway, + @Override boolean isArea, @Override String service, @Override String usage, @Override SourceFeature source + ) implements Row, WithRailway, WithRef, WithNetwork, WithZOrder, WithLayer, WithLevel, WithIndoor, WithName, + WithNameEn, WithNameDe, WithShortName, WithIsTunnel, WithIsBridge, WithIsRamp, WithIsFord, WithIsOneway, WithIsArea, + WithService, WithUsage, WithSource { + + public OsmRailwayLinestring(SourceFeature source, String mappingKey) { + this(source.getString("railway"), source.getString("ref"), source.getString("network"), source.getWayZorder(), + source.getLong("layer"), source.getLong("level"), source.getBoolean("indoor"), source.getString("name"), + source.getString("name:en"), source.getString("name:de"), source.getString("short_name"), + source.getBoolean("tunnel"), source.getBoolean("bridge"), source.getBoolean("ramp"), source.getBoolean("ford"), + source.getDirection("oneway"), source.getBoolean("area"), source.getString("service"), + source.getString("usage"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and( + matchAny("railway", "rail", "narrow_gauge", "preserved", "funicular", "subway", "light_rail", "monorail", "tram"), + matchType("linestring")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmRailwayLinestring}. + */ + public interface Handler { + + void process(OsmRailwayLinestring element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_aerialway_linestring} table generated by imposm3. */ + public static record OsmAerialwayLinestring( + @Override String aerialway, @Override int zOrder, @Override long layer, @Override String name, + @Override String nameEn, @Override String nameDe, @Override String shortName, @Override boolean isTunnel, + @Override boolean isBridge, @Override boolean isRamp, @Override boolean isFord, @Override int isOneway, + @Override boolean isArea, @Override String service, @Override String usage, @Override SourceFeature source + ) implements Row, WithAerialway, WithZOrder, WithLayer, WithName, WithNameEn, WithNameDe, WithShortName, WithIsTunnel, + WithIsBridge, WithIsRamp, WithIsFord, WithIsOneway, WithIsArea, WithService, WithUsage, WithSource { + + public OsmAerialwayLinestring(SourceFeature source, String mappingKey) { + this(source.getString("aerialway"), source.getWayZorder(), source.getLong("layer"), source.getString("name"), + source.getString("name:en"), source.getString("name:de"), source.getString("short_name"), + source.getBoolean("tunnel"), source.getBoolean("bridge"), source.getBoolean("ramp"), source.getBoolean("ford"), + source.getDirection("oneway"), source.getBoolean("area"), source.getString("service"), + source.getString("usage"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(matchAny("aerialway", "cable_car", "gondola"), + matchType("linestring")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmAerialwayLinestring}. + */ + public interface Handler { + + void process(OsmAerialwayLinestring element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_shipway_linestring} table generated by imposm3. */ + public static record OsmShipwayLinestring( + @Override String shipway, @Override int zOrder, @Override long layer, @Override String name, + @Override String nameEn, @Override String nameDe, @Override String shortName, @Override boolean isTunnel, + @Override boolean isBridge, @Override boolean isRamp, @Override boolean isFord, @Override int isOneway, + @Override boolean isArea, @Override String service, @Override String usage, @Override SourceFeature source + ) implements Row, WithShipway, WithZOrder, WithLayer, WithName, WithNameEn, WithNameDe, WithShortName, WithIsTunnel, + WithIsBridge, WithIsRamp, WithIsFord, WithIsOneway, WithIsArea, WithService, WithUsage, WithSource { + + public OsmShipwayLinestring(SourceFeature source, String mappingKey) { + this(source.getString("route"), source.getWayZorder(), source.getLong("layer"), source.getString("name"), + source.getString("name:en"), source.getString("name:de"), source.getString("short_name"), + source.getBoolean("tunnel"), source.getBoolean("bridge"), source.getBoolean("ramp"), source.getBoolean("ford"), + source.getDirection("oneway"), source.getBoolean("area"), source.getString("service"), + source.getString("usage"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(matchAny("route", "ferry"), matchType("linestring")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmShipwayLinestring}. + */ + public interface Handler { + + void process(OsmShipwayLinestring element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_highway_polygon} table generated by imposm3. */ + public static record OsmHighwayPolygon( + @Override String highway, @Override int zOrder, @Override long layer, @Override long level, + @Override boolean indoor, @Override boolean isArea, @Override String publicTransport, @Override String manMade, + @Override SourceFeature source + ) implements Row, WithHighway, WithZOrder, WithLayer, WithLevel, WithIndoor, WithIsArea, WithPublicTransport, + WithManMade, WithSource { + + public OsmHighwayPolygon(SourceFeature source, String mappingKey) { + this(source.getString("highway"), source.getWayZorder(), source.getLong("layer"), source.getLong("level"), + source.getBoolean("indoor"), source.getBoolean("area"), source.getString("public_transport"), + source.getString("man_made"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and( + or(matchAny("highway", "path", "cycleway", "bridleway", "footway", "corridor", "pedestrian", "steps"), + matchAny("public_transport", "platform"), matchAny("man_made", "bridge", "pier")), matchType("polygon")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmHighwayPolygon}. + */ + public interface Handler { + + void process(OsmHighwayPolygon element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_building_polygon} table generated by imposm3. */ + public static record OsmBuildingPolygon( + @Override String material, @Override String colour, @Override String building, @Override String buildingpart, + @Override String buildingheight, @Override String buildingminHeight, @Override String buildinglevels, + @Override String buildingminLevel, @Override String height, @Override String minHeight, @Override String levels, + @Override String minLevel, @Override SourceFeature source + ) implements Row, WithMaterial, WithColour, WithBuilding, WithBuildingpart, WithBuildingheight, WithBuildingminHeight, + WithBuildinglevels, WithBuildingminLevel, WithHeight, WithMinHeight, WithLevels, WithMinLevel, WithSource { + + public OsmBuildingPolygon(SourceFeature source, String mappingKey) { + this(source.getString("building:material"), source.getString("building:colour"), source.getString("building"), + source.getString("building:part"), source.getString("building:height"), source.getString("building:min_height"), + source.getString("building:levels"), source.getString("building:min_level"), source.getString("height"), + source.getString("min_height"), source.getString("levels"), source.getString("min_level"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and( + or(matchField("building:part"), matchField("building"), matchAny("aeroway", "terminal", "hangar")), + not(matchAny("building", "no", "none", "No")), not(matchAny("building:part", "no", "none", "No")), + not(matchAny("man_made", "bridge")), matchType("polygon")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmBuildingPolygon}. + */ + public interface Handler { + + void process(OsmBuildingPolygon element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_marine_point} table generated by imposm3. */ + public static record OsmMarinePoint( + @Override String name, @Override String nameEn, @Override String nameDe, @Override String place, + @Override long rank, @Override boolean isIntermittent, @Override SourceFeature source + ) implements Row, WithName, WithNameEn, WithNameDe, WithPlace, WithRank, WithIsIntermittent, WithSource { + + public OsmMarinePoint(SourceFeature source, String mappingKey) { + this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), + source.getString("place"), source.getLong("rank"), source.getBoolean("intermittent"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(matchAny("place", "ocean", "sea"), matchField("name"), + matchType("point")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmMarinePoint}. + */ + public interface Handler { + + void process(OsmMarinePoint element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_continent_point} table generated by imposm3. */ + public static record OsmContinentPoint( + @Override String name, @Override String nameEn, @Override String nameDe, @Override SourceFeature source + ) implements Row, WithName, WithNameEn, WithNameDe, WithSource { + + public OsmContinentPoint(SourceFeature source, String mappingKey) { + this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(matchAny("place", "continent"), matchField("name"), + matchType("point")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmContinentPoint}. + */ + public interface Handler { + + void process(OsmContinentPoint element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_country_point} table generated by imposm3. */ + public static record OsmCountryPoint( + @Override String name, @Override String nameEn, @Override String nameDe, @Override long rank, + @Override String countryCodeIso31661Alpha2, @Override String iso31661Alpha2, @Override String iso31661, + @Override SourceFeature source + ) implements Row, WithName, WithNameEn, WithNameDe, WithRank, WithCountryCodeIso31661Alpha2, WithIso31661Alpha2, + WithIso31661, WithSource { + + public OsmCountryPoint(SourceFeature source, String mappingKey) { + this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), source.getLong("rank"), + source.getString("country_code_iso3166_1_alpha_2"), source.getString("ISO3166-1:alpha2"), + source.getString("ISO3166-1"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(matchAny("place", "country"), matchField("name"), matchType("point")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmCountryPoint}. + */ + public interface Handler { + + void process(OsmCountryPoint element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_island_polygon} table generated by imposm3. */ + public static record OsmIslandPolygon( + @Override String name, @Override String nameEn, @Override String nameDe, @Override long rank, + @Override SourceFeature source + ) implements Row, WithName, WithNameEn, WithNameDe, WithRank, WithSource { + + public OsmIslandPolygon(SourceFeature source, String mappingKey) { + this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), source.getLong("rank"), + source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(matchAny("place", "island"), matchField("name"), matchType("polygon")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmIslandPolygon}. + */ + public interface Handler { + + void process(OsmIslandPolygon element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_island_point} table generated by imposm3. */ + public static record OsmIslandPoint( + @Override String name, @Override String nameEn, @Override String nameDe, @Override long rank, + @Override SourceFeature source + ) implements Row, WithName, WithNameEn, WithNameDe, WithRank, WithSource { + + public OsmIslandPoint(SourceFeature source, String mappingKey) { + this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), source.getLong("rank"), + source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(matchAny("place", "island"), matchField("name"), matchType("point")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmIslandPoint}. + */ + public interface Handler { + + void process(OsmIslandPoint element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_state_point} table generated by imposm3. */ + public static record OsmStatePoint( + @Override String name, @Override String nameEn, @Override String nameDe, @Override String isInCountry, + @Override String isInCountryCode, @Override String ref, @Override long rank, @Override SourceFeature source + ) implements Row, WithName, WithNameEn, WithNameDe, WithIsInCountry, WithIsInCountryCode, WithRef, WithRank, + WithSource { + + public OsmStatePoint(SourceFeature source, String mappingKey) { + this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), + source.getString("is_in:country"), source.getString("is_in:country_code"), source.getString("ref"), + source.getLong("rank"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(matchAny("place", "state"), matchField("name"), matchType("point")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmStatePoint}. + */ + public interface Handler { + + void process(OsmStatePoint element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_city_point} table generated by imposm3. */ + public static record OsmCityPoint( + @Override String name, @Override String nameEn, @Override String nameDe, @Override String place, + @Override long population, @Override String capital, @Override long rank, @Override SourceFeature source + ) implements Row, WithName, WithNameEn, WithNameDe, WithPlace, WithPopulation, WithCapital, WithRank, WithSource { + + public OsmCityPoint(SourceFeature source, String mappingKey) { + this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), + source.getString("place"), source.getLong("population"), source.getString("capital"), source.getLong("rank"), + source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and( + matchAny("place", "city", "town", "village", "hamlet", "suburb", "quarter", "neighbourhood", "isolated_dwelling"), + matchField("name"), matchType("point")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmCityPoint}. + */ + public interface Handler { + + void process(OsmCityPoint element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_housenumber_point} table generated by imposm3. */ + public static record OsmHousenumberPoint(@Override String housenumber, @Override SourceFeature source) implements Row, + WithHousenumber, WithSource { + + public OsmHousenumberPoint(SourceFeature source, String mappingKey) { + this(source.getString("addr:housenumber"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = or(and(matchField("addr:housenumber"), matchType("point")), + and(matchField("addr:housenumber"), matchType("polygon"))); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmHousenumberPoint}. + */ + public interface Handler { + + void process(OsmHousenumberPoint element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_poi_point} table generated by imposm3. */ + public static record OsmPoiPoint( + @Override String name, @Override String nameEn, @Override String nameDe, @Override String subclass, + @Override String mappingKey, @Override String station, @Override String funicular, @Override String information, + @Override String uicRef, @Override String religion, @Override long level, @Override boolean indoor, + @Override long layer, @Override String sport, @Override SourceFeature source + ) implements Row, WithName, WithNameEn, WithNameDe, WithSubclass, WithMappingKey, WithStation, WithFunicular, + WithInformation, WithUicRef, WithReligion, WithLevel, WithIndoor, WithLayer, WithSport, WithSource { + + public OsmPoiPoint(SourceFeature source, String mappingKey) { + this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), + source.getString(mappingKey), mappingKey, source.getString("station"), source.getString("funicular"), + source.getString("information"), source.getString("uic_ref"), source.getString("religion"), + source.getLong("level"), source.getBoolean("indoor"), source.getLong("layer"), source.getString("sport"), + source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(or(matchAny("aerialway", "station"), + matchAny("amenity", "arts_centre", "bank", "bar", "bbq", "bicycle_parking", "bicycle_rental", "biergarten", + "bus_station", "cafe", "cinema", "clinic", "college", "community_centre", "courthouse", "dentist", "doctors", + "drinking_water", "embassy", "fast_food", "ferry_terminal", "fire_station", "food_court", "fuel", "grave_yard", + "hospital", "ice_cream", "kindergarten", "library", "marketplace", "motorcycle_parking", "nightclub", + "nursing_home", "parking", "pharmacy", "place_of_worship", "police", "post_box", "post_office", "prison", "pub", + "public_building", "recycling", "restaurant", "school", "shelter", "swimming_pool", "taxi", "telephone", + "theatre", "toilets", "townhall", "university", "veterinary", "waste_basket"), + matchAny("barrier", "bollard", "border_control", "cycle_barrier", "gate", "lift_gate", "sally_port", "stile", + "toll_booth"), matchAny("building", "dormitory"), matchAny("highway", "bus_stop"), + matchAny("historic", "monument", "castle", "ruins"), + matchAny("landuse", "basin", "brownfield", "cemetery", "reservoir", "winter_sports"), + matchAny("leisure", "dog_park", "escape_game", "garden", "golf_course", "ice_rink", "hackerspace", "marina", + "miniature_golf", "park", "pitch", "playground", "sports_centre", "stadium", "swimming_area", "swimming_pool", + "water_park"), matchAny("railway", "halt", "station", "subway_entrance", "train_station_entrance", "tram_stop"), + matchAny("shop", "accessories", "alcohol", "antiques", "art", "bag", "bakery", "beauty", "bed", "beverages", + "bicycle", "books", "boutique", "butcher", "camera", "car", "car_repair", "car_parts", "carpet", "charity", + "chemist", "chocolate", "clothes", "coffee", "computer", "confectionery", "convenience", "copyshop", + "cosmetics", "deli", "delicatessen", "department_store", "doityourself", "dry_cleaning", "electronics", + "erotic", "fabric", "florist", "frozen_food", "furniture", "garden_centre", "general", "gift", "greengrocer", + "hairdresser", "hardware", "hearing_aids", "hifi", "ice_cream", "interior_decoration", "jewelry", "kiosk", + "lamps", "laundry", "mall", "massage", "mobile_phone", "motorcycle", "music", "musical_instrument", "newsagent", + "optician", "outdoor", "perfume", "perfumery", "pet", "photo", "second_hand", "shoes", "sports", "stationery", + "supermarket", "tailor", "tattoo", "ticket", "tobacco", "toys", "travel_agency", "video", "video_games", + "watches", "weapons", "wholesale", "wine"), + matchAny("sport", "american_football", "archery", "athletics", "australian_football", "badminton", "baseball", + "basketball", "beachvolleyball", "billiards", "bmx", "boules", "bowls", "boxing", "canadian_football", "canoe", + "chess", "climbing", "climbing_adventure", "cricket", "cricket_nets", "croquet", "curling", "cycling", + "disc_golf", "diving", "dog_racing", "equestrian", "fatsal", "field_hockey", "free_flying", "gaelic_games", + "golf", "gymnastics", "handball", "hockey", "horse_racing", "horseshoes", "ice_hockey", "ice_stock", "judo", + "karting", "korfball", "long_jump", "model_aerodrome", "motocross", "motor", "multi", "netball", "orienteering", + "paddle_tennis", "paintball", "paragliding", "pelota", "racquet", "rc_car", "rowing", "rugby", "rugby_league", + "rugby_union", "running", "sailing", "scuba_diving", "shooting", "shooting_range", "skateboard", "skating", + "skiing", "soccer", "surfing", "swimming", "table_soccer", "table_tennis", "team_handball", "tennis", + "toboggan", "volleyball", "water_ski", "yoga"), + matchAny("tourism", "alpine_hut", "aquarium", "artwork", "attraction", "bed_and_breakfast", "camp_site", + "caravan_site", "chalet", "gallery", "guest_house", "hostel", "hotel", "information", "motel", "museum", + "picnic_site", "theme_park", "viewpoint", "zoo"), matchAny("waterway", "dock")), matchType("point")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmPoiPoint}. + */ + public interface Handler { + + void process(OsmPoiPoint element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_poi_polygon} table generated by imposm3. */ + public static record OsmPoiPolygon( + @Override String name, @Override String nameEn, @Override String nameDe, @Override String subclass, + @Override String mappingKey, @Override String station, @Override String funicular, @Override String information, + @Override String uicRef, @Override String religion, @Override long level, @Override boolean indoor, + @Override long layer, @Override String sport, @Override SourceFeature source + ) implements Row, WithName, WithNameEn, WithNameDe, WithSubclass, WithMappingKey, WithStation, WithFunicular, + WithInformation, WithUicRef, WithReligion, WithLevel, WithIndoor, WithLayer, WithSport, WithSource { + + public OsmPoiPolygon(SourceFeature source, String mappingKey) { + this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), + source.getString(mappingKey), mappingKey, source.getString("station"), source.getString("funicular"), + source.getString("information"), source.getString("uic_ref"), source.getString("religion"), + source.getLong("level"), source.getBoolean("indoor"), source.getLong("layer"), source.getString("sport"), + source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(or(matchAny("aerialway", "station"), + matchAny("amenity", "arts_centre", "bank", "bar", "bbq", "bicycle_parking", "bicycle_rental", "biergarten", + "bus_station", "cafe", "cinema", "clinic", "college", "community_centre", "courthouse", "dentist", "doctors", + "drinking_water", "embassy", "fast_food", "ferry_terminal", "fire_station", "food_court", "fuel", "grave_yard", + "hospital", "ice_cream", "kindergarten", "library", "marketplace", "motorcycle_parking", "nightclub", + "nursing_home", "parking", "pharmacy", "place_of_worship", "police", "post_box", "post_office", "prison", "pub", + "public_building", "recycling", "restaurant", "school", "shelter", "swimming_pool", "taxi", "telephone", + "theatre", "toilets", "townhall", "university", "veterinary", "waste_basket"), + matchAny("barrier", "bollard", "border_control", "cycle_barrier", "gate", "lift_gate", "sally_port", "stile", + "toll_booth"), matchAny("building", "dormitory"), matchAny("highway", "bus_stop"), + matchAny("historic", "monument", "castle", "ruins"), + matchAny("landuse", "basin", "brownfield", "cemetery", "reservoir", "winter_sports"), + matchAny("leisure", "dog_park", "escape_game", "garden", "golf_course", "ice_rink", "hackerspace", "marina", + "miniature_golf", "park", "pitch", "playground", "sports_centre", "stadium", "swimming_area", "swimming_pool", + "water_park"), matchAny("railway", "halt", "station", "subway_entrance", "train_station_entrance", "tram_stop"), + matchAny("shop", "accessories", "alcohol", "antiques", "art", "bag", "bakery", "beauty", "bed", "beverages", + "bicycle", "books", "boutique", "butcher", "camera", "car", "car_repair", "car_parts", "carpet", "charity", + "chemist", "chocolate", "clothes", "coffee", "computer", "confectionery", "convenience", "copyshop", + "cosmetics", "deli", "delicatessen", "department_store", "doityourself", "dry_cleaning", "electronics", + "erotic", "fabric", "florist", "frozen_food", "furniture", "garden_centre", "general", "gift", "greengrocer", + "hairdresser", "hardware", "hearing_aids", "hifi", "ice_cream", "interior_decoration", "jewelry", "kiosk", + "lamps", "laundry", "mall", "massage", "mobile_phone", "motorcycle", "music", "musical_instrument", "newsagent", + "optician", "outdoor", "perfume", "perfumery", "pet", "photo", "second_hand", "shoes", "sports", "stationery", + "supermarket", "tailor", "tattoo", "ticket", "tobacco", "toys", "travel_agency", "video", "video_games", + "watches", "weapons", "wholesale", "wine"), + matchAny("sport", "american_football", "archery", "athletics", "australian_football", "badminton", "baseball", + "basketball", "beachvolleyball", "billiards", "bmx", "boules", "bowls", "boxing", "canadian_football", "canoe", + "chess", "climbing", "climbing_adventure", "cricket", "cricket_nets", "croquet", "curling", "cycling", + "disc_golf", "diving", "dog_racing", "equestrian", "fatsal", "field_hockey", "free_flying", "gaelic_games", + "golf", "gymnastics", "handball", "hockey", "horse_racing", "horseshoes", "ice_hockey", "ice_stock", "judo", + "karting", "korfball", "long_jump", "model_aerodrome", "motocross", "motor", "multi", "netball", "orienteering", + "paddle_tennis", "paintball", "paragliding", "pelota", "racquet", "rc_car", "rowing", "rugby", "rugby_league", + "rugby_union", "running", "sailing", "scuba_diving", "shooting", "shooting_range", "skateboard", "skating", + "skiing", "soccer", "surfing", "swimming", "table_soccer", "table_tennis", "team_handball", "tennis", + "toboggan", "volleyball", "water_ski", "yoga"), + matchAny("tourism", "alpine_hut", "aquarium", "artwork", "attraction", "bed_and_breakfast", "camp_site", + "caravan_site", "chalet", "gallery", "guest_house", "hostel", "hotel", "information", "motel", "museum", + "picnic_site", "theme_park", "viewpoint", "zoo"), matchAny("waterway", "dock")), matchType("polygon")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmPoiPolygon}. + */ + public interface Handler { + + void process(OsmPoiPolygon element, FeatureCollector features); + } + } + + /** An OSM element that would appear in the {@code osm_aerodrome_label_point} table generated by imposm3. */ + public static record OsmAerodromeLabelPoint( + @Override String name, @Override String nameEn, @Override String nameDe, @Override String aerodromeType, + @Override String aerodrome, @Override String military, @Override String iata, @Override String icao, + @Override String ele, @Override SourceFeature source + ) implements Row, WithName, WithNameEn, WithNameDe, WithAerodromeType, WithAerodrome, WithMilitary, WithIata, + WithIcao, WithEle, WithSource { + + public OsmAerodromeLabelPoint(SourceFeature source, String mappingKey) { + this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), + source.getString("aerodrome:type"), source.getString("aerodrome"), source.getString("military"), + source.getString("iata"), source.getString("icao"), source.getString("ele"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = or(and(matchAny("aeroway", "aerodrome"), matchType("point")), + and(matchAny("aeroway", "aerodrome"), matchType("polygon"))); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmAerodromeLabelPoint}. + */ + public interface Handler { + + void process(OsmAerodromeLabelPoint element, FeatureCollector features); + } + } + + /** Rows with a long adminLevel attribute. */ + public interface WithAdminLevel { + + long adminLevel(); + } + + /** Rows with a String aerialway attribute. */ + public interface WithAerialway { + + String aerialway(); + } + + /** Rows with a String aerodrome attribute. */ + public interface WithAerodrome { + + String aerodrome(); + } + + /** Rows with a String aerodromeType attribute. */ + public interface WithAerodromeType { + + String aerodromeType(); + } + + /** Rows with a String aeroway attribute. */ + public interface WithAeroway { + + String aeroway(); + } + + /** Rows with a String amenity attribute. */ + public interface WithAmenity { + + String amenity(); + } + + /** Rows with a String bicycle attribute. */ + public interface WithBicycle { + + String bicycle(); + } + + /** Rows with a String boundary attribute. */ + public interface WithBoundary { + + String boundary(); + } + + /** Rows with a String building attribute. */ + public interface WithBuilding { + + String building(); + } + + /** Rows with a String buildingheight attribute. */ + public interface WithBuildingheight { + + String buildingheight(); + } + + /** Rows with a String buildinglevels attribute. */ + public interface WithBuildinglevels { + + String buildinglevels(); + } + + /** Rows with a String buildingminHeight attribute. */ + public interface WithBuildingminHeight { + + String buildingminHeight(); + } + + /** Rows with a String buildingminLevel attribute. */ + public interface WithBuildingminLevel { + + String buildingminLevel(); + } + + /** Rows with a String buildingpart attribute. */ + public interface WithBuildingpart { + + String buildingpart(); + } + + /** Rows with a String capital attribute. */ + public interface WithCapital { + + String capital(); + } + + /** Rows with a String claimedBy attribute. */ + public interface WithClaimedBy { + + String claimedBy(); + } + + /** Rows with a String colour attribute. */ + public interface WithColour { + + String colour(); + } + + /** Rows with a String construction attribute. */ + public interface WithConstruction { + + String construction(); + } + + /** Rows with a String countryCodeIso31661Alpha2 attribute. */ + public interface WithCountryCodeIso31661Alpha2 { + + String countryCodeIso31661Alpha2(); + } + + /** Rows with a String disputedBy attribute. */ + public interface WithDisputedBy { + + String disputedBy(); + } + + /** Rows with a String ele attribute. */ + public interface WithEle { + + String ele(); + } + + /** Rows with a String foot attribute. */ + public interface WithFoot { + + String foot(); + } + + /** Rows with a String funicular attribute. */ + public interface WithFunicular { + + String funicular(); + } + + /** Rows with a String height attribute. */ + public interface WithHeight { + + String height(); + } + + /** Rows with a String highway attribute. */ + public interface WithHighway { + + String highway(); + } + + /** Rows with a String horse attribute. */ + public interface WithHorse { + + String horse(); + } + + /** Rows with a String housenumber attribute. */ + public interface WithHousenumber { + + String housenumber(); + } + + /** Rows with a String iata attribute. */ + public interface WithIata { + + String iata(); + } + + /** Rows with a String icao attribute. */ + public interface WithIcao { + + String icao(); + } + + /** Rows with a boolean indoor attribute. */ + public interface WithIndoor { + + boolean indoor(); + } + + /** Rows with a String information attribute. */ + public interface WithInformation { + + String information(); + } + + /** Rows with a boolean isArea attribute. */ + public interface WithIsArea { + + boolean isArea(); + } + + /** Rows with a boolean isBridge attribute. */ + public interface WithIsBridge { + + boolean isBridge(); + } + + /** Rows with a boolean isFord attribute. */ + public interface WithIsFord { + + boolean isFord(); + } + + /** Rows with a String isInCountry attribute. */ + public interface WithIsInCountry { + + String isInCountry(); + } + + /** Rows with a String isInCountryCode attribute. */ + public interface WithIsInCountryCode { + + String isInCountryCode(); + } + + /** Rows with a boolean isIntermittent attribute. */ + public interface WithIsIntermittent { + + boolean isIntermittent(); + } + + /** Rows with a int isOneway attribute. */ + public interface WithIsOneway { + + int isOneway(); + } + + /** Rows with a boolean isRamp attribute. */ + public interface WithIsRamp { + + boolean isRamp(); + } + + /** Rows with a boolean isTunnel attribute. */ + public interface WithIsTunnel { + + boolean isTunnel(); + } + + /** Rows with a String iso31661 attribute. */ + public interface WithIso31661 { + + String iso31661(); + } + + /** Rows with a String iso31661Alpha2 attribute. */ + public interface WithIso31661Alpha2 { + + String iso31661Alpha2(); + } + + /** Rows with a String landuse attribute. */ + public interface WithLanduse { + + String landuse(); + } + + /** Rows with a long layer attribute. */ + public interface WithLayer { + + long layer(); + } + + /** Rows with a String leisure attribute. */ + public interface WithLeisure { + + String leisure(); + } + + /** Rows with a long level attribute. */ + public interface WithLevel { + + long level(); + } + + /** Rows with a String levels attribute. */ + public interface WithLevels { + + String levels(); + } + + /** Rows with a String manMade attribute. */ + public interface WithManMade { + + String manMade(); + } + + /** Rows with a String mappingKey attribute. */ + public interface WithMappingKey { + + String mappingKey(); + } + + /** Rows with a String material attribute. */ + public interface WithMaterial { + + String material(); + } + + /** Rows with a String military attribute. */ + public interface WithMilitary { + + String military(); + } + + /** Rows with a String minHeight attribute. */ + public interface WithMinHeight { + + String minHeight(); + } + + /** Rows with a String minLevel attribute. */ + public interface WithMinLevel { + + String minLevel(); + } + + /** Rows with a String mtbScale attribute. */ + public interface WithMtbScale { + + String mtbScale(); + } + + /** Rows with a String name attribute. */ + public interface WithName { + + String name(); + } + + /** Rows with a String nameDe attribute. */ + public interface WithNameDe { + + String nameDe(); + } + + /** Rows with a String nameEn attribute. */ + public interface WithNameEn { + + String nameEn(); + } + + /** Rows with a String natural attribute. */ + public interface WithNatural { + + String natural(); + } + + /** Rows with a String network attribute. */ + public interface WithNetwork { + + String network(); + } + + /** Rows with a String place attribute. */ + public interface WithPlace { + + String place(); + } + + /** Rows with a long population attribute. */ + public interface WithPopulation { + + long population(); + } + + /** Rows with a String protectionTitle attribute. */ + public interface WithProtectionTitle { + + String protectionTitle(); + } + + /** Rows with a String publicTransport attribute. */ + public interface WithPublicTransport { + + String publicTransport(); + } + + /** Rows with a String railway attribute. */ + public interface WithRailway { + + String railway(); + } + + /** Rows with a long rank attribute. */ + public interface WithRank { + + long rank(); + } + + /** Rows with a String ref attribute. */ + public interface WithRef { + + String ref(); + } + + /** Rows with a String relbuildingheight attribute. */ + public interface WithRelbuildingheight { + + String relbuildingheight(); + } + + /** Rows with a String relbuildinglevels attribute. */ + public interface WithRelbuildinglevels { + + String relbuildinglevels(); + } + + /** Rows with a String relbuildingminHeight attribute. */ + public interface WithRelbuildingminHeight { + + String relbuildingminHeight(); + } + + /** Rows with a String relbuildingminLevel attribute. */ + public interface WithRelbuildingminLevel { + + String relbuildingminLevel(); + } + + /** Rows with a String relheight attribute. */ + public interface WithRelheight { + + String relheight(); + } + + /** Rows with a String religion attribute. */ + public interface WithReligion { + + String religion(); + } + + /** Rows with a String rellevels attribute. */ + public interface WithRellevels { + + String rellevels(); + } + + /** Rows with a String relminHeight attribute. */ + public interface WithRelminHeight { + + String relminHeight(); + } + + /** Rows with a String relminLevel attribute. */ + public interface WithRelminLevel { + + String relminLevel(); + } + + /** Rows with a String service attribute. */ + public interface WithService { + + String service(); + } + + /** Rows with a String shipway attribute. */ + public interface WithShipway { + + String shipway(); + } + + /** Rows with a String shortName attribute. */ + public interface WithShortName { + + String shortName(); + } + + /** Rows with a SourceFeature source attribute. */ + public interface WithSource { + + SourceFeature source(); + } + + /** Rows with a String sport attribute. */ + public interface WithSport { + + String sport(); + } + + /** Rows with a String station attribute. */ + public interface WithStation { + + String station(); + } + + /** Rows with a String subclass attribute. */ + public interface WithSubclass { + + String subclass(); + } + + /** Rows with a String surface attribute. */ + public interface WithSurface { + + String surface(); + } + + /** Rows with a String tourism attribute. */ + public interface WithTourism { + + String tourism(); + } + + /** Rows with a String uicRef attribute. */ + public interface WithUicRef { + + String uicRef(); + } + + /** Rows with a String usage attribute. */ + public interface WithUsage { + + String usage(); + } + + /** Rows with a String waterway attribute. */ + public interface WithWaterway { + + String waterway(); + } + + /** Rows with a String wikipedia attribute. */ + public interface WithWikipedia { + + String wikipedia(); + } + + /** Rows with a int zOrder attribute. */ + public interface WithZOrder { + + int zOrder(); + } + + /** Index to efficiently choose which imposm3 "tables" an element should appear in based on its attributes. */ + public static final MultiExpression MAPPINGS = MultiExpression.of(List.of( + MultiExpression.entry(new RowClassAndConstructor(OsmWaterPolygon.class, OsmWaterPolygon::new), + OsmWaterPolygon.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmWaterwayLinestring.class, OsmWaterwayLinestring::new), + OsmWaterwayLinestring.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmLandcoverPolygon.class, OsmLandcoverPolygon::new), + OsmLandcoverPolygon.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmLandusePolygon.class, OsmLandusePolygon::new), + OsmLandusePolygon.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmPeakPoint.class, OsmPeakPoint::new), OsmPeakPoint.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmParkPolygon.class, OsmParkPolygon::new), + OsmParkPolygon.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmAerowayPolygon.class, OsmAerowayPolygon::new), + OsmAerowayPolygon.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmAerowayLinestring.class, OsmAerowayLinestring::new), + OsmAerowayLinestring.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmAerowayPoint.class, OsmAerowayPoint::new), + OsmAerowayPoint.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmHighwayLinestring.class, OsmHighwayLinestring::new), + OsmHighwayLinestring.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmRailwayLinestring.class, OsmRailwayLinestring::new), + OsmRailwayLinestring.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmAerialwayLinestring.class, OsmAerialwayLinestring::new), + OsmAerialwayLinestring.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmShipwayLinestring.class, OsmShipwayLinestring::new), + OsmShipwayLinestring.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmHighwayPolygon.class, OsmHighwayPolygon::new), + OsmHighwayPolygon.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmBuildingPolygon.class, OsmBuildingPolygon::new), + OsmBuildingPolygon.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmMarinePoint.class, OsmMarinePoint::new), + OsmMarinePoint.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmContinentPoint.class, OsmContinentPoint::new), + OsmContinentPoint.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmCountryPoint.class, OsmCountryPoint::new), + OsmCountryPoint.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmIslandPolygon.class, OsmIslandPolygon::new), + OsmIslandPolygon.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmIslandPoint.class, OsmIslandPoint::new), + OsmIslandPoint.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmStatePoint.class, OsmStatePoint::new), OsmStatePoint.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmCityPoint.class, OsmCityPoint::new), OsmCityPoint.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmHousenumberPoint.class, OsmHousenumberPoint::new), + OsmHousenumberPoint.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmPoiPoint.class, OsmPoiPoint::new), OsmPoiPoint.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmPoiPolygon.class, OsmPoiPolygon::new), OsmPoiPolygon.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmAerodromeLabelPoint.class, OsmAerodromeLabelPoint::new), + OsmAerodromeLabelPoint.MAPPING) + )); + + /** + * 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, List>> generateDispatchMap(List handlers) { + Map, List>> result = new HashMap<>(); + for (var handler : handlers) { + if (handler instanceof OsmWaterPolygon.Handler typedHandler) { + result.computeIfAbsent(OsmWaterPolygon.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmWaterwayLinestring.Handler typedHandler) { + result.computeIfAbsent(OsmWaterwayLinestring.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmLandcoverPolygon.Handler typedHandler) { + result.computeIfAbsent(OsmLandcoverPolygon.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmLandusePolygon.Handler typedHandler) { + result.computeIfAbsent(OsmLandusePolygon.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmPeakPoint.Handler typedHandler) { + result.computeIfAbsent(OsmPeakPoint.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmParkPolygon.Handler typedHandler) { + result.computeIfAbsent(OsmParkPolygon.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmAerowayPolygon.Handler typedHandler) { + result.computeIfAbsent(OsmAerowayPolygon.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmAerowayLinestring.Handler typedHandler) { + result.computeIfAbsent(OsmAerowayLinestring.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmAerowayPoint.Handler typedHandler) { + result.computeIfAbsent(OsmAerowayPoint.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmHighwayLinestring.Handler typedHandler) { + result.computeIfAbsent(OsmHighwayLinestring.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmRailwayLinestring.Handler typedHandler) { + result.computeIfAbsent(OsmRailwayLinestring.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmAerialwayLinestring.Handler typedHandler) { + result.computeIfAbsent(OsmAerialwayLinestring.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmShipwayLinestring.Handler typedHandler) { + result.computeIfAbsent(OsmShipwayLinestring.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmHighwayPolygon.Handler typedHandler) { + result.computeIfAbsent(OsmHighwayPolygon.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmBuildingPolygon.Handler typedHandler) { + result.computeIfAbsent(OsmBuildingPolygon.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmMarinePoint.Handler typedHandler) { + result.computeIfAbsent(OsmMarinePoint.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmContinentPoint.Handler typedHandler) { + result.computeIfAbsent(OsmContinentPoint.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmCountryPoint.Handler typedHandler) { + result.computeIfAbsent(OsmCountryPoint.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmIslandPolygon.Handler typedHandler) { + result.computeIfAbsent(OsmIslandPolygon.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmIslandPoint.Handler typedHandler) { + result.computeIfAbsent(OsmIslandPoint.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmStatePoint.Handler typedHandler) { + result.computeIfAbsent(OsmStatePoint.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmCityPoint.Handler typedHandler) { + result.computeIfAbsent(OsmCityPoint.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmHousenumberPoint.Handler typedHandler) { + result.computeIfAbsent(OsmHousenumberPoint.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmPoiPoint.Handler typedHandler) { + result.computeIfAbsent(OsmPoiPoint.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmPoiPolygon.Handler typedHandler) { + result.computeIfAbsent(OsmPoiPolygon.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + if (handler instanceof OsmAerodromeLabelPoint.Handler typedHandler) { + result.computeIfAbsent(OsmAerodromeLabelPoint.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } + } + return result; + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/AerodromeLabel.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/AerodromeLabel.java new file mode 100644 index 0000000..1230a30 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/AerodromeLabel.java @@ -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. + *

    + * This class is ported to Java from OpenMapTiles + * aerodrome_layer sql files. + */ +public class AerodromeLabel implements + OpenMapTilesSchema.AerodromeLabel, + Tables.OsmAerodromeLabelPoint.Handler { + + private final MultiExpression.Index 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)); + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Aeroway.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Aeroway.java new file mode 100644 index 0000000..316ab52 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Aeroway.java @@ -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. + *

    + * This class is ported to Java from OpenMapTiles + * aeroway sql files. + */ +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()); + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Boundary.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Boundary.java new file mode 100644 index 0000000..e7c0a36 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Boundary.java @@ -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. + *

    + * This class is ported to Java from OpenMapTiles + * boundary sql files. + */ +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 regionNames = new ConcurrentHashMap<>(); + // need to synchronize updates to these shared data structures: + private final Map> regionGeometries = new HashMap<>(); + private final Map> 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 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 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 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 emit) { + if (BasemapProfile.OSM_SOURCE.equals(sourceName)) { + var timer = stats.startStage("boundaries"); + LongObjectMap 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 postProcess(int zoom, List 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 countryBoundaries, + Set allRegions, + LineString lineString + ) { + Set validRegions = allRegions.stream() + .filter(countryBoundaries::containsKey) + .collect(Collectors.toSet()); + if (validRegions.isEmpty()) { + return BorderingRegions.empty(); + } + List rights = new ArrayList<>(); + List 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 prepareRegionPolygons() { + LOGGER.info("Creating polygons for " + regionGeometries.size() + " boundaries"); + LongObjectMap 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 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 regions, + String claimedBy, + String name + ) { + + CountryBoundaryComponent groupingKey() { + return new CountryBoundaryComponent(adminLevel, disputed, maritime, minzoom, null, regions, claimedBy, name); + } + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Building.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Building.java new file mode 100644 index 0000000..a5e92e3 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Building.java @@ -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. + *

    + * This class is ported to Java from OpenMapTiles + * building sql files. + */ +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 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 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 postProcess(int zoom, + List 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); + } + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Housenumber.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Housenumber.java new file mode 100644 index 0000000..c10ae0f --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Housenumber.java @@ -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. + *

    + * This class is ported to Java from OpenMapTiles + * housenumber sql files. + */ +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); + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Landcover.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Landcover.java new file mode 100644 index 0000000..629c9a1 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Landcover.java @@ -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. + *

    + * This class is ported to Java from OpenMapTiles + * landcover sql files. + */ +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 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 WOOD_OR_FOREST = Set.of( + FieldValues.SUBCLASS_WOOD, + FieldValues.SUBCLASS_FOREST + ); + private final MultiExpression.Index 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 postProcess(int zoom, List 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 result = new ArrayList<>(); + List toMerge = new ArrayList<>(); + for (var item : items) { + Map 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; + } + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Landuse.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Landuse.java new file mode 100644 index 0000000..1125571 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Landuse.java @@ -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. + *

    + * This class is ported to Java from OpenMapTiles + * landuse sql files. + */ +public class Landuse implements + OpenMapTilesSchema.Landuse, + BasemapProfile.NaturalEarthProcessor, + Tables.OsmLandusePolygon.Handler { + + private static final ZoomFunction MIN_PIXEL_SIZE_THRESHOLDS = ZoomFunction.fromMaxZoomThresholds(Map.of( + 13, 4, + 7, 2, + 6, 1 + )); + private static final Set 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); + } + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/MountainPeak.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/MountainPeak.java new file mode 100644 index 0000000..f12b7ff --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/MountainPeak.java @@ -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. + *

    + * This class is ported to Java from OpenMapTiles + * mountain_peak sql files. + */ +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 postProcess(int zoom, List 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; + } + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Park.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Park.java new file mode 100644 index 0000000..1892006 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Park.java @@ -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. + *

    + * This class is ported to Java from OpenMapTiles + * park sql files. + */ +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 postProcess(int zoom, List 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; + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Place.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Place.java new file mode 100644 index 0000000..753c723 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Place.java @@ -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. + *

    + * This class is ported to Java from OpenMapTiles + * place sql files. + */ +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 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 MAJOR_CITY_PLACES = Set.of("city", "town", "village"); + private static final ZoomFunction 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 countries = PolygonIndex.create(); + private PolygonIndex states = PolygonIndex.create(); + private PointIndex 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 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 postProcess(int zoom, List 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 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 names) {} +} + diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Poi.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Poi.java new file mode 100644 index 0000000..40f1425 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Poi.java @@ -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. + *

    + * This class is ported to Java from OpenMapTiles + * poi sql files. + */ +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 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 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 + 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 postProcess(int zoom, List 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; + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Transportation.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Transportation.java new file mode 100644 index 0000000..69f231f --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Transportation.java @@ -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. + *

    + * This class is ported to Java from OpenMapTiles + * transportation sql files. + */ +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 classMapping = FieldMappings.Class.index(); + private static final Set RAILWAY_RAIL_VALUES = Set.of( + FieldValues.SUBCLASS_RAIL, + FieldValues.SUBCLASS_NARROW_GAUGE, + FieldValues.SUBCLASS_PRESERVED, + FieldValues.SUBCLASS_FUNICULAR + ); + private static final Set RAILWAY_TRANSIT_VALUES = Set.of( + FieldValues.SUBCLASS_SUBWAY, + FieldValues.SUBCLASS_LIGHT_RAIL, + FieldValues.SUBCLASS_MONORAIL, + FieldValues.SUBCLASS_TRAM + ); + private static final Set 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 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 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 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) 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 postProcess(int zoom, List 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); + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/TransportationName.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/TransportationName.java new file mode 100644 index 0000000..e36b195 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/TransportationName.java @@ -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. + *

    + * This class is ported to Java from OpenMapTiles + * transportation_name sql files. + */ +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 RELATION_ORDERING = Comparator + .comparingInt(r -> r.network.ordinal()) + // TODO also compare network string? + .thenComparingInt(r -> r.ref.length()) + .thenComparing(RouteRelation::ref); + private final Map 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 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> 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> 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 postProcess(int zoom, List 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, 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 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); + } + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Water.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Water.java new file mode 100644 index 0000000..b9ce7f5 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Water.java @@ -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. + *

    + * This class is ported to Java from OpenMapTiles + * water sql files. + */ +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 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)); + } + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/WaterName.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/WaterName.java new file mode 100644 index 0000000..a29fe8d --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/WaterName.java @@ -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. + *

    + * This class is ported to Java from OpenMapTiles + * water_name sql files. + */ +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 lakeCenterlines = new GHLongObjectHashMap<>(); + // may be updated concurrently by multiple threads + private final ConcurrentSkipListMap 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 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()); + } + } + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Waterway.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Waterway.java new file mode 100644 index 0000000..99756d9 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Waterway.java @@ -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. + *

    + * This class is ported to Java from OpenMapTiles + * waterway sql files. + */ +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 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 postProcess(int zoom, List items) { + if (zoom >= 9 && zoom <= 11) { + return FeatureMerge.mergeLineStrings( + items, + MIN_PIXEL_LENGTHS.apply(zoom).doubleValue(), + config.tolerance(zoom), + BUFFER_SIZE + ); + } + return items; + } +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/util/LanguageUtils.java b/src/main/java/com/onthegomap/planetiler/basemap/util/LanguageUtils.java new file mode 100644 index 0000000..c2ff26d --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/util/LanguageUtils.java @@ -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. + *

    + * Ported from openmaptiles-tools. + */ +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 EN_DE_NAME_KEYS = Set.of("name:en", "name:de"); + + private static void putIfNotEmpty(Map 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 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 " ( - " + // 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. + * + *

      + *
    • name is the original name value from the element
    • + *
    • name_en is the original name:en value from the element, or name if missing
    • + *
    • name_de is the original name:de value from the element, or name/ name_en if missing
    • + *
    • name:latin is the first of name, int_name, or any name: attribute that contains only latin characters
    • + *
    • name:nonlatin is any nonlatin part of name if present
    • + *
    • name_int is the first of int_name name:en name:latin name
    • + *
    + */ + public static Map getNamesWithoutTranslations(Map 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 getNames(Map tags, Translations translations) { + Map 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 getAllNameTranslationsBesidesEnglishAndGerman(Map 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); + } + +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/util/Utils.java b/src/main/java/com/onthegomap/planetiler/basemap/util/Utils.java new file mode 100644 index 0000000..0a603f0 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/util/Utils.java @@ -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 coalesce(T a, T b) { + return a != null ? a : b; + } + + public static T coalesce(T a, T b, T c) { + return a != null ? a : b != null ? b : c; + } + + public static T coalesce(T a, T b, T c, T d) { + return a != null ? a : b != null ? b : c != null ? c : d; + } + + public static 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 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 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 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 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; + } + +} diff --git a/src/main/java/com/onthegomap/planetiler/basemap/util/VerifyMonaco.java b/src/main/java/com/onthegomap/planetiler/basemap/util/VerifyMonaco.java new file mode 100644 index 0000000..5250a80 --- /dev/null +++ b/src/main/java/com/onthegomap/planetiler/basemap/util/VerifyMonaco.java @@ -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(); + } + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/BasemapProfileTest.java b/src/test/java/com/onthegomap/planetiler/basemap/BasemapProfileTest.java new file mode 100644 index 0000000..86b6589 --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/BasemapProfileTest.java @@ -0,0 +1,38 @@ +package com.onthegomap.planetiler.basemap; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.onthegomap.planetiler.config.PlanetilerConfig; +import com.onthegomap.planetiler.reader.osm.OsmElement; +import com.onthegomap.planetiler.stats.Stats; +import com.onthegomap.planetiler.util.Translations; +import com.onthegomap.planetiler.util.Wikidata; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class BasemapProfileTest { + + private final Wikidata.WikidataTranslations wikidataTranslations = new Wikidata.WikidataTranslations(); + private final Translations translations = Translations.defaultProvider(List.of("en", "es", "de")) + .addTranslationProvider(wikidataTranslations); + private final BasemapProfile profile = new BasemapProfile(translations, PlanetilerConfig.defaults(), + Stats.inMemory()); + + @Test + public void testCaresAboutWikidata() { + var node = new OsmElement.Node(1, 1, 1); + node.setTag("aeroway", "gate"); + assertTrue(profile.caresAboutWikidataTranslation(node)); + + node.setTag("aeroway", "other"); + assertFalse(profile.caresAboutWikidataTranslation(node)); + } + + @Test + public void testDoesntCareAboutWikidataForRoads() { + var way = new OsmElement.Way(1); + way.setTag("highway", "footway"); + assertFalse(profile.caresAboutWikidataTranslation(way)); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/BasemapTest.java b/src/test/java/com/onthegomap/planetiler/basemap/BasemapTest.java new file mode 100644 index 0000000..7b690e2 --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/BasemapTest.java @@ -0,0 +1,225 @@ +package com.onthegomap.planetiler.basemap; + +import static com.onthegomap.planetiler.TestUtils.assertContains; +import static com.onthegomap.planetiler.TestUtils.assertFeatureNear; +import static com.onthegomap.planetiler.basemap.util.VerifyMonaco.MONACO_BOUNDS; +import static com.onthegomap.planetiler.util.Gzip.gunzip; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +import com.onthegomap.planetiler.TestUtils; +import com.onthegomap.planetiler.VectorTile; +import com.onthegomap.planetiler.basemap.util.VerifyMonaco; +import com.onthegomap.planetiler.config.Arguments; +import com.onthegomap.planetiler.mbtiles.Mbtiles; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.io.TempDir; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; + +/** + * End-to-end tests for basemap generation. + *

    + * Generates an entire map for the smallest openstreetmap extract available (Monaco) and asserts that expected output + * features exist + */ +public class BasemapTest { + + @TempDir + static Path tmpDir; + private static Mbtiles mbtiles; + + @BeforeAll + public static void runPlanetiler() throws Exception { + Path dbPath = tmpDir.resolve("output.mbtiles"); + BasemapMain.run(Arguments.of( + // Override input source locations + "osm_path", TestUtils.pathToResource("monaco-latest.osm.pbf"), + "natural_earth_path", TestUtils.pathToResource("natural_earth_vector.sqlite.zip"), + "water_polygons_path", TestUtils.pathToResource("water-polygons-split-3857.zip"), + // no centerlines in monaco - so fake it out with an empty source + "lake_centerlines_path", TestUtils.pathToResource("water-polygons-split-3857.zip"), + + // Override temp dir location + "tmp", tmpDir.toString(), + + // Override output location + "mbtiles", dbPath.toString() + )); + mbtiles = Mbtiles.newReadOnlyDatabase(dbPath); + } + + @AfterAll + public static void close() throws IOException { + mbtiles.close(); + } + + @Test + public void testMetadata() { + Map metadata = mbtiles.metadata().getAll(); + assertEquals("OpenMapTiles", metadata.get("name")); + assertEquals("0", metadata.get("minzoom")); + assertEquals("14", metadata.get("maxzoom")); + assertEquals("baselayer", metadata.get("type")); + assertEquals("pbf", metadata.get("format")); + assertEquals("7.40921,43.72335,7.44864,43.75169", metadata.get("bounds")); + assertEquals("7.42892,43.73752,14", metadata.get("center")); + assertContains("openmaptiles.org", metadata.get("description")); + assertContains("openmaptiles.org", metadata.get("attribution")); + assertContains("www.openstreetmap.org/copyright", metadata.get("attribution")); + } + + @Test + public void ensureValidGeometries() throws Exception { + Set parsedTiles = TestUtils.getAllTiles(mbtiles); + for (var tileEntry : parsedTiles) { + var decoded = VectorTile.decode(gunzip(tileEntry.bytes())); + for (VectorTile.Feature feature : decoded) { + TestUtils.validateGeometry(feature.geometry().decode()); + } + } + } + + @Test + public void testContainsOceanPolyons() { + assertFeatureNear(mbtiles, "water", Map.of( + "class", "ocean" + ), 7.4484, 43.70783, 0, 14); + } + + @Test + public void testContainsCountryName() { + assertFeatureNear(mbtiles, "place", Map.of( + "class", "country", + "iso_a2", "MC", + "name", "Monaco" + ), 7.42769, 43.73235, 2, 14); + } + + @Test + public void testContainsSuburb() { + assertFeatureNear(mbtiles, "place", Map.of( + "name", "Les Moneghetti", + "class", "suburb" + ), 7.41746, 43.73638, 11, 14); + } + + @Test + public void testContainsBuildings() { + assertFeatureNear(mbtiles, "building", Map.of(), 7.41919, 43.73401, 13, 14); + assertNumFeatures("building", Map.of(), 14, 1316, Polygon.class); + assertNumFeatures("building", Map.of(), 13, 196, Polygon.class); + } + + @Test + public void testContainsHousenumber() { + assertFeatureNear(mbtiles, "housenumber", Map.of( + "housenumber", "27" + ), 7.42117, 43.73652, 14, 14); + assertNumFeatures("housenumber", Map.of(), 14, 274, Point.class); + } + + @Test + public void testBoundary() { + assertFeatureNear(mbtiles, "boundary", Map.of( + "admin_level", 2L, + "maritime", 1L, + "disputed", 0L + ), 7.41884, 43.72396, 4, 14); + } + + @Test + public void testAeroway() { + assertNumFeatures("aeroway", Map.of( + "class", "heliport" + ), 14, 1, Polygon.class); + assertNumFeatures("aeroway", Map.of( + "class", "helipad" + ), 14, 11, Polygon.class); + } + + @Test + public void testLandcover() { + assertNumFeatures("landcover", Map.of( + "class", "grass", + "subclass", "park" + ), 14, 20, Polygon.class); + assertNumFeatures("landcover", Map.of( + "class", "grass", + "subclass", "garden" + ), 14, 33, Polygon.class); + } + + @Test + public void testPoi() { + assertNumFeatures("poi", Map.of( + "class", "restaurant", + "subclass", "restaurant" + ), 14, 217, Point.class); + assertNumFeatures("poi", Map.of( + "class", "art_gallery", + "subclass", "artwork" + ), 14, 132, Point.class); + } + + @Test + public void testLanduse() { + assertNumFeatures("landuse", Map.of( + "class", "residential" + ), 14, 8, Polygon.class); + assertNumFeatures("landuse", Map.of( + "class", "hospital" + ), 14, 4, Polygon.class); + } + + @Test + public void testTransportation() { + assertNumFeatures("transportation", Map.of( + "class", "path", + "subclass", "footway" + ), 14, 909, LineString.class); + assertNumFeatures("transportation", Map.of( + "class", "primary" + ), 14, 170, LineString.class); + } + + @Test + public void testTransportationName() { + assertNumFeatures("transportation_name", Map.of( + "name", "Boulevard du Larvotto", + "class", "primary" + ), 14, 12, LineString.class); + } + + @Test + public void testWaterway() { + assertNumFeatures("waterway", Map.of( + "class", "stream" + ), 14, 6, LineString.class); + } + + @TestFactory + public Stream testVerifyChecks() { + return VerifyMonaco.verify(mbtiles).results().stream() + .map(check -> dynamicTest(check.name(), () -> { + check.error().ifPresent(Assertions::fail); + })); + } + + private static void assertNumFeatures(String layer, Map attrs, int zoom, + int expected, Class clazz) { + TestUtils.assertNumFeatures(mbtiles, layer, zoom, attrs, MONACO_BOUNDS, expected, clazz); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/GenerateTest.java b/src/test/java/com/onthegomap/planetiler/basemap/GenerateTest.java new file mode 100644 index 0000000..ec92b11 --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/GenerateTest.java @@ -0,0 +1,227 @@ +package com.onthegomap.planetiler.basemap; + +import static com.onthegomap.planetiler.basemap.Generate.parseYaml; +import static com.onthegomap.planetiler.expression.Expression.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +import com.fasterxml.jackson.databind.JsonNode; +import com.onthegomap.planetiler.expression.Expression; +import com.onthegomap.planetiler.expression.MultiExpression; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; + +public class GenerateTest { + + @Test + public void testParseSimple() { + MultiExpression parsed = Generate.generateFieldMapping(parseYaml(""" + output: + key: value + key2: + - value2 + - '%value3%' + """)); + assertEquals(MultiExpression.of(List.of( + MultiExpression.entry("output", or( + matchAny("key", "value"), + matchAny("key2", "value2", "%value3%") + )) + )), parsed); + } + + @Test + public void testParseAnd() { + MultiExpression parsed = Generate.generateFieldMapping(parseYaml(""" + output: + __AND__: + key1: val1 + key2: val2 + """)); + assertEquals(MultiExpression.of(List.of( + MultiExpression.entry("output", and( + matchAny("key1", "val1"), + matchAny("key2", "val2") + )) + )), parsed); + } + + @Test + public void testParseAndWithOthers() { + MultiExpression parsed = Generate.generateFieldMapping(parseYaml(""" + output: + - key0: val0 + - __AND__: + key1: val1 + key2: val2 + """)); + assertEquals(MultiExpression.of(List.of( + MultiExpression.entry("output", or( + matchAny("key0", "val0"), + and( + matchAny("key1", "val1"), + matchAny("key2", "val2") + ) + )) + )), parsed); + } + + @Test + public void testParseAndContainingOthers() { + MultiExpression parsed = Generate.generateFieldMapping(parseYaml(""" + output: + __AND__: + - key1: val1 + - __OR__: + key2: val2 + key3: val3 + """)); + assertEquals(MultiExpression.of(List.of( + MultiExpression.entry("output", and( + matchAny("key1", "val1"), + or( + matchAny("key2", "val2"), + matchAny("key3", "val3") + ) + )) + )), parsed); + } + + @Test + public void testParseContainsKey() { + MultiExpression parsed = Generate.generateFieldMapping(parseYaml(""" + output: + key1: val1 + key2: + """)); + assertEquals(MultiExpression.of(List.of( + MultiExpression.entry("output", or( + matchAny("key1", "val1"), + matchField("key2") + )) + )), parsed); + } + + @TestFactory + public Stream testParseImposm3Mapping() { + record TestCase(String name, String mapping, String require, String reject, Expression expected) { + + TestCase(String mapping, Expression expected) { + this(mapping, mapping, null, null, expected); + } + } + return Stream.of( + new TestCase( + "key: val", matchAny("key", "val") + ), + new TestCase( + "key: [val1, val2]", matchAny("key", "val1", "val2") + ), + new TestCase( + "key: [\"__any__\"]", matchField("key") + ), + new TestCase("reject", + "key: val", + "mustkey: mustval", + null, + and( + matchAny("key", "val"), + matchAny("mustkey", "mustval") + ) + ), + new TestCase("require", + "key: val", + null, + "badkey: badval", + and( + matchAny("key", "val"), + not(matchAny("badkey", "badval")) + ) + ), + new TestCase("require and reject complex", + """ + key: val + key2: + - val1 + - val2 + """, + """ + mustkey: mustval + mustkey2: + - mustval1 + - mustval2 + """, + """ + notkey: notval + notkey2: + - notval1 + - notval2 + """, + and( + or( + matchAny("key", "val"), + matchAny("key2", "val1", "val2") + ), + matchAny("mustkey", "mustval"), + matchAny("mustkey2", "mustval1", "mustval2"), + not(matchAny("notkey", "notval")), + not(matchAny("notkey2", "notval1", "notval2")) + ) + ) + ).map(test -> dynamicTest(test.name, () -> { + Expression parsed = Generate + .parseImposm3MappingExpression("point", parseYaml(test.mapping), new Generate.Imposm3Filters( + parseYaml(test.reject), + parseYaml(test.require) + )); + assertEquals(test.expected, parsed.replace(matchType("point"), TRUE).simplify()); + })); + } + + @Test + public void testTypeMappingTopLevelType() { + Expression parsed = Generate + .parseImposm3MappingExpression("point", parseYaml(""" + key: val + """), new Generate.Imposm3Filters(null, null)); + assertEquals(and( + matchAny("key", "val"), + matchType("point") + ), parsed); + } + + @Test + public void testTypeMappings() { + Map props = new LinkedHashMap<>(); + props.put("points", parseYaml(""" + key: val + """)); + props.put("polygons", parseYaml(""" + key2: val2 + """)); + Expression parsed = Generate + .parseImposm3MappingExpression(new Generate.Imposm3Table( + "geometry", + false, + List.of(), + null, + null, + props + )); + assertEquals(or( + and( + matchAny("key", "val"), + matchType("point") + ), + and( + matchAny("key2", "val2"), + matchType("polygon") + ) + ), parsed); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/AbstractLayerTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/AbstractLayerTest.java new file mode 100644 index 0000000..084abce --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/AbstractLayerTest.java @@ -0,0 +1,214 @@ +package com.onthegomap.planetiler.basemap.layers; + +import static com.onthegomap.planetiler.TestUtils.assertSubmap; +import static com.onthegomap.planetiler.TestUtils.newLineString; +import static com.onthegomap.planetiler.TestUtils.newPoint; +import static com.onthegomap.planetiler.TestUtils.rectangle; +import static com.onthegomap.planetiler.basemap.BasemapProfile.OSM_SOURCE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import com.onthegomap.planetiler.FeatureCollector; +import com.onthegomap.planetiler.TestUtils; +import com.onthegomap.planetiler.VectorTile; +import com.onthegomap.planetiler.basemap.BasemapProfile; +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.OsmReader; +import com.onthegomap.planetiler.reader.osm.OsmRelationInfo; +import com.onthegomap.planetiler.stats.Stats; +import com.onthegomap.planetiler.util.Translations; +import com.onthegomap.planetiler.util.Wikidata; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.StreamSupport; + +public abstract class AbstractLayerTest { + + final Wikidata.WikidataTranslations wikidataTranslations = new Wikidata.WikidataTranslations(); + final Translations translations = Translations.defaultProvider(List.of("en", "es", "de")) + .addTranslationProvider(wikidataTranslations); + + final PlanetilerConfig params = PlanetilerConfig.defaults(); + final BasemapProfile profile = new BasemapProfile(translations, PlanetilerConfig.defaults(), + Stats.inMemory()); + final Stats stats = Stats.inMemory(); + final FeatureCollector.Factory featureCollectorFactory = new FeatureCollector.Factory(params, stats); + + static void assertFeatures(int zoom, List> expected, Iterable actual) { + List actualList = StreamSupport.stream(actual.spliterator(), false).toList(); + assertEquals(expected.size(), actualList.size(), () -> "size: " + actualList); + for (int i = 0; i < expected.size(); i++) { + assertSubmap(expected.get(i), TestUtils.toMap(actualList.get(i), zoom)); + } + } + + static void assertDescending(int... vals) { + for (int i = 1; i < vals.length; i++) { + if (vals[i - 1] < vals[i]) { + fail("element at " + (i - 1) + " is less than element at " + i); + } + } + } + + static void assertAscending(int... vals) { + for (int i = 1; i < vals.length; i++) { + if (vals[i - 1] > vals[i]) { + fail( + Arrays.toString(vals) + + System.lineSeparator() + "element at " + (i - 1) + " (" + vals[i - 1] + ") is greater than element at " + i + + " (" + vals[i] + ")"); + } + } + } + + VectorTile.Feature pointFeature(String layer, Map map, int group) { + return new VectorTile.Feature( + layer, + 1, + VectorTile.encodeGeometry(newPoint(0, 0)), + new HashMap<>(map), + group + ); + } + + FeatureCollector process(SourceFeature feature) { + var collector = featureCollectorFactory.get(feature); + profile.processFeature(feature, collector); + return collector; + } + + void assertCoversZoomRange(int minzoom, int maxzoom, String layer, FeatureCollector... featureCollectors) { + Map[] zooms = new Map[Math.max(15, maxzoom + 1)]; + for (var features : featureCollectors) { + for (var feature : features) { + if (feature.getLayer().equals(layer)) { + for (int zoom = feature.getMinZoom(); zoom <= feature.getMaxZoom(); zoom++) { + Map map = TestUtils.toMap(feature, zoom); + if (zooms[zoom] != null) { + fail("Multiple features at z" + zoom + ":" + System.lineSeparator() + zooms[zoom] + "\n" + map); + } + zooms[zoom] = map; + } + } + } + } + for (int zoom = 0; zoom <= 14; zoom++) { + if (zoom < minzoom || zoom > maxzoom) { + if (zooms[zoom] != null) { + fail("Expected nothing at z" + zoom + " but found: " + zooms[zoom]); + } + } else { + if (zooms[zoom] == null) { + fail("No feature at z" + zoom); + } + } + } + } + + SourceFeature pointFeature(Map props) { + return SimpleFeature.create( + newPoint(0, 0), + new HashMap<>(props), + OSM_SOURCE, + null, + 0 + ); + } + + SourceFeature lineFeature(Map props) { + return SimpleFeature.create( + newLineString(0, 0, 1, 1), + new HashMap<>(props), + OSM_SOURCE, + null, + 0 + ); + } + + SourceFeature polygonFeatureWithArea(double area, Map props) { + return SimpleFeature.create( + GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(area))), + new HashMap<>(props), + OSM_SOURCE, + null, + 0 + ); + } + + SourceFeature polygonFeature(Map props) { + return polygonFeatureWithArea(1, props); + } + + + protected SimpleFeature lineFeatureWithRelation(List relationInfos, + Map map) { + return SimpleFeature.createFakeOsmFeature( + newLineString(0, 0, 1, 1), + map, + OSM_SOURCE, + null, + 0, + (relationInfos == null ? List.of() : relationInfos).stream() + .map(r -> new OsmReader.RelationMember<>("", r)).toList() + ); + } + + protected void testMergesLinestrings(Map attrs, String layer, + double length, int zoom) throws GeometryException { + var line1 = new VectorTile.Feature( + layer, + 1, + VectorTile.encodeGeometry(newLineString(0, 0, length / 2, 0)), + attrs, + 0 + ); + var line2 = new VectorTile.Feature( + layer, + 1, + VectorTile.encodeGeometry(newLineString(length / 2, 0, length, 0)), + attrs, + 0 + ); + var connected = new VectorTile.Feature( + layer, + 1, + VectorTile.encodeGeometry(newLineString(0, 0, length, 0)), + attrs, + 0 + ); + + assertEquals( + List.of(connected), + profile.postProcessLayerFeatures(layer, zoom, List.of(line1, line2)) + ); + } + + protected void testDoesNotMergeLinestrings(Map attrs, String layer, + double length, int zoom) throws GeometryException { + var line1 = new VectorTile.Feature( + layer, + 1, + VectorTile.encodeGeometry(newLineString(0, 0, length / 2, 0)), + attrs, + 0 + ); + var line2 = new VectorTile.Feature( + layer, + 1, + VectorTile.encodeGeometry(newLineString(length / 2, 0, length, 0)), + attrs, + 0 + ); + + assertEquals( + List.of(line1, line2), + profile.postProcessLayerFeatures(layer, zoom, List.of(line1, line2)) + ); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/AerodromeLabelTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/AerodromeLabelTest.java new file mode 100644 index 0000000..e73de7b --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/AerodromeLabelTest.java @@ -0,0 +1,121 @@ +package com.onthegomap.planetiler.basemap.layers; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class AerodromeLabelTest extends AbstractLayerTest { + + @BeforeEach + public void setupWikidataTranslation() { + wikidataTranslations.put(123, "es", "es wd name"); + } + + @Test + public void testHappyPathPoint() { + assertFeatures(14, List.of(Map.of( + "class", "international", + "ele", 100, + "ele_ft", 328, + "name", "osm name", + "name:es", "es wd name", + + "_layer", "aerodrome_label", + "_type", "point", + "_minzoom", 10, + "_maxzoom", 14, + "_buffer", 64d + )), process(pointFeature(Map.of( + "aeroway", "aerodrome", + "name", "osm name", + "wikidata", "Q123", + "ele", "100", + "aerodrome", "international", + "iata", "123", + "icao", "1234" + )))); + } + + @Test + public void testInternational() { + assertFeatures(14, List.of(Map.of( + "class", "international", + "_layer", "aerodrome_label" + )), process(pointFeature(Map.of( + "aeroway", "aerodrome", + "aerodrome_type", "international" + )))); + } + + @Test + public void testPublic() { + assertFeatures(14, List.of(Map.of( + "class", "public", + "_layer", "aerodrome_label" + )), process(pointFeature(Map.of( + "aeroway", "aerodrome", + "aerodrome_type", "public airport" + )))); + assertFeatures(14, List.of(Map.of( + "class", "public", + "_layer", "aerodrome_label" + )), process(pointFeature(Map.of( + "aeroway", "aerodrome", + "aerodrome_type", "civil" + )))); + } + + @Test + public void testMilitary() { + assertFeatures(14, List.of(Map.of( + "class", "military", + "_layer", "aerodrome_label" + )), process(pointFeature(Map.of( + "aeroway", "aerodrome", + "aerodrome_type", "military airport" + )))); + assertFeatures(14, List.of(Map.of( + "class", "military", + "_layer", "aerodrome_label" + )), process(pointFeature(Map.of( + "aeroway", "aerodrome", + "military", "airfield" + )))); + } + + @Test + public void testPrivate() { + assertFeatures(14, List.of(Map.of( + "class", "private", + "_layer", "aerodrome_label" + )), process(pointFeature(Map.of( + "aeroway", "aerodrome", + "aerodrome_type", "private" + )))); + assertFeatures(14, List.of(Map.of( + "class", "private", + "_layer", "aerodrome_label" + )), process(pointFeature(Map.of( + "aeroway", "aerodrome", + "aerodrome", "private" + )))); + } + + @Test + public void testOther() { + assertFeatures(14, List.of(Map.of( + "class", "other", + "_layer", "aerodrome_label" + )), process(pointFeature(Map.of( + "aeroway", "aerodrome" + )))); + } + + @Test + public void testIgnoreNonPoints() { + assertFeatures(14, List.of(), process(lineFeature(Map.of( + "aeroway", "aerodrome" + )))); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/AerowayTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/AerowayTest.java new file mode 100644 index 0000000..90cdd75 --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/AerowayTest.java @@ -0,0 +1,92 @@ +package com.onthegomap.planetiler.basemap.layers; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class AerowayTest extends AbstractLayerTest { + + @Test + public void aerowayGate() { + assertFeatures(14, List.of(Map.of( + "class", "gate", + "ref", "123", + + "_layer", "aeroway", + "_type", "point", + "_minzoom", 14, + "_maxzoom", 14, + "_buffer", 4d + )), process(pointFeature(Map.of( + "aeroway", "gate", + "ref", "123" + )))); + assertFeatures(14, List.of(), process(lineFeature(Map.of( + "aeroway", "gate" + )))); + assertFeatures(14, List.of(), process(polygonFeature(Map.of( + "aeroway", "gate" + )))); + } + + @Test + public void aerowayLine() { + assertFeatures(14, List.of(Map.of( + "class", "runway", + "ref", "123", + + "_layer", "aeroway", + "_type", "line", + "_minzoom", 10, + "_maxzoom", 14, + "_buffer", 4d + )), process(lineFeature(Map.of( + "aeroway", "runway", + "ref", "123" + )))); + assertFeatures(14, List.of(), process(pointFeature(Map.of( + "aeroway", "runway" + )))); + } + + @Test + public void aerowayPolygon() { + assertFeatures(14, List.of(Map.of( + "class", "runway", + "ref", "123", + + "_layer", "aeroway", + "_type", "polygon", + "_minzoom", 10, + "_maxzoom", 14, + "_buffer", 4d + )), process(polygonFeature(Map.of( + "aeroway", "runway", + "ref", "123" + )))); + assertFeatures(14, List.of(Map.of( + "class", "runway", + "ref", "123", + "_layer", "aeroway", + "_type", "polygon" + )), process(polygonFeature(Map.of( + "area:aeroway", "runway", + "ref", "123" + )))); + assertFeatures(14, List.of(Map.of( + "class", "heliport", + "ref", "123", + "_layer", "aeroway", + "_type", "polygon" + )), process(polygonFeature(Map.of( + "aeroway", "heliport", + "ref", "123" + )))); + assertFeatures(14, List.of(), process(lineFeature(Map.of( + "aeroway", "heliport" + )))); + assertFeatures(14, List.of(), process(pointFeature(Map.of( + "aeroway", "heliport" + )))); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/BoundaryTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/BoundaryTest.java new file mode 100644 index 0000000..38d2d70 --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/BoundaryTest.java @@ -0,0 +1,622 @@ +package com.onthegomap.planetiler.basemap.layers; + +import static com.onthegomap.planetiler.TestUtils.newLineString; +import static com.onthegomap.planetiler.TestUtils.rectangle; +import static com.onthegomap.planetiler.basemap.BasemapProfile.NATURAL_EARTH_SOURCE; +import static com.onthegomap.planetiler.basemap.BasemapProfile.OSM_SOURCE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.onthegomap.planetiler.FeatureCollector; +import com.onthegomap.planetiler.geo.GeoUtils; +import com.onthegomap.planetiler.geo.GeometryException; +import com.onthegomap.planetiler.reader.SimpleFeature; +import com.onthegomap.planetiler.reader.osm.OsmElement; +import com.onthegomap.planetiler.reader.osm.OsmReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; + +public class BoundaryTest extends AbstractLayerTest { + + @Test + public void testNaturalEarthCountryBoundaries() { + assertCoversZoomRange( + 0, 4, "boundary", + process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_110m_admin_0_boundary_lines_land", + 0 + )), + process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_50m_admin_0_boundary_lines_land", + 1 + )), + process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_boundary_lines_land", + 2 + )) + ); + + assertFeatures(0, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "disputed", 0, + "maritime", 0, + "admin_level", 2, + + "_minzoom", 0, + "_buffer", 4d + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "International boundary (verify)" + ), + NATURAL_EARTH_SOURCE, + "ne_110m_admin_0_boundary_lines_land", + 0 + ))); + + assertFeatures(0, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "disputed", 1, + "maritime", 0, + "admin_level", 2, + "_buffer", 4d + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "Disputed (please verify)" + ), + NATURAL_EARTH_SOURCE, + "ne_110m_admin_0_boundary_lines_land", + 0 + ))); + + assertFeatures(0, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "admin_level", 2 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "International boundary (verify)" + ), + NATURAL_EARTH_SOURCE, + "ne_50m_admin_0_boundary_lines_land", + 0 + ))); + + assertFeatures(0, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "admin_level", 2 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "International boundary (verify)" + ), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_boundary_lines_land", + 0 + ))); + + assertFeatures(0, List.of(), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "Lease Limit" + ), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_boundary_lines_land", + 0 + ))); + } + + @Test + public void testNaturalEarthStateBoundaries() { + assertFeatures(0, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "disputed", 0, + "maritime", 0, + "admin_level", 4, + + "_minzoom", 1, + "_maxzoom", 4, + "_buffer", 4d + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "min_zoom", 7d + ), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_1_states_provinces_lines", + 0 + ))); + + assertFeatures(0, List.of(), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "min_zoom", 7.1d + ), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_1_states_provinces_lines", + 0 + ))); + + assertFeatures(0, List.of(), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_1_states_provinces_lines", + 0 + ))); + } + + @Test + public void testMergesDisconnectedLineFeatures() throws GeometryException { + testMergesLinestrings(Map.of("admin_level", 2), Boundary.LAYER_NAME, 10, 13); + testMergesLinestrings(Map.of("admin_level", 2), Boundary.LAYER_NAME, 10, 14); + } + + @Test + public void testOsmTownBoundary() { + var relation = new OsmElement.Relation(1); + relation.setTag("type", "boundary"); + relation.setTag("admin_level", "10"); + relation.setTag("boundary", "administrative"); + + assertFeatures(14, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "disputed", 0, + "maritime", 0, + "admin_level", 10, + + "_minzoom", 12, + "_maxzoom", 14, + "_buffer", 4d, + "_minpixelsize", 0d + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation), + Map.of()))); + } + + @Test + public void testOsmBoundaryLevelTwoAndAHalf() { + var relation = new OsmElement.Relation(1); + relation.setTag("type", "boundary"); + relation.setTag("admin_level", "2.5"); + relation.setTag("boundary", "administrative"); + + assertFeatures(14, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "disputed", 0, + "maritime", 0, + "admin_level", 3, + + "_minzoom", 5, + "_maxzoom", 14, + "_buffer", 4d, + "_minpixelsize", 0d + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation), + Map.of()))); + } + + @Test + public void testOsmBoundaryTakesMinAdminLevel() { + var relation1 = new OsmElement.Relation(1); + relation1.setTag("type", "boundary"); + relation1.setTag("admin_level", "10"); + relation1.setTag("name", "Town"); + relation1.setTag("boundary", "administrative"); + var relation2 = new OsmElement.Relation(2); + relation2.setTag("type", "boundary"); + relation2.setTag("admin_level", "4"); + relation2.setTag("name", "State"); + relation2.setTag("boundary", "administrative"); + + assertFeatures(14, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "disputed", 0, + "maritime", 0, + "admin_level", 4 + )), process(lineFeatureWithRelation( + Stream.concat( + profile.preprocessOsmRelation(relation2).stream(), + profile.preprocessOsmRelation(relation1).stream() + ).toList(), + Map.of()))); + } + + @Test + public void testOsmBoundarySetsMaritimeFromWay() { + var relation1 = new OsmElement.Relation(1); + relation1.setTag("type", "boundary"); + relation1.setTag("admin_level", "10"); + relation1.setTag("boundary", "administrative"); + + assertFeatures(14, List.of(Map.of( + "maritime", 1 + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation1), + Map.of( + "maritime", "yes" + )) + )); + assertFeatures(14, List.of(Map.of( + "maritime", 1 + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation1), + Map.of( + "natural", "coastline" + )) + )); + assertFeatures(14, List.of(Map.of( + "maritime", 1 + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation1), + Map.of( + "boundary_type", "maritime" + )) + )); + } + + @Test + public void testIgnoresProtectedAreas() { + var relation1 = new OsmElement.Relation(1); + relation1.setTag("type", "boundary"); + relation1.setTag("admin_level", "10"); + relation1.setTag("boundary", "protected_area"); + + assertNull(profile.preprocessOsmRelation(relation1)); + } + + @Test + public void testIgnoresProtectedAdminLevelOver10() { + var relation1 = new OsmElement.Relation(1); + relation1.setTag("type", "boundary"); + relation1.setTag("admin_level", "11"); + relation1.setTag("boundary", "administrative"); + + assertNull(profile.preprocessOsmRelation(relation1)); + } + + @Test + public void testOsmBoundaryDisputed() { + var relation = new OsmElement.Relation(1); + relation.setTag("type", "boundary"); + relation.setTag("admin_level", "5"); + relation.setTag("boundary", "administrative"); + relation.setTag("disputed", "yes"); + relation.setTag("name", "Border A - B"); + relation.setTag("claimed_by", "A"); + assertFeatures(14, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "disputed_name", "BorderA-B", + "claimed_by", "A", + + "disputed", 1, + "maritime", 0, + "admin_level", 5 + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation), + Map.of()) + )); + } + + @Test + public void testOsmBoundaryDisputedFromWay() { + var relation = new OsmElement.Relation(1); + relation.setTag("type", "boundary"); + relation.setTag("admin_level", "5"); + relation.setTag("boundary", "administrative"); + + assertFeatures(14, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + + "disputed", 1, + "maritime", 0, + "admin_level", 5 + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation), + Map.of( + "disputed", "yes" + )) + )); + + assertFeatures(14, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + + "disputed", 1, + "maritime", 0, + "admin_level", 5, + "claimed_by", "A", + "disputed_name", "AB" + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation), + Map.of( + "disputed", "yes", + "claimed_by", "A", + "name", "AB" + )) + )); + } + + @Test + public void testCountryBoundaryEmittedIfNoName() { + var relation = new OsmElement.Relation(1); + relation.setTag("type", "boundary"); + relation.setTag("admin_level", "2"); + relation.setTag("boundary", "administrative"); + + assertFeatures(14, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + + "disputed", 0, + "maritime", 0, + "admin_level", 2 + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation), + Map.of()) + )); + } + + @Test + public void testCountryLeftRightName() { + var country1 = new OsmElement.Relation(1); + country1.setTag("type", "boundary"); + country1.setTag("admin_level", "2"); + country1.setTag("boundary", "administrative"); + country1.setTag("ISO3166-1:alpha3", "C1"); + var country2 = new OsmElement.Relation(2); + country2.setTag("type", "boundary"); + country2.setTag("admin_level", "2"); + country2.setTag("boundary", "administrative"); + country2.setTag("ISO3166-1:alpha3", "C2"); + + // shared edge + assertFeatures(14, List.of(), process(SimpleFeature.createFakeOsmFeature( + newLineString(0, 0, 0, 10), + Map.of(), + OSM_SOURCE, + null, + 3, + Stream.concat( + profile.preprocessOsmRelation(country1).stream(), + profile.preprocessOsmRelation(country2).stream() + ).map(r -> new OsmReader.RelationMember<>("", r)).toList() + ) + )); + + // other 2 edges of country 1 + assertFeatures(14, List.of(), process(SimpleFeature.createFakeOsmFeature( + newLineString(0, 0, 5, 10), + Map.of(), + OSM_SOURCE, + null, + 4, + profile.preprocessOsmRelation(country1).stream().map(r -> new OsmReader.RelationMember<>("", r)) + .toList() + ) + )); + assertFeatures(14, List.of(), process(SimpleFeature.createFakeOsmFeature( + newLineString(0, 10, 5, 10), + Map.of(), + OSM_SOURCE, + null, + 4, + profile.preprocessOsmRelation(country1).stream().map(r -> new OsmReader.RelationMember<>("", r)) + .toList() + ) + )); + + // other 2 edges of country 2 + assertFeatures(14, List.of(), process(SimpleFeature.createFakeOsmFeature( + newLineString(0, 0, -5, 10), + Map.of(), + OSM_SOURCE, + null, + 4, + profile.preprocessOsmRelation(country2).stream().map(r -> new OsmReader.RelationMember<>("", r)) + .toList() + ) + )); + assertFeatures(14, List.of(), process(SimpleFeature.createFakeOsmFeature( + newLineString(0, 10, -5, 10), + Map.of(), + OSM_SOURCE, + null, + 4, + profile.preprocessOsmRelation(country2).stream().map(r -> new OsmReader.RelationMember<>("", r)) + .toList() + ) + )); + + List features = new ArrayList<>(); + profile.finish(OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add); + assertEquals(3, features.size()); + + // ensure shared edge has country labels on right sides + var sharedEdge = features.stream() + .filter(c -> c.getAttrsAtZoom(0).containsKey("adm0_l") && c.getAttrsAtZoom(0).containsKey("adm0_r")).findFirst() + .get(); + if (sharedEdge.getGeometry().getCoordinate().y == 0.5) { // going up + assertEquals("C1", sharedEdge.getAttrsAtZoom(0).get("adm0_r")); + assertEquals("C2", sharedEdge.getAttrsAtZoom(0).get("adm0_l")); + } else { // going down + assertEquals("C2", sharedEdge.getAttrsAtZoom(0).get("adm0_r")); + assertEquals("C1", sharedEdge.getAttrsAtZoom(0).get("adm0_l")); + } + var c1 = features.stream() + .filter(c -> c.getGeometry().getEnvelopeInternal().getMaxX() > 0.5).findFirst() + .get(); + if (c1.getGeometry().getCoordinate().y == 0.5) { // going up + assertEquals("C1", c1.getAttrsAtZoom(0).get("adm0_l")); + } else { // going down + assertEquals("C1", c1.getAttrsAtZoom(0).get("adm0_r")); + } + var c2 = features.stream() + .filter(c -> c.getGeometry().getEnvelopeInternal().getMinX() < 0.5).findFirst() + .get(); + if (c2.getGeometry().getCoordinate().y == 0.5) { // going up + assertEquals("C2", c2.getAttrsAtZoom(0).get("adm0_r")); + } else { // going down + assertEquals("C2", c2.getAttrsAtZoom(0).get("adm0_l")); + } + } + + @Test + public void testCountryBoundaryNotClosed() { + var country1 = new OsmElement.Relation(1); + country1.setTag("type", "boundary"); + country1.setTag("admin_level", "2"); + country1.setTag("boundary", "administrative"); + country1.setTag("ISO3166-1:alpha3", "C1"); + + // shared edge + assertFeatures(14, List.of(), process(SimpleFeature.createFakeOsmFeature( + newLineString(0, 0, 0, 10, 5, 5), + Map.of(), + OSM_SOURCE, + null, + 3, + profile.preprocessOsmRelation(country1).stream().map(r -> new OsmReader.RelationMember<>("", r)) + .toList() + ))); + + List features = new ArrayList<>(); + profile.finish(OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add); + assertFeatures(0, List.of(Map.of( + "adm0_r", "", + "adm0_l", "", + "maritime", 0, + "disputed", 0, + "admin_level", 2, + + "_layer", "boundary" + )), features); + } + + @Test + public void testNestedCountry() throws GeometryException { + var country1 = new OsmElement.Relation(1); + country1.setTag("type", "boundary"); + country1.setTag("admin_level", "2"); + country1.setTag("boundary", "administrative"); + country1.setTag("ISO3166-1:alpha3", "C1"); + + assertFeatures(14, List.of(), process(SimpleFeature.createFakeOsmFeature( + GeoUtils.polygonToLineString(rectangle(0, 10)), + Map.of(), + OSM_SOURCE, + null, + 3, + profile.preprocessOsmRelation(country1).stream().map(r -> new OsmReader.RelationMember<>("", r)) + .toList() + ))); + assertFeatures(14, List.of(), process(SimpleFeature.createFakeOsmFeature( + GeoUtils.polygonToLineString(rectangle(1, 9)), + Map.of(), + OSM_SOURCE, + null, + 3, + profile.preprocessOsmRelation(country1).stream().map(r -> new OsmReader.RelationMember<>("", r)) + .toList() + ))); + + List features = new ArrayList<>(); + profile.finish(OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add); + assertFeatures(0, List.of(Map.of( + "adm0_l", "C1", + "adm0_r", "" + ), Map.of( + "adm0_r", "C1", + "adm0_l", "" + )), features); + } + + @Test + public void testDontLabelBadPolygon() { + var country1 = new OsmElement.Relation(1); + country1.setTag("type", "boundary"); + country1.setTag("admin_level", "2"); + country1.setTag("boundary", "administrative"); + country1.setTag("ISO3166-1:alpha3", "C1"); + + assertFeatures(14, List.of(), process(SimpleFeature.createFakeOsmFeature( + GeoUtils.worldToLatLonCoords(newLineString(0, 0, 0.1, 0, 0.1, 0.1, 0.02, 0.1, 0.02, -0.02)), + Map.of(), + OSM_SOURCE, + null, + 3, + profile.preprocessOsmRelation(country1).stream().map(r -> new OsmReader.RelationMember<>("", r)) + .toList() + ))); + + List features = new ArrayList<>(); + profile.finish(OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add); + assertFeatures(0, List.of(Map.of( + "adm0_l", "", + "adm0_r", "" + )), features); + } + + @Test + public void testIgnoreBadPolygonAndLabelGoodPart() throws GeometryException { + var country1 = new OsmElement.Relation(1); + country1.setTag("type", "boundary"); + country1.setTag("admin_level", "2"); + country1.setTag("boundary", "administrative"); + country1.setTag("ISO3166-1:alpha3", "C1"); + + assertFeatures(14, List.of(), process(SimpleFeature.createFakeOsmFeature( + GeoUtils.worldToLatLonCoords(newLineString(0, 0, 0.1, 0, 0.1, 0.1, 0.2, 0.1, 0.2, -0.2)), + Map.of(), + OSM_SOURCE, + null, + 3, + profile.preprocessOsmRelation(country1).stream().map(r -> new OsmReader.RelationMember<>("", r)) + .toList() + ))); + + assertFeatures(14, List.of(), process(SimpleFeature.createFakeOsmFeature( + GeoUtils.worldToLatLonCoords(GeoUtils.polygonToLineString(rectangle(0.2, 0.3))), + Map.of(), + OSM_SOURCE, + null, + 3, + profile.preprocessOsmRelation(country1).stream().map(r -> new OsmReader.RelationMember<>("", r)) + .toList() + ))); + + List features = new ArrayList<>(); + profile.finish(OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add); + assertFeatures(0, List.of(Map.of( + "adm0_l", "", + "adm0_r", "" + ), Map.of( + "adm0_l", "", + "adm0_r", "C1" + )), features); + } + +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/BuildingTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/BuildingTest.java new file mode 100644 index 0000000..40ec4dc --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/BuildingTest.java @@ -0,0 +1,171 @@ +package com.onthegomap.planetiler.basemap.layers; + +import static com.onthegomap.planetiler.TestUtils.rectangle; +import static com.onthegomap.planetiler.basemap.BasemapProfile.OSM_SOURCE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.onthegomap.planetiler.VectorTile; +import com.onthegomap.planetiler.geo.GeoUtils; +import com.onthegomap.planetiler.geo.GeometryException; +import com.onthegomap.planetiler.reader.SimpleFeature; +import com.onthegomap.planetiler.reader.osm.OsmElement; +import com.onthegomap.planetiler.reader.osm.OsmReader; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class BuildingTest extends AbstractLayerTest { + + @Test + public void testBuilding() { + assertFeatures(13, List.of(Map.of( + "colour", "", + "hide_3d", "", + "_layer", "building", + "_type", "polygon", + "_minzoom", 13, + "_maxzoom", 14, + "_buffer", 4d, + "_minpixelsize", 0.1d + )), process(polygonFeature(Map.of( + "building", "yes" + )))); + assertFeatures(13, List.of(Map.of( + "_layer", "building", + "_type", "polygon" + )), process(polygonFeature(Map.of( + "building:part", "yes" + )))); + assertFeatures(13, List.of(), process(polygonFeature(Map.of( + "building", "no" + )))); + } + + @Test + public void testAirportBuildings() { + assertFeatures(13, List.of(Map.of( + "_layer", "building", + "_type", "polygon" + )), process(polygonFeature(Map.of( + "aeroway", "terminal" + )))); + assertFeatures(13, List.of(Map.of( + "_layer", "building", + "_type", "polygon" + )), process(polygonFeature(Map.of( + "aeroway", "hangar" + )))); + } + + @Test + public void testRenderHeights() { + assertFeatures(13, List.of(Map.of( + "render_height", "", + "render_min_height", "" + )), process(polygonFeature(Map.of( + "building", "yes" + )))); + assertFeatures(14, List.of(Map.of( + "render_height", 5, + "render_min_height", 0 + )), process(polygonFeature(Map.of( + "building", "yes" + )))); + assertFeatures(14, List.of(Map.of( + "render_height", 12, + "render_min_height", 3 + )), process(polygonFeature(Map.of( + "building", "yes", + "building:min_height", "3", + "building:height", "12" + )))); + assertFeatures(14, List.of(Map.of( + "render_height", 44, + "render_min_height", 10 + )), process(polygonFeature(Map.of( + "building", "yes", + "building:min_level", "3", + "building:levels", "12" + )))); + assertFeatures(14, List.of(), process(polygonFeature(Map.of( + "building", "yes", + "building:min_level", "1500", + "building:levels", "1500" + )))); + } + + @Test + public void testOutlineHides3d() { + var relation = new OsmElement.Relation(1); + relation.setTag("type", "building"); + + var relationInfos = profile.preprocessOsmRelation(relation).stream() + .map(i -> new OsmReader.RelationMember<>("outline", i)).toList(); + + assertFeatures(14, List.of(Map.of( + "_layer", "building", + "hide_3d", true + )), process(SimpleFeature.createFakeOsmFeature( + GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), + Map.of( + "building", "yes" + ), + OSM_SOURCE, + null, + 0, + relationInfos + ))); + } + + @Test + public void testMergePolygonsZ13() throws GeometryException { + var poly1 = new VectorTile.Feature( + Building.LAYER_NAME, + 1, + VectorTile.encodeGeometry(rectangle(10, 20)), + Map.of(), + 0 + ); + var poly2 = new VectorTile.Feature( + Building.LAYER_NAME, + 1, + VectorTile.encodeGeometry(rectangle(20, 10, 22, 20)), + Map.of(), + 0 + ); + + assertEquals( + 2, + profile.postProcessLayerFeatures(Building.LAYER_NAME, 14, List.of(poly1, poly2)).size() + ); + assertEquals( + 1, + profile.postProcessLayerFeatures(Building.LAYER_NAME, 13, List.of(poly1, poly2)).size() + ); + } + + @Test + public void testColor() { + assertFeatures(14, List.of(Map.of( + "colour", "#ff0000" + )), process(polygonFeature(Map.of( + "building", "yes", + "building:colour", "#ff0000", + "building:material", "brick" + )))); + assertFeatures(14, List.of(Map.of( + "colour", "#bd8161" + )), process(polygonFeature(Map.of( + "building", "yes", + "building:building", "yes", + "building:material", "brick" + )))); + assertFeatures(13, List.of(Map.of( + "colour", "" + )), process(polygonFeature(Map.of( + "building", "yes", + "building:building", "yes", + "building:colour", "#ff0000" + )))); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/HousenumberTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/HousenumberTest.java new file mode 100644 index 0000000..39a2d73 --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/HousenumberTest.java @@ -0,0 +1,30 @@ +package com.onthegomap.planetiler.basemap.layers; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class HousenumberTest extends AbstractLayerTest { + + @Test + public void testHousenumber() { + assertFeatures(14, List.of(Map.of( + "_layer", "housenumber", + "_type", "point", + "_minzoom", 14, + "_maxzoom", 14, + "_buffer", 8d + )), process(pointFeature(Map.of( + "addr:housenumber", "10" + )))); + assertFeatures(14, List.of(Map.of( + "_layer", "housenumber", + "_type", "point", + "_minzoom", 14, + "_maxzoom", 14, + "_buffer", 8d + )), process(polygonFeature(Map.of( + "addr:housenumber", "10" + )))); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/LandcoverTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/LandcoverTest.java new file mode 100644 index 0000000..4c937e2 --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/LandcoverTest.java @@ -0,0 +1,201 @@ +package com.onthegomap.planetiler.basemap.layers; + +import static com.onthegomap.planetiler.TestUtils.rectangle; +import static com.onthegomap.planetiler.basemap.BasemapProfile.NATURAL_EARTH_SOURCE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.onthegomap.planetiler.VectorTile; +import com.onthegomap.planetiler.geo.GeoUtils; +import com.onthegomap.planetiler.geo.GeometryException; +import com.onthegomap.planetiler.reader.SimpleFeature; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class LandcoverTest extends AbstractLayerTest { + + @Test + public void testNaturalEarthGlaciers() { + var glacier1 = process(SimpleFeature.create( + GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_110m_glaciated_areas", + 0 + )); + var glacier2 = process(SimpleFeature.create( + GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_50m_glaciated_areas", + 0 + )); + var glacier3 = process(SimpleFeature.create( + GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_10m_glaciated_areas", + 0 + )); + assertFeatures(0, List.of(Map.of( + "_layer", "landcover", + "subclass", "glacier", + "class", "ice", + "_buffer", 4d + )), glacier1); + assertFeatures(0, List.of(Map.of( + "_layer", "landcover", + "subclass", "glacier", + "class", "ice", + "_buffer", 4d + )), glacier2); + assertFeatures(0, List.of(Map.of( + "_layer", "landcover", + "subclass", "glacier", + "class", "ice", + "_buffer", 4d + )), glacier3); + assertCoversZoomRange(0, 6, "landcover", + glacier1, + glacier2, + glacier3 + ); + } + + @Test + public void testNaturalEarthAntarcticIceShelves() { + var ice1 = process(SimpleFeature.create( + GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_50m_antarctic_ice_shelves_polys", + 0 + )); + var ice2 = process(SimpleFeature.create( + GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_10m_antarctic_ice_shelves_polys", + 0 + )); + assertFeatures(0, List.of(Map.of( + "_layer", "landcover", + "subclass", "ice_shelf", + "class", "ice", + "_buffer", 4d + )), ice1); + assertFeatures(0, List.of(Map.of( + "_layer", "landcover", + "subclass", "ice_shelf", + "class", "ice", + "_buffer", 4d + )), ice2); + assertCoversZoomRange(2, 6, "landcover", + ice1, + ice2 + ); + } + + @Test + public void testOsmLandcover() { + assertFeatures(13, List.of(Map.of( + "_layer", "landcover", + "subclass", "wood", + "class", "wood", + "_minpixelsize", 8d, + "_numpointsattr", "_numpoints", + "_minzoom", 9, + "_maxzoom", 14 + )), process(polygonFeature(Map.of( + "natural", "wood" + )))); + assertFeatures(12, List.of(Map.of( + "_layer", "landcover", + "subclass", "forest", + "class", "wood", + "_minpixelsize", 8d, + "_minzoom", 9, + "_maxzoom", 14 + )), process(polygonFeature(Map.of( + "landuse", "forest" + )))); + assertFeatures(10, List.of(Map.of( + "_layer", "landcover", + "subclass", "dune", + "class", "sand", + "_minpixelsize", 4d, + "_minzoom", 7, + "_maxzoom", 14 + )), process(polygonFeature(Map.of( + "natural", "dune" + )))); + } + + @Test + public void testMergeForestsBuNumPointsZ9to13() throws GeometryException { + Map map = Map.of("subclass", "wood"); + + assertMerges(List.of(map, map, map, map, map, map), List.of( + feature(rectangle(10, 20), Map.of("_numpoints", 48, "subclass", "wood")), + feature(rectangle(10, 20), Map.of("_numpoints", 49, "subclass", "wood")), + feature(rectangle(12, 18), Map.of("_numpoints", 50, "subclass", "wood")), + feature(rectangle(12, 18), Map.of("_numpoints", 299, "subclass", "wood")), + feature(rectangle(12, 18), Map.of("_numpoints", 300, "subclass", "wood")), + feature(rectangle(12, 18), Map.of("_numpoints", 301, "subclass", "wood")) + ), 14); + assertMerges(List.of(map, map, map, map), List.of( + feature(rectangle(10, 20), Map.of("_numpoints", 48, "subclass", "wood")), + feature(rectangle(10, 20), Map.of("_numpoints", 49, "subclass", "wood")), + feature(rectangle(12, 18), Map.of("_numpoints", 50, "subclass", "wood")), + feature(rectangle(12, 18), Map.of("_numpoints", 299, "subclass", "wood")), + feature(rectangle(12, 18), Map.of("_numpoints", 300, "subclass", "wood")), + feature(rectangle(12, 18), Map.of("_numpoints", 301, "subclass", "wood")) + ), 13); + assertMerges(List.of(map, map, map), List.of( + feature(rectangle(10, 20), Map.of("_numpoints", 48, "subclass", "wood")), + feature(rectangle(10, 20), Map.of("_numpoints", 49, "subclass", "wood")), + feature(rectangle(12, 18), Map.of("_numpoints", 50, "subclass", "wood")), + feature(rectangle(12, 18), Map.of("_numpoints", 299, "subclass", "wood")), + feature(rectangle(12, 18), Map.of("_numpoints", 300, "subclass", "wood")), + feature(rectangle(12, 18), Map.of("_numpoints", 301, "subclass", "wood")) + ), 9); + } + + @Test + public void testMergeNonForestsBelowZ9() throws GeometryException { + Map map = Map.of("subclass", "dune"); + + assertMerges(List.of(map, map), List.of( + feature(rectangle(10, 20), Map.of("_numpoints", 48, "subclass", "dune")), + feature(rectangle(12, 18), Map.of("_numpoints", 301, "subclass", "dune")) + ), 9); + assertMerges(List.of(map), List.of( + feature(rectangle(10, 20), Map.of("_numpoints", 48, "subclass", "dune")), + feature(rectangle(12, 18), Map.of("_numpoints", 301, "subclass", "dune")) + ), 8); + assertMerges(List.of(map, map), List.of( + feature(rectangle(10, 20), Map.of("_numpoints", 48, "subclass", "dune")), + feature(rectangle(12, 18), Map.of("_numpoints", 301, "subclass", "dune")) + ), 6); + } + + + private VectorTile.Feature feature(org.locationtech.jts.geom.Polygon geom, Map m) { + return new VectorTile.Feature( + "landcover", + 1, + VectorTile.encodeGeometry(geom), + new HashMap<>(m), + 0 + ); + } + + private void assertMerges(List> expected, List in, int zoom) + throws GeometryException { + assertEquals(expected, + profile.postProcessLayerFeatures("landcover", zoom, in).stream().map( + VectorTile.Feature::attrs) + .toList()); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/LanduseTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/LanduseTest.java new file mode 100644 index 0000000..8b87cdb --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/LanduseTest.java @@ -0,0 +1,82 @@ +package com.onthegomap.planetiler.basemap.layers; + +import static com.onthegomap.planetiler.TestUtils.rectangle; +import static com.onthegomap.planetiler.basemap.BasemapProfile.NATURAL_EARTH_SOURCE; + +import com.onthegomap.planetiler.geo.GeoUtils; +import com.onthegomap.planetiler.reader.SimpleFeature; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class LanduseTest extends AbstractLayerTest { + + @Test + public void testNaturalEarthUrbanAreas() { + assertFeatures(0, List.of(Map.of( + "_layer", "landuse", + "class", "residential", + "_buffer", 4d + )), process(SimpleFeature.create( + GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), + Map.of("scalerank", 1.9), + NATURAL_EARTH_SOURCE, + "ne_50m_urban_areas", + 0 + ))); + assertFeatures(0, List.of(), process(SimpleFeature.create( + GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), + Map.of("scalerank", 2.1), + NATURAL_EARTH_SOURCE, + "ne_50m_urban_areas", + 0 + ))); + } + + @Test + public void testOsmLanduse() { + assertFeatures(13, List.of( + Map.of("_layer", "poi"), + Map.of( + "_layer", "landuse", + "class", "railway", + "_minpixelsize", 4d, + "_minzoom", 9, + "_maxzoom", 14 + )), process(polygonFeature(Map.of( + "landuse", "railway", + "amenity", "school" + )))); + assertFeatures(13, List.of(Map.of("_layer", "poi"), Map.of( + "_layer", "landuse", + "class", "school", + "_minpixelsize", 4d, + "_minzoom", 9, + "_maxzoom", 14 + )), process(polygonFeature(Map.of( + "amenity", "school" + )))); + } + + @Test + public void testOsmLanduseLowerZoom() { + assertFeatures(6, List.of(Map.of( + "_layer", "landuse", + "class", "suburb", + "_minzoom", 6, + "_maxzoom", 14, + "_minpixelsize", 1d + )), process(polygonFeature(Map.of( + "place", "suburb" + )))); + assertFeatures(7, List.of(Map.of( + "_layer", "landuse", + "class", "residential", + "_minzoom", 6, + "_maxzoom", 14, + "_minpixelsize", 2d + )), process(polygonFeature(Map.of( + "landuse", "residential" + )))); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/MountainPeakTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/MountainPeakTest.java new file mode 100644 index 0000000..9f364e3 --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/MountainPeakTest.java @@ -0,0 +1,222 @@ +package com.onthegomap.planetiler.basemap.layers; + +import static com.onthegomap.planetiler.TestUtils.newPoint; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.common.collect.Lists; +import com.onthegomap.planetiler.VectorTile; +import com.onthegomap.planetiler.geo.GeometryException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class MountainPeakTest extends AbstractLayerTest { + + @BeforeEach + public void setupWikidataTranslation() { + wikidataTranslations.put(123, "es", "es wd name"); + } + + @Test + public void testHappyPath() { + var peak = process(pointFeature(Map.of( + "natural", "peak", + "name", "test", + "ele", "100", + "wikidata", "Q123" + ))); + assertFeatures(14, List.of(Map.of( + "class", "peak", + "ele", 100, + "ele_ft", 328, + + "_layer", "mountain_peak", + "_type", "point", + "_minzoom", 7, + "_maxzoom", 14, + "_buffer", 100d + )), peak); + assertFeatures(14, List.of(Map.of( + "name:latin", "test", + "name", "test", + "name:es", "es wd name" + )), peak); + } + + @Test + public void testLabelGrid() { + var peak = process(pointFeature(Map.of( + "natural", "peak", + "ele", "100" + ))); + assertFeatures(14, List.of(Map.of( + "_labelgrid_limit", 0 + )), peak); + assertFeatures(13, List.of(Map.of( + "_labelgrid_limit", 5, + "_labelgrid_size", 100d + )), peak); + } + + @Test + public void testVolcano() { + assertFeatures(14, List.of(Map.of( + "class", "volcano" + )), process(pointFeature(Map.of( + "natural", "volcano", + "ele", "100" + )))); + } + + @Test + public void testNoElevation() { + assertFeatures(14, List.of(), process(pointFeature(Map.of( + "natural", "volcano" + )))); + } + + @Test + public void testBogusElevation() { + assertFeatures(14, List.of(), process(pointFeature(Map.of( + "natural", "volcano", + "ele", "11000" + )))); + } + + @Test + public void testIgnoreLines() { + assertFeatures(14, List.of(), process(lineFeature(Map.of( + "natural", "peak", + "name", "name", + "ele", "100" + )))); + } + + private int getSortKey(Map tags) { + return process(pointFeature(Map.of( + "natural", "peak", + "ele", "100" + ))).iterator().next().getSortKey(); + } + + @Test + public void testSortKey() { + assertAscending( + getSortKey(Map.of( + "natural", "peak", + "name", "name", + "wikipedia", "wikilink", + "ele", "100" + )), + getSortKey(Map.of( + "natural", "peak", + "name", "name", + "ele", "100" + )), + getSortKey(Map.of( + "natural", "peak", + "ele", "100" + )) + ); + } + + @Test + public void testMountainPeakPostProcessing() throws GeometryException { + assertEquals(List.of(), profile.postProcessLayerFeatures(MountainPeak.LAYER_NAME, 13, List.of())); + + assertEquals(List.of(pointFeature( + MountainPeak.LAYER_NAME, + Map.of("rank", 1), + 1 + )), profile.postProcessLayerFeatures(MountainPeak.LAYER_NAME, 13, List.of(pointFeature( + MountainPeak.LAYER_NAME, + Map.of(), + 1 + )))); + + assertEquals(List.of( + pointFeature( + MountainPeak.LAYER_NAME, + Map.of("rank", 1, "name", "a"), + 1 + ), pointFeature( + MountainPeak.LAYER_NAME, + Map.of("rank", 2, "name", "b"), + 1 + ), pointFeature( + MountainPeak.LAYER_NAME, + Map.of("rank", 1, "name", "c"), + 2 + ) + ), profile.postProcessLayerFeatures(MountainPeak.LAYER_NAME, 13, List.of( + pointFeature( + MountainPeak.LAYER_NAME, + Map.of("name", "a"), + 1 + ), + pointFeature( + MountainPeak.LAYER_NAME, + Map.of("name", "b"), + 1 + ), + pointFeature( + MountainPeak.LAYER_NAME, + Map.of("name", "c"), + 2 + ) + ))); + } + + @Test + public void testMountainPeakPostProcessingLimitsFeaturesOutsideZoom() throws GeometryException { + assertEquals(Lists.newArrayList( + new VectorTile.Feature( + MountainPeak.LAYER_NAME, + 1, + VectorTile.encodeGeometry(newPoint(-64, -64)), + Map.of("rank", 1), + 1 + ), + null, + new VectorTile.Feature( + MountainPeak.LAYER_NAME, + 3, + VectorTile.encodeGeometry(newPoint(256 + 64, 256 + 64)), + Map.of("rank", 1), + 2 + ), + null + ), profile.postProcessLayerFeatures(MountainPeak.LAYER_NAME, 13, Lists.newArrayList( + new VectorTile.Feature( + MountainPeak.LAYER_NAME, + 1, + VectorTile.encodeGeometry(newPoint(-64, -64)), + new HashMap<>(), + 1 + ), + new VectorTile.Feature( + MountainPeak.LAYER_NAME, + 2, + VectorTile.encodeGeometry(newPoint(-65, -65)), + new HashMap<>(), + 1 + ), + new VectorTile.Feature( + MountainPeak.LAYER_NAME, + 3, + VectorTile.encodeGeometry(newPoint(256 + 64, 256 + 64)), + new HashMap<>(), + 2 + ), + new VectorTile.Feature( + MountainPeak.LAYER_NAME, + 4, + VectorTile.encodeGeometry(newPoint(256 + 65, 256 + 65)), + new HashMap<>(), + 2 + ) + ))); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/ParkTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/ParkTest.java new file mode 100644 index 0000000..7e71368 --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/ParkTest.java @@ -0,0 +1,135 @@ +package com.onthegomap.planetiler.basemap.layers; + +import com.onthegomap.planetiler.geo.GeoUtils; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class ParkTest extends AbstractLayerTest { + + @Test + public void testNationalPark() { + assertFeatures(13, List.of(Map.of( + "_layer", "park", + "_type", "polygon", + "class", "national_park", + "name", "", + "_minpixelsize", 2d, + "_minzoom", 6, + "_maxzoom", 14 + ), Map.of( + "_layer", "park", + "_type", "point", + "class", "national_park", + "name", "Grand Canyon National Park", + "name_int", "Grand Canyon National Park", + "name:latin", "Grand Canyon National Park", + "name:es", "es name", + "_minzoom", 6, + "_maxzoom", 14 + )), process(polygonFeature(Map.of( + "boundary", "national_park", + "name", "Grand Canyon National Park", + "name:es", "es name", + "protection_title", "National Park", + "wikipedia", "en:Grand Canyon National Park" + )))); + + // needs a name + assertFeatures(13, List.of(Map.of( + "_layer", "park", + "_type", "polygon" + )), process(polygonFeature(Map.of( + "boundary", "national_park", + "protection_title", "National Park" + )))); + } + + @Test + public void testSmallerPark() { + double z11area = Math.pow((GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d), 2) * Math.pow(2, 20 - 11); + assertFeatures(13, List.of(Map.of( + "_layer", "park", + "_type", "polygon", + "class", "protected_area", + "name", "", + "_minpixelsize", 2d, + "_minzoom", 6, + "_maxzoom", 14 + ), Map.of( + "_layer", "park", + "_type", "point", + "class", "protected_area", + "name", "Small park", + "name_int", "Small park", + "_minzoom", 11, + "_maxzoom", 14 + )), process(polygonFeatureWithArea(z11area, Map.of( + "boundary", "protected_area", + "name", "Small park", + "wikipedia", "en:Small park" + )))); + assertFeatures(13, List.of(Map.of( + "_layer", "park", + "_type", "polygon" + ), Map.of( + "_layer", "park", + "_type", "point", + "_minzoom", 6, + "_maxzoom", 14 + )), process(polygonFeatureWithArea(1, Map.of( + "boundary", "protected_area", + "name", "Small park", + "wikidata", "Q123" + )))); + } + + @Test + public void testSortKeys() { + assertAscending( + getLabelSortKey(1, Map.of( + "boundary", "national_park", + "name", "a", + "wikipedia", "en:park" + )), + getLabelSortKey(1e-10, Map.of( + "boundary", "national_park", + "name", "a", + "wikipedia", "en:Park" + )), + getLabelSortKey(1, Map.of( + "boundary", "national_park", + "name", "a" + )), + getLabelSortKey(1e-10, Map.of( + "boundary", "national_park", + "name", "a" + )), + + getLabelSortKey(1, Map.of( + "boundary", "protected_area", + "name", "a", + "wikipedia", "en:park" + )), + getLabelSortKey(1e-10, Map.of( + "boundary", "protected_area", + "name", "a", + "wikipedia", "en:Park" + )), + getLabelSortKey(1, Map.of( + "boundary", "protected_area", + "name", "a" + )), + getLabelSortKey(1e-10, Map.of( + "boundary", "protected_area", + "name", "a" + )) + ); + } + + private int getLabelSortKey(double area, Map tags) { + var iter = process(polygonFeatureWithArea(area, tags)).iterator(); + iter.next(); + return iter.next().getSortKey(); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/PlaceTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/PlaceTest.java new file mode 100644 index 0000000..4d2baf2 --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/PlaceTest.java @@ -0,0 +1,485 @@ +package com.onthegomap.planetiler.basemap.layers; + +import static com.onthegomap.planetiler.TestUtils.newPoint; +import static com.onthegomap.planetiler.TestUtils.rectangle; +import static com.onthegomap.planetiler.basemap.BasemapProfile.NATURAL_EARTH_SOURCE; +import static com.onthegomap.planetiler.basemap.BasemapProfile.OSM_SOURCE; +import static com.onthegomap.planetiler.basemap.layers.Place.getSortKey; +import static com.onthegomap.planetiler.collection.FeatureGroup.SORT_KEY_MAX; +import static com.onthegomap.planetiler.collection.FeatureGroup.SORT_KEY_MIN; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import com.onthegomap.planetiler.geo.GeoUtils; +import com.onthegomap.planetiler.geo.GeometryException; +import com.onthegomap.planetiler.reader.SimpleFeature; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class PlaceTest extends AbstractLayerTest { + + @Test + public void testContinent() { + wikidataTranslations.put(49, "es", "América del Norte y América Central"); + assertFeatures(0, List.of(Map.of( + "_layer", "place", + "class", "continent", + "name", "North America", + "name:en", "North America", + "name:es", "América del Norte y América Central", + "name:latin", "North America", + "rank", 1, + + "_type", "point", + "_minzoom", 0, + "_maxzoom", 3 + )), process(pointFeature(Map.of( + "place", "continent", + "wikidata", "Q49", + "name:es", "América del Norte", + "name", "North America", + "name:en", "North America" + )))); + } + + @Test + public void testCountry() { + wikidataTranslations.put(30, "es", "Estados Unidos"); + process(SimpleFeature.create( + rectangle(0, 0.25), + Map.of( + "name", "United States", + "scalerank", 0, + "labelrank", 2 + ), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_countries", + 0 + )); + assertFeatures(0, List.of(Map.of( + "_layer", "place", + "class", "country", + "name", "United States of America", + "name_en", "United States of America", + "name:es", "Estados Unidos", + "name:latin", "United States of America", + "iso_a2", "US", + "rank", 6, + + "_type", "point", + "_minzoom", 5 + )), process(SimpleFeature.create( + newPoint(0.5, 0.5), + Map.of( + "place", "country", + "wikidata", "Q30", + "name:es", "Estados Unidos de América", + "name", "United States of America", + "name:en", "United States of America", + "country_code_iso3166_1_alpha_2", "US" + ), + OSM_SOURCE, + null, + 0 + ))); + assertFeatures(0, List.of(Map.of( + "_layer", "place", + "class", "country", + "name", "United States of America", + "name_en", "United States of America", + "name:es", "Estados Unidos", + "name:latin", "United States of America", + "iso_a2", "US", + "rank", 1, + + "_type", "point", + "_minzoom", 0 + )), process(SimpleFeature.create( + newPoint(0.1, 0.1), + Map.of( + "place", "country", + "wikidata", "Q30", + "name:es", "Estados Unidos de América", + "name", "United States of America", + "name:en", "United States of America", + "country_code_iso3166_1_alpha_2", "US" + ), + OSM_SOURCE, + null, + 0 + ))); + } + + @Test + public void testState() { + wikidataTranslations.put(771, "es", "Massachusetts es"); + process(SimpleFeature.create( + rectangle(0, 0.25), + Map.of( + "name", "Massachusetts", + "scalerank", 0, + "labelrank", 2, + "datarank", 1 + ), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_1_states_provinces", + 0 + )); + + process(SimpleFeature.create( + rectangle(0.4, 0.6), + Map.of( + "name", "Massachusetts - not important", + "scalerank", 4, + "labelrank", 4, + "datarank", 1 + ), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_1_states_provinces", + 0 + )); + + // no match + assertFeatures(0, List.of(), process(SimpleFeature.create( + newPoint(1, 1), + Map.of( + "place", "state", + "wikidata", "Q771", + "name", "Massachusetts", + "name:en", "Massachusetts" + ), + OSM_SOURCE, + null, + 0 + ))); + + // unimportant match + assertFeatures(0, List.of(), process(SimpleFeature.create( + newPoint(0.5, 0.5), + Map.of( + "place", "state", + "wikidata", "Q771", + "name", "Massachusetts", + "name:en", "Massachusetts" + ), + OSM_SOURCE, + null, + 0 + ))); + + // important match + assertFeatures(0, List.of(Map.of( + "_layer", "place", + "class", "state", + "name", "Massachusetts", + "name_en", "Massachusetts", + "name:es", "Massachusetts es", + "name:latin", "Massachusetts", + "rank", 1, + + "_type", "point", + "_minzoom", 2 + )), process(SimpleFeature.create( + newPoint(0.1, 0.1), + Map.of( + "place", "state", + "wikidata", "Q771", + "name", "Massachusetts", + "name:en", "Massachusetts" + ), + OSM_SOURCE, + null, + 0 + ))); + } + + @Test + public void testIslandPoint() { + assertFeatures(0, List.of(Map.of( + "_layer", "place", + "class", "island", + "name", "Nantucket", + "name_en", "Nantucket", + "name:latin", "Nantucket", + "rank", 7, + + "_type", "point", + "_minzoom", 12 + )), process(pointFeature( + Map.of( + "place", "island", + "name", "Nantucket", + "name:en", "Nantucket" + )))); + } + + @Test + public void testIslandPolygon() { + assertFeatures(0, List.of(Map.of( + "_layer", "place", + "class", "island", + "name", "Nantucket", + "name_en", "Nantucket", + "name:latin", "Nantucket", + "rank", 3, + + "_type", "point", + "_minzoom", 8 + )), process(polygonFeatureWithArea(1, + Map.of( + "place", "island", + "name", "Nantucket", + "name:en", "Nantucket" + )))); + + double rank4area = Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(40_000_000 - 1)) / 256d, 2); + + assertFeatures(0, List.of(Map.of( + "_layer", "place", + "class", "island", + "name", "Nantucket", + "rank", 4, + + "_type", "point", + "_minzoom", 9 + )), process(polygonFeatureWithArea(rank4area, + Map.of( + "place", "island", + "name", "Nantucket", + "name:en", "Nantucket" + )))); + } + + @Test + public void testPlaceSortKeyRanking() { + int[] sortKeys = new int[]{ + // max + getSortKey(0, Place.PlaceType.CITY, 1_000_000_000, "name"), + + getSortKey(0, Place.PlaceType.CITY, 1_000_000_000, "name longer"), + getSortKey(0, Place.PlaceType.CITY, 1_000_000_000, "x".repeat(32)), + + getSortKey(0, Place.PlaceType.CITY, 10_000_000, "name"), + getSortKey(0, Place.PlaceType.CITY, 0, "name"), + + getSortKey(0, Place.PlaceType.TOWN, 1_000_000_000, "name"), + getSortKey(0, Place.PlaceType.ISOLATED_DWELLING, 1_000_000_000, "name"), + getSortKey(0, null, 1_000_000_000, "name"), + + getSortKey(1, Place.PlaceType.CITY, 1_000_000_000, "name"), + getSortKey(10, Place.PlaceType.CITY, 1_000_000_000, "name"), + getSortKey(null, Place.PlaceType.CITY, 1_000_000_000, "name"), + + // min + getSortKey(null, null, 0, null), + }; + for (int i = 0; i < sortKeys.length; i++) { + if (sortKeys[i] < SORT_KEY_MIN) { + fail("Item at index " + i + " is < " + SORT_KEY_MIN + ": " + sortKeys[i]); + } + if (sortKeys[i] > SORT_KEY_MAX) { + fail("Item at index " + i + " is > " + SORT_KEY_MAX + ": " + sortKeys[i]); + } + } + assertAscending(sortKeys); + } + + @Test + public void testCountryCapital() { + process(SimpleFeature.create( + newPoint(0, 0), + Map.of( + "name", "Washington, D.C.", + "scalerank", 0, + "wikidataid", "Q61" + ), + NATURAL_EARTH_SOURCE, + "ne_10m_populated_places", + 0 + )); + assertFeatures(7, List.of(Map.of( + "_layer", "place", + "class", "city", + "name", "Washington, D.C.", + "rank", 1, + "capital", 2, + "_labelgrid_limit", 0, + "_labelgrid_size", 128d, + + "_type", "point", + "_minzoom", 2 + )), process(pointFeature( + Map.of( + "place", "city", + "name", "Washington, D.C.", + "population", "672228", + "wikidata", "Q61", + "capital", "yes" + )))); + } + + @Test + public void testStateCapital() { + process(SimpleFeature.create( + newPoint(0, 0), + Map.of( + "name", "Boston", + "scalerank", 2, + "wikidataid", "Q100" + ), + NATURAL_EARTH_SOURCE, + "ne_10m_populated_places", + 0 + )); + assertFeatures(0, List.of(Map.of( + "_layer", "place", + "class", "city", + "name", "Boston", + "rank", 3, + "capital", 4, + + "_type", "point", + "_minzoom", 3 + )), process(pointFeature( + Map.of( + "place", "city", + "name", "Boston", + "population", "667137", + "capital", "4" + )))); + // no match when far away + assertFeatures(0, List.of(Map.of( + "_layer", "place", + "class", "city", + "name", "Boston", + "rank", "" + )), process(SimpleFeature.create( + newPoint(1, 1), + Map.of( + "place", "city", + "name", "Boston", + "wikidata", "Q100", + "population", "667137", + "capital", "4" + ), + OSM_SOURCE, + null, + 0 + ))); + // unaccented name match + assertFeatures(0, List.of(Map.of( + "_layer", "place", + "class", "city", + "rank", 3 + )), process(pointFeature( + Map.of( + "place", "city", + "name", "Böston", + "population", "667137", + "capital", "4" + )))); + // wikidata only match + assertFeatures(0, List.of(Map.of( + "_layer", "place", + "class", "city", + "rank", 3 + )), process(pointFeature( + Map.of( + "place", "city", + "name", "Other name", + "population", "667137", + "wikidata", "Q100", + "capital", "4" + )))); + } + + + @Test + public void testCityWithoutNaturalEarthMatch() { + assertFeatures(7, List.of(Map.of( + "_layer", "place", + "class", "city", + "rank", "", + "_minzoom", 7, + "_labelgrid_limit", 4, + "_labelgrid_size", 128d + )), process(pointFeature( + Map.of( + "place", "city", + "name", "City name" + )))); + assertFeatures(13, List.of(Map.of( + "_layer", "place", + "class", "isolated_dwelling", + "rank", "", + "_labelgrid_limit", 0, + "_labelgrid_size", 0d, + "_minzoom", 14 + )), process(pointFeature( + Map.of( + "place", "isolated_dwelling", + "name", "City name" + )))); + assertFeatures(12, List.of(Map.of( + "_layer", "place", + "class", "isolated_dwelling", + "rank", "", + "_labelgrid_limit", 14, + "_labelgrid_size", 128d, + "_minzoom", 14 + )), process(pointFeature( + Map.of( + "place", "isolated_dwelling", + "name", "City name" + )))); + } + + @Test + public void testCitySetRankFromGridrank() throws GeometryException { + var layerName = Place.LAYER_NAME; + assertEquals(List.of(), profile.postProcessLayerFeatures(layerName, 13, List.of())); + + assertEquals(List.of(pointFeature( + layerName, + Map.of("rank", 11), + 1 + )), profile.postProcessLayerFeatures(layerName, 13, List.of(pointFeature( + layerName, + Map.of(), + 1 + )))); + + assertEquals(List.of( + pointFeature( + layerName, + Map.of("rank", 11, "name", "a"), + 1 + ), pointFeature( + layerName, + Map.of("rank", 12, "name", "b"), + 1 + ), pointFeature( + layerName, + Map.of("rank", 11, "name", "c"), + 2 + ) + ), profile.postProcessLayerFeatures(layerName, 13, List.of( + pointFeature( + layerName, + Map.of("name", "a"), + 1 + ), + pointFeature( + layerName, + Map.of("name", "b"), + 1 + ), + pointFeature( + layerName, + Map.of("name", "c"), + 2 + ) + ))); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/PoiTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/PoiTest.java new file mode 100644 index 0000000..570073b --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/PoiTest.java @@ -0,0 +1,184 @@ +package com.onthegomap.planetiler.basemap.layers; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.onthegomap.planetiler.geo.GeometryException; +import com.onthegomap.planetiler.reader.SourceFeature; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class PoiTest extends AbstractLayerTest { + + private SourceFeature feature(boolean area, Map tags) { + return area ? polygonFeature(tags) : pointFeature(tags); + } + + @Test + public void testFenwayPark() { + assertFeatures(7, List.of(Map.of( + "_layer", "poi", + "class", "stadium", + "subclass", "stadium", + "name", "Fenway Park", + "rank", "", + "_minzoom", 14, + "_labelgrid_size", 64d + )), process(pointFeature(Map.of( + "leisure", "stadium", + "name", "Fenway Park" + )))); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void testFunicularHalt(boolean area) { + assertFeatures(7, List.of(Map.of( + "_layer", "poi", + "class", "railway", + "subclass", "halt", + "rank", "", + "_minzoom", 12 + )), process(feature(area, Map.of( + "railway", "station", + "funicular", "yes", + "name", "station" + )))); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void testSubway(boolean area) { + assertFeatures(7, List.of(Map.of( + "_layer", "poi", + "class", "railway", + "subclass", "subway", + "rank", "", + "_minzoom", 12 + )), process(feature(area, Map.of( + "railway", "station", + "station", "subway", + "name", "station" + )))); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void testPlaceOfWorshipFromReligionTag(boolean area) { + assertFeatures(7, List.of(Map.of( + "_layer", "poi", + "class", "place_of_worship", + "subclass", "religion value", + "rank", "", + "_minzoom", 14 + )), process(feature(area, Map.of( + "amenity", "place_of_worship", + "religion", "religion value", + "name", "station" + )))); + } + + @Test + public void testPitchFromSportTag() { + assertFeatures(7, List.of(Map.of( + "_layer", "poi", + "class", "pitch", + "subclass", "soccer", + "rank", "" + )), process(pointFeature(Map.of( + "leisure", "pitch", + "sport", "soccer", + "name", "station" + )))); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void testInformation(boolean area) { + assertFeatures(7, List.of(Map.of( + "_layer", "poi", + "class", "information", + "subclass", "infotype", + "layer", 3L, + "level", 2L, + "indoor", 1, + "rank", "" + )), process(feature(area, Map.of( + "tourism", "information", + "information", "infotype", + "name", "station", + "layer", "3", + "level", "2", + "indoor", "yes" + )))); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void testFerryTerminal(boolean area) { + assertFeatures(7, List.of(Map.of( + "_layer", "poi", + "class", "ferry_terminal", + "subclass", "ferry_terminal", + "name", "Water Taxi", + "_minzoom", 12 + )), process(feature(area, Map.of( + "amenity", "ferry_terminal", + "information", "infotype", + "name", "Water Taxi", + "layer", "3", + "level", "2", + "indoor", "yes" + )))); + } + + @Test + public void testGridRank() throws GeometryException { + var layerName = Poi.LAYER_NAME; + assertEquals(List.of(), profile.postProcessLayerFeatures(layerName, 13, List.of())); + + assertEquals(List.of(pointFeature( + layerName, + Map.of("rank", 1), + 1 + )), profile.postProcessLayerFeatures(layerName, 14, List.of(pointFeature( + layerName, + Map.of(), + 1 + )))); + + assertEquals(List.of( + pointFeature( + layerName, + Map.of("rank", 1, "name", "a"), + 1 + ), pointFeature( + layerName, + Map.of("rank", 2, "name", "b"), + 1 + ), pointFeature( + layerName, + Map.of("rank", 1, "name", "c"), + 2 + ) + ), profile.postProcessLayerFeatures(layerName, 14, List.of( + pointFeature( + layerName, + Map.of("name", "a"), + 1 + ), + pointFeature( + layerName, + Map.of("name", "b"), + 1 + ), + pointFeature( + layerName, + Map.of("name", "c"), + 2 + ) + ))); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/TransportationTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/TransportationTest.java new file mode 100644 index 0000000..11f4b13 --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/TransportationTest.java @@ -0,0 +1,721 @@ +package com.onthegomap.planetiler.basemap.layers; + +import static com.onthegomap.planetiler.TestUtils.newLineString; +import static com.onthegomap.planetiler.TestUtils.rectangle; +import static com.onthegomap.planetiler.basemap.BasemapProfile.NATURAL_EARTH_SOURCE; +import static com.onthegomap.planetiler.basemap.BasemapProfile.OSM_SOURCE; + +import com.onthegomap.planetiler.FeatureCollector; +import com.onthegomap.planetiler.geo.GeometryException; +import com.onthegomap.planetiler.reader.SimpleFeature; +import com.onthegomap.planetiler.reader.osm.OsmElement; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; + +public class TransportationTest extends AbstractLayerTest { + + @Test + public void testNamedFootway() { + FeatureCollector result = process(lineFeature(Map.of( + "name", "Lagoon Path", + "surface", "asphalt", + "level", "0", + "highway", "footway", + "indoor", "no", + "oneway", "no", + "foot", "designated", + "bicycle", "dismount" + ))); + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "_type", "line", + "class", "path", + "subclass", "footway", + "oneway", 0, + "name", "", + "_buffer", 4d, + "_minpixelsize", 0d, + "_minzoom", 13, + "_maxzoom", 14 + ), Map.of( + "_layer", "transportation_name", + "_type", "line", + "class", "path", + "subclass", "footway", + "name", "Lagoon Path", + "name_int", "Lagoon Path", + "name:latin", "Lagoon Path", + "_minpixelsize", 0d, + "_minzoom", 13, + "_maxzoom", 14 + )), result); + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "surface", "paved", + "oneway", 0, + "level", 0L, + "ramp", 0, + "bicycle", "dismount", + "foot", "designated" + ), Map.of( + "_layer", "transportation_name", + "level", 0L, + "surface", "", + "oneway", "", + "ramp", "", + "bicycle", "", + "foot", "" + )), result); + } + + @Test + public void testUnnamedPath() { + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "path", + "subclass", "path", + "surface", "unpaved", + "oneway", 0 + )), process(lineFeature(Map.of( + "surface", "dirt", + "highway", "path" + )))); + } + + @Test + public void testIndoorTunnelSteps() { + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "path", + "subclass", "steps", + "brunnel", "tunnel", + "indoor", 1, + "oneway", 1, + "ramp", 1 + )), process(lineFeature(Map.of( + "highway", "steps", + "tunnel", "building_passage", + "oneway", "yes", + "indoor", "yes" + )))); + } + + @Test + public void testInterstateMotorway() { + var rel = new OsmElement.Relation(1); + rel.setTag("type", "route"); + rel.setTag("route", "road"); + rel.setTag("network", "US:I"); + rel.setTag("ref", "90"); + + FeatureCollector features = process(lineFeatureWithRelation( + profile.preprocessOsmRelation(rel), + Map.of( + "highway", "motorway", + "oneway", "yes", + "name", "Massachusetts Turnpike", + "ref", "I 90", + "surface", "asphalt", + "foot", "no", + "bicycle", "no", + "horse", "no", + "bridge", "yes" + ))); + + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "motorway", + "surface", "paved", + "oneway", 1, + "ramp", 0, + "bicycle", "no", + "foot", "no", + "horse", "no", + "brunnel", "bridge", + "_minzoom", 4 + ), Map.of( + "_layer", "transportation_name", + "class", "motorway", + "name", "Massachusetts Turnpike", + "name_en", "Massachusetts Turnpike", + "ref", "90", + "ref_length", 2, + "network", "us-interstate", + "brunnel", "", + "_minzoom", 6 + )), features); + + assertFeatures(8, List.of(Map.of( + "_layer", "transportation", + "class", "motorway", + "surface", "", + "oneway", "", + "ramp", "", + "bicycle", "", + "foot", "", + "horse", "", + "brunnel", "bridge", + "_minzoom", 4 + ), Map.of( + "_layer", "transportation_name", + "class", "motorway", + "name", "Massachusetts Turnpike", + "name_en", "Massachusetts Turnpike", + "ref", "90", + "ref_length", 2, + "network", "us-interstate", + "brunnel", "", + "_minzoom", 6 + )), features); + } + + @Test + public void testPrimaryRoadConstruction() { + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "primary_construction", + "brunnel", "bridge", + "layer", 1L, + "oneway", 1, + "_minzoom", 7 + ), Map.of( + "_layer", "transportation_name", + "name", "North Washington Street", + "class", "primary_construction", + "brunnel", "", + "_minzoom", 12 + )), process(lineFeature(Map.of( + "highway", "construction", + "construction", "primary", + "bridge", "yes", + "layer", "1", + "name", "North Washington Street", + "oneway", "yes" + )))); + } + + @Test + public void testRaceway() { + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "raceway", + "oneway", 1, + "_minzoom", 12 + ), Map.of( + "_layer", "transportation_name", + "class", "raceway", + "name", "Climbing Turn", + "ref", "5", + "_minzoom", 12 + )), process(lineFeature(Map.of( + "highway", "raceway", + "oneway", "yes", + "ref", "5", + "name", "Climbing Turn" + )))); + } + + @Test + public void testDriveway() { + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "service", + "service", "driveway", + "_minzoom", 12 + )), process(lineFeature(Map.of( + "highway", "service", + "service", "driveway" + )))); + } + + @Test + public void testMountainBikeTrail() { + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "path", + "subclass", "path", + "mtb_scale", "4", + "surface", "unpaved", + "bicycle", "yes", + "_minzoom", 13 + ), Map.of( + "_layer", "transportation_name", + "class", "path", + "subclass", "path", + "name", "Path name", + "_minzoom", 13 + )), process(lineFeature(Map.of( + "highway", "path", + "mtb:scale", "4", + "name", "Path name", + "bicycle", "yes", + "surface", "ground" + )))); + } + + @Test + public void testTrack() { + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "track", + "surface", "unpaved", + "horse", "yes", + "_minzoom", 14 + )), process(lineFeature(Map.of( + "highway", "track", + "surface", "dirt", + "horse", "yes" + )))); + } + + final OsmElement.Relation relUS = new OsmElement.Relation(1); + + { + relUS.setTag("type", "route"); + relUS.setTag("route", "road"); + relUS.setTag("network", "US:US"); + relUS.setTag("ref", "3"); + } + + final OsmElement.Relation relMA = new OsmElement.Relation(2); + + { + relMA.setTag("type", "route"); + relMA.setTag("route", "road"); + relMA.setTag("network", "US:MA"); + relMA.setTag("ref", "2"); + } + + @Test + public void testUSAndStateHighway() { + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "primary", + "surface", "paved", + "oneway", 0, + "ramp", 0, + "_minzoom", 7 + ), Map.of( + "_layer", "transportation_name", + "class", "primary", + "name", "Memorial Drive", + "name_en", "Memorial Drive", + "ref", "3", + "ref_length", 1, + "network", "us-highway", + "_minzoom", 12 + )), process(lineFeatureWithRelation( + Stream.concat( + profile.preprocessOsmRelation(relUS).stream(), + profile.preprocessOsmRelation(relMA).stream() + ).toList(), + Map.of( + "highway", "primary", + "name", "Memorial Drive", + "ref", "US 3;MA 2", + "surface", "asphalt" + )))); + + // swap order + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "primary" + ), Map.of( + "_layer", "transportation_name", + "class", "primary", + "ref", "3", + "network", "us-highway" + )), process(lineFeatureWithRelation( + Stream.concat( + profile.preprocessOsmRelation(relMA).stream(), + profile.preprocessOsmRelation(relUS).stream() + ).toList(), + Map.of( + "highway", "primary", + "name", "Memorial Drive", + "ref", "US 3;MA 2", + "surface", "asphalt" + )))); + } + + @Test + public void testUsStateHighway() { + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "primary" + ), Map.of( + "_layer", "transportation_name", + "class", "primary", + "name", "Memorial Drive", + "name_en", "Memorial Drive", + "ref", "2", + "ref_length", 1, + "network", "us-state", + "_minzoom", 12 + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relMA), + Map.of( + "highway", "primary", + "name", "Memorial Drive", + "ref", "US 3;MA 2", + "surface", "asphalt" + )))); + } + + @Test + public void testCompoundRef() { + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "primary" + ), Map.of( + "_layer", "transportation_name", + "class", "primary", + "name", "Memorial Drive", + "name_en", "Memorial Drive", + "ref", "US 3;MA 2", + "ref_length", 9, + "network", "road", + "_minzoom", 12 + )), process(lineFeature( + Map.of( + "highway", "primary", + "name", "Memorial Drive", + "ref", "US 3;MA 2", + "surface", "asphalt" + )))); + } + + @Test + public void testTransCanadaHighway() { + var rel = new OsmElement.Relation(1); + rel.setTag("type", "route"); + rel.setTag("route", "road"); + rel.setTag("network", "CA:transcanada:namedRoute"); + + FeatureCollector features = process(lineFeatureWithRelation( + profile.preprocessOsmRelation(rel), + Map.of( + "highway", "motorway", + "oneway", "yes", + "name", "Autoroute Claude-Béchard", + "ref", "85", + "surface", "asphalt" + ))); + + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "motorway", + "surface", "paved", + "oneway", 1, + "ramp", 0, + "_minzoom", 4 + ), Map.of( + "_layer", "transportation_name", + "class", "motorway", + "name", "Autoroute Claude-Béchard", + "name_en", "Autoroute Claude-Béchard", + "ref", "85", + "ref_length", 2, + "network", "ca-transcanada", + "_minzoom", 6 + )), features); + } + + @Test + public void testGreatBritainHighway() { + process(SimpleFeature.create( + rectangle(0, 0.1), + Map.of("iso_a2", "GB"), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_countries", + 0 + )); + + // in GB + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "motorway", + "oneway", 1, + "ramp", 0, + "_minzoom", 4 + ), Map.of( + "_layer", "transportation_name", + "class", "motorway", + "ref", "M1", + "ref_length", 2, + "network", "gb-motorway", + "_minzoom", 6 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "highway", "motorway", + "oneway", "yes", + "ref", "M1" + ), + OSM_SOURCE, + null, + 0 + ))); + + // not in GB + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "motorway", + "oneway", 1, + "ramp", 0, + "_minzoom", 4 + ), Map.of( + "_layer", "transportation_name", + "class", "motorway", + "ref", "M1", + "ref_length", 2, + "network", "road", + "_minzoom", 6 + )), process(SimpleFeature.create( + newLineString(1, 0, 0, 1), + Map.of( + "highway", "motorway", + "oneway", "yes", + "ref", "M1" + ), + OSM_SOURCE, + null, + 0 + ))); + } + + @Test + public void testMergesDisconnectedRoadFeatures() throws GeometryException { + testMergesLinestrings(Map.of("class", "motorway"), Transportation.LAYER_NAME, 10, 14); + } + + @Test + public void testMergesDisconnectedRoadNameFeatures() throws GeometryException { + testMergesLinestrings(Map.of("class", "motorway"), TransportationName.LAYER_NAME, 10, 14); + } + + @Test + public void testLightRail() { + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "transit", + "subclass", "light_rail", + "brunnel", "tunnel", + "layer", -1L, + "oneway", 0, + "ramp", 0, + + "_minzoom", 11, + "_maxzoom", 14, + "_type", "line" + )), process(lineFeature(Map.of( + "railway", "light_rail", + "name", "Green Line", + "tunnel", "yes", + "layer", "-1" + )))); + } + + @Test + public void testSubway() { + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "transit", + "subclass", "subway", + "brunnel", "tunnel", + "layer", -2L, + "oneway", 0, + "ramp", 0, + + "_minzoom", 14, + "_maxzoom", 14, + "_type", "line" + )), process(lineFeature(Map.of( + "railway", "subway", + "name", "Red Line", + "tunnel", "yes", + "layer", "-2", + "level", "-2" + )))); + } + + @Test + public void testRail() { + assertFeatures(8, List.of(Map.of( + "_layer", "transportation", + "class", "rail", + "subclass", "rail", + "brunnel", "", + "layer", "", + + "_minzoom", 8, + "_maxzoom", 14, + "_type", "line" + )), process(lineFeature(Map.of( + "railway", "rail", + "name", "Boston Subdivision", + "usage", "main", + "tunnel", "yes", + "layer", "-2" + )))); + assertFeatures(13, List.of(Map.of( + "_minzoom", 10 + )), process(lineFeature(Map.of( + "railway", "rail", + "name", "Boston Subdivision" + )))); + assertFeatures(13, List.of(), + process(polygonFeature(Map.of( + "railway", "rail" + )))); + assertFeatures(13, List.of(Map.of( + "class", "rail", + "subclass", "rail", + "_minzoom", 14, + "service", "yard" + )), process(lineFeature(Map.of( + "railway", "rail", + "name", "Boston Subdivision", + "service", "yard" + )))); + } + + @Test + public void testNarrowGauge() { + assertFeatures(10, List.of(Map.of( + "_layer", "transportation", + "class", "rail", + "subclass", "narrow_gauge", + + "_minzoom", 10, + "_maxzoom", 14, + "_type", "line" + )), process(lineFeature(Map.of( + "railway", "narrow_gauge" + )))); + } + + @Test + public void testAerialway() { + assertFeatures(10, List.of(Map.of( + "_layer", "transportation", + "class", "aerialway", + "subclass", "gondola", + + "_minzoom", 12, + "_maxzoom", 14, + "_type", "line" + )), process(lineFeature(Map.of( + "aerialway", "gondola", + "name", "Summit Gondola" + )))); + assertFeatures(10, List.of(), + process(polygonFeature(Map.of( + "aerialway", "gondola", + "name", "Summit Gondola" + )))); + } + + @Test + public void testFerry() { + assertFeatures(10, List.of(Map.of( + "_layer", "transportation", + "class", "ferry", + + "_minzoom", 11, + "_maxzoom", 14, + "_type", "line" + )), process(lineFeature(Map.of( + "route", "ferry", + "name", "Boston - Provincetown Ferry", + "motor_vehicle", "no", + "foot", "yes", + "bicycle", "yes" + )))); + assertFeatures(10, List.of(), + process(polygonFeature(Map.of( + "route", "ferry", + "name", "Boston - Provincetown Ferry", + "motor_vehicle", "no", + "foot", "yes", + "bicycle", "yes" + )))); + } + + @Test + public void testPiers() { + // area + assertFeatures(10, List.of(Map.of( + "_layer", "transportation", + "class", "pier", + + "_minzoom", 13, + "_maxzoom", 14, + "_type", "polygon" + )), process(polygonFeature(Map.of( + "man_made", "pier" + )))); + assertFeatures(10, List.of(Map.of( + "_layer", "transportation", + "class", "pier", + + "_minzoom", 13, + "_maxzoom", 14, + "_type", "line" + )), process(lineFeature(Map.of( + "man_made", "pier" + )))); + } + + @Test + public void testPedestrianArea() { + assertFeatures(10, List.of(Map.of( + "_layer", "transportation", + "class", "path", + "subclass", "pedestrian", + + "_minzoom", 13, + "_maxzoom", 14, + "_type", "polygon" + )), process(polygonFeature(Map.of( + "highway", "pedestrian", + "area", "yes", + "foot", "yes" + )))); + // ignore underground pedestrian areas + assertFeatures(10, List.of(), + process(polygonFeature(Map.of( + "highway", "pedestrian", + "area", "yes", + "foot", "yes", + "layer", "-1" + )))); + } + + private int getWaySortKey(Map tags) { + var iter = process(lineFeature(tags)).iterator(); + return iter.next().getSortKey(); + } + + @Test + public void testSortKeys() { + assertDescending( + getWaySortKey(Map.of("highway", "footway", "layer", "2")), + getWaySortKey(Map.of("highway", "motorway", "bridge", "yes")), + getWaySortKey(Map.of("highway", "footway", "bridge", "yes")), + getWaySortKey(Map.of("highway", "motorway")), + getWaySortKey(Map.of("highway", "trunk")), + getWaySortKey(Map.of("railway", "rail")), + getWaySortKey(Map.of("highway", "primary")), + getWaySortKey(Map.of("highway", "secondary")), + getWaySortKey(Map.of("highway", "tertiary")), + getWaySortKey(Map.of("highway", "motorway_link")), + getWaySortKey(Map.of("highway", "footway")), + getWaySortKey(Map.of("highway", "motorway", "tunnel", "yes")), + getWaySortKey(Map.of("highway", "footway", "tunnel", "yes")), + getWaySortKey(Map.of("highway", "motorway", "layer", "-2")) + ); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterNameTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterNameTest.java new file mode 100644 index 0000000..a3deee8 --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterNameTest.java @@ -0,0 +1,155 @@ +package com.onthegomap.planetiler.basemap.layers; + +import static com.onthegomap.planetiler.TestUtils.newLineString; +import static com.onthegomap.planetiler.TestUtils.rectangle; +import static com.onthegomap.planetiler.basemap.BasemapProfile.LAKE_CENTERLINE_SOURCE; +import static com.onthegomap.planetiler.basemap.BasemapProfile.NATURAL_EARTH_SOURCE; +import static com.onthegomap.planetiler.basemap.BasemapProfile.OSM_SOURCE; + +import com.onthegomap.planetiler.TestUtils; +import com.onthegomap.planetiler.geo.GeoUtils; +import com.onthegomap.planetiler.reader.SimpleFeature; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class WaterNameTest extends AbstractLayerTest { + + @Test + public void testWaterNamePoint() { + assertFeatures(11, List.of(Map.of( + "_layer", "water" + ), Map.of( + "class", "lake", + "name", "waterway", + "name:es", "waterway es", + "intermittent", 1, + + "_layer", "water_name", + "_type", "point", + "_minzoom", 9, + "_maxzoom", 14 + )), process(polygonFeatureWithArea(1, Map.of( + "name", "waterway", + "name:es", "waterway es", + "natural", "water", + "water", "pond", + "intermittent", "1" + )))); + double z11area = Math.pow((GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d), 2) * Math.pow(2, 20 - 11); + assertFeatures(10, List.of(Map.of( + "_layer", "water" + ), Map.of( + "_layer", "water_name", + "_type", "point", + "_minzoom", 11, + "_maxzoom", 14 + )), process(polygonFeatureWithArea(z11area, Map.of( + "name", "waterway", + "natural", "water", + "water", "pond" + )))); + } + + @Test + public void testWaterNameLakeline() { + assertFeatures(11, List.of(), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + new HashMap<>(Map.of( + "OSM_ID", -10 + )), + LAKE_CENTERLINE_SOURCE, + null, + 0 + ))); + assertFeatures(10, List.of(Map.of( + "_layer", "water" + ), Map.of( + "name", "waterway", + "name:es", "waterway es", + + "_layer", "water_name", + "_type", "line", + "_geom", new TestUtils.NormGeometry(GeoUtils.latLonToWorldCoords(newLineString(0, 0, 1, 1))), + "_minzoom", 9, + "_maxzoom", 14, + "_minpixelsize", "waterway".length() * 6d + )), process(SimpleFeature.create( + GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), + new HashMap<>(Map.of( + "name", "waterway", + "name:es", "waterway es", + "natural", "water", + "water", "pond" + )), + OSM_SOURCE, + null, + 10 + ))); + } + + @Test + public void testMarinePoint() { + assertFeatures(11, List.of(), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + new HashMap<>(Map.of( + "scalerank", 1, + "name", "Black sea" + )), + NATURAL_EARTH_SOURCE, + "ne_10m_geography_marine_polys", + 0 + ))); + + // name match - use scale rank from NE + assertFeatures(10, List.of(Map.of( + "name", "Black Sea", + "name:es", "Mar Negro", + "_layer", "water_name", + "_type", "point", + "_minzoom", 1, + "_maxzoom", 14 + )), process(pointFeature(Map.of( + "rank", 9, + "name", "Black Sea", + "name:es", "Mar Negro", + "place", "sea" + )))); + + // name match but ocean - use min zoom=0 + assertFeatures(10, List.of(Map.of( + "_layer", "water_name", + "_type", "point", + "_minzoom", 0, + "_maxzoom", 14 + )), process(pointFeature(Map.of( + "rank", 9, + "name", "Black Sea", + "place", "ocean" + )))); + + // no name match - use OSM rank + assertFeatures(10, List.of(Map.of( + "_layer", "water_name", + "_type", "point", + "_minzoom", 9, + "_maxzoom", 14 + )), process(pointFeature(Map.of( + "rank", 9, + "name", "Atlantic", + "place", "sea" + )))); + + // no rank at all, default to 8 + assertFeatures(10, List.of(Map.of( + "_layer", "water_name", + "_type", "point", + "_minzoom", 8, + "_maxzoom", 14 + )), process(pointFeature(Map.of( + "name", "Atlantic", + "place", "sea" + )))); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterTest.java new file mode 100644 index 0000000..73563bd --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterTest.java @@ -0,0 +1,225 @@ +package com.onthegomap.planetiler.basemap.layers; + +import static com.onthegomap.planetiler.TestUtils.rectangle; +import static com.onthegomap.planetiler.basemap.BasemapProfile.NATURAL_EARTH_SOURCE; +import static com.onthegomap.planetiler.basemap.BasemapProfile.OSM_SOURCE; +import static com.onthegomap.planetiler.basemap.BasemapProfile.WATER_POLYGON_SOURCE; + +import com.onthegomap.planetiler.reader.SimpleFeature; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class WaterTest extends AbstractLayerTest { + + + @Test + public void testWaterNaturalEarth() { + assertFeatures(0, List.of(Map.of( + "class", "lake", + "intermittent", "", + "_layer", "water", + "_type", "polygon", + "_minzoom", 0 + )), process(SimpleFeature.create( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_110m_lakes", + 0 + ))); + + assertFeatures(0, List.of(Map.of( + "class", "ocean", + "intermittent", "", + "_layer", "water", + "_type", "polygon", + "_minzoom", 0 + )), process(SimpleFeature.create( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_110m_ocean", + 0 + ))); + + assertFeatures(6, List.of(Map.of( + "class", "lake", + "_layer", "water", + "_type", "polygon", + "_maxzoom", 5 + )), process(SimpleFeature.create( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_10m_lakes", + 0 + ))); + + assertFeatures(6, List.of(Map.of( + "class", "ocean", + "_layer", "water", + "_type", "polygon", + "_maxzoom", 5 + )), process(SimpleFeature.create( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_10m_ocean", + 0 + ))); + } + + @Test + public void testWaterOsmWaterPolygon() { + assertFeatures(0, List.of(Map.of( + "class", "ocean", + "intermittent", "", + "_layer", "water", + "_type", "polygon", + "_minzoom", 6, + "_maxzoom", 14 + )), process(SimpleFeature.create( + rectangle(0, 10), + Map.of(), + WATER_POLYGON_SOURCE, + null, + 0 + ))); + } + + @Test + public void testWater() { + assertFeatures(14, List.of(Map.of( + "class", "lake", + "_layer", "water", + "_type", "polygon", + "_minzoom", 6, + "_maxzoom", 14 + )), process(polygonFeature(Map.of( + "natural", "water", + "water", "reservoir" + )))); + assertFeatures(14, List.of( + Map.of("_layer", "poi"), + Map.of( + "class", "lake", + + "_layer", "water", + "_type", "polygon", + "_minzoom", 6, + "_maxzoom", 14 + )), process(polygonFeature(Map.of( + "leisure", "swimming_pool" + )))); + assertFeatures(14, List.of(), process(polygonFeature(Map.of( + "natural", "bay" + )))); + assertFeatures(14, List.of(Map.of()), process(polygonFeature(Map.of( + "natural", "water" + )))); + assertFeatures(14, List.of(), process(polygonFeature(Map.of( + "natural", "water", + "covered", "yes" + )))); + assertFeatures(14, List.of(Map.of( + "class", "river", + "brunnel", "bridge", + "intermittent", 1, + + "_layer", "water", + "_type", "polygon", + "_minzoom", 6, + "_maxzoom", 14 + )), process(polygonFeature(Map.of( + "waterway", "stream", + "bridge", "1", + "intermittent", "1" + )))); + assertFeatures(11, List.of(Map.of( + "class", "lake", + "brunnel", "", + "intermittent", 0, + + "_layer", "water", + "_type", "polygon", + "_minzoom", 6, + "_maxzoom", 14, + "_minpixelsize", 2d + )), process(polygonFeature(Map.of( + "landuse", "salt_pond", + "bridge", "1" + )))); + } + + @Test + public void testOceanZoomLevels() { + assertCoversZoomRange(0, 14, "water", + process(SimpleFeature.create( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_110m_ocean", + 0 + )), + process(SimpleFeature.create( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_50m_ocean", + 0 + )), + process(SimpleFeature.create( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_10m_ocean", + 0 + )), + process(SimpleFeature.create( + rectangle(0, 10), + Map.of(), + WATER_POLYGON_SOURCE, + null, + 0 + )) + ); + } + + @Test + public void testLakeZoomLevels() { + assertCoversZoomRange(0, 14, "water", + process(SimpleFeature.create( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_110m_lakes", + 0 + )), + process(SimpleFeature.create( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_50m_lakes", + 0 + )), + process(SimpleFeature.create( + rectangle(0, 10), + Map.of(), + NATURAL_EARTH_SOURCE, + "ne_10m_lakes", + 0 + )), + process(SimpleFeature.create( + rectangle(0, 10), + Map.of( + "natural", "water", + "water", "reservoir" + ), + OSM_SOURCE, + null, + 0 + )) + ); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterwayTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterwayTest.java new file mode 100644 index 0000000..c77c4f1 --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterwayTest.java @@ -0,0 +1,186 @@ +package com.onthegomap.planetiler.basemap.layers; + +import static com.onthegomap.planetiler.TestUtils.newLineString; +import static com.onthegomap.planetiler.basemap.BasemapProfile.NATURAL_EARTH_SOURCE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.onthegomap.planetiler.VectorTile; +import com.onthegomap.planetiler.geo.GeometryException; +import com.onthegomap.planetiler.reader.SimpleFeature; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class WaterwayTest extends AbstractLayerTest { + + @Test + public void testWaterwayImportantRiverProcess() { + var charlesRiver = process(lineFeature(Map.of( + "waterway", "river", + "name", "charles river", + "name:es", "es name" + ))); + assertFeatures(14, List.of(Map.of( + "class", "river", + "name", "charles river", + "name:es", "es name", + "intermittent", 0, + + "_layer", "waterway", + "_type", "line", + "_minzoom", 9, + "_maxzoom", 14, + "_buffer", 4d + )), charlesRiver); + assertFeatures(11, List.of(Map.of( + "class", "river", + "name", "charles river", + "name:es", "es name", + "intermittent", "", + "_buffer", 13.082664546679323 + )), charlesRiver); + assertFeatures(10, List.of(Map.of( + "class", "river", + "_buffer", 26.165329093358647 + )), charlesRiver); + assertFeatures(9, List.of(Map.of( + "class", "river", + "_buffer", 26.165329093358647 + )), charlesRiver); + } + + @Test + public void testWaterwayImportantRiverPostProcess() throws GeometryException { + var line1 = new VectorTile.Feature( + Waterway.LAYER_NAME, + 1, + VectorTile.encodeGeometry(newLineString(0, 0, 10, 0)), + Map.of("name", "river"), + 0 + ); + var line2 = new VectorTile.Feature( + Waterway.LAYER_NAME, + 1, + VectorTile.encodeGeometry(newLineString(10, 0, 20, 0)), + Map.of("name", "river"), + 0 + ); + var connected = new VectorTile.Feature( + Waterway.LAYER_NAME, + 1, + VectorTile.encodeGeometry(newLineString(0, 0, 20, 0)), + Map.of("name", "river"), + 0 + ); + + assertEquals( + List.of(), + profile.postProcessLayerFeatures(Waterway.LAYER_NAME, 11, List.of()) + ); + assertEquals( + List.of(line1, line2), + profile.postProcessLayerFeatures(Waterway.LAYER_NAME, 12, List.of(line1, line2)) + ); + assertEquals( + List.of(connected), + profile.postProcessLayerFeatures(Waterway.LAYER_NAME, 11, List.of(line1, line2)) + ); + } + + @Test + public void testWaterwaySmaller() { + // river with no name is not important + assertFeatures(14, List.of(Map.of( + "class", "river", + "brunnel", "bridge", + + "_layer", "waterway", + "_type", "line", + "_minzoom", 12 + )), process(lineFeature(Map.of( + "waterway", "river", + "bridge", "1" + )))); + + assertFeatures(14, List.of(Map.of( + "class", "canal", + "_layer", "waterway", + "_type", "line", + "_minzoom", 12 + )), process(lineFeature(Map.of( + "waterway", "canal", + "name", "name" + )))); + + assertFeatures(14, List.of(Map.of( + "class", "stream", + "_layer", "waterway", + "_type", "line", + "_minzoom", 13 + )), process(lineFeature(Map.of( + "waterway", "stream", + "name", "name" + )))); + } + + @Test + public void testWaterwayNaturalEarth() { + assertFeatures(3, List.of(Map.of( + "class", "river", + "name", "", + "intermittent", "", + + "_layer", "waterway", + "_type", "line", + "_minzoom", 3, + "_maxzoom", 3 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "River", + "name", "name" + ), + NATURAL_EARTH_SOURCE, + "ne_110m_rivers_lake_centerlines", + 0 + ))); + + assertFeatures(6, List.of(Map.of( + "class", "river", + "intermittent", "", + + "_layer", "waterway", + "_type", "line", + "_minzoom", 4, + "_maxzoom", 5 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "River", + "name", "name" + ), + NATURAL_EARTH_SOURCE, + "ne_50m_rivers_lake_centerlines", + 0 + ))); + + assertFeatures(6, List.of(Map.of( + "class", "river", + "intermittent", "", + + "_layer", "waterway", + "_type", "line", + "_minzoom", 6, + "_maxzoom", 8 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "River", + "name", "name" + ), + NATURAL_EARTH_SOURCE, + "ne_10m_rivers_lake_centerlines", + 0 + ))); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/util/LanguageUtilsTest.java b/src/test/java/com/onthegomap/planetiler/basemap/util/LanguageUtilsTest.java new file mode 100644 index 0000000..20caacd --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/util/LanguageUtilsTest.java @@ -0,0 +1,193 @@ +package com.onthegomap.planetiler.basemap.util; + +import static com.onthegomap.planetiler.TestUtils.assertSubmap; +import static com.onthegomap.planetiler.basemap.util.LanguageUtils.containsOnlyLatinCharacters; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.onthegomap.planetiler.util.Translations; +import com.onthegomap.planetiler.util.Wikidata; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +public class LanguageUtilsTest { + + private final Wikidata.WikidataTranslations wikidataTranslations = new Wikidata.WikidataTranslations(); + private final Translations translations = Translations.defaultProvider(List.of("en", "es", "de")) + .addTranslationProvider(wikidataTranslations); + + @Test + public void testSimpleExample() { + assertSubmap(Map.of( + "name", "name", + "name_en", "english name", + "name_de", "german name" + ), LanguageUtils.getNames(Map.of( + "name", "name", + "name:en", "english name", + "name:de", "german name" + ), translations)); + + assertSubmap(Map.of( + "name", "name", + "name_en", "name", + "name_de", "german name" + ), LanguageUtils.getNames(Map.of( + "name", "name", + "name:de", "german name" + ), translations)); + + assertSubmap(Map.of( + "name", "name", + "name_en", "english name", + "name_de", "name" + ), LanguageUtils.getNames(Map.of( + "name", "name", + "name:en", "english name" + ), translations)); + } + + @ParameterizedTest + @CsvSource({ + "abc, true", + "5!, true", + "5~, true", + "é, true", + "éś, true", + "ɏə, true", + "ɐ, false", + "ᵿἀ, false", + "Ḁỿ, true", + "\u02ff\u0370, false", + "\u0030\u036f, true", + "日本, false", + "abc本123, false", + }) + public void testIsLatin(String in, boolean isLatin) { + if (!isLatin) { + assertFalse(containsOnlyLatinCharacters(in)); + } else { + assertEquals(in, LanguageUtils.getNames(Map.of( + "name", in + ), translations).get("name:latin")); + } + } + + @ParameterizedTest + @CsvSource(value = { + "abcaāíìś+, null", + "abca日āíìś+, 日+", + "(abc), null", + "日本 (Japan), 日本", + "日本 [Japan - Nippon], 日本", + " Japan - Nippon (Japan) - Japan - 日本 - Japan - Nippon (Japan), 日本", + "Japan - 日本~+ , 日本~+", + "Japan / 日本 / Japan , 日本", + }, nullValues = "null") + public void testRemoveNonLatin(String in, String out) { + assertEquals(out, LanguageUtils.getNames(Map.of( + "name", in + ), translations).get("name:nonlatin")); + } + + @ParameterizedTest + @CsvSource({ + "name, a, true", + "name:en, a, true", + "int_name, a, true", + "name:fr, a, true", + "name:es, a, true", + "name:pt, a, true", + "name:de, a, true", + "name:ar, ِغَّ, false", + "name:it, a, true", + "name:jp, ア, false", + "name:jp-Latn, a, true", + "name:jp_rm, a, true", + }) + public void testLatinFallbacks(String key, String value, boolean use) { + assertEquals(use ? value : null, LanguageUtils.getNames(Map.of( + key, value + ), translations).get("name:latin")); + } + + @ParameterizedTest + @CsvSource({ + "キャンパス, kyanpasu", + "Αλφαβητικός Κατάλογος, Alphabētikós Katálogos", + "биологическом, biologičeskom", + }) + public void testTransliterate(String in, String out) { + assertEquals(out, LanguageUtils.getNames(Map.of( + "name", in + ), translations).get("name:latin")); + translations.setShouldTransliterate(false); + assertNull(LanguageUtils.getNames(Map.of( + "name", in + ), translations).get("name:latin")); + } + + @Test + public void testUseWikidata() { + wikidataTranslations.put(123, "es", "es name"); + assertSubmap(Map.of( + "name:es", "es name" + ), LanguageUtils.getNames(Map.of( + "name", "name", + "wikidata", "Q123" + ), translations)); + } + + @Test + public void testUseOsm() { + assertSubmap(Map.of( + "name:es", "es name osm" + ), LanguageUtils.getNames(Map.of( + "name", "name", + "wikidata", "Q123", + "name:es", "es name osm" + ), translations)); + } + + @Test + public void testPreferWikidata() { + wikidataTranslations.put(123, "es", "wd es name"); + assertSubmap(Map.of( + "name:es", "wd es name", + "name:de", "de name osm" + ), LanguageUtils.getNames(Map.of( + "name", "name", + "wikidata", "Q123", + "name:es", "es name osm", + "name:de", "de name osm" + ), translations)); + } + + @Test + public void testDontUseTranslationsWhenNotSpecified() { + var result = LanguageUtils.getNamesWithoutTranslations(Map.of( + "name", "name", + "wikidata", "Q123", + "name:es", "es name osm", + "name:de", "de name osm" + )); + assertNull(result.get("name:es")); + assertNull(result.get("name:de")); + assertEquals("name", result.get("name")); + } + + @Test + public void testIgnoreLanguages() { + wikidataTranslations.put(123, "ja", "ja name wd"); + var result = LanguageUtils.getNamesWithoutTranslations(Map.of( + "name", "name", + "wikidata", "Q123", + "name:ja", "ja name osm" + )); + assertNull(result.get("name:ja")); + } +} diff --git a/src/test/java/com/onthegomap/planetiler/basemap/util/VerifyMonacoTest.java b/src/test/java/com/onthegomap/planetiler/basemap/util/VerifyMonacoTest.java new file mode 100644 index 0000000..9371783 --- /dev/null +++ b/src/test/java/com/onthegomap/planetiler/basemap/util/VerifyMonacoTest.java @@ -0,0 +1,62 @@ +package com.onthegomap.planetiler.basemap.util; + +import static com.onthegomap.planetiler.geo.GeoUtils.point; +import static com.onthegomap.planetiler.util.Gzip.gzip; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.onthegomap.planetiler.VectorTile; +import com.onthegomap.planetiler.geo.TileCoord; +import com.onthegomap.planetiler.mbtiles.Mbtiles; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class VerifyMonacoTest { + + private Mbtiles mbtiles; + + @BeforeEach + public void setup() { + mbtiles = Mbtiles.newInMemoryDatabase(); + } + + @AfterEach + public void teardown() throws IOException { + mbtiles.close(); + } + + @Test + public void testEmptyFileInvalid() { + assertInvalid(mbtiles); + } + + @Test + public void testEmptyTablesInvalid() { + mbtiles.createTables().addTileIndex(); + assertInvalid(mbtiles); + } + + @Test + public void testStilInvalidWithOneTile() throws IOException { + mbtiles.createTables().addTileIndex(); + mbtiles.metadata().setName("name"); + try (var writer = mbtiles.newBatchedTileWriter()) { + VectorTile tile = new VectorTile(); + tile.addLayerFeatures("layer", List.of(new VectorTile.Feature( + "layer", + 1, + VectorTile.encodeGeometry(point(0, 0)), + Map.of() + ))); + writer.write(TileCoord.ofXYZ(0, 0, 0), gzip(tile.encode())); + } + assertInvalid(mbtiles); + } + + private void assertInvalid(Mbtiles mbtiles) { + assertTrue(VerifyMonaco.verify(mbtiles).numErrors() > 0); + } +}