diff --git a/README.md b/README.md index 90ae11f..f6f4977 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,11 @@ available options. ## Differences from OpenMapTiles - Road name abbreviations are not implemented yet in the `transportation_name` layer -- `agg_stop` tag not implemented yet in the `poi` layer - `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) +- some line and polygon tolerances are different, can be tweaked with `--simplify-tolerance` parameter ## Customizing diff --git a/pom.xml b/pom.xml index 241eea8..fb9ab5e 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ org.openmaptiles planetiler-openmaptiles - 3.14.0 + 3.15.0-SNAPSHOT OpenMapTiles Vector Tile Schema implementation for Planetiler tool diff --git a/scripts/regenerate-openmaptiles.sh b/scripts/regenerate-openmaptiles.sh index 54fb8d7..2597334 100755 --- a/scripts/regenerate-openmaptiles.sh +++ b/scripts/regenerate-openmaptiles.sh @@ -4,7 +4,8 @@ set -o errexit set -o pipefail set -o nounset -TAG="${1:-"v3.14"}" +# TODO: change to "v3.15" once that is released +TAG="${1:-"master"}" echo "tag=${TAG}" BASE_URL="${2:-"https://raw.githubusercontent.com/openmaptiles/openmaptiles/"}" diff --git a/src/main/java/org/openmaptiles/OpenMapTilesProfile.java b/src/main/java/org/openmaptiles/OpenMapTilesProfile.java index e6d20e7..d5d992f 100644 --- a/src/main/java/org/openmaptiles/OpenMapTilesProfile.java +++ b/src/main/java/org/openmaptiles/OpenMapTilesProfile.java @@ -220,12 +220,12 @@ public class OpenMapTilesProfile extends ForwardingProfile { /** * Layers should implement this interface to subscribe to elements from - * OSM lake centerlines source. + * OSM lake centerlines source. */ public interface LakeCenterlineProcessor { /** - * Process an element from the OSM lake centerlines + * Process an element from the OSM lake centerlines * source * * @see Profile#processFeature(SourceFeature, FeatureCollector) diff --git a/src/main/java/org/openmaptiles/generated/OpenMapTilesSchema.java b/src/main/java/org/openmaptiles/generated/OpenMapTilesSchema.java index 8ae534a..03014fc 100644 --- a/src/main/java/org/openmaptiles/generated/OpenMapTilesSchema.java +++ b/src/main/java/org/openmaptiles/generated/OpenMapTilesSchema.java @@ -49,8 +49,8 @@ import org.openmaptiles.Layer; /** * All vector tile layer definitions, attributes, and allowed values generated from the - * OpenMapTiles vector tile schema - * v3.14. + * OpenMapTiles vector tile schema + * master. */ @SuppressWarnings("unused") public class OpenMapTilesSchema { @@ -59,11 +59,11 @@ public class OpenMapTilesSchema { public static final String VERSION = "3.14.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", - "is", "it", "ja", "ja_kana", "ja_rm", "ja-Latn", "ja-Hira", "ka", "kk", "kn", "ko", "ko-Latn", "ku", "la", "lb", - "lt", "lv", "mk", "mt", "ml", "nl", "no", "oc", "pl", "pt", "rm", "ro", "ru", "sk", "sl", "sq", "sr", "sr-Latn", - "sv", "ta", "te", "th", "tr", "uk", "zh"); + public static final List LANGUAGES = List.of("am", "ar", "az", "be", "bg", "bn", "br", "bs", "ca", "co", "cs", + "cy", "da", "de", "el", "en", "eo", "es", "et", "eu", "fa", "fi", "fr", "fy", "ga", "gd", "he", "hi", "hr", "hu", + "hy", "id", "is", "it", "ja", "ja_kana", "ja_rm", "ja-Latn", "ja-Hira", "ka", "kk", "kn", "ko", "ko-Latn", "ku", + "la", "lb", "lt", "lv", "mk", "mt", "ml", "nl", "no", "oc", "pa", "pnb", "pl", "pt", "rm", "ro", "ru", "sk", "sl", + "sq", "sr", "sr-Latn", "sv", "ta", "te", "th", "tr", "uk", "ur", "vi", "zh", "zh-Hant", "zh-Hans"); /** Returns a list of expected layer implementation instances from the {@code layers} package. */ public static List createInstances(Translations translations, PlanetilerConfig config, Stats stats) { @@ -96,7 +96,7 @@ public class OpenMapTilesSchema { * boundaries show up. So you might not be able to use border styling for ocean water features. * * Generated from - * water.yaml + * water.yaml */ public interface Water extends Layer { double BUFFER_SIZE = 4.0; @@ -117,11 +117,15 @@ public class OpenMapTilesSchema { /** * All water polygons from OpenStreetMapData have the class - * ocean. Water bodies with the - * water=river tag are classified as - * river. Wet and dry docks tagged + * ocean. The water-covered areas of flowing water bodies with the + * water=river, + * water=canal, + * water=stream, + * water=ditch, or + * water=drain tags are classified + * as river. Wet and dry docks tagged * waterway=dock are classified as - * a dock. Swimming pools tagged + * a dock. Various minor waterbodies are classified as a pond. Swimming pools tagged * leisure=swimming_pool * are classified as a swimming_pool All other water bodies are classified as lake. *

@@ -129,6 +133,7 @@ public class OpenMapTilesSchema { *

    *
  • dock *
  • river + *
  • pond *
  • lake *
  • ocean *
  • swimming_pool @@ -163,10 +168,11 @@ public class OpenMapTilesSchema { final class FieldValues { public static final String CLASS_DOCK = "dock"; public static final String CLASS_RIVER = "river"; + public static final String CLASS_POND = "pond"; public static final String CLASS_LAKE = "lake"; public static final String CLASS_OCEAN = "ocean"; public static final String CLASS_SWIMMING_POOL = "swimming_pool"; - public static final Set CLASS_VALUES = Set.of("dock", "river", "lake", "ocean", "swimming_pool"); + public static final Set CLASS_VALUES = Set.of("dock", "river", "pond", "lake", "ocean", "swimming_pool"); public static final String BRUNNEL_BRIDGE = "bridge"; public static final String BRUNNEL_TUNNEL = "tunnel"; public static final Set BRUNNEL_VALUES = Set.of("bridge", "tunnel"); @@ -175,8 +181,9 @@ public class OpenMapTilesSchema { final class FieldMappings { public static final MultiExpression Class = MultiExpression.of(List.of(MultiExpression.entry("dock", matchAny("waterway", "dock")), - MultiExpression.entry("river", matchAny("water", "river")), MultiExpression.entry("lake", FALSE), - MultiExpression.entry("ocean", FALSE), + MultiExpression.entry("river", matchAny("water", "river", "stream", "canal", "ditch", "drain")), + MultiExpression.entry("pond", matchAny("water", "pond", "basin", "wastewater")), + MultiExpression.entry("lake", FALSE), MultiExpression.entry("ocean", FALSE), MultiExpression.entry("swimming_pool", matchAny("leisure", "swimming_pool")))); } } @@ -188,7 +195,7 @@ public class OpenMapTilesSchema { * field applied. Waterways do not have a subclass field. * * Generated from - * waterway.yaml + * waterway.yaml */ public interface Waterway extends Layer { double BUFFER_SIZE = 4.0; @@ -273,7 +280,7 @@ public class OpenMapTilesSchema { * layer is to style wood (class=wood) and grass (class=grass) areas. * * Generated from landcover.yaml + * "https://github.com/openmaptiles/openmaptiles/blob/master/layers/landcover/landcover.yaml">landcover.yaml */ public interface Landcover extends Layer { double BUFFER_SIZE = 4.0; @@ -430,7 +437,7 @@ public class OpenMapTilesSchema { * residential (urban) areas and at higher zoom levels mostly OSM landuse tags. * * Generated from - * landuse.yaml + * landuse.yaml */ public interface Landuse extends Layer { double BUFFER_SIZE = 4.0; @@ -526,7 +533,7 @@ public class OpenMapTilesSchema { * Natural peaks * * Generated from mountain_peak.yaml + * "https://github.com/openmaptiles/openmaptiles/blob/master/layers/mountain_peak/mountain_peak.yaml">mountain_peak.yaml */ public interface MountainPeak extends Layer { double BUFFER_SIZE = 64.0; @@ -594,14 +601,18 @@ public class OpenMapTilesSchema { } } /** - * The park layer contains parks from OpenStreetMap tagged with - * boundary=national_park, + * The park layer in OpenMapTiles contains natural and protected areas from OpenStreetMap, such as parks tagged with + * boundary=national_park, * boundary=protected_area, or - * leisure=nature_reserve. + * "https://wiki.openstreetmap.org/wiki/Tag:boundary%3Dprotected_area">boundary=protected_area, or + * leisure=nature_reserve. + * This layer also includes boundaries for indigenous lands tagged with boundary=aboriginal_lands. + * Indigenous boundaries are not parks, but they are included in this layer for technical reasons related to data + * processing. These boundaries represent areas with special legal and administrative status for indigenous peoples. * * Generated from - * park.yaml + * park.yaml */ public interface Park extends Layer { double BUFFER_SIZE = 4.0; @@ -615,15 +626,16 @@ public class OpenMapTilesSchema { /** Attribute names for map elements in the park layer. */ final class Fields { /** - * Use the class to differentiate between different parks. The class for - * boundary=protected_area parks is the lower-case of the + * Use the class to differentiate between different kinds of features in the parks + * layer, for example between parks and non-parks. The class for boundary=protected_area parks is the + * lower-case of the * protection_title value with * blanks replaced by _. national_park is the class of * protection_title=National Park and boundary=national_park. * nature_reserve is the class of protection_title=Nature Reserve and * leisure=nature_reserve. The class for other * protection_title values is - * similarly assigned. + * similarly assigned. The class for boundary=aboriginal_lands is aboriginal_lands. */ public static final String CLASS = "class"; /** @@ -661,7 +673,7 @@ public class OpenMapTilesSchema { * but for most styles it makes sense to just style admin_level=2 and admin_level=4. * * Generated from - * boundary.yaml + * boundary.yaml */ public interface Boundary extends Layer { double BUFFER_SIZE = 4.0; @@ -762,7 +774,7 @@ public class OpenMapTilesSchema { * in the aeroway layer. * * Generated from - * aeroway.yaml + * aeroway.yaml */ public interface Aeroway extends Layer { double BUFFER_SIZE = 4.0; @@ -822,7 +834,7 @@ public class OpenMapTilesSchema { * features like plazas. * * Generated from transportation.yaml + * "https://github.com/openmaptiles/openmaptiles/blob/master/layers/transportation/transportation.yaml">transportation.yaml */ public interface Transportation extends Layer { double BUFFER_SIZE = 4.0; @@ -907,8 +919,9 @@ public class OpenMapTilesSchema { * The network type derived mainly from * network tag of the road. See more * info about us- , - * ca-transcanada, or - * gb- . + * ca-transcanada, + * gb- , + * or ie- . */ public static final String NETWORK = "network"; @@ -1166,7 +1179,7 @@ public class OpenMapTilesSchema { * location:underground are excluded. * * Generated from - * building.yaml + * building.yaml */ public interface Building extends Layer { double BUFFER_SIZE = 4.0; @@ -1208,7 +1221,7 @@ public class OpenMapTilesSchema { * from OSM water bodies. Only the most important lakes contain labels. * * Generated from water_name.yaml + * "https://github.com/openmaptiles/openmaptiles/blob/master/layers/water_name/water_name.yaml">water_name.yaml */ public interface WaterName extends Layer { double BUFFER_SIZE = 256.0; @@ -1231,11 +1244,14 @@ public class OpenMapTilesSchema { public static final String NAME_DE = "name_de"; /** - * Distinguish between lake, ocean and sea. + * Distinguish between lake, ocean, bay, strait, and + * sea. *

    * allowed values: *

      *
    • "lake" + *
    • "bay" + *
    • "strait" *
    • "sea" *
    • "ocean" *
    @@ -1257,9 +1273,11 @@ public class OpenMapTilesSchema { /** Attribute values for map elements in the water_name layer. */ final class FieldValues { public static final String CLASS_LAKE = "lake"; + public static final String CLASS_BAY = "bay"; + public static final String CLASS_STRAIT = "strait"; public static final String CLASS_SEA = "sea"; public static final String CLASS_OCEAN = "ocean"; - public static final Set CLASS_VALUES = Set.of("lake", "sea", "ocean"); + public static final Set CLASS_VALUES = Set.of("lake", "bay", "strait", "sea", "ocean"); } /** Complex mappings to generate attribute values from OSM element tags in the water_name layer. */ final class FieldMappings { @@ -1273,7 +1291,7 @@ public class OpenMapTilesSchema { * while for other roads you should use name. * * Generated from transportation_name.yaml + * "https://github.com/openmaptiles/openmaptiles/blob/master/layers/transportation_name/transportation_name.yaml">transportation_name.yaml */ public interface TransportationName extends Layer { double BUFFER_SIZE = 8.0; @@ -1316,8 +1334,14 @@ public class OpenMapTilesSchema { *
  • "us-highway" *
  • "us-state" *
  • "ca-transcanada" + *
  • "ca-provincial-arterial" + *
  • "ca-provincial" *
  • "gb-motorway" *
  • "gb-trunk" + *
  • "gb-primary" + *
  • "ie-motorway" + *
  • "ie-national" + *
  • "ie-regional" *
  • "road (default)" *
*/ @@ -1427,11 +1451,18 @@ public class OpenMapTilesSchema { public static final String NETWORK_US_HIGHWAY = "us-highway"; public static final String NETWORK_US_STATE = "us-state"; public static final String NETWORK_CA_TRANSCANADA = "ca-transcanada"; + public static final String NETWORK_CA_PROVINCIAL_ARTERIAL = "ca-provincial-arterial"; + public static final String NETWORK_CA_PROVINCIAL = "ca-provincial"; public static final String NETWORK_GB_MOTORWAY = "gb-motorway"; public static final String NETWORK_GB_TRUNK = "gb-trunk"; + public static final String NETWORK_GB_PRIMARY = "gb-primary"; + public static final String NETWORK_IE_MOTORWAY = "ie-motorway"; + public static final String NETWORK_IE_NATIONAL = "ie-national"; + public static final String NETWORK_IE_REGIONAL = "ie-regional"; public static final String NETWORK_ROAD = "road"; public static final Set NETWORK_VALUES = - Set.of("us-interstate", "us-highway", "us-state", "ca-transcanada", "gb-motorway", "gb-trunk", "road"); + Set.of("us-interstate", "us-highway", "us-state", "ca-transcanada", "ca-provincial-arterial", "ca-provincial", + "gb-motorway", "gb-trunk", "gb-primary", "ie-motorway", "ie-national", "ie-regional", "road"); public static final String CLASS_MOTORWAY = "motorway"; public static final String CLASS_TRUNK = "trunk"; public static final String CLASS_PRIMARY = "primary"; @@ -1490,7 +1521,7 @@ public class OpenMapTilesSchema { * create a text hierarchy. * * Generated from - * place.yaml + * place.yaml */ public interface Place extends Layer { double BUFFER_SIZE = 256.0; @@ -1542,6 +1573,7 @@ public class OpenMapTilesSchema { *
  • "town" *
  • "village" *
  • "hamlet" + *
  • "borough" *
  • "suburb" *
  • "quarter" *
  • "neighbourhood" @@ -1579,13 +1611,14 @@ public class OpenMapTilesSchema { public static final String CLASS_TOWN = "town"; public static final String CLASS_VILLAGE = "village"; public static final String CLASS_HAMLET = "hamlet"; + public static final String CLASS_BOROUGH = "borough"; public static final String CLASS_SUBURB = "suburb"; public static final String CLASS_QUARTER = "quarter"; public static final String CLASS_NEIGHBOURHOOD = "neighbourhood"; public static final String CLASS_ISOLATED_DWELLING = "isolated_dwelling"; public static final String CLASS_ISLAND = "island"; public static final Set CLASS_VALUES = Set.of("continent", "country", "state", "province", "city", "town", - "village", "hamlet", "suburb", "quarter", "neighbourhood", "isolated_dwelling", "island"); + "village", "hamlet", "borough", "suburb", "quarter", "neighbourhood", "isolated_dwelling", "island"); } /** Complex mappings to generate attribute values from OSM element tags in the place layer. */ final class FieldMappings { @@ -1595,10 +1628,11 @@ public class OpenMapTilesSchema { /** * Everything in OpenStreetMap which contains a addr:housenumber tag useful for labelling housenumbers on * a map. This adds significant size to z14. For buildings the centroid of the building is used as - * housenumber. + * housenumber. Duplicates within a tile are dropped if they have the same street/block_number (records without name + * tag are prioritized for preservation). * * Generated from housenumber.yaml + * "https://github.com/openmaptiles/openmaptiles/blob/master/layers/housenumber/housenumber.yaml">housenumber.yaml */ public interface Housenumber extends Layer { double BUFFER_SIZE = 8.0; @@ -1611,7 +1645,10 @@ public class OpenMapTilesSchema { /** Attribute names for map elements in the housenumber layer. */ final class Fields { - /** Value of the addr:housenumber tag. */ + /** + * Value of the addr:housenumber tag. If + * there are multiple values separated by semi-colons, the first and last value separated by a dash. + */ public static final String HOUSENUMBER = "housenumber"; } /** Attribute values for map elements in the housenumber layer. */ @@ -1627,7 +1664,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 { double BUFFER_SIZE = 64.0; @@ -1656,6 +1693,7 @@ public class OpenMapTilesSchema { * allowed values: *
      *
    • shop + *
    • office *
    • town_hall *
    • golf *
    • fast_food @@ -1689,6 +1727,7 @@ public class OpenMapTilesSchema { *
    • swimming *
    • castle *
    • atm + *
    • fuel *
    */ public static final String CLASS = "class"; @@ -1751,6 +1790,7 @@ public class OpenMapTilesSchema { /** Attribute values for map elements in the poi layer. */ final class FieldValues { public static final String CLASS_SHOP = "shop"; + public static final String CLASS_OFFICE = "office"; public static final String CLASS_TOWN_HALL = "town_hall"; public static final String CLASS_GOLF = "golf"; public static final String CLASS_FAST_FOOD = "fast_food"; @@ -1784,10 +1824,11 @@ public class OpenMapTilesSchema { public static final String CLASS_SWIMMING = "swimming"; public static final String CLASS_CASTLE = "castle"; public static final String CLASS_ATM = "atm"; - public static final Set CLASS_VALUES = Set.of("shop", "town_hall", "golf", "fast_food", "park", "bus", - "railway", "aerialway", "entrance", "campsite", "laundry", "grocery", "library", "college", "lodging", + public static final String CLASS_FUEL = "fuel"; + public static final Set CLASS_VALUES = Set.of("shop", "office", "town_hall", "golf", "fast_food", "park", + "bus", "railway", "aerialway", "entrance", "campsite", "laundry", "grocery", "library", "college", "lodging", "ice_cream", "post", "cafe", "school", "alcohol_shop", "bar", "harbor", "car", "hospital", "cemetery", - "attraction", "beer", "music", "stadium", "art_gallery", "clothing_store", "swimming", "castle", "atm"); + "attraction", "beer", "music", "stadium", "art_gallery", "clothing_store", "swimming", "castle", "atm", "fuel"); } /** Complex mappings to generate attribute values from OSM element tags in the poi layer. */ final class FieldMappings { @@ -1798,8 +1839,18 @@ public class OpenMapTilesSchema { "erotic", "electronics", "fabric", "florist", "frozen_food", "furniture", "video_games", "video", "general", "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")), + "paint", "perfumery", "perfume", "pet", "photo", "second_hand", "shoes", "sports", "stationery", "tailor", + "tattoo", "ticket", "tobacco", "toys", "travel_agency", "watches", "weapons", "wholesale")), + MultiExpression.entry("office", + matchAny("subclass", "accountant", "advertising_agency", "architect", "association", "bail_bond_agent", + "charity", "company", "construction_company", "consulting", "cooperative", "courier", "coworking", + "diplomatic", "educational_institution", "employment_agency", "energy_supplier", "engineer", "estate_agent", + "financial", "financial_advisor", "forestry", "foundation", "geodesist", "government", "graphic_design", + "guide", "harbour_master", "health_insurance", "insurance", "interior_design", "it", "lawyer", "logistics", + "marketing", "moving_company", "newspaper", "ngo", "notary", "physician", "political_party", + "private_investigator", "property_management", "publisher", "quango", "religion", "research", "security", + "surveyor", "tax_advisor", "taxi", "telecommunication", "therapist", "translator", "travel_agent", + "tutoring", "union", "university", "water_utility", "web_design", "wedding_planner")), MultiExpression.entry("town_hall", matchAny("subclass", "townhall", "public_building", "courthouse", "community_centre")), MultiExpression.entry("golf", matchAny("subclass", "golf", "golf_course", "miniature_golf")), @@ -1839,14 +1890,15 @@ public class OpenMapTilesSchema { MultiExpression.entry("clothing_store", matchAny("subclass", "bag", "clothes")), MultiExpression.entry("swimming", matchAny("subclass", "swimming_area", "swimming")), MultiExpression.entry("castle", matchAny("subclass", "castle", "ruins")), - MultiExpression.entry("atm", matchAny("subclass", "atm")))); + MultiExpression.entry("atm", matchAny("subclass", "atm")), + MultiExpression.entry("fuel", matchAny("subclass", "fuel", "charging_station")))); } } /** * Aerodrome labels * * Generated from aerodrome_label.yaml + * "https://github.com/openmaptiles/openmaptiles/blob/master/layers/aerodrome_label/aerodrome_label.yaml">aerodrome_label.yaml */ public interface AerodromeLabel extends Layer { double BUFFER_SIZE = 64.0; diff --git a/src/main/java/org/openmaptiles/generated/Tables.java b/src/main/java/org/openmaptiles/generated/Tables.java index a29e15e..1d2458e 100644 --- a/src/main/java/org/openmaptiles/generated/Tables.java +++ b/src/main/java/org/openmaptiles/generated/Tables.java @@ -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 @@ -94,21 +94,23 @@ public class Tables { ) {} /** An OSM element that would appear in the {@code osm_water_polygon} table generated by imposm3. */ public record OsmWaterPolygon(@Override String name, @Override String nameEn, @Override String nameDe, - @Override String natural, @Override String landuse, @Override String waterway, @Override String leisure, - @Override String water, @Override boolean isIntermittent, @Override boolean isTunnel, @Override boolean isBridge, - @Override SourceFeature source) implements Row, WithName, WithNameEn, WithNameDe, WithNatural, WithLanduse, - WithWaterway, WithLeisure, WithWater, WithIsIntermittent, WithIsTunnel, WithIsBridge, WithSource { + @Override String place, @Override String natural, @Override String landuse, @Override String waterway, + @Override String leisure, @Override String water, @Override boolean isIntermittent, @Override boolean isTunnel, + @Override boolean isBridge, @Override SourceFeature source) + implements Row, WithName, WithNameEn, WithNameDe, WithPlace, WithNatural, WithLanduse, WithWaterway, WithLeisure, + 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.getString("leisure"), source.getString("water"), source.getBoolean("intermittent"), - source.getBoolean("tunnel"), source.getBoolean("bridge"), source); + source.getString("place"), source.getString("natural"), source.getString("landuse"), + source.getString("waterway"), source.getString("leisure"), 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", "spring"), matchAny("waterway", "dock"), matchAny("water", "river")), + matchAny("natural", "water", "bay", "spring"), matchAny("waterway", "dock"), + matchAny("water", "river", "stream", "canal", "ditch", "drain", "pond", "basin", "wastewater")), not(matchAny("covered", "yes")), matchType("polygon")); /** @@ -246,9 +248,8 @@ public class Tables { } /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ - public static final Expression MAPPING = - and(or(matchAny("leisure", "nature_reserve"), matchAny("boundary", "national_park", "protected_area")), - matchType("polygon")); + public static final Expression MAPPING = and(or(matchAny("leisure", "nature_reserve"), + matchAny("boundary", "national_park", "protected_area", "aboriginal_lands")), matchType("polygon")); /** * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as @@ -316,25 +317,27 @@ public class Tables { } } /** An OSM element that would appear in the {@code osm_highway_linestring} table generated by imposm3. */ - 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 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, WithAccess, WithToll, WithUsage, WithPublicTransport, + public record OsmHighwayLinestring(@Override String highway, @Override String construction, + @Override String tracktype, @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 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, WithTracktype, WithRef, WithNetwork, WithZOrder, WithLayer, + WithLevel, WithIndoor, WithName, WithNameEn, WithNameDe, WithShortName, WithIsTunnel, WithIsBridge, WithIsRamp, + WithIsFord, WithIsOneway, 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"), - source.getString("network"), source.getWayZorder(), source.getLong("layer"), source.getLong("level"), - source.getBoolean("indoor"), source.getString("name"), source.getString("name:en"), source.getString("name:de"), - source.getString("short_name"), source.getBoolean("tunnel"), source.getBoolean("bridge"), - source.getBoolean("ramp"), source.getBoolean("ford"), source.getDirection("oneway"), source.getBoolean("area"), - source.getString("service"), source.getString("access"), source.getBoolean("toll"), source.getString("usage"), + this(source.getString("highway"), source.getString("construction"), source.getString("tracktype"), + source.getString("ref"), source.getString("network"), source.getWayZorder(), source.getLong("layer"), + source.getLong("level"), source.getBoolean("indoor"), source.getString("name"), source.getString("name:en"), + source.getString("name:de"), source.getString("short_name"), source.getBoolean("tunnel"), + source.getBoolean("bridge"), source.getBoolean("ramp"), source.getBoolean("ford"), + source.getDirection("oneway"), source.getBoolean("area"), source.getString("service"), + source.getString("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); @@ -361,18 +364,16 @@ public class Tables { 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, - @Override boolean isBridge, @Override boolean isRamp, @Override boolean isFord, @Override int isOneway, - @Override boolean isArea, @Override String service, @Override String usage, @Override SourceFeature source) - implements Row, WithRailway, WithRef, WithNetwork, WithZOrder, WithLayer, WithLevel, WithIndoor, WithName, - WithNameEn, WithNameDe, WithShortName, WithIsTunnel, WithIsBridge, WithIsRamp, WithIsFord, WithIsOneway, WithIsArea, - WithService, WithUsage, WithSource { + @Override boolean isBridge, @Override boolean isRamp, @Override boolean isFord, @Override boolean isArea, + @Override String service, @Override String usage, @Override SourceFeature source) implements Row, WithRailway, + WithRef, WithNetwork, WithZOrder, WithLayer, WithLevel, WithIndoor, WithName, WithNameEn, WithNameDe, WithShortName, + WithIsTunnel, WithIsBridge, WithIsRamp, WithIsFord, WithIsArea, WithService, WithUsage, WithSource { public OsmRailwayLinestring(SourceFeature source, String mappingKey) { this(source.getString("railway"), source.getString("ref"), source.getString("network"), source.getWayZorder(), source.getLong("layer"), source.getLong("level"), source.getBoolean("indoor"), source.getString("name"), source.getString("name:en"), source.getString("name:de"), source.getString("short_name"), source.getBoolean("tunnel"), source.getBoolean("bridge"), source.getBoolean("ramp"), source.getBoolean("ford"), - source.getDirection("oneway"), source.getBoolean("area"), source.getString("service"), - source.getString("usage"), source); + source.getBoolean("area"), source.getString("service"), source.getString("usage"), source); } /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ @@ -420,16 +421,14 @@ public class Tables { 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, @Override boolean isArea, @Override String service, @Override String usage, - @Override SourceFeature source) + @Override boolean isArea, @Override String service, @Override String usage, @Override SourceFeature source) implements Row, WithShipway, WithZOrder, WithLayer, WithName, WithNameEn, WithNameDe, WithShortName, WithIsTunnel, - WithIsBridge, WithIsRamp, WithIsFord, WithIsOneway, WithIsArea, WithService, WithUsage, WithSource { + WithIsBridge, WithIsRamp, WithIsFord, WithIsArea, WithService, WithUsage, WithSource { public OsmShipwayLinestring(SourceFeature source, String mappingKey) { this(source.getString("route"), source.getWayZorder(), source.getLong("layer"), source.getString("name"), source.getString("name:en"), source.getString("name:de"), source.getString("short_name"), source.getBoolean("tunnel"), source.getBoolean("bridge"), source.getBoolean("ramp"), source.getBoolean("ford"), - source.getDirection("oneway"), source.getBoolean("area"), source.getString("service"), - source.getString("usage"), source); + source.getBoolean("area"), source.getString("service"), source.getString("usage"), source); } /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ @@ -521,16 +520,19 @@ public class Tables { } /** An OSM element that would appear in the {@code osm_marine_point} table generated by imposm3. */ 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 { + @Override String place, @Override String natural, @Override long rank, @Override boolean isIntermittent, + @Override SourceFeature source) + implements Row, WithName, WithNameEn, WithNameDe, WithPlace, WithNatural, WithRank, WithIsIntermittent, WithSource { public OsmMarinePoint(SourceFeature source, String mappingKey) { this(source.getString("name"), source.getString("name:en"), source.getString("name:de"), - source.getString("place"), source.getLong("rank"), source.getBoolean("intermittent"), source); + source.getString("place"), source.getString("natural"), source.getLong("rank"), + source.getBoolean("intermittent"), source); } /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ public static final Expression MAPPING = - and(matchAny("place", "ocean", "sea"), matchField("name"), matchType("point")); + and(or(matchAny("place", "ocean", "sea"), matchAny("natural", "bay", "strait")), matchField("name"), + matchType("point")); /** * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as @@ -656,9 +658,8 @@ public class Tables { } /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ - public static final Expression MAPPING = and( - matchAny("place", "city", "town", "village", "hamlet", "suburb", "quarter", "neighbourhood", "isolated_dwelling"), - matchField("name"), matchType("point")); + public static final Expression MAPPING = and(matchAny("place", "city", "town", "village", "hamlet", "borough", + "suburb", "quarter", "neighbourhood", "isolated_dwelling"), matchField("name"), matchType("point")); /** * Interface for layer implementations to extend to subscribe to OSM elements filtered and parsed as @@ -669,10 +670,12 @@ public class Tables { } } /** An OSM element that would appear in the {@code osm_housenumber_point} table generated by imposm3. */ - public record OsmHousenumberPoint(@Override String housenumber, @Override SourceFeature source) - implements Row, WithHousenumber, WithSource { + public record OsmHousenumberPoint(@Override String housenumber, @Override String street, @Override String blockNumber, + @Override String hasName, @Override SourceFeature source) + implements Row, WithHousenumber, WithStreet, WithBlockNumber, WithHasName, WithSource { public OsmHousenumberPoint(SourceFeature source, String mappingKey) { - this(source.getString("addr:housenumber"), source); + this(source.getString("addr:housenumber"), source.getString("addr:street"), source.getString("addr:block_number"), + source.getString("name"), source); } /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ @@ -708,12 +711,13 @@ public class Tables { /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ public static final Expression MAPPING = and(or(matchAny("aerialway", "station"), matchAny("amenity", "arts_centre", "atm", "bank", "bar", "bbq", "bicycle_parking", "bicycle_rental", "biergarten", - "bus_station", "cafe", "cinema", "clinic", "college", "community_centre", "courthouse", "dentist", "doctors", - "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", "parcel_locker", "post_box", "post_office", "prison", - "pub", "public_building", "recycling", "restaurant", "school", "shelter", "swimming_pool", "taxi", "telephone", - "theatre", "toilets", "townhall", "university", "veterinary", "waste_basket"), + "bus_station", "cafe", "charging_station", "cinema", "clinic", "college", "community_centre", "courthouse", + "dentist", "doctors", "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", "parcel_locker", "post_box", + "post_office", "prison", "pub", "public_building", "recycling", "restaurant", "school", "shelter", + "swimming_pool", "taxi", "telephone", "theatre", "toilets", "townhall", "university", "veterinary", + "waste_basket"), matchAny("barrier", "bollard", "border_control", "cycle_barrier", "gate", "lift_gate", "sally_port", "stile", "toll_booth"), matchAny("building", "dormitory"), matchAny("highway", "bus_stop"), @@ -722,7 +726,15 @@ public class Tables { 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("office", "diplomatic"), + matchAny("office", "accountant", "advertising_agency", "architect", "association", "bail_bond_agent", "charity", + "company", "construction_company", "consulting", "cooperative", "courier", "coworking", "diplomatic", + "educational_institution", "employment_agency", "energy_supplier", "engineer", "estate_agent", "financial", + "financial_advisor", "forestry", "foundation", "geodesist", "government", "graphic_design", "guide", + "harbour_master", "health_insurance", "insurance", "interior_design", "it", "lawyer", "logistics", "marketing", + "moving_company", "newspaper", "ngo", "notary", "physician", "political_party", "private_investigator", + "property_management", "publisher", "quango", "religion", "research", "security", "surveyor", "tax_advisor", + "taxi", "telecommunication", "therapist", "translator", "travel_agent", "tutoring", "union", "university", + "water_utility", "web_design", "wedding_planner"), 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", @@ -731,9 +743,9 @@ public class Tables { "erotic", "fabric", "florist", "frozen_food", "furniture", "garden_centre", "general", "gift", "greengrocer", "hairdresser", "hardware", "hearing_aids", "hifi", "ice_cream", "interior_decoration", "jewelry", "kiosk", "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"), + "newsagent", "optician", "outdoor", "paint", "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", @@ -778,12 +790,13 @@ public class Tables { /** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */ public static final Expression MAPPING = and(or(matchAny("aerialway", "station"), matchAny("amenity", "arts_centre", "atm", "bank", "bar", "bbq", "bicycle_parking", "bicycle_rental", "biergarten", - "bus_station", "cafe", "cinema", "clinic", "college", "community_centre", "courthouse", "dentist", "doctors", - "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", "parcel_locker", "post_box", "post_office", "prison", - "pub", "public_building", "recycling", "restaurant", "school", "shelter", "swimming_pool", "taxi", "telephone", - "theatre", "toilets", "townhall", "university", "veterinary", "waste_basket"), + "bus_station", "cafe", "charging_station", "cinema", "clinic", "college", "community_centre", "courthouse", + "dentist", "doctors", "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", "parcel_locker", "post_box", + "post_office", "prison", "pub", "public_building", "recycling", "restaurant", "school", "shelter", + "swimming_pool", "taxi", "telephone", "theatre", "toilets", "townhall", "university", "veterinary", + "waste_basket"), matchAny("barrier", "bollard", "border_control", "cycle_barrier", "gate", "lift_gate", "sally_port", "stile", "toll_booth"), matchAny("building", "dormitory"), matchAny("highway", "bus_stop"), @@ -792,7 +805,15 @@ public class Tables { 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("office", "diplomatic"), + matchAny("office", "accountant", "advertising_agency", "architect", "association", "bail_bond_agent", "charity", + "company", "construction_company", "consulting", "cooperative", "courier", "coworking", "diplomatic", + "educational_institution", "employment_agency", "energy_supplier", "engineer", "estate_agent", "financial", + "financial_advisor", "forestry", "foundation", "geodesist", "government", "graphic_design", "guide", + "harbour_master", "health_insurance", "insurance", "interior_design", "it", "lawyer", "logistics", "marketing", + "moving_company", "newspaper", "ngo", "notary", "physician", "political_party", "private_investigator", + "property_management", "publisher", "quango", "religion", "research", "security", "surveyor", "tax_advisor", + "taxi", "telecommunication", "therapist", "translator", "travel_agent", "tutoring", "union", "university", + "water_utility", "web_design", "wedding_planner"), 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", @@ -801,9 +822,9 @@ public class Tables { "erotic", "fabric", "florist", "frozen_food", "furniture", "garden_centre", "general", "gift", "greengrocer", "hairdresser", "hardware", "hearing_aids", "hifi", "ice_cream", "interior_decoration", "jewelry", "kiosk", "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"), + "newsagent", "optician", "outdoor", "paint", "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", @@ -885,6 +906,11 @@ public class Tables { String bicycle(); } + /** Rows with a String blockNumber attribute. */ + public interface WithBlockNumber { + String blockNumber(); + } + /** Rows with a String boundary attribute. */ public interface WithBoundary { String boundary(); @@ -965,6 +991,11 @@ public class Tables { String funicular(); } + /** Rows with a String hasName attribute. */ + public interface WithHasName { + String hasName(); + } + /** Rows with a String height attribute. */ public interface WithHeight { String height(); @@ -1225,6 +1256,11 @@ public class Tables { String station(); } + /** Rows with a String street attribute. */ + public interface WithStreet { + String street(); + } + /** Rows with a String subclass attribute. */ public interface WithSubclass { String subclass(); @@ -1245,6 +1281,11 @@ public class Tables { String tourism(); } + /** Rows with a String tracktype attribute. */ + public interface WithTracktype { + String tracktype(); + } + /** Rows with a String uicRef attribute. */ public interface WithUicRef { String uicRef(); diff --git a/src/main/java/org/openmaptiles/layers/Housenumber.java b/src/main/java/org/openmaptiles/layers/Housenumber.java index 6633859..2d53c42 100644 --- a/src/main/java/org/openmaptiles/layers/Housenumber.java +++ b/src/main/java/org/openmaptiles/layers/Housenumber.java @@ -43,9 +43,18 @@ import com.onthegomap.planetiler.config.PlanetilerConfig; import com.onthegomap.planetiler.geo.GeometryException; import com.onthegomap.planetiler.stats.Stats; import com.onthegomap.planetiler.util.Translations; +import java.util.Arrays; +import java.util.Comparator; import java.util.List; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.openmaptiles.generated.OpenMapTilesSchema; import org.openmaptiles.generated.Tables; +import org.openmaptiles.util.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Defines the logic for generating map elements in the {@code housenumber} layer from source features. @@ -59,19 +68,96 @@ public class Housenumber implements Tables.OsmHousenumberPoint.Handler, ForwardingProfile.FeaturePostProcessor { - public Housenumber(Translations translations, PlanetilerConfig config, Stats stats) {} + private static final Logger LOGGER = LoggerFactory.getLogger(Housenumber.class); + private static final String OSM_SEPARATOR = ";"; + private static final String DISPLAY_SEPARATOR = "–"; + private static final Pattern NO_CONVERSION_PATTERN = Pattern.compile("[^0-9;]"); + private static final String TEMP_PARTITION = "_partition"; + private static final String TEMP_HAS_NAME = "_has_name"; + private static final Comparator BY_TEMP_HAS_NAME = Comparator + .comparing(i -> (Boolean) i.attrs().get(TEMP_HAS_NAME), Boolean::compare); + private final Stats stats; + + public Housenumber(Translations translations, PlanetilerConfig config, Stats stats) { + this.stats = stats; + } + + private static String displayHousenumberNonumeric(List numbers) { + return numbers.getFirst() + .concat(DISPLAY_SEPARATOR) + .concat(numbers.getLast()); + } + + protected static String displayHousenumber(String housenumber) { + if (!housenumber.contains(OSM_SEPARATOR)) { + return housenumber; + } + + List numbers = Arrays.stream(housenumber.split(OSM_SEPARATOR)) + .map(String::trim) + .filter(Predicate.not(String::isEmpty)) + .toList(); + if (numbers.isEmpty()) { + // not much to do with strange/invalid entries like "3;" or ";" etc. + return housenumber; + } + + Matcher matcher = NO_CONVERSION_PATTERN.matcher(housenumber); + if (matcher.find()) { + return displayHousenumberNonumeric(numbers); + } + + // numeric display house number + var statistics = numbers.stream() + .collect(Collectors.summarizingLong(Long::parseUnsignedLong)); + return String.valueOf(statistics.getMin()) + .concat(DISPLAY_SEPARATOR) + .concat(String.valueOf(statistics.getMax())); + } @Override public void process(Tables.OsmHousenumberPoint element, FeatureCollector features) { + String housenumber; + try { + housenumber = displayHousenumber(element.housenumber()); + } catch (NumberFormatException e) { + // should not be happening (thanks to NO_CONVERSION_PATTERN) but ... + stats.dataError("housenumber_range"); + LOGGER.warn("Failed to convert housenumber range: {}", element.housenumber()); + housenumber = element.housenumber(); + } + + String partition = Utils.coalesce(element.street(), "") + .concat(Utils.coalesce(element.blockNumber(), "")) + .concat(housenumber); + Boolean hasName = element.hasName() == null ? Boolean.FALSE : !element.hasName().isEmpty(); + features.centroidIfConvex(LAYER_NAME) .setBufferPixels(BUFFER_SIZE) - .setAttr(Fields.HOUSENUMBER, element.housenumber()) + .setAttr(Fields.HOUSENUMBER, housenumber) + .setAttr(TEMP_PARTITION, partition) + .setAttr(TEMP_HAS_NAME, hasName) .setMinZoom(14); } @Override public List postProcess(int zoom, List list) throws GeometryException { + // remove duplicate house numbers, features without name tag are prioritized + var items = list.stream() + .collect(Collectors.groupingBy(f -> f.attrs().get(TEMP_PARTITION))) + .values().stream() + .flatMap( + g -> g.stream().min(BY_TEMP_HAS_NAME).stream() + ) + .toList(); + + // remove temporary attributes + for (var item : items) { + item.attrs().remove(TEMP_HAS_NAME); + item.attrs().remove(TEMP_PARTITION); + } + // reduces the size of some heavy z14 tiles with many repeated housenumber values by 60% or more - return FeatureMerge.mergeMultiPoint(list); + return FeatureMerge.mergeMultiPoint(items); } } diff --git a/src/main/java/org/openmaptiles/layers/Park.java b/src/main/java/org/openmaptiles/layers/Park.java index 6bcc653..026f1f6 100644 --- a/src/main/java/org/openmaptiles/layers/Park.java +++ b/src/main/java/org/openmaptiles/layers/Park.java @@ -91,15 +91,20 @@ public class Park implements @Override public void process(Tables.OsmParkPolygon element, FeatureCollector features) { - String protectionTitle = element.protectionTitle(); - if (protectionTitle != null) { - protectionTitle = protectionTitle.replace(' ', '_').toLowerCase(Locale.ROOT); + String clazz; + if ("aboriginal_lands".equals(element.boundary())) { + clazz = "aboriginal_lands"; + } else { + String protectionTitle = element.protectionTitle(); + if (protectionTitle != null) { + protectionTitle = protectionTitle.replace(' ', '_').toLowerCase(Locale.ROOT); + } + clazz = coalesce( + nullIfEmpty(protectionTitle), + nullIfEmpty(element.boundary()), + nullIfEmpty(element.leisure()) + ); } - String clazz = coalesce( - nullIfEmpty(protectionTitle), - nullIfEmpty(element.boundary()), - nullIfEmpty(element.leisure()) - ); // park shape var outline = features.polygon(LAYER_NAME).setBufferPixels(BUFFER_SIZE) @@ -138,7 +143,7 @@ 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(5, minzoom)); + minzoom = Math.clamp(minzoom, 5, 14); return minzoom; } diff --git a/src/main/java/org/openmaptiles/layers/Place.java b/src/main/java/org/openmaptiles/layers/Place.java index e9e58ed..ed1e522 100644 --- a/src/main/java/org/openmaptiles/layers/Place.java +++ b/src/main/java/org/openmaptiles/layers/Place.java @@ -235,7 +235,7 @@ public class Place implements rank = country.rank; } - rank = Math.max(1, Math.min(6, rank)); + rank = Math.clamp(rank, 1, 6); features.point(LAYER_NAME).setBufferPixels(BUFFER_SIZE) .putAttrs(names) @@ -261,7 +261,7 @@ public class Place implements if (nullOrEmpty(names.get(Fields.NAME_EN))) { names.put(Fields.NAME_EN, state.name); } - int rank = Math.min(6, Math.max(1, state.rank)); + int rank = Math.clamp(state.rank, 1, 6); features.point(LAYER_NAME).setBufferPixels(BUFFER_SIZE) .putAttrs(names) diff --git a/src/main/java/org/openmaptiles/layers/Poi.java b/src/main/java/org/openmaptiles/layers/Poi.java index b12b830..084f583 100644 --- a/src/main/java/org/openmaptiles/layers/Poi.java +++ b/src/main/java/org/openmaptiles/layers/Poi.java @@ -48,14 +48,26 @@ import com.onthegomap.planetiler.VectorTile; import com.onthegomap.planetiler.collection.Hppc; 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.SimpleFeature; import com.onthegomap.planetiler.stats.Stats; import com.onthegomap.planetiler.util.Parse; import com.onthegomap.planetiler.util.Translations; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import org.locationtech.jts.geom.Point; +import org.openmaptiles.OpenMapTilesProfile; import org.openmaptiles.generated.OpenMapTilesSchema; import org.openmaptiles.generated.Tables; import org.openmaptiles.util.OmtLanguageUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Defines the logic for generating map elements for things like shops, parks, and schools in the {@code poi} layer from @@ -68,13 +80,15 @@ public class Poi implements OpenMapTilesSchema.Poi, Tables.OsmPoiPoint.Handler, Tables.OsmPoiPolygon.Handler, - ForwardingProfile.FeaturePostProcessor { + ForwardingProfile.FeaturePostProcessor, + OpenMapTilesProfile.FinishHandler { /* * process() creates the raw POI feature from OSM elements and postProcess() * assigns the feature rank from order in the tile at render-time. */ + private static final Logger LOGGER = LoggerFactory.getLogger(Poi.class); private static final Map CLASS_RANKS = Map.ofEntries( entry(FieldValues.CLASS_HOSPITAL, 20), entry(FieldValues.CLASS_RAILWAY, 40), @@ -99,12 +113,25 @@ public class Poi implements entry(FieldValues.CLASS_CLOTHING_STORE, 700), entry(FieldValues.CLASS_BAR, 800) ); + private static final Set UNIVERSITY_POI_SUBCLASSES = Set.of("university", "college"); + private static final List AGG_STOP_SUBCLASS_ORDER = List.of( + "subway", + "tram_stop", + "bus_station", + "bus_stop" + ); + private static final Comparator BY_SUBCLASS = Comparator + .comparingInt(s -> AGG_STOP_SUBCLASS_ORDER.indexOf(s.subclass())); + private static final Set BRAND_OPERATOR_REF_SUBCLASSES = Set.of("charging_station", "parcel_locker"); private final MultiExpression.Index classMapping; private final Translations translations; + private final Stats stats; + private final Map> aggStops = new HashMap<>(); public Poi(Translations translations, PlanetilerConfig config, Stats stats) { this.classMapping = FieldMappings.Class.index(); this.translations = translations; + this.stats = stats; } static int poiClassRank(String clazz) { @@ -125,19 +152,112 @@ public class Poi implements return lowZoom ? 12 : 14; } + @Override + public void release() { + aggStops.clear(); + } + @Override public void process(Tables.OsmPoiPoint element, FeatureCollector features) { - // TODO handle uic_ref => agg_stop - setupPoiFeature(element, features.point(LAYER_NAME)); + if (element.uicRef() != null && AGG_STOP_SUBCLASS_ORDER.contains(element.subclass())) { + // multiple threads may update this concurrently + String aggStopKey = element.uicRef() + .concat(coalesce(nullIfEmpty(element.name()), "")) + .concat(coalesce(nullIfEmpty(element.network()), "")) + .concat(coalesce(nullIfEmpty(element.operator()), "")); + synchronized (this) { + aggStops.computeIfAbsent(aggStopKey, key -> new ArrayList<>()).add(element); + } + } else { + setupPoiFeature(element, features.point(LAYER_NAME), null); + } + } + + private void processAggStop(Tables.OsmPoiPoint element, FeatureCollector.Factory featureCollectors, + Consumer emit, Integer aggStop) { + try { + var features = featureCollectors.get(SimpleFeature.fromWorldGeometry(element.source().worldGeometry())); + setupPoiFeature(element, features.point(LAYER_NAME), aggStop); + for (var feature : features) { + emit.accept(feature); + } + } catch (GeometryException e) { + e.log(stats, "agg_stop_geometry_2", + "Error getting geometry for the stop " + element.source().id() + " (agg_stop)"); + } + } + + /** + * We've put aside some stops for {@code agg_stop} processing and we do that processing here. + *

    + * The main point is to group together stops with same {@code uid_ref} and then order them first based on subclass + * (see {@code AGG_STOP_ORDER}) and then based on distance from centroid (calculated from all the stops). The first + * one gets {@code agg_stop=1}, the rest will be "normal" (e.g. no {@code agg_stop} attribute). + *

    + * ref: poi_stop_agg.sql + */ + @Override + public void finish(String sourceName, FeatureCollector.Factory featureCollectors, + Consumer emit) { + if (OpenMapTilesProfile.OSM_SOURCE.equals(sourceName)) { + var timer = stats.startStage("agg_stop"); + LOGGER.info("Processing {} agg_stop sets", aggStops.size()); + + for (var aggStopSet : aggStops.values()) { + if (aggStopSet.size() == 1) { + processAggStop(aggStopSet.getFirst(), featureCollectors, emit, 1); + continue; + } + + Tables.OsmPoiPoint nearest = null; + try { + // find most important stops based on subclass + var firstSubclass = aggStopSet.stream().min(BY_SUBCLASS).get().subclass(); + var topAggStops = + aggStopSet.stream().filter(s -> firstSubclass.equals(s.subclass())).toArray(Tables.OsmPoiPoint[]::new); + + // calculate the centroid and ... + List aggStopPoints = new ArrayList<>(aggStopSet.size()); + for (var aggStop : aggStopSet) { + aggStopPoints.add(aggStop.source().worldGeometry().getCentroid()); + } + var aggStopCentroid = GeoUtils.combinePoints(aggStopPoints).getCentroid(); + + // ... find one stop nearest to the centroid + double minDistance = Double.MAX_VALUE; + for (var aggStop : topAggStops) { + double distance = aggStopCentroid.distance(aggStop.source().worldGeometry()); + if (distance < minDistance) { + minDistance = distance; + nearest = aggStop; + } + } + } catch (GeometryException e) { + e.log(stats, "agg_stop_geometry_1", + "Error getting geometry for some of the stops with UIC ref. " + aggStopSet.getFirst().uicRef() + + " (agg_stop)"); + // we're not able to calculate agg_stop, so simply dump the stops as they are + nearest = null; + } + + // now emit the stops + final Tables.OsmPoiPoint nearestFinal = nearest; // final needed for lambda + aggStopSet + .forEach(s -> processAggStop(s, featureCollectors, emit, s == nearestFinal ? 1 : null)); + } + + timer.stop(); + } } @Override public void process(Tables.OsmPoiPolygon element, FeatureCollector features) { - setupPoiFeature(element, features.centroidIfConvex(LAYER_NAME)); + setupPoiFeature(element, features.centroidIfConvex(LAYER_NAME), null); } private void setupPoiFeature( - T element, FeatureCollector.Feature output) { + T element, FeatureCollector.Feature output, Integer aggStop) { String rawSubclass = element.subclass(); if ("station".equals(rawSubclass) && "subway".equals(element.station())) { rawSubclass = "subway"; @@ -157,7 +277,7 @@ public class Poi implements } // Parcel locker without name: use either brand or operator and add ref if present - if ("parcel_locker".equals(rawSubclass) && nullOrEmpty(name)) { + if (BRAND_OPERATOR_REF_SUBCLASSES.contains(rawSubclass) && nullOrEmpty(name)) { name = coalesce(nullIfEmpty(element.brand()), nullIfEmpty(element.operator())); String ref = nullIfEmpty(element.ref()); if (ref != null) { @@ -178,16 +298,24 @@ public class Poi implements int poiClassRank = poiClassRank(poiClass); int rankOrder = poiClassRank + ((nullOrEmpty(name)) ? 2000 : 0); + int minzoom = minzoom(element.subclass(), element.mappingKey()); + if (UNIVERSITY_POI_SUBCLASSES.contains(rawSubclass)) { + // universities that are at least 10% of a tile may appear from Z10 + output.setMinPixelSizeBelowZoom(13, 80); // 80x80px is ~10% of a 256x256px tile + minzoom = 10; + } + output.setBufferPixels(BUFFER_SIZE) .setAttr(Fields.CLASS, poiClass) .setAttr(Fields.SUBCLASS, subclass) .setAttr(Fields.LAYER, nullIfLong(element.layer(), 0)) .setAttr(Fields.LEVEL, Parse.parseLongOrNull(element.source().getTag("level"))) .setAttr(Fields.INDOOR, element.indoor() ? 1 : null) + .setAttr(Fields.AGG_STOP, aggStop) .putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations)) .setPointLabelGridPixelSize(14, 64) .setSortKey(rankOrder) - .setMinZoom(minzoom(element.subclass(), element.mappingKey())); + .setMinZoom(minzoom); } @Override diff --git a/src/main/java/org/openmaptiles/layers/Transportation.java b/src/main/java/org/openmaptiles/layers/Transportation.java index a8cc8e2..4aa898e 100644 --- a/src/main/java/org/openmaptiles/layers/Transportation.java +++ b/src/main/java/org/openmaptiles/layers/Transportation.java @@ -62,6 +62,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; @@ -103,7 +104,8 @@ public class Transportation implements */ 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 Pattern GREAT_BRITAIN_REF_NETWORK_PATTERN = Pattern.compile("^[ABM][0-9ABM()]+"); + private static final Pattern IRELAND_REF_NETWORK_PATTERN = Pattern.compile("^[MNRL][0-9]+"); private static final MultiExpression.Index classMapping = FieldMappings.Class.index(); private static final Set RAILWAY_RAIL_VALUES = Set.of( FieldValues.SUBCLASS_RAIL, @@ -132,11 +134,22 @@ public class Transportation implements ); private static final Set SURFACE_PAVED_VALUES = Set.of( "paved", "asphalt", "cobblestone", "concrete", "concrete:lanes", "concrete:plates", "metal", - "paving_stones", "sett", "unhewn_cobblestone", "wood" + "paving_stones", "sett", "unhewn_cobblestone", "wood", "grade1" ); private static final Set ACCESS_NO_VALUES = Set.of( "private", "no" ); + private static final Set TRUNK_AS_MOTORWAY_BY_NETWORK = Set.of( + RouteNetwork.CA_TRANSCANADA, + RouteNetwork.CA_PROVINCIAL_ARTERIAL, + RouteNetwork.US_INTERSTATE + ); + private static final Set CA_AB_PRIMARY_AS_ARTERIAL_BY_REF = Set.of( + "2", "3", "4" + ); + private static final Set CA_BC_AS_ARTERIAL_BY_REF = Set.of( + "3", "5", "99" + ); private static final ZoomFunction.MeterToPixelThresholds MIN_LENGTH = ZoomFunction.meterThresholds() .put(7, 50) .put(6, 100) @@ -150,13 +163,15 @@ public class Transportation implements .thenComparingInt(r -> r.ref().length()) .thenComparing(RouteRelation::ref); private static final Set ONEWAY_VALUES = Set.of(-1, 1); + private final Map MINZOOMS; private static final String LIMIT_MERGE_TAG = "__limit_merge"; private final AtomicBoolean loggedNoGb = new AtomicBoolean(false); + private final AtomicBoolean loggedNoIreland = new AtomicBoolean(false); private final boolean z13Paths; - private final Map MINZOOMS; private final Stats stats; private final PlanetilerConfig config; private PreparedGeometry greatBritain = null; + private PreparedGeometry ireland = null; public Transportation(Translations translations, PlanetilerConfig config, Stats stats) { this.config = config; @@ -239,16 +254,32 @@ public class Transportation implements @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); + if (!"ne_10m_admin_0_countries".equals(table)) { + return; + } + // multiple threads call this method concurrently, GB (or IE) polygon *should* only be found + // once, but just to be safe synchronize updates to that field + if (feature.hasTag("iso_a2", "GB")) { + try { + var prepared = PreparedGeometryFactory.prepare( + feature.polygon().buffer(GeoUtils.metersToPixelAtEquator(0, 10_000) / 256d) + ); + synchronized (this) { + greatBritain = prepared; } + } catch (GeometryException e) { + LOGGER.error("Failed to get Great Britain Polygon: " + e); + } + } else if (feature.hasTag("iso_a2", "IE")) { + try { + var prepared = PreparedGeometryFactory.prepare( + feature.polygon().buffer(GeoUtils.metersToPixelAtEquator(0, 10_000) / 256d) + ); + synchronized (this) { + ireland = prepared; + } + } catch (GeometryException e) { + LOGGER.error("Failed to get Ireland Polygon: " + e); } } } @@ -268,6 +299,26 @@ public class Transportation implements networkType = RouteNetwork.US_STATE; } else if (network != null && network.startsWith("CA:transcanada")) { networkType = RouteNetwork.CA_TRANSCANADA; + } else if ("CA:QC:A".equals(network)) { + networkType = RouteNetwork.CA_PROVINCIAL_ARTERIAL; + } else if ("CA:ON:primary".equals(network)) { + if (ref != null && ref.length() == 3 && ref.startsWith("4")) { + networkType = RouteNetwork.CA_PROVINCIAL_ARTERIAL; + } else if ("QEW".equals(ref)) { + networkType = RouteNetwork.CA_PROVINCIAL_ARTERIAL; + } else { + networkType = RouteNetwork.CA_PROVINCIAL; + } + } else if ("CA:MB:PTH".equals(network) && "75".equals(ref)) { + networkType = RouteNetwork.CA_PROVINCIAL_ARTERIAL; + } else if ("CA:AB:primary".equals(network) && ref != null && CA_AB_PRIMARY_AS_ARTERIAL_BY_REF.contains(ref)) { + networkType = RouteNetwork.CA_PROVINCIAL_ARTERIAL; + } else if ("CA:BC".equals(network) && ref != null && CA_BC_AS_ARTERIAL_BY_REF.contains(ref)) { + networkType = RouteNetwork.CA_PROVINCIAL_ARTERIAL; + } else if (network != null && ((network.length() == 5 && network.startsWith("CA:")) || + (network.length() >= 6 && network.startsWith("CA:") && network.charAt(5) == ':'))) { + // in SQL: LIKE 'CA:__' OR network LIKE 'CA:__:%'; but wanted to avoid regexp hence more ugly + networkType = RouteNetwork.CA_PROVINCIAL; } int rank = switch (coalesce(network, "")) { @@ -307,12 +358,15 @@ public class Transportation implements 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)); + Transportation.RouteNetwork networkType = switch (element.highway()) { + case "motorway" -> Transportation.RouteNetwork.GB_MOTORWAY; + case "trunk" -> RouteNetwork.GB_TRUNK; + case "primary", "secondary" -> RouteNetwork.GB_PRIMARY; + default -> null; + }; + result.add(new RouteRelation(refMatcher.group(), + networkType == null ? null : networkType.network, + networkType, (byte) -1, 0)); } } catch (GeometryException e) { e.log(stats, "omt_transportation_name_gb_test", @@ -320,6 +374,31 @@ public class Transportation implements } } } + // Similarly Ireland. + refMatcher = IRELAND_REF_NETWORK_PATTERN.matcher(ref); + if (refMatcher.find()) { + if (ireland == null) { + if (!loggedNoIreland.get() && loggedNoIreland.compareAndSet(false, true)) { + LOGGER.warn("No IE polygon for inferring route network types"); + } + } else { + try { + Geometry wayGeometry = element.source().worldGeometry(); + if (ireland.intersects(wayGeometry)) { + String highway = coalesce(element.highway(), ""); + Transportation.RouteNetwork networkType = switch (highway) { + case "motorway" -> Transportation.RouteNetwork.IE_MOTORWAY; + case "trunk", "primary" -> RouteNetwork.IE_NATIONAL; + default -> RouteNetwork.IE_REGIONAL; + }; + result.add(new RouteRelation(refMatcher.group(), networkType.network, networkType, (byte) -1, 0)); + } + } catch (GeometryException e) { + e.log(stats, "omt_transportation_name_ie_test", + "Unable to test highway against IE route network: " + element.source().id()); + } + } + } } Collections.sort(result); return result; @@ -362,13 +441,11 @@ public class Transportation implements .setAttr(Fields.CLASS, highwayClass) .setAttr(Fields.SUBCLASS, highwaySubclass(highwayClass, element.publicTransport(), highway)) .setAttr(Fields.NETWORK, networkType != null ? networkType.name : null) - // TODO: including brunnel at low zooms leads to some large 300-400+kb z4-7 tiles, instead - // we should only set brunnel if the line is above a certain length - .setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord())) + .setAttrWithMinSize(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()), 4, 4, 12) // z8+ .setAttrWithMinzoom(Fields.EXPRESSWAY, element.expressway() && !"motorway".equals(highway) ? 1 : null, 8) // z9+ - .setAttrWithMinzoom(Fields.LAYER, nullIfLong(element.layer(), 0), 9) + .setAttrWithMinSize(Fields.LAYER, nullIfLong(element.layer(), 0), 4, 9, 12) .setAttrWithMinzoom(Fields.BICYCLE, nullIfEmpty(element.bicycle()), 9) .setAttrWithMinzoom(Fields.FOOT, nullIfEmpty(element.foot()), 9) .setAttrWithMinzoom(Fields.HORSE, nullIfEmpty(element.horse()), 9) @@ -381,7 +458,7 @@ public class Transportation implements // z12+ .setAttrWithMinzoom(Fields.SERVICE, service, 12) .setAttrWithMinzoom(Fields.ONEWAY, nullIfInt(element.isOneway(), 0), 12) - .setAttrWithMinzoom(Fields.SURFACE, surface(element.surface()), 12) + .setAttrWithMinzoom(Fields.SURFACE, surface(coalesce(element.surface(), element.tracktype())), 12) .setMinPixelSize(0) // merge during post-processing, then limit by size .setSortKey(element.zOrder()) .setMinZoom(minzoom); @@ -415,6 +492,14 @@ public class Transportation implements 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; + case FieldValues.CLASS_TRUNK -> { + // trunks in some networks to have same min. zoom as highway = "motorway" + String clazz = routeRelations.stream() + .map(RouteRelation::networkType) + .filter(Objects::nonNull) + .anyMatch(TRUNK_AS_MOTORWAY_BY_NETWORK::contains) ? FieldValues.CLASS_MOTORWAY : FieldValues.CLASS_TRUNK; + yield MINZOOMS.getOrDefault(clazz, Integer.MAX_VALUE); + } default -> MINZOOMS.getOrDefault(baseClass, Integer.MAX_VALUE); }; } @@ -463,7 +548,6 @@ public class Transportation implements .setAttr(Fields.CLASS, clazz) .setAttr(Fields.SUBCLASS, railway) .setAttr(Fields.SERVICE, service(service)) - .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, nullIfLong(element.layer(), 0), 9) @@ -494,13 +578,13 @@ public class Transportation implements .setAttr(Fields.CLASS, element.shipway()) // "ferry" // no subclass .setAttr(Fields.SERVICE, service(element.service())) - .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, nullIfLong(element.layer(), 0)) .setSortKey(element.zOrder()) .setMinPixelSize(0) // merge during post-processing, then limit by size - .setMinZoom(11); + .setMinZoom(4) + .setMinPixelSizeBelowZoom(10, 32); // `sql_filter: ST_Length(...)` used in OpenMapTiles translates to 32px } @Override @@ -548,17 +632,25 @@ public class Transportation implements 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"); + US_INTERSTATE("us-interstate", null), + US_HIGHWAY("us-highway", null), + US_STATE("us-state", null), + CA_TRANSCANADA("ca-transcanada", null), + CA_PROVINCIAL_ARTERIAL("ca-provincial-arterial", null), + CA_PROVINCIAL("ca-provincial", null), + GB_MOTORWAY("gb-motorway", "omt-gb-motorway"), + GB_TRUNK("gb-trunk", "omt-gb-trunk"), + GB_PRIMARY("gb-primary", "omt-gb-primary"), + IE_MOTORWAY("ie-motorway", "omt-ie-motorway"), + IE_NATIONAL("ie-national", "omt-ie-national"), + IE_REGIONAL("ie-regional", "omt-ie-regional"); final String name; + final String network; - RouteNetwork(String name) { + RouteNetwork(String name, String network) { this.name = name; + this.network = network; } } diff --git a/src/main/java/org/openmaptiles/layers/TransportationName.java b/src/main/java/org/openmaptiles/layers/TransportationName.java index 27372a3..4e8fefd 100644 --- a/src/main/java/org/openmaptiles/layers/TransportationName.java +++ b/src/main/java/org/openmaptiles/layers/TransportationName.java @@ -111,8 +111,8 @@ public class TransportationName implements .put(7, 20_000) .put(8, 14_000) .put(9, 8_000) - .put(10, 8_000) - .put(11, 8_000); + .put(10, 4_000) + .put(11, 2_000); private final boolean brunnel; private final boolean sizeForShield; private final boolean limitMerge; @@ -279,7 +279,9 @@ public class TransportationName implements } if (brunnel) { - feature.setAttr(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord())); + // from OMT: "Drop brunnel if length of way < 2% of tile width (less than 3 pixels)" + feature.setAttrWithMinSize(Fields.BRUNNEL, brunnel(element.isBridge(), element.isTunnel(), element.isFord()), + 3, 4, 12); } /* diff --git a/src/main/java/org/openmaptiles/layers/WaterName.java b/src/main/java/org/openmaptiles/layers/WaterName.java index 2bc19fc..cebaeff 100644 --- a/src/main/java/org/openmaptiles/layers/WaterName.java +++ b/src/main/java/org/openmaptiles/layers/WaterName.java @@ -35,6 +35,7 @@ See https://github.com/openmaptiles/openmaptiles/blob/master/LICENSE.md for deta */ package org.openmaptiles.layers; +import static org.openmaptiles.util.Utils.coalesce; import static org.openmaptiles.util.Utils.nullIfEmpty; import com.carrotsearch.hppc.LongObjectMap; @@ -48,6 +49,7 @@ import com.onthegomap.planetiler.stats.Stats; import com.onthegomap.planetiler.util.Parse; import com.onthegomap.planetiler.util.Translations; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentSkipListMap; import org.locationtech.jts.geom.Geometry; import org.openmaptiles.OpenMapTilesProfile; @@ -80,9 +82,7 @@ public class WaterName implements */ private static final Logger LOGGER = LoggerFactory.getLogger(WaterName.class); - private static final double WORLD_AREA_FOR_70K_SQUARE_METERS = - Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2); - private static final double LOG2 = Math.log(2); + private static final Set SEA_OR_OCEAN_PLACE = Set.of("sea", "ocean"); private final Translations translations; // need to synchronize updates from multiple threads private final LongObjectMap lakeCenterlines = Hppc.newLongObjectHashMap(); @@ -141,7 +141,10 @@ public class WaterName implements @Override public void process(Tables.OsmMarinePoint element, FeatureCollector features) { if (!element.name().isBlank()) { - String place = element.place(); + String clazz = coalesce( + nullIfEmpty(element.natural()), + nullIfEmpty(element.place()) + ); var source = element.source(); // use name from OSM, but get min zoom from natural earth based on fuzzy name match... Integer rank = Parse.parseIntOrNull(source.getTag("rank")); @@ -159,11 +162,25 @@ public class WaterName implements rank = next.getValue(); } } - int minZoom = "ocean".equals(place) ? 0 : rank != null ? rank : 8; + int minZoom; + if ("ocean".equals(element.place())) { + minZoom = 0; + } else if (rank != null) { + // FIXME: While this looks like matching properly stuff in https://github.com/openmaptiles/openmaptiles/pull/1457/files#diff-201daa1c61c99073fe3280d440c9feca5ed2236b251ad454caa14cc203f952d1R74 , + // it includes not just https://www.openstreetmap.org/relation/13360255 but also https://www.openstreetmap.org/node/1385157299 (and some others). + // Hence check how that OpenMapTiles code works for "James Bay" and: + // a) if same as here then, fix there and then here + // b) if OK (while here NOK), fix only here + minZoom = rank; + } else if ("bay".equals(element.natural())) { + minZoom = 13; + } else { + minZoom = 8; + } features.point(LAYER_NAME) .setBufferPixels(BUFFER_SIZE) .putAttrs(OmtLanguageUtils.getNames(source.tags(), translations)) - .setAttr(Fields.CLASS, place) + .setAttr(Fields.CLASS, clazz) .setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0) .setMinZoom(minZoom); } @@ -172,31 +189,35 @@ public class WaterName implements @Override public void process(Tables.OsmWaterPolygon element, FeatureCollector features) { if (nullIfEmpty(element.name()) != null) { - try { - Geometry centerlineGeometry = lakeCenterlines.get(element.source().id()); - FeatureCollector.Feature feature; - int minzoom = 9; - if (centerlineGeometry != null) { - // prefer lake centerline if it exists - feature = features.geometry(LAYER_NAME, centerlineGeometry) - .setMinPixelSizeBelowZoom(13, 6d * element.name().length()); - } else { - // otherwise just use a label point inside the lake - feature = features.pointOnSurface(LAYER_NAME); - Geometry geometry = element.source().worldGeometry(); - double area = geometry.getArea(); - minzoom = (int) Math.floor(20 - Math.log(area / WORLD_AREA_FOR_70K_SQUARE_METERS) / LOG2); - minzoom = Math.min(14, Math.max(9, minzoom)); - } - feature - .setAttr(Fields.CLASS, FieldValues.CLASS_LAKE) - .setBufferPixels(BUFFER_SIZE) - .putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations)) - .setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0) - .setMinZoom(minzoom); - } catch (GeometryException e) { - e.log(stats, "omt_water_polygon", "Unable to get geometry for water polygon " + element.source().id()); + Geometry centerlineGeometry = lakeCenterlines.get(element.source().id()); + FeatureCollector.Feature feature; + int minzoom = 9; + String place = element.place(); + String clazz; + if ("bay".equals(element.natural())) { + clazz = FieldValues.CLASS_BAY; + } else if ("sea".equals(place)) { + clazz = FieldValues.CLASS_SEA; + } else { + clazz = FieldValues.CLASS_LAKE; + minzoom = 3; } + if (centerlineGeometry != null) { + // prefer lake centerline if it exists + feature = features.geometry(LAYER_NAME, centerlineGeometry) + .setMinPixelSizeBelowZoom(13, 6d * element.name().length()); + } else { + // otherwise just use a label point inside the lake + feature = features.pointOnSurface(LAYER_NAME) + .setMinZoom(place != null && SEA_OR_OCEAN_PLACE.contains(place) ? 0 : 3) + .setMinPixelSize(128); // tiles are 256x256, so 128x128 is 1/4 of a tile + } + feature + .setAttr(Fields.CLASS, clazz) + .setBufferPixels(BUFFER_SIZE) + .putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations)) + .setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0) + .setMinZoom(minzoom); } } } diff --git a/src/main/java/org/openmaptiles/util/Utils.java b/src/main/java/org/openmaptiles/util/Utils.java index de9f4bf..993ca09 100644 --- a/src/main/java/org/openmaptiles/util/Utils.java +++ b/src/main/java/org/openmaptiles/util/Utils.java @@ -74,5 +74,4 @@ public class Utils { public static String brunnel(boolean isBridge, boolean isTunnel, boolean isFord) { return isBridge ? "bridge" : isTunnel ? "tunnel" : isFord ? "ford" : null; } - } diff --git a/src/test/java/org/openmaptiles/OpenMapTilesTest.java b/src/test/java/org/openmaptiles/OpenMapTilesTest.java index 44d428d..187fccf 100644 --- a/src/test/java/org/openmaptiles/OpenMapTilesTest.java +++ b/src/test/java/org/openmaptiles/OpenMapTilesTest.java @@ -137,7 +137,7 @@ class OpenMapTilesTest { assertFeatureNear(mbtiles, "housenumber", Map.of( "housenumber", "27" ), 7.42117, 43.73652, 14, 14); - assertNumFeatures("housenumber", Map.of(), 14, 274, Point.class); + assertNumFeatures("housenumber", Map.of(), 14, 231, Point.class); } @Test diff --git a/src/test/java/org/openmaptiles/layers/AbstractLayerTest.java b/src/test/java/org/openmaptiles/layers/AbstractLayerTest.java index 05e94d9..e3f649e 100644 --- a/src/test/java/org/openmaptiles/layers/AbstractLayerTest.java +++ b/src/test/java/org/openmaptiles/layers/AbstractLayerTest.java @@ -137,6 +137,16 @@ public abstract class AbstractLayerTest { ); } + SourceFeature lineFeatureWithLength(double length, Map props) { + return SimpleFeature.create( + GeoUtils.worldToLatLonCoords(newLineString(0, 0, 0, length)), + new HashMap<>(props), + OpenMapTilesProfile.OSM_SOURCE, + null, + 0 + ); + } + SourceFeature closedWayFeature(Map props) { return SimpleFeature.createFakeOsmFeature( newLineString(0, 0, 1, 0, 1, 1, 0, 1, 0, 0), diff --git a/src/test/java/org/openmaptiles/layers/HousenumberTest.java b/src/test/java/org/openmaptiles/layers/HousenumberTest.java index 6ead5e9..2fa3129 100644 --- a/src/test/java/org/openmaptiles/layers/HousenumberTest.java +++ b/src/test/java/org/openmaptiles/layers/HousenumberTest.java @@ -1,8 +1,14 @@ package org.openmaptiles.layers; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.onthegomap.planetiler.geo.GeometryException; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; class HousenumberTest extends AbstractLayerTest { @@ -27,4 +33,127 @@ class HousenumberTest extends AbstractLayerTest { "addr:housenumber", "10" )))); } + + @ParameterizedTest + @CsvSource({ + "1, 1", + "1;1a;2;2/b;20;3, 1–3", + "1;1a;2;2/b;20;3;, 1–3", + "1;2;20;3, 1–20", + "1;2;20;3;, 1–20", + ";, ;", + ";;, ;;", + "2712;935803935803, 2712–935803935803", + }) + void testDisplayHousenumber(String outlier, String expected) { + assertEquals(expected, Housenumber.displayHousenumber(outlier)); + } + + @Test + void testTempAttrs() { + assertFeatures(14, List.of(Map.of( + "_has_name", Boolean.TRUE, + "_partition", "streetX765/6" + )), process(polygonFeature(Map.of( + "addr:housenumber", "765/6", + "addr:block_number", "X", + "addr:street", "street", + "name", "name" + )))); + } + + @Test + void testNonduplicateHousenumber() throws GeometryException { + var layerName = Housenumber.LAYER_NAME; + var hn1 = pointFeature( + layerName, + Map.of( + "housenumber", "764/2", + "_partition", "764/2" + ), + 1 + ); + var hn2 = pointFeature( + layerName, + Map.of( + "housenumber", "765/6", + "_partition", "765/6" + ), + 1 + ); + + Assertions.assertEquals( + 2, + profile.postProcessLayerFeatures(layerName, 14, List.of(hn1, hn2)).size() + ); + } + + @Test + void testNonduplicateStreet() throws GeometryException { + var layerName = Housenumber.LAYER_NAME; + var housenumber = "765/6"; + var hn1 = pointFeature( + layerName, + Map.of( + "housenumber", housenumber, + "_partition", "street 1" + housenumber + ), + 1 + ); + var hn2 = pointFeature( + layerName, + Map.of( + "housenumber", housenumber, + "_partition", "street 2" + housenumber + ), + 1 + ); + + var result = profile.postProcessLayerFeatures(layerName, 14, List.of(hn1, hn2)); + + Assertions.assertEquals( + 1, // same housenumber => two points merged into one multipoint + result.size() + ); + Assertions.assertEquals( + 5, // two point in multipoint => 5 commands + result.getFirst().geometry().commands().length); + } + + @Test + void testDuplicateHousenumber() throws GeometryException { + var layerName = Housenumber.LAYER_NAME; + var housenumber = "765/6"; + var hn1 = pointFeature( + layerName, + Map.of( + "housenumber", housenumber + " (no name)", + "_has_name", false, + "_partition", housenumber + ), + 1 + ); + var hn2 = pointFeature( + layerName, + Map.of( + "housenumber", housenumber + " (with name)", + "_has_name", true, + "_partition", housenumber + ), + 1 + ); + + var result = profile.postProcessLayerFeatures(layerName, 14, List.of(hn1, hn2)); + + Assertions.assertEquals(List.of( + pointFeature( + layerName, + Map.of("housenumber", "765/6 (no name)"), + 1 + ) + ), result); + Assertions.assertEquals( + 3, // only one point in multipoint => 3 commands + result.getFirst().geometry().commands().length); + } } diff --git a/src/test/java/org/openmaptiles/layers/ParkTest.java b/src/test/java/org/openmaptiles/layers/ParkTest.java index a393b21..2383a13 100644 --- a/src/test/java/org/openmaptiles/layers/ParkTest.java +++ b/src/test/java/org/openmaptiles/layers/ParkTest.java @@ -45,6 +45,30 @@ class ParkTest extends AbstractLayerTest { )))); } + @Test + void testAbotiginalLand() { + assertFeatures(13, List.of(Map.of( + "_layer", "park", + "_type", "polygon", + "class", "aboriginal_lands", + "name", "Hualapai Tribe", + "_minpixelsize", 2d, + "_minzoom", 4, + "_maxzoom", 14 + ), Map.of( + "_layer", "park", + "_type", "point", + "class", "aboriginal_lands", + "name", "Hualapai Tribe", + "_minzoom", 5, + "_maxzoom", 14 + )), process(polygonFeature(Map.of( + "boundary", "aboriginal_lands", + "name", "Hualapai Tribe", + "protection_title", "National Park" + )))); + } + @Test void testSmallerPark() { double z11area = Math.pow((GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d), 2) * Math.pow(2, 20 - 11); diff --git a/src/test/java/org/openmaptiles/layers/PoiTest.java b/src/test/java/org/openmaptiles/layers/PoiTest.java index 11a9a70..9027cf5 100644 --- a/src/test/java/org/openmaptiles/layers/PoiTest.java +++ b/src/test/java/org/openmaptiles/layers/PoiTest.java @@ -1,13 +1,19 @@ package org.openmaptiles.layers; +import static com.onthegomap.planetiler.TestUtils.newPoint; + +import com.onthegomap.planetiler.FeatureCollector; import com.onthegomap.planetiler.geo.GeometryException; +import com.onthegomap.planetiler.reader.SimpleFeature; import com.onthegomap.planetiler.reader.SourceFeature; +import java.util.ArrayList; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.openmaptiles.OpenMapTilesProfile; class PoiTest extends AbstractLayerTest { @@ -63,6 +69,167 @@ class PoiTest extends AbstractLayerTest { )))); } + private List testAggStops(List sourceFeatures) { + sourceFeatures.forEach(this::process); + + List features = new ArrayList<>(); + profile.finish(OpenMapTilesProfile.OSM_SOURCE, featureCollectorFactory, features::add); + + return features; + } + + @Test + void testAggStopJustOne() { + var result = testAggStops(List.of(pointFeature(Map.of( + "highway", "bus_stop", + "name", "station", + "uic_ref", "1" + )))); + assertFeatures(14, List.of(Map.of( + "_layer", "poi", + "class", "bus", + "subclass", "bus_stop", + "agg_stop", 1, + "_minzoom", 14 + )), result); + } + + @Test + void testAggStopTwoWithSameSubclass() { + var result = testAggStops(List.of( + pointFeature(Map.of( + "railway", "tram_stop", + "name", "station", + "name:es", "test 1", + "uic_ref", "1" + )), + pointFeature(Map.of( + "railway", "tram_stop", + "name", "station", + "name:es", "test 2", + "uic_ref", "1" + )) + )); + assertFeatures(14, List.of( + Map.of( + "_layer", "poi", + "name:es", "test 1", + "class", "railway", + "subclass", "tram_stop", + "agg_stop", 1, + "_minzoom", 14 + ), + Map.of( + "_layer", "poi", + "name:es", "test 2", + "class", "railway", + "subclass", "tram_stop", + "agg_stop", "", + "_minzoom", 14 + ) + ), result); + } + + @Test + void testAggStopThreeWithMixedSubclass() { + var result = testAggStops(List.of( + pointFeature(Map.of( + "highway", "bus_stop", + "name", "station", + "name:es", "test 1", + "uic_ref", "1" + )), + pointFeature(Map.of( + "highway", "bus_stop", + "name", "station", + "name:es", "test 2", + "uic_ref", "1" + )), + pointFeature(Map.of( + "railway", "tram_stop", + "name", "station", + "name:es", "test 3", + "uic_ref", "1" + )) + )); + assertFeatures(14, List.of( + Map.of( + "_layer", "poi", + "name:es", "test 1", + "class", "bus", + "subclass", "bus_stop", + "agg_stop", "", + "_minzoom", 14 + ), + Map.of( + "_layer", "poi", + "name:es", "test 2", + "class", "bus", + "subclass", "bus_stop", + "agg_stop", "", + "_minzoom", 14 + ), + Map.of( + "_layer", "poi", + "name:es", "test 3", + "class", "railway", + "subclass", "tram_stop", + "agg_stop", 1, + "_minzoom", 14 + ) + ), result); + } + + @Test + void testAggStopThreeWithSameSubclass() { + var result = testAggStops(List.of( + SimpleFeature.create(newPoint(0, 0), Map.of( + "highway", "bus_stop", + "name", "station", + "name:es", "test 1", + "uic_ref", "1" + ), OpenMapTilesProfile.OSM_SOURCE, null, 0), + SimpleFeature.create(newPoint(1, 0), Map.of( + "highway", "bus_stop", + "name", "station", + "name:es", "test 2", + "uic_ref", "1" + ), OpenMapTilesProfile.OSM_SOURCE, null, 1), + SimpleFeature.create(newPoint(2, 0), Map.of( + "highway", "bus_stop", + "name", "station", + "name:es", "test 3", + "uic_ref", "1" + ), OpenMapTilesProfile.OSM_SOURCE, null, 2) + )); + assertFeatures(14, List.of( + Map.of( + "_layer", "poi", + "name:es", "test 1", + "class", "bus", + "subclass", "bus_stop", + "agg_stop", "", + "_minzoom", 14 + ), + Map.of( + "_layer", "poi", + "name:es", "test 2", + "class", "bus", + "subclass", "bus_stop", + "agg_stop", 1, + "_minzoom", 14 + ), + Map.of( + "_layer", "poi", + "name:es", "test 3", + "class", "bus", + "subclass", "bus_stop", + "agg_stop", "", + "_minzoom", 14 + ) + ), result); + } + @ParameterizedTest @ValueSource(booleans = {false, true}) void testPlaceOfWorshipFromReligionTag(boolean area) { @@ -185,7 +352,7 @@ class PoiTest extends AbstractLayerTest { void testEmbassy() { assertFeatures(7, List.of(Map.of( "_layer", "poi", - "class", "diplomatic", + "class", "office", "subclass", "diplomatic", "name", "The Embassy" )), process(pointFeature(Map.of( @@ -274,4 +441,28 @@ class PoiTest extends AbstractLayerTest { "ref", "Corner Case" )))); } + + @Test + void testChargingStation() { + List> expected = List.of(Map.of( + "_layer", "poi", + "class", "fuel", + "subclass", "charging_station", + "name", "Some Charging Station Operator" + )); + assertFeatures(14, expected, process(pointFeature(Map.of( + "amenity", "charging_station", + "brand", "Some Charging Station Operator" + )))); + assertFeatures(14, expected, process(pointFeature(Map.of( + "amenity", "charging_station", + "operator", "Some Charging Station Operator" + )))); + assertFeatures(14, expected, process(pointFeature(Map.of( + "amenity", "charging_station", + "operator", "Some Charging Station", + "ref", "Operator" + )))); + } + } diff --git a/src/test/java/org/openmaptiles/layers/TransportationTest.java b/src/test/java/org/openmaptiles/layers/TransportationTest.java index e3fda66..fe32a98 100644 --- a/src/test/java/org/openmaptiles/layers/TransportationTest.java +++ b/src/test/java/org/openmaptiles/layers/TransportationTest.java @@ -3,6 +3,7 @@ package org.openmaptiles.layers; import static com.onthegomap.planetiler.TestUtils.newLineString; import static com.onthegomap.planetiler.TestUtils.newPoint; import static com.onthegomap.planetiler.TestUtils.rectangle; +import static org.junit.jupiter.api.Assertions.assertFalse; import com.onthegomap.planetiler.FeatureCollector; import com.onthegomap.planetiler.config.Arguments; @@ -17,6 +18,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Stream; +import java.util.stream.StreamSupport; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -267,31 +269,6 @@ 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", @@ -341,6 +318,51 @@ class TransportationTest extends AbstractLayerTest { )), features); } + @Test + void testDuplicateRoute() { + var rel1 = new OsmElement.Relation(1); + rel1.setTag("type", "route"); + rel1.setTag("route", "road"); + rel1.setTag("network", "US:OK"); + rel1.setTag("ref", "104"); + rel1.setTag("direction", "north"); + var rel2 = new OsmElement.Relation(2); + rel2.setTag("type", "route"); + rel2.setTag("route", "road"); + rel2.setTag("network", "US:OK"); + rel2.setTag("ref", "104"); + rel2.setTag("direction", "south"); + + FeatureCollector features = process(lineFeatureWithRelation( + Stream.concat( + profile.preprocessOsmRelation(rel2).stream(), + profile.preprocessOsmRelation(rel1).stream() + ).toList(), + Map.of( + "highway", "trunk", + "ref", "US 23;SR 104", + "lanes", 5, + "maxspeed", "55 mph", + "expressway", "no" + ))); + + assertFeatures(13, List.of(mapOf( + "_layer", "transportation", + "class", "trunk", + "network", "us-state", + "_minzoom", 5 + ), Map.of( + "_layer", "transportation_name", + "class", "trunk", + "ref", "104", + "ref_length", 3, + "network", "us-state", + "route_1", "US:OK=104", + "route_2", "", + "_minzoom", 8 + )), features); + } + @Test void testRouteWithoutNetworkType() { var rel1 = new OsmElement.Relation(1); @@ -951,6 +973,316 @@ class TransportationTest extends AbstractLayerTest { )), features); } + @Test + void testTransCanadaTrunk() { + var rel = new OsmElement.Relation(1); + rel.setTag("type", "route"); + rel.setTag("route", "road"); + rel.setTag("network", "CA:transcanada:namedRoute"); + + FeatureCollector features = process(lineFeatureWithRelation( + profile.preprocessOsmRelation(rel), + Map.of( + "highway", "trunk" + ))); + + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "trunk", + "_minzoom", 4 + )), features); + } + + @Test + void testTransCanadaProvincialCaQcA() { + var rel = new OsmElement.Relation(1); + rel.setTag("type", "route"); + rel.setTag("route", "road"); + rel.setTag("network", "CA:QC:A"); + + FeatureCollector features = process(lineFeatureWithRelation( + profile.preprocessOsmRelation(rel), + Map.of( + "highway", "trunk" + ))); + + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "trunk", + "network", "ca-provincial-arterial", + "_minzoom", 4 + )), features); + } + + @Test + void testTransCanadaProvincialCaOnPrimaryRef4xx() { + var rel = new OsmElement.Relation(1); + rel.setTag("type", "route"); + rel.setTag("route", "road"); + rel.setTag("network", "CA:ON:primary"); + rel.setTag("ref", "420"); + + FeatureCollector features = process(lineFeatureWithRelation( + profile.preprocessOsmRelation(rel), + Map.of( + "highway", "trunk" + ))); + + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "trunk", + "network", "ca-provincial-arterial", + "_minzoom", 4 + ), Map.of( + "_layer", "transportation_name", + "class", "trunk", + "ref", "420", + "network", "ca-provincial-arterial" + )), features); + } + + @Test + void testTransCanadaProvincialCaOnPrimaryRefQew() { + var rel = new OsmElement.Relation(1); + rel.setTag("type", "route"); + rel.setTag("route", "road"); + rel.setTag("network", "CA:ON:primary"); + rel.setTag("ref", "QEW"); + + FeatureCollector features = process(lineFeatureWithRelation( + profile.preprocessOsmRelation(rel), + Map.of( + "highway", "trunk" + ))); + + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "trunk", + "network", "ca-provincial-arterial", + "_minzoom", 4 + ), Map.of( + "_layer", "transportation_name", + "class", "trunk", + "ref", "QEW", + "network", "ca-provincial-arterial" + )), features); + } + + @Test + void testTransCanadaProvincialCaOnPrimaryRefOther() { + var rel = new OsmElement.Relation(1); + rel.setTag("type", "route"); + rel.setTag("route", "road"); + rel.setTag("network", "CA:ON:primary"); + rel.setTag("ref", "85"); + + FeatureCollector features = process(lineFeatureWithRelation( + profile.preprocessOsmRelation(rel), + Map.of( + "highway", "trunk" + ))); + + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "trunk", + "network", "ca-provincial", + "_minzoom", 5 + ), Map.of( + "_layer", "transportation_name", + "class", "trunk", + "ref", "85", + "network", "ca-provincial" + )), features); + } + + @Test + void testTransCanadaProvincialCaMbPthRef75() { + var rel = new OsmElement.Relation(1); + rel.setTag("type", "route"); + rel.setTag("route", "road"); + rel.setTag("network", "CA:MB:PTH"); + rel.setTag("ref", "75"); + + FeatureCollector features = process(lineFeatureWithRelation( + profile.preprocessOsmRelation(rel), + Map.of( + "highway", "trunk" + ))); + + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "trunk", + "network", "ca-provincial-arterial", + "_minzoom", 4 + ), Map.of( + "_layer", "transportation_name", + "class", "trunk", + "ref", "75", + "network", "ca-provincial-arterial" + )), features); + } + + @Test + void testTransCanadaProvincialCaMbPthRefOther() { + var rel = new OsmElement.Relation(1); + rel.setTag("type", "route"); + rel.setTag("route", "road"); + rel.setTag("network", "CA:MB:PTH"); + rel.setTag("ref", "77"); + + FeatureCollector features = process(lineFeatureWithRelation( + profile.preprocessOsmRelation(rel), + Map.of( + "highway", "trunk" + ))); + + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "trunk", + "network", "ca-provincial", + "_minzoom", 5 + ), Map.of( + "_layer", "transportation_name", + "class", "trunk", + "ref", "77", + "network", "ca-provincial" + )), features); + } + + @Test + void testTransCanadaProvincialCaAbPrimaryRef3() { + var rel = new OsmElement.Relation(1); + rel.setTag("type", "route"); + rel.setTag("route", "road"); + rel.setTag("network", "CA:AB:primary"); + rel.setTag("ref", "3"); + + FeatureCollector features = process(lineFeatureWithRelation( + profile.preprocessOsmRelation(rel), + Map.of( + "highway", "trunk" + ))); + + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "trunk", + "network", "ca-provincial-arterial", + "_minzoom", 4 + ), Map.of( + "_layer", "transportation_name", + "class", "trunk", + "ref", "3", + "network", "ca-provincial-arterial" + )), features); + } + + @Test + void testTransCanadaProvincialCaAbPrimaryRefOther() { + var rel = new OsmElement.Relation(1); + rel.setTag("type", "route"); + rel.setTag("route", "road"); + rel.setTag("network", "CA:AB:primary"); + rel.setTag("ref", "10"); + + FeatureCollector features = process(lineFeatureWithRelation( + profile.preprocessOsmRelation(rel), + Map.of( + "highway", "trunk" + ))); + + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "trunk", + "network", "ca-provincial", + "_minzoom", 5 + ), Map.of( + "_layer", "transportation_name", + "class", "trunk", + "ref", "10", + "network", "ca-provincial" + )), features); + } + + @Test + void testTransCanadaProvincialCaBcRef3() { + var rel = new OsmElement.Relation(1); + rel.setTag("type", "route"); + rel.setTag("route", "road"); + rel.setTag("network", "CA:BC"); + rel.setTag("ref", "3"); + + FeatureCollector features = process(lineFeatureWithRelation( + profile.preprocessOsmRelation(rel), + Map.of( + "highway", "trunk" + ))); + + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "trunk", + "network", "ca-provincial-arterial", + "_minzoom", 4 + ), Map.of( + "_layer", "transportation_name", + "class", "trunk", + "ref", "3", + "network", "ca-provincial-arterial" + )), features); + } + + @Test + void testTransCanadaProvincialCaBcRefOther() { + var rel = new OsmElement.Relation(1); + rel.setTag("type", "route"); + rel.setTag("route", "road"); + rel.setTag("network", "CA:BC"); + rel.setTag("ref", "10"); + + FeatureCollector features = process(lineFeatureWithRelation( + profile.preprocessOsmRelation(rel), + Map.of( + "highway", "trunk" + ))); + + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "trunk", + "network", "ca-provincial", + "_minzoom", 5 + ), Map.of( + "_layer", "transportation_name", + "class", "trunk", + "ref", "10", + "network", "ca-provincial" + )), features); + } + + @Test + void testTransCanadaProvincialCaOther() { + var rel = new OsmElement.Relation(1); + rel.setTag("type", "route"); + rel.setTag("route", "road"); + rel.setTag("network", "CA:yellowhead"); + + FeatureCollector features = process(lineFeatureWithRelation( + profile.preprocessOsmRelation(rel), + Map.of( + "highway", "trunk" + ))); + + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "trunk", + "_minzoom", 5 + )), features); + boolean caProvPresent = StreamSupport.stream(features.spliterator(), false) + .flatMap(f -> f.getAttrsAtZoom(13).entrySet().stream()) + .filter(e -> "network".equals(e.getKey())) + .map(Map.Entry::getValue) + .anyMatch(v -> "ca-provincial".equals(v) || "ca-provincial-arterial".equals(v)); + assertFalse(caProvPresent, "ca-provincial present"); + } + @Test void testGreatBritainHighway() { process(SimpleFeature.create( @@ -1014,6 +1346,307 @@ class TransportationTest extends AbstractLayerTest { ))); } + @Test + void testGreatBritainTrunk() { + process(SimpleFeature.create( + rectangle(0, 0.1), + Map.of("iso_a2", "GB"), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_countries", + 0 + )); + + // in GB + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "trunk", + "_minzoom", 5 + ), Map.of( + "_layer", "transportation_name", + "class", "trunk", + "ref", "A272", + "ref_length", 4, + "network", "gb-trunk", + "_minzoom", 8 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "highway", "trunk", + "ref", "A272" + ), + OpenMapTilesProfile.OSM_SOURCE, + null, + 0 + ))); + } + + @Test + void testGreatBritainPrimary() { + process(SimpleFeature.create( + rectangle(0, 0.1), + Map.of("iso_a2", "GB"), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_countries", + 0 + )); + + // in GB + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "primary", + "_minzoom", 7 + ), Map.of( + "_layer", "transportation_name", + "class", "primary", + "ref", "A598", + "ref_length", 4, + "network", "gb-primary", + "_minzoom", 12 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "highway", "primary", + "ref", "A598" + ), + OpenMapTilesProfile.OSM_SOURCE, + null, + 0 + ))); + } + + @Test + void testGreatBritainSecondary() { + process(SimpleFeature.create( + rectangle(0, 0.1), + Map.of("iso_a2", "GB"), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_countries", + 0 + )); + + // in GB + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "secondary", + "_minzoom", 9 + ), Map.of( + "_layer", "transportation_name", + "class", "secondary", + "ref", "B4558", + "ref_length", 5, + "network", "gb-primary", + "_minzoom", 12 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "highway", "secondary", + "ref", "B4558" + ), + OpenMapTilesProfile.OSM_SOURCE, + null, + 0 + ))); + } + + @Test + void testGreatBritainTertiary() { + process(SimpleFeature.create( + rectangle(0, 0.1), + Map.of("iso_a2", "GB"), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_countries", + 0 + )); + + // in GB + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "tertiary", + "_minzoom", 11 + ), Map.of( + "_layer", "transportation_name", + "class", "tertiary", + "ref", "B4086", + "ref_length", 5, + "network", "road", + "_minzoom", 12 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "highway", "tertiary", + "ref", "B4086" + ), + OpenMapTilesProfile.OSM_SOURCE, + null, + 0 + ))); + } + + @Test + void testIrelandHighway() { + process(SimpleFeature.create( + rectangle(0, 0.1), + Map.of("iso_a2", "IE"), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_countries", + 0 + )); + + // in IE + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "motorway", + "oneway", 1, + "ramp", "", + "_minzoom", 4 + ), Map.of( + "_layer", "transportation_name", + "class", "motorway", + "ref", "M18", + "ref_length", 3, + "network", "ie-motorway", + "_minzoom", 6 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "highway", "motorway", + "oneway", "yes", + "ref", "M18" + ), + OpenMapTilesProfile.OSM_SOURCE, + null, + 0 + ))); + + // not in IE + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "motorway", + "oneway", 1, + "ramp", "", + "_minzoom", 4 + ), Map.of( + "_layer", "transportation_name", + "class", "motorway", + "ref", "M18", + "ref_length", 3, + "network", "road", + "_minzoom", 6 + )), process(SimpleFeature.create( + newLineString(1, 0, 0, 1), + Map.of( + "highway", "motorway", + "oneway", "yes", + "ref", "M18" + ), + OpenMapTilesProfile.OSM_SOURCE, + null, + 0 + ))); + } + + @Test + void testIrelandTrunk() { + process(SimpleFeature.create( + rectangle(0, 0.1), + Map.of("iso_a2", "IE"), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_countries", + 0 + )); + + // in IE + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "trunk", + "_minzoom", 5 + ), Map.of( + "_layer", "transportation_name", + "class", "trunk", + "ref", "N8", + "ref_length", 2, + "network", "ie-national", + "_minzoom", 8 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "highway", "trunk", + "ref", "N8" + ), + OpenMapTilesProfile.OSM_SOURCE, + null, + 0 + ))); + } + + @Test + void testIrelandPrimary() { + process(SimpleFeature.create( + rectangle(0, 0.1), + Map.of("iso_a2", "IE"), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_countries", + 0 + )); + + // in IE + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "primary", + "_minzoom", 7 + ), Map.of( + "_layer", "transportation_name", + "class", "primary", + "ref", "N59", + "ref_length", 3, + "network", "ie-national", + "_minzoom", 12 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "highway", "primary", + "ref", "N59" + ), + OpenMapTilesProfile.OSM_SOURCE, + null, + 0 + ))); + } + + @Test + void testIrelandSecondary() { + process(SimpleFeature.create( + rectangle(0, 0.1), + Map.of("iso_a2", "IE"), + OpenMapTilesProfile.NATURAL_EARTH_SOURCE, + "ne_10m_admin_0_countries", + 0 + )); + + // in IE + assertFeatures(13, List.of(Map.of( + "_layer", "transportation", + "class", "secondary", + "_minzoom", 9 + ), Map.of( + "_layer", "transportation_name", + "class", "secondary", + "ref", "R813", + "ref_length", 4, + "network", "ie-regional", + "_minzoom", 12 + )), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + Map.of( + "highway", "secondary", + "ref", "R813" + ), + OpenMapTilesProfile.OSM_SOURCE, + null, + 0 + ))); + } + @Test void testMergesDisconnectedRoadNameFeatures() throws GeometryException { testMergesLinestrings(Map.of("class", "motorway"), TransportationName.LAYER_NAME, 10, 14); @@ -1176,8 +1809,9 @@ class TransportationTest extends AbstractLayerTest { "_layer", "transportation", "class", "ferry", - "_minzoom", 11, + "_minzoom", 4, "_maxzoom", 14, + "_minpixelsize", 32d, "_type", "line" ), Map.of( "_layer", "transportation_name", @@ -1358,6 +1992,30 @@ class TransportationTest extends AbstractLayerTest { )))); } + @Test + void testGrade1SurfacePath() { + assertFeatures(14, List.of(Map.of( + "_layer", "transportation", + "class", "track", + "surface", "paved" + )), process(lineFeature(Map.of( + "surface", "grade1", + "highway", "track" + )))); + } + + @Test + void testGrade1TracktypePath() { + assertFeatures(14, List.of(Map.of( + "_layer", "transportation", + "class", "track", + "surface", "paved" + )), process(lineFeature(Map.of( + "tracktype", "grade1", + "highway", "track" + )))); + } + @Test void testIssue58() { // test subject: https://www.openstreetmap.org/way/222564359 diff --git a/src/test/java/org/openmaptiles/layers/WaterNameTest.java b/src/test/java/org/openmaptiles/layers/WaterNameTest.java index 54e0b00..02b7c98 100644 --- a/src/test/java/org/openmaptiles/layers/WaterNameTest.java +++ b/src/test/java/org/openmaptiles/layers/WaterNameTest.java @@ -27,7 +27,7 @@ class WaterNameTest extends AbstractLayerTest { "_layer", "water_name", "_type", "point", - "_minzoom", 9, + "_minzoom", 3, "_maxzoom", 14 )), process(polygonFeatureWithArea(1, Map.of( "name", "waterway", @@ -36,19 +36,6 @@ class WaterNameTest extends AbstractLayerTest { "water", "pond", "intermittent", "1" )))); - double z11area = Math.pow((GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d), 2) * Math.pow(2, 20 - 11); - assertFeatures(10, List.of(Map.of( - "_layer", "water" - ), Map.of( - "_layer", "water_name", - "_type", "point", - "_minzoom", 11, - "_maxzoom", 14 - )), process(polygonFeatureWithArea(z11area, Map.of( - "name", "waterway", - "natural", "water", - "water", "pond" - )))); } @Test @@ -71,7 +58,7 @@ class WaterNameTest extends AbstractLayerTest { "_layer", "water_name", "_type", "line", "_geom", new TestUtils.NormGeometry(GeoUtils.latLonToWorldCoords(newLineString(0, 0, 1, 1))), - "_minzoom", 9, + "_minzoom", 3, "_maxzoom", 14, "_minpixelsize", "waterway".length() * 6d )), process(SimpleFeature.create( @@ -121,7 +108,7 @@ class WaterNameTest extends AbstractLayerTest { newLineString(0, 0, 1, 1), newLineString(2, 2, 3, 3) }))), - "_minzoom", 9, + "_minzoom", 3, "_maxzoom", 14, "_minpixelsize", "waterway".length() * 6d )), process(SimpleFeature.create( @@ -138,6 +125,40 @@ class WaterNameTest extends AbstractLayerTest { ))); } + @Test + void testWaterNameBay() { + assertFeatures(11, List.of(), process(SimpleFeature.create( + newLineString(0, 0, 1, 1), + new HashMap<>(Map.of( + "OSM_ID", -10 + )), + OpenMapTilesProfile.LAKE_CENTERLINE_SOURCE, + null, + 0 + ))); + assertFeatures(10, List.of(Map.of( + "name", "bay", + "name:es", "bay es", + + "_layer", "water_name", + "_type", "line", + "_geom", new TestUtils.NormGeometry(GeoUtils.latLonToWorldCoords(newLineString(0, 0, 1, 1))), + "_minzoom", 9, + "_maxzoom", 14, + "_minpixelsize", "bay".length() * 6d + )), process(SimpleFeature.create( + GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))), + new HashMap<>(Map.of( + "name", "bay", + "name:es", "bay es", + "natural", "bay" + )), + OpenMapTilesProfile.OSM_SOURCE, + null, + 10 + ))); + } + @Test void testMarinePoint() { assertFeatures(11, List.of(), process(SimpleFeature.create(