mirror of
https://github.com/cfpwastaken/planetiler-openmaptiles.git
synced 2026-02-04 04:21:08 +00:00
Change name to Planetiler (#40)
* change name from flatmap to planetiler * bump version to 0.2-SNAPSHOT
This commit is contained in:
58
README.md
Normal file
58
README.md
Normal file
@@ -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).
|
||||
57
pom.xml
Normal file
57
pom.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>planetiler-basemap</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>com.onthegomap.planetiler</groupId>
|
||||
<artifactId>planetiler-parent</artifactId>
|
||||
<version>0.2-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.onthegomap.planetiler</groupId>
|
||||
<artifactId>planetiler-core</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>1.30</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.commonmark</groupId>
|
||||
<artifactId>commonmark</artifactId>
|
||||
<version>0.18.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.onthegomap.planetiler</groupId>
|
||||
<artifactId>planetiler-core</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
<type>test-jar</type>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.github.zlika</groupId>
|
||||
<artifactId>reproducible-build-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<configuration>
|
||||
<!-- we don't want to deploy this module -->
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.onthegomap.planetiler.basemap;
|
||||
|
||||
import com.onthegomap.planetiler.Planetiler;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Main entrypoint for generating a map using the basemap schema.
|
||||
*/
|
||||
public class BasemapMain {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
run(Arguments.fromArgsOrConfigFile(args));
|
||||
}
|
||||
|
||||
static void run(Arguments arguments) throws Exception {
|
||||
Path dataDir = Path.of("data");
|
||||
Path sourcesDir = dataDir.resolve("sources");
|
||||
// use --area=... argument, AREA=... env var or area=... in config to set the region of the world to use
|
||||
// will be ignored if osm_path or osm_url are set
|
||||
String area = arguments.getString(
|
||||
"area",
|
||||
"name of the extract to download if osm_url/osm_path not specified (i.e. 'monaco' 'rhode island' 'australia' or 'planet')",
|
||||
"monaco"
|
||||
);
|
||||
|
||||
Planetiler.create(arguments)
|
||||
.setDefaultLanguages(OpenMapTilesSchema.LANGUAGES)
|
||||
.fetchWikidataNameTranslations(sourcesDir.resolve("wikidata_names.json"))
|
||||
// defer creation of the profile because it depends on data from the runner
|
||||
.setProfile(BasemapProfile::new)
|
||||
// override any of these with arguments: --osm_path=... or --osm_url=...
|
||||
// or OSM_PATH=... OSM_URL=... environmental argument
|
||||
// or osm_path=... osm_url=... in a config file
|
||||
.addShapefileSource("EPSG:3857", BasemapProfile.LAKE_CENTERLINE_SOURCE,
|
||||
sourcesDir.resolve("lake_centerline.shp.zip"),
|
||||
"https://github.com/lukasmartinelli/osm-lakelines/releases/download/v0.9/lake_centerline.shp.zip")
|
||||
.addShapefileSource(BasemapProfile.WATER_POLYGON_SOURCE,
|
||||
sourcesDir.resolve("water-polygons-split-3857.zip"),
|
||||
"https://osmdata.openstreetmap.de/download/water-polygons-split-3857.zip")
|
||||
.addNaturalEarthSource(BasemapProfile.NATURAL_EARTH_SOURCE,
|
||||
sourcesDir.resolve("natural_earth_vector.sqlite.zip"),
|
||||
"https://naciscdn.org/naturalearth/packages/natural_earth_vector.sqlite.zip")
|
||||
.addOsmSource(BasemapProfile.OSM_SOURCE,
|
||||
sourcesDir.resolve(area.replaceAll("[^a-zA-Z]+", "_") + ".osm.pbf"),
|
||||
"planet".equalsIgnoreCase(area) ? ("aws:latest") : ("geofabrik:" + area))
|
||||
// override with --mbtiles=... argument or MBTILES=... env var or mbtiles=... in a config file
|
||||
.setOutput("mbtiles", dataDir.resolve("output.mbtiles"))
|
||||
.run();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package com.onthegomap.planetiler.basemap;
|
||||
|
||||
import static com.onthegomap.planetiler.geo.GeoUtils.EMPTY_LINE;
|
||||
import static com.onthegomap.planetiler.geo.GeoUtils.EMPTY_POINT;
|
||||
import static com.onthegomap.planetiler.geo.GeoUtils.EMPTY_POLYGON;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.ForwardingProfile;
|
||||
import com.onthegomap.planetiler.Planetiler;
|
||||
import com.onthegomap.planetiler.Profile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.reader.SimpleFeature;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmElement;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Delegates the logic for generating a map to individual implementations in the {@code layers} package.
|
||||
* <p>
|
||||
* Layer implementations extend these interfaces to subscribe to elements from different sources:
|
||||
* <ul>
|
||||
* <li>{@link LakeCenterlineProcessor}</li>
|
||||
* <li>{@link NaturalEarthProcessor}</li>
|
||||
* <li>{@link OsmWaterPolygonProcessor}</li>
|
||||
* <li>{@link OsmAllProcessor} to process every OSM feature</li>
|
||||
* <li>{@link OsmRelationPreprocessor} to process every OSM relation during first pass through OSM file</li>
|
||||
* <li>A {@link Tables.RowHandler} implementation in {@code Tables.java} to process input features filtered and parsed
|
||||
* according to the imposm3 mappings defined in the OpenMapTiles schema. Each element corresponds to a row in the
|
||||
* table that imposm3 would have generated, with generated methods for accessing the data that would have been in each
|
||||
* column</li>
|
||||
* </ul>
|
||||
* Layers can also subscribe to notifications when we finished processing an input source by implementing
|
||||
* {@link FinishHandler} or post-process features in that layer before rendering the output tile by implementing
|
||||
* {@link FeaturePostProcessor}.
|
||||
*/
|
||||
public class BasemapProfile extends ForwardingProfile {
|
||||
|
||||
// IDs used in stats and logs for each input source, as well as argument/config file overrides to source locations
|
||||
public static final String LAKE_CENTERLINE_SOURCE = "lake_centerlines";
|
||||
public static final String WATER_POLYGON_SOURCE = "water_polygons";
|
||||
public static final String NATURAL_EARTH_SOURCE = "natural_earth";
|
||||
public static final String OSM_SOURCE = "osm";
|
||||
/** Index to efficiently find the imposm3 "table row" constructor from an OSM element based on its tags. */
|
||||
private final MultiExpression.Index<RowDispatch> osmMappings;
|
||||
/** Index variant that filters out any table only used by layers that implement IgnoreWikidata class. */
|
||||
private final MultiExpression.Index<Boolean> wikidataMappings;
|
||||
|
||||
public BasemapProfile(Planetiler runner) {
|
||||
this(runner.translations(), runner.config(), runner.stats());
|
||||
}
|
||||
|
||||
public BasemapProfile(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
List<String> onlyLayers = config.arguments().getList("only_layers", "Include only certain layers", List.of());
|
||||
List<String> excludeLayers = config.arguments().getList("exclude_layers", "Exclude certain layers", List.of());
|
||||
|
||||
// register release/finish/feature postprocessor/osm relationship handler methods...
|
||||
List<Handler> layers = new ArrayList<>();
|
||||
for (Layer layer : OpenMapTilesSchema.createInstances(translations, config, stats)) {
|
||||
if ((onlyLayers.isEmpty() || onlyLayers.contains(layer.name())) && !excludeLayers.contains(layer.name())) {
|
||||
layers.add(layer);
|
||||
registerHandler(layer);
|
||||
}
|
||||
}
|
||||
|
||||
// register per-source input element handlers
|
||||
for (Handler handler : layers) {
|
||||
if (handler instanceof NaturalEarthProcessor processor) {
|
||||
registerSourceHandler(NATURAL_EARTH_SOURCE,
|
||||
(source, features) -> processor.processNaturalEarth(source.getSourceLayer(), source, features));
|
||||
}
|
||||
if (handler instanceof OsmWaterPolygonProcessor processor) {
|
||||
registerSourceHandler(WATER_POLYGON_SOURCE, processor::processOsmWater);
|
||||
}
|
||||
if (handler instanceof LakeCenterlineProcessor processor) {
|
||||
registerSourceHandler(LAKE_CENTERLINE_SOURCE, processor::processLakeCenterline);
|
||||
}
|
||||
if (handler instanceof OsmAllProcessor processor) {
|
||||
registerSourceHandler(OSM_SOURCE, processor::processAllOsm);
|
||||
}
|
||||
}
|
||||
|
||||
// pre-process layers to build efficient indexes for matching OSM elements based on matching expressions
|
||||
// Map from imposm3 table row class to the layers that implement its handler.
|
||||
var handlerMap = Tables.generateDispatchMap(layers);
|
||||
osmMappings = Tables.MAPPINGS
|
||||
.mapResults(constructor -> {
|
||||
var handlers = handlerMap.getOrDefault(constructor.rowClass(), List.of()).stream()
|
||||
.map(r -> {
|
||||
@SuppressWarnings("unchecked") var handler = (Tables.RowHandler<Tables.Row>) r.handler();
|
||||
return handler;
|
||||
})
|
||||
.toList();
|
||||
return new RowDispatch(constructor.create(), handlers);
|
||||
}).simplify().index();
|
||||
wikidataMappings = Tables.MAPPINGS
|
||||
.mapResults(constructor ->
|
||||
handlerMap.getOrDefault(constructor.rowClass(), List.of()).stream()
|
||||
.anyMatch(handler -> !IgnoreWikidata.class.isAssignableFrom(handler.handlerClass()))
|
||||
).filterResults(b -> b).simplify().index();
|
||||
|
||||
// register a handler for all OSM elements that forwards to imposm3 "table row" handler methods
|
||||
// based on efficient pre-processed index
|
||||
if (!osmMappings.isEmpty()) {
|
||||
registerSourceHandler(OSM_SOURCE, (source, features) -> {
|
||||
for (var match : getTableMatches(source)) {
|
||||
RowDispatch rowDispatch = match.match();
|
||||
var row = rowDispatch.constructor.create(source, match.keys().get(0));
|
||||
for (Tables.RowHandler<Tables.Row> handler : rowDispatch.handlers()) {
|
||||
handler.process(row, features);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the imposm3 table row constructors that match an input element's tags. */
|
||||
public List<MultiExpression.Match<RowDispatch>> getTableMatches(SourceFeature input) {
|
||||
return osmMappings.getMatchesWithTriggers(input);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean caresAboutWikidataTranslation(OsmElement elem) {
|
||||
var tags = elem.tags();
|
||||
if (elem instanceof OsmElement.Node) {
|
||||
return wikidataMappings.getOrElse(SimpleFeature.create(EMPTY_POINT, tags), false);
|
||||
} else if (elem instanceof OsmElement.Way) {
|
||||
return wikidataMappings.getOrElse(SimpleFeature.create(EMPTY_POLYGON, tags), false)
|
||||
|| wikidataMappings.getOrElse(SimpleFeature.create(EMPTY_LINE, tags), false);
|
||||
} else if (elem instanceof OsmElement.Relation) {
|
||||
return wikidataMappings.getOrElse(SimpleFeature.create(EMPTY_POLYGON, tags), false);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Pass-through constants generated from the OpenMapTiles vector schema
|
||||
*/
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return OpenMapTilesSchema.NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return OpenMapTilesSchema.DESCRIPTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String attribution() {
|
||||
return OpenMapTilesSchema.ATTRIBUTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String version() {
|
||||
return OpenMapTilesSchema.VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layers should implement this interface to subscribe to elements from <a href="https://www.naturalearthdata.com/">natural
|
||||
* earth</a>.
|
||||
*/
|
||||
public interface NaturalEarthProcessor {
|
||||
|
||||
/**
|
||||
* Process an element from {@code table} in the<a href="https://www.naturalearthdata.com/">natural earth
|
||||
* source</a>.
|
||||
*
|
||||
* @see Profile#processFeature(SourceFeature, FeatureCollector)
|
||||
*/
|
||||
void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layers should implement this interface to subscribe to elements from <a href="https://github.com/lukasmartinelli/osm-lakelines">OSM
|
||||
* lake centerlines source</a>.
|
||||
*/
|
||||
public interface LakeCenterlineProcessor {
|
||||
|
||||
/**
|
||||
* Process an element from the <a href="https://github.com/lukasmartinelli/osm-lakelines">OSM lake centerlines
|
||||
* source</a>
|
||||
*
|
||||
* @see Profile#processFeature(SourceFeature, FeatureCollector)
|
||||
*/
|
||||
void processLakeCenterline(SourceFeature feature, FeatureCollector features);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layers should implement this interface to subscribe to elements from <a href="https://osmdata.openstreetmap.de/data/water-polygons.html">OSM
|
||||
* water polygons source</a>.
|
||||
*/
|
||||
public interface OsmWaterPolygonProcessor {
|
||||
|
||||
/**
|
||||
* Process an element from the <a href="https://osmdata.openstreetmap.de/data/water-polygons.html">OSM water
|
||||
* polygons source</a>
|
||||
*
|
||||
* @see Profile#processFeature(SourceFeature, FeatureCollector)
|
||||
*/
|
||||
void processOsmWater(SourceFeature feature, FeatureCollector features);
|
||||
}
|
||||
|
||||
/** Layers should implement this interface to subscribe to every OSM element. */
|
||||
public interface OsmAllProcessor {
|
||||
|
||||
/**
|
||||
* Process an OSM element during the second pass through the OSM data file.
|
||||
*
|
||||
* @see Profile#processFeature(SourceFeature, FeatureCollector)
|
||||
*/
|
||||
void processAllOsm(SourceFeature feature, FeatureCollector features);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layers should implement to indicate they do not need wikidata name translations to avoid downloading more
|
||||
* translations than are needed.
|
||||
*/
|
||||
public interface IgnoreWikidata {}
|
||||
|
||||
private static record RowDispatch(
|
||||
Tables.Constructor constructor,
|
||||
List<Tables.RowHandler<Tables.Row>> handlers
|
||||
) {}
|
||||
}
|
||||
757
src/main/java/com/onthegomap/planetiler/basemap/Generate.java
Normal file
757
src/main/java/com/onthegomap/planetiler/basemap/Generate.java
Normal file
@@ -0,0 +1,757 @@
|
||||
package com.onthegomap.planetiler.basemap;
|
||||
|
||||
import static com.onthegomap.planetiler.expression.Expression.*;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.CaseFormat;
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.Expression;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.util.Downloader;
|
||||
import com.onthegomap.planetiler.util.FileUtils;
|
||||
import com.onthegomap.planetiler.util.Format;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Stream;
|
||||
import org.commonmark.parser.Parser;
|
||||
import org.commonmark.renderer.html.HtmlRenderer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.yaml.snakeyaml.LoaderOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
/**
|
||||
* Generates code in the {@code generated} package from the OpenMapTiles schema crawled from a tag or branch in the <a
|
||||
* href="https://github.com/openmaptiles/openmaptiles">OpenMapTiles GitHub repo</a>.
|
||||
* <p>
|
||||
* {@code OpenMapTilesSchema.java} contains the output layer definitions (i.e. attributes and allowed values) so that
|
||||
* layer implementations in {@code layers} package can reference them instead of hard-coding.
|
||||
* <p>
|
||||
* {@code Tables.java} contains the <a href="https://github.com/omniscale/imposm3">imposm3</a> table definitions from
|
||||
* mapping.yaml files in the OpenMapTiles repo. Layers in the {@code layer} package can extend the {@code Handler}
|
||||
* nested class for a table definition to "subscribe" to OSM elements that imposm3 would put in that table.
|
||||
* <p>
|
||||
* To run use {@code ./scripts/regenerate-openmaptiles.sh}
|
||||
*/
|
||||
public class Generate {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(Generate.class);
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
private static final Yaml yaml;
|
||||
private static final String LINE_SEPARATOR = System.lineSeparator();
|
||||
private static final String GENERATED_FILE_HEADER = """
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
// AUTOGENERATED BY Generate.java -- DO NOT MODIFY
|
||||
""";
|
||||
private static final Parser parser = Parser.builder().build();
|
||||
private static final HtmlRenderer renderer = HtmlRenderer.builder().build();
|
||||
|
||||
static {
|
||||
// bump the default limit of 50
|
||||
var options = new LoaderOptions();
|
||||
options.setMaxAliasesForCollections(1_000);
|
||||
yaml = new Yaml(options);
|
||||
}
|
||||
|
||||
private static <T> T loadAndParseYaml(String url, PlanetilerConfig config, Class<T> clazz) throws IOException {
|
||||
LOGGER.info("reading " + url);
|
||||
try (var stream = Downloader.openStream(url, config)) {
|
||||
// Jackson yaml parsing does not handle anchors and references, so first parse the input
|
||||
// using SnakeYAML, then parse SnakeYAML's output using Jackson to get it into our records.
|
||||
Map<String, Object> parsed = yaml.load(stream);
|
||||
return mapper.convertValue(parsed, clazz);
|
||||
}
|
||||
}
|
||||
|
||||
static <T> T parseYaml(String string, Class<T> clazz) {
|
||||
// Jackson yaml parsing does not handle anchors and references, so first parse the input
|
||||
// using SnakeYAML, then parse SnakeYAML's output using Jackson to get it into our records.
|
||||
Map<String, Object> parsed = yaml.load(string);
|
||||
return mapper.convertValue(parsed, clazz);
|
||||
}
|
||||
|
||||
static JsonNode parseYaml(String string) {
|
||||
return string == null ? null : parseYaml(string, JsonNode.class);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
Arguments arguments = Arguments.fromArgsOrConfigFile(args);
|
||||
PlanetilerConfig planetilerConfig = PlanetilerConfig.from(arguments);
|
||||
String tag = arguments.getString("tag", "openmaptiles tag to use", "v3.12.2");
|
||||
String base = "https://raw.githubusercontent.com/openmaptiles/openmaptiles/" + tag + "/";
|
||||
|
||||
// start crawling from openmaptiles.yaml
|
||||
// then crawl schema from each layers/<layer>/<layer>.yaml file that it references
|
||||
// then crawl table definitions from each layers/<layer>/mapping.yaml file that the layer references
|
||||
String rootUrl = base + "openmaptiles.yaml";
|
||||
OpenmaptilesConfig config = loadAndParseYaml(rootUrl, planetilerConfig, OpenmaptilesConfig.class);
|
||||
|
||||
List<LayerConfig> layers = new ArrayList<>();
|
||||
Set<String> imposm3MappingFiles = new LinkedHashSet<>();
|
||||
for (String layerFile : config.tileset.layers) {
|
||||
String layerURL = base + layerFile;
|
||||
LayerConfig layer = loadAndParseYaml(layerURL, planetilerConfig, LayerConfig.class);
|
||||
layers.add(layer);
|
||||
for (Datasource datasource : layer.datasources) {
|
||||
if ("imposm3".equals(datasource.type)) {
|
||||
String mappingPath = Path.of(layerFile).resolveSibling(datasource.mapping_file).normalize().toString();
|
||||
imposm3MappingFiles.add(base + mappingPath);
|
||||
} else {
|
||||
LOGGER.warn("Unknown datasource type: " + datasource.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Imposm3Table> tables = new LinkedHashMap<>();
|
||||
for (String uri : imposm3MappingFiles) {
|
||||
Imposm3Mapping layer = loadAndParseYaml(uri, planetilerConfig, Imposm3Mapping.class);
|
||||
tables.putAll(layer.tables);
|
||||
}
|
||||
|
||||
String packageName = "com.onthegomap.planetiler.basemap.generated";
|
||||
String[] packageParts = packageName.split("\\.");
|
||||
Path output = Path.of("planetiler-basemap", "src", "main", "java")
|
||||
.resolve(Path.of(packageParts[0], Arrays.copyOfRange(packageParts, 1, packageParts.length)));
|
||||
|
||||
FileUtils.deleteDirectory(output);
|
||||
Files.createDirectories(output);
|
||||
|
||||
emitLayerSchemaDefinitions(config.tileset, layers, packageName, output, tag);
|
||||
emitTableDefinitions(tables, packageName, output, tag);
|
||||
LOGGER.info(
|
||||
"Done generating code in 'generated' package, now run IntelliJ 'Reformat Code' operation with 'Optimize imports' and 'Cleanup code' options selected.");
|
||||
}
|
||||
|
||||
/** Generates {@code OpenMapTilesSchema.java} */
|
||||
private static void emitLayerSchemaDefinitions(OpenmaptilesTileSet info, List<LayerConfig> layers, String packageName,
|
||||
Path output, String tag)
|
||||
throws IOException {
|
||||
StringBuilder schemaClass = new StringBuilder();
|
||||
schemaClass.append("""
|
||||
%s
|
||||
package %s;
|
||||
|
||||
import static com.onthegomap.planetiler.expression.Expression.*;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.basemap.Layer;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* All vector tile layer definitions, attributes, and allowed values generated from the
|
||||
* <a href="https://github.com/openmaptiles/openmaptiles/blob/%s/openmaptiles.yaml">OpenMapTiles vector tile schema %s</a>.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class OpenMapTilesSchema {
|
||||
public static final String NAME = %s;
|
||||
public static final String DESCRIPTION = %s;
|
||||
public static final String VERSION = %s;
|
||||
public static final String ATTRIBUTION = %s;
|
||||
public static final List<String> LANGUAGES = List.of(%s);
|
||||
|
||||
/** Returns a list of expected layer implementation instances from the {@code layers} package. */
|
||||
public static List<Layer> createInstances(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
return List.of(
|
||||
%s
|
||||
);
|
||||
}
|
||||
"""
|
||||
.formatted(
|
||||
GENERATED_FILE_HEADER,
|
||||
packageName,
|
||||
escapeJavadoc(tag),
|
||||
escapeJavadoc(tag),
|
||||
Format.quote(info.name),
|
||||
Format.quote(info.description),
|
||||
Format.quote(info.version),
|
||||
Format.quote(info.attribution),
|
||||
info.languages.stream().map(Format::quote).collect(joining(", ")),
|
||||
layers.stream()
|
||||
.map(
|
||||
l -> "new com.onthegomap.planetiler.basemap.layers.%s(translations, config, stats)"
|
||||
.formatted(lowerUnderscoreToUpperCamel(l.layer.id)))
|
||||
.collect(joining("," + LINE_SEPARATOR))
|
||||
.indent(6).trim()
|
||||
));
|
||||
for (var layer : layers) {
|
||||
String layerCode = generateCodeForLayer(tag, layer);
|
||||
schemaClass.append(layerCode);
|
||||
}
|
||||
|
||||
schemaClass.append("}");
|
||||
Files.writeString(output.resolve("OpenMapTilesSchema.java"), schemaClass);
|
||||
}
|
||||
|
||||
private static String generateCodeForLayer(String tag, LayerConfig layer) {
|
||||
String layerName = layer.layer.id;
|
||||
String className = lowerUnderscoreToUpperCamel(layerName);
|
||||
|
||||
StringBuilder fields = new StringBuilder();
|
||||
StringBuilder fieldValues = new StringBuilder();
|
||||
StringBuilder fieldMappings = new StringBuilder();
|
||||
|
||||
layer.layer.fields.forEach((name, value) -> {
|
||||
JsonNode valuesNode = value.get("values");
|
||||
List<String> valuesForComment = valuesNode == null ? List.of() : valuesNode.isArray() ?
|
||||
iterToList(valuesNode.elements()).stream().map(Objects::toString).toList() :
|
||||
iterToList(valuesNode.fieldNames());
|
||||
String javadocDescription = markdownToJavadoc(getFieldDescription(value));
|
||||
fields.append("""
|
||||
%s
|
||||
public static final String %s = %s;
|
||||
""".formatted(
|
||||
valuesForComment.isEmpty() ? "/** %s */".formatted(javadocDescription) : """
|
||||
|
||||
/**
|
||||
* %s
|
||||
* <p>
|
||||
* allowed values:
|
||||
* <ul>
|
||||
* %s
|
||||
* </ul>
|
||||
*/
|
||||
""".stripTrailing().formatted(javadocDescription,
|
||||
valuesForComment.stream().map(v -> "<li>" + v).collect(joining(LINE_SEPARATOR + " * "))),
|
||||
name.toUpperCase(Locale.ROOT),
|
||||
Format.quote(name)
|
||||
).indent(4));
|
||||
|
||||
List<String> values = valuesNode == null ? List.of() : valuesNode.isArray() ?
|
||||
iterToList(valuesNode.elements()).stream().filter(JsonNode::isTextual).map(JsonNode::textValue)
|
||||
.map(t -> t.replaceAll(" .*", "")).toList() :
|
||||
iterToList(valuesNode.fieldNames());
|
||||
if (values.size() > 0) {
|
||||
fieldValues.append(values.stream()
|
||||
.map(v -> "public static final String %s = %s;"
|
||||
.formatted(name.toUpperCase(Locale.ROOT) + "_" + v.toUpperCase(Locale.ROOT).replace('-', '_'),
|
||||
Format.quote(v)))
|
||||
.collect(joining(LINE_SEPARATOR)).indent(2).strip()
|
||||
.indent(4));
|
||||
fieldValues.append("public static final Set<String> %s = Set.of(%s);".formatted(
|
||||
name.toUpperCase(Locale.ROOT) + "_VALUES",
|
||||
values.stream().map(Format::quote).collect(joining(", "))
|
||||
).indent(4));
|
||||
}
|
||||
|
||||
if (valuesNode != null && valuesNode.isObject()) {
|
||||
MultiExpression<String> mapping = generateFieldMapping(valuesNode);
|
||||
fieldMappings.append(" public static final MultiExpression<String> %s = %s;%n"
|
||||
.formatted(lowerUnderscoreToUpperCamel(name), generateJavaCode(mapping)));
|
||||
}
|
||||
});
|
||||
|
||||
return """
|
||||
/**
|
||||
* %s
|
||||
*
|
||||
* Generated from <a href="https://github.com/openmaptiles/openmaptiles/blob/%s/layers/%s/%s.yaml">%s.yaml</a>
|
||||
*/
|
||||
public interface %s extends Layer {
|
||||
double BUFFER_SIZE = %s;
|
||||
String LAYER_NAME = %s;
|
||||
@Override
|
||||
default String name() {
|
||||
return LAYER_NAME;
|
||||
}
|
||||
/** Attribute names for map elements in the %s layer. */
|
||||
final class Fields {
|
||||
%s
|
||||
}
|
||||
/** Attribute values for map elements in the %s layer. */
|
||||
final class FieldValues {
|
||||
%s
|
||||
}
|
||||
/** Complex mappings to generate attribute values from OSM element tags in the %s layer. */
|
||||
final class FieldMappings {
|
||||
%s
|
||||
}
|
||||
}
|
||||
""".formatted(
|
||||
markdownToJavadoc(layer.layer.description),
|
||||
escapeJavadoc(tag),
|
||||
escapeJavadoc(layerName),
|
||||
escapeJavadoc(layerName),
|
||||
escapeJavadoc(layerName),
|
||||
className,
|
||||
layer.layer.buffer_size,
|
||||
Format.quote(layerName),
|
||||
escapeJavadoc(layerName),
|
||||
fields.toString().strip(),
|
||||
escapeJavadoc(layerName),
|
||||
fieldValues.toString().strip(),
|
||||
escapeJavadoc(layerName),
|
||||
fieldMappings.toString().strip()
|
||||
).indent(2);
|
||||
}
|
||||
|
||||
/** Generates {@code Tables.java} */
|
||||
private static void emitTableDefinitions(Map<String, Imposm3Table> tables, String packageName, Path output,
|
||||
String tag)
|
||||
throws IOException {
|
||||
StringBuilder tablesClass = new StringBuilder();
|
||||
tablesClass.append("""
|
||||
%s
|
||||
package %s;
|
||||
|
||||
import static com.onthegomap.planetiler.expression.Expression.*;
|
||||
|
||||
import com.onthegomap.planetiler.expression.Expression;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* OSM element parsers generated from the <a href="https://github.com/omniscale/imposm3">imposm3</a> table definitions
|
||||
* in the <a href="https://github.com/openmaptiles/openmaptiles/blob/%s/openmaptiles.yaml">OpenMapTiles vector tile schema</a>.
|
||||
*
|
||||
* These filter and parse the raw OSM key/value attribute pairs on tags into records with fields that match the
|
||||
* columns in the tables that imposm3 would generate. Layer implementations can "subscribe" to elements from each
|
||||
* "table" but implementing the table's {@code Handler} interface and use the element's typed API to access
|
||||
* attributes.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class Tables {
|
||||
/** A parsed OSM element that would appear in a "row" of the imposm3 table. */
|
||||
public interface Row {
|
||||
|
||||
/** Returns the original OSM element. */
|
||||
SourceFeature source();
|
||||
}
|
||||
|
||||
/** A functional interface that the constructor of a new table row can be coerced to. */
|
||||
@FunctionalInterface
|
||||
public interface Constructor {
|
||||
|
||||
Row create(SourceFeature source, String mappingKey);
|
||||
}
|
||||
|
||||
/** The {@code rowClass} of an imposm3 table row and its constructor coerced to a {@link Constructor}. */
|
||||
public static record RowClassAndConstructor(
|
||||
Class<? extends Row> rowClass,
|
||||
Constructor create
|
||||
) {}
|
||||
|
||||
/** A functional interface that the typed handler method that a layer implementation can be coerced to. */
|
||||
@FunctionalInterface
|
||||
public interface RowHandler<T extends Row> {
|
||||
|
||||
/** Process a typed element according to the profile. */
|
||||
void process(T element, FeatureCollector features);
|
||||
}
|
||||
|
||||
/** The {@code handlerClass} of a layer handler and it's {@code process} method coerced to a {@link RowHandler}. */
|
||||
public static record RowHandlerAndClass<T extends Row>(
|
||||
Class<?> handlerClass,
|
||||
RowHandler<T> handler
|
||||
) {}
|
||||
""".formatted(GENERATED_FILE_HEADER, packageName, escapeJavadoc(tag)));
|
||||
|
||||
List<String> classNames = new ArrayList<>();
|
||||
Map<String, String> fieldNameToType = new TreeMap<>();
|
||||
for (var entry : tables.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Imposm3Table table = entry.getValue();
|
||||
List<OsmTableField> fields = parseTableFields(table);
|
||||
for (var field : fields) {
|
||||
String existing = fieldNameToType.get(field.name);
|
||||
if (existing == null) {
|
||||
fieldNameToType.put(field.name, field.clazz);
|
||||
} else if (!existing.equals(field.clazz)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Field " + field.name + " has both " + existing + " and " + field.clazz + " types");
|
||||
}
|
||||
}
|
||||
Expression mappingExpression = parseImposm3MappingExpression(table);
|
||||
String mapping = """
|
||||
/** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */
|
||||
public static final Expression MAPPING = %s;
|
||||
""".formatted(
|
||||
mappingExpression
|
||||
);
|
||||
String tableName = "osm_" + key;
|
||||
String className = lowerUnderscoreToUpperCamel(tableName);
|
||||
if (!"relation_member".equals(table.type)) {
|
||||
classNames.add(className);
|
||||
|
||||
tablesClass.append("""
|
||||
/** An OSM element that would appear in the {@code %s} table generated by imposm3. */
|
||||
public static record %s(%s) implements Row, %s {
|
||||
public %s(SourceFeature source, String mappingKey) {
|
||||
this(%s);
|
||||
}
|
||||
%s
|
||||
/**
|
||||
* Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as
|
||||
* {@link %s}.
|
||||
*/
|
||||
public interface Handler {
|
||||
void process(%s element, FeatureCollector features);
|
||||
}
|
||||
}
|
||||
""".formatted(
|
||||
tableName,
|
||||
escapeJavadoc(className),
|
||||
fields.stream().map(c -> "@Override " + c.clazz + " " + lowerUnderscoreToLowerCamel(c.name))
|
||||
.collect(joining(", ")),
|
||||
fields.stream().map(c -> lowerUnderscoreToUpperCamel("with_" + c.name))
|
||||
.collect(joining(", ")),
|
||||
className,
|
||||
fields.stream().map(c -> c.extractCode).collect(joining(", ")),
|
||||
mapping,
|
||||
escapeJavadoc(className),
|
||||
className
|
||||
).indent(2));
|
||||
}
|
||||
}
|
||||
|
||||
tablesClass.append(fieldNameToType.entrySet().stream().map(e -> {
|
||||
String attrName = lowerUnderscoreToLowerCamel(e.getKey());
|
||||
String type = e.getValue();
|
||||
String interfaceName = lowerUnderscoreToUpperCamel("with_" + e.getKey());
|
||||
return """
|
||||
/** Rows with a %s %s attribute. */
|
||||
public interface %s {
|
||||
%s %s();
|
||||
}
|
||||
""".formatted(
|
||||
escapeJavadoc(type),
|
||||
escapeJavadoc(attrName),
|
||||
interfaceName,
|
||||
type,
|
||||
attrName);
|
||||
}).collect(joining(LINE_SEPARATOR)).indent(2));
|
||||
|
||||
tablesClass.append("""
|
||||
/** Index to efficiently choose which imposm3 "tables" an element should appear in based on its attributes. */
|
||||
public static final MultiExpression<RowClassAndConstructor> MAPPINGS = MultiExpression.of(List.of(
|
||||
%s
|
||||
));
|
||||
""".formatted(
|
||||
classNames.stream().map(
|
||||
className -> "MultiExpression.entry(new RowClassAndConstructor(%s.class, %s::new), %s.MAPPING)".formatted(
|
||||
className, className, className))
|
||||
.collect(joining("," + LINE_SEPARATOR)).indent(2).strip()
|
||||
).indent(2));
|
||||
|
||||
String handlerCondition = classNames.stream().map(className ->
|
||||
"""
|
||||
if (handler instanceof %s.Handler typedHandler) {
|
||||
result.computeIfAbsent(%s.class, cls -> new ArrayList<>()).add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process));
|
||||
}""".formatted(className, className)
|
||||
).collect(joining(LINE_SEPARATOR));
|
||||
tablesClass.append("""
|
||||
/**
|
||||
* Returns a map from imposm3 "table row" class to the layers that have a handler for it from a list of layer
|
||||
* implementations.
|
||||
*/
|
||||
public static Map<Class<? extends Row>, List<RowHandlerAndClass<?>>> generateDispatchMap(List<?> handlers) {
|
||||
Map<Class<? extends Row>, List<RowHandlerAndClass<?>>> result = new HashMap<>();
|
||||
for (var handler : handlers) {
|
||||
%s
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
""".formatted(handlerCondition.indent(6).trim()));
|
||||
Files.writeString(output.resolve("Tables.java"), tablesClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link Expression} that implements the same logic as the <a href="https://imposm.org/docs/imposm3/latest/mapping.html">Imposm3
|
||||
* Data Mapping</a> definition for a table.
|
||||
*/
|
||||
static Expression parseImposm3MappingExpression(Imposm3Table table) {
|
||||
if (table.type_mappings != null) {
|
||||
return or(
|
||||
table.type_mappings.entrySet().stream().map(entry ->
|
||||
parseImposm3MappingExpression(entry.getKey(), entry.getValue(), table.filters)
|
||||
).toList()
|
||||
).simplify();
|
||||
} else {
|
||||
return parseImposm3MappingExpression(table.type, table.mapping, table.filters);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link Expression} that implements the same logic as the <a href="https://imposm.org/docs/imposm3/latest/mapping.html#filters">Imposm3
|
||||
* Data Mapping filters</a> for a table.
|
||||
*/
|
||||
static Expression parseImposm3MappingExpression(String type, JsonNode mapping, Imposm3Filters filters) {
|
||||
return and(
|
||||
or(parseFieldMappingExpression(mapping).toList()),
|
||||
and(
|
||||
filters == null || filters.require == null ? List.of() : parseFieldMappingExpression(filters.require).toList()),
|
||||
not(or(
|
||||
filters == null || filters.reject == null ? List.of() : parseFieldMappingExpression(filters.reject).toList())),
|
||||
matchType(type.replaceAll("s$", ""))
|
||||
).simplify();
|
||||
}
|
||||
|
||||
private static List<OsmTableField> parseTableFields(Imposm3Table tableDefinition) {
|
||||
List<OsmTableField> result = new ArrayList<>();
|
||||
boolean relationMember = "relation_member".equals(tableDefinition.type);
|
||||
for (Imposm3Column col : tableDefinition.columns) {
|
||||
if (relationMember && col.from_member) {
|
||||
// layers process relation info that they need manually
|
||||
continue;
|
||||
}
|
||||
switch (col.type) {
|
||||
case "id", "validated_geometry", "area", "hstore_tags", "geometry" -> {
|
||||
// do nothing - already on source feature
|
||||
}
|
||||
case "member_id", "member_role", "member_type", "member_index" -> {
|
||||
// do nothing
|
||||
}
|
||||
case "mapping_key" -> result
|
||||
.add(new OsmTableField("String", col.name, "mappingKey"));
|
||||
case "mapping_value" -> result
|
||||
.add(new OsmTableField("String", col.name, "source.getString(mappingKey)"));
|
||||
case "string" -> result
|
||||
.add(new OsmTableField("String", col.name,
|
||||
"source.getString(\"%s\")".formatted(Objects.requireNonNull(col.key, col.toString()))));
|
||||
case "bool" -> result
|
||||
.add(new OsmTableField("boolean", col.name,
|
||||
"source.getBoolean(\"%s\")".formatted(Objects.requireNonNull(col.key, col.toString()))));
|
||||
case "integer" -> result
|
||||
.add(new OsmTableField("long", col.name,
|
||||
"source.getLong(\"%s\")".formatted(Objects.requireNonNull(col.key, col.toString()))));
|
||||
case "wayzorder" -> result.add(new OsmTableField("int", col.name, "source.getWayZorder()"));
|
||||
case "direction" -> result.add(new OsmTableField("int", col.name,
|
||||
"source.getDirection(\"%s\")".formatted(Objects.requireNonNull(col.key, col.toString()))));
|
||||
default -> throw new IllegalArgumentException("Unhandled column: " + col.type);
|
||||
}
|
||||
}
|
||||
result.add(new OsmTableField("SourceFeature", "source", "source"));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link MultiExpression} to efficiently determine the value for an output vector tile feature (i.e.
|
||||
* "class") based on the "field mapping" defined in the layer schema definition.
|
||||
*/
|
||||
static MultiExpression<String> generateFieldMapping(JsonNode valuesNode) {
|
||||
MultiExpression<String> mapping = MultiExpression.of(new ArrayList<>());
|
||||
valuesNode.fields().forEachRemaining(entry -> {
|
||||
String field = entry.getKey();
|
||||
JsonNode node = entry.getValue();
|
||||
Expression expression = or(parseFieldMappingExpression(node).toList()).simplify();
|
||||
if (!expression.equals(or()) && !expression.equals(and())) {
|
||||
mapping.expressions().add(MultiExpression.entry(field, expression));
|
||||
}
|
||||
});
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private static Stream<Expression> parseFieldMappingExpression(JsonNode node) {
|
||||
if (node.isObject()) {
|
||||
List<String> keys = iterToList(node.fieldNames());
|
||||
if (keys.contains("__AND__")) {
|
||||
if (keys.size() > 1) {
|
||||
throw new IllegalArgumentException("Cannot combine __AND__ with others");
|
||||
}
|
||||
return Stream.of(and(parseFieldMappingExpression(node.get("__AND__")).toList()));
|
||||
} else if (keys.contains("__OR__")) {
|
||||
if (keys.size() > 1) {
|
||||
throw new IllegalArgumentException("Cannot combine __OR__ with others");
|
||||
}
|
||||
return Stream.of(or(parseFieldMappingExpression(node.get("__OR__")).toList()));
|
||||
} else {
|
||||
return iterToList(node.fields()).stream().map(entry -> {
|
||||
String field = entry.getKey();
|
||||
List<String> value = toFlatList(entry.getValue()).map(JsonNode::textValue).filter(Objects::nonNull).toList();
|
||||
return value.isEmpty() || value.contains("__any__") ? matchField(field) : matchAny(field, value);
|
||||
});
|
||||
}
|
||||
} else if (node.isArray()) {
|
||||
return iterToList(node.elements()).stream().flatMap(Generate::parseFieldMappingExpression);
|
||||
} else if (node.isNull()) {
|
||||
return Stream.empty();
|
||||
} else {
|
||||
throw new IllegalArgumentException("parseExpression input not handled: " + node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a flattened list of all the elements in nested arrays from {@code node}.
|
||||
* <p>
|
||||
* For example: {@code [[[a, b], c], [d]} becomes {@code [a, b, c, d]}
|
||||
* <p>
|
||||
* And {@code a} becomes {@code [a]}
|
||||
*/
|
||||
private static Stream<JsonNode> toFlatList(JsonNode node) {
|
||||
return node.isArray() ? iterToList(node.elements()).stream().flatMap(Generate::toFlatList) : Stream.of(node);
|
||||
}
|
||||
|
||||
/** Returns java code that will recreate an {@link MultiExpression} identical to {@code mapping}. */
|
||||
private static String generateJavaCode(MultiExpression<String> mapping) {
|
||||
return "MultiExpression.of(List.of(" + mapping.expressions().stream()
|
||||
.map(s -> "MultiExpression.entry(%s, %s)".formatted(Format.quote(s.result()), s.expression()))
|
||||
.collect(joining(", ")) + "))";
|
||||
}
|
||||
|
||||
private static String lowerUnderscoreToLowerCamel(String name) {
|
||||
return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, name);
|
||||
}
|
||||
|
||||
private static String lowerUnderscoreToUpperCamel(String name) {
|
||||
return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, name);
|
||||
}
|
||||
|
||||
private static <T> List<T> iterToList(Iterator<T> iter) {
|
||||
List<T> result = new ArrayList<>();
|
||||
iter.forEachRemaining(result::add);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Renders {@code markdown} as HTML and returns comment text safe to insert in generated javadoc. */
|
||||
private static String markdownToJavadoc(String markdown) {
|
||||
return Stream.of(markdown.strip().split("[\r\n][\r\n]+"))
|
||||
.map(p -> parser.parse(p.strip()))
|
||||
.map(node -> escapeJavadoc(renderer.render(node)))
|
||||
.map(p -> p.replaceAll("(^<p>|</p>$)", "").strip())
|
||||
.collect(joining(LINE_SEPARATOR + "<p>" + LINE_SEPARATOR));
|
||||
}
|
||||
|
||||
/** Returns {@code comment} text safe to insert in generated javadoc. */
|
||||
private static String escapeJavadoc(String comment) {
|
||||
return comment.strip().replaceAll("[\n\r*\\s]+", " ");
|
||||
}
|
||||
|
||||
private static String getFieldDescription(JsonNode value) {
|
||||
if (value.isTextual()) {
|
||||
return value.textValue();
|
||||
} else {
|
||||
return value.get("description").textValue();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Models for deserializing yaml into:
|
||||
*/
|
||||
|
||||
private static record OpenmaptilesConfig(
|
||||
OpenmaptilesTileSet tileset
|
||||
) {}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static record OpenmaptilesTileSet(
|
||||
List<String> layers,
|
||||
String version,
|
||||
String attribution,
|
||||
String name,
|
||||
String description,
|
||||
List<String> languages
|
||||
) {}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static record LayerDetails(
|
||||
String id,
|
||||
String description,
|
||||
Map<String, JsonNode> fields,
|
||||
double buffer_size
|
||||
) {}
|
||||
|
||||
private static record Datasource(
|
||||
String type,
|
||||
String mapping_file
|
||||
) {}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static record LayerConfig(
|
||||
LayerDetails layer,
|
||||
List<Datasource> datasources
|
||||
) {}
|
||||
|
||||
private static record Imposm3Column(
|
||||
String type,
|
||||
String name,
|
||||
String key,
|
||||
boolean from_member
|
||||
) {}
|
||||
|
||||
static record Imposm3Filters(
|
||||
JsonNode reject,
|
||||
JsonNode require
|
||||
) {}
|
||||
|
||||
static record Imposm3Table(
|
||||
String type,
|
||||
@JsonProperty("_resolve_wikidata") boolean resolveWikidata,
|
||||
List<Imposm3Column> columns,
|
||||
Imposm3Filters filters,
|
||||
JsonNode mapping,
|
||||
Map<String, JsonNode> type_mappings
|
||||
) {}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static record Imposm3Mapping(
|
||||
Map<String, Imposm3Table> tables
|
||||
) {}
|
||||
|
||||
private static record OsmTableField(
|
||||
String clazz,
|
||||
String name,
|
||||
String extractCode
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.onthegomap.planetiler.basemap;
|
||||
|
||||
import com.onthegomap.planetiler.ForwardingProfile;
|
||||
|
||||
/** Interface for all vector tile layer implementations that {@link BasemapProfile} delegates to. */
|
||||
public interface Layer extends
|
||||
ForwardingProfile.Handler,
|
||||
ForwardingProfile.HandlerForLayer {}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.basemap.util.Utils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements in the {@code aerodrome_label} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/aerodrome_label">OpenMapTiles
|
||||
* aerodrome_layer sql files</a>.
|
||||
*/
|
||||
public class AerodromeLabel implements
|
||||
OpenMapTilesSchema.AerodromeLabel,
|
||||
Tables.OsmAerodromeLabelPoint.Handler {
|
||||
|
||||
private final MultiExpression.Index<String> classLookup;
|
||||
private final Translations translations;
|
||||
|
||||
public AerodromeLabel(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.classLookup = FieldMappings.Class.index();
|
||||
this.translations = translations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmAerodromeLabelPoint element, FeatureCollector features) {
|
||||
features.centroid(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setMinZoom(10)
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.putAttrs(Utils.elevationTags(element.ele()))
|
||||
.setAttr(Fields.IATA, nullIfEmpty(element.iata()))
|
||||
.setAttr(Fields.ICAO, nullIfEmpty(element.icao()))
|
||||
.setAttr(Fields.CLASS, classLookup.getOrElse(element.source(), FieldValues.CLASS_OTHER));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements in the {@code aeroway} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/aeroway">OpenMapTiles
|
||||
* aeroway sql files</a>.
|
||||
*/
|
||||
public class Aeroway implements
|
||||
OpenMapTilesSchema.Aeroway,
|
||||
Tables.OsmAerowayLinestring.Handler,
|
||||
Tables.OsmAerowayPolygon.Handler,
|
||||
Tables.OsmAerowayPoint.Handler {
|
||||
|
||||
public Aeroway(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmAerowayPolygon element, FeatureCollector features) {
|
||||
features.polygon(LAYER_NAME)
|
||||
.setMinZoom(10)
|
||||
.setMinPixelSize(2)
|
||||
.setAttr(Fields.CLASS, element.aeroway())
|
||||
.setAttr(Fields.REF, element.ref());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmAerowayLinestring element, FeatureCollector features) {
|
||||
features.line(LAYER_NAME)
|
||||
.setMinZoom(10)
|
||||
.setAttr(Fields.CLASS, element.aeroway())
|
||||
.setAttr(Fields.REF, element.ref());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmAerowayPoint element, FeatureCollector features) {
|
||||
features.point(LAYER_NAME)
|
||||
.setMinZoom(14)
|
||||
.setAttr(Fields.CLASS, element.aeroway())
|
||||
.setAttr(Fields.REF, element.ref());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.util.MemoryEstimator.CLASS_HEADER_BYTES;
|
||||
import static com.onthegomap.planetiler.util.MemoryEstimator.POINTER_BYTES;
|
||||
import static com.onthegomap.planetiler.util.MemoryEstimator.estimateSize;
|
||||
import static java.util.stream.Collectors.counting;
|
||||
import static java.util.stream.Collectors.groupingBy;
|
||||
|
||||
import com.carrotsearch.hppc.LongObjectMap;
|
||||
import com.graphhopper.coll.GHLongObjectHashMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.reader.SimpleFeature;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmElement;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Format;
|
||||
import com.onthegomap.planetiler.util.MemoryEstimator;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.TopologyException;
|
||||
import org.locationtech.jts.geom.prep.PreparedGeometry;
|
||||
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
|
||||
import org.locationtech.jts.operation.linemerge.LineMerger;
|
||||
import org.locationtech.jts.operation.polygonize.Polygonizer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for country, state, and town boundaries in the {@code boundary} layer
|
||||
* from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/boundary">OpenMapTiles
|
||||
* boundary sql files</a>.
|
||||
*/
|
||||
public class Boundary implements
|
||||
OpenMapTilesSchema.Boundary,
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
BasemapProfile.OsmRelationPreprocessor,
|
||||
BasemapProfile.OsmAllProcessor,
|
||||
BasemapProfile.FeaturePostProcessor,
|
||||
BasemapProfile.FinishHandler {
|
||||
|
||||
/*
|
||||
* Uses natural earth at lower zoom levels and OpenStreetMap at higher zoom levels.
|
||||
*
|
||||
* For OpenStreetMap data at higher zoom levels:
|
||||
* 1) Preprocess relations on the first pass to extract info for relations where
|
||||
* type=boundary and boundary=administrative and store the admin_level for
|
||||
* later.
|
||||
* 2) When processing individual ways, take the minimum (most important) admin
|
||||
* level of every relation they are a part of and use that as the admin level
|
||||
* for the way.
|
||||
* 3) If boundary_country_names argument is true and the way is part of a country
|
||||
* (admin_level=2) boundary, then hold onto it for later
|
||||
* 4) When we finish processing the OSM source, build country polygons from the
|
||||
* saved ways and use that to determine which country is on the left and right
|
||||
* side of each way, then emit the way with ADM0_L and ADM0_R keys set.
|
||||
* 5) Before emitting boundary lines, merge linestrings with the same tags.
|
||||
*/
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(Boundary.class);
|
||||
private static final double COUNTRY_TEST_OFFSET = GeoUtils.metersToPixelAtEquator(0, 10) / 256d;
|
||||
private final Stats stats;
|
||||
private final boolean addCountryNames;
|
||||
// may be updated concurrently by multiple threads
|
||||
private final Map<Long, String> regionNames = new ConcurrentHashMap<>();
|
||||
// need to synchronize updates to these shared data structures:
|
||||
private final Map<Long, List<Geometry>> regionGeometries = new HashMap<>();
|
||||
private final Map<CountryBoundaryComponent, List<Geometry>> boundariesToMerge = new HashMap<>();
|
||||
private final PlanetilerConfig config;
|
||||
|
||||
public Boundary(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.config = config;
|
||||
this.addCountryNames = config.arguments().getBoolean(
|
||||
"boundary_country_names",
|
||||
"boundary layer: add left/right codes of neighboring countries",
|
||||
true
|
||||
);
|
||||
this.stats = stats;
|
||||
}
|
||||
|
||||
private static boolean isDisputed(Map<String, Object> tags) {
|
||||
return Parse.bool(tags.get("disputed")) ||
|
||||
Parse.bool(tags.get("dispute")) ||
|
||||
"dispute".equals(tags.get("border_status")) ||
|
||||
tags.containsKey("disputed_by") ||
|
||||
tags.containsKey("claimed_by");
|
||||
}
|
||||
|
||||
private static String editName(String name) {
|
||||
return name == null ? null : name.replace(" at ", "")
|
||||
.replaceAll("\\s+", "")
|
||||
.replace("Extentof", "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
regionGeometries.clear();
|
||||
boundariesToMerge.clear();
|
||||
regionNames.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) {
|
||||
boolean disputed = feature.getString("featurecla", "").startsWith("Disputed");
|
||||
record BoundaryInfo(int adminLevel, int minzoom, int maxzoom) {}
|
||||
BoundaryInfo info = switch (table) {
|
||||
case "ne_110m_admin_0_boundary_lines_land" -> new BoundaryInfo(2, 0, 0);
|
||||
case "ne_50m_admin_0_boundary_lines_land" -> new BoundaryInfo(2, 1, 3);
|
||||
case "ne_10m_admin_0_boundary_lines_land" -> feature.hasTag("featurecla", "Lease Limit") ? null
|
||||
: new BoundaryInfo(2, 4, 4);
|
||||
case "ne_10m_admin_1_states_provinces_lines" -> {
|
||||
Double minZoom = Parse.parseDoubleOrNull(feature.getTag("min_zoom"));
|
||||
yield minZoom != null && minZoom <= 7 ? new BoundaryInfo(4, 1, 4) : null;
|
||||
}
|
||||
default -> null;
|
||||
};
|
||||
if (info != null) {
|
||||
features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setZoomRange(info.minzoom, info.maxzoom)
|
||||
.setMinPixelSizeAtAllZooms(0)
|
||||
.setAttr(Fields.ADMIN_LEVEL, info.adminLevel)
|
||||
.setAttr(Fields.MARITIME, 0)
|
||||
.setAttr(Fields.DISPUTED, disputed ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OsmRelationInfo> preprocessOsmRelation(OsmElement.Relation relation) {
|
||||
if (relation.hasTag("type", "boundary") &&
|
||||
relation.hasTag("admin_level") &&
|
||||
relation.hasTag("boundary", "administrative")) {
|
||||
Integer adminLevelValue = Parse.parseRoundInt(relation.getTag("admin_level"));
|
||||
String code = relation.getString("ISO3166-1:alpha3");
|
||||
if (adminLevelValue != null && adminLevelValue >= 2 && adminLevelValue <= 10) {
|
||||
boolean disputed = isDisputed(relation.tags());
|
||||
if (code != null) {
|
||||
regionNames.put(relation.id(), code);
|
||||
}
|
||||
return List.of(new BoundaryRelation(
|
||||
relation.id(),
|
||||
adminLevelValue,
|
||||
disputed,
|
||||
relation.getString("name"),
|
||||
disputed ? relation.getString("claimed_by") : null,
|
||||
code
|
||||
));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processAllOsm(SourceFeature feature, FeatureCollector features) {
|
||||
if (!feature.canBeLine()) {
|
||||
return;
|
||||
}
|
||||
var relationInfos = feature.relationInfo(BoundaryRelation.class);
|
||||
if (!relationInfos.isEmpty()) {
|
||||
int minAdminLevel = Integer.MAX_VALUE;
|
||||
String disputedName = null, claimedBy = null;
|
||||
Set<Long> regionIds = new HashSet<>();
|
||||
boolean disputed = false;
|
||||
// aggregate all borders this way is a part of - take the lowest
|
||||
// admin level, and assume it is disputed if any relation is disputed.
|
||||
for (var info : relationInfos) {
|
||||
BoundaryRelation rel = info.relation();
|
||||
disputed |= rel.disputed;
|
||||
if (rel.adminLevel < minAdminLevel) {
|
||||
minAdminLevel = rel.adminLevel;
|
||||
}
|
||||
if (rel.disputed) {
|
||||
disputedName = disputedName == null ? rel.name : disputedName;
|
||||
claimedBy = claimedBy == null ? rel.claimedBy : claimedBy;
|
||||
}
|
||||
if (minAdminLevel == 2 && regionNames.containsKey(info.relation().id)) {
|
||||
regionIds.add(info.relation().id);
|
||||
}
|
||||
}
|
||||
|
||||
if (minAdminLevel <= 10) {
|
||||
boolean wayIsDisputed = isDisputed(feature.tags());
|
||||
disputed |= wayIsDisputed;
|
||||
if (wayIsDisputed) {
|
||||
disputedName = disputedName == null ? feature.getString("name") : disputedName;
|
||||
claimedBy = claimedBy == null ? feature.getString("claimed_by") : claimedBy;
|
||||
}
|
||||
boolean maritime = feature.getBoolean("maritime") ||
|
||||
feature.hasTag("natural", "coastline") ||
|
||||
feature.hasTag("boundary_type", "maritime");
|
||||
int minzoom =
|
||||
(maritime && minAdminLevel == 2) ? 4 :
|
||||
minAdminLevel <= 4 ? 5 :
|
||||
minAdminLevel <= 6 ? 9 :
|
||||
minAdminLevel <= 8 ? 11 : 12;
|
||||
if (addCountryNames && !regionIds.isEmpty()) {
|
||||
// save for later
|
||||
try {
|
||||
CountryBoundaryComponent component = new CountryBoundaryComponent(
|
||||
minAdminLevel,
|
||||
disputed,
|
||||
maritime,
|
||||
minzoom,
|
||||
feature.line(),
|
||||
regionIds,
|
||||
claimedBy,
|
||||
disputedName
|
||||
);
|
||||
// multiple threads may update this concurrently
|
||||
synchronized (this) {
|
||||
boundariesToMerge.computeIfAbsent(component.groupingKey(), key -> new ArrayList<>()).add(component.line);
|
||||
for (var info : relationInfos) {
|
||||
var rel = info.relation();
|
||||
if (rel.adminLevel <= 2) {
|
||||
regionGeometries.computeIfAbsent(rel.id, id -> new ArrayList<>()).add(component.line);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
LOGGER.warn("Cannot extract boundary line from " + feature);
|
||||
}
|
||||
} else {
|
||||
features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.ADMIN_LEVEL, minAdminLevel)
|
||||
.setAttr(Fields.DISPUTED, disputed ? 1 : 0)
|
||||
.setAttr(Fields.MARITIME, maritime ? 1 : 0)
|
||||
.setMinPixelSizeAtAllZooms(0)
|
||||
.setMinZoom(minzoom)
|
||||
.setAttr(Fields.CLAIMED_BY, claimedBy)
|
||||
.setAttr(Fields.DISPUTED_NAME, editName(disputedName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish(String sourceName, FeatureCollector.Factory featureCollectors,
|
||||
Consumer<FeatureCollector.Feature> emit) {
|
||||
if (BasemapProfile.OSM_SOURCE.equals(sourceName)) {
|
||||
var timer = stats.startStage("boundaries");
|
||||
LongObjectMap<PreparedGeometry> countryBoundaries = prepareRegionPolygons();
|
||||
|
||||
for (var entry : boundariesToMerge.entrySet()) {
|
||||
CountryBoundaryComponent key = entry.getKey();
|
||||
LineMerger merger = new LineMerger();
|
||||
for (Geometry geom : entry.getValue()) {
|
||||
merger.add(geom);
|
||||
}
|
||||
entry.getValue().clear();
|
||||
for (Object merged : merger.getMergedLineStrings()) {
|
||||
if (merged instanceof LineString lineString) {
|
||||
BorderingRegions borderingRegions = getBorderingRegions(countryBoundaries, key.regions, lineString);
|
||||
|
||||
var features = featureCollectors.get(SimpleFeature.fromWorldGeometry(lineString));
|
||||
features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.ADMIN_LEVEL, key.adminLevel)
|
||||
.setAttr(Fields.DISPUTED, key.disputed ? 1 : 0)
|
||||
.setAttr(Fields.MARITIME, key.maritime ? 1 : 0)
|
||||
.setAttr(Fields.CLAIMED_BY, key.claimedBy)
|
||||
.setAttr(Fields.DISPUTED_NAME, key.disputed ? editName(key.name) : null)
|
||||
.setAttr(Fields.ADM0_L, borderingRegions.left == null ? null : regionNames.get(borderingRegions.left))
|
||||
.setAttr(Fields.ADM0_R, borderingRegions.right == null ? null : regionNames.get(borderingRegions.right))
|
||||
.setMinPixelSizeAtAllZooms(0)
|
||||
.setMinZoom(key.minzoom);
|
||||
for (var feature : features) {
|
||||
emit.accept(feature);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
timer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
double minLength = config.minFeatureSize(zoom);
|
||||
double tolerance = config.tolerance(zoom);
|
||||
return FeatureMerge.mergeLineStrings(items, attrs -> minLength, tolerance, BUFFER_SIZE);
|
||||
}
|
||||
|
||||
/** Returns the left and right country for {@code lineString}. */
|
||||
private BorderingRegions getBorderingRegions(
|
||||
LongObjectMap<PreparedGeometry> countryBoundaries,
|
||||
Set<Long> allRegions,
|
||||
LineString lineString
|
||||
) {
|
||||
Set<Long> validRegions = allRegions.stream()
|
||||
.filter(countryBoundaries::containsKey)
|
||||
.collect(Collectors.toSet());
|
||||
if (validRegions.isEmpty()) {
|
||||
return BorderingRegions.empty();
|
||||
}
|
||||
List<Long> rights = new ArrayList<>();
|
||||
List<Long> lefts = new ArrayList<>();
|
||||
int steps = 10;
|
||||
for (int i = 0; i < steps; i++) {
|
||||
double ratio = (double) (i + 1) / (steps + 2);
|
||||
Point right = GeoUtils.pointAlongOffset(lineString, ratio, COUNTRY_TEST_OFFSET);
|
||||
Point left = GeoUtils.pointAlongOffset(lineString, ratio, -COUNTRY_TEST_OFFSET);
|
||||
for (Long regionId : validRegions) {
|
||||
PreparedGeometry geom = countryBoundaries.get(regionId);
|
||||
if (geom != null) {
|
||||
if (geom.contains(right)) {
|
||||
rights.add(regionId);
|
||||
} else if (geom.contains(left)) {
|
||||
lefts.add(regionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var right = mode(rights);
|
||||
if (right != null) {
|
||||
lefts.removeAll(List.of(right));
|
||||
}
|
||||
var left = mode(lefts);
|
||||
|
||||
if (left == null && right == null) {
|
||||
Coordinate point = GeoUtils.worldToLatLonCoords(GeoUtils.pointAlongOffset(lineString, 0.5, 0)).getCoordinate();
|
||||
LOGGER.warn("no left or right country for border between OSM country relations: %s around %s"
|
||||
.formatted(
|
||||
validRegions,
|
||||
Format.osmDebugUrl(10, point)
|
||||
));
|
||||
}
|
||||
|
||||
return new BorderingRegions(left, right);
|
||||
}
|
||||
|
||||
/** Returns a map from region ID to prepared geometry optimized for {@code contains} queries. */
|
||||
private LongObjectMap<PreparedGeometry> prepareRegionPolygons() {
|
||||
LOGGER.info("Creating polygons for " + regionGeometries.size() + " boundaries");
|
||||
LongObjectMap<PreparedGeometry> countryBoundaries = new GHLongObjectHashMap<>();
|
||||
for (var entry : regionGeometries.entrySet()) {
|
||||
Long regionId = entry.getKey();
|
||||
Polygonizer polygonizer = new Polygonizer();
|
||||
polygonizer.add(entry.getValue());
|
||||
try {
|
||||
Geometry combined = polygonizer.getGeometry().union();
|
||||
if (combined.isEmpty()) {
|
||||
LOGGER.warn("Unable to form closed polygon for OSM relation " + regionId
|
||||
+ " (likely missing edges)");
|
||||
} else {
|
||||
countryBoundaries.put(regionId, PreparedGeometryFactory.prepare(combined));
|
||||
}
|
||||
} catch (TopologyException e) {
|
||||
LOGGER
|
||||
.warn("Unable to build boundary polygon for OSM relation " + regionId + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
LOGGER.info("Finished creating " + countryBoundaries.size() + " country polygons");
|
||||
return countryBoundaries;
|
||||
}
|
||||
|
||||
/** Returns most frequently-occurring element in {@code list}. */
|
||||
private static Long mode(List<Long> list) {
|
||||
return list.stream()
|
||||
.collect(groupingBy(Function.identity(), counting())).entrySet().stream()
|
||||
.max(Map.Entry.comparingByValue())
|
||||
.map(Map.Entry::getKey)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private static record BorderingRegions(Long left, Long right) {
|
||||
|
||||
public static BorderingRegions empty() {
|
||||
return new BorderingRegions(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal set of information extracted from a boundary relation to be used when processing each way in that
|
||||
* relation.
|
||||
*/
|
||||
private static record BoundaryRelation(
|
||||
long id,
|
||||
int adminLevel,
|
||||
boolean disputed,
|
||||
String name,
|
||||
String claimedBy,
|
||||
String iso3166alpha3
|
||||
) implements OsmRelationInfo {
|
||||
|
||||
@Override
|
||||
public long estimateMemoryUsageBytes() {
|
||||
return CLASS_HEADER_BYTES
|
||||
+ MemoryEstimator.estimateSizeLong(id)
|
||||
+ MemoryEstimator.estimateSizeInt(adminLevel)
|
||||
+ estimateSize(disputed)
|
||||
+ POINTER_BYTES + estimateSize(name)
|
||||
+ POINTER_BYTES + estimateSize(claimedBy)
|
||||
+ POINTER_BYTES + estimateSize(iso3166alpha3);
|
||||
}
|
||||
}
|
||||
|
||||
/** Information to hold onto from processing a way in a boundary relation to determine the left/right region ID later. */
|
||||
private static record CountryBoundaryComponent(
|
||||
int adminLevel,
|
||||
boolean disputed,
|
||||
boolean maritime,
|
||||
int minzoom,
|
||||
Geometry line,
|
||||
Set<Long> regions,
|
||||
String claimedBy,
|
||||
String name
|
||||
) {
|
||||
|
||||
CountryBoundaryComponent groupingKey() {
|
||||
return new CountryBoundaryComponent(adminLevel, disputed, maritime, minzoom, null, regions, claimedBy, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
import static com.onthegomap.planetiler.util.MemoryEstimator.CLASS_HEADER_BYTES;
|
||||
import static com.onthegomap.planetiler.util.Parse.parseDoubleOrNull;
|
||||
import static java.util.Map.entry;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmElement;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.MemoryEstimator;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for buildings in the {@code building} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/building">OpenMapTiles
|
||||
* building sql files</a>.
|
||||
*/
|
||||
public class Building implements
|
||||
OpenMapTilesSchema.Building,
|
||||
Tables.OsmBuildingPolygon.Handler,
|
||||
BasemapProfile.FeaturePostProcessor,
|
||||
BasemapProfile.OsmRelationPreprocessor {
|
||||
|
||||
/*
|
||||
* Emit all buildings from OSM data at z14.
|
||||
*
|
||||
* At z13, emit all buildings at process-time, but then at tile render-time,
|
||||
* merge buildings that are overlapping or almost touching into combined
|
||||
* buildings so that entire city blocks show up as a single building polygon.
|
||||
*
|
||||
* THIS IS VERY EXPENSIVE! Merging buildings at z13 adds about 50% to the
|
||||
* total map generation time. To disable it, set building_merge_z13 argument
|
||||
* to false.
|
||||
*/
|
||||
|
||||
private static final Map<String, String> MATERIAL_COLORS = Map.ofEntries(
|
||||
entry("cement_block", "#6a7880"),
|
||||
entry("brick", "#bd8161"),
|
||||
entry("plaster", "#dadbdb"),
|
||||
entry("wood", "#d48741"),
|
||||
entry("concrete", "#d3c2b0"),
|
||||
entry("metal", "#b7b1a6"),
|
||||
entry("stone", "#b4a995"),
|
||||
entry("mud", "#9d8b75"),
|
||||
entry("steel", "#b7b1a6"), // same as metal
|
||||
entry("glass", "#5a81a0"),
|
||||
entry("traditional", "#bd8161"), // same as brick
|
||||
entry("masonry", "#bd8161"), // same as brick
|
||||
entry("Brick", "#bd8161"), // same as brick
|
||||
entry("tin", "#b7b1a6"), // same as metal
|
||||
entry("timber_framing", "#b3b0a9"),
|
||||
entry("sandstone", "#b4a995"), // same as stone
|
||||
entry("clay", "#9d8b75") // same as mud
|
||||
);
|
||||
private final boolean mergeZ13Buildings;
|
||||
|
||||
public Building(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.mergeZ13Buildings = config.arguments().getBoolean(
|
||||
"building_merge_z13",
|
||||
"building layer: merge nearby buildings at z13",
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OsmRelationInfo> preprocessOsmRelation(OsmElement.Relation relation) {
|
||||
if (relation.hasTag("type", "building")) {
|
||||
return List.of(new BuildingRelationInfo(relation.id()));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmBuildingPolygon element, FeatureCollector features) {
|
||||
Boolean hide3d = null;
|
||||
var relations = element.source().relationInfo(BuildingRelationInfo.class);
|
||||
for (var relation : relations) {
|
||||
if ("outline".equals(relation.role())) {
|
||||
hide3d = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
String color = element.colour();
|
||||
if (color == null && element.material() != null) {
|
||||
color = MATERIAL_COLORS.get(element.material());
|
||||
}
|
||||
if (color != null) {
|
||||
color = color.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
Double height = coalesce(
|
||||
parseDoubleOrNull(element.height()),
|
||||
parseDoubleOrNull(element.buildingheight())
|
||||
);
|
||||
Double minHeight = coalesce(
|
||||
parseDoubleOrNull(element.minHeight()),
|
||||
parseDoubleOrNull(element.buildingminHeight())
|
||||
);
|
||||
Double levels = coalesce(
|
||||
parseDoubleOrNull(element.levels()),
|
||||
parseDoubleOrNull(element.buildinglevels())
|
||||
);
|
||||
Double minLevels = coalesce(
|
||||
parseDoubleOrNull(element.minLevel()),
|
||||
parseDoubleOrNull(element.buildingminLevel())
|
||||
);
|
||||
|
||||
int renderHeight = (int) Math.ceil(height != null ? height
|
||||
: levels != null ? (levels * 3.66) : 5);
|
||||
int renderMinHeight = (int) Math.floor(minHeight != null ? minHeight
|
||||
: minLevels != null ? (minLevels * 3.66) : 0);
|
||||
|
||||
if (renderHeight < 3660 && renderMinHeight < 3660) {
|
||||
var feature = features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setMinZoom(13)
|
||||
.setMinPixelSize(2)
|
||||
.setAttrWithMinzoom(Fields.RENDER_HEIGHT, renderHeight, 14)
|
||||
.setAttrWithMinzoom(Fields.RENDER_MIN_HEIGHT, renderMinHeight, 14)
|
||||
.setAttrWithMinzoom(Fields.COLOUR, color, 14)
|
||||
.setAttrWithMinzoom(Fields.HIDE_3D, hide3d, 14)
|
||||
.setSortKey(renderHeight);
|
||||
if (mergeZ13Buildings) {
|
||||
feature
|
||||
.setMinPixelSize(0.1)
|
||||
.setPixelTolerance(0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom,
|
||||
List<VectorTile.Feature> items) throws GeometryException {
|
||||
return (mergeZ13Buildings && zoom == 13) ? FeatureMerge.mergeNearbyPolygons(items, 4, 4, 0.5, 0.5) : items;
|
||||
}
|
||||
|
||||
private static record BuildingRelationInfo(long id) implements OsmRelationInfo {
|
||||
|
||||
@Override
|
||||
public long estimateMemoryUsageBytes() {
|
||||
return CLASS_HEADER_BYTES + MemoryEstimator.estimateSizeLong(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements in the {@code housenumber} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/housenumber">OpenMapTiles
|
||||
* housenumber sql files</a>.
|
||||
*/
|
||||
public class Housenumber implements
|
||||
OpenMapTilesSchema.Housenumber,
|
||||
Tables.OsmHousenumberPoint.Handler {
|
||||
|
||||
public Housenumber(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmHousenumberPoint element, FeatureCollector features) {
|
||||
features.centroidIfConvex(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.HOUSENUMBER, element.housenumber())
|
||||
.setMinZoom(14);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import com.onthegomap.planetiler.util.ZoomFunction;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for natural land cover polygons like ice, sand, and forest in the
|
||||
* {@code landcover} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/landcover">OpenMapTiles
|
||||
* landcover sql files</a>.
|
||||
*/
|
||||
public class Landcover implements
|
||||
OpenMapTilesSchema.Landcover,
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
Tables.OsmLandcoverPolygon.Handler,
|
||||
BasemapProfile.FeaturePostProcessor {
|
||||
|
||||
/*
|
||||
* Large ice areas come from natural earth and the rest come from OpenStreetMap at higher zoom
|
||||
* levels. At render-time, postProcess() merges polygons into larger connected area based
|
||||
* on the number of points in the original area. Since postProcess() only has visibility into
|
||||
* features on a single tile, process() needs to pass the number of points the original feature
|
||||
* had through using a temporary "_numpoints" attribute.
|
||||
*/
|
||||
|
||||
public static final ZoomFunction<Number> MIN_PIXEL_SIZE_THRESHOLDS = ZoomFunction.fromMaxZoomThresholds(Map.of(
|
||||
13, 8,
|
||||
10, 4,
|
||||
9, 2
|
||||
));
|
||||
private static final String TEMP_NUM_POINTS_ATTR = "_numpoints";
|
||||
private static final Set<String> WOOD_OR_FOREST = Set.of(
|
||||
FieldValues.SUBCLASS_WOOD,
|
||||
FieldValues.SUBCLASS_FOREST
|
||||
);
|
||||
private final MultiExpression.Index<String> classMapping;
|
||||
|
||||
public Landcover(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.classMapping = FieldMappings.Class.index();
|
||||
}
|
||||
|
||||
private String getClassFromSubclass(String subclass) {
|
||||
return subclass == null ? null : classMapping.getOrElse(Map.of(Fields.SUBCLASS, subclass), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processNaturalEarth(String table, SourceFeature feature,
|
||||
FeatureCollector features) {
|
||||
record LandcoverInfo(String subclass, int minzoom, int maxzoom) {}
|
||||
LandcoverInfo info = switch (table) {
|
||||
case "ne_110m_glaciated_areas" -> new LandcoverInfo(FieldValues.SUBCLASS_GLACIER, 0, 1);
|
||||
case "ne_50m_glaciated_areas" -> new LandcoverInfo(FieldValues.SUBCLASS_GLACIER, 2, 4);
|
||||
case "ne_10m_glaciated_areas" -> new LandcoverInfo(FieldValues.SUBCLASS_GLACIER, 5, 6);
|
||||
case "ne_50m_antarctic_ice_shelves_polys" -> new LandcoverInfo("ice_shelf", 2, 4);
|
||||
case "ne_10m_antarctic_ice_shelves_polys" -> new LandcoverInfo("ice_shelf", 5, 6);
|
||||
default -> null;
|
||||
};
|
||||
if (info != null) {
|
||||
String clazz = getClassFromSubclass(info.subclass);
|
||||
if (clazz != null) {
|
||||
features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
.setAttr(Fields.SUBCLASS, info.subclass)
|
||||
.setZoomRange(info.minzoom, info.maxzoom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmLandcoverPolygon element, FeatureCollector features) {
|
||||
String subclass = element.subclass();
|
||||
String clazz = getClassFromSubclass(subclass);
|
||||
if (clazz != null) {
|
||||
features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setMinPixelSizeOverrides(MIN_PIXEL_SIZE_THRESHOLDS)
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
.setAttr(Fields.SUBCLASS, subclass)
|
||||
.setNumPointsAttr(TEMP_NUM_POINTS_ATTR)
|
||||
.setMinZoom(WOOD_OR_FOREST.contains(subclass) ? 9 : 7);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) throws GeometryException {
|
||||
if (zoom < 7 || zoom > 13) {
|
||||
for (var item : items) {
|
||||
item.attrs().remove(TEMP_NUM_POINTS_ATTR);
|
||||
}
|
||||
return items;
|
||||
} else { // z7-13
|
||||
// merging only merges polygons with the same attributes, so use this temporary key
|
||||
// to separate features into layers that will be merged separately
|
||||
String tempGroupKey = "_group";
|
||||
List<VectorTile.Feature> result = new ArrayList<>();
|
||||
List<VectorTile.Feature> toMerge = new ArrayList<>();
|
||||
for (var item : items) {
|
||||
Map<String, Object> attrs = item.attrs();
|
||||
Object numPointsObj = attrs.remove(TEMP_NUM_POINTS_ATTR);
|
||||
Object subclassObj = attrs.get(Fields.SUBCLASS);
|
||||
if (numPointsObj instanceof Number num && subclassObj instanceof String subclass) {
|
||||
long numPoints = num.longValue();
|
||||
if (zoom >= 10) {
|
||||
if (WOOD_OR_FOREST.contains(subclass) && numPoints < 300) {
|
||||
attrs.put(tempGroupKey, numPoints < 50 ? "<50" : "<300");
|
||||
toMerge.add(item);
|
||||
} else { // don't merge
|
||||
result.add(item);
|
||||
}
|
||||
} else if (zoom == 9) {
|
||||
if (WOOD_OR_FOREST.contains(subclass)) {
|
||||
attrs.put(tempGroupKey, numPoints < 50 ? "<50" : numPoints < 300 ? "<300" : ">300");
|
||||
toMerge.add(item);
|
||||
} else { // don't merge
|
||||
result.add(item);
|
||||
}
|
||||
} else { // zoom between 7 and 8
|
||||
toMerge.add(item);
|
||||
}
|
||||
} else {
|
||||
result.add(item);
|
||||
}
|
||||
}
|
||||
var merged = FeatureMerge.mergeOverlappingPolygons(toMerge, 4);
|
||||
for (var item : merged) {
|
||||
item.attrs().remove(tempGroupKey);
|
||||
}
|
||||
result.addAll(merged);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import com.onthegomap.planetiler.util.ZoomFunction;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for man-made land use polygons like cemeteries, zoos, and hospitals in
|
||||
* the {@code landuse} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/landuse">OpenMapTiles
|
||||
* landuse sql files</a>.
|
||||
*/
|
||||
public class Landuse implements
|
||||
OpenMapTilesSchema.Landuse,
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
Tables.OsmLandusePolygon.Handler {
|
||||
|
||||
private static final ZoomFunction<Number> MIN_PIXEL_SIZE_THRESHOLDS = ZoomFunction.fromMaxZoomThresholds(Map.of(
|
||||
13, 4,
|
||||
7, 2,
|
||||
6, 1
|
||||
));
|
||||
private static final Set<String> Z6_CLASSES = Set.of(
|
||||
FieldValues.CLASS_RESIDENTIAL,
|
||||
FieldValues.CLASS_SUBURB,
|
||||
FieldValues.CLASS_QUARTER,
|
||||
FieldValues.CLASS_NEIGHBOURHOOD
|
||||
);
|
||||
|
||||
public Landuse(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) {
|
||||
if ("ne_50m_urban_areas".equals(table)) {
|
||||
Double scalerank = Parse.parseDoubleOrNull(feature.getTag("scalerank"));
|
||||
if (scalerank != null && scalerank <= 2) {
|
||||
features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_RESIDENTIAL)
|
||||
.setZoomRange(4, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmLandusePolygon element, FeatureCollector features) {
|
||||
String clazz = coalesce(
|
||||
nullIfEmpty(element.landuse()),
|
||||
nullIfEmpty(element.amenity()),
|
||||
nullIfEmpty(element.leisure()),
|
||||
nullIfEmpty(element.tourism()),
|
||||
nullIfEmpty(element.place()),
|
||||
nullIfEmpty(element.waterway())
|
||||
);
|
||||
if (clazz != null) {
|
||||
features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
.setMinPixelSizeOverrides(MIN_PIXEL_SIZE_THRESHOLDS)
|
||||
.setMinZoom(Z6_CLASSES.contains(clazz) ? 6 : 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.elevationTags;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.carrotsearch.hppc.LongIntHashMap;
|
||||
import com.carrotsearch.hppc.LongIntMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.List;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for mountain peak label points in the {@code mountain_peak} layer from
|
||||
* source features.
|
||||
* <p>
|
||||
* This class is ported to Java from <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/mountain_peak">OpenMapTiles
|
||||
* mountain_peak sql files</a>.
|
||||
*/
|
||||
public class MountainPeak implements
|
||||
OpenMapTilesSchema.MountainPeak,
|
||||
Tables.OsmPeakPoint.Handler,
|
||||
BasemapProfile.FeaturePostProcessor {
|
||||
|
||||
/*
|
||||
* Mountain peaks come from OpenStreetMap data and are ranked by importance (based on if they
|
||||
* have a name or wikipedia page) then by elevation. Uses the "label grid" feature to limit
|
||||
* label density by only taking the top 5 most important mountain peaks within each 100x100px
|
||||
* square.
|
||||
*/
|
||||
|
||||
private final Translations translations;
|
||||
private final Stats stats;
|
||||
|
||||
public MountainPeak(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.translations = translations;
|
||||
this.stats = stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmPeakPoint element, FeatureCollector features) {
|
||||
Integer meters = Parse.parseIntSubstring(element.ele());
|
||||
if (meters != null && Math.abs(meters) < 10_000) {
|
||||
features.point(LAYER_NAME)
|
||||
.setAttr(Fields.CLASS, element.source().getTag("natural"))
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.putAttrs(elevationTags(meters))
|
||||
.setSortKeyDescending(
|
||||
meters +
|
||||
(nullIfEmpty(element.wikipedia()) != null ? 10_000 : 0) +
|
||||
(nullIfEmpty(element.name()) != null ? 10_000 : 0)
|
||||
)
|
||||
.setMinZoom(7)
|
||||
// need to use a larger buffer size to allow enough points through to not cut off
|
||||
// any label grid squares which could lead to inconsistent label ranks for a feature
|
||||
// in adjacent tiles. postProcess() will remove anything outside the desired buffer.
|
||||
.setBufferPixels(100)
|
||||
.setPointLabelGridSizeAndLimit(13, 100, 5);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
LongIntMap groupCounts = new LongIntHashMap();
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
VectorTile.Feature feature = items.get(i);
|
||||
int gridrank = groupCounts.getOrDefault(feature.group(), 1);
|
||||
groupCounts.put(feature.group(), gridrank + 1);
|
||||
// now that we have accurate ranks, remove anything outside the desired buffer
|
||||
if (!insideTileBuffer(feature)) {
|
||||
items.set(i, null);
|
||||
} else if (!feature.attrs().containsKey(Fields.RANK)) {
|
||||
feature.attrs().put(Fields.RANK, gridrank);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private static boolean insideTileBuffer(double xOrY) {
|
||||
return xOrY >= -BUFFER_SIZE && xOrY <= 256 + BUFFER_SIZE;
|
||||
}
|
||||
|
||||
private boolean insideTileBuffer(VectorTile.Feature feature) {
|
||||
try {
|
||||
Geometry geom = feature.geometry().decode();
|
||||
return !(geom instanceof Point point) || (insideTileBuffer(point.getX()) && insideTileBuffer(point.getY()));
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "mountain_peak_decode_point", "Error decoding mountain peak point: " + feature.attrs());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
152
src/main/java/com/onthegomap/planetiler/basemap/layers/Park.java
Normal file
152
src/main/java/com/onthegomap/planetiler/basemap/layers/Park.java
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
import static com.onthegomap.planetiler.collection.FeatureGroup.SORT_KEY_BITS;
|
||||
|
||||
import com.carrotsearch.hppc.LongIntHashMap;
|
||||
import com.carrotsearch.hppc.LongIntMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.geo.GeometryType;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.SortKey;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for designated parks polygons and their label points in the {@code
|
||||
* park} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/park">OpenMapTiles
|
||||
* park sql files</a>.
|
||||
*/
|
||||
public class Park implements
|
||||
OpenMapTilesSchema.Park,
|
||||
Tables.OsmParkPolygon.Handler,
|
||||
BasemapProfile.FeaturePostProcessor {
|
||||
|
||||
// constants for packing the minimum zoom ordering of park labels into the sort-key field
|
||||
private static final int PARK_NATIONAL_PARK_BOOST = 1 << (SORT_KEY_BITS - 1);
|
||||
private static final int PARK_WIKIPEDIA_BOOST = 1 << (SORT_KEY_BITS - 2);
|
||||
|
||||
// constants for determining the minimum zoom level for a park label based on its area
|
||||
private static final double WORLD_AREA_FOR_70K_SQUARE_METERS =
|
||||
Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2);
|
||||
private static final double LOG2 = Math.log(2);
|
||||
private static final int PARK_AREA_RANGE = 1 << (SORT_KEY_BITS - 3);
|
||||
private static final double SMALLEST_PARK_WORLD_AREA = Math.pow(4, -26); // 2^14 tiles, 2^12 pixels per tile
|
||||
|
||||
private final Translations translations;
|
||||
private final Stats stats;
|
||||
|
||||
public Park(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.stats = stats;
|
||||
this.translations = translations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmParkPolygon element, FeatureCollector features) {
|
||||
String protectionTitle = element.protectionTitle();
|
||||
if (protectionTitle != null) {
|
||||
protectionTitle = protectionTitle.replace(' ', '_').toLowerCase(Locale.ROOT);
|
||||
}
|
||||
String clazz = coalesce(
|
||||
nullIfEmpty(protectionTitle),
|
||||
nullIfEmpty(element.boundary()),
|
||||
nullIfEmpty(element.leisure())
|
||||
);
|
||||
|
||||
// park shape
|
||||
features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
.setMinPixelSize(2)
|
||||
.setMinZoom(6);
|
||||
|
||||
// park name label point (if it has one)
|
||||
if (element.name() != null) {
|
||||
try {
|
||||
double area = element.source().area();
|
||||
int minzoom = getMinZoomForArea(area);
|
||||
|
||||
features.centroid(LAYER_NAME).setBufferPixels(256)
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setPointLabelGridPixelSize(14, 100)
|
||||
.setSortKey(SortKey
|
||||
.orderByTruesFirst("national_park".equals(clazz))
|
||||
.thenByTruesFirst(element.source().hasTag("wikipedia") || element.source().hasTag("wikidata"))
|
||||
.thenByLog(area, 1d, SMALLEST_PARK_WORLD_AREA, 1 << (SORT_KEY_BITS - 2) - 1)
|
||||
.get()
|
||||
).setMinZoom(minzoom);
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_park_area", "Unable to get park area for " + element.source().id());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getMinZoomForArea(double area) {
|
||||
// sql filter: area > 70000*2^(20-zoom_level)
|
||||
// simplifies to: zoom_level > 20 - log(area / 70000) / log(2)
|
||||
int minzoom = (int) Math.floor(20 - Math.log(area / WORLD_AREA_FOR_70K_SQUARE_METERS) / LOG2);
|
||||
minzoom = Math.min(14, Math.max(6, minzoom));
|
||||
return minzoom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
// infer the "rank" attribute from point ordering within each label grid square
|
||||
LongIntMap counts = new LongIntHashMap();
|
||||
for (VectorTile.Feature feature : items) {
|
||||
if (feature.geometry().geomType() == GeometryType.POINT && feature.hasGroup()) {
|
||||
int count = counts.getOrDefault(feature.group(), 0) + 1;
|
||||
feature.attrs().put("rank", count);
|
||||
counts.put(feature.group(), count);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullOrEmpty;
|
||||
import static com.onthegomap.planetiler.collection.FeatureGroup.SORT_KEY_BITS;
|
||||
|
||||
import com.carrotsearch.hppc.LongIntHashMap;
|
||||
import com.carrotsearch.hppc.LongIntMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.geo.PointIndex;
|
||||
import com.onthegomap.planetiler.geo.PolygonIndex;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.SortKey;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import com.onthegomap.planetiler.util.ZoomFunction;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.DoubleStream;
|
||||
import java.util.stream.Stream;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating label points for populated places like continents, countries, cities, and towns in
|
||||
* the {@code place} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/place">OpenMapTiles
|
||||
* place sql files</a>.
|
||||
*/
|
||||
public class Place implements
|
||||
OpenMapTilesSchema.Place,
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
Tables.OsmContinentPoint.Handler,
|
||||
Tables.OsmCountryPoint.Handler,
|
||||
Tables.OsmStatePoint.Handler,
|
||||
Tables.OsmIslandPoint.Handler,
|
||||
Tables.OsmIslandPolygon.Handler,
|
||||
Tables.OsmCityPoint.Handler,
|
||||
BasemapProfile.FeaturePostProcessor {
|
||||
|
||||
/*
|
||||
* Place labels locations and names come from OpenStreetMap, but we also join with natural
|
||||
* earth state/country geographic areas and city point labels to give a hint for what rank
|
||||
* and minimum zoom level to use for those points.
|
||||
*/
|
||||
|
||||
private static final TreeMap<Double, Integer> ISLAND_AREA_RANKS = new TreeMap<>(Map.of(
|
||||
Double.MAX_VALUE, 3,
|
||||
squareMetersToWorldArea(40_000_000), 4,
|
||||
squareMetersToWorldArea(15_000_000), 5,
|
||||
squareMetersToWorldArea(1_000_000), 6
|
||||
));
|
||||
private static final double MIN_ISLAND_WORLD_AREA = Math.pow(4, -26); // 2^14 tiles, 2^12 pixels per tile
|
||||
private static final double CITY_JOIN_DISTANCE = GeoUtils.metersToPixelAtEquator(0, 50_000) / 256d;
|
||||
// constants for packing place label precedence into the sort-key field
|
||||
private static final double MAX_CITY_POPULATION = 100_000_000d;
|
||||
private static final Set<String> MAJOR_CITY_PLACES = Set.of("city", "town", "village");
|
||||
private static final ZoomFunction<Number> LABEL_GRID_LIMITS = ZoomFunction.fromMaxZoomThresholds(Map.of(
|
||||
8, 4,
|
||||
9, 8,
|
||||
10, 12,
|
||||
12, 14
|
||||
), 0);
|
||||
private final Translations translations;
|
||||
private final Stats stats;
|
||||
// spatial indexes for joining natural earth place labels with their corresponding points
|
||||
// from openstreetmap
|
||||
private PolygonIndex<NaturalEarthRegion> countries = PolygonIndex.create();
|
||||
private PolygonIndex<NaturalEarthRegion> states = PolygonIndex.create();
|
||||
private PointIndex<NaturalEarthPoint> cities = PointIndex.create();
|
||||
|
||||
public Place(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.translations = translations;
|
||||
this.stats = stats;
|
||||
}
|
||||
|
||||
/** Returns the portion of the world that {@code squareMeters} covers where 1 is the entire planet. */
|
||||
private static double squareMetersToWorldArea(double squareMeters) {
|
||||
double oneSideMeters = Math.sqrt(squareMeters);
|
||||
double oneSideWorld = GeoUtils.metersToPixelAtEquator(0, oneSideMeters) / 256d;
|
||||
return Math.pow(oneSideWorld, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs place precedence ordering ({@code rank asc, place asc, population desc, name.length asc}) into an integer for
|
||||
* the sort-key field.
|
||||
*/
|
||||
static int getSortKey(Integer rank, PlaceType place, long population, String name) {
|
||||
return SortKey
|
||||
// ORDER BY "rank" ASC NULLS LAST,
|
||||
.orderByInt(rank == null ? 15 : rank, 0, 15) // 4 bits
|
||||
// place ASC NULLS LAST,
|
||||
.thenByInt(place == null ? 15 : place.ordinal(), 0, 15) // 4 bits
|
||||
// population DESC NULLS LAST,
|
||||
.thenByLog(population, MAX_CITY_POPULATION, 1, 1 << (SORT_KEY_BITS - 13) - 1)
|
||||
// length(name) ASC
|
||||
.thenByInt(name == null ? 0 : name.length(), 0, 31) // 5 bits
|
||||
.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
countries = null;
|
||||
states = null;
|
||||
cities = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) {
|
||||
// store data from natural earth to help with ranks and min zoom levels when actually
|
||||
// emitting features from openstreetmap data.
|
||||
try {
|
||||
switch (table) {
|
||||
case "ne_10m_admin_0_countries" -> countries.put(feature.worldGeometry(), new NaturalEarthRegion(
|
||||
feature.getString("name"), 6,
|
||||
feature.getLong("scalerank"),
|
||||
feature.getLong("labelrank")
|
||||
));
|
||||
case "ne_10m_admin_1_states_provinces" -> {
|
||||
Double scalerank = Parse.parseDoubleOrNull(feature.getTag("scalerank"));
|
||||
Double labelrank = Parse.parseDoubleOrNull(feature.getTag("labelrank"));
|
||||
if (scalerank != null && scalerank <= 3 && labelrank != null && labelrank <= 2) {
|
||||
states.put(feature.worldGeometry(), new NaturalEarthRegion(
|
||||
feature.getString("name"), 6,
|
||||
scalerank,
|
||||
labelrank,
|
||||
feature.getLong("datarank")
|
||||
));
|
||||
}
|
||||
}
|
||||
case "ne_10m_populated_places" -> cities.put(feature.worldGeometry(), new NaturalEarthPoint(
|
||||
feature.getString("name"),
|
||||
feature.getString("wikidataid"),
|
||||
(int) feature.getLong("scalerank"),
|
||||
Stream.of("name", "namealt", "meganame", "gn_ascii", "nameascii").map(feature::getString)
|
||||
.filter(Objects::nonNull)
|
||||
.map(s -> s.toLowerCase(Locale.ROOT))
|
||||
.collect(Collectors.toSet())
|
||||
));
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_place_ne",
|
||||
"Error getting geometry for natural earth feature " + table + " " + feature.getTag("ogc_fid"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmContinentPoint element, FeatureCollector features) {
|
||||
if (!nullOrEmpty(element.name())) {
|
||||
features.point(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_CONTINENT)
|
||||
.setAttr(Fields.RANK, 1)
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setZoomRange(0, 3);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmCountryPoint element, FeatureCollector features) {
|
||||
if (nullOrEmpty(element.name())) {
|
||||
return;
|
||||
}
|
||||
String isoA2 = coalesce(
|
||||
nullIfEmpty(element.countryCodeIso31661Alpha2()),
|
||||
nullIfEmpty(element.iso31661Alpha2()),
|
||||
nullIfEmpty(element.iso31661())
|
||||
);
|
||||
if (isoA2 == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// set country rank to 6, unless there is a match in natural earth that indicates it
|
||||
// should be lower
|
||||
int rank = 7;
|
||||
NaturalEarthRegion country = countries.get(element.source().worldGeometry().getCentroid());
|
||||
var names = LanguageUtils.getNames(element.source().tags(), translations);
|
||||
|
||||
if (country != null) {
|
||||
if (nullOrEmpty(names.get(Fields.NAME_EN))) {
|
||||
names.put(Fields.NAME_EN, country.name);
|
||||
}
|
||||
rank = country.rank;
|
||||
}
|
||||
|
||||
rank = Math.min(6, Math.max(1, rank));
|
||||
|
||||
features.point(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(names)
|
||||
.setAttr(Fields.ISO_A2, isoA2)
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_COUNTRY)
|
||||
.setAttr(Fields.RANK, rank)
|
||||
.setMinZoom(rank - 1)
|
||||
.setSortKey(rank);
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_place_country",
|
||||
"Unable to get point for OSM country " + element.source().id());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmStatePoint element, FeatureCollector features) {
|
||||
try {
|
||||
// want the containing (not nearest) state polygon since we pre-filter the states in the polygon index
|
||||
// use natural earth to filter out any spurious states, and to set the rank field
|
||||
NaturalEarthRegion state = states.getOnlyContaining(element.source().worldGeometry().getCentroid());
|
||||
if (state != null) {
|
||||
var names = LanguageUtils.getNames(element.source().tags(), translations);
|
||||
if (nullOrEmpty(names.get(Fields.NAME_EN))) {
|
||||
names.put(Fields.NAME_EN, state.name);
|
||||
}
|
||||
int rank = Math.min(6, Math.max(1, state.rank));
|
||||
|
||||
features.point(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(names)
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_STATE)
|
||||
.setAttr(Fields.RANK, rank)
|
||||
.setMinZoom(2)
|
||||
.setSortKey(rank);
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_place_state",
|
||||
"Unable to get point for OSM state " + element.source().id());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmIslandPolygon element, FeatureCollector features) {
|
||||
try {
|
||||
double area = element.source().area();
|
||||
int rank = ISLAND_AREA_RANKS.ceilingEntry(area).getValue();
|
||||
int minzoom = rank <= 3 ? 8 : rank <= 4 ? 9 : 10;
|
||||
|
||||
features.pointOnSurface(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setAttr(Fields.CLASS, "island")
|
||||
.setAttr(Fields.RANK, rank)
|
||||
.setMinZoom(minzoom)
|
||||
.setSortKey(SortKey.orderByLog(area, 1d, MIN_ISLAND_WORLD_AREA).get());
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_place_island_poly",
|
||||
"Unable to get point for OSM island polygon " + element.source().id());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmIslandPoint element, FeatureCollector features) {
|
||||
features.point(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setAttr(Fields.CLASS, "island")
|
||||
.setAttr(Fields.RANK, 7)
|
||||
.setMinZoom(12);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmCityPoint element, FeatureCollector features) {
|
||||
Integer rank = null;
|
||||
if (MAJOR_CITY_PLACES.contains(element.place())) {
|
||||
// only for major cities, attempt to find a nearby natural earth label with a similar
|
||||
// name and use that to set a rank from OSM that causes the label to be shown at lower
|
||||
// zoom levels
|
||||
try {
|
||||
Point point = element.source().worldGeometry().getCentroid();
|
||||
List<NaturalEarthPoint> neCities = cities.getWithin(point, CITY_JOIN_DISTANCE);
|
||||
String rawName = coalesce(element.name(), "");
|
||||
String name = coalesce(rawName, "").toLowerCase(Locale.ROOT);
|
||||
String nameEn = coalesce(element.nameEn(), "").toLowerCase(Locale.ROOT);
|
||||
String normalizedName = StringUtils.stripAccents(rawName);
|
||||
String wikidata = element.source().getString("wikidata", "");
|
||||
for (var neCity : neCities) {
|
||||
if (wikidata.equals(neCity.wikidata) ||
|
||||
neCity.names.contains(name) ||
|
||||
neCity.names.contains(nameEn) ||
|
||||
normalizedName.equals(neCity.name)) {
|
||||
rank = neCity.scaleRank <= 5 ? neCity.scaleRank + 1 : neCity.scaleRank;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_place_city",
|
||||
"Unable to get point for OSM city " + element.source().id());
|
||||
}
|
||||
}
|
||||
|
||||
String capital = element.capital();
|
||||
|
||||
PlaceType placeType = PlaceType.forName(element.place());
|
||||
|
||||
int minzoom = rank != null && rank == 1 ? 2 :
|
||||
rank != null && rank <= 8 ? Math.max(3, rank - 1) :
|
||||
placeType.ordinal() <= PlaceType.TOWN.ordinal() ? 7 :
|
||||
placeType.ordinal() <= PlaceType.VILLAGE.ordinal() ? 8 :
|
||||
placeType.ordinal() <= PlaceType.SUBURB.ordinal() ? 11 : 14;
|
||||
|
||||
var feature = features.point(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setAttr(Fields.CLASS, element.place())
|
||||
.setAttr(Fields.RANK, rank)
|
||||
.setMinZoom(minzoom)
|
||||
.setSortKey(getSortKey(rank, placeType, element.population(), element.name()))
|
||||
.setPointLabelGridPixelSize(12, 128);
|
||||
|
||||
if (rank == null) {
|
||||
feature.setPointLabelGridLimit(LABEL_GRID_LIMITS);
|
||||
}
|
||||
|
||||
if ("2".equals(capital) || "yes".equals(capital)) {
|
||||
feature.setAttr(Fields.CAPITAL, 2);
|
||||
} else if ("4".equals(capital)) {
|
||||
feature.setAttr(Fields.CAPITAL, 4);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
// infer the rank field from ordering of the place labels with each label grid square
|
||||
LongIntMap groupCounts = new LongIntHashMap();
|
||||
for (VectorTile.Feature feature : items) {
|
||||
int gridrank = groupCounts.getOrDefault(feature.group(), 1);
|
||||
groupCounts.put(feature.group(), gridrank + 1);
|
||||
if (!feature.attrs().containsKey(Fields.RANK)) {
|
||||
feature.attrs().put(Fields.RANK, 10 + gridrank);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/** Ordering defines the precedence of place classes. */
|
||||
enum PlaceType {
|
||||
CITY("city"),
|
||||
TOWN("town"),
|
||||
VILLAGE("village"),
|
||||
HAMLET("hamlet"),
|
||||
SUBURB("suburb"),
|
||||
QUARTER("quarter"),
|
||||
NEIGHBORHOOD("neighbourhood"),
|
||||
ISOLATED_DWELLING("isolated_dwelling"),
|
||||
UNKNOWN("unknown");
|
||||
|
||||
private static final Map<String, PlaceType> byName = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (PlaceType place : values()) {
|
||||
byName.put(place.name, place);
|
||||
}
|
||||
}
|
||||
|
||||
private final String name;
|
||||
|
||||
PlaceType(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public static PlaceType forName(String name) {
|
||||
return byName.getOrDefault(name, UNKNOWN);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Information extracted from a natural earth geographic region that will be inspected when joining with OpenStreetMap
|
||||
* data.
|
||||
*/
|
||||
private static record NaturalEarthRegion(String name, int rank) {
|
||||
|
||||
NaturalEarthRegion(String name, int maxRank, double... ranks) {
|
||||
this(name, (int) Math.ceil(DoubleStream.of(ranks).average().orElse(maxRank)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Information extracted from a natural earth place label that will be inspected when joining with OpenStreetMap
|
||||
* data.
|
||||
*/
|
||||
private static record NaturalEarthPoint(String name, String wikidata, int scaleRank, Set<String> names) {}
|
||||
}
|
||||
|
||||
195
src/main/java/com/onthegomap/planetiler/basemap/layers/Poi.java
Normal file
195
src/main/java/com/onthegomap/planetiler/basemap/layers/Poi.java
Normal file
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIf;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullOrEmpty;
|
||||
import static java.util.Map.entry;
|
||||
|
||||
import com.carrotsearch.hppc.LongIntHashMap;
|
||||
import com.carrotsearch.hppc.LongIntMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for things like shops, parks, and schools in the {@code poi} layer from
|
||||
* source features.
|
||||
* <p>
|
||||
* This class is ported to Java from <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/poi">OpenMapTiles
|
||||
* poi sql files</a>.
|
||||
*/
|
||||
public class Poi implements
|
||||
OpenMapTilesSchema.Poi,
|
||||
Tables.OsmPoiPoint.Handler,
|
||||
Tables.OsmPoiPolygon.Handler,
|
||||
BasemapProfile.FeaturePostProcessor {
|
||||
|
||||
/*
|
||||
* process() creates the raw POI feature from OSM elements and postProcess()
|
||||
* assigns the feature rank from order in the tile at render-time.
|
||||
*/
|
||||
|
||||
private static final Map<String, Integer> CLASS_RANKS = Map.ofEntries(
|
||||
entry(FieldValues.CLASS_HOSPITAL, 20),
|
||||
entry(FieldValues.CLASS_RAILWAY, 40),
|
||||
entry(FieldValues.CLASS_BUS, 50),
|
||||
entry(FieldValues.CLASS_ATTRACTION, 70),
|
||||
entry(FieldValues.CLASS_HARBOR, 75),
|
||||
entry(FieldValues.CLASS_COLLEGE, 80),
|
||||
entry(FieldValues.CLASS_SCHOOL, 85),
|
||||
entry(FieldValues.CLASS_STADIUM, 90),
|
||||
entry("zoo", 95),
|
||||
entry(FieldValues.CLASS_TOWN_HALL, 100),
|
||||
entry(FieldValues.CLASS_CAMPSITE, 110),
|
||||
entry(FieldValues.CLASS_CEMETERY, 115),
|
||||
entry(FieldValues.CLASS_PARK, 120),
|
||||
entry(FieldValues.CLASS_LIBRARY, 130),
|
||||
entry("police", 135),
|
||||
entry(FieldValues.CLASS_POST, 140),
|
||||
entry(FieldValues.CLASS_GOLF, 150),
|
||||
entry(FieldValues.CLASS_SHOP, 400),
|
||||
entry(FieldValues.CLASS_GROCERY, 500),
|
||||
entry(FieldValues.CLASS_FAST_FOOD, 600),
|
||||
entry(FieldValues.CLASS_CLOTHING_STORE, 700),
|
||||
entry(FieldValues.CLASS_BAR, 800)
|
||||
);
|
||||
private final MultiExpression.Index<String> classMapping;
|
||||
private final Translations translations;
|
||||
|
||||
public Poi(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.classMapping = FieldMappings.Class.index();
|
||||
this.translations = translations;
|
||||
}
|
||||
|
||||
static int poiClassRank(String clazz) {
|
||||
return CLASS_RANKS.getOrDefault(clazz, 1_000);
|
||||
}
|
||||
|
||||
private String poiClass(String subclass, String mappingKey) {
|
||||
subclass = coalesce(subclass, "");
|
||||
return classMapping.getOrElse(Map.of(
|
||||
"subclass", subclass,
|
||||
"mapping_key", coalesce(mappingKey, "")
|
||||
), subclass);
|
||||
}
|
||||
|
||||
private int minzoom(String subclass, String mappingKey) {
|
||||
boolean lowZoom = ("station".equals(subclass) && "railway".equals(mappingKey)) ||
|
||||
"halt".equals(subclass) || "ferry_terminal".equals(subclass);
|
||||
return lowZoom ? 12 : 14;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmPoiPoint element, FeatureCollector features) {
|
||||
// TODO handle uic_ref => agg_stop
|
||||
setupPoiFeature(element, features.point(LAYER_NAME));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmPoiPolygon element, FeatureCollector features) {
|
||||
setupPoiFeature(element, features.centroidIfConvex(LAYER_NAME));
|
||||
}
|
||||
|
||||
private <T extends
|
||||
Tables.WithSubclass &
|
||||
Tables.WithStation &
|
||||
Tables.WithFunicular &
|
||||
Tables.WithSport &
|
||||
Tables.WithInformation &
|
||||
Tables.WithReligion &
|
||||
Tables.WithMappingKey &
|
||||
Tables.WithName &
|
||||
Tables.WithIndoor &
|
||||
Tables.WithLayer &
|
||||
Tables.WithSource>
|
||||
void setupPoiFeature(T element, FeatureCollector.Feature output) {
|
||||
String rawSubclass = element.subclass();
|
||||
if ("station".equals(rawSubclass) && "subway".equals(element.station())) {
|
||||
rawSubclass = "subway";
|
||||
}
|
||||
if ("station".equals(rawSubclass) && "yes".equals(element.funicular())) {
|
||||
rawSubclass = "halt";
|
||||
}
|
||||
|
||||
String subclass = switch (rawSubclass) {
|
||||
case "information" -> nullIfEmpty(element.information());
|
||||
case "place_of_worship" -> nullIfEmpty(element.religion());
|
||||
case "pitch" -> nullIfEmpty(element.sport());
|
||||
default -> rawSubclass;
|
||||
};
|
||||
String poiClass = poiClass(rawSubclass, element.mappingKey());
|
||||
int poiClassRank = poiClassRank(poiClass);
|
||||
int rankOrder = poiClassRank + ((nullOrEmpty(element.name())) ? 2000 : 0);
|
||||
|
||||
output.setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, poiClass)
|
||||
.setAttr(Fields.SUBCLASS, subclass)
|
||||
.setAttr(Fields.LAYER, nullIf(element.layer(), 0))
|
||||
.setAttr(Fields.LEVEL, Parse.parseLongOrNull(element.source().getTag("level")))
|
||||
.setAttr(Fields.INDOOR, element.indoor() ? 1 : null)
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setPointLabelGridPixelSize(14, 64)
|
||||
.setSortKey(rankOrder)
|
||||
.setMinZoom(minzoom(element.subclass(), element.mappingKey()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
// infer the "rank" field from the order of features within each label grid square
|
||||
LongIntMap groupCounts = new LongIntHashMap();
|
||||
for (VectorTile.Feature feature : items) {
|
||||
int gridrank = groupCounts.getOrDefault(feature.group(), 1);
|
||||
groupCounts.put(feature.group(), gridrank + 1);
|
||||
if (!feature.attrs().containsKey(Fields.RANK)) {
|
||||
feature.attrs().put(Fields.RANK, gridrank);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.*;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import com.onthegomap.planetiler.util.ZoomFunction;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for roads, shipways, railroads, and paths in the {@code transportation}
|
||||
* layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/transportation">OpenMapTiles
|
||||
* transportation sql files</a>.
|
||||
*/
|
||||
public class Transportation implements
|
||||
OpenMapTilesSchema.Transportation,
|
||||
Tables.OsmAerialwayLinestring.Handler,
|
||||
Tables.OsmHighwayLinestring.Handler,
|
||||
Tables.OsmRailwayLinestring.Handler,
|
||||
Tables.OsmShipwayLinestring.Handler,
|
||||
Tables.OsmHighwayPolygon.Handler,
|
||||
BasemapProfile.FeaturePostProcessor,
|
||||
BasemapProfile.IgnoreWikidata {
|
||||
|
||||
/*
|
||||
* Generates the shape for roads, trails, ferries, railways with detailed
|
||||
* attributes for rendering, but not any names. The transportation_name
|
||||
* layer includes names, but less detailed attributes.
|
||||
*/
|
||||
|
||||
private static final MultiExpression.Index<String> classMapping = FieldMappings.Class.index();
|
||||
private static final Set<String> RAILWAY_RAIL_VALUES = Set.of(
|
||||
FieldValues.SUBCLASS_RAIL,
|
||||
FieldValues.SUBCLASS_NARROW_GAUGE,
|
||||
FieldValues.SUBCLASS_PRESERVED,
|
||||
FieldValues.SUBCLASS_FUNICULAR
|
||||
);
|
||||
private static final Set<String> RAILWAY_TRANSIT_VALUES = Set.of(
|
||||
FieldValues.SUBCLASS_SUBWAY,
|
||||
FieldValues.SUBCLASS_LIGHT_RAIL,
|
||||
FieldValues.SUBCLASS_MONORAIL,
|
||||
FieldValues.SUBCLASS_TRAM
|
||||
);
|
||||
private static final Set<String> SERVICE_VALUES = Set.of(
|
||||
FieldValues.SERVICE_SPUR,
|
||||
FieldValues.SERVICE_YARD,
|
||||
FieldValues.SERVICE_SIDING,
|
||||
FieldValues.SERVICE_CROSSOVER,
|
||||
FieldValues.SERVICE_DRIVEWAY,
|
||||
FieldValues.SERVICE_ALLEY,
|
||||
FieldValues.SERVICE_PARKING_AISLE
|
||||
);
|
||||
private static final Set<String> SURFACE_UNPAVED_VALUES = Set.of(
|
||||
"unpaved", "compacted", "dirt", "earth", "fine_gravel", "grass", "grass_paver", "gravel", "gravel_turf", "ground",
|
||||
"ice", "mud", "pebblestone", "salt", "sand", "snow", "woodchips"
|
||||
);
|
||||
private static final Set<String> SURFACE_PAVED_VALUES = Set.of(
|
||||
"paved", "asphalt", "cobblestone", "concrete", "concrete:lanes", "concrete:plates", "metal",
|
||||
"paving_stones", "sett", "unhewn_cobblestone", "wood"
|
||||
);
|
||||
private static final ZoomFunction.MeterToPixelThresholds MIN_LENGTH = ZoomFunction.meterThresholds()
|
||||
.put(7, 50)
|
||||
.put(6, 100)
|
||||
.put(5, 500)
|
||||
.put(4, 1_000);
|
||||
private final Map<String, Integer> MINZOOMS;
|
||||
private final Stats stats;
|
||||
private final PlanetilerConfig config;
|
||||
|
||||
public Transportation(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.config = config;
|
||||
this.stats = stats;
|
||||
boolean z13Paths = config.arguments().getBoolean(
|
||||
"transportation_z13_paths",
|
||||
"transportation(_name) layer: show paths on z13",
|
||||
true
|
||||
);
|
||||
MINZOOMS = Map.of(
|
||||
FieldValues.CLASS_TRACK, 14,
|
||||
FieldValues.CLASS_PATH, z13Paths ? 13 : 14,
|
||||
FieldValues.CLASS_MINOR, 13,
|
||||
FieldValues.CLASS_RACEWAY, 12,
|
||||
FieldValues.CLASS_TERTIARY, 11,
|
||||
FieldValues.CLASS_SECONDARY, 9,
|
||||
FieldValues.CLASS_PRIMARY, 7,
|
||||
FieldValues.CLASS_TRUNK, 5,
|
||||
FieldValues.CLASS_MOTORWAY, 4
|
||||
);
|
||||
}
|
||||
|
||||
/** Returns a value for {@code surface} tag constrained to a small set of known values from raw OSM data. */
|
||||
private static String surface(String value) {
|
||||
return value == null ? null : SURFACE_PAVED_VALUES.contains(value) ? FieldValues.SURFACE_PAVED :
|
||||
SURFACE_UNPAVED_VALUES.contains(value) ? FieldValues.SURFACE_UNPAVED : null;
|
||||
}
|
||||
|
||||
/** Returns a value for {@code service} tag constrained to a small set of known values from raw OSM data. */
|
||||
private static String service(String value) {
|
||||
return (value == null || !SERVICE_VALUES.contains(value)) ? null : value;
|
||||
}
|
||||
|
||||
private static String railwayClass(String value) {
|
||||
return value == null ? null :
|
||||
RAILWAY_RAIL_VALUES.contains(value) ? "rail" :
|
||||
RAILWAY_TRANSIT_VALUES.contains(value) ? "transit" : null;
|
||||
}
|
||||
|
||||
static String highwayClass(String highway, String publicTransport, String construction, String manMade) {
|
||||
return (!nullOrEmpty(highway) || !nullOrEmpty(publicTransport)) ? classMapping.getOrElse(Map.of(
|
||||
"highway", coalesce(highway, ""),
|
||||
"public_transport", coalesce(publicTransport, ""),
|
||||
"construction", coalesce(construction, "")
|
||||
), manMade) : manMade;
|
||||
}
|
||||
|
||||
static String highwaySubclass(String highwayClass, String publicTransport, String highway) {
|
||||
return FieldValues.CLASS_PATH.equals(highwayClass) ? coalesce(nullIfEmpty(publicTransport), highway) : null;
|
||||
}
|
||||
|
||||
static boolean isFootwayOrSteps(String highway) {
|
||||
return "footway".equals(highway) || "steps".equals(highway);
|
||||
}
|
||||
|
||||
private static boolean isResidentialOrUnclassified(String highway) {
|
||||
return "residential".equals(highway) || "unclassified".equals(highway);
|
||||
}
|
||||
|
||||
private static boolean isBridgeOrPier(String manMade) {
|
||||
return "bridge".equals(manMade) || "pier".equals(manMade);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmHighwayLinestring element, FeatureCollector features) {
|
||||
if (element.isArea()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String highway = element.highway();
|
||||
String highwayClass = highwayClass(element.highway(), element.publicTransport(), element.construction(),
|
||||
element.manMade());
|
||||
if (highwayClass != null) {
|
||||
int minzoom;
|
||||
if ("pier".equals(element.manMade())) {
|
||||
try {
|
||||
if (element.source().worldGeometry() instanceof LineString lineString && lineString.isClosed()) {
|
||||
// ignore this because it's a polygon
|
||||
return;
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_transportation_pier",
|
||||
"Unable to decode pier geometry for " + element.source().id());
|
||||
return;
|
||||
}
|
||||
minzoom = 13;
|
||||
} else if (isResidentialOrUnclassified(highway)) {
|
||||
minzoom = 12;
|
||||
} else {
|
||||
String baseClass = highwayClass.replace("_construction", "");
|
||||
minzoom = MINZOOMS.getOrDefault(baseClass, 12);
|
||||
}
|
||||
boolean highwayIsLink = coalesce(highway, "").endsWith("_link");
|
||||
|
||||
if (highwayIsLink) {
|
||||
minzoom = Math.max(minzoom, 9);
|
||||
}
|
||||
|
||||
boolean highwayRamp = highwayIsLink || "steps".equals(highway);
|
||||
int rampAboveZ12 = (highwayRamp || element.isRamp()) ? 1 : 0;
|
||||
int rampBelowZ12 = highwayRamp ? 1 : 0;
|
||||
|
||||
FeatureCollector.Feature feature = features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
// main attributes at all zoom levels (used for grouping <= z8)
|
||||
.setAttr(Fields.CLASS, highwayClass)
|
||||
.setAttr(Fields.SUBCLASS, highwaySubclass(highwayClass, element.publicTransport(), highway))
|
||||
.setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()))
|
||||
// rest at z9+
|
||||
.setAttrWithMinzoom(Fields.SERVICE, service(element.service()), 12)
|
||||
.setAttrWithMinzoom(Fields.ONEWAY, element.isOneway(), 12)
|
||||
.setAttr(Fields.RAMP, minzoom >= 12 ? rampAboveZ12 :
|
||||
((ZoomFunction<Integer>) z -> z < 9 ? null : z >= 12 ? rampAboveZ12 : rampBelowZ12))
|
||||
.setAttrWithMinzoom(Fields.LAYER, nullIf(element.layer(), 0), 9)
|
||||
.setAttrWithMinzoom(Fields.BICYCLE, nullIfEmpty(element.bicycle()), 9)
|
||||
.setAttrWithMinzoom(Fields.FOOT, nullIfEmpty(element.foot()), 9)
|
||||
.setAttrWithMinzoom(Fields.HORSE, nullIfEmpty(element.horse()), 9)
|
||||
.setAttrWithMinzoom(Fields.MTB_SCALE, nullIfEmpty(element.mtbScale()), 9)
|
||||
.setAttrWithMinzoom(Fields.SURFACE, surface(element.surface()), 12)
|
||||
.setMinPixelSize(0) // merge during post-processing, then limit by size
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinZoom(minzoom);
|
||||
|
||||
if (isFootwayOrSteps(highway)) {
|
||||
feature
|
||||
.setAttr(Fields.LEVEL, Parse.parseLongOrNull(element.source().getTag("level")))
|
||||
.setAttr(Fields.INDOOR, element.indoor() ? 1 : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmRailwayLinestring element, FeatureCollector features) {
|
||||
String railway = element.railway();
|
||||
String clazz = railwayClass(railway);
|
||||
if (clazz != null) {
|
||||
String service = nullIfEmpty(element.service());
|
||||
int minzoom;
|
||||
if (service != null) {
|
||||
minzoom = 14;
|
||||
} else if (FieldValues.SUBCLASS_RAIL.equals(railway)) {
|
||||
minzoom = "main".equals(element.usage()) ? 8 : 10;
|
||||
} else if (FieldValues.SUBCLASS_NARROW_GAUGE.equals(railway)) {
|
||||
minzoom = 10;
|
||||
} else if (FieldValues.SUBCLASS_LIGHT_RAIL.equals(railway)) {
|
||||
minzoom = 11;
|
||||
} else {
|
||||
minzoom = 14;
|
||||
}
|
||||
features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, clazz)
|
||||
.setAttr(Fields.SUBCLASS, railway)
|
||||
.setAttr(Fields.SERVICE, service(service))
|
||||
.setAttr(Fields.ONEWAY, element.isOneway())
|
||||
.setAttr(Fields.RAMP, element.isRamp() ? 1 : 0)
|
||||
.setAttrWithMinzoom(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()), 10)
|
||||
.setAttrWithMinzoom(Fields.LAYER, nullIf(element.layer(), 0), 9)
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinPixelSize(0) // merge during post-processing, then limit by size
|
||||
.setMinZoom(minzoom);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmAerialwayLinestring element, FeatureCollector features) {
|
||||
features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, "aerialway")
|
||||
.setAttr(Fields.SUBCLASS, element.aerialway())
|
||||
.setAttr(Fields.SERVICE, service(element.service()))
|
||||
.setAttr(Fields.ONEWAY, element.isOneway())
|
||||
.setAttr(Fields.RAMP, element.isRamp() ? 1 : 0)
|
||||
.setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()))
|
||||
.setAttr(Fields.LAYER, nullIf(element.layer(), 0))
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinPixelSize(0) // merge during post-processing, then limit by size
|
||||
.setMinZoom(12);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmShipwayLinestring element, FeatureCollector features) {
|
||||
features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, element.shipway()) // "ferry"
|
||||
// no subclass
|
||||
.setAttr(Fields.SERVICE, service(element.service()))
|
||||
.setAttr(Fields.ONEWAY, element.isOneway())
|
||||
.setAttr(Fields.RAMP, element.isRamp() ? 1 : 0)
|
||||
.setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()))
|
||||
.setAttr(Fields.LAYER, nullIf(element.layer(), 0))
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinPixelSize(0) // merge during post-processing, then limit by size
|
||||
.setMinZoom(11);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmHighwayPolygon element, FeatureCollector features) {
|
||||
String manMade = element.manMade();
|
||||
if (isBridgeOrPier(manMade) ||
|
||||
// ignore underground pedestrian areas
|
||||
(element.isArea() && element.layer() >= 0)) {
|
||||
String highwayClass = highwayClass(element.highway(), element.publicTransport(), null, element.manMade());
|
||||
if (highwayClass != null) {
|
||||
features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, highwayClass)
|
||||
.setAttr(Fields.SUBCLASS, highwaySubclass(highwayClass, element.publicTransport(), element.highway()))
|
||||
.setAttr(Fields.BRUNNEL, brunnel("bridge".equals(manMade), false, false))
|
||||
.setAttr(Fields.LAYER, nullIf(element.layer(), 0))
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinZoom(13);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
double tolerance = config.tolerance(zoom);
|
||||
double minLength = coalesce(MIN_LENGTH.apply(zoom), config.minFeatureSize(zoom)).doubleValue();
|
||||
return FeatureMerge.mergeLineStrings(items, minLength, tolerance, BUFFER_SIZE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.layers.Transportation.highwayClass;
|
||||
import static com.onthegomap.planetiler.basemap.layers.Transportation.highwaySubclass;
|
||||
import static com.onthegomap.planetiler.basemap.layers.Transportation.isFootwayOrSteps;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.brunnel;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIf;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
import static com.onthegomap.planetiler.util.MemoryEstimator.CLASS_HEADER_BYTES;
|
||||
import static com.onthegomap.planetiler.util.MemoryEstimator.POINTER_BYTES;
|
||||
import static com.onthegomap.planetiler.util.MemoryEstimator.estimateSize;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmElement;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmReader;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.MemoryEstimator;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import com.onthegomap.planetiler.util.ZoomFunction;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.prep.PreparedGeometry;
|
||||
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for road, shipway, rail, and path names in the {@code
|
||||
* transportation_name} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/transportation_name">OpenMapTiles
|
||||
* transportation_name sql files</a>.
|
||||
*/
|
||||
public class TransportationName implements
|
||||
OpenMapTilesSchema.TransportationName,
|
||||
Tables.OsmHighwayLinestring.Handler,
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
BasemapProfile.FeaturePostProcessor,
|
||||
BasemapProfile.OsmRelationPreprocessor,
|
||||
BasemapProfile.IgnoreWikidata {
|
||||
|
||||
/*
|
||||
* Generate road names from OSM data. Route network and ref are copied
|
||||
* from relations that roads are a part of - except in Great Britain which
|
||||
* uses a naming convention instead of relations.
|
||||
*
|
||||
* The goal is to make name linestrings as long as possible to give clients
|
||||
* the best chance of showing road names at different zoom levels, so do not
|
||||
* limit linestrings by length at process time and merge them at tile
|
||||
* render-time.
|
||||
*
|
||||
* Any 3-way nodes and intersections break line merging so set the
|
||||
* transportation_name_limit_merge argument to true to add temporary
|
||||
* "is link" and "relation" keys to prevent opposite directions of a
|
||||
* divided highway or on/off ramps from getting merged for main highways.
|
||||
*/
|
||||
|
||||
// extra temp key used to group on/off-ramps separately from main highways
|
||||
private static final String LINK_TEMP_KEY = "__islink";
|
||||
private static final String RELATION_ID_TEMP_KEY = "__relid";
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(TransportationName.class);
|
||||
private static final Pattern GREAT_BRITAIN_REF_NETWORK_PATTERN = Pattern.compile("^[AM][0-9AM()]+");
|
||||
private static final ZoomFunction.MeterToPixelThresholds MIN_LENGTH = ZoomFunction.meterThresholds()
|
||||
.put(6, 20_000)
|
||||
.put(7, 20_000)
|
||||
.put(8, 14_000)
|
||||
.put(9, 8_000)
|
||||
.put(10, 8_000)
|
||||
.put(11, 8_000);
|
||||
private static final Comparator<RouteRelation> RELATION_ORDERING = Comparator
|
||||
.<RouteRelation>comparingInt(r -> r.network.ordinal())
|
||||
// TODO also compare network string?
|
||||
.thenComparingInt(r -> r.ref.length())
|
||||
.thenComparing(RouteRelation::ref);
|
||||
private final Map<String, Integer> MINZOOMS;
|
||||
private final boolean brunnel;
|
||||
private final boolean sizeForShield;
|
||||
private final boolean limitMerge;
|
||||
private final Stats stats;
|
||||
private final PlanetilerConfig config;
|
||||
private final AtomicBoolean loggedNoGb = new AtomicBoolean(false);
|
||||
private PreparedGeometry greatBritain = null;
|
||||
|
||||
public TransportationName(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.config = config;
|
||||
this.stats = stats;
|
||||
this.brunnel = config.arguments().getBoolean(
|
||||
"transportation_name_brunnel",
|
||||
"transportation_name layer: set to false to omit brunnel and help merge long highways",
|
||||
false
|
||||
);
|
||||
this.sizeForShield = config.arguments().getBoolean(
|
||||
"transportation_name_size_for_shield",
|
||||
"transportation_name layer: allow road names on shorter segments (ie. they will have a shield)",
|
||||
false
|
||||
);
|
||||
this.limitMerge = config.arguments().getBoolean(
|
||||
"transportation_name_limit_merge",
|
||||
"transportation_name layer: limit merge so we don't combine different relations to help merge long highways",
|
||||
false
|
||||
);
|
||||
boolean z13Paths = config.arguments().getBoolean(
|
||||
"transportation_z13_paths",
|
||||
"transportation(_name) layer: show paths on z13",
|
||||
true
|
||||
);
|
||||
MINZOOMS = Map.of(
|
||||
FieldValues.CLASS_TRACK, 14,
|
||||
FieldValues.CLASS_PATH, z13Paths ? 13 : 14,
|
||||
FieldValues.CLASS_MINOR, 13,
|
||||
FieldValues.CLASS_TRUNK, 8,
|
||||
FieldValues.CLASS_MOTORWAY, 6
|
||||
// default: 12
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processNaturalEarth(String table, SourceFeature feature,
|
||||
FeatureCollector features) {
|
||||
if ("ne_10m_admin_0_countries".equals(table) && feature.hasTag("iso_a2", "GB")) {
|
||||
// multiple threads call this method concurrently, GB polygon *should* only be found
|
||||
// once, but just to be safe synchronize updates to that field
|
||||
synchronized (this) {
|
||||
try {
|
||||
Geometry boundary = feature.polygon().buffer(GeoUtils.metersToPixelAtEquator(0, 10_000) / 256d);
|
||||
greatBritain = PreparedGeometryFactory.prepare(boundary);
|
||||
} catch (GeometryException e) {
|
||||
LOGGER.error("Failed to get Great Britain Polygon: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OsmRelationInfo> preprocessOsmRelation(OsmElement.Relation relation) {
|
||||
if (relation.hasTag("route", "road")) {
|
||||
RouteNetwork networkType = null;
|
||||
String network = relation.getString("network");
|
||||
String ref = relation.getString("ref");
|
||||
|
||||
if ("US:I".equals(network)) {
|
||||
networkType = RouteNetwork.US_INTERSTATE;
|
||||
} else if ("US:US".equals(network)) {
|
||||
networkType = RouteNetwork.US_HIGHWAY;
|
||||
} else if (network != null && network.length() == 5 && network.startsWith("US:")) {
|
||||
networkType = RouteNetwork.US_STATE;
|
||||
} else if (network != null && network.startsWith("CA:transcanada")) {
|
||||
networkType = RouteNetwork.CA_TRANSCANADA;
|
||||
}
|
||||
|
||||
if (networkType != null) {
|
||||
return List.of(new RouteRelation(coalesce(ref, ""), networkType, relation.id()));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmHighwayLinestring element, FeatureCollector features) {
|
||||
List<OsmReader.RelationMember<RouteRelation>> relations = element.source()
|
||||
.relationInfo(RouteRelation.class);
|
||||
|
||||
String ref = element.ref();
|
||||
RouteRelation relation = getRouteRelation(element, relations, ref);
|
||||
if (relation != null && nullIfEmpty(relation.ref) != null) {
|
||||
ref = relation.ref;
|
||||
}
|
||||
|
||||
String name = nullIfEmpty(element.name());
|
||||
ref = nullIfEmpty(ref);
|
||||
String highway = nullIfEmpty(element.highway());
|
||||
|
||||
String highwayClass = highwayClass(element.highway(), null, element.construction(), element.manMade());
|
||||
if (element.isArea() || highway == null || highwayClass == null || (name == null && ref == null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String baseClass = highwayClass.replace("_construction", "");
|
||||
|
||||
int minzoom = MINZOOMS.getOrDefault(baseClass, 12);
|
||||
boolean isLink = highway.endsWith("_link");
|
||||
if (isLink) {
|
||||
minzoom = Math.max(13, minzoom);
|
||||
}
|
||||
|
||||
FeatureCollector.Feature feature = features.line(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setBufferPixelOverrides(MIN_LENGTH)
|
||||
// TODO abbreviate road names - can't port osml10n because it is AGPL
|
||||
.putAttrs(LanguageUtils.getNamesWithoutTranslations(element.source().tags()))
|
||||
.setAttr(Fields.REF, ref)
|
||||
.setAttr(Fields.REF_LENGTH, ref != null ? ref.length() : null)
|
||||
.setAttr(Fields.NETWORK,
|
||||
(relation != null && relation.network != null) ? relation.network.name : ref != null ? "road" : null)
|
||||
.setAttr(Fields.CLASS, highwayClass)
|
||||
.setAttr(Fields.SUBCLASS, highwaySubclass(highwayClass, null, highway))
|
||||
.setMinPixelSize(0)
|
||||
.setSortKey(element.zOrder())
|
||||
.setMinZoom(minzoom);
|
||||
|
||||
if (brunnel) {
|
||||
feature.setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()));
|
||||
}
|
||||
|
||||
/*
|
||||
* to help group roads into longer segments, add temporary tags to limit which segments get grouped together. Since
|
||||
* a divided highway typically has a separate relation for each direction, this ends up keeping segments going
|
||||
* opposite directions group getting grouped together and confusing the line merging process
|
||||
*/
|
||||
if (limitMerge) {
|
||||
feature
|
||||
.setAttr(LINK_TEMP_KEY, isLink ? 1 : 0)
|
||||
.setAttr(RELATION_ID_TEMP_KEY, relation == null ? null : relation.id);
|
||||
}
|
||||
|
||||
if (isFootwayOrSteps(highway)) {
|
||||
feature
|
||||
.setAttrWithMinzoom(Fields.LAYER, nullIf(element.layer(), 0), 12)
|
||||
.setAttrWithMinzoom(Fields.LEVEL, Parse.parseLongOrNull(element.source().getTag("level")), 12)
|
||||
.setAttrWithMinzoom(Fields.INDOOR, element.indoor() ? 1 : null, 12);
|
||||
}
|
||||
}
|
||||
|
||||
private RouteRelation getRouteRelation(Tables.OsmHighwayLinestring element,
|
||||
List<OsmReader.RelationMember<RouteRelation>> relations, String ref) {
|
||||
RouteRelation relation = relations.stream()
|
||||
.map(OsmReader.RelationMember::relation)
|
||||
.min(RELATION_ORDERING)
|
||||
.orElse(null);
|
||||
if (relation == null && ref != null) {
|
||||
// GB doesn't use regular relations like everywhere else, so if we are
|
||||
// in GB then use a naming convention instead.
|
||||
Matcher refMatcher = GREAT_BRITAIN_REF_NETWORK_PATTERN.matcher(ref);
|
||||
if (refMatcher.find()) {
|
||||
if (greatBritain == null) {
|
||||
if (!loggedNoGb.get() && loggedNoGb.compareAndSet(false, true)) {
|
||||
LOGGER.warn("No GB polygon for inferring route network types");
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Geometry wayGeometry = element.source().worldGeometry();
|
||||
if (greatBritain.intersects(wayGeometry)) {
|
||||
RouteNetwork networkType =
|
||||
"motorway".equals(element.highway()) ? RouteNetwork.GB_MOTORWAY : RouteNetwork.GB_TRUNK;
|
||||
relation = new RouteRelation(refMatcher.group(), networkType, 0);
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_transportation_name_gb_test",
|
||||
"Unable to test highway against GB route network: " + element.source().id());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return relation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
double tolerance = config.tolerance(zoom);
|
||||
double minLength = coalesce(MIN_LENGTH.apply(zoom), 0).doubleValue();
|
||||
// TODO tolerances:
|
||||
// z6: (tolerance: 500)
|
||||
// z7: (tolerance: 200)
|
||||
// z8: (tolerance: 120)
|
||||
// z9-11: (tolerance: 50)
|
||||
Function<Map<String, Object>, Double> lengthLimitCalculator =
|
||||
zoom >= 14 ? (p -> 0d) :
|
||||
minLength > 0 ? (p -> minLength) :
|
||||
this::getMinLengthForName;
|
||||
var result = FeatureMerge.mergeLineStrings(items, lengthLimitCalculator, tolerance, BUFFER_SIZE);
|
||||
if (limitMerge) {
|
||||
// remove temp keys that were just used to improve line merging
|
||||
for (var feature : result) {
|
||||
feature.attrs().remove(LINK_TEMP_KEY);
|
||||
feature.attrs().remove(RELATION_ID_TEMP_KEY);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Returns the minimum pixel length that a name will fit into. */
|
||||
private double getMinLengthForName(Map<String, Object> attrs) {
|
||||
Object ref = attrs.get(Fields.REF);
|
||||
Object name = coalesce(attrs.get(Fields.NAME), ref);
|
||||
return (sizeForShield && ref instanceof String) ? 6 :
|
||||
name instanceof String str ? str.length() * 6 : Double.MAX_VALUE;
|
||||
}
|
||||
|
||||
private enum RouteNetwork {
|
||||
|
||||
US_INTERSTATE("us-interstate"),
|
||||
US_HIGHWAY("us-highway"),
|
||||
US_STATE("us-state"),
|
||||
CA_TRANSCANADA("ca-transcanada"),
|
||||
GB_MOTORWAY("gb-motorway"),
|
||||
GB_TRUNK("gb-trunk");
|
||||
|
||||
final String name;
|
||||
|
||||
RouteNetwork(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
/** Information extracted from route relations to use when processing ways in that relation. */
|
||||
private static record RouteRelation(
|
||||
String ref,
|
||||
RouteNetwork network,
|
||||
@Override long id
|
||||
) implements OsmRelationInfo {
|
||||
|
||||
@Override
|
||||
public long estimateMemoryUsageBytes() {
|
||||
return CLASS_HEADER_BYTES +
|
||||
POINTER_BYTES + estimateSize(ref) +
|
||||
POINTER_BYTES + // network
|
||||
MemoryEstimator.estimateSizeLong(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.Utils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for oceans and lakes in the {@code water} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/water">OpenMapTiles
|
||||
* water sql files</a>.
|
||||
*/
|
||||
public class Water implements
|
||||
OpenMapTilesSchema.Water,
|
||||
Tables.OsmWaterPolygon.Handler,
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
BasemapProfile.OsmWaterPolygonProcessor {
|
||||
|
||||
/*
|
||||
* At low zoom levels, use natural earth for oceans and major lakes, and at high zoom levels
|
||||
* use OpenStreetMap data. OpenStreetMap data contains smaller bodies of water, but not
|
||||
* large ocean polygons. For oceans, use https://osmdata.openstreetmap.de/data/water-polygons.html
|
||||
* which infers ocean polygons by preprocessing all coastline elements.
|
||||
*/
|
||||
|
||||
private final MultiExpression.Index<String> classMapping;
|
||||
|
||||
public Water(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.classMapping = FieldMappings.Class.index();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) {
|
||||
record WaterInfo(int minZoom, int maxZoom, String clazz) {}
|
||||
WaterInfo info = switch (table) {
|
||||
case "ne_110m_ocean" -> new WaterInfo(0, 1, FieldValues.CLASS_OCEAN);
|
||||
case "ne_50m_ocean" -> new WaterInfo(2, 4, FieldValues.CLASS_OCEAN);
|
||||
case "ne_10m_ocean" -> new WaterInfo(5, 5, FieldValues.CLASS_OCEAN);
|
||||
|
||||
case "ne_110m_lakes" -> new WaterInfo(0, 1, FieldValues.CLASS_LAKE);
|
||||
case "ne_50m_lakes" -> new WaterInfo(2, 3, FieldValues.CLASS_LAKE);
|
||||
case "ne_10m_lakes" -> new WaterInfo(4, 5, FieldValues.CLASS_LAKE);
|
||||
default -> null;
|
||||
};
|
||||
if (info != null) {
|
||||
features.polygon(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setZoomRange(info.minZoom, info.maxZoom)
|
||||
.setAttr(Fields.CLASS, info.clazz);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processOsmWater(SourceFeature feature, FeatureCollector features) {
|
||||
features.polygon(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_OCEAN)
|
||||
.setMinZoom(6);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmWaterPolygon element, FeatureCollector features) {
|
||||
if (!"bay".equals(element.natural())) {
|
||||
features.polygon(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setMinPixelSizeBelowZoom(11, 2)
|
||||
.setMinZoom(6)
|
||||
.setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0)
|
||||
.setAttrWithMinzoom(Fields.BRUNNEL, Utils.brunnel(element.isBridge(), element.isTunnel()), 12)
|
||||
.setAttr(Fields.CLASS, classMapping.getOrElse(element.source(), FieldValues.CLASS_RIVER));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.carrotsearch.hppc.LongObjectMap;
|
||||
import com.graphhopper.coll.GHLongObjectHashMap;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentSkipListMap;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating map elements for ocean and lake names in the {@code water_name} layer from source
|
||||
* features.
|
||||
* <p>
|
||||
* This class is ported to Java from <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/water_name">OpenMapTiles
|
||||
* water_name sql files</a>.
|
||||
*/
|
||||
public class WaterName implements
|
||||
OpenMapTilesSchema.WaterName,
|
||||
Tables.OsmMarinePoint.Handler,
|
||||
Tables.OsmWaterPolygon.Handler,
|
||||
BasemapProfile.NaturalEarthProcessor,
|
||||
BasemapProfile.LakeCenterlineProcessor {
|
||||
|
||||
/*
|
||||
* Labels for lakes and oceans come primarily from OpenStreetMap data, but we also join
|
||||
* with the lake centerlines source to get linestring geometries for prominent lakes.
|
||||
* We also join with natural earth to make certain important lake/ocean labels visible
|
||||
* at lower zoom levels.
|
||||
*/
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WaterName.class);
|
||||
private static final double WORLD_AREA_FOR_70K_SQUARE_METERS =
|
||||
Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2);
|
||||
private static final double LOG2 = Math.log(2);
|
||||
private final Translations translations;
|
||||
// need to synchronize updates from multiple threads
|
||||
private final LongObjectMap<Geometry> lakeCenterlines = new GHLongObjectHashMap<>();
|
||||
// may be updated concurrently by multiple threads
|
||||
private final ConcurrentSkipListMap<String, Integer> importantMarinePoints = new ConcurrentSkipListMap<>();
|
||||
private final Stats stats;
|
||||
|
||||
public WaterName(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.translations = translations;
|
||||
this.stats = stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
lakeCenterlines.release();
|
||||
importantMarinePoints.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processLakeCenterline(SourceFeature feature, FeatureCollector features) {
|
||||
// TODO pull lake centerline computation into planetiler?
|
||||
long osmId = Math.abs(feature.getLong("OSM_ID"));
|
||||
if (osmId == 0L) {
|
||||
LOGGER.warn("Bad lake centerline. Tags: " + feature.tags());
|
||||
} else {
|
||||
try {
|
||||
// multiple threads call this concurrently
|
||||
synchronized (this) {
|
||||
lakeCenterlines.put(osmId, feature.worldGeometry());
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_water_name_lakeline", "Bad lake centerline: " + feature);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) {
|
||||
// use natural earth named polygons just as a source of name to zoom-level mappings for later
|
||||
if ("ne_10m_geography_marine_polys".equals(table)) {
|
||||
String name = feature.getString("name");
|
||||
Integer scalerank = Parse.parseIntOrNull(feature.getTag("scalerank"));
|
||||
if (name != null && scalerank != null) {
|
||||
name = name.replaceAll("\\s+", " ").trim().toLowerCase();
|
||||
importantMarinePoints.put(name, scalerank);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmMarinePoint element, FeatureCollector features) {
|
||||
if (!element.name().isBlank()) {
|
||||
String place = element.place();
|
||||
var source = element.source();
|
||||
// use name from OSM, but get min zoom from natural earth based on fuzzy name match...
|
||||
Integer rank = Parse.parseIntOrNull(source.getTag("rank"));
|
||||
String name = element.name().toLowerCase();
|
||||
Integer nerank;
|
||||
if ((nerank = importantMarinePoints.get(name)) != null) {
|
||||
rank = nerank;
|
||||
} else if ((nerank = importantMarinePoints.get(source.getString("name:en", "").toLowerCase())) != null) {
|
||||
rank = nerank;
|
||||
} else if ((nerank = importantMarinePoints.get(source.getString("name:es", "").toLowerCase())) != null) {
|
||||
rank = nerank;
|
||||
} else {
|
||||
Map.Entry<String, Integer> next = importantMarinePoints.ceilingEntry(name);
|
||||
if (next != null && next.getKey().startsWith(name)) {
|
||||
rank = next.getValue();
|
||||
}
|
||||
}
|
||||
int minZoom = "ocean".equals(place) ? 0 : rank != null ? rank : 8;
|
||||
features.point(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(LanguageUtils.getNames(source.tags(), translations))
|
||||
.setAttr(Fields.CLASS, place)
|
||||
.setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0)
|
||||
.setMinZoom(minZoom);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmWaterPolygon element, FeatureCollector features) {
|
||||
if (nullIfEmpty(element.name()) != null) {
|
||||
try {
|
||||
Geometry centerlineGeometry = lakeCenterlines.get(element.source().id());
|
||||
FeatureCollector.Feature feature;
|
||||
int minzoom = 9;
|
||||
if (centerlineGeometry != null) {
|
||||
// prefer lake centerline if it exists
|
||||
feature = features.geometry(LAYER_NAME, centerlineGeometry)
|
||||
.setMinPixelSizeBelowZoom(13, 6 * element.name().length());
|
||||
} else {
|
||||
// otherwise just use a label point inside the lake
|
||||
feature = features.pointOnSurface(LAYER_NAME);
|
||||
Geometry geometry = element.source().worldGeometry();
|
||||
double area = geometry.getArea();
|
||||
minzoom = (int) Math.floor(20 - Math.log(area / WORLD_AREA_FOR_70K_SQUARE_METERS) / LOG2);
|
||||
minzoom = Math.min(14, Math.max(9, minzoom));
|
||||
}
|
||||
feature
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_LAKE)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0)
|
||||
.setMinZoom(minzoom);
|
||||
} catch (GeometryException e) {
|
||||
e.log(stats, "omt_water_polygon", "Unable to get geometry for water polygon " + element.source().id());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.layers;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.basemap.BasemapProfile;
|
||||
import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.planetiler.basemap.generated.Tables;
|
||||
import com.onthegomap.planetiler.basemap.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.basemap.util.Utils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import com.onthegomap.planetiler.util.ZoomFunction;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Defines the logic for generating river map elements in the {@code waterway} layer from source features.
|
||||
* <p>
|
||||
* This class is ported to Java from <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/waterway">OpenMapTiles
|
||||
* waterway sql files</a>.
|
||||
*/
|
||||
public class Waterway implements
|
||||
OpenMapTilesSchema.Waterway,
|
||||
Tables.OsmWaterwayLinestring.Handler,
|
||||
BasemapProfile.FeaturePostProcessor,
|
||||
BasemapProfile.NaturalEarthProcessor {
|
||||
|
||||
/*
|
||||
* Uses Natural Earth at lower zoom-levels and OpenStreetMap at higher zoom levels.
|
||||
*
|
||||
* For OpenStreetMap, attempts to merge disconnected linestrings with the same name
|
||||
* at lower zoom levels so that clients can more easily render the name. We also
|
||||
* limit their length at merge-time which only has visibilty into that feature in a
|
||||
* single tile, so at render-time we need to allow through features far enough outside
|
||||
* the tile boundary enough to not accidentally filter out a long river only because a
|
||||
* short segment of it goes through this tile.
|
||||
*/
|
||||
|
||||
private final Translations translations;
|
||||
private final PlanetilerConfig config;
|
||||
|
||||
public Waterway(Translations translations, PlanetilerConfig config, Stats stats) {
|
||||
this.config = config;
|
||||
this.translations = translations;
|
||||
}
|
||||
|
||||
private static final Map<String, Integer> CLASS_MINZOOM = Map.of(
|
||||
"river", 12,
|
||||
"canal", 12,
|
||||
|
||||
"stream", 13,
|
||||
"drain", 13,
|
||||
"ditch", 13
|
||||
);
|
||||
|
||||
private static final ZoomFunction.MeterToPixelThresholds MIN_PIXEL_LENGTHS = ZoomFunction.meterThresholds()
|
||||
.put(9, 8_000)
|
||||
.put(10, 4_000)
|
||||
.put(11, 1_000);
|
||||
|
||||
@Override
|
||||
public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) {
|
||||
if (feature.hasTag("featurecla", "River")) {
|
||||
record ZoomRange(int min, int max) {}
|
||||
ZoomRange zoom = switch (table) {
|
||||
case "ne_110m_rivers_lake_centerlines" -> new ZoomRange(3, 3);
|
||||
case "ne_50m_rivers_lake_centerlines" -> new ZoomRange(4, 5);
|
||||
case "ne_10m_rivers_lake_centerlines" -> new ZoomRange(6, 8);
|
||||
default -> null;
|
||||
};
|
||||
if (zoom != null) {
|
||||
features.line(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, FieldValues.CLASS_RIVER)
|
||||
.setZoomRange(zoom.min, zoom.max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmWaterwayLinestring element, FeatureCollector features) {
|
||||
String waterway = element.waterway();
|
||||
String name = nullIfEmpty(element.name());
|
||||
boolean important = "river".equals(waterway) && name != null;
|
||||
int minzoom = important ? 9 : CLASS_MINZOOM.getOrDefault(element.waterway(), 14);
|
||||
features.line(LAYER_NAME)
|
||||
.setBufferPixels(BUFFER_SIZE)
|
||||
.setAttr(Fields.CLASS, element.waterway())
|
||||
.putAttrs(LanguageUtils.getNames(element.source().tags(), translations))
|
||||
.setMinZoom(minzoom)
|
||||
// details only at higher zoom levels so that named rivers can be merged more aggressively
|
||||
.setAttrWithMinzoom(Fields.BRUNNEL, Utils.brunnel(element.isBridge(), element.isTunnel()), 12)
|
||||
.setAttrWithMinzoom(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0, 12)
|
||||
// at lower zoom levels, we'll merge linestrings and limit length/clip afterwards
|
||||
.setBufferPixelOverrides(MIN_PIXEL_LENGTHS).setMinPixelSizeBelowZoom(11, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
|
||||
if (zoom >= 9 && zoom <= 11) {
|
||||
return FeatureMerge.mergeLineStrings(
|
||||
items,
|
||||
MIN_PIXEL_LENGTHS.apply(zoom).doubleValue(),
|
||||
config.tolerance(zoom),
|
||||
BUFFER_SIZE
|
||||
);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors.
|
||||
All rights reserved.
|
||||
|
||||
Code license: BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Design license: CC-BY 4.0
|
||||
|
||||
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
|
||||
*/
|
||||
package com.onthegomap.planetiler.basemap.util;
|
||||
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty;
|
||||
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Utilities to extract common name fields (name, name_en, name_de, name:latin, name:nonlatin, name_int) that the
|
||||
* OpenMapTiles schema uses across any map element with a name.
|
||||
* <p>
|
||||
* Ported from <a href="https://github.com/openmaptiles/openmaptiles-tools/blob/master/sql/zzz_language.sql">openmaptiles-tools</a>.
|
||||
*/
|
||||
public class LanguageUtils {
|
||||
|
||||
private static final Pattern NONLATIN = Pattern
|
||||
.compile("[^\\x{0000}-\\x{024f}\\x{1E00}-\\x{1EFF}\\x{0300}-\\x{036f}\\x{0259}]");
|
||||
private static final Pattern LETTER = Pattern.compile("[A-Za-zÀ-ÖØ-öø-ÿĀ-ɏ]+");
|
||||
private static final Pattern EMPTY_PARENS = Pattern.compile("(\\([ -.]*\\)|\\[[ -.]*])");
|
||||
private static final Pattern LEADING_TRAILING_JUNK = Pattern.compile("(^\\s*([./-]\\s*)*|(\\s+[./-])*\\s*$)");
|
||||
private static final Pattern WHITESPACE = Pattern.compile("\\s+");
|
||||
private static final Set<String> EN_DE_NAME_KEYS = Set.of("name:en", "name:de");
|
||||
|
||||
private static void putIfNotEmpty(Map<String, Object> dest, String key, Object value) {
|
||||
if (value != null && !value.equals("")) {
|
||||
dest.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static String string(Object obj) {
|
||||
return nullIfEmpty(obj == null ? null : obj.toString());
|
||||
}
|
||||
|
||||
static boolean containsOnlyLatinCharacters(String string) {
|
||||
return string != null && !NONLATIN.matcher(string).find();
|
||||
}
|
||||
|
||||
private static String transliteratedName(Map<String, Object> tags) {
|
||||
return Translations.transliterate(string(tags.get("name")));
|
||||
}
|
||||
|
||||
static String removeLatinCharacters(String name) {
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
var matcher = LETTER.matcher(name);
|
||||
if (matcher.find()) {
|
||||
String result = matcher.replaceAll("");
|
||||
// if the name was "<nonlatin text> (<latin description)"
|
||||
// or "<nonlatin text> - <latin description>"
|
||||
// then remove any of those extra characters now
|
||||
result = EMPTY_PARENS.matcher(result).replaceAll("");
|
||||
result = LEADING_TRAILING_JUNK.matcher(result).replaceAll("");
|
||||
return WHITESPACE.matcher(result).replaceAll(" ");
|
||||
}
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map with default name attributes (name, name_en, name_de, name:latin, name:nonlatin, name_int) that every
|
||||
* element should have, derived from name, int_name, name:en, and name:de tags on the input element.
|
||||
*
|
||||
* <ul>
|
||||
* <li>name is the original name value from the element</li>
|
||||
* <li>name_en is the original name:en value from the element, or name if missing</li>
|
||||
* <li>name_de is the original name:de value from the element, or name/ name_en if missing</li>
|
||||
* <li>name:latin is the first of name, int_name, or any name: attribute that contains only latin characters</li>
|
||||
* <li>name:nonlatin is any nonlatin part of name if present</li>
|
||||
* <li>name_int is the first of int_name name:en name:latin name</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static Map<String, Object> getNamesWithoutTranslations(Map<String, Object> tags) {
|
||||
return getNames(tags, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map with default name attributes that {@link #getNamesWithoutTranslations(Map)} adds, but also
|
||||
* translations for every language that {@code translations} is configured to handle.
|
||||
*/
|
||||
public static Map<String, Object> getNames(Map<String, Object> tags, Translations translations) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
String name = string(tags.get("name"));
|
||||
String intName = string(tags.get("int_name"));
|
||||
String nameEn = string(tags.get("name:en"));
|
||||
String nameDe = string(tags.get("name:de"));
|
||||
|
||||
boolean isLatin = containsOnlyLatinCharacters(name);
|
||||
String latin = isLatin ? name
|
||||
: Stream.concat(Stream.of(nameEn, intName, nameDe), getAllNameTranslationsBesidesEnglishAndGerman(tags))
|
||||
.filter(LanguageUtils::containsOnlyLatinCharacters)
|
||||
.findFirst().orElse(null);
|
||||
if (latin == null && translations != null && translations.getShouldTransliterate()) {
|
||||
latin = transliteratedName(tags);
|
||||
}
|
||||
String nonLatin = isLatin ? null : removeLatinCharacters(name);
|
||||
if (coalesce(nonLatin, "").equals(latin)) {
|
||||
nonLatin = null;
|
||||
}
|
||||
|
||||
putIfNotEmpty(result, "name", name);
|
||||
putIfNotEmpty(result, "name_en", coalesce(nameEn, name));
|
||||
putIfNotEmpty(result, "name_de", coalesce(nameDe, name, nameEn));
|
||||
putIfNotEmpty(result, "name:latin", latin);
|
||||
putIfNotEmpty(result, "name:nonlatin", nonLatin);
|
||||
putIfNotEmpty(result, "name_int", coalesce(
|
||||
intName,
|
||||
nameEn,
|
||||
latin,
|
||||
name
|
||||
));
|
||||
|
||||
if (translations != null) {
|
||||
translations.addTranslations(result, tags);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Stream<String> getAllNameTranslationsBesidesEnglishAndGerman(Map<String, Object> tags) {
|
||||
return tags.entrySet().stream()
|
||||
.filter(e -> {
|
||||
String key = e.getKey();
|
||||
return key.startsWith("name:") && !EN_DE_NAME_KEYS.contains(key);
|
||||
})
|
||||
.map(Map.Entry::getValue)
|
||||
.map(LanguageUtils::string);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.onthegomap.planetiler.basemap.util;
|
||||
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Common utilities for working with data and the OpenMapTiles schema in {@code layers} implementations.
|
||||
*/
|
||||
public class Utils {
|
||||
|
||||
public static <T> T coalesce(T a, T b) {
|
||||
return a != null ? a : b;
|
||||
}
|
||||
|
||||
public static <T> T coalesce(T a, T b, T c) {
|
||||
return a != null ? a : b != null ? b : c;
|
||||
}
|
||||
|
||||
public static <T> T coalesce(T a, T b, T c, T d) {
|
||||
return a != null ? a : b != null ? b : c != null ? c : d;
|
||||
}
|
||||
|
||||
public static <T> T coalesce(T a, T b, T c, T d, T e) {
|
||||
return a != null ? a : b != null ? b : c != null ? c : d != null ? d : e;
|
||||
}
|
||||
|
||||
public static <T> T coalesce(T a, T b, T c, T d, T e, T f) {
|
||||
return a != null ? a : b != null ? b : c != null ? c : d != null ? d : e != null ? e : f;
|
||||
}
|
||||
|
||||
/** Returns {@code a} or {@code nullValue} if {@code a} is null. */
|
||||
public static <T> T nullIf(T a, T nullValue) {
|
||||
return nullValue.equals(a) ? null : a;
|
||||
}
|
||||
|
||||
/** Returns {@code a}, or null if {@code a} is "". */
|
||||
public static String nullIfEmpty(String a) {
|
||||
return (a == null || a.isEmpty()) ? null : a;
|
||||
}
|
||||
|
||||
/** Returns true if {@code a} is null, or its {@link Object#toString()} value is "". */
|
||||
public static boolean nullOrEmpty(Object a) {
|
||||
return a == null || a.toString().isEmpty();
|
||||
}
|
||||
|
||||
/** Returns a map with {@code ele} (meters) and {ele_ft} attributes from an elevation in meters. */
|
||||
public static Map<String, Object> elevationTags(int meters) {
|
||||
return Map.of(
|
||||
"ele", meters,
|
||||
"ele_ft", (int) Math.round(meters * 3.2808399)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map with {@code ele} (meters) and {ele_ft} attributes from an elevation string in meters, if {@code
|
||||
* meters} can be parsed as a valid number.
|
||||
*/
|
||||
public static Map<String, Object> elevationTags(String meters) {
|
||||
Integer ele = Parse.parseIntSubstring(meters);
|
||||
return ele == null ? Map.of() : elevationTags(ele);
|
||||
}
|
||||
|
||||
/** Returns "bridge" or "tunnel" string used for "brunnel" attribute by OpenMapTiles schema. */
|
||||
public static String brunnel(boolean isBridge, boolean isTunnel) {
|
||||
return brunnel(isBridge, isTunnel, false);
|
||||
}
|
||||
|
||||
/** Returns "bridge" or "tunnel" or "ford" string used for "brunnel" attribute by OpenMapTiles schema. */
|
||||
public static String brunnel(boolean isBridge, boolean isTunnel, boolean isFord) {
|
||||
return isBridge ? "bridge" : isTunnel ? "tunnel" : isFord ? "ford" : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.onthegomap.planetiler.basemap.util;
|
||||
|
||||
import com.onthegomap.planetiler.mbtiles.Mbtiles;
|
||||
import com.onthegomap.planetiler.mbtiles.Verify;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import org.locationtech.jts.geom.Envelope;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
|
||||
/**
|
||||
* A utility to check the contents of an mbtiles file generated for Monaco.
|
||||
*/
|
||||
public class VerifyMonaco {
|
||||
|
||||
public static final Envelope MONACO_BOUNDS = new Envelope(7.40921, 7.44864, 43.72335, 43.75169);
|
||||
|
||||
/**
|
||||
* Returns a verification result with a basic set of checks against an openmaptiles map built from an extract for
|
||||
* Monaco.
|
||||
*/
|
||||
public static Verify verify(Mbtiles mbtiles) {
|
||||
Verify verify = Verify.verify(mbtiles);
|
||||
verify.checkMinFeatureCount(MONACO_BOUNDS, "building", Map.of(), 13, 14, 100, Polygon.class);
|
||||
verify.checkMinFeatureCount(MONACO_BOUNDS, "transportation", Map.of(), 10, 14, 5, LineString.class);
|
||||
verify.checkMinFeatureCount(MONACO_BOUNDS, "landcover", Map.of(
|
||||
"class", "grass",
|
||||
"subclass", "park"
|
||||
), 14, 10, Polygon.class);
|
||||
verify.checkMinFeatureCount(MONACO_BOUNDS, "water", Map.of("class", "ocean"), 0, 14, 1, Polygon.class);
|
||||
verify.checkMinFeatureCount(MONACO_BOUNDS, "place", Map.of("class", "country"), 2, 14, 1, Point.class);
|
||||
return verify;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
try (var mbtiles = Mbtiles.newReadOnlyDatabase(Path.of(args[0]))) {
|
||||
var result = verify(mbtiles);
|
||||
result.print();
|
||||
result.failIfErrors();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
225
src/test/java/com/onthegomap/planetiler/basemap/BasemapTest.java
Normal file
225
src/test/java/com/onthegomap/planetiler/basemap/BasemapTest.java
Normal file
@@ -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.
|
||||
* <p>
|
||||
* 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<String, String> 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<Mbtiles.TileEntry> 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<DynamicTest> testVerifyChecks() {
|
||||
return VerifyMonaco.verify(mbtiles).results().stream()
|
||||
.map(check -> dynamicTest(check.name(), () -> {
|
||||
check.error().ifPresent(Assertions::fail);
|
||||
}));
|
||||
}
|
||||
|
||||
private static void assertNumFeatures(String layer, Map<String, Object> attrs, int zoom,
|
||||
int expected, Class<? extends Geometry> clazz) {
|
||||
TestUtils.assertNumFeatures(mbtiles, layer, zoom, attrs, MONACO_BOUNDS, expected, clazz);
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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<String> 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<String> 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<String> 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<String> 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<DynamicTest> 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<String, JsonNode> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<Map<String, Object>> expected, Iterable<FeatureCollector.Feature> actual) {
|
||||
List<FeatureCollector.Feature> 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<String, Object> 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<String, Object> 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<String, Object> props) {
|
||||
return SimpleFeature.create(
|
||||
newPoint(0, 0),
|
||||
new HashMap<>(props),
|
||||
OSM_SOURCE,
|
||||
null,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
SourceFeature lineFeature(Map<String, Object> props) {
|
||||
return SimpleFeature.create(
|
||||
newLineString(0, 0, 1, 1),
|
||||
new HashMap<>(props),
|
||||
OSM_SOURCE,
|
||||
null,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
SourceFeature polygonFeatureWithArea(double area, Map<String, Object> props) {
|
||||
return SimpleFeature.create(
|
||||
GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(area))),
|
||||
new HashMap<>(props),
|
||||
OSM_SOURCE,
|
||||
null,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
SourceFeature polygonFeature(Map<String, Object> props) {
|
||||
return polygonFeatureWithArea(1, props);
|
||||
}
|
||||
|
||||
|
||||
protected SimpleFeature lineFeatureWithRelation(List<OsmRelationInfo> relationInfos,
|
||||
Map<String, Object> map) {
|
||||
return SimpleFeature.createFakeOsmFeature(
|
||||
newLineString(0, 0, 1, 1),
|
||||
map,
|
||||
OSM_SOURCE,
|
||||
null,
|
||||
0,
|
||||
(relationInfos == null ? List.<OsmRelationInfo>of() : relationInfos).stream()
|
||||
.map(r -> new OsmReader.RelationMember<>("", r)).toList()
|
||||
);
|
||||
}
|
||||
|
||||
protected void testMergesLinestrings(Map<String, Object> 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<String, Object> 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))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
))));
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
))));
|
||||
}
|
||||
}
|
||||
@@ -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<FeatureCollector.Feature> 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<FeatureCollector.Feature> features = new ArrayList<>();
|
||||
profile.finish(OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add);
|
||||
assertFeatures(0, List.of(Map.of(
|
||||
"adm0_r", "<null>",
|
||||
"adm0_l", "<null>",
|
||||
"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<FeatureCollector.Feature> 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", "<null>"
|
||||
), Map.of(
|
||||
"adm0_r", "C1",
|
||||
"adm0_l", "<null>"
|
||||
)), 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<FeatureCollector.Feature> features = new ArrayList<>();
|
||||
profile.finish(OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add);
|
||||
assertFeatures(0, List.of(Map.of(
|
||||
"adm0_l", "<null>",
|
||||
"adm0_r", "<null>"
|
||||
)), 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<FeatureCollector.Feature> features = new ArrayList<>();
|
||||
profile.finish(OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add);
|
||||
assertFeatures(0, List.of(Map.of(
|
||||
"adm0_l", "<null>",
|
||||
"adm0_r", "<null>"
|
||||
), Map.of(
|
||||
"adm0_l", "<null>",
|
||||
"adm0_r", "C1"
|
||||
)), features);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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", "<null>",
|
||||
"hide_3d", "<null>",
|
||||
"_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", "<null>",
|
||||
"render_min_height", "<null>"
|
||||
)), 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", "<null>"
|
||||
)), process(polygonFeature(Map.of(
|
||||
"building", "yes",
|
||||
"building:building", "yes",
|
||||
"building:colour", "#ff0000"
|
||||
))));
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
))));
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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<String, Object> 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<String, Object> m) {
|
||||
return new VectorTile.Feature(
|
||||
"landcover",
|
||||
1,
|
||||
VectorTile.encodeGeometry(geom),
|
||||
new HashMap<>(m),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
private void assertMerges(List<Map<String, Object>> expected, List<VectorTile.Feature> in, int zoom)
|
||||
throws GeometryException {
|
||||
assertEquals(expected,
|
||||
profile.postProcessLayerFeatures("landcover", zoom, in).stream().map(
|
||||
VectorTile.Feature::attrs)
|
||||
.toList());
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
))));
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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
|
||||
)
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -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", "<null>",
|
||||
"_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", "<null>",
|
||||
"_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<String, Object> tags) {
|
||||
var iter = process(polygonFeatureWithArea(area, tags)).iterator();
|
||||
iter.next();
|
||||
return iter.next().getSortKey();
|
||||
}
|
||||
}
|
||||
@@ -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", "<null>"
|
||||
)), 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", "<null>",
|
||||
"_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", "<null>",
|
||||
"_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", "<null>",
|
||||
"_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
|
||||
)
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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", "<null>",
|
||||
"_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", "<null>",
|
||||
"_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", "<null>",
|
||||
"_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", "<null>",
|
||||
"_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", "<null>"
|
||||
)), 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", "<null>"
|
||||
)), 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
|
||||
)
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -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", "<null>",
|
||||
"_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", "<null>",
|
||||
"oneway", "<null>",
|
||||
"ramp", "<null>",
|
||||
"bicycle", "<null>",
|
||||
"foot", "<null>"
|
||||
)), 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", "<null>",
|
||||
"_minzoom", 6
|
||||
)), features);
|
||||
|
||||
assertFeatures(8, List.of(Map.of(
|
||||
"_layer", "transportation",
|
||||
"class", "motorway",
|
||||
"surface", "<null>",
|
||||
"oneway", "<null>",
|
||||
"ramp", "<null>",
|
||||
"bicycle", "<null>",
|
||||
"foot", "<null>",
|
||||
"horse", "<null>",
|
||||
"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", "<null>",
|
||||
"_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", "<null>",
|
||||
"_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", "<null>",
|
||||
"layer", "<null>",
|
||||
|
||||
"_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<String, Object> 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"))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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.<String, Object>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.<String, Object>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.<String, Object>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"
|
||||
))));
|
||||
}
|
||||
}
|
||||
@@ -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", "<null>",
|
||||
"_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", "<null>",
|
||||
"_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", "<null>",
|
||||
"_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", "<null>",
|
||||
"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
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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", "<null>",
|
||||
"_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", "<null>",
|
||||
"intermittent", "<null>",
|
||||
|
||||
"_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", "<null>",
|
||||
|
||||
"_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", "<null>",
|
||||
|
||||
"_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
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user