Files
planetiler-openmaptiles/src/main/java/org/openmaptiles/layers/Landcover.java
Peter Hanecak c662ef2d30 Update to OMT 3.14 (#50)
* initial update step: 3.13.1 bumped to 3.14

* automated update steps: regenerate-openmaptiles.sh v3.14 + mvn spotless:apply

* riverbank was removed from waterway -> replaced with dock un the unit test

* riverbank was removed from waterway -> testRiverbank() changed into testDock()

* riverbank was removed in OpenMapTiles v3.14

* parcel_locker (with brand, operator and/or ref) was added in OpenMapTiles v3.14

* bus_guideway was added in OpenMapTiles v3.14

* support for county seats (capital=6) was added in OpenMapTiles v3.14

* spotless:apply

* gn_ascii replaced with name_en

gn_ascii was removed from NE5 - see commit b14da2ea in OMT for more
details

* merging of buildings at Z13 replicated also for landuse from Z9 (or for some from Z6) to Z13 to match updates in OpenMapTiles v3.14

* landuse_merge_z9_to_z13 argument removed since it has negligible perf. overhead hence no need to have it

* cover a corner case for parcel_locker: no brand, no operator, just ref

* corrected landuse polygon merging to better match OpenMapTiles 3.14 (1)

* corrected landuse polygon merging to better match OpenMapTiles 3.14 (1): merge only landuse=residential

* clean-up: since evaluation in Landuse is simpler than in Landcover, streaming and Collectors.partitioningBy() used

* clean-up of naming: splitList -> splitLists

* land-use merging unit test adjusted and extended to match recent changes

* other unit tests adjusted to match recent changes

* spotless:apply

* clean-up: added comment, to match Landover

* 3.13.1 bumped to 3.14 (follow-up/fix for e26e13a6f)

* adjusted WOOD_OR_FOREST handling for Z8 to match change in OMT (osm_landcover_gen_z8)

* updated unit test to match adjusted WOOD_OR_FOREST handling for Z8

* clean-up: if-else replaced with switch

* clean-up: conditional grouped to make it more readable
2022-12-28 13:36:12 +01:00

184 lines
7.4 KiB
Java

/*
Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors.
All rights reserved.
Code license: BSD 3-Clause License
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Design license: CC-BY 4.0
See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for details on usage
*/
package org.openmaptiles.layers;
import com.onthegomap.planetiler.FeatureCollector;
import com.onthegomap.planetiler.FeatureMerge;
import com.onthegomap.planetiler.VectorTile;
import com.onthegomap.planetiler.config.PlanetilerConfig;
import com.onthegomap.planetiler.expression.MultiExpression;
import com.onthegomap.planetiler.geo.GeometryException;
import com.onthegomap.planetiler.reader.SourceFeature;
import com.onthegomap.planetiler.stats.Stats;
import com.onthegomap.planetiler.util.Translations;
import com.onthegomap.planetiler.util.ZoomFunction;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.openmaptiles.OpenMapTilesProfile;
import org.openmaptiles.generated.OpenMapTilesSchema;
import org.openmaptiles.generated.Tables;
/**
* Defines the logic for generating map elements for natural land cover polygons like ice, sand, and forest in the
* {@code landcover} layer from source features.
* <p>
* This class is ported to Java from
* <a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers/landcover">OpenMapTiles landcover sql
* files</a>.
*/
public class Landcover implements
OpenMapTilesSchema.Landcover,
OpenMapTilesProfile.NaturalEarthProcessor,
Tables.OsmLandcoverPolygon.Handler,
OpenMapTilesProfile.FeaturePostProcessor {
/*
* Large ice areas come from natural earth and the rest come from OpenStreetMap at higher zoom
* levels. At render-time, postProcess() merges polygons into larger connected area based
* on the number of points in the original area. Since postProcess() only has visibility into
* features on a single tile, process() needs to pass the number of points the original feature
* had through using a temporary "_numpoints" attribute.
*/
public static final ZoomFunction<Number> MIN_PIXEL_SIZE_THRESHOLDS = ZoomFunction.fromMaxZoomThresholds(Map.of(
13, 8,
10, 4,
9, 2
));
private static final String TEMP_NUM_POINTS_ATTR = "_numpoints";
private static final Set<String> WOOD_OR_FOREST = Set.of(
FieldValues.SUBCLASS_WOOD,
FieldValues.SUBCLASS_FOREST
);
private final MultiExpression.Index<String> classMapping;
public Landcover(Translations translations, PlanetilerConfig config, Stats stats) {
this.classMapping = FieldMappings.Class.index();
}
private String getClassFromSubclass(String subclass) {
return subclass == null ? null : classMapping.getOrElse(Map.of(Fields.SUBCLASS, subclass), null);
}
@Override
public void processNaturalEarth(String table, SourceFeature feature,
FeatureCollector features) {
record LandcoverInfo(String subclass, int minzoom, int maxzoom) {}
LandcoverInfo info = switch (table) {
case "ne_110m_glaciated_areas" -> new LandcoverInfo(FieldValues.SUBCLASS_GLACIER, 0, 1);
case "ne_50m_glaciated_areas" -> new LandcoverInfo(FieldValues.SUBCLASS_GLACIER, 2, 4);
case "ne_10m_glaciated_areas" -> new LandcoverInfo(FieldValues.SUBCLASS_GLACIER, 5, 6);
case "ne_50m_antarctic_ice_shelves_polys" -> new LandcoverInfo("ice_shelf", 2, 4);
case "ne_10m_antarctic_ice_shelves_polys" -> new LandcoverInfo("ice_shelf", 5, 6);
default -> null;
};
if (info != null) {
String clazz = getClassFromSubclass(info.subclass);
if (clazz != null) {
features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
.setAttr(Fields.CLASS, clazz)
.setAttr(Fields.SUBCLASS, info.subclass)
.setZoomRange(info.minzoom, info.maxzoom);
}
}
}
@Override
public void process(Tables.OsmLandcoverPolygon element, FeatureCollector features) {
String subclass = element.subclass();
String clazz = getClassFromSubclass(subclass);
if (clazz != null) {
features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
.setMinPixelSizeOverrides(MIN_PIXEL_SIZE_THRESHOLDS)
.setAttr(Fields.CLASS, clazz)
.setAttr(Fields.SUBCLASS, subclass)
.setNumPointsAttr(TEMP_NUM_POINTS_ATTR)
.setMinZoom(7);
}
}
@Override
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) throws GeometryException {
if (zoom < 7 || zoom > 13) {
for (var item : items) {
item.attrs().remove(TEMP_NUM_POINTS_ATTR);
}
return items;
} else { // z7-13
// merging only merges polygons with the same attributes, so use this temporary key
// to separate features into layers that will be merged separately
String tempGroupKey = "_group";
List<VectorTile.Feature> result = new ArrayList<>();
List<VectorTile.Feature> toMerge = new ArrayList<>();
for (var item : items) {
Map<String, Object> attrs = item.attrs();
Object numPointsObj = attrs.remove(TEMP_NUM_POINTS_ATTR);
Object subclassObj = attrs.get(Fields.SUBCLASS);
if (numPointsObj instanceof Number num && subclassObj instanceof String subclass) {
long numPoints = num.longValue();
if (zoom >= 10) {
if (WOOD_OR_FOREST.contains(subclass) && numPoints < 300) {
attrs.put(tempGroupKey, "<300");
toMerge.add(item);
} else { // don't merge
result.add(item);
}
} else if (zoom >= 8 && zoom <= 9) {
if (WOOD_OR_FOREST.contains(subclass)) {
attrs.put(tempGroupKey, numPoints < 300 ? "<300" : ">300");
toMerge.add(item);
} else { // don't merge
result.add(item);
}
} else { // zoom 7
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;
}
}
}