diff --git a/LICENSE.md b/LICENSE.md index 860a7563..973211fd 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2022, MapTiler.com & OpenMapTiles contributors. +Copyright (c) 2024, MapTiler.com & OpenMapTiles contributors. All rights reserved. The vector tile schema has been developed by Klokan Technologies GmbH and diff --git a/README.md b/README.md index ee3ae320..24d43f9d 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,8 @@ available options. lines, to revert this behavior set `--transportation-name-brunnel=true` - `rank` field on `mountain_peak` linestrings only has 3 levels (1: has wikipedia page and name, 2: has name, 3: no name or wikipedia page or name) -- some line and polygon tolerances are different, can be tweaked with `--simplify-tolerance` parameter +- Some line and polygon tolerances are different, can be tweaked with `--simplify-tolerance` parameter +- For bigger bays whose label points show above Z9, centerline is used for Z9+ ## Customizing @@ -149,7 +150,7 @@ script with the OpenMapTiles release tag: ```bash -./scripts/regenerate-openmaptiles.sh v3.14 +./scripts/regenerate-openmaptiles.sh v3.15 ``` Then follow the instructions it prints for reformatting generated code. @@ -157,7 +158,7 @@ Then follow the instructions it prints for reformatting generated code. If you want to regenerate from a different repository than the default openmaptiles, you can specify the url like this: ```bash -./scripts/regenerate-openmaptiles.sh v3.14 https://raw.githubusercontent.com/openmaptiles/openmaptiles/ +./scripts/regenerate-openmaptiles.sh v3.15 https://raw.githubusercontent.com/openmaptiles/openmaptiles/ ``` ## License @@ -165,8 +166,8 @@ If you want to regenerate from a different repository than the default openmapti All code in this repository is under the [BSD license](./LICENSE.md) and the cartography decisions encoded in the schema and SQL are licensed under [CC-BY](./LICENSE.md). -Products or services using maps derived from OpenMapTiles schema need to visibly credit "OpenMapTiles.org" or -reference "OpenMapTiles" with a link to https://openmaptiles.org/. Exceptions to attribution requirement can be granted +Products or services using maps derived from OpenMapTiles schema need to **visibly credit "OpenMapTiles.org"** or +**reference "OpenMapTiles"** with a link to https://openmaptiles.org/. Exceptions to attribution requirement can be granted on request. For a browsable electronic map based on OpenMapTiles and OpenStreetMap data, the diff --git a/scripts/regenerate-openmaptiles.sh b/scripts/regenerate-openmaptiles.sh index 2597334f..a598d3b8 100755 --- a/scripts/regenerate-openmaptiles.sh +++ b/scripts/regenerate-openmaptiles.sh @@ -4,8 +4,7 @@ set -o errexit set -o pipefail set -o nounset -# TODO: change to "v3.15" once that is released -TAG="${1:-"master"}" +TAG="${1:-"v3.15"}" echo "tag=${TAG}" BASE_URL="${2:-"https://raw.githubusercontent.com/openmaptiles/openmaptiles/"}" diff --git a/src/main/java/org/openmaptiles/Generate.java b/src/main/java/org/openmaptiles/Generate.java index c8af4db9..69782ffd 100644 --- a/src/main/java/org/openmaptiles/Generate.java +++ b/src/main/java/org/openmaptiles/Generate.java @@ -61,7 +61,7 @@ public class Generate { private static final String LINE_SEPARATOR = System.lineSeparator(); private static final String GENERATED_FILE_HEADER = """ /* - Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. + Copyright (c) 2024, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -133,7 +133,7 @@ static JsonNode parseYaml(String string) { 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.14.0"); + String tag = arguments.getString("tag", "openmaptiles tag to use", "v3.15.0"); String baseUrl = arguments.getString("base-url", "the url used to download the openmaptiles.yml", "https://raw.githubusercontent.com/openmaptiles/openmaptiles/"); String base = baseUrl + tag + "/"; diff --git a/src/main/java/org/openmaptiles/OpenMapTilesMain.java b/src/main/java/org/openmaptiles/OpenMapTilesMain.java index 6b82c0d3..b51156f1 100644 --- a/src/main/java/org/openmaptiles/OpenMapTilesMain.java +++ b/src/main/java/org/openmaptiles/OpenMapTilesMain.java @@ -4,12 +4,16 @@ import com.onthegomap.planetiler.config.Arguments; import java.nio.file.Path; import org.openmaptiles.generated.OpenMapTilesSchema; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Main entrypoint for generating a map using the OpenMapTiles schema. */ public class OpenMapTilesMain { + private static final Logger LOGGER = LoggerFactory.getLogger(OpenMapTilesMain.class); + public static void main(String[] args) throws Exception { run(Arguments.fromArgsOrConfigFile(args)); } @@ -52,5 +56,15 @@ static void run(Arguments arguments) throws Exception { // override with --mbtiles=... argument or MBTILES=... env var or mbtiles=... in a config file .setOutput("mbtiles", dataDir.resolve("output.mbtiles")) .run(); + + LOGGER.info(""" + Acknowledgments + Generated vector tiles are produced work of OpenStreetMap data. + Such tiles are reusable under CC-BY license granted by OpenMapTiles team: + - https://github.com/openmaptiles/openmaptiles/#license + Maps made with these vector tiles must display a visible credit: + - © OpenMapTiles © OpenStreetMap contributors + Thanks to all free, open source software developers and Open Data Contributors! + """); } } diff --git a/src/main/java/org/openmaptiles/generated/OpenMapTilesSchema.java b/src/main/java/org/openmaptiles/generated/OpenMapTilesSchema.java index 676b5a46..a8f2e4d1 100644 --- a/src/main/java/org/openmaptiles/generated/OpenMapTilesSchema.java +++ b/src/main/java/org/openmaptiles/generated/OpenMapTilesSchema.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. +Copyright (c) 2024, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -49,14 +49,14 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE /** * All vector tile layer definitions, attributes, and allowed values generated from the - * OpenMapTiles vector tile schema - * master. + * OpenMapTiles vector tile schema + * v3.15. */ @SuppressWarnings("unused") public class OpenMapTilesSchema { public static final String NAME = "OpenMapTiles"; public static final String DESCRIPTION = "A tileset showcasing all layers in OpenMapTiles. https://openmaptiles.org"; - public static final String VERSION = "3.14.0"; + public static final String VERSION = "3.15.0"; public static final String ATTRIBUTION = "© OpenMapTiles © OpenStreetMap contributors"; public static final List LANGUAGES = List.of("am", "ar", "az", "be", "bg", "bn", "br", "bs", "ca", "co", "cs", @@ -96,7 +96,7 @@ public static List createInstances(Translations translations, PlanetilerC * boundaries show up. So you might not be able to use border styling for ocean water features. * * Generated from - * water.yaml + * water.yaml */ public interface Water extends Layer { double BUFFER_SIZE = 4.0; @@ -182,7 +182,7 @@ final class FieldMappings { public static final MultiExpression Class = MultiExpression.of(List.of(MultiExpression.entry("dock", matchAny("waterway", "dock")), MultiExpression.entry("river", matchAny("water", "river", "stream", "canal", "ditch", "drain")), - MultiExpression.entry("pond", matchAny("water", "pond", "basin", "wastewater")), + MultiExpression.entry("pond", matchAny("water", "pond", "basin", "wastewater", "salt_pond")), MultiExpression.entry("lake", FALSE), MultiExpression.entry("ocean", FALSE), MultiExpression.entry("swimming_pool", matchAny("leisure", "swimming_pool")))); } @@ -195,7 +195,7 @@ final class FieldMappings { * field applied. Waterways do not have a subclass field. * * Generated from - * waterway.yaml + * waterway.yaml */ public interface Waterway extends Layer { double BUFFER_SIZE = 4.0; @@ -287,7 +287,7 @@ final class FieldMappings { * layer is to style wood (class=wood) and grass (class=grass) areas. * * Generated from landcover.yaml + * "https://github.com/openmaptiles/openmaptiles/blob/v3.15/layers/landcover/landcover.yaml">landcover.yaml */ public interface Landcover extends Layer { double BUFFER_SIZE = 4.0; @@ -444,7 +444,7 @@ final class FieldMappings { * residential (urban) areas and at higher zoom levels mostly OSM landuse tags. * * Generated from - * landuse.yaml + * landuse.yaml */ public interface Landuse extends Layer { double BUFFER_SIZE = 4.0; @@ -540,7 +540,7 @@ final class FieldMappings { * Natural peaks * * Generated from mountain_peak.yaml + * "https://github.com/openmaptiles/openmaptiles/blob/v3.15/layers/mountain_peak/mountain_peak.yaml">mountain_peak.yaml */ public interface MountainPeak extends Layer { double BUFFER_SIZE = 64.0; @@ -563,7 +563,10 @@ final class Fields { * removed in a future release in favor of name:en. */ public static final String NAME_EN = "name_en"; - /** German name name:de if available, otherwise name or name:en. */ + /** + * German name name:de if available, otherwise name or name:en. This is + * deprecated and will be removed in a future release in favor of name:de. + */ public static final String NAME_DE = "name_de"; /** @@ -621,7 +624,7 @@ final class FieldMappings { * leisure=nature_reserve. * * Generated from - * park.yaml + * park.yaml */ public interface Park extends Layer { double BUFFER_SIZE = 4.0; @@ -684,7 +687,7 @@ final class FieldMappings { * but for most styles it makes sense to just style admin_level=2 and admin_level=4. * * Generated from - * boundary.yaml + * boundary.yaml */ public interface Boundary extends Layer { double BUFFER_SIZE = 4.0; @@ -794,7 +797,7 @@ final class FieldMappings { * in the aeroway layer. * * Generated from - * aeroway.yaml + * aeroway.yaml */ public interface Aeroway extends Layer { double BUFFER_SIZE = 4.0; @@ -854,7 +857,7 @@ final class FieldMappings { * features like plazas. * * Generated from transportation.yaml + * "https://github.com/openmaptiles/openmaptiles/blob/v3.15/layers/transportation/transportation.yaml">transportation.yaml */ public interface Transportation extends Layer { double BUFFER_SIZE = 4.0; @@ -1199,7 +1202,7 @@ final class FieldMappings { * location:underground are excluded. * * Generated from - * building.yaml + * building.yaml */ public interface Building extends Layer { double BUFFER_SIZE = 4.0; @@ -1241,7 +1244,7 @@ final class FieldMappings { * from OSM water bodies. Only the most important lakes contain labels. * * Generated from water_name.yaml + * "https://github.com/openmaptiles/openmaptiles/blob/v3.15/layers/water_name/water_name.yaml">water_name.yaml */ public interface WaterName extends Layer { double BUFFER_SIZE = 256.0; @@ -1318,7 +1321,7 @@ final class FieldMappings { * while for other roads you should use name. * * Generated from transportation_name.yaml + * "https://github.com/openmaptiles/openmaptiles/blob/v3.15/layers/transportation_name/transportation_name.yaml">transportation_name.yaml */ public interface TransportationName extends Layer { double BUFFER_SIZE = 8.0; @@ -1590,7 +1593,7 @@ final class FieldMappings { * create a text hierarchy. * * Generated from - * place.yaml + * place.yaml */ public interface Place extends Layer { double BUFFER_SIZE = 256.0; @@ -1714,7 +1717,7 @@ final class FieldMappings { * tag are prioritized for preservation). * * Generated from housenumber.yaml + * "https://github.com/openmaptiles/openmaptiles/blob/v3.15/layers/housenumber/housenumber.yaml">housenumber.yaml */ public interface Housenumber extends Layer { double BUFFER_SIZE = 8.0; @@ -1746,7 +1749,7 @@ final class FieldMappings { * Points of interests containing a of a variety * of OpenStreetMap tags. Mostly contains amenities, sport, shop and tourist POIs. * - * Generated from poi.yaml + * Generated from poi.yaml */ public interface Poi extends Layer { double BUFFER_SIZE = 64.0; @@ -1990,7 +1993,7 @@ final class FieldMappings { * Aerodrome labels * * Generated from aerodrome_label.yaml + * "https://github.com/openmaptiles/openmaptiles/blob/v3.15/layers/aerodrome_label/aerodrome_label.yaml">aerodrome_label.yaml */ public interface AerodromeLabel extends Layer { double BUFFER_SIZE = 64.0; diff --git a/src/main/java/org/openmaptiles/generated/Tables.java b/src/main/java/org/openmaptiles/generated/Tables.java index 88fa4ec2..7c87d28a 100644 --- a/src/main/java/org/openmaptiles/generated/Tables.java +++ b/src/main/java/org/openmaptiles/generated/Tables.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. +Copyright (c) 2024, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -50,7 +50,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE /** * OSM element parsers generated from the imposm3 table definitions - * in the OpenMapTiles vector tile + * in the OpenMapTiles vector tile * schema. * * These filter and parse the raw OSM key/value attribute pairs on tags into records with fields that match the columns diff --git a/src/main/java/org/openmaptiles/layers/Boundary.java b/src/main/java/org/openmaptiles/layers/Boundary.java index 99a29977..2f72e4fe 100644 --- a/src/main/java/org/openmaptiles/layers/Boundary.java +++ b/src/main/java/org/openmaptiles/layers/Boundary.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. +Copyright (c) 2024, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -121,6 +121,8 @@ public class Boundary implements private static final Logger LOGGER = LoggerFactory.getLogger(Boundary.class); private static final double COUNTRY_TEST_OFFSET = GeoUtils.metersToPixelAtEquator(0, 10) / 256d; + private static final String COUNTRY_KE = "Kenya"; + private static final String COUNTRY_SS = "South Sudan"; private final Stats stats; private final boolean addCountryNames; private final boolean onlyOsmBoundaries; @@ -179,8 +181,21 @@ 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_0_boundary_lines_land" -> { + boolean isDisputedSouthSudanAndKenya = false; + if (disputed) { + String left = feature.getString("adm0_left"); + String right = feature.getString("adm0_right"); + if (COUNTRY_SS.equals(left)) { + isDisputedSouthSudanAndKenya = COUNTRY_KE.equals(right); + } else if (COUNTRY_KE.equals(left)) { + isDisputedSouthSudanAndKenya = COUNTRY_SS.equals(right); + } + } + yield isDisputedSouthSudanAndKenya ? new BoundaryInfo(2, 1, 4) : + 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) : diff --git a/src/main/java/org/openmaptiles/layers/Building.java b/src/main/java/org/openmaptiles/layers/Building.java index 49dc6b25..c1a1a3dc 100644 --- a/src/main/java/org/openmaptiles/layers/Building.java +++ b/src/main/java/org/openmaptiles/layers/Building.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. +Copyright (c) 2023, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License diff --git a/src/main/java/org/openmaptiles/layers/Housenumber.java b/src/main/java/org/openmaptiles/layers/Housenumber.java index 2d53c42a..8a0bddd5 100644 --- a/src/main/java/org/openmaptiles/layers/Housenumber.java +++ b/src/main/java/org/openmaptiles/layers/Housenumber.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. +Copyright (c) 2024, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License diff --git a/src/main/java/org/openmaptiles/layers/Landcover.java b/src/main/java/org/openmaptiles/layers/Landcover.java index 7bc66d78..43a5b647 100644 --- a/src/main/java/org/openmaptiles/layers/Landcover.java +++ b/src/main/java/org/openmaptiles/layers/Landcover.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. +Copyright (c) 2023, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License diff --git a/src/main/java/org/openmaptiles/layers/Landuse.java b/src/main/java/org/openmaptiles/layers/Landuse.java index 16dfcfb0..1faeb60f 100644 --- a/src/main/java/org/openmaptiles/layers/Landuse.java +++ b/src/main/java/org/openmaptiles/layers/Landuse.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. +Copyright (c) 2024, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -52,6 +52,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; import org.openmaptiles.OpenMapTilesProfile; import org.openmaptiles.generated.OpenMapTilesSchema; import org.openmaptiles.generated.Tables; @@ -74,6 +75,14 @@ public class Landuse implements 7, 2, 6, 1 )); + private static final TreeMap MINDIST_AND_BUFFER_SIZES = new TreeMap<>(Map.of( + 5, 0.1, + // there is quite huge jump between Z5:NE and Z6:OSM => bigger generalization needed to make the transition more smooth + 6, 0.5, + 7, 0.25, + 8, 0.125, + Integer.MAX_VALUE, 0.1 + )); private static final Set Z6_CLASSES = Set.of( FieldValues.CLASS_RESIDENTIAL, FieldValues.CLASS_SUBURB, @@ -87,11 +96,10 @@ public Landuse(Translations translations, PlanetilerConfig config, Stats stats) 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); - } + int minzoom = (scalerank != null && scalerank <= 2) ? 4 : 5; + features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE) + .setAttr(Fields.CLASS, FieldValues.CLASS_RESIDENTIAL) + .setZoomRange(minzoom, 5); } } @@ -135,10 +143,14 @@ public List postProcess(int zoom, result.add(item); } } - var merged = zoom <= 12 ? - FeatureMerge.mergeNearbyPolygons(toMerge, 1, 1, 0.1, 0.1) : + List merged; + if (zoom <= 12) { + double minDistAndBuffer = MINDIST_AND_BUFFER_SIZES.ceilingEntry(zoom).getValue(); + merged = FeatureMerge.mergeNearbyPolygons(toMerge, 1, 1, minDistAndBuffer, minDistAndBuffer); + } else { // reduces size of some heavy z13-14 tiles with lots of small polygons - FeatureMerge.mergeMultiPolygon(toMerge); + merged = FeatureMerge.mergeMultiPolygon(toMerge); + } result.addAll(merged); return result; } diff --git a/src/main/java/org/openmaptiles/layers/Park.java b/src/main/java/org/openmaptiles/layers/Park.java index 648df0f3..438745fb 100644 --- a/src/main/java/org/openmaptiles/layers/Park.java +++ b/src/main/java/org/openmaptiles/layers/Park.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. +Copyright (c) 2024, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License diff --git a/src/main/java/org/openmaptiles/layers/Place.java b/src/main/java/org/openmaptiles/layers/Place.java index f764d15f..19e84942 100644 --- a/src/main/java/org/openmaptiles/layers/Place.java +++ b/src/main/java/org/openmaptiles/layers/Place.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. +Copyright (c) 2024, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License diff --git a/src/main/java/org/openmaptiles/layers/Poi.java b/src/main/java/org/openmaptiles/layers/Poi.java index e511cc50..22736837 100644 --- a/src/main/java/org/openmaptiles/layers/Poi.java +++ b/src/main/java/org/openmaptiles/layers/Poi.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. +Copyright (c) 2024, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License diff --git a/src/main/java/org/openmaptiles/layers/Transportation.java b/src/main/java/org/openmaptiles/layers/Transportation.java index ba828a44..17f17e2a 100644 --- a/src/main/java/org/openmaptiles/layers/Transportation.java +++ b/src/main/java/org/openmaptiles/layers/Transportation.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. +Copyright (c) 2024, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -139,13 +139,23 @@ public class Transportation implements private static final Set ACCESS_NO_VALUES = Set.of( "private", "no" ); - private static final Set TRUNK_AS_MOTORWAY_BY_NETWORK = Set.of( + // ... and also Z4_MOTORWAY_NY_NETWORK, except those in Z5_MOTORWAYS_BY_NETWORK: + private static final Set Z5_TRUNK_BY_NETWORK = Set.of( RouteNetwork.CA_TRANSCANADA, RouteNetwork.CA_PROVINCIAL_ARTERIAL, RouteNetwork.US_INTERSTATE, + RouteNetwork.US_HIGHWAY, + RouteNetwork.GB_MOTORWAY, + RouteNetwork.GB_TRUNK, + RouteNetwork.IE_MOTORWAY, + RouteNetwork.IE_NATIONAL, RouteNetwork.E_ROAD, RouteNetwork.A_ROAD ); + private static final Set Z5_MOTORWAYS_BY_NETWORK = Set.of( + RouteNetwork.GB_TRUNK, + RouteNetwork.US_HIGHWAY + ); private static final Set CA_AB_PRIMARY_AS_ARTERIAL_BY_REF = Set.of( "2", "3", "4" ); @@ -194,7 +204,7 @@ public Transportation(Translations translations, PlanetilerConfig config, Stats entry(FieldValues.CLASS_BUS_GUIDEWAY, 11), entry(FieldValues.CLASS_SECONDARY, 9), entry(FieldValues.CLASS_PRIMARY, 7), - entry(FieldValues.CLASS_TRUNK, 5), + entry(FieldValues.CLASS_TRUNK, 6), entry(FieldValues.CLASS_MOTORWAY, 4) ); } @@ -245,6 +255,40 @@ private static boolean isResidentialOrUnclassified(String highway) { return "residential".equals(highway) || "unclassified".equals(highway); } + private static boolean isTrunkForZ5(String highway, List routeRelations) { + // Allow trunk roads that are part of a nation's most important route network to show at z5 + if (!"trunk".equals(highway)) { + return false; + } + return routeRelations.stream() + .map(RouteRelation::networkType) + .filter(Objects::nonNull) + .anyMatch(Z5_TRUNK_BY_NETWORK::contains); + } + + private static boolean isMotorwayWithNetworkForZ4(List routeRelations) { + // All roads in network included in osm_national_network except gb-trunk and us-highway + return routeRelations.stream() + .map(RouteRelation::networkType) + .filter(Objects::nonNull) + .filter(nt -> !Z5_MOTORWAYS_BY_NETWORK.contains(nt)) + .anyMatch(Z5_TRUNK_BY_NETWORK::contains); + } + + private static boolean isMotorwayWoNetworkForZ4(List routeRelations) { + // All motorways without network (e.g. EU, Asia, South America) + return routeRelations.stream() + .map(RouteRelation::networkType) + .noneMatch(Objects::nonNull); + } + + private static boolean isMotorwayForZ4(List routeRelations) { + if (isMotorwayWoNetworkForZ4(routeRelations)) { + return true; + } + return isMotorwayWithNetworkForZ4(routeRelations); + } + private static boolean isDrivewayOrParkingAisle(String service) { return FieldValues.SERVICE_PARKING_AISLE.equals(service) || FieldValues.SERVICE_DRIVEWAY.equals(service); } @@ -296,11 +340,7 @@ public List preprocessOsmRelation(OsmElement.Relation relation) String colour = coalesce( nullIfEmpty(relation.getString("colour")), nullIfEmpty(relation.getString("ref:colour"))); - if ("e-road".equals(network)) { - networkType = RouteNetwork.E_ROAD; - } else if ("AsianHighway".equals(network)) { - networkType = RouteNetwork.A_ROAD; - } else if ("US:I".equals(network)) { + if ("US:I".equals(network)) { networkType = RouteNetwork.US_INTERSTATE; } else if ("US:US".equals(network)) { networkType = RouteNetwork.US_HIGHWAY; @@ -495,6 +535,7 @@ int getMinzoom(Tables.OsmHighwayLinestring element, String highwayClass) { } } String highway = element.highway(); + String construction = element.construction(); int minzoom; if ("pier".equals(element.manMade())) { @@ -508,18 +549,22 @@ int getMinzoom(Tables.OsmHighwayLinestring element, String highwayClass) { case FieldValues.CLASS_TRACK, FieldValues.CLASS_PATH -> routeRank == 1 ? 12 : (z13Paths || !nullOrEmpty(element.name()) || routeRank <= 2 || !nullOrEmpty(element.sacScale())) ? 13 : 14; case FieldValues.CLASS_TRUNK -> { - // trunks in some networks to have same min. zoom as highway = "motorway" - String clazz = routeRelations.stream() - .map(RouteRelation::networkType) - .filter(Objects::nonNull) - .anyMatch(TRUNK_AS_MOTORWAY_BY_NETWORK::contains) ? FieldValues.CLASS_MOTORWAY : FieldValues.CLASS_TRUNK; - yield MINZOOMS.getOrDefault(clazz, Integer.MAX_VALUE); + boolean z5trunk = isTrunkForZ5(highway, routeRelations); + // and if it is good for Z5, it may be good also for Z4 (see CLASS_MOTORWAY bellow): + String clazz = FieldValues.CLASS_TRUNK; + if (z5trunk && isMotorwayWithNetworkForZ4(routeRelations)) { + clazz = FieldValues.CLASS_MOTORWAY; + z5trunk = false; + } + yield (z5trunk) ? 5 : MINZOOMS.getOrDefault(clazz, Integer.MAX_VALUE); } + case FieldValues.CLASS_MOTORWAY -> isMotorwayForZ4(routeRelations) ? + MINZOOMS.getOrDefault(FieldValues.CLASS_MOTORWAY, Integer.MAX_VALUE) : 5; default -> MINZOOMS.getOrDefault(baseClass, Integer.MAX_VALUE); }; } - if (isLink(highway)) { + if (isLink(highway) || isLink(construction)) { minzoom = Math.max(minzoom, 9); } return minzoom; diff --git a/src/main/java/org/openmaptiles/layers/TransportationName.java b/src/main/java/org/openmaptiles/layers/TransportationName.java index 2f82c2ec..2525f52c 100644 --- a/src/main/java/org/openmaptiles/layers/TransportationName.java +++ b/src/main/java/org/openmaptiles/layers/TransportationName.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. +Copyright (c) 2024, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License diff --git a/src/main/java/org/openmaptiles/layers/Water.java b/src/main/java/org/openmaptiles/layers/Water.java index 8a3d9a9c..e3683a1a 100644 --- a/src/main/java/org/openmaptiles/layers/Water.java +++ b/src/main/java/org/openmaptiles/layers/Water.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. +Copyright (c) 2024, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -42,14 +42,24 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import com.onthegomap.planetiler.config.PlanetilerConfig; import com.onthegomap.planetiler.expression.MultiExpression; import com.onthegomap.planetiler.geo.GeometryException; +import com.onthegomap.planetiler.geo.PolygonIndex; +import com.onthegomap.planetiler.reader.SimpleFeature; import com.onthegomap.planetiler.reader.SourceFeature; import com.onthegomap.planetiler.stats.Stats; import com.onthegomap.planetiler.util.Translations; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.util.GeometryFixer; import org.openmaptiles.OpenMapTilesProfile; import org.openmaptiles.generated.OpenMapTilesSchema; import org.openmaptiles.generated.Tables; import org.openmaptiles.util.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Defines the logic for generating map elements for oceans and lakes in the {@code water} layer from source features. @@ -62,7 +72,8 @@ public class Water implements Tables.OsmWaterPolygon.Handler, OpenMapTilesProfile.NaturalEarthProcessor, OpenMapTilesProfile.OsmWaterPolygonProcessor, - ForwardingProfile.FeaturePostProcessor { + ForwardingProfile.FeaturePostProcessor, + OpenMapTilesProfile.FinishHandler { /* * At low zoom levels, use natural earth for oceans and major lakes, and at high zoom levels @@ -71,12 +82,22 @@ public class Water implements * which infers ocean polygons by preprocessing all coastline elements. */ + private static final Logger LOGGER = LoggerFactory.getLogger(Water.class); + // smallest NE lake is around 4.42E-13, smallest matching OSM lake is 9.34E-13, this is slightly bellow that + // and approx. 33% of OSM features are smaller than this, hence to save some CPU cycles: + private static final double OSM_ID_MATCH_AREA_LIMIT = Math.pow(4, -20); + private final MultiExpression.Index classMapping; private final PlanetilerConfig config; + private final Stats stats; + private PolygonIndex neLakeIndex = PolygonIndex.create(); + private final Map> neLakeNameMaps = new ConcurrentHashMap<>(); + private final List neAllLakeInfos = new ArrayList<>(); public Water(Translations translations, PlanetilerConfig config, Stats stats) { this.classMapping = FieldMappings.Class.index(); this.config = config; + this.stats = stats; } @Override @@ -86,21 +107,60 @@ record WaterInfo(int minZoom, int maxZoom, String clazz) {} case "ne_110m_ocean" -> new WaterInfo(0, 1, FieldValues.CLASS_OCEAN); case "ne_50m_ocean" -> new WaterInfo(2, 4, FieldValues.CLASS_OCEAN); case "ne_10m_ocean" -> new WaterInfo(5, 5, FieldValues.CLASS_OCEAN); - - // TODO: get OSM ID from low-zoom natural earth lakes - case "ne_110m_lakes" -> new WaterInfo(0, 1, FieldValues.CLASS_LAKE); - case "ne_50m_lakes" -> new WaterInfo(2, 3, FieldValues.CLASS_LAKE); - case "ne_10m_lakes" -> new WaterInfo(4, 5, FieldValues.CLASS_LAKE); default -> null; }; if (info != null) { - features.polygon(LAYER_NAME) - .setBufferPixels(BUFFER_SIZE) - .setZoomRange(info.minZoom, info.maxZoom) - .setAttr(Fields.CLASS, info.clazz); + setupNeWaterFeature(features, info.minZoom, info.maxZoom, info.clazz, null); + return; + } + + LakeInfo lakeInfo = switch (table) { + case "ne_110m_lakes" -> new LakeInfo(0, 1, FieldValues.CLASS_LAKE); + case "ne_50m_lakes" -> new LakeInfo(2, 3, FieldValues.CLASS_LAKE); + case "ne_10m_lakes" -> new LakeInfo(4, 5, FieldValues.CLASS_LAKE); + default -> null; + }; + if (lakeInfo != null) { + try { + var geom = feature.worldGeometry(); + if (geom.isValid()) { + lakeInfo.geom = geom; + } else { + LOGGER.debug("Fixing geometry of NE lake {}", feature.getLong("ne_id")); + lakeInfo.geom = GeometryFixer.fix(geom); + } + lakeInfo.name = feature.getString("name"); + lakeInfo.neId = feature.getLong("ne_id"); + + var neLakeNameMap = neLakeNameMaps.computeIfAbsent(table, t -> new ConcurrentHashMap<>()); + + // need to externally synchronize inserts into ArrayList + synchronized (this) { + neAllLakeInfos.add(lakeInfo); + } + neLakeIndex.put(geom, lakeInfo); + if (lakeInfo.name != null) { + // on name collision, bigger lake gets on the name list + neLakeNameMap.merge(lakeInfo.name, lakeInfo, + (prev, next) -> next.geom.getArea() > prev.geom.getArea() ? next : prev); + } + } catch (GeometryException e) { + e.log(stats, "omt_water_ne", + "Error getting geometry for natural earth feature " + table + " " + feature.getTag("ogc_fid")); + // make sure we have this NE lake even if without OSM ID + setupNeWaterFeature(features, lakeInfo.minZoom, lakeInfo.maxZoom, lakeInfo.clazz, null); + } } } + private void setupNeWaterFeature(FeatureCollector features, int minZoom, int maxZoom, String clazz, Long osmId) { + features.polygon(LAYER_NAME) + .setBufferPixels(BUFFER_SIZE) + .setZoomRange(minZoom, maxZoom) + .setAttr(Fields.CLASS, clazz) + .setAttr(Fields.ID, osmId); + } + @Override public void processOsmWater(SourceFeature feature, FeatureCollector features) { features.polygon(LAYER_NAME) @@ -121,6 +181,87 @@ public void process(Tables.OsmWaterPolygon element, FeatureCollector features) { .setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0) .setAttrWithMinzoom(Fields.BRUNNEL, Utils.brunnel(element.isBridge(), element.isTunnel()), 12) .setAttr(Fields.CLASS, clazz); + + try { + attemptNeLakeIdMapping(element); + } catch (GeometryException e) { + e.log(stats, "omt_water", + "Unable to add OSM ID to natural earth water feature", config.logJtsExceptions()); + } + } + } + + void attemptNeLakeIdMapping(Tables.OsmWaterPolygon element) throws GeometryException { + // if OSM lake is too small for Z6 (e.g. area bellow ~4px) we assume there is no matching NE lake + var geom = element.source().worldGeometry(); + if (geom.getArea() < OSM_ID_MATCH_AREA_LIMIT) { + return; + } + + if (!geom.isValid()) { + geom = GeometryFixer.fix(geom); + stats.dataError("omt_fix_water_before_ne_intersect"); + LOGGER.debug("Fixing geometry of OSM element {} before attempt to add ID to natural earth water feature", + element.source().id()); + } + + // match by name: + boolean match = false; + if (element.name() != null) { + for (var map : neLakeNameMaps.values()) { + var lakeInfo = map.get(element.name()); + if (lakeInfo != null) { + match = true; + fillOsmIdIntoNeLake(element, geom, lakeInfo, true); + } + } + } + if (match) { + return; + } + + // match by intersection: + List items = neLakeIndex.getIntersecting(geom); + for (var lakeInfo : items) { + fillOsmIdIntoNeLake(element, geom, lakeInfo, false); + } + } + + /* + * When we match lakes with `neLakeIndexes` then `intersetsCheckNeeded` should be `false`, + * otherwise `true`, to make sure we DO check the intersection but to avoid checking it twice. + */ + void fillOsmIdIntoNeLake(Tables.OsmWaterPolygon element, Geometry geom, LakeInfo lakeInfo, + boolean intersetsCheckNeeded) { + final Geometry neGeom = lakeInfo.geom; + if (intersetsCheckNeeded && !neGeom.intersects(geom)) { + return; + } + final var intersection = neGeom.intersection(geom); + + // Should match following in OpenMapTiles: Distinct on keeps just the first occurence -> order by 'area_ratio DESC' + // With a twist: NE geometry is always the same, hence we can make it a little bit faster by dropping "ratio" + // and compare only the intersection area: bigger area -> bigger ratio. + double area = intersection.getArea(); + lakeInfo.mergeId(element.source().id(), area); + } + + @Override + public void finish(String sourceName, FeatureCollector.Factory featureCollectors, + Consumer emit) { + if (OpenMapTilesProfile.OSM_SOURCE.equals(sourceName)) { + var timer = stats.startStage("ne_lakes"); + for (var item : neAllLakeInfos) { + var features = featureCollectors.get(SimpleFeature.fromWorldGeometry(item.geom)); + setupNeWaterFeature(features, item.minZoom, item.maxZoom, item.clazz, item.osmId); + for (var feature : features) { + emit.accept(feature); + } + } + neLakeNameMaps.clear(); + neLakeIndex = null; + neAllLakeInfos.clear(); + timer.stop(); } } @@ -128,4 +269,35 @@ public void process(Tables.OsmWaterPolygon element, FeatureCollector features) { public List postProcess(int zoom, List items) throws GeometryException { return items.size() > 1 ? FeatureMerge.mergeOverlappingPolygons(items, config.minFeatureSize(zoom)) : items; } + + /** + * Information to hold onto from processing an NE lake to determine OSM ID later. + */ + private static class LakeInfo { + String name; + int minZoom; + int maxZoom; + String clazz; + Geometry geom; + Long osmId; + long neId; + double area; + + public LakeInfo(int minZoom, int maxZoom, String clazz) { + this.name = null; + this.minZoom = minZoom; + this.maxZoom = maxZoom; + this.clazz = clazz; + this.osmId = null; + this.neId = -1; + this.area = 0; + } + + public synchronized void mergeId(Long newId, double newArea) { + if (newArea > area) { + osmId = newId; + area = newArea; + } + } + } } diff --git a/src/main/java/org/openmaptiles/layers/WaterName.java b/src/main/java/org/openmaptiles/layers/WaterName.java index cd3a1a8b..bd48b18d 100644 --- a/src/main/java/org/openmaptiles/layers/WaterName.java +++ b/src/main/java/org/openmaptiles/layers/WaterName.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. +Copyright (c) 2024, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -85,6 +85,9 @@ public class WaterName implements private static final Set SEA_OR_OCEAN_PLACE = Set.of("sea", "ocean"); private static final double IMPORTANT_MARINE_REGIONS_JOIN_DISTANCE = GeoUtils.metersToPixelAtEquator(0, 50_000) / 256d; + private static final int MINZOOM_BAY = 9; + private static final int MINZOOM_LAKE = 3; + private static final int MINZOOM_SEA_AND_OCEAN = 0; private final Translations translations; // need to synchronize updates from multiple threads private final LongObjectMap lakeCenterlines = Hppc.newLongObjectHashMap(); @@ -215,8 +218,7 @@ public void process(Tables.OsmMarinePoint element, FeatureCollector features) { public void process(Tables.OsmWaterPolygon element, FeatureCollector features) { if (nullIfEmpty(element.name()) != null) { Geometry centerlineGeometry = lakeCenterlines.get(element.source().id()); - FeatureCollector.Feature feature; - int minzoom = 9; + int minzoomCL = MINZOOM_BAY; String place = element.place(); String clazz; if ("bay".equals(element.natural())) { @@ -225,27 +227,41 @@ public void process(Tables.OsmWaterPolygon element, FeatureCollector features) { clazz = FieldValues.CLASS_SEA; } else { clazz = FieldValues.CLASS_LAKE; - minzoom = 3; + minzoomCL = MINZOOM_LAKE; } if (centerlineGeometry != null) { - // prefer lake centerline if it exists - feature = features.geometry(LAYER_NAME, centerlineGeometry) - .setMinPixelSizeBelowZoom(13, 6d * element.name().length()); - } else { - // otherwise just use a label point inside the lake - feature = features.pointOnSurface(LAYER_NAME) - .setMinZoom(place != null && SEA_OR_OCEAN_PLACE.contains(place) ? 0 : 3) - .setMinPixelSize(128); // tiles are 256x256, so 128x128 is 1/4 of a tile + // prefer lake centerline if it exists, but point will be also used if minzoom below 9 is calculated from area + // note: Here we're diverging from OpenMapTiles: For bays with minzoom (based on area) point is used between + // minzoom and Z8 and for Z9+ centerline is used, while OpenMaptiles sticks with points. + setupOsmWaterPolygonFeature( + element, features.geometry(LAYER_NAME, centerlineGeometry), clazz, minzoomCL) + .setMinPixelSizeBelowZoom(13, 6d * element.name().length()); + } + + int minzoom = place != null && SEA_OR_OCEAN_PLACE.contains(place) ? MINZOOM_SEA_AND_OCEAN : MINZOOM_LAKE; + if (centerlineGeometry == null || minzoom < minzoomCL) { + // use a label point inside the lake but ... + // ... if centerline already created, adjust maxzoom here to make sure we're not having both at same zoom level + int maxzoom = centerlineGeometry != null ? minzoomCL - 1 : 14; + setupOsmWaterPolygonFeature(element, features.pointOnSurface(LAYER_NAME), clazz, minzoom) + .setMaxZoom(maxzoom) + // Show a label if a water feature covers at least 1/4 of a tile or z14+ + .setMinPixelSizeBelowZoom(13, 128); } - feature - .setAttr(Fields.CLASS, clazz) - .setBufferPixels(BUFFER_SIZE) - .putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations)) - .setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0) - .setMinZoom(minzoom); } } + private FeatureCollector.Feature setupOsmWaterPolygonFeature(Tables.OsmWaterPolygon element, + FeatureCollector.Feature output, String clazz, int minzoom) { + output + .setAttr(Fields.CLASS, clazz) + .setBufferPixels(BUFFER_SIZE) + .putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations)) + .setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0) + .setMinZoom(minzoom); + return output; + } + private record NaturalEarthRegion( Geometry geometry, int scalerank diff --git a/src/test/java/org/openmaptiles/layers/BoundaryTest.java b/src/test/java/org/openmaptiles/layers/BoundaryTest.java index 95e56d09..1aa9e676 100644 --- a/src/test/java/org/openmaptiles/layers/BoundaryTest.java +++ b/src/test/java/org/openmaptiles/layers/BoundaryTest.java @@ -104,6 +104,7 @@ void testNaturalEarthCountryBoundaries() { assertFeatures(0, List.of(Map.of( "_layer", "boundary", "_type", "line", + "_minzoom", 4, "admin_level", 2 )), process(SimpleFeature.create( newLineString(0, 0, 1, 1), @@ -115,10 +116,65 @@ void testNaturalEarthCountryBoundaries() { 0 ))); + assertFeatures(0, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "_minzoom", 1, + "admin_level", 2 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "Disputed (please verify)", + "adm0_left", "South Sudan", + "adm0_right", "Kenya" + ), + OpenMapTilesProfile.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" + "featurecla", "Lease limit" + ), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_boundary_lines_land", + 0 + ))); + } + + @Test + void testNaturalEarthCountryKeSsBoundaryReversed() { + assertFeatures(0, List.of(Map.of( + "_layer", "boundary", + "_minzoom", 1, + "admin_level", 2 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "Disputed (please verify)", + "adm0_right", "South Sudan", + "adm0_left", "Kenya" + ), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_boundary_lines_land", + 0 + ))); + } + + @Test + void testNaturalEarthCountryNotKeSsBoundary() { + assertFeatures(0, List.of(Map.of( + "_layer", "boundary", + "_minzoom", 4, + "admin_level", 2 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "featurecla", "Disputed (please verify)", + "adm0_left", "South Sudan", + "adm0_right", "Uganda" ), OpenMapTilesProfile.NATURAL_EARTH_SOURCE, "ne_10m_admin_0_boundary_lines_land", @@ -382,6 +438,29 @@ void testOsmBoundaryDisputedFromWay() { )); } + @Test + void testOsmAl2BoundaryDisputedMinZoom() { + var relation = new OsmElement.Relation(1); + relation.setTag("type", "boundary"); + relation.setTag("admin_level", "2"); + relation.setTag("boundary", "administrative"); + + assertFeatures(3, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "_minzoom", 5, + + "disputed", 1, + "maritime", 0, + "admin_level", 2 + )), process(lineFeatureWithRelation( + profile.preprocessOsmRelation(relation), + Map.of( + "disputed", "yes" + )) + )); + } + @Test void testCountryBoundaryEmittedIfNoName() { var relation = new OsmElement.Relation(1); diff --git a/src/test/java/org/openmaptiles/layers/LanduseTest.java b/src/test/java/org/openmaptiles/layers/LanduseTest.java index c3449364..57d7ea8e 100644 --- a/src/test/java/org/openmaptiles/layers/LanduseTest.java +++ b/src/test/java/org/openmaptiles/layers/LanduseTest.java @@ -19,7 +19,8 @@ void testNaturalEarthUrbanAreas() { assertFeatures(0, List.of(Map.of( "_layer", "landuse", "class", "residential", - "_buffer", 4d + "_buffer", 4d, + "_minzoom", 4 )), process(SimpleFeature.create( GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), Map.of("scalerank", 1.9), @@ -27,7 +28,12 @@ void testNaturalEarthUrbanAreas() { "ne_50m_urban_areas", 0 ))); - assertFeatures(0, List.of(), process(SimpleFeature.create( + assertFeatures(0, List.of(Map.of( + "_layer", "landuse", + "class", "residential", + "_buffer", 4d, + "_minzoom", 5 + )), process(SimpleFeature.create( GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), Map.of("scalerank", 2.1), OpenMapTilesProfile.NATURAL_EARTH_SOURCE, diff --git a/src/test/java/org/openmaptiles/layers/TransportationTest.java b/src/test/java/org/openmaptiles/layers/TransportationTest.java index 2210aa65..729b3b05 100644 --- a/src/test/java/org/openmaptiles/layers/TransportationTest.java +++ b/src/test/java/org/openmaptiles/layers/TransportationTest.java @@ -353,7 +353,7 @@ void testDuplicateRoute() { "_layer", "transportation", "class", "trunk", "network", "us-state", - "_minzoom", 5 + "_minzoom", 6 ), Map.of( "_layer", "transportation_name", "class", "trunk", @@ -580,8 +580,8 @@ void testPolishHighwayIssue165() { "_layer", "transportation_name", "class", "trunk", "name", "", - "ref", "E 28", - "ref_length", 4, + "ref", "S7", + "ref_length", 2, "route_1_network", "e-road", "route_1_ref", "E 28", "route_2_network", "e-road", @@ -670,6 +670,26 @@ void testInterstateMotorwayWithoutWayInfo() { )), features); } + @Test + void testMotorwayRoadConstruction() { + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "motorway_construction", + "oneway", 1, + "_minzoom", 4 + ), Map.of( + "_layer", "transportation_name", + "name", "D4 II/118 – Milín", + "class", "motorway_construction", + "_minzoom", 6 + )), process(lineFeature(Map.of( + "highway", "construction", + "construction", "motorway", + "name", "D4 II/118 – Milín", + "oneway", "yes" + )))); + } + @Test void testPrimaryRoadConstruction() { assertFeatures(13, List.of(Map.of( @@ -1154,7 +1174,7 @@ void testTransCanadaProvincialCaOnPrimaryRefOther() { "_layer", "transportation", "class", "trunk", "network", "ca-provincial", - "_minzoom", 5 + "_minzoom", 6 ), Map.of( "_layer", "transportation_name", "class", "trunk", @@ -1208,7 +1228,7 @@ void testTransCanadaProvincialCaMbPthRefOther() { "_layer", "transportation", "class", "trunk", "network", "ca-provincial", - "_minzoom", 5 + "_minzoom", 6 ), Map.of( "_layer", "transportation_name", "class", "trunk", @@ -1262,7 +1282,7 @@ void testTransCanadaProvincialCaAbPrimaryRefOther() { "_layer", "transportation", "class", "trunk", "network", "ca-provincial", - "_minzoom", 5 + "_minzoom", 6 ), Map.of( "_layer", "transportation_name", "class", "trunk", @@ -1316,7 +1336,7 @@ void testTransCanadaProvincialCaBcRefOther() { "_layer", "transportation", "class", "trunk", "network", "ca-provincial", - "_minzoom", 5 + "_minzoom", 6 ), Map.of( "_layer", "transportation_name", "class", "trunk", @@ -1341,7 +1361,7 @@ void testTransCanadaProvincialCaOther() { assertFeatures(13, List.of(Map.of( "_layer", "transportation", "class", "trunk", - "_minzoom", 5 + "_minzoom", 6 )), features); boolean caProvPresent = StreamSupport.stream(features.spliterator(), false) .flatMap(f -> f.getAttrsAtZoom(13).entrySet().stream()) @@ -1627,7 +1647,7 @@ void testIrelandTrunk() { assertFeatures(13, List.of(Map.of( "_layer", "transportation", "class", "trunk", - "_minzoom", 5 + "_minzoom", 4 ), Map.of( "_layer", "transportation_name", "class", "trunk", @@ -2121,19 +2141,20 @@ void testARoad() { FeatureCollector features = process(lineFeatureWithRelation( profile.preprocessOsmRelation(rel), Map.of( - "highway", "trunk" + "highway", "trunk", + "name", "National Highway 7", + "ref", "7" ))); assertFeatures(13, List.of(Map.of( "_layer", "transportation", "class", "trunk", - "network", "a-road", - "_minzoom", 4 + "_minzoom", 6 ), Map.of( "_layer", "transportation_name", "class", "trunk", - "ref", "AH11", - "network", "a-road" + "ref", "7", + "route_1_ref", "AH11" )), features); } @@ -2143,24 +2164,24 @@ void testERoad() { rel.setTag("type", "route"); rel.setTag("route", "road"); rel.setTag("network", "e-road"); - rel.setTag("ref", "E 50"); + rel.setTag("ref", "E 77"); FeatureCollector features = process(lineFeatureWithRelation( profile.preprocessOsmRelation(rel), Map.of( - "highway", "motorway" + "highway", "motorway", + "ref", "S7" ))); assertFeatures(13, List.of(Map.of( "_layer", "transportation", "class", "motorway", - "network", "e-road", "_minzoom", 4 ), Map.of( "_layer", "transportation_name", "class", "motorway", - "ref", "E 50", - "network", "e-road" + "ref", "S7", + "route_1_ref", "E 77" )), features); } } diff --git a/src/test/java/org/openmaptiles/layers/WaterNameTest.java b/src/test/java/org/openmaptiles/layers/WaterNameTest.java index 02b7c984..6ee686b6 100644 --- a/src/test/java/org/openmaptiles/layers/WaterNameTest.java +++ b/src/test/java/org/openmaptiles/layers/WaterNameTest.java @@ -62,7 +62,7 @@ void testWaterNameLakeline() { "_maxzoom", 14, "_minpixelsize", "waterway".length() * 6d )), process(SimpleFeature.create( - GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), + GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1E-7))), new HashMap<>(Map.of( "name", "waterway", "name:es", "waterway es", @@ -112,7 +112,7 @@ void testWaterNameMultipleLakelines() { "_maxzoom", 14, "_minpixelsize", "waterway".length() * 6d )), process(SimpleFeature.create( - GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), + GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1E-7))), new HashMap<>(Map.of( "name", "waterway", "name:es", "waterway es", @@ -126,7 +126,7 @@ void testWaterNameMultipleLakelines() { } @Test - void testWaterNameBay() { + void testWaterNameBaySmall() { assertFeatures(11, List.of(), process(SimpleFeature.create( newLineString(0, 0, 1, 1), new HashMap<>(Map.of( @@ -146,6 +146,55 @@ void testWaterNameBay() { "_minzoom", 9, "_maxzoom", 14, "_minpixelsize", "bay".length() * 6d + ), Map.of( + "name", "bay", + "name:es", "bay es", + + "_layer", "water_name", + "_type", "point", + "_minzoom", 3, + "_maxzoom", 8, + "_minpixelsize", 128d + )), process(SimpleFeature.create( + GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1E-7))), + new HashMap<>(Map.of( + "name", "bay", + "name:es", "bay es", + "natural", "bay" + )), + OpenMapTilesProfile.OSM_SOURCE, + null, + 10 + ))); + } + + @Test + void testWaterNameBayBig() { + assertFeatures(11, List.of(), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + new HashMap<>(Map.of( + "OSM_ID", -10 + )), + OpenMapTilesProfile.LAKE_CENTERLINE_SOURCE, + null, + 0 + ))); + assertFeatures(10, List.of(Map.of( + "name", "bay", + "name:es", "bay es", + + "_layer", "water_name", + "_type", "line", + "_minzoom", 9, + "_maxzoom", 14 + ), Map.of( + "name", "bay", + "name:es", "bay es", + + "_layer", "water_name", + "_type", "point", + "_minzoom", 3, + "_maxzoom", 8 )), process(SimpleFeature.create( GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), new HashMap<>(Map.of( diff --git a/src/test/java/org/openmaptiles/layers/WaterTest.java b/src/test/java/org/openmaptiles/layers/WaterTest.java index ee900dc5..e413538e 100644 --- a/src/test/java/org/openmaptiles/layers/WaterTest.java +++ b/src/test/java/org/openmaptiles/layers/WaterTest.java @@ -2,8 +2,10 @@ import static com.onthegomap.planetiler.TestUtils.rectangle; +import com.onthegomap.planetiler.FeatureCollector; import com.onthegomap.planetiler.geo.GeoUtils; import com.onthegomap.planetiler.reader.SimpleFeature; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -16,7 +18,7 @@ class WaterTest extends AbstractLayerTest { @Test void testWaterNaturalEarth() { assertFeatures(0, List.of(Map.of( - "class", "lake", + "class", "ocean", "intermittent", "", "_layer", "water", "_type", "polygon", @@ -25,49 +27,353 @@ void testWaterNaturalEarth() { rectangle(0, 10), Map.of(), OpenMapTilesProfile.NATURAL_EARTH_SOURCE, - "ne_110m_lakes", + "ne_110m_ocean", 0 ))); - assertFeatures(0, List.of(Map.of( + assertFeatures(6, List.of(Map.of( "class", "ocean", - "intermittent", "", "_layer", "water", "_type", "polygon", - "_minzoom", 0 + "_maxzoom", 5 )), process(SimpleFeature.create( rectangle(0, 10), Map.of(), OpenMapTilesProfile.NATURAL_EARTH_SOURCE, - "ne_110m_ocean", + "ne_10m_ocean", 0 ))); + } - assertFeatures(6, List.of(Map.of( + @Test + void testLakeNaturalEarthByIntersection() { + final var polygon = rectangle(0, 0.1); + // NE lakes: + process(SimpleFeature.create( + polygon, + Map.of(), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_110m_lakes", + 0 + )); + process(SimpleFeature.create( + polygon, + Map.of(), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_10m_lakes", + 0 + )); + // OSM lake to take the ID from: + process(SimpleFeature.create( + polygon, + new HashMap<>(Map.of( + "natural", "water", + "water", "reservoir" + )), + OpenMapTilesProfile.OSM_SOURCE, + null, + 123 + )); + + List features = new ArrayList<>(); + profile.finish(OpenMapTilesProfile.OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add); + assertFeatures(0, List.of(Map.of( "class", "lake", + "intermittent", "", + "id", 123L, + "_layer", "water", + "_type", "polygon", + "_minzoom", 0, + "_maxzoom", 1 + ), Map.of( + "class", "lake", + "intermittent", "", + "id", 123L, "_layer", "water", "_type", "polygon", + "_minzoom", 4, "_maxzoom", 5 - )), process(SimpleFeature.create( - rectangle(0, 10), + )), features); + } + + @Test + void testLakeNaturalEarthIntersectionMiss() { + final var polygon1 = rectangle(0, 0.1); + final var polygon2 = rectangle(0.2, 0.3); + // NE lakes: + process(SimpleFeature.create( + polygon1, + Map.of(), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_110m_lakes", + 0 + )); + process(SimpleFeature.create( + polygon1, Map.of(), OpenMapTilesProfile.NATURAL_EARTH_SOURCE, "ne_10m_lakes", 0 - ))); + )); + // OSM lake to take the ID from: + process(SimpleFeature.create( + polygon2, + new HashMap<>(Map.of( + "natural", "water", + "water", "reservoir" + )), + OpenMapTilesProfile.OSM_SOURCE, + null, + 123 + )); - assertFeatures(6, List.of(Map.of( - "class", "ocean", + List features = new ArrayList<>(); + profile.finish(OpenMapTilesProfile.OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add); + assertFeatures(0, List.of(Map.of( + "class", "lake", + "id", "", + "_layer", "water", + "_type", "polygon" + ), Map.of( + "class", "lake", + "id", "", + "_layer", "water", + "_type", "polygon" + )), features); + } + + @Test + void testLakeNaturalEarthByBiggerIntersection() { + final var polygon1 = rectangle(0, 0.1); + final var polygon2 = rectangle(0, 0.2); + // NE lakes: + process(SimpleFeature.create( + polygon2, + Map.of(), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_110m_lakes", + 0 + )); + process(SimpleFeature.create( + polygon2, + Map.of(), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_10m_lakes", + 0 + )); + // OSM lakes to take the ID from: + process(SimpleFeature.create( + polygon1, + new HashMap<>(Map.of( + "natural", "water", + "water", "reservoir" + )), + OpenMapTilesProfile.OSM_SOURCE, + null, + 123 + )); + process(SimpleFeature.create( + polygon2, + new HashMap<>(Map.of( + "natural", "water", + "water", "reservoir" + )), + OpenMapTilesProfile.OSM_SOURCE, + null, + 234 + )); + + List features = new ArrayList<>(); + profile.finish(OpenMapTilesProfile.OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add); + assertFeatures(0, List.of(Map.of( + "class", "lake", + "intermittent", "", + "id", 234L, "_layer", "water", "_type", "polygon", + "_minzoom", 0, + "_maxzoom", 1 + ), Map.of( + "class", "lake", + "intermittent", "", + "id", 234L, + "_layer", "water", + "_type", "polygon", + "_minzoom", 4, "_maxzoom", 5 - )), process(SimpleFeature.create( - rectangle(0, 10), - Map.of(), + )), features); + } + + @Test + void testLakeNaturalEarthByName() { + final var polygon = rectangle(0, 0.1); + // NE lakes: + process(SimpleFeature.create( + polygon, + Map.of("name", "Test Lake"), OpenMapTilesProfile.NATURAL_EARTH_SOURCE, - "ne_10m_ocean", + "ne_50m_lakes", 0 - ))); + )); + process(SimpleFeature.create( + polygon, + Map.of("name", "Test Lake"), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_10m_lakes", + 0 + )); + // OSM lake to take the ID from: + process(SimpleFeature.create( + polygon, + new HashMap<>(Map.of( + "name", "Test Lake", + "natural", "water", + "water", "reservoir" + )), + OpenMapTilesProfile.OSM_SOURCE, + null, + 123 + )); + + List features = new ArrayList<>(); + profile.finish(OpenMapTilesProfile.OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add); + assertFeatures(0, List.of(Map.of( + "class", "lake", + "id", 123L, + "_layer", "water", + "_minzoom", 2, + "_maxzoom", 3 + ), Map.of( + "class", "lake", + "id", 123L, + "_layer", "water", + "_minzoom", 4, + "_maxzoom", 5 + )), features); + } + + @Test + void testLakeNaturalEarthByNameIntersectionMiss() { + final var polygon1 = rectangle(0, 0.1); + final var polygon2 = rectangle(0.2, 0.3); + // NE lake: + process(SimpleFeature.create( + polygon1, + Map.of("name", "Test Lake"), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_50m_lakes", + 0 + )); + // OSM lake to take the ID from: + process(SimpleFeature.create( + polygon2, + new HashMap<>(Map.of( + "name", "Test Lake", + "natural", "water", + "water", "reservoir" + )), + OpenMapTilesProfile.OSM_SOURCE, + null, + 123 + )); + + List features = new ArrayList<>(); + profile.finish(OpenMapTilesProfile.OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add); + assertFeatures(0, List.of(Map.of( + "class", "lake", + "id", "", + "_layer", "water" + )), features); + } + + @Test + void testLakeNaturalEarthByNameAndBiggerIntersection() { + final var polygon1 = rectangle(0, 0.1); + final var polygon2 = rectangle(0, 0.2); + // NE lake: + process(SimpleFeature.create( + polygon2, + Map.of("name", "Test Lake"), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_50m_lakes", + 0 + )); + // OSM lakes to take the ID from: + process(SimpleFeature.create( + polygon1, + new HashMap<>(Map.of( + "name", "Test Lake", + "natural", "water", + "water", "reservoir" + )), + OpenMapTilesProfile.OSM_SOURCE, + null, + 123 + )); + process(SimpleFeature.create( + polygon2, + new HashMap<>(Map.of( + "name", "Test Lake", + "natural", "water", + "water", "reservoir" + )), + OpenMapTilesProfile.OSM_SOURCE, + null, + 234 + )); + + List features = new ArrayList<>(); + profile.finish(OpenMapTilesProfile.OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add); + assertFeatures(0, List.of(Map.of( + "class", "lake", + "id", 234L, + "_layer", "water" + )), features); + } + + @Test + void testLakeNaturalEarthByNameWithColision() { + final var polygonSmaller = rectangle(0, 0.1); + final var polygonBigger = rectangle(0, 0.2); + // NE lakes: + process(SimpleFeature.create( + polygonSmaller, + Map.of("name", "Test Lake"), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_10m_lakes", + 0 + )); + process(SimpleFeature.create( + polygonBigger, + Map.of("name", "Test Lake"), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_10m_lakes", + 0 + )); + // OSM lake to take the ID from: + process(SimpleFeature.create( + polygonBigger, + new HashMap<>(Map.of( + "name", "Test Lake", + "natural", "water", + "water", "reservoir" + )), + OpenMapTilesProfile.OSM_SOURCE, + null, + 123 + )); + + List features = new ArrayList<>(); + profile.finish(OpenMapTilesProfile.OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add); + assertFeatures(0, List.of(Map.of( + "class", "lake", + "id", "", + "_layer", "water" + ), Map.of( + "class", "lake", + "id", 123L, + "_layer", "water" + )), features); } @Test @@ -255,28 +561,7 @@ void testOceanZoomLevels() { @Test void testLakeZoomLevels() { - assertCoversZoomRange(0, 14, "water", - process(SimpleFeature.create( - rectangle(0, 10), - Map.of(), - OpenMapTilesProfile.NATURAL_EARTH_SOURCE, - "ne_110m_lakes", - 0 - )), - process(SimpleFeature.create( - rectangle(0, 10), - Map.of(), - OpenMapTilesProfile.NATURAL_EARTH_SOURCE, - "ne_50m_lakes", - 0 - )), - process(SimpleFeature.create( - rectangle(0, 10), - Map.of(), - OpenMapTilesProfile.NATURAL_EARTH_SOURCE, - "ne_10m_lakes", - 0 - )), + assertCoversZoomRange(6, 14, "water", process(SimpleFeature.create( rectangle(0, 10), Map.of(