diff --git a/README.md b/README.md index 23dda83..9771ff7 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ See [README.md](../README.md) in the parent directory for instructions on how to - 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` +- `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) ## Code Layout diff --git a/src/main/java/com/onthegomap/planetiler/basemap/BasemapProfile.java b/src/main/java/com/onthegomap/planetiler/basemap/BasemapProfile.java index 3cffa8c..a9cc055 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/BasemapProfile.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/BasemapProfile.java @@ -10,6 +10,8 @@ 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.basemap.layers.Transportation; +import com.onthegomap.planetiler.basemap.layers.TransportationName; import com.onthegomap.planetiler.config.PlanetilerConfig; import com.onthegomap.planetiler.expression.MultiExpression; import com.onthegomap.planetiler.reader.SimpleFeature; @@ -61,10 +63,27 @@ public class BasemapProfile extends ForwardingProfile { // register release/finish/feature postprocessor/osm relationship handler methods... List layers = new ArrayList<>(); + Transportation transportationLayer = null; + TransportationName transportationNameLayer = null; for (Layer layer : OpenMapTilesSchema.createInstances(translations, config, stats)) { if ((onlyLayers.isEmpty() || onlyLayers.contains(layer.name())) && !excludeLayers.contains(layer.name())) { layers.add(layer); registerHandler(layer); + if (layer instanceof TransportationName transportationName) { + transportationNameLayer = transportationName; + } + } + if (layer instanceof Transportation transportation) { + transportationLayer = transportation; + } + } + + // special-case: transportation_name layer depends on transportation layer + if (transportationNameLayer != null) { + transportationNameLayer.needsTransportationLayer(transportationLayer); + if (!layers.contains(transportationLayer)) { + layers.add(transportationLayer); + registerHandler(transportationLayer); } } diff --git a/src/main/java/com/onthegomap/planetiler/basemap/Generate.java b/src/main/java/com/onthegomap/planetiler/basemap/Generate.java index 25b0a62..f9e1397 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/Generate.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/Generate.java @@ -58,7 +58,7 @@ public class Generate { private static final String LINE_SEPARATOR = System.lineSeparator(); private static final String GENERATED_FILE_HEADER = """ /* - Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. + Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -690,12 +690,12 @@ public class Generate { * Models for deserializing yaml into: */ - private static record OpenmaptilesConfig( + private record OpenmaptilesConfig( OpenmaptilesTileSet tileset ) {} @JsonIgnoreProperties(ignoreUnknown = true) - private static record OpenmaptilesTileSet( + private record OpenmaptilesTileSet( List layers, String version, String attribution, @@ -705,51 +705,52 @@ public class Generate { ) {} @JsonIgnoreProperties(ignoreUnknown = true) - private static record LayerDetails( + private record LayerDetails( String id, String description, Map fields, double buffer_size ) {} - private static record Datasource( + private record Datasource( String type, String mapping_file ) {} @JsonIgnoreProperties(ignoreUnknown = true) - private static record LayerConfig( + private record LayerConfig( LayerDetails layer, List datasources ) {} - private static record Imposm3Column( + private record Imposm3Column( String type, String name, String key, boolean from_member ) {} - static record Imposm3Filters( + record Imposm3Filters( JsonNode reject, JsonNode require ) {} - static record Imposm3Table( + record Imposm3Table( String type, @JsonProperty("_resolve_wikidata") boolean resolveWikidata, List columns, Imposm3Filters filters, JsonNode mapping, - Map type_mappings + Map type_mappings, + @JsonProperty("relation_types") List relationTypes ) {} @JsonIgnoreProperties(ignoreUnknown = true) - private static record Imposm3Mapping( + private record Imposm3Mapping( Map tables ) {} - private static record OsmTableField( + private record OsmTableField( String clazz, String name, String extractCode diff --git a/src/main/java/com/onthegomap/planetiler/basemap/generated/OpenMapTilesSchema.java b/src/main/java/com/onthegomap/planetiler/basemap/generated/OpenMapTilesSchema.java index 8878a0e..30a69e0 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/generated/OpenMapTilesSchema.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/generated/OpenMapTilesSchema.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -52,16 +52,15 @@ import java.util.Set; /** * All vector tile layer definitions, attributes, and allowed values generated from the - * OpenMapTiles vector tile - * schema - * v3.12.2. + * OpenMapTiles vector tile schema + * v3.13. */ @SuppressWarnings("unused") public class OpenMapTilesSchema { public static final String NAME = "OpenMapTiles"; public static final String DESCRIPTION = "A tileset showcasing all layers in OpenMapTiles. https://openmaptiles.org"; - public static final String VERSION = "3.12.1"; + public static final String VERSION = "3.13.0"; public static final String ATTRIBUTION = "© OpenMapTiles © OpenStreetMap contributors"; public static final List LANGUAGES = List.of("am", "ar", "az", "be", "bg", "br", "bs", "ca", "co", "cs", "cy", "da", "de", "el", "en", "eo", "es", "et", "eu", "fi", "fr", "fy", "ga", "gd", "he", "hi", "hr", "hu", "hy", "id", @@ -99,7 +98,7 @@ public class OpenMapTilesSchema { * polygons to improve rendering performance. This however can lead to less rendering options in clients since these * boundaries show up. So you might not be able to use border styling for ocean water features. *

- * Generated from water.yaml + * Generated from water.yaml */ public interface Water extends Layer { @@ -116,14 +115,16 @@ public class OpenMapTilesSchema { /** * All water polygons from OpenStreetMapData have the class - * ocean. Water bodies are classified as lake or river for water bodies - * with the waterway tag. + * ocean. Water bodies with the waterway=riverbank + * or water=river tag are classified + * as river. Wet and dry docks tagged waterway=dock + * are classified as a dock. All other water bodies are classified as lake. *

* allowed values: *

    - *
  • lake *
  • dock *
  • river + *
  • lake *
  • ocean *
*/ @@ -156,11 +157,11 @@ public class OpenMapTilesSchema { /** Attribute values for map elements in the water layer. */ final class FieldValues { - public static final String CLASS_LAKE = "lake"; public static final String CLASS_DOCK = "dock"; public static final String CLASS_RIVER = "river"; + public static final String CLASS_LAKE = "lake"; public static final String CLASS_OCEAN = "ocean"; - public static final Set CLASS_VALUES = Set.of("lake", "dock", "river", "ocean"); + public static final Set CLASS_VALUES = Set.of("dock", "river", "lake", "ocean"); public static final String BRUNNEL_BRIDGE = "bridge"; public static final String BRUNNEL_TUNNEL = "tunnel"; public static final Set BRUNNEL_VALUES = Set.of("bridge", "tunnel"); @@ -170,9 +171,9 @@ public class OpenMapTilesSchema { final class FieldMappings { public static final MultiExpression Class = MultiExpression.of( - List.of(MultiExpression.entry("lake", matchAny("waterway", "", "lake")), - MultiExpression.entry("dock", matchAny("waterway", "dock")), MultiExpression.entry("river", FALSE), - MultiExpression.entry("ocean", FALSE))); + List.of(MultiExpression.entry("dock", matchAny("waterway", "dock")), + MultiExpression.entry("river", or(matchAny("water", "river"), matchAny("waterway", "riverbank"))), + MultiExpression.entry("lake", matchAny("waterway", "")), MultiExpression.entry("ocean", FALSE))); } } @@ -183,7 +184,7 @@ public class OpenMapTilesSchema { * there is also canal generated, starting z13 there is no generalization according to class * field applied. Waterways do not have a subclass field. *

- * Generated from waterway.yaml + * Generated from waterway.yaml */ public interface Waterway extends Layer { @@ -273,7 +274,7 @@ public class OpenMapTilesSchema { * href="http://wiki.openstreetmap.org/wiki/Landcover">implied by OSM tags. The most common use case for this * layer is to style wood (class=wood) and grass (class=grass) areas. *

- * Generated from landcover.yaml + * Generated from landcover.yaml */ public interface Landcover extends Layer { @@ -428,7 +429,7 @@ public class OpenMapTilesSchema { * Landuse is used to describe use of land by humans. At lower zoom levels this is from Natural Earth data for * residential (urban) areas and at higher zoom levels mostly OSM landuse tags. *

- * Generated from landuse.yaml + * Generated from landuse.yaml */ public interface Landuse extends Layer { @@ -527,7 +528,7 @@ public class OpenMapTilesSchema { /** * Natural peaks *

- * Generated from mountain_peak.yaml + * Generated from mountain_peak.yaml */ public interface MountainPeak extends Layer { @@ -556,13 +557,27 @@ public class OpenMapTilesSchema { *

    *
  • "peak" *
  • "volcano" + *
  • "ridge" + *
  • "cliff" + *
  • "arete" *
*/ public static final String CLASS = "class"; /** Elevation (ele) in meters. */ public static final String ELE = "ele"; - /** Elevation (ele) in feets. */ + /** Elevation (ele) in feet. */ public static final String ELE_FT = "ele_ft"; + + /** + * Value 1 for peaks in location where feet is used as customary unit (USA). + *

+ * allowed values: + *

    + *
  • 1 + *
  • null + *
+ */ + public static final String CUSTOMARY_FT = "customary_ft"; /** Rank of the peak within one tile (starting at 1 that is the most important peak). */ public static final String RANK = "rank"; } @@ -572,7 +587,10 @@ public class OpenMapTilesSchema { public static final String CLASS_PEAK = "peak"; public static final String CLASS_VOLCANO = "volcano"; - public static final Set CLASS_VALUES = Set.of("peak", "volcano"); + public static final String CLASS_RIDGE = "ridge"; + public static final String CLASS_CLIFF = "cliff"; + public static final String CLASS_ARETE = "arete"; + public static final Set CLASS_VALUES = Set.of("peak", "volcano", "ridge", "cliff", "arete"); } /** Complex mappings to generate attribute values from OSM element tags in the mountain_peak layer. */ @@ -586,7 +604,7 @@ public class OpenMapTilesSchema { * boundary=protected_area, * or leisure=nature_reserve. *

- * Generated from park.yaml + * Generated from park.yaml */ public interface Park extends Layer { @@ -645,7 +663,7 @@ public class OpenMapTilesSchema { * contains several admin_level * but for most styles it makes sense to just style admin_level=2 and admin_level=4. *

- * Generated from boundary.yaml + * Generated from boundary.yaml */ public interface Boundary extends Layer { @@ -751,7 +769,7 @@ public class OpenMapTilesSchema { * buildings are contained in the building layer but all other airport related polygons can be found * in the aeroway layer. *

- * Generated from aeroway.yaml + * Generated from aeroway.yaml */ public interface Aeroway extends Layer { @@ -814,7 +832,7 @@ public class OpenMapTilesSchema { * roads is the most essential part of the map. The transportation layer also contains polygons for * features like plazas. *

- * Generated from transportation.yaml + * Generated from transportation.yaml */ public interface Transportation extends Layer { @@ -836,7 +854,7 @@ public class OpenMapTilesSchema { * href="http://wiki.openstreetmap.org/wiki/Key:railway">railway, aerialway, route tag (for shipping ways), or man_made. + * href="http://wiki.openstreetmap.org/wiki/Key:man_made">man_made. *

* allowed values: *

    @@ -850,6 +868,7 @@ public class OpenMapTilesSchema { *
  • service *
  • track *
  • raceway + *
  • busway *
  • motorway_construction *
  • trunk_construction *
  • primary_construction @@ -892,6 +911,13 @@ public class OpenMapTilesSchema { *
*/ public static final String SUBCLASS = "subclass"; + /** + * The network type derived mainly from network + * tag of the road. See more info about us- + * , ca-transcanada, or + * gb- . + */ + public static final String NETWORK = "network"; /** * Mark whether way is a tunnel or bridge. @@ -944,6 +970,40 @@ public class OpenMapTilesSchema { * */ public static final String SERVICE = "service"; + + /** + * Access restrictions on this road. Supported values of the access + * tag are no and private, which resolve to no. + *

+ * allowed values: + *

    + *
  • false + *
+ */ + public static final String ACCESS = "access"; + + /** + * Whether this is a toll road, based on the toll + * tag. + *

+ * allowed values: + *

    + *
  • 0 + *
  • 1 + *
+ */ + public static final String TOLL = "toll"; + + /** + * Whether this is an expressway, based on the expressway + * tag. + *

+ * allowed values: + *

    + *
  • 1 + *
+ */ + public static final String EXPRESSWAY = "expressway"; /** Original value of the layer tag. */ public static final String LAYER = "layer"; /** @@ -1012,6 +1072,7 @@ public class OpenMapTilesSchema { public static final String CLASS_SERVICE = "service"; public static final String CLASS_TRACK = "track"; public static final String CLASS_RACEWAY = "raceway"; + public static final String CLASS_BUSWAY = "busway"; public static final String CLASS_MOTORWAY_CONSTRUCTION = "motorway_construction"; public static final String CLASS_TRUNK_CONSTRUCTION = "trunk_construction"; public static final String CLASS_PRIMARY_CONSTRUCTION = "primary_construction"; @@ -1023,7 +1084,7 @@ public class OpenMapTilesSchema { public static final String CLASS_TRACK_CONSTRUCTION = "track_construction"; public static final String CLASS_RACEWAY_CONSTRUCTION = "raceway_construction"; public static final Set CLASS_VALUES = Set.of("motorway", "trunk", "primary", "secondary", "tertiary", - "minor", "path", "service", "track", "raceway", "motorway_construction", "trunk_construction", + "minor", "path", "service", "track", "raceway", "busway", "motorway_construction", "trunk_construction", "primary_construction", "secondary_construction", "tertiary_construction", "minor_construction", "path_construction", "service_construction", "track_construction", "raceway_construction"); public static final String SUBCLASS_RAIL = "rail"; @@ -1079,7 +1140,7 @@ public class OpenMapTilesSchema { MultiExpression.entry("service", matchAny("highway", "service")), MultiExpression.entry("track", matchAny("highway", "track")), MultiExpression.entry("raceway", matchAny("highway", "raceway")), - MultiExpression.entry("motorway_construction", + MultiExpression.entry("busway", matchAny("highway", "busway")), MultiExpression.entry("motorway_construction", and(matchAny("highway", "construction"), matchAny("construction", "motorway", "motorway_link"))), MultiExpression.entry("trunk_construction", and(matchAny("highway", "construction"), matchAny("construction", "trunk", "trunk_link"))), @@ -1107,7 +1168,7 @@ public class OpenMapTilesSchema { * href="http://wiki.openstreetmap.org/wiki/Key:building">building= ). The buildings are not yet * ready for 3D rendering support and any help to improve this is welcomed. *

- * Generated from building.yaml + * Generated from building.yaml */ public interface Building extends Layer { @@ -1157,7 +1218,7 @@ public class OpenMapTilesSchema { * Lake center lines for labelling lake bodies. This is based of the osm-lakelines * project which derives nice centerlines from OSM water bodies. Only the most important lakes contain labels. *

- * Generated from water_name.yaml + * Generated from water_name.yaml */ public interface WaterName extends Layer { @@ -1221,7 +1282,7 @@ public class OpenMapTilesSchema { * placement than having many small linestrings. For motorways you should use the ref field to label them * while for other roads you should use name. *

- * Generated from transportation_name.yaml + * Generated from transportation_name.yaml */ public interface TransportationName extends Layer { @@ -1299,13 +1360,15 @@ public class OpenMapTilesSchema { *

  • "raceway_construction" *
  • "rail" *
  • "transit" + *
  • "motorway_junction" * */ public static final String CLASS = "class"; /** * Distinguish more specific classes of path: Subclass is value of the highway - * (for paths). + * (for paths), and "junction" for motorway + * junctions. *

    * allowed values: *

      @@ -1317,6 +1380,7 @@ public class OpenMapTilesSchema { *
    • "bridleway" *
    • "corridor" *
    • "platform" + *
    • "junction" *
    */ public static final String SUBCLASS = "subclass"; @@ -1353,6 +1417,18 @@ public class OpenMapTilesSchema { * */ public static final String INDOOR = "indoor"; + /** 1st route concurrency. */ + public static final String ROUTE_1 = "route_1"; + /** 2nd route concurrency. */ + public static final String ROUTE_2 = "route_2"; + /** 3rd route concurrency. */ + public static final String ROUTE_3 = "route_3"; + /** 4th route concurrency. */ + public static final String ROUTE_4 = "route_4"; + /** 5th route concurrency. */ + public static final String ROUTE_5 = "route_5"; + /** 6th route concurrency. */ + public static final String ROUTE_6 = "route_6"; } /** Attribute values for map elements in the transportation_name layer. */ @@ -1389,10 +1465,12 @@ public class OpenMapTilesSchema { public static final String CLASS_RACEWAY_CONSTRUCTION = "raceway_construction"; public static final String CLASS_RAIL = "rail"; public static final String CLASS_TRANSIT = "transit"; + public static final String CLASS_MOTORWAY_JUNCTION = "motorway_junction"; public static final Set CLASS_VALUES = Set.of("motorway", "trunk", "primary", "secondary", "tertiary", "minor", "service", "track", "path", "raceway", "motorway_construction", "trunk_construction", "primary_construction", "secondary_construction", "tertiary_construction", "minor_construction", - "service_construction", "track_construction", "path_construction", "raceway_construction", "rail", "transit"); + "service_construction", "track_construction", "path_construction", "raceway_construction", "rail", "transit", + "motorway_junction"); public static final String SUBCLASS_PEDESTRIAN = "pedestrian"; public static final String SUBCLASS_PATH = "path"; public static final String SUBCLASS_FOOTWAY = "footway"; @@ -1401,8 +1479,9 @@ public class OpenMapTilesSchema { public static final String SUBCLASS_BRIDLEWAY = "bridleway"; public static final String SUBCLASS_CORRIDOR = "corridor"; public static final String SUBCLASS_PLATFORM = "platform"; + public static final String SUBCLASS_JUNCTION = "junction"; public static final Set SUBCLASS_VALUES = Set.of("pedestrian", "path", "footway", "cycleway", "steps", - "bridleway", "corridor", "platform"); + "bridleway", "corridor", "platform", "junction"); public static final String BRUNNEL_BRIDGE = "bridge"; public static final String BRUNNEL_TUNNEL = "tunnel"; public static final String BRUNNEL_FORD = "ford"; @@ -1422,7 +1501,7 @@ public class OpenMapTilesSchema { * important layers to create a beautiful map. We suggest you use different font styles and sizes to create a text * hierarchy. *

    - * Generated from place.yaml + * Generated from place.yaml */ public interface Place extends Layer { @@ -1467,6 +1546,7 @@ public class OpenMapTilesSchema { *

  • "continent" *
  • "country" *
  • "state" + *
  • "province" *
  • "city" *
  • "town" *
  • "village" @@ -1505,6 +1585,7 @@ public class OpenMapTilesSchema { public static final String CLASS_CONTINENT = "continent"; public static final String CLASS_COUNTRY = "country"; public static final String CLASS_STATE = "state"; + public static final String CLASS_PROVINCE = "province"; public static final String CLASS_CITY = "city"; public static final String CLASS_TOWN = "town"; public static final String CLASS_VILLAGE = "village"; @@ -1513,8 +1594,8 @@ public class OpenMapTilesSchema { public static final String CLASS_QUARTER = "quarter"; public static final String CLASS_NEIGHBOURHOOD = "neighbourhood"; public static final String CLASS_ISOLATED_DWELLING = "isolated_dwelling"; - public static final Set CLASS_VALUES = Set.of("continent", "country", "state", "city", "town", "village", - "hamlet", "suburb", "quarter", "neighbourhood", "isolated_dwelling"); + public static final Set CLASS_VALUES = Set.of("continent", "country", "state", "province", "city", "town", + "village", "hamlet", "suburb", "quarter", "neighbourhood", "isolated_dwelling"); } /** Complex mappings to generate attribute values from OSM element tags in the place layer. */ @@ -1528,7 +1609,7 @@ public class OpenMapTilesSchema { * a map. This adds significant size to z14. For buildings the centroid of the building is used as * housenumber. *

    - * Generated from housenumber.yaml + * Generated from housenumber.yaml */ public interface Housenumber extends Layer { @@ -1562,7 +1643,7 @@ public class OpenMapTilesSchema { * 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 { @@ -1732,10 +1813,11 @@ public class OpenMapTilesSchema { matchAny("subclass", "accessories", "antiques", "beauty", "bed", "boutique", "camera", "carpet", "charity", "chemist", "coffee", "computer", "convenience", "copyshop", "cosmetics", "garden_centre", "doityourself", "erotic", "electronics", "fabric", "florist", "frozen_food", "furniture", "video_games", "video", "general", - "gift", "hardware", "hearing_aids", "hifi", "ice_cream", "interior_decoration", "jewelry", "kiosk", "lamps", - "mall", "massage", "motorcycle", "mobile_phone", "newsagent", "optician", "outdoor", "perfumery", "perfume", - "pet", "photo", "second_hand", "shoes", "sports", "stationery", "tailor", "tattoo", "ticket", "tobacco", - "toys", "travel_agency", "watches", "weapons", "wholesale")), MultiExpression.entry("town_hall", + "gift", "hardware", "hearing_aids", "hifi", "ice_cream", "interior_decoration", "jewelry", "kiosk", + "locksmith", "lamps", "mall", "massage", "motorcycle", "mobile_phone", "newsagent", "optician", "outdoor", + "perfumery", "perfume", "pet", "photo", "second_hand", "shoes", "sports", "stationery", "tailor", "tattoo", + "ticket", "tobacco", "toys", "travel_agency", "watches", "weapons", "wholesale")), + MultiExpression.entry("town_hall", matchAny("subclass", "townhall", "public_building", "courthouse", "community_centre")), MultiExpression.entry("golf", matchAny("subclass", "golf", "golf_course", "miniature_golf")), MultiExpression.entry("fast_food", matchAny("subclass", "fast_food", "food_court")), @@ -1777,7 +1859,7 @@ public class OpenMapTilesSchema { /** * Aerodrome labels *

    - * Generated from aerodrome_label.yaml + * Generated from aerodrome_label.yaml */ public interface AerodromeLabel extends Layer { diff --git a/src/main/java/com/onthegomap/planetiler/basemap/generated/Tables.java b/src/main/java/com/onthegomap/planetiler/basemap/generated/Tables.java index c242959..af81966 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/generated/Tables.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/generated/Tables.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -50,7 +50,7 @@ import java.util.Map; /** * 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 @@ -75,7 +75,7 @@ public class Tables { } /** The {@code rowClass} of an imposm3 table row and its constructor coerced to a {@link Constructor}. */ - public static record RowClassAndConstructor( + public record RowClassAndConstructor( Class rowClass, Constructor create ) {} @@ -89,31 +89,31 @@ public class Tables { } /** The {@code handlerClass} of a layer handler and it's {@code process} method coerced to a {@link RowHandler}. */ - public static record RowHandlerAndClass( + public record RowHandlerAndClass( Class handlerClass, RowHandler handler ) {} /** An OSM element that would appear in the {@code osm_water_polygon} table generated by imposm3. */ - public static record OsmWaterPolygon( + public record OsmWaterPolygon( @Override String name, @Override String nameEn, @Override String nameDe, @Override String natural, - @Override String landuse, @Override String waterway, @Override boolean isIntermittent, @Override boolean isTunnel, - @Override boolean isBridge, @Override SourceFeature source - ) implements Row, WithName, WithNameEn, WithNameDe, WithNatural, WithLanduse, WithWaterway, WithIsIntermittent, - WithIsTunnel, WithIsBridge, WithSource { + @Override String landuse, @Override String waterway, @Override String water, @Override boolean isIntermittent, + @Override boolean isTunnel, @Override boolean isBridge, @Override SourceFeature source + ) implements Row, WithName, WithNameEn, WithNameDe, WithNatural, WithLanduse, WithWaterway, WithWater, + WithIsIntermittent, WithIsTunnel, WithIsBridge, WithSource { public OsmWaterPolygon(SourceFeature source, String mappingKey) { this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), source.getString("natural"), source.getString("landuse"), source.getString("waterway"), - source.getBoolean("intermittent"), source.getBoolean("tunnel"), source.getBoolean("bridge"), source); + source.getString("water"), source.getBoolean("intermittent"), source.getBoolean("tunnel"), + source.getBoolean("bridge"), source); } /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ public static final Expression MAPPING = and( or(matchAny("landuse", "reservoir", "basin", "salt_pond"), matchAny("leisure", "swimming_pool"), - matchAny("natural", "water", "bay"), - matchAny("waterway", "river", "riverbank", "stream", "canal", "drain", "ditch", "dock")), - not(matchAny("covered", "yes")), matchType("polygon")); + matchAny("natural", "water", "bay", "spring"), matchAny("waterway", "riverbank", "dock"), + matchAny("water", "river")), not(matchAny("covered", "yes")), matchType("polygon")); /** * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link @@ -126,7 +126,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_waterway_linestring} table generated by imposm3. */ - public static record OsmWaterwayLinestring( + public record OsmWaterwayLinestring( @Override String waterway, @Override String name, @Override String nameEn, @Override String nameDe, @Override boolean isTunnel, @Override boolean isBridge, @Override boolean isIntermittent, @Override SourceFeature source @@ -154,7 +154,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_landcover_polygon} table generated by imposm3. */ - public static record OsmLandcoverPolygon( + public record OsmLandcoverPolygon( @Override String subclass, @Override String mappingKey, @Override SourceFeature source ) implements Row, WithSubclass, WithMappingKey, WithSource { @@ -165,7 +165,7 @@ public class Tables { /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ public static final Expression MAPPING = and(or( matchAny("landuse", "allotments", "farm", "farmland", "orchard", "plant_nursery", "vineyard", "grass", - "grassland", "meadow", "forest", "village_green", "recreation_ground", "park"), + "grassland", "meadow", "forest", "village_green", "recreation_ground"), matchAny("natural", "wood", "wetland", "fell", "grassland", "heath", "scrub", "tundra", "glacier", "bare_rock", "scree", "beach", "sand", "dune"), matchAny("leisure", "park", "garden", "golf_course"), matchAny("wetland", "bog", "swamp", "wet_meadow", "marsh", "reedbed", "saltern", "tidalflat", "saltmarsh", @@ -182,7 +182,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_landuse_polygon} table generated by imposm3. */ - public static record OsmLandusePolygon( + public record OsmLandusePolygon( @Override String landuse, @Override String amenity, @Override String leisure, @Override String tourism, @Override String place, @Override String waterway, @Override SourceFeature source ) implements Row, WithLanduse, WithAmenity, WithLeisure, WithTourism, WithPlace, WithWaterway, WithSource { @@ -196,9 +196,10 @@ public class Tables { public static final Expression MAPPING = and(or( matchAny("landuse", "railway", "cemetery", "military", "residential", "commercial", "industrial", "garages", "retail"), - matchAny("amenity", "bus_station", "school", "university", "kindergarten", "college", "library", "hospital"), - matchAny("leisure", "stadium", "pitch", "playground", "track"), matchAny("tourism", "theme_park", "zoo"), - matchAny("place", "suburb", "quarter", "neighbourhood"), matchAny("waterway", "dam")), matchType("polygon")); + matchAny("amenity", "bus_station", "school", "university", "kindergarten", "college", "library", "hospital", + "grave_yard"), matchAny("leisure", "stadium", "pitch", "playground", "track"), + matchAny("tourism", "theme_park", "zoo"), matchAny("place", "suburb", "quarter", "neighbourhood"), + matchAny("waterway", "dam")), matchType("polygon")); /** * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link @@ -211,7 +212,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_peak_point} table generated by imposm3. */ - public static record OsmPeakPoint( + public record OsmPeakPoint( @Override String name, @Override String nameEn, @Override String nameDe, @Override String ele, @Override String wikipedia, @Override SourceFeature source ) implements Row, WithName, WithNameEn, WithNameDe, WithEle, WithWikipedia, WithSource { @@ -222,7 +223,7 @@ public class Tables { } /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ - public static final Expression MAPPING = and(matchAny("natural", "peak", "volcano"), matchType("point")); + public static final Expression MAPPING = and(matchAny("natural", "peak", "volcano", "saddle"), matchType("point")); /** * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link @@ -234,8 +235,33 @@ public class Tables { } } + /** An OSM element that would appear in the {@code osm_mountain_linestring} table generated by imposm3. */ + public record OsmMountainLinestring( + @Override String name, @Override String nameEn, @Override String nameDe, @Override String wikipedia, + @Override SourceFeature source + ) implements Row, WithName, WithNameEn, WithNameDe, WithWikipedia, WithSource { + + public OsmMountainLinestring(SourceFeature source, String mappingKey) { + this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), + source.getString("wikipedia"), source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(matchAny("natural", "ridge", "cliff", "arete"), + matchType("linestring")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmMountainLinestring}. + */ + public interface Handler { + + void process(OsmMountainLinestring element, FeatureCollector features); + } + } + /** An OSM element that would appear in the {@code osm_park_polygon} table generated by imposm3. */ - public static record OsmParkPolygon( + public record OsmParkPolygon( @Override String name, @Override String nameEn, @Override String nameDe, @Override String landuse, @Override String leisure, @Override String boundary, @Override String protectionTitle, @Override SourceFeature source @@ -264,7 +290,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_aeroway_polygon} table generated by imposm3. */ - public static record OsmAerowayPolygon( + public record OsmAerowayPolygon( @Override String ref, @Override String aeroway, @Override SourceFeature source ) implements Row, WithRef, WithAeroway, WithSource { @@ -289,7 +315,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_aeroway_linestring} table generated by imposm3. */ - public static record OsmAerowayLinestring( + public record OsmAerowayLinestring( @Override String ref, @Override String aeroway, @Override SourceFeature source ) implements Row, WithRef, WithAeroway, WithSource { @@ -311,7 +337,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_aeroway_point} table generated by imposm3. */ - public static record OsmAerowayPoint( + public record OsmAerowayPoint( @Override String ref, @Override String aeroway, @Override SourceFeature source ) implements Row, WithRef, WithAeroway, WithSource { @@ -333,18 +359,19 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_highway_linestring} table generated by imposm3. */ - public static record OsmHighwayLinestring( + public record OsmHighwayLinestring( @Override String highway, @Override String construction, @Override String ref, @Override String network, @Override int zOrder, @Override long layer, @Override long level, @Override boolean indoor, @Override String name, @Override String nameEn, @Override String nameDe, @Override String shortName, @Override boolean isTunnel, @Override boolean isBridge, @Override boolean isRamp, @Override boolean isFord, @Override int isOneway, - @Override boolean isArea, @Override String service, @Override String usage, @Override String publicTransport, - @Override String manMade, @Override String bicycle, @Override String foot, @Override String horse, - @Override String mtbScale, @Override String surface, @Override SourceFeature source + @Override boolean isArea, @Override String service, @Override String access, @Override boolean toll, + @Override String usage, @Override String publicTransport, @Override String manMade, @Override String bicycle, + @Override String foot, @Override String horse, @Override String mtbScale, @Override String sacScale, + @Override String surface, @Override boolean expressway, @Override SourceFeature source ) implements Row, WithHighway, WithConstruction, WithRef, WithNetwork, WithZOrder, WithLayer, WithLevel, WithIndoor, WithName, WithNameEn, WithNameDe, WithShortName, WithIsTunnel, WithIsBridge, WithIsRamp, WithIsFord, WithIsOneway, - WithIsArea, WithService, WithUsage, WithPublicTransport, WithManMade, WithBicycle, WithFoot, WithHorse, - WithMtbScale, WithSurface, WithSource { + WithIsArea, WithService, WithAccess, WithToll, WithUsage, WithPublicTransport, WithManMade, WithBicycle, WithFoot, + WithHorse, WithMtbScale, WithSacScale, WithSurface, WithExpressway, WithSource { public OsmHighwayLinestring(SourceFeature source, String mappingKey) { this(source.getString("highway"), source.getString("construction"), source.getString("ref"), @@ -352,18 +379,19 @@ public class Tables { source.getBoolean("indoor"), source.getString("name"), source.getString("name:en"), source.getString("name:de"), source.getString("short_name"), source.getBoolean("tunnel"), source.getBoolean("bridge"), source.getBoolean("ramp"), source.getBoolean("ford"), source.getDirection("oneway"), source.getBoolean("area"), - source.getString("service"), source.getString("usage"), source.getString("public_transport"), - source.getString("man_made"), source.getString("bicycle"), source.getString("foot"), source.getString("horse"), - source.getString("mtb:scale"), source.getString("surface"), source); + source.getString("service"), source.getString("access"), source.getBoolean("toll"), source.getString("usage"), + source.getString("public_transport"), source.getString("man_made"), source.getString("bicycle"), + source.getString("foot"), source.getString("horse"), source.getString("mtb:scale"), + source.getString("sac_scale"), source.getString("surface"), source.getBoolean("expressway"), source); } /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ public static final Expression MAPPING = and(or( - matchAny("highway", "motorway", "motorway_link", "trunk", "trunk_link", "primary", "primary_link", "secondary", - "secondary_link", "tertiary", "tertiary_link", "unclassified", "residential", "living_street", "road", - "pedestrian", "path", "footway", "cycleway", "steps", "bridleway", "corridor", "service", "track", "raceway", - "construction"), matchAny("public_transport", "platform"), matchAny("man_made", "pier")), - matchType("linestring")); + matchAny("highway", "motorway", "motorway_link", "trunk", "trunk_link", "primary", "primary_link", "secondary", + "secondary_link", "tertiary", "tertiary_link", "unclassified", "residential", "living_street", "road", + "pedestrian", "path", "footway", "cycleway", "steps", "bridleway", "corridor", "service", "track", "raceway", + "busway", "construction"), matchAny("public_transport", "platform"), matchAny("man_made", "pier"), + matchAny("service", "driveway", "parking_aisle")), matchType("linestring")); /** * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link @@ -376,7 +404,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_railway_linestring} table generated by imposm3. */ - public static record OsmRailwayLinestring( + public record OsmRailwayLinestring( @Override String railway, @Override String ref, @Override String network, @Override int zOrder, @Override long layer, @Override long level, @Override boolean indoor, @Override String name, @Override String nameEn, @Override String nameDe, @Override String shortName, @Override boolean isTunnel, @@ -411,7 +439,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_aerialway_linestring} table generated by imposm3. */ - public static record OsmAerialwayLinestring( + public record OsmAerialwayLinestring( @Override String aerialway, @Override int zOrder, @Override long layer, @Override String name, @Override String nameEn, @Override String nameDe, @Override String shortName, @Override boolean isTunnel, @Override boolean isBridge, @Override boolean isRamp, @Override boolean isFord, @Override int isOneway, @@ -442,7 +470,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_shipway_linestring} table generated by imposm3. */ - public static record OsmShipwayLinestring( + public record OsmShipwayLinestring( @Override String shipway, @Override int zOrder, @Override long layer, @Override String name, @Override String nameEn, @Override String nameDe, @Override String shortName, @Override boolean isTunnel, @Override boolean isBridge, @Override boolean isRamp, @Override boolean isFord, @Override int isOneway, @@ -472,17 +500,17 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_highway_polygon} table generated by imposm3. */ - public static record OsmHighwayPolygon( + public record OsmHighwayPolygon( @Override String highway, @Override int zOrder, @Override long layer, @Override long level, @Override boolean indoor, @Override boolean isArea, @Override String publicTransport, @Override String manMade, - @Override SourceFeature source + @Override String service, @Override SourceFeature source ) implements Row, WithHighway, WithZOrder, WithLayer, WithLevel, WithIndoor, WithIsArea, WithPublicTransport, - WithManMade, WithSource { + WithManMade, WithService, WithSource { public OsmHighwayPolygon(SourceFeature source, String mappingKey) { this(source.getString("highway"), source.getWayZorder(), source.getLong("layer"), source.getLong("level"), source.getBoolean("indoor"), source.getBoolean("area"), source.getString("public_transport"), - source.getString("man_made"), source); + source.getString("man_made"), source.getString("service"), source); } /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ @@ -500,8 +528,34 @@ public class Tables { } } + /** An OSM element that would appear in the {@code osm_highway_point} table generated by imposm3. */ + public record OsmHighwayPoint( + @Override String highway, @Override int zOrder, @Override long layer, @Override long level, @Override String name, + @Override String nameEn, @Override String nameDe, @Override String ref, @Override SourceFeature source + ) implements Row, WithHighway, WithZOrder, WithLayer, WithLevel, WithName, WithNameEn, WithNameDe, WithRef, + WithSource { + + public OsmHighwayPoint(SourceFeature source, String mappingKey) { + this(source.getString("highway"), source.getWayZorder(), source.getLong("layer"), source.getLong("level"), + source.getString("name"), source.getString("name:en"), source.getString("name:de"), source.getString("ref"), + source); + } + + /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ + public static final Expression MAPPING = and(matchAny("highway", "motorway_junction"), matchType("point")); + + /** + * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link + * OsmHighwayPoint}. + */ + public interface Handler { + + void process(OsmHighwayPoint element, FeatureCollector features); + } + } + /** An OSM element that would appear in the {@code osm_building_polygon} table generated by imposm3. */ - public static record OsmBuildingPolygon( + public record OsmBuildingPolygon( @Override String material, @Override String colour, @Override String building, @Override String buildingpart, @Override String buildingheight, @Override String buildingminHeight, @Override String buildinglevels, @Override String buildingminLevel, @Override String height, @Override String minHeight, @Override String levels, @@ -518,9 +572,10 @@ public class Tables { /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ public static final Expression MAPPING = and( - or(matchField("building:part"), matchField("building"), matchAny("aeroway", "terminal", "hangar")), - not(matchAny("building", "no", "none", "No")), not(matchAny("building:part", "no", "none", "No")), - not(matchAny("man_made", "bridge")), matchType("polygon")); + or(matchField("building:part"), matchField("building"), matchAny("aeroway", "terminal", "hangar"), + matchAny("location", "underground")), not(matchAny("building", "no", "none", "No")), + not(matchAny("building:part", "no", "none", "No")), not(matchAny("man_made", "bridge")), + not(matchAny("location", "underground")), matchType("polygon")); /** * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link @@ -533,7 +588,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_marine_point} table generated by imposm3. */ - public static record OsmMarinePoint( + public record OsmMarinePoint( @Override String name, @Override String nameEn, @Override String nameDe, @Override String place, @Override long rank, @Override boolean isIntermittent, @Override SourceFeature source ) implements Row, WithName, WithNameEn, WithNameDe, WithPlace, WithRank, WithIsIntermittent, WithSource { @@ -558,7 +613,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_continent_point} table generated by imposm3. */ - public static record OsmContinentPoint( + public record OsmContinentPoint( @Override String name, @Override String nameEn, @Override String nameDe, @Override SourceFeature source ) implements Row, WithName, WithNameEn, WithNameDe, WithSource { @@ -581,7 +636,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_country_point} table generated by imposm3. */ - public static record OsmCountryPoint( + public record OsmCountryPoint( @Override String name, @Override String nameEn, @Override String nameDe, @Override long rank, @Override String countryCodeIso31661Alpha2, @Override String iso31661Alpha2, @Override String iso31661, @Override SourceFeature source @@ -608,7 +663,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_island_polygon} table generated by imposm3. */ - public static record OsmIslandPolygon( + public record OsmIslandPolygon( @Override String name, @Override String nameEn, @Override String nameDe, @Override long rank, @Override SourceFeature source ) implements Row, WithName, WithNameEn, WithNameDe, WithRank, WithSource { @@ -632,7 +687,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_island_point} table generated by imposm3. */ - public static record OsmIslandPoint( + public record OsmIslandPoint( @Override String name, @Override String nameEn, @Override String nameDe, @Override long rank, @Override SourceFeature source ) implements Row, WithName, WithNameEn, WithNameDe, WithRank, WithSource { @@ -656,20 +711,22 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_state_point} table generated by imposm3. */ - public static record OsmStatePoint( - @Override String name, @Override String nameEn, @Override String nameDe, @Override String isInCountry, - @Override String isInCountryCode, @Override String ref, @Override long rank, @Override SourceFeature source - ) implements Row, WithName, WithNameEn, WithNameDe, WithIsInCountry, WithIsInCountryCode, WithRef, WithRank, - WithSource { + public record OsmStatePoint( + @Override String name, @Override String nameEn, @Override String nameDe, @Override String place, + @Override String isInCountry, @Override String isInCountryCode, @Override String ref, @Override long rank, + @Override SourceFeature source + ) implements Row, WithName, WithNameEn, WithNameDe, WithPlace, WithIsInCountry, WithIsInCountryCode, WithRef, + WithRank, WithSource { public OsmStatePoint(SourceFeature source, String mappingKey) { this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), - source.getString("is_in:country"), source.getString("is_in:country_code"), source.getString("ref"), - source.getLong("rank"), source); + source.getString("place"), source.getString("is_in:country"), source.getString("is_in:country_code"), + source.getString("ref"), source.getLong("rank"), source); } /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ - public static final Expression MAPPING = and(matchAny("place", "state"), matchField("name"), matchType("point")); + public static final Expression MAPPING = and(matchAny("place", "state", "province"), matchField("name"), + matchType("point")); /** * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as {@link @@ -682,7 +739,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_city_point} table generated by imposm3. */ - public static record OsmCityPoint( + public record OsmCityPoint( @Override String name, @Override String nameEn, @Override String nameDe, @Override String place, @Override long population, @Override String capital, @Override long rank, @Override SourceFeature source ) implements Row, WithName, WithNameEn, WithNameDe, WithPlace, WithPopulation, WithCapital, WithRank, WithSource { @@ -709,7 +766,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_housenumber_point} table generated by imposm3. */ - public static record OsmHousenumberPoint(@Override String housenumber, @Override SourceFeature source) implements Row, + public record OsmHousenumberPoint(@Override String housenumber, @Override SourceFeature source) implements Row, WithHousenumber, WithSource { public OsmHousenumberPoint(SourceFeature source, String mappingKey) { @@ -731,7 +788,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_poi_point} table generated by imposm3. */ - public static record OsmPoiPoint( + public record OsmPoiPoint( @Override String name, @Override String nameEn, @Override String nameDe, @Override String subclass, @Override String mappingKey, @Override String station, @Override String funicular, @Override String information, @Override String uicRef, @Override String religion, @Override long level, @Override boolean indoor, @@ -751,9 +808,9 @@ public class Tables { public static final Expression MAPPING = and(or(matchAny("aerialway", "station"), matchAny("amenity", "arts_centre", "bank", "bar", "bbq", "bicycle_parking", "bicycle_rental", "biergarten", "bus_station", "cafe", "cinema", "clinic", "college", "community_centre", "courthouse", "dentist", "doctors", - "drinking_water", "embassy", "fast_food", "ferry_terminal", "fire_station", "food_court", "fuel", "grave_yard", - "hospital", "ice_cream", "kindergarten", "library", "marketplace", "motorcycle_parking", "nightclub", - "nursing_home", "parking", "pharmacy", "place_of_worship", "police", "post_box", "post_office", "prison", "pub", + "drinking_water", "fast_food", "ferry_terminal", "fire_station", "food_court", "fuel", "grave_yard", "hospital", + "ice_cream", "kindergarten", "library", "marketplace", "motorcycle_parking", "nightclub", "nursing_home", + "parking", "pharmacy", "place_of_worship", "police", "post_box", "post_office", "prison", "pub", "public_building", "recycling", "restaurant", "school", "shelter", "swimming_pool", "taxi", "telephone", "theatre", "toilets", "townhall", "university", "veterinary", "waste_basket"), matchAny("barrier", "bollard", "border_control", "cycle_barrier", "gate", "lift_gate", "sally_port", "stile", @@ -762,17 +819,18 @@ public class Tables { matchAny("landuse", "basin", "brownfield", "cemetery", "reservoir", "winter_sports"), matchAny("leisure", "dog_park", "escape_game", "garden", "golf_course", "ice_rink", "hackerspace", "marina", "miniature_golf", "park", "pitch", "playground", "sports_centre", "stadium", "swimming_area", "swimming_pool", - "water_park"), matchAny("railway", "halt", "station", "subway_entrance", "train_station_entrance", "tram_stop"), + "water_park"), matchAny("office", "diplomatic"), + matchAny("railway", "halt", "station", "subway_entrance", "train_station_entrance", "tram_stop"), matchAny("shop", "accessories", "alcohol", "antiques", "art", "bag", "bakery", "beauty", "bed", "beverages", "bicycle", "books", "boutique", "butcher", "camera", "car", "car_repair", "car_parts", "carpet", "charity", "chemist", "chocolate", "clothes", "coffee", "computer", "confectionery", "convenience", "copyshop", "cosmetics", "deli", "delicatessen", "department_store", "doityourself", "dry_cleaning", "electronics", "erotic", "fabric", "florist", "frozen_food", "furniture", "garden_centre", "general", "gift", "greengrocer", "hairdresser", "hardware", "hearing_aids", "hifi", "ice_cream", "interior_decoration", "jewelry", "kiosk", - "lamps", "laundry", "mall", "massage", "mobile_phone", "motorcycle", "music", "musical_instrument", "newsagent", - "optician", "outdoor", "perfume", "perfumery", "pet", "photo", "second_hand", "shoes", "sports", "stationery", - "supermarket", "tailor", "tattoo", "ticket", "tobacco", "toys", "travel_agency", "video", "video_games", - "watches", "weapons", "wholesale", "wine"), + "lamps", "laundry", "locksmith", "mall", "massage", "mobile_phone", "motorcycle", "music", "musical_instrument", + "newsagent", "optician", "outdoor", "perfume", "perfumery", "pet", "photo", "second_hand", "shoes", "sports", + "stationery", "supermarket", "tailor", "tattoo", "ticket", "tobacco", "toys", "travel_agency", "video", + "video_games", "watches", "weapons", "wholesale", "wine"), matchAny("sport", "american_football", "archery", "athletics", "australian_football", "badminton", "baseball", "basketball", "beachvolleyball", "billiards", "bmx", "boules", "bowls", "boxing", "canadian_football", "canoe", "chess", "climbing", "climbing_adventure", "cricket", "cricket_nets", "croquet", "curling", "cycling", @@ -798,7 +856,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_poi_polygon} table generated by imposm3. */ - public static record OsmPoiPolygon( + public record OsmPoiPolygon( @Override String name, @Override String nameEn, @Override String nameDe, @Override String subclass, @Override String mappingKey, @Override String station, @Override String funicular, @Override String information, @Override String uicRef, @Override String religion, @Override long level, @Override boolean indoor, @@ -818,9 +876,9 @@ public class Tables { public static final Expression MAPPING = and(or(matchAny("aerialway", "station"), matchAny("amenity", "arts_centre", "bank", "bar", "bbq", "bicycle_parking", "bicycle_rental", "biergarten", "bus_station", "cafe", "cinema", "clinic", "college", "community_centre", "courthouse", "dentist", "doctors", - "drinking_water", "embassy", "fast_food", "ferry_terminal", "fire_station", "food_court", "fuel", "grave_yard", - "hospital", "ice_cream", "kindergarten", "library", "marketplace", "motorcycle_parking", "nightclub", - "nursing_home", "parking", "pharmacy", "place_of_worship", "police", "post_box", "post_office", "prison", "pub", + "drinking_water", "fast_food", "ferry_terminal", "fire_station", "food_court", "fuel", "grave_yard", "hospital", + "ice_cream", "kindergarten", "library", "marketplace", "motorcycle_parking", "nightclub", "nursing_home", + "parking", "pharmacy", "place_of_worship", "police", "post_box", "post_office", "prison", "pub", "public_building", "recycling", "restaurant", "school", "shelter", "swimming_pool", "taxi", "telephone", "theatre", "toilets", "townhall", "university", "veterinary", "waste_basket"), matchAny("barrier", "bollard", "border_control", "cycle_barrier", "gate", "lift_gate", "sally_port", "stile", @@ -829,17 +887,18 @@ public class Tables { matchAny("landuse", "basin", "brownfield", "cemetery", "reservoir", "winter_sports"), matchAny("leisure", "dog_park", "escape_game", "garden", "golf_course", "ice_rink", "hackerspace", "marina", "miniature_golf", "park", "pitch", "playground", "sports_centre", "stadium", "swimming_area", "swimming_pool", - "water_park"), matchAny("railway", "halt", "station", "subway_entrance", "train_station_entrance", "tram_stop"), + "water_park"), matchAny("office", "diplomatic"), + matchAny("railway", "halt", "station", "subway_entrance", "train_station_entrance", "tram_stop"), matchAny("shop", "accessories", "alcohol", "antiques", "art", "bag", "bakery", "beauty", "bed", "beverages", "bicycle", "books", "boutique", "butcher", "camera", "car", "car_repair", "car_parts", "carpet", "charity", "chemist", "chocolate", "clothes", "coffee", "computer", "confectionery", "convenience", "copyshop", "cosmetics", "deli", "delicatessen", "department_store", "doityourself", "dry_cleaning", "electronics", "erotic", "fabric", "florist", "frozen_food", "furniture", "garden_centre", "general", "gift", "greengrocer", "hairdresser", "hardware", "hearing_aids", "hifi", "ice_cream", "interior_decoration", "jewelry", "kiosk", - "lamps", "laundry", "mall", "massage", "mobile_phone", "motorcycle", "music", "musical_instrument", "newsagent", - "optician", "outdoor", "perfume", "perfumery", "pet", "photo", "second_hand", "shoes", "sports", "stationery", - "supermarket", "tailor", "tattoo", "ticket", "tobacco", "toys", "travel_agency", "video", "video_games", - "watches", "weapons", "wholesale", "wine"), + "lamps", "laundry", "locksmith", "mall", "massage", "mobile_phone", "motorcycle", "music", "musical_instrument", + "newsagent", "optician", "outdoor", "perfume", "perfumery", "pet", "photo", "second_hand", "shoes", "sports", + "stationery", "supermarket", "tailor", "tattoo", "ticket", "tobacco", "toys", "travel_agency", "video", + "video_games", "watches", "weapons", "wholesale", "wine"), matchAny("sport", "american_football", "archery", "athletics", "australian_football", "badminton", "baseball", "basketball", "beachvolleyball", "billiards", "bmx", "boules", "bowls", "boxing", "canadian_football", "canoe", "chess", "climbing", "climbing_adventure", "cricket", "cricket_nets", "croquet", "curling", "cycling", @@ -865,7 +924,7 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_aerodrome_label_point} table generated by imposm3. */ - public static record OsmAerodromeLabelPoint( + public record OsmAerodromeLabelPoint( @Override String name, @Override String nameEn, @Override String nameDe, @Override String aerodromeType, @Override String aerodrome, @Override String military, @Override String iata, @Override String icao, @Override String ele, @Override SourceFeature source @@ -892,6 +951,12 @@ public class Tables { } } + /** Rows with a String access attribute. */ + public interface WithAccess { + + String access(); + } + /** Rows with a long adminLevel attribute. */ public interface WithAdminLevel { @@ -1006,18 +1071,18 @@ public class Tables { String countryCodeIso31661Alpha2(); } - /** Rows with a String disputedBy attribute. */ - public interface WithDisputedBy { - - String disputedBy(); - } - /** Rows with a String ele attribute. */ public interface WithEle { String ele(); } + /** Rows with a boolean expressway attribute. */ + public interface WithExpressway { + + boolean expressway(); + } + /** Rows with a String foot attribute. */ public interface WithFoot { @@ -1246,6 +1311,12 @@ public class Tables { String network(); } + /** Rows with a String osmcSymbol attribute. */ + public interface WithOsmcSymbol { + + String osmcSymbol(); + } + /** Rows with a String place attribute. */ public interface WithPlace { @@ -1342,6 +1413,12 @@ public class Tables { String relminLevel(); } + /** Rows with a String sacScale attribute. */ + public interface WithSacScale { + + String sacScale(); + } + /** Rows with a String service attribute. */ public interface WithService { @@ -1390,6 +1467,12 @@ public class Tables { String surface(); } + /** Rows with a boolean toll attribute. */ + public interface WithToll { + + boolean toll(); + } + /** Rows with a String tourism attribute. */ public interface WithTourism { @@ -1408,6 +1491,12 @@ public class Tables { String usage(); } + /** Rows with a String water attribute. */ + public interface WithWater { + + String water(); + } + /** Rows with a String waterway attribute. */ public interface WithWaterway { @@ -1437,6 +1526,8 @@ public class Tables { MultiExpression.entry(new RowClassAndConstructor(OsmLandusePolygon.class, OsmLandusePolygon::new), OsmLandusePolygon.MAPPING), MultiExpression.entry(new RowClassAndConstructor(OsmPeakPoint.class, OsmPeakPoint::new), OsmPeakPoint.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmMountainLinestring.class, OsmMountainLinestring::new), + OsmMountainLinestring.MAPPING), MultiExpression.entry(new RowClassAndConstructor(OsmParkPolygon.class, OsmParkPolygon::new), OsmParkPolygon.MAPPING), MultiExpression.entry(new RowClassAndConstructor(OsmAerowayPolygon.class, OsmAerowayPolygon::new), @@ -1455,6 +1546,8 @@ public class Tables { OsmShipwayLinestring.MAPPING), MultiExpression.entry(new RowClassAndConstructor(OsmHighwayPolygon.class, OsmHighwayPolygon::new), OsmHighwayPolygon.MAPPING), + MultiExpression.entry(new RowClassAndConstructor(OsmHighwayPoint.class, OsmHighwayPoint::new), + OsmHighwayPoint.MAPPING), MultiExpression.entry(new RowClassAndConstructor(OsmBuildingPolygon.class, OsmBuildingPolygon::new), OsmBuildingPolygon.MAPPING), MultiExpression.entry(new RowClassAndConstructor(OsmMarinePoint.class, OsmMarinePoint::new), @@ -1504,6 +1597,10 @@ public class Tables { result.computeIfAbsent(OsmPeakPoint.class, cls -> new ArrayList<>()) .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); } + if (handler instanceof OsmMountainLinestring.Handler typedHandler) { + result.computeIfAbsent(OsmMountainLinestring.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } if (handler instanceof OsmParkPolygon.Handler typedHandler) { result.computeIfAbsent(OsmParkPolygon.class, cls -> new ArrayList<>()) .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); @@ -1540,6 +1637,10 @@ public class Tables { result.computeIfAbsent(OsmHighwayPolygon.class, cls -> new ArrayList<>()) .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); } + if (handler instanceof OsmHighwayPoint.Handler typedHandler) { + result.computeIfAbsent(OsmHighwayPoint.class, cls -> new ArrayList<>()) + .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); + } if (handler instanceof OsmBuildingPolygon.Handler typedHandler) { result.computeIfAbsent(OsmBuildingPolygon.class, cls -> new ArrayList<>()) .add(new RowHandlerAndClass<>(typedHandler.getClass(), typedHandler::process)); diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/AerodromeLabel.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/AerodromeLabel.java index 1230a30..e5d4bcd 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/layers/AerodromeLabel.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/AerodromeLabel.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -36,6 +36,7 @@ See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for deta package com.onthegomap.planetiler.basemap.layers; import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty; +import static com.onthegomap.planetiler.basemap.util.Utils.nullOrEmpty; import com.onthegomap.planetiler.FeatureCollector; import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema; @@ -67,13 +68,15 @@ public class AerodromeLabel implements @Override public void process(Tables.OsmAerodromeLabelPoint element, FeatureCollector features) { + String clazz = classLookup.getOrElse(element.source(), FieldValues.CLASS_OTHER); + boolean important = !nullOrEmpty(element.iata()) && FieldValues.CLASS_INTERNATIONAL.equals(clazz); features.centroid(LAYER_NAME) .setBufferPixels(BUFFER_SIZE) - .setMinZoom(10) + .setMinZoom(important ? 8 : 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)); + .setAttr(Fields.CLASS, clazz); } } diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Aeroway.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Aeroway.java index 316ab52..9e5201c 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/layers/Aeroway.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Aeroway.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Boundary.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Boundary.java index e7c0a36..9f70621 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/layers/Boundary.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Boundary.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -168,7 +168,9 @@ public class Boundary implements : 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; + yield minZoom != null && minZoom <= 7 ? new BoundaryInfo(4, 1, 4) : + minZoom != null && minZoom <= 7.7 ? new BoundaryInfo(4, 4, 4) : + null; } default -> null; }; diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Building.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Building.java index a5e92e3..029af36 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/layers/Building.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Building.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Housenumber.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Housenumber.java index c10ae0f..b25823b 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/layers/Housenumber.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Housenumber.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Landcover.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Landcover.java index 629c9a1..e01068e 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/layers/Landcover.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Landcover.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -152,14 +152,14 @@ public class Landcover implements long numPoints = num.longValue(); if (zoom >= 10) { if (WOOD_OR_FOREST.contains(subclass) && numPoints < 300) { - attrs.put(tempGroupKey, numPoints < 50 ? "<50" : "<300"); + attrs.put(tempGroupKey, "<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"); + attrs.put(tempGroupKey, numPoints < 300 ? "<300" : ">300"); toMerge.add(item); } else { // don't merge result.add(item); diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Landuse.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Landuse.java index 1125571..f69722a 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/layers/Landuse.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Landuse.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -101,6 +101,9 @@ public class Landuse implements nullIfEmpty(element.waterway()) ); if (clazz != null) { + if ("grave_yard".equals(clazz)) { + clazz = FieldValues.CLASS_CEMETERY; + } features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE) .setAttr(Fields.CLASS, clazz) .setMinPixelSizeOverrides(MIN_PIXEL_SIZE_THRESHOLDS) diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/MountainPeak.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/MountainPeak.java index f12b7ff..6786912 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/layers/MountainPeak.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/MountainPeak.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -48,12 +48,18 @@ 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.reader.SourceFeature; 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.concurrent.atomic.AtomicBoolean; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.Point; +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 mountain peak label points in the {@code mountain_peak} layer from @@ -63,8 +69,10 @@ import org.locationtech.jts.geom.Point; * mountain_peak sql files. */ public class MountainPeak implements + BasemapProfile.NaturalEarthProcessor, OpenMapTilesSchema.MountainPeak, Tables.OsmPeakPoint.Handler, + Tables.OsmMountainLinestring.Handler, BasemapProfile.FeaturePostProcessor { /* @@ -73,20 +81,40 @@ public class MountainPeak implements * label density by only taking the top 5 most important mountain peaks within each 100x100px * square. */ + private static final Logger LOGGER = LoggerFactory.getLogger(TransportationName.class); private final Translations translations; private final Stats stats; + // keep track of areas that prefer feet to meters to set customary_ft=1 (just U.S.) + private PreparedGeometry unitedStates = null; + private final AtomicBoolean loggedNoUS = new AtomicBoolean(false); public MountainPeak(Translations translations, PlanetilerConfig config, Stats stats) { this.translations = translations; this.stats = stats; } + @Override + public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) { + if ("ne_10m_admin_0_countries".equals(table) && feature.hasTag("iso_a2", "US")) { + // multiple threads call this method concurrently, US polygon *should* only be found + // once, but just to be safe synchronize updates to that field + synchronized (this) { + try { + Geometry boundary = feature.polygon(); + unitedStates = PreparedGeometryFactory.prepare(boundary); + } catch (GeometryException e) { + LOGGER.error("Failed to get United States Polygon for mountain_peak layer: " + e); + } + } + } + } + @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) + var feature = features.point(LAYER_NAME) .setAttr(Fields.CLASS, element.source().getTag("natural")) .putAttrs(LanguageUtils.getNames(element.source().tags(), translations)) .putAttrs(elevationTags(meters)) @@ -101,9 +129,46 @@ public class MountainPeak implements // in adjacent tiles. postProcess() will remove anything outside the desired buffer. .setBufferPixels(100) .setPointLabelGridSizeAndLimit(13, 100, 5); + + if (peakInAreaUsingFeet(element)) { + feature.setAttr(Fields.CUSTOMARY_FT, 1); + } } } + @Override + public void process(Tables.OsmMountainLinestring element, FeatureCollector features) { + // TODO rank is approximate to sort important/named ridges before others, should switch to labelgrid for linestrings later + int rank = 3 - + (nullIfEmpty(element.wikipedia()) != null ? 1 : 0) - + (nullIfEmpty(element.name()) != null ? 1 : 0); + features.line(LAYER_NAME) + .setAttr(Fields.CLASS, element.source().getTag("natural")) + .setAttr(Fields.RANK, rank) + .putAttrs(LanguageUtils.getNames(element.source().tags(), translations)) + .setSortKey(rank) + .setMinZoom(13) + .setBufferPixels(100); + } + + /** Returns true if {@code element} is a point in an area where feet are used insead of meters (the US). */ + private boolean peakInAreaUsingFeet(Tables.OsmPeakPoint element) { + if (unitedStates == null) { + if (!loggedNoUS.get() && loggedNoUS.compareAndSet(false, true)) { + LOGGER.warn("No US polygon for inferring mountain_peak customary_ft tag"); + } + } else { + try { + Geometry wayGeometry = element.source().worldGeometry(); + return unitedStates.intersects(wayGeometry); + } catch (GeometryException e) { + e.log(stats, "omt_mountain_peak_us_test", + "Unable to test mountain_peak against US polygon: " + element.source().id()); + } + } + return false; + } + @Override public List postProcess(int zoom, List items) { LongIntMap groupCounts = new LongIntHashMap(); diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Park.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Park.java index 1892006..40ad466 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/layers/Park.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Park.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -42,6 +42,7 @@ 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.FeatureMerge; import com.onthegomap.planetiler.VectorTile; import com.onthegomap.planetiler.basemap.BasemapProfile; import com.onthegomap.planetiler.basemap.generated.OpenMapTilesSchema; @@ -101,10 +102,10 @@ public class Park implements ); // park shape - features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE) - .setAttr(Fields.CLASS, clazz) + var outline = features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE) + .setAttrWithMinzoom(Fields.CLASS, clazz, 5) .setMinPixelSize(2) - .setMinZoom(6); + .setMinZoom(4); // park name label point (if it has one) if (element.name() != null) { @@ -112,8 +113,13 @@ public class Park implements double area = element.source().area(); int minzoom = getMinZoomForArea(area); - features.centroid(LAYER_NAME).setBufferPixels(256) + var names = LanguageUtils.getNamesWithoutTranslations(element.source().tags()); + + outline.putAttrsWithMinzoom(names, 5); + + features.pointOnSurface(LAYER_NAME).setBufferPixels(256) .setAttr(Fields.CLASS, clazz) + .putAttrs(names) .putAttrs(LanguageUtils.getNames(element.source().tags(), translations)) .setPointLabelGridPixelSize(14, 100) .setSortKey(SortKey @@ -132,12 +138,12 @@ public class Park implements // 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)); + minzoom = Math.min(14, Math.max(5, minzoom)); return minzoom; } @Override - public List postProcess(int zoom, List items) { + public List postProcess(int zoom, List items) throws GeometryException { // infer the "rank" attribute from point ordering within each label grid square LongIntMap counts = new LongIntHashMap(); for (VectorTile.Feature feature : items) { @@ -147,6 +153,9 @@ public class Park implements counts.put(feature.group(), count); } } + if (zoom <= 4) { + items = FeatureMerge.mergeOverlappingPolygons(items, 0); + } return items; } } diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Place.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Place.java index 753c723..4efe834 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/layers/Place.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Place.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -171,7 +171,7 @@ public class Place implements 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) { + if (scalerank != null && scalerank <= 6 && labelrank != null && labelrank <= 7) { states.put(feature.worldGeometry(), new NaturalEarthRegion( feature.getString("name"), 6, scalerank, @@ -265,7 +265,7 @@ public class Place implements features.point(LAYER_NAME).setBufferPixels(BUFFER_SIZE) .putAttrs(names) - .setAttr(Fields.CLASS, FieldValues.CLASS_STATE) + .setAttr(Fields.CLASS, element.place()) .setAttr(Fields.RANK, rank) .setMinZoom(2) .setSortKey(rank); diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Poi.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Poi.java index 40f1425..46f73a2 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/layers/Poi.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Poi.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -36,8 +36,8 @@ See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for deta 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.nullIfLong; import static com.onthegomap.planetiler.basemap.util.Utils.nullOrEmpty; import static java.util.Map.entry; @@ -170,7 +170,7 @@ public class Poi implements output.setBufferPixels(BUFFER_SIZE) .setAttr(Fields.CLASS, poiClass) .setAttr(Fields.SUBCLASS, subclass) - .setAttr(Fields.LAYER, nullIf(element.layer(), 0)) + .setAttr(Fields.LAYER, nullIfLong(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)) diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Transportation.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Transportation.java index 69f231f..3f116c1 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/layers/Transportation.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Transportation.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -36,6 +36,10 @@ See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for deta package com.onthegomap.planetiler.basemap.layers; import static com.onthegomap.planetiler.basemap.util.Utils.*; +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.Map.entry; import com.onthegomap.planetiler.FeatureCollector; import com.onthegomap.planetiler.FeatureMerge; @@ -45,15 +49,32 @@ 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.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.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineString; +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 roads, shipways, railroads, and paths in the {@code transportation} @@ -69,7 +90,9 @@ public class Transportation implements Tables.OsmRailwayLinestring.Handler, Tables.OsmShipwayLinestring.Handler, Tables.OsmHighwayPolygon.Handler, + BasemapProfile.NaturalEarthProcessor, BasemapProfile.FeaturePostProcessor, + BasemapProfile.OsmRelationPreprocessor, BasemapProfile.IgnoreWikidata { /* @@ -78,6 +101,8 @@ public class Transportation implements * layer includes names, but less detailed attributes. */ + private static final Logger LOGGER = LoggerFactory.getLogger(Transportation.class); + private static final Pattern GREAT_BRITAIN_REF_NETWORK_PATTERN = Pattern.compile("^[AM][0-9AM()]+"); private static final MultiExpression.Index classMapping = FieldMappings.Class.index(); private static final Set RAILWAY_RAIL_VALUES = Set.of( FieldValues.SUBCLASS_RAIL, @@ -108,11 +133,24 @@ public class Transportation implements "paved", "asphalt", "cobblestone", "concrete", "concrete:lanes", "concrete:plates", "metal", "paving_stones", "sett", "unhewn_cobblestone", "wood" ); + private static final Set ACCESS_NO_VALUES = Set.of( + "private", "no" + ); private static final ZoomFunction.MeterToPixelThresholds MIN_LENGTH = ZoomFunction.meterThresholds() .put(7, 50) .put(6, 100) .put(5, 500) .put(4, 1_000); + // ORDER BY network_type, network, LENGTH(ref), ref) + private static final Comparator RELATION_ORDERING = Comparator + .comparingInt( + r -> r.networkType() != null ? r.networkType.ordinal() : Integer.MAX_VALUE) + .thenComparing(routeRelation -> coalesce(routeRelation.network(), "")) + .thenComparingInt(r -> r.ref().length()) + .thenComparing(RouteRelation::ref); + private final AtomicBoolean loggedNoGb = new AtomicBoolean(false); + private final boolean z13Paths; + private PreparedGeometry greatBritain = null; private final Map MINZOOMS; private final Stats stats; private final PlanetilerConfig config; @@ -120,30 +158,54 @@ public class Transportation implements public Transportation(Translations translations, PlanetilerConfig config, Stats stats) { this.config = config; this.stats = stats; - boolean z13Paths = config.arguments().getBoolean( + z13Paths = config.arguments().getBoolean( "transportation_z13_paths", - "transportation(_name) layer: show paths on z13", - true + "transportation(_name) layer: show all paths on z13", + false ); - 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 + MINZOOMS = Map.ofEntries( + entry(FieldValues.CLASS_PATH, z13Paths ? 13 : 14), + entry(FieldValues.CLASS_TRACK, 14), + entry(FieldValues.CLASS_SERVICE, 13), + entry(FieldValues.CLASS_MINOR, 13), + entry(FieldValues.CLASS_RACEWAY, 12), + entry(FieldValues.CLASS_TERTIARY, 11), + entry(FieldValues.CLASS_BUSWAY, 11), + entry(FieldValues.CLASS_SECONDARY, 9), + entry(FieldValues.CLASS_PRIMARY, 7), + entry(FieldValues.CLASS_TRUNK, 5), + entry(FieldValues.CLASS_MOTORWAY, 4) ); } + @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); + } + } + } + } + /** 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 access} tag constrained to a small set of known values from raw OSM data. */ + private static String access(String value) { + return value == null ? null : ACCESS_NO_VALUES.contains(value) ? "no" : 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; @@ -171,68 +233,160 @@ public class Transportation implements return "footway".equals(highway) || "steps".equals(highway); } + static boolean isLink(String highway) { + return highway != null && highway.endsWith("_link"); + } + private static boolean isResidentialOrUnclassified(String highway) { return "residential".equals(highway) || "unclassified".equals(highway); } + private static boolean isDrivewayOrParkingAisle(String service) { + return FieldValues.SERVICE_PARKING_AISLE.equals(service) || FieldValues.SERVICE_DRIVEWAY.equals(service); + } + private static boolean isBridgeOrPier(String manMade) { return "bridge".equals(manMade) || "pier".equals(manMade); } + 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; + } + } + + @Override + public List preprocessOsmRelation(OsmElement.Relation relation) { + if (relation.hasTag("route", "road", "hiking")) { + 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; + } + + int rank = switch (coalesce(network, "")) { + case "iwn", "nwn", "rwn" -> 1; + case "lwn" -> 2; + default -> (relation.hasTag("osmc:symbol") || relation.hasTag("colour")) ? 2 : 3; + }; + + if (networkType != null || rank < 3) { + return List.of(new RouteRelation(coalesce(ref, ""), network, networkType, (byte) rank, relation.id())); + } + } + return null; + } + + List getRouteRelations(Tables.OsmHighwayLinestring element) { + String ref = element.ref(); + List> relations = element.source().relationInfo(RouteRelation.class); + List result = new ArrayList<>(relations.size() + 1); + for (var relationMember : relations) { + var relation = relationMember.relation(); + // avoid duplicates - list should be very small and usually only one + if (!result.contains(relation)) { + result.add(relation); + } + } + if (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)) { + Transportation.RouteNetwork networkType = + "motorway".equals(element.highway()) ? Transportation.RouteNetwork.GB_MOTORWAY + : Transportation.RouteNetwork.GB_TRUNK; + String network = "motorway".equals(element.highway()) ? "omt-gb-motorway" : "omt-gb-trunk"; + result.add(new RouteRelation(refMatcher.group(), network, networkType, (byte) -1, + 0)); + } + } catch (GeometryException e) { + e.log(stats, "omt_transportation_name_gb_test", + "Unable to test highway against GB route network: " + element.source().id()); + } + } + } + } + Collections.sort(result); + return result; + } + + RouteRelation getRouteRelation(Tables.OsmHighwayLinestring element) { + List all = getRouteRelations(element); + return all.isEmpty() ? null : all.get(0); + } + @Override public void process(Tables.OsmHighwayLinestring element, FeatureCollector features) { if (element.isArea()) { return; } + RouteRelation routeRelation = getRouteRelation(element); + RouteNetwork networkType = routeRelation != null ? routeRelation.networkType : null; + String highway = element.highway(); String highwayClass = highwayClass(element.highway(), element.publicTransport(), element.construction(), element.manMade()); + String service = service(element.service()); 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); + if (isPierPolygon(element)) { + return; } - boolean highwayIsLink = coalesce(highway, "").endsWith("_link"); + int minzoom = getMinzoom(element, highwayClass); - 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; + boolean highwayRamp = isLink(highway); + Integer rampAboveZ12 = (highwayRamp || element.isRamp()) ? 1 : null; + Integer rampBelowZ12 = highwayRamp ? 1 : null; FeatureCollector.Feature feature = features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE) // main attributes at all zoom levels (used for grouping <= z8) .setAttr(Fields.CLASS, highwayClass) .setAttr(Fields.SUBCLASS, highwaySubclass(highwayClass, element.publicTransport(), highway)) .setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord())) - // rest at z9+ - .setAttrWithMinzoom(Fields.SERVICE, service(element.service()), 12) - .setAttrWithMinzoom(Fields.ONEWAY, element.isOneway(), 12) - .setAttr(Fields.RAMP, minzoom >= 12 ? rampAboveZ12 : - ((ZoomFunction) z -> z < 9 ? null : z >= 12 ? rampAboveZ12 : rampBelowZ12)) - .setAttrWithMinzoom(Fields.LAYER, nullIf(element.layer(), 0), 9) + .setAttr(Fields.NETWORK, networkType != null ? networkType.name : null) + // z8+ + .setAttrWithMinzoom(Fields.EXPRESSWAY, element.expressway() && !"motorway".equals(highway) ? 1 : null, 8) + // z9+ + .setAttrWithMinzoom(Fields.LAYER, nullIfLong(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.ACCESS, access(element.access()), 9) + .setAttrWithMinzoom(Fields.TOLL, element.toll() ? 1 : null, 9) + // sometimes z9+, sometimes z12+ + .setAttr(Fields.RAMP, minzoom >= 12 ? rampAboveZ12 : + ((ZoomFunction) z -> z < 9 ? null : z >= 12 ? rampAboveZ12 : rampBelowZ12)) + // z12+ + .setAttrWithMinzoom(Fields.SERVICE, service, 12) + .setAttrWithMinzoom(Fields.ONEWAY, nullIfInt(element.isOneway(), 0), 12) .setAttrWithMinzoom(Fields.SURFACE, surface(element.surface()), 12) .setMinPixelSize(0) // merge during post-processing, then limit by size .setSortKey(element.zOrder()) @@ -246,6 +400,53 @@ public class Transportation implements } } + int getMinzoom(Tables.OsmHighwayLinestring element, String highwayClass) { + List routeRelations = getRouteRelations(element); + int routeRank = 3; + for (var rel : routeRelations) { + if (rel.intRank() < routeRank) { + routeRank = rel.intRank(); + } + } + String highway = element.highway(); + + int minzoom; + if ("pier".equals(element.manMade())) { + minzoom = 13; + } else if (isResidentialOrUnclassified(highway)) { + minzoom = 12; + } else { + String baseClass = highwayClass.replace("_construction", ""); + minzoom = switch (baseClass) { + case FieldValues.CLASS_SERVICE -> isDrivewayOrParkingAisle(service(element.service())) ? 14 : 13; + case FieldValues.CLASS_TRACK, FieldValues.CLASS_PATH -> routeRank == 1 ? 12 : + (z13Paths || !nullOrEmpty(element.name()) || routeRank <= 2 || !nullOrEmpty(element.sacScale())) ? 13 : 14; + default -> MINZOOMS.get(baseClass); + }; + } + + if (isLink(highway)) { + minzoom = Math.max(minzoom, 9); + } + return minzoom; + } + + private boolean isPierPolygon(Tables.OsmHighwayLinestring element) { + if ("pier".equals(element.manMade())) { + try { + if (element.source().worldGeometry() instanceof LineString lineString && lineString.isClosed()) { + // ignore this because it's a polygon + return true; + } + } catch (GeometryException e) { + e.log(stats, "omt_transportation_pier", + "Unable to decode pier geometry for " + element.source().id()); + return true; + } + } + return false; + } + @Override public void process(Tables.OsmRailwayLinestring element, FeatureCollector features) { String railway = element.railway(); @@ -268,10 +469,10 @@ public class Transportation implements .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) + .setAttr(Fields.ONEWAY, nullIfInt(element.isOneway(), 0)) + .setAttr(Fields.RAMP, element.isRamp() ? 1L : null) .setAttrWithMinzoom(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()), 10) - .setAttrWithMinzoom(Fields.LAYER, nullIf(element.layer(), 0), 9) + .setAttrWithMinzoom(Fields.LAYER, nullIfLong(element.layer(), 0), 9) .setSortKey(element.zOrder()) .setMinPixelSize(0) // merge during post-processing, then limit by size .setMinZoom(minzoom); @@ -284,10 +485,10 @@ public class Transportation implements .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.ONEWAY, nullIfInt(element.isOneway(), 0)) + .setAttr(Fields.RAMP, element.isRamp() ? 1L : null) .setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord())) - .setAttr(Fields.LAYER, nullIf(element.layer(), 0)) + .setAttr(Fields.LAYER, nullIfLong(element.layer(), 0)) .setSortKey(element.zOrder()) .setMinPixelSize(0) // merge during post-processing, then limit by size .setMinZoom(12); @@ -299,10 +500,10 @@ public class Transportation implements .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.ONEWAY, nullIfInt(element.isOneway(), 0)) + .setAttr(Fields.RAMP, element.isRamp() ? 1L : null) .setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord())) - .setAttr(Fields.LAYER, nullIf(element.layer(), 0)) + .setAttr(Fields.LAYER, nullIfLong(element.layer(), 0)) .setSortKey(element.zOrder()) .setMinPixelSize(0) // merge during post-processing, then limit by size .setMinZoom(11); @@ -320,7 +521,7 @@ public class Transportation implements .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)) + .setAttr(Fields.LAYER, nullIfLong(element.layer(), 0)) .setSortKey(element.zOrder()) .setMinZoom(13); } @@ -330,7 +531,37 @@ public class Transportation implements @Override public List postProcess(int zoom, List items) { double tolerance = config.tolerance(zoom); - double minLength = coalesce(MIN_LENGTH.apply(zoom), config.minFeatureSize(zoom)).doubleValue(); + double minLength = coalesce(MIN_LENGTH.apply(zoom), 0).doubleValue(); + // TODO preserve direction for one-way? return FeatureMerge.mergeLineStrings(items, minLength, tolerance, BUFFER_SIZE); } + + /** Information extracted from route relations to use when processing ways in that relation. */ + record RouteRelation( + String ref, + String network, + RouteNetwork networkType, + byte rank, + @Override long id + ) implements OsmRelationInfo, Comparable { + + @Override + public long estimateMemoryUsageBytes() { + return CLASS_HEADER_BYTES + + MemoryEstimator.estimateSize(rank) + + POINTER_BYTES + estimateSize(ref) + + POINTER_BYTES + estimateSize(network) + + POINTER_BYTES + // networkType + MemoryEstimator.estimateSizeLong(id); + } + + public int intRank() { + return rank; + } + + @Override + public int compareTo(RouteRelation o) { + return RELATION_ORDERING.compare(this, o); + } + } } diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/TransportationName.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/TransportationName.java index e36b195..0b25092 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/layers/TransportationName.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/TransportationName.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -38,45 +38,30 @@ 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 static com.onthegomap.planetiler.basemap.util.Utils.*; +import com.carrotsearch.hppc.LongArrayList; +import com.carrotsearch.hppc.LongByteHashMap; +import com.carrotsearch.hppc.LongByteMap; import com.onthegomap.planetiler.FeatureCollector; import com.onthegomap.planetiler.FeatureMerge; +import com.onthegomap.planetiler.ForwardingProfile; 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.Arrays; +import java.util.HashMap; 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 @@ -87,14 +72,17 @@ import org.slf4j.LoggerFactory; */ public class TransportationName implements OpenMapTilesSchema.TransportationName, + Tables.OsmHighwayPoint.Handler, Tables.OsmHighwayLinestring.Handler, - BasemapProfile.NaturalEarthProcessor, + Tables.OsmAerialwayLinestring.Handler, + Tables.OsmShipwayLinestring.Handler, BasemapProfile.FeaturePostProcessor, - BasemapProfile.OsmRelationPreprocessor, - BasemapProfile.IgnoreWikidata { + BasemapProfile.IgnoreWikidata, + ForwardingProfile.OsmNodePreprocessor, + ForwardingProfile.OsmWayPreprocessor { /* - * Generate road names from OSM data. Route network and ref are copied + * Generate road names from OSM data. Route networkType and ref are copied * from relations that roads are a part of - except in Great Britain which * uses a naming convention instead of relations. * @@ -113,8 +101,6 @@ public class TransportationName implements 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) @@ -122,23 +108,23 @@ public class TransportationName implements .put(9, 8_000) .put(10, 8_000) .put(11, 8_000); - private static final Comparator RELATION_ORDERING = Comparator - .comparingInt(r -> r.network.ordinal()) - // TODO also compare network string? - .thenComparingInt(r -> r.ref.length()) - .thenComparing(RouteRelation::ref); - private final Map MINZOOMS; + private static final List CONCURRENT_ROUTE_KEYS = List.of( + Fields.ROUTE_1, + Fields.ROUTE_2, + Fields.ROUTE_3, + Fields.ROUTE_4, + Fields.ROUTE_5, + Fields.ROUTE_6 + ); 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; + private Transportation transportation; + private final LongByteMap motorwayJunctionHighwayClasses = new LongByteHashMap(); 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", @@ -154,71 +140,72 @@ public class TransportationName implements "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 - ); + } + + public void needsTransportationLayer(Transportation transportation) { + this.transportation = transportation; + } + + + @Override + public void preprocessOsmNode(OsmElement.Node node) { + if (node.hasTag("highway", "motorway_junction")) { + motorwayJunctionHighwayClasses.put(node.id(), HighwayClass.UNKNOWN.value); + } } @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); + public void preprocessOsmWay(OsmElement.Way way) { + String highway = way.getString("highway"); + if (highway != null) { + HighwayClass cls = HighwayClass.from(highway); + if (cls != HighwayClass.UNKNOWN) { + LongArrayList nodes = way.nodes(); + for (int i = 0; i < nodes.size(); i++) { + long node = nodes.get(i); + if (motorwayJunctionHighwayClasses.containsKey(node)) { + byte oldValue = motorwayJunctionHighwayClasses.get(node); + byte newValue = cls.value; + if (newValue > oldValue) { + motorwayJunctionHighwayClasses.put(node, newValue); + } + } } } } } @Override - public List preprocessOsmRelation(OsmElement.Relation relation) { - if (relation.hasTag("route", "road")) { - RouteNetwork networkType = null; - String network = relation.getString("network"); - String ref = relation.getString("ref"); + public void process(Tables.OsmHighwayPoint element, FeatureCollector features) { + long id = element.source().id(); + byte value = motorwayJunctionHighwayClasses.getOrDefault(id, (byte) -1); + if (value > 0) { + HighwayClass cls = HighwayClass.from(value); + if (cls != HighwayClass.UNKNOWN) { + String subclass = FieldValues.SUBCLASS_JUNCTION; + String ref = element.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())); + features.point(LAYER_NAME) + .setBufferPixels(BUFFER_SIZE) + .putAttrs(LanguageUtils.getNamesWithoutTranslations(element.source().tags())) + .setAttr(Fields.REF, ref) + .setAttr(Fields.REF_LENGTH, ref != null ? ref.length() : null) + .setAttr(Fields.CLASS, highwayClass(cls.highwayValue, null, null, null)) + .setAttr(Fields.SUBCLASS, subclass) + .setAttr(Fields.LAYER, nullIfLong(element.layer(), 0)) + .setSortKeyDescending(element.zOrder()) + .setMinZoom(10); } } - return null; } @Override public void process(Tables.OsmHighwayLinestring element, FeatureCollector features) { - List> relations = element.source() - .relationInfo(RouteRelation.class); - String ref = element.ref(); - RouteRelation relation = getRouteRelation(element, relations, ref); - if (relation != null && nullIfEmpty(relation.ref) != null) { - ref = relation.ref; + List relations = transportation.getRouteRelations(element); + Transportation.RouteRelation relation = relations.isEmpty() ? null : relations.get(0); + if (relation != null && nullIfEmpty(relation.ref()) != null) { + ref = relation.ref(); } String name = nullIfEmpty(element.name()); @@ -230,13 +217,15 @@ public class TransportationName implements return; } + boolean isLink = Transportation.isLink(highway); String baseClass = highwayClass.replace("_construction", ""); - int minzoom = MINZOOMS.getOrDefault(baseClass, 12); - boolean isLink = highway.endsWith("_link"); - if (isLink) { - minzoom = Math.max(13, minzoom); - } + int minzoom = FieldValues.CLASS_TRUNK.equals(baseClass) ? 8 : + FieldValues.CLASS_MOTORWAY.equals(baseClass) ? 6 : + isLink ? 13 : 12; // fallback - get from line minzoom, but floor at 12 + + // inherit min zoom threshold from visible road, and ensure we never show a label on a road that's not visible yet. + minzoom = Math.max(minzoom, transportation.getMinzoom(element, highwayClass)); FeatureCollector.Feature feature = features.line(LAYER_NAME) .setBufferPixels(BUFFER_SIZE) @@ -246,13 +235,21 @@ public class TransportationName implements .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) + (relation != null && relation.networkType() != null) ? relation.networkType().name + : !nullOrEmpty(ref) ? "road" : null) .setAttr(Fields.CLASS, highwayClass) .setAttr(Fields.SUBCLASS, highwaySubclass(highwayClass, null, highway)) .setMinPixelSize(0) .setSortKey(element.zOrder()) .setMinZoom(minzoom); + // populate route_1, route_2, ... tags + for (int i = 0; i < Math.min(CONCURRENT_ROUTE_KEYS.size(), relations.size()); i++) { + Transportation.RouteRelation routeRelation = relations.get(i); + feature.setAttr(CONCURRENT_ROUTE_KEYS.get(i), routeRelation.network() == null ? null : + routeRelation.network() + "=" + coalesce(routeRelation.ref(), "")); + } + if (brunnel) { feature.setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord())); } @@ -265,48 +262,44 @@ public class TransportationName implements if (limitMerge) { feature .setAttr(LINK_TEMP_KEY, isLink ? 1 : 0) - .setAttr(RELATION_ID_TEMP_KEY, relation == null ? null : relation.id); + .setAttr(RELATION_ID_TEMP_KEY, relation == null ? null : relation.id()); } if (isFootwayOrSteps(highway)) { feature - .setAttrWithMinzoom(Fields.LAYER, nullIf(element.layer(), 0), 12) + .setAttrWithMinzoom(Fields.LAYER, nullIfLong(element.layer(), 0), 12) .setAttrWithMinzoom(Fields.LEVEL, Parse.parseLongOrNull(element.source().getTag("level")), 12) .setAttrWithMinzoom(Fields.INDOOR, element.indoor() ? 1 : null, 12); } } - private RouteRelation getRouteRelation(Tables.OsmHighwayLinestring element, - List> relations, String ref) { - RouteRelation relation = relations.stream() - .map(OsmReader.RelationMember::relation) - .min(RELATION_ORDERING) - .orElse(null); - if (relation == null && ref != null) { - // GB doesn't use regular relations like everywhere else, so if we are - // in GB then use a naming convention instead. - Matcher refMatcher = GREAT_BRITAIN_REF_NETWORK_PATTERN.matcher(ref); - if (refMatcher.find()) { - if (greatBritain == null) { - if (!loggedNoGb.get() && loggedNoGb.compareAndSet(false, true)) { - LOGGER.warn("No GB polygon for inferring route network types"); - } - } else { - try { - Geometry wayGeometry = element.source().worldGeometry(); - if (greatBritain.intersects(wayGeometry)) { - RouteNetwork networkType = - "motorway".equals(element.highway()) ? RouteNetwork.GB_MOTORWAY : RouteNetwork.GB_TRUNK; - relation = new RouteRelation(refMatcher.group(), networkType, 0); - } - } catch (GeometryException e) { - e.log(stats, "omt_transportation_name_gb_test", - "Unable to test highway against GB route network: " + element.source().id()); - } - } - } + @Override + public void process(Tables.OsmAerialwayLinestring element, FeatureCollector features) { + if (!nullOrEmpty(element.name())) { + features.line(LAYER_NAME) + .setBufferPixels(BUFFER_SIZE) + .setBufferPixelOverrides(MIN_LENGTH) + .putAttrs(LanguageUtils.getNamesWithoutTranslations(element.source().tags())) + .setAttr(Fields.CLASS, "aerialway") + .setAttr(Fields.SUBCLASS, element.aerialway()) + .setMinPixelSize(0) + .setSortKey(element.zOrder()) + .setMinZoom(12); + } + } + + @Override + public void process(Tables.OsmShipwayLinestring element, FeatureCollector features) { + if (!nullOrEmpty(element.name())) { + features.line(LAYER_NAME) + .setBufferPixels(BUFFER_SIZE) + .setBufferPixelOverrides(MIN_LENGTH) + .putAttrs(LanguageUtils.getNamesWithoutTranslations(element.source().tags())) + .setAttr(Fields.CLASS, element.shipway()) + .setMinPixelSize(0) + .setSortKey(element.zOrder()) + .setMinZoom(12); } - return relation; } @Override @@ -341,35 +334,38 @@ public class TransportationName implements name instanceof String str ? str.length() * 6 : Double.MAX_VALUE; } - private enum RouteNetwork { + private enum HighwayClass { + MOTORWAY("motorway", 6), + TRUNK("trunk", 5), + PRIMARY("primary", 4), + SECONDARY("secondary", 3), + TERTIARY("tertiary", 2), + UNCLASSIFIED("unclassified", 1), + UNKNOWN("", 0); - US_INTERSTATE("us-interstate"), - US_HIGHWAY("us-highway"), - US_STATE("us-state"), - CA_TRANSCANADA("ca-transcanada"), - GB_MOTORWAY("gb-motorway"), - GB_TRUNK("gb-trunk"); + private static final Map indexByString = new HashMap<>(); + private static final Map indexByByte = new HashMap<>(); + final byte value; + final String highwayValue; - final String name; - - RouteNetwork(String name) { - this.name = name; + HighwayClass(String highwayValue, int id) { + this.highwayValue = highwayValue; + this.value = (byte) id; } - } - /** 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 { + static { + Arrays.stream(values()).forEach(cls -> { + indexByString.put(cls.highwayValue, cls); + indexByByte.put(cls.value, cls); + }); + } - @Override - public long estimateMemoryUsageBytes() { - return CLASS_HEADER_BYTES + - POINTER_BYTES + estimateSize(ref) + - POINTER_BYTES + // network - MemoryEstimator.estimateSizeLong(id); + static HighwayClass from(String highway) { + return indexByString.getOrDefault(highway, UNKNOWN); + } + + static HighwayClass from(byte value) { + return indexByByte.getOrDefault(value, UNKNOWN); } } } diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Water.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Water.java index b9ce7f5..cc9f061 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/layers/Water.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Water.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -103,13 +103,15 @@ public class Water implements @Override public void process(Tables.OsmWaterPolygon element, FeatureCollector features) { if (!"bay".equals(element.natural())) { + String clazz = "riverbank".equals(element.waterway()) ? FieldValues.CLASS_RIVER : + classMapping.getOrElse(element.source(), FieldValues.CLASS_LAKE); 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)); + .setAttr(Fields.CLASS, clazz); } } } diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/WaterName.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/WaterName.java index a29fe8d..fbe29fa 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/layers/WaterName.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/WaterName.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License diff --git a/src/main/java/com/onthegomap/planetiler/basemap/layers/Waterway.java b/src/main/java/com/onthegomap/planetiler/basemap/layers/Waterway.java index 99756d9..ce1928e 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/layers/Waterway.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/layers/Waterway.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License @@ -37,6 +37,9 @@ package com.onthegomap.planetiler.basemap.layers; import static com.onthegomap.planetiler.basemap.util.Utils.nullIfEmpty; +import com.carrotsearch.hppc.LongObjectHashMap; +import com.google.common.util.concurrent.AtomicDouble; +import com.graphhopper.coll.GHLongObjectHashMap; import com.onthegomap.planetiler.FeatureCollector; import com.onthegomap.planetiler.FeatureMerge; import com.onthegomap.planetiler.VectorTile; @@ -46,7 +49,11 @@ 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.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.Translations; import com.onthegomap.planetiler.util.ZoomFunction; @@ -63,7 +70,9 @@ public class Waterway implements OpenMapTilesSchema.Waterway, Tables.OsmWaterwayLinestring.Handler, BasemapProfile.FeaturePostProcessor, - BasemapProfile.NaturalEarthProcessor { + BasemapProfile.NaturalEarthProcessor, + BasemapProfile.OsmRelationPreprocessor, + BasemapProfile.OsmAllProcessor { /* * Uses Natural Earth at lower zoom-levels and OpenStreetMap at higher zoom levels. @@ -76,14 +85,6 @@ public class Waterway implements * short segment of it goes through this tile. */ - private final Translations translations; - private final PlanetilerConfig config; - - public Waterway(Translations translations, PlanetilerConfig config, Stats stats) { - this.config = config; - this.translations = translations; - } - private static final Map CLASS_MINZOOM = Map.of( "river", 12, "canal", 12, @@ -92,12 +93,29 @@ public class Waterway implements "drain", 13, "ditch", 13 ); + private static final String TEMP_REL_ID_ADDR = "_relid"; + + private final Translations translations; + private final PlanetilerConfig config; + private final Stats stats; + private final LongObjectHashMap riverRelationLengths = new GHLongObjectHashMap<>(); + + public Waterway(Translations translations, PlanetilerConfig config, Stats stats) { + this.config = config; + this.translations = translations; + this.stats = stats; + } private static final ZoomFunction.MeterToPixelThresholds MIN_PIXEL_LENGTHS = ZoomFunction.meterThresholds() + .put(6, 500_000) + .put(7, 400_000) + .put(8, 300_000) .put(9, 8_000) .put(10, 4_000) .put(11, 1_000); + // zoom-level 3-5 come from natural earth + @Override public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) { if (feature.hasTag("featurecla", "River")) { @@ -105,7 +123,6 @@ public class Waterway implements 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) { @@ -117,6 +134,52 @@ public class Waterway implements } } + // zoom-level 6-8 come from OSM river relations + + private record WaterwayRelation( + long id, + Map names + ) implements OsmRelationInfo {} + + @Override + public List preprocessOsmRelation(OsmElement.Relation relation) { + if (relation.hasTag("waterway", "river") && !Utils.nullOrEmpty(relation.getString("name"))) { + riverRelationLengths.put(relation.id(), new AtomicDouble()); + return List.of(new WaterwayRelation(relation.id(), LanguageUtils.getNames(relation.tags(), translations))); + } + return null; + } + + @Override + public void processAllOsm(SourceFeature feature, FeatureCollector features) { + List> waterways = feature.relationInfo(WaterwayRelation.class); + if (waterways != null && !waterways.isEmpty() && feature.canBeLine()) { + for (var waterway : waterways) { + String role = waterway.role(); + if (Utils.nullOrEmpty(role) || "main_stream".equals(role)) { + long relId = waterway.relation().id(); + try { + AtomicDouble counter = riverRelationLengths.get(relId); + if (counter != null) { + counter.addAndGet(feature.length()); + } + } catch (GeometryException e) { + e.log(stats, "waterway_decode", "Unable to get waterway length for " + feature.id()); + } + features.line(LAYER_NAME) + .setAttr(TEMP_REL_ID_ADDR, relId) + .setBufferPixels(BUFFER_SIZE) + .setAttr(Fields.CLASS, FieldValues.CLASS_RIVER) + .putAttrs(waterway.relation().names()) + .setZoomRange(6, 8) + .setMinPixelSize(0); + } + } + } + } + + // zoom-level 9+ come from OSM river ways + @Override public void process(Tables.OsmWaterwayLinestring element, FeatureCollector features) { String waterway = element.waterway(); @@ -137,7 +200,22 @@ public class Waterway implements @Override public List postProcess(int zoom, List items) { - if (zoom >= 9 && zoom <= 11) { + if (zoom >= 6 && zoom <= 8) { + // remove ways for river relations if relation is not long enough + double minSizeAtZoom = MIN_PIXEL_LENGTHS.apply(zoom).doubleValue() / Math.pow(2, zoom) / 256d; + for (int i = 0; i < items.size(); i++) { + Object relIdObj = items.get(i).attrs().remove(TEMP_REL_ID_ADDR); + if (relIdObj instanceof Long relId && riverRelationLengths.get(relId).get() < minSizeAtZoom) { + items.set(i, null); + } + } + return FeatureMerge.mergeLineStrings( + items, + config.minFeatureSize(zoom), + config.tolerance(zoom), + BUFFER_SIZE + ); + } else if (zoom >= 9 && zoom <= 11) { return FeatureMerge.mergeLineStrings( items, MIN_PIXEL_LENGTHS.apply(zoom).doubleValue(), diff --git a/src/main/java/com/onthegomap/planetiler/basemap/util/LanguageUtils.java b/src/main/java/com/onthegomap/planetiler/basemap/util/LanguageUtils.java index c2ff26d..269ad45 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/util/LanguageUtils.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/util/LanguageUtils.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, KlokanTech.com & OpenMapTiles contributors. +Copyright (c) 2021, MapTiler.com & OpenMapTiles contributors. All rights reserved. Code license: BSD 3-Clause License diff --git a/src/main/java/com/onthegomap/planetiler/basemap/util/Utils.java b/src/main/java/com/onthegomap/planetiler/basemap/util/Utils.java index 0a603f0..7c88695 100644 --- a/src/main/java/com/onthegomap/planetiler/basemap/util/Utils.java +++ b/src/main/java/com/onthegomap/planetiler/basemap/util/Utils.java @@ -28,9 +28,14 @@ public class Utils { return a != null ? a : b != null ? b : c != null ? c : d != null ? d : e != null ? e : f; } - /** Returns {@code a} or {@code nullValue} if {@code a} is null. */ - public static T nullIf(T a, T nullValue) { - return nullValue.equals(a) ? null : a; + /** Boxes {@code a} into an {@link Integer}, or {@code null} if {@code a} is {@code nullValue}. */ + public static Long nullIfLong(long a, long nullValue) { + return a == nullValue ? null : a; + } + + /** Boxes {@code a} into a {@link Long}, or {@code null} if {@code a} is {@code nullValue}. */ + public static Integer nullIfInt(int a, int nullValue) { + return a == nullValue ? null : a; } /** Returns {@code a}, or null if {@code a} is "". */ diff --git a/src/test/java/com/onthegomap/planetiler/basemap/GenerateTest.java b/src/test/java/com/onthegomap/planetiler/basemap/GenerateTest.java index ec92b11..d894fd0 100644 --- a/src/test/java/com/onthegomap/planetiler/basemap/GenerateTest.java +++ b/src/test/java/com/onthegomap/planetiler/basemap/GenerateTest.java @@ -211,7 +211,8 @@ public class GenerateTest { List.of(), null, null, - props + props, + List.of() )); assertEquals(or( and( diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/AbstractLayerTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/AbstractLayerTest.java index 084abce..d40a902 100644 --- a/src/test/java/com/onthegomap/planetiler/basemap/layers/AbstractLayerTest.java +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/AbstractLayerTest.java @@ -211,4 +211,15 @@ public abstract class AbstractLayerTest { profile.postProcessLayerFeatures(layer, zoom, List.of(line1, line2)) ); } + + public static Map mapOf(Object... args) { + assert args.length % 2 == 0; + Map result = new HashMap<>(); + for (int i = 0; i < args.length; i += 2) { + String key = args[i].toString(); + Object value = args[i + 1]; + result.put(key, value == null ? "" : value); + } + return result; + } } diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/AerodromeLabelTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/AerodromeLabelTest.java index e73de7b..529af58 100644 --- a/src/test/java/com/onthegomap/planetiler/basemap/layers/AerodromeLabelTest.java +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/AerodromeLabelTest.java @@ -13,7 +13,7 @@ public class AerodromeLabelTest extends AbstractLayerTest { } @Test - public void testHappyPathPoint() { + public void testIntlWithIata() { assertFeatures(14, List.of(Map.of( "class", "international", "ele", 100, @@ -23,7 +23,7 @@ public class AerodromeLabelTest extends AbstractLayerTest { "_layer", "aerodrome_label", "_type", "point", - "_minzoom", 10, + "_minzoom", 8, "_maxzoom", 14, "_buffer", 64d )), process(pointFeature(Map.of( @@ -41,7 +41,8 @@ public class AerodromeLabelTest extends AbstractLayerTest { public void testInternational() { assertFeatures(14, List.of(Map.of( "class", "international", - "_layer", "aerodrome_label" + "_layer", "aerodrome_label", + "_minzoom", 10 // no IATA )), process(pointFeature(Map.of( "aeroway", "aerodrome", "aerodrome_type", "international" @@ -52,7 +53,8 @@ public class AerodromeLabelTest extends AbstractLayerTest { public void testPublic() { assertFeatures(14, List.of(Map.of( "class", "public", - "_layer", "aerodrome_label" + "_layer", "aerodrome_label", + "_minzoom", 10 )), process(pointFeature(Map.of( "aeroway", "aerodrome", "aerodrome_type", "public airport" @@ -70,7 +72,8 @@ public class AerodromeLabelTest extends AbstractLayerTest { public void testMilitary() { assertFeatures(14, List.of(Map.of( "class", "military", - "_layer", "aerodrome_label" + "_layer", "aerodrome_label", + "_minzoom", 10 )), process(pointFeature(Map.of( "aeroway", "aerodrome", "aerodrome_type", "military airport" @@ -88,7 +91,8 @@ public class AerodromeLabelTest extends AbstractLayerTest { public void testPrivate() { assertFeatures(14, List.of(Map.of( "class", "private", - "_layer", "aerodrome_label" + "_layer", "aerodrome_label", + "_minzoom", 10 )), process(pointFeature(Map.of( "aeroway", "aerodrome", "aerodrome_type", "private" @@ -106,7 +110,8 @@ public class AerodromeLabelTest extends AbstractLayerTest { public void testOther() { assertFeatures(14, List.of(Map.of( "class", "other", - "_layer", "aerodrome_label" + "_layer", "aerodrome_label", + "_minzoom", 10 )), process(pointFeature(Map.of( "aeroway", "aerodrome" )))); diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/BoundaryTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/BoundaryTest.java index 38d2d70..6fbe4e8 100644 --- a/src/test/java/com/onthegomap/planetiler/basemap/layers/BoundaryTest.java +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/BoundaryTest.java @@ -144,11 +144,30 @@ public class BoundaryTest extends AbstractLayerTest { "ne_10m_admin_1_states_provinces_lines", 0 ))); + assertFeatures(0, List.of(Map.of( + "_layer", "boundary", + "_type", "line", + "disputed", 0, + "maritime", 0, + "admin_level", 4, + + "_minzoom", 4, + "_maxzoom", 4, + "_buffer", 4d + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "min_zoom", 7.6d + ), + 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 + "min_zoom", 7.9d ), NATURAL_EARTH_SOURCE, "ne_10m_admin_1_states_provinces_lines", diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/BuildingTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/BuildingTest.java index 40ec4dc..a7006e8 100644 --- a/src/test/java/com/onthegomap/planetiler/basemap/layers/BuildingTest.java +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/BuildingTest.java @@ -41,6 +41,14 @@ public class BuildingTest extends AbstractLayerTest { )))); } + @Test + public void testIgnoreUndergroundBuilding() { + assertFeatures(14, List.of(), process(polygonFeature(Map.of( + "building", "yes", + "location", "underground" + )))); + } + @Test public void testAirportBuildings() { assertFeatures(13, List.of(Map.of( diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/LandcoverTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/LandcoverTest.java index 4c937e2..1d53ddb 100644 --- a/src/test/java/com/onthegomap/planetiler/basemap/layers/LandcoverTest.java +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/LandcoverTest.java @@ -130,6 +130,9 @@ public class LandcoverTest extends AbstractLayerTest { )), process(polygonFeature(Map.of( "natural", "dune" )))); + assertFeatures(10, List.of(), process(polygonFeature(Map.of( + "landuse", "park" + )))); } @Test @@ -137,6 +140,7 @@ public class LandcoverTest extends AbstractLayerTest { Map map = Map.of("subclass", "wood"); assertMerges(List.of(map, map, map, map, map, map), List.of( + // don't merge any 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")), @@ -144,19 +148,23 @@ public class LandcoverTest extends AbstractLayerTest { 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( + assertMerges(List.of(map, map, map), List.of( + // < 300 - merge 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")), + // >= 300 - don't merge 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( + assertMerges(List.of(map, map), List.of( + // < 300 - merge 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")), + // >= 300 - merge feature(rectangle(12, 18), Map.of("_numpoints", 300, "subclass", "wood")), feature(rectangle(12, 18), Map.of("_numpoints", 301, "subclass", "wood")) ), 9); diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/LanduseTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/LanduseTest.java index 8b87cdb..cbd6c1b 100644 --- a/src/test/java/com/onthegomap/planetiler/basemap/layers/LanduseTest.java +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/LanduseTest.java @@ -58,6 +58,18 @@ public class LanduseTest extends AbstractLayerTest { )))); } + @Test + public void testGraveYardBecomesCemetery() { + assertFeatures(14, List.of( + Map.of("_layer", "poi"), + Map.of( + "_layer", "landuse", + "class", "cemetery" + )), process(polygonFeature(Map.of( + "amenity", "grave_yard" + )))); + } + @Test public void testOsmLanduseLowerZoom() { assertFeatures(6, List.of(Map.of( diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/MountainPeakTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/MountainPeakTest.java index 9f364e3..e36e294 100644 --- a/src/test/java/com/onthegomap/planetiler/basemap/layers/MountainPeakTest.java +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/MountainPeakTest.java @@ -1,11 +1,15 @@ 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 org.junit.jupiter.api.Assertions.assertEquals; import com.google.common.collect.Lists; import com.onthegomap.planetiler.VectorTile; import com.onthegomap.planetiler.geo.GeometryException; +import com.onthegomap.planetiler.reader.SimpleFeature; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -31,6 +35,7 @@ public class MountainPeakTest extends AbstractLayerTest { "class", "peak", "ele", 100, "ele_ft", 328, + "customary_ft", "", "_layer", "mountain_peak", "_type", "point", @@ -70,6 +75,17 @@ public class MountainPeakTest extends AbstractLayerTest { )))); } + @Test + public void testSaddle() { + assertFeatures(14, List.of(Map.of( + "class", "saddle" + )), process(pointFeature(Map.of( + "natural", "saddle", + "ele", "100" + )))); + } + + @Test public void testNoElevation() { assertFeatures(14, List.of(), process(pointFeature(Map.of( @@ -86,7 +102,7 @@ public class MountainPeakTest extends AbstractLayerTest { } @Test - public void testIgnoreLines() { + public void testIgnorePeakLines() { assertFeatures(14, List.of(), process(lineFeature(Map.of( "natural", "peak", "name", "name", @@ -94,6 +110,68 @@ public class MountainPeakTest extends AbstractLayerTest { )))); } + @Test + public void testMountainLinestring() { + assertFeatures(14, List.of(Map.of( + "class", "ridge", + "name", "Ridge", + + "_layer", "mountain_peak", + "_type", "line", + "_minzoom", 13, + "_maxzoom", 14, + "_buffer", 100d + )), process(lineFeature(Map.of( + "natural", "ridge", + "name", "Ridge" + )))); + } + + @Test + public void testCustomaryFt() { + process(SimpleFeature.create( + rectangle(0, 0.1), + Map.of("iso_a2", "US"), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_countries", + 0 + )); + + // inside US - customary_ft=1 + assertFeatures(14, List.of(Map.of( + "class", "volcano", + "customary_ft", 1, + "ele", 100, + "ele_ft", 328 + )), process(SimpleFeature.create( + newPoint(0, 0), + new HashMap<>(Map.of( + "natural", "volcano", + "ele", "100" + )), + OSM_SOURCE, + null, + 0 + ))); + + // outside US - customary_ft omitted + assertFeatures(14, List.of(Map.of( + "class", "volcano", + "customary_ft", "", + "ele", 100, + "ele_ft", 328 + )), process(SimpleFeature.create( + newPoint(1, 1), + new HashMap<>(Map.of( + "natural", "volcano", + "ele", "100" + )), + OSM_SOURCE, + null, + 0 + ))); + } + private int getSortKey(Map tags) { return process(pointFeature(Map.of( "natural", "peak", diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/ParkTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/ParkTest.java index 7e71368..6024023 100644 --- a/src/test/java/com/onthegomap/planetiler/basemap/layers/ParkTest.java +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/ParkTest.java @@ -13,9 +13,9 @@ public class ParkTest extends AbstractLayerTest { "_layer", "park", "_type", "polygon", "class", "national_park", - "name", "", + "name", "Grand Canyon National Park", "_minpixelsize", 2d, - "_minzoom", 6, + "_minzoom", 4, "_maxzoom", 14 ), Map.of( "_layer", "park", @@ -24,8 +24,8 @@ public class ParkTest extends AbstractLayerTest { "name", "Grand Canyon National Park", "name_int", "Grand Canyon National Park", "name:latin", "Grand Canyon National Park", - "name:es", "es name", - "_minzoom", 6, +// "name:es", "es name", // don't include all translations + "_minzoom", 5, "_maxzoom", 14 )), process(polygonFeature(Map.of( "boundary", "national_park", @@ -52,9 +52,9 @@ public class ParkTest extends AbstractLayerTest { "_layer", "park", "_type", "polygon", "class", "protected_area", - "name", "", + "name", "Small park", "_minpixelsize", 2d, - "_minzoom", 6, + "_minzoom", 4, "_maxzoom", 14 ), Map.of( "_layer", "park", @@ -75,7 +75,7 @@ public class ParkTest extends AbstractLayerTest { ), Map.of( "_layer", "park", "_type", "point", - "_minzoom", 6, + "_minzoom", 5, "_maxzoom", 14 )), process(polygonFeatureWithArea(1, Map.of( "boundary", "protected_area", diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/PlaceTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/PlaceTest.java index 4d2baf2..afdcb04 100644 --- a/src/test/java/com/onthegomap/planetiler/basemap/layers/PlaceTest.java +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/PlaceTest.java @@ -131,8 +131,8 @@ public class PlaceTest extends AbstractLayerTest { rectangle(0.4, 0.6), Map.of( "name", "Massachusetts - not important", - "scalerank", 4, - "labelrank", 4, + "scalerank", 8, + "labelrank", 8, "datarank", 1 ), NATURAL_EARTH_SOURCE, @@ -194,6 +194,44 @@ public class PlaceTest extends AbstractLayerTest { ))); } + @Test + public void testProvince() { + wikidataTranslations.put(95027, "es", "provincia de Lugo"); + process(SimpleFeature.create( + rectangle(0, 0.25), + Map.of( + "name", "Nova Scotia", + "scalerank", 3, + "labelrank", 3, + "datarank", 3 + ), + NATURAL_EARTH_SOURCE, + "ne_10m_admin_1_states_provinces", + 0 + )); + + assertFeatures(4, List.of(Map.of( + "_layer", "place", + "class", "province", + "name", "Lugo", + "name:es", "provincia de Lugo", + "rank", 3, + + "_type", "point", + "_minzoom", 2 + )), process(SimpleFeature.create( + newPoint(0.1, 0.1), + Map.of( + "place", "province", + "wikidata", "Q95027", + "name", "Lugo" + ), + OSM_SOURCE, + null, + 0 + ))); + } + @Test public void testIslandPoint() { assertFeatures(0, List.of(Map.of( diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/PoiTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/PoiTest.java index 570073b..8a0da20 100644 --- a/src/test/java/com/onthegomap/planetiler/basemap/layers/PoiTest.java +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/PoiTest.java @@ -181,4 +181,30 @@ public class PoiTest extends AbstractLayerTest { ) ))); } + + @Test + public void testEmbassy() { + assertFeatures(7, List.of(Map.of( + "_layer", "poi", + "class", "diplomatic", + "subclass", "diplomatic", + "name", "The Embassy" + )), process(pointFeature(Map.of( + "office", "diplomatic", + "name", "The Embassy" + )))); + } + + @Test + public void testLocksmith() { + assertFeatures(7, List.of(Map.of( + "_layer", "poi", + "class", "shop", + "subclass", "locksmith", + "name", "The Locksmith" + )), process(pointFeature(Map.of( + "shop", "locksmith", + "name", "The Locksmith" + )))); + } } diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/TransportationTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/TransportationTest.java index 11f4b13..04fb63f 100644 --- a/src/test/java/com/onthegomap/planetiler/basemap/layers/TransportationTest.java +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/TransportationTest.java @@ -1,14 +1,20 @@ package com.onthegomap.planetiler.basemap.layers; 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.NATURAL_EARTH_SOURCE; import static com.onthegomap.planetiler.basemap.BasemapProfile.OSM_SOURCE; import com.onthegomap.planetiler.FeatureCollector; +import com.onthegomap.planetiler.basemap.BasemapProfile; +import com.onthegomap.planetiler.config.Arguments; +import com.onthegomap.planetiler.config.PlanetilerConfig; 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.stats.Stats; import java.util.List; import java.util.Map; import java.util.stream.Stream; @@ -33,12 +39,12 @@ public class TransportationTest extends AbstractLayerTest { "_type", "line", "class", "path", "subclass", "footway", - "oneway", 0, + "oneway", "", "name", "", + "layer", "", "_buffer", 4d, "_minpixelsize", 0d, - "_minzoom", 13, - "_maxzoom", 14 + "_minzoom", 13 ), Map.of( "_layer", "transportation_name", "_type", "line", @@ -54,9 +60,10 @@ public class TransportationTest extends AbstractLayerTest { assertFeatures(13, List.of(Map.of( "_layer", "transportation", "surface", "paved", - "oneway", 0, + "oneway", "", + "layer", "", "level", 0L, - "ramp", 0, + "ramp", "", "bicycle", "dismount", "foot", "designated" ), Map.of( @@ -70,20 +77,154 @@ public class TransportationTest extends AbstractLayerTest { )), result); } + @Test + public void testImportantPath() { + var rel = new OsmElement.Relation(1); + + rel.setTag("colour", "white"); + rel.setTag("name", "Appalachian Trail - 11 MA"); + rel.setTag("network", "nwn"); + rel.setTag("osmc", "symbol white::white_stripe"); + rel.setTag("ref", "AT"); + rel.setTag("route", "hiking"); + rel.setTag("short_name", "AT 11 MA"); + rel.setTag("symbol", "white-paint blazes"); + rel.setTag("type", "route"); + rel.setTag("wikidata", "Q620648"); + rel.setTag("wikipedia", "en:Appalachian Trail"); + + FeatureCollector features = process(lineFeatureWithRelation( + profile.preprocessOsmRelation(rel), + Map.of( + "bicycle", "no", + "highway", "path", + "horse", "no", + "name", "Appalachian Trail", + "ref", "AT", + "surface", "ground" + ))); + assertFeatures(12, List.of(Map.of( + "_layer", "transportation", + "_type", "line", + "class", "path", + "subclass", "path", + "oneway", "", + "name", "", + "layer", "", + "_buffer", 4d, + "_minpixelsize", 0d, + "_minzoom", 12 + ), Map.of( + "_layer", "transportation_name", + "_type", "line", + "class", "path", + "subclass", "path", + "name", "Appalachian Trail", + "name_int", "Appalachian Trail", + "name:latin", "Appalachian Trail", + "_minpixelsize", 0d, + "_minzoom", 12 + )), features); + } + @Test public void testUnnamedPath() { - assertFeatures(13, List.of(Map.of( + assertFeatures(14, List.of(Map.of( "_layer", "transportation", "class", "path", "subclass", "path", "surface", "unpaved", - "oneway", 0 + "oneway", "", + "_minzoom", 14 )), process(lineFeature(Map.of( "surface", "dirt", "highway", "path" )))); } + @Test + public void testPrivatePath() { + assertFeatures(9, List.of(Map.of( + "_layer", "transportation", + "class", "path", + "access", "no" + )), process(lineFeature(Map.of( + "access", "private", + "highway", "path" + )))); + assertFeatures(9, List.of(Map.of( + "_layer", "transportation", + "class", "path", + "access", "no" + )), process(lineFeature(Map.of( + "access", "no", + "highway", "path" + )))); + assertFeatures(8, List.of(Map.of( + "_layer", "transportation", + "class", "path", + "access", "" + )), process(lineFeature(Map.of( + "access", "no", + "highway", "path" + )))); + } + + @Test + public void testExpressway() { + assertFeatures(8, List.of(Map.of( + "_layer", "transportation", + "class", "motorway", + "expressway", "" + )), process(lineFeature(Map.of( + "highway", "motorway", + "expressway", "yes" + )))); + assertFeatures(8, List.of(Map.of( + "_layer", "transportation", + "class", "primary", + "expressway", 1 + )), process(lineFeature(Map.of( + "highway", "primary", + "expressway", "yes" + )))); + assertFeatures(7, List.of(Map.of( + "_layer", "transportation", + "class", "primary", + "expressway", "" + )), process(lineFeature(Map.of( + "highway", "primary", + "expressway", "yes" + )))); + } + + @Test + public void testToll() { + assertFeatures(9, List.of(Map.of( + "_layer", "transportation", + "class", "motorway", + "toll", "" + )), process(lineFeature(Map.of( + "highway", "motorway" + )))); + assertFeatures(9, List.of(Map.of( + "_layer", "transportation", + "class", "motorway", + "toll", 1 + )), process(lineFeature(Map.of( + "highway", "motorway", + "toll", "yes" + )))); + assertFeatures(8, List.of(Map.of( + "_layer", "transportation", + "class", "motorway", + "toll", "" + )), process(lineFeature(Map.of( + "highway", "motorway", + "toll", "yes" + )))); + } + @Test public void testIndoorTunnelSteps() { assertFeatures(13, List.of(Map.of( @@ -93,7 +234,7 @@ public class TransportationTest extends AbstractLayerTest { "brunnel", "tunnel", "indoor", 1, "oneway", 1, - "ramp", 1 + "ramp", "" )), process(lineFeature(Map.of( "highway", "steps", "tunnel", "building_passage", @@ -124,12 +265,37 @@ public class TransportationTest extends AbstractLayerTest { "bridge", "yes" ))); + assertFeatures(13, List.of(mapOf( + "_layer", "transportation", + "class", "motorway", + "surface", "paved", + "oneway", 1, + "ramp", "", + "bicycle", "no", + "foot", "no", + "horse", "no", + "brunnel", "bridge", + "network", "us-interstate", + "_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", "", + "route_1", "US:I=90", + "_minzoom", 6 + )), features); + assertFeatures(13, List.of(Map.of( "_layer", "transportation", "class", "motorway", "surface", "paved", "oneway", 1, - "ramp", 0, + "ramp", "", "bicycle", "no", "foot", "no", "horse", "no", @@ -143,6 +309,7 @@ public class TransportationTest extends AbstractLayerTest { "ref", "90", "ref_length", 2, "network", "us-interstate", + "route_1", "US:I=90", "brunnel", "", "_minzoom", 6 )), features); @@ -166,11 +333,92 @@ public class TransportationTest extends AbstractLayerTest { "ref", "90", "ref_length", 2, "network", "us-interstate", + "route_1", "US:I=90", "brunnel", "", "_minzoom", 6 )), features); } + @Test + public void testMotorwayJunction() { + var otherNode1 = new OsmElement.Node(1, 1, 1); + var junctionNode = new OsmElement.Node(2, 1, 2); + var otherNode2 = new OsmElement.Node(3, 1, 3); + var otherNode3 = new OsmElement.Node(4, 2, 3); + + junctionNode.setTag("highway", "motorway_junction"); + junctionNode.setTag("name", "exit 1"); + junctionNode.setTag("layer", "1"); + junctionNode.setTag("ref", "12"); + + // 2 ways meet at junctionNode (id=2) - use most important class of a highway intersecting it (motorway) + var way1 = new OsmElement.Way(5); + way1.setTag("highway", "motorway"); + way1.nodes().add(otherNode1.id(), junctionNode.id(), otherNode2.id()); + var way2 = new OsmElement.Way(6); + way2.setTag("highway", "primary"); + way2.nodes().add(junctionNode.id(), otherNode3.id()); + + profile.preprocessOsmNode(otherNode1); + profile.preprocessOsmNode(junctionNode); + profile.preprocessOsmNode(otherNode2); + profile.preprocessOsmNode(otherNode3); + + profile.preprocessOsmWay(way1); + profile.preprocessOsmWay(way2); + + FeatureCollector features = process(SimpleFeature.create( + newPoint(1, 2), + junctionNode.tags(), + OSM_SOURCE, + null, + junctionNode.id() + )); + + assertFeatures(13, List.of(mapOf( + "_layer", "transportation_name", + "class", "motorway", + "subclass", "junction", + "name", "exit 1", + "ref", "12", + "ref_length", 2, + "layer", 1L, + "_type", "point", + "_minzoom", 10 + )), features); + } + + @Test + public void testInterstateMotorwayWithoutWayInfo() { + 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" + ))); + + assertFeatures(13, List.of(mapOf( + "_layer", "transportation", + "class", "motorway", + "network", "us-interstate", + "_minzoom", 4 + ), Map.of( + "_layer", "transportation_name", + "class", "motorway", + "ref", "90", + "ref_length", 2, + "network", "us-interstate", + "brunnel", "", + "route_1", "US:I=90", + "_minzoom", 6 + )), features); + } + @Test public void testPrimaryRoadConstruction() { assertFeatures(13, List.of(Map.of( @@ -223,7 +471,7 @@ public class TransportationTest extends AbstractLayerTest { "_layer", "transportation", "class", "service", "service", "driveway", - "_minzoom", 12 + "_minzoom", 14 )), process(lineFeature(Map.of( "highway", "service", "service", "driveway" @@ -256,7 +504,28 @@ public class TransportationTest extends AbstractLayerTest { } @Test - public void testTrack() { + public void testNamedTrack() { + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "track", + "surface", "unpaved", + "horse", "yes", + "_minzoom", 13 + ), Map.of( + "_layer", "transportation_name", + "class", "track", + "name", "name", + "_minzoom", 13 + )), process(lineFeature(Map.of( + "highway", "track", + "surface", "dirt", + "horse", "yes", + "name", "name" + )))); + } + + @Test + public void testUnnamedTrack() { assertFeatures(13, List.of(Map.of( "_layer", "transportation", "class", "track", @@ -270,6 +539,29 @@ public class TransportationTest extends AbstractLayerTest { )))); } + @Test + public void testBusway() { + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "busway", + "brunnel", "tunnel", + "_minzoom", 11 + ), Map.of( + "_layer", "transportation_name", + "class", "busway", + "name", "Silver Line", + "_minzoom", 12 + )), process(lineFeature(Map.of( + "access", "no", + "bus", "yes", + "highway", "busway", + "layer", "-1", + "name", "Silver Line", + "trolley_wire", "yes", + "tunnel", "yes" + )))); + } + final OsmElement.Relation relUS = new OsmElement.Relation(1); { @@ -294,8 +586,9 @@ public class TransportationTest extends AbstractLayerTest { "_layer", "transportation", "class", "primary", "surface", "paved", - "oneway", 0, - "ramp", 0, + "oneway", "", + "ramp", "", + "network", "us-highway", "_minzoom", 7 ), Map.of( "_layer", "transportation_name", @@ -305,6 +598,8 @@ public class TransportationTest extends AbstractLayerTest { "ref", "3", "ref_length", 1, "network", "us-highway", + "route_1", "US:US=3", + "route_2", "US:MA=2", "_minzoom", 12 )), process(lineFeatureWithRelation( Stream.concat( @@ -325,12 +620,17 @@ public class TransportationTest extends AbstractLayerTest { ), Map.of( "_layer", "transportation_name", "class", "primary", + "route_1", "US:US=3", + "route_2", "US:MA=2", "ref", "3", "network", "us-highway" )), process(lineFeatureWithRelation( Stream.concat( profile.preprocessOsmRelation(relMA).stream(), - profile.preprocessOsmRelation(relUS).stream() + Stream.concat( // ignore duplicates + profile.preprocessOsmRelation(relUS).stream(), + profile.preprocessOsmRelation(relUS).stream() + ) ).toList(), Map.of( "highway", "primary", @@ -368,7 +668,8 @@ public class TransportationTest extends AbstractLayerTest { public void testCompoundRef() { assertFeatures(13, List.of(Map.of( "_layer", "transportation", - "class", "primary" + "class", "primary", + "network", "" ), Map.of( "_layer", "transportation_name", "class", "primary", @@ -409,7 +710,7 @@ public class TransportationTest extends AbstractLayerTest { "class", "motorway", "surface", "paved", "oneway", 1, - "ramp", 0, + "ramp", "", "_minzoom", 4 ), Map.of( "_layer", "transportation_name", @@ -438,7 +739,7 @@ public class TransportationTest extends AbstractLayerTest { "_layer", "transportation", "class", "motorway", "oneway", 1, - "ramp", 0, + "ramp", "", "_minzoom", 4 ), Map.of( "_layer", "transportation_name", @@ -464,7 +765,7 @@ public class TransportationTest extends AbstractLayerTest { "_layer", "transportation", "class", "motorway", "oneway", 1, - "ramp", 0, + "ramp", "", "_minzoom", 4 ), Map.of( "_layer", "transportation_name", @@ -504,8 +805,8 @@ public class TransportationTest extends AbstractLayerTest { "subclass", "light_rail", "brunnel", "tunnel", "layer", -1L, - "oneway", 0, - "ramp", 0, + "oneway", "", + "ramp", "", "_minzoom", 11, "_maxzoom", 14, @@ -526,8 +827,8 @@ public class TransportationTest extends AbstractLayerTest { "subclass", "subway", "brunnel", "tunnel", "layer", -2L, - "oneway", 0, - "ramp", 0, + "oneway", "", + "ramp", "", "_minzoom", 14, "_maxzoom", 14, @@ -561,6 +862,7 @@ public class TransportationTest extends AbstractLayerTest { "layer", "-2" )))); assertFeatures(13, List.of(Map.of( + "layer", "", "_minzoom", 10 )), process(lineFeature(Map.of( "railway", "rail", @@ -599,11 +901,20 @@ public class TransportationTest extends AbstractLayerTest { @Test public void testAerialway() { - assertFeatures(10, List.of(Map.of( + assertFeatures(12, List.of(Map.of( "_layer", "transportation", "class", "aerialway", "subclass", "gondola", + "_minzoom", 12, + "_maxzoom", 14, + "_type", "line" + ), Map.of( + "_layer", "transportation_name", + "class", "aerialway", + "subclass", "gondola", + "name", "Summit Gondola", + "_minzoom", 12, "_maxzoom", 14, "_type", "line" @@ -627,6 +938,14 @@ public class TransportationTest extends AbstractLayerTest { "_minzoom", 11, "_maxzoom", 14, "_type", "line" + ), Map.of( + "_layer", "transportation_name", + "class", "ferry", + "name", "Boston - Provincetown Ferry", + + "_minzoom", 12, + "_maxzoom", 14, + "_type", "line" )), process(lineFeature(Map.of( "route", "ferry", "name", "Boston - Provincetown Ferry", @@ -718,4 +1037,25 @@ public class TransportationTest extends AbstractLayerTest { getWaySortKey(Map.of("highway", "motorway", "layer", "-2")) ); } + + @Test + public void testTransportationNameLayerRequiresTransportationLayer() { + var profile = new BasemapProfile(translations, PlanetilerConfig.from(Arguments.of( + "only_layers", "transportation_name" + )), Stats.inMemory()); + SourceFeature feature = lineFeature(Map.of( + "highway", "path", + "name", "test" + )); + var collector = featureCollectorFactory.get(feature); + profile.processFeature(feature, collector); + assertFeatures(14, List.of(Map.of( + "_layer", "transportation_name", + "class", "path", + "name", "test" + ), Map.of( + "_layer", "transportation", + "class", "path" + )), collector); + } } diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterTest.java index 73563bd..e667ce4 100644 --- a/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterTest.java +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterTest.java @@ -132,7 +132,7 @@ public class WaterTest extends AbstractLayerTest { "_minzoom", 6, "_maxzoom", 14 )), process(polygonFeature(Map.of( - "waterway", "stream", + "waterway", "riverbank", "bridge", "1", "intermittent", "1" )))); @@ -152,6 +152,39 @@ public class WaterTest extends AbstractLayerTest { )))); } + @Test + public void testRiverbank() { + assertFeatures(11, List.of(Map.of( + "class", "river", + "_layer", "water", + "_type", "polygon" + )), process(polygonFeature(Map.of( + "waterway", "riverbank" + )))); + } + + @Test + public void testRiverk() { + assertFeatures(11, List.of(Map.of( + "class", "river", + "_layer", "water", + "_type", "polygon" + )), process(polygonFeature(Map.of( + "water", "river" + )))); + } + + @Test + public void testSpring() { + assertFeatures(11, List.of(Map.of( + "class", "lake", + "_layer", "water", + "_type", "polygon" + )), process(polygonFeature(Map.of( + "natural", "spring" + )))); + } + @Test public void testOceanZoomLevels() { assertCoversZoomRange(0, 14, "water", diff --git a/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterwayTest.java b/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterwayTest.java index c77c4f1..4137a8b 100644 --- a/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterwayTest.java +++ b/src/test/java/com/onthegomap/planetiler/basemap/layers/WaterwayTest.java @@ -2,17 +2,86 @@ 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 com.onthegomap.planetiler.basemap.BasemapProfile.OSM_SOURCE; import static org.junit.jupiter.api.Assertions.assertEquals; +import com.onthegomap.planetiler.FeatureCollector; import com.onthegomap.planetiler.VectorTile; 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 com.onthegomap.planetiler.reader.osm.OsmRelationInfo; +import java.util.ArrayList; 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 WaterwayTest extends AbstractLayerTest { + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void testOsmWaterwayRelation(boolean isLongEnough) throws GeometryException { + var rel = new OsmElement.Relation(1); + rel.setTag("name", "River Relation"); + rel.setTag("name:es", "ES name"); + rel.setTag("waterway", "river"); + + List relationInfos = profile.preprocessOsmRelation(rel); + FeatureCollector features = process(SimpleFeature.createFakeOsmFeature( + newLineString(0, 0, 0, isLongEnough ? 3 : 1), + Map.of(), + OSM_SOURCE, + null, + 0, + (relationInfos == null ? List.of() : relationInfos).stream() + .map(r -> new OsmReader.RelationMember<>("", r)).toList() + )); + assertFeatures(14, List.of(Map.of( + "class", "river", + "name", "River Relation", + "name:es", "ES name", + "_relid", 1L, + + "_layer", "waterway", + "_type", "line", + "_minzoom", 6, + "_maxzoom", 8, + "_buffer", 4d + )), features); + + // ensure that post-processing combines waterways, and filters out ones that + // belong to rivers that are not long enough to be shown + var line1 = new VectorTile.Feature( + Waterway.LAYER_NAME, + 1, + VectorTile.encodeGeometry(newLineString(0, 0, 10, 0)), + mapOf("name", "river", "_relid", 1L), + 0 + ); + var line2 = new VectorTile.Feature( + Waterway.LAYER_NAME, + 1, + VectorTile.encodeGeometry(newLineString(10, 0, 20, 0)), + mapOf("name", "river", "_relid", 1L), + 0 + ); + var connected = new VectorTile.Feature( + Waterway.LAYER_NAME, + 1, + VectorTile.encodeGeometry(newLineString(0, 0, 20, 0)), + mapOf("name", "river"), + 0 + ); + + assertEquals( + isLongEnough ? List.of(connected) : List.of(), + profile.postProcessLayerFeatures(Waterway.LAYER_NAME, 8, new ArrayList<>(List.of(line1, line2))) + ); + } + @Test public void testWaterwayImportantRiverProcess() { var charlesRiver = process(lineFeature(Map.of( @@ -163,24 +232,5 @@ public class WaterwayTest extends AbstractLayerTest { "ne_50m_rivers_lake_centerlines", 0 ))); - - assertFeatures(6, List.of(Map.of( - "class", "river", - "intermittent", "", - - "_layer", "waterway", - "_type", "line", - "_minzoom", 6, - "_maxzoom", 8 - )), process(SimpleFeature.create( - newLineString(0, 0, 1, 1), - Map.of( - "featurecla", "River", - "name", "name" - ), - NATURAL_EARTH_SOURCE, - "ne_10m_rivers_lake_centerlines", - 0 - ))); } }