diff --git a/pom.xml b/pom.xml index b21b1a22..b6e6f104 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/src/main/java/org/openmaptiles/generated/OpenMapTilesSchema.java b/src/main/java/org/openmaptiles/generated/OpenMapTilesSchema.java index 8ae534a5..55ec4e54 100644 --- a/src/main/java/org/openmaptiles/generated/OpenMapTilesSchema.java +++ b/src/main/java/org/openmaptiles/generated/OpenMapTilesSchema.java @@ -49,8 +49,8 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE /** * All vector tile layer definitions, attributes, and allowed values generated from the - * OpenMapTiles vector tile schema - * 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 static List createInstances(Translations translations, PlanetilerC * boundaries show up. So you might not be able to use border styling for ocean water features. * * Generated from - * water.yaml + * water.yaml */ public interface Water extends Layer { double BUFFER_SIZE = 4.0; @@ -117,11 +117,15 @@ final class Fields { /** * 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 @@ final class Fields { *

    *
  • dock *
  • river + *
  • pond *
  • lake *
  • ocean *
  • swimming_pool @@ -163,10 +168,11 @@ final class Fields { 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 @@ final class FieldValues { 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 @@ final class FieldMappings { * field applied. Waterways do not have a subclass field. * * Generated from - * waterway.yaml + * waterway.yaml */ public interface Waterway extends Layer { double BUFFER_SIZE = 4.0; @@ -273,7 +280,7 @@ final class FieldMappings { * layer is to style wood (class=wood) and grass (class=grass) areas. * * Generated from landcover.yaml + * "https://github.com/openmaptiles/openmaptiles/blob/master/layers/landcover/landcover.yaml">landcover.yaml */ public interface Landcover extends Layer { double BUFFER_SIZE = 4.0; @@ -430,7 +437,7 @@ final class FieldMappings { * residential (urban) areas and at higher zoom levels mostly OSM landuse tags. * * Generated from - * landuse.yaml + * landuse.yaml */ public interface Landuse extends Layer { double BUFFER_SIZE = 4.0; @@ -526,7 +533,7 @@ final class FieldMappings { * 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 @@ final class FieldMappings { } } /** - * 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 @@ default String name() { /** 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 @@ final class FieldMappings { * but for most styles it makes sense to just style admin_level=2 and admin_level=4. * * Generated from - * boundary.yaml + * boundary.yaml */ public interface Boundary extends Layer { double BUFFER_SIZE = 4.0; @@ -762,7 +774,7 @@ final class FieldMappings { * in the aeroway layer. * * Generated from - * aeroway.yaml + * aeroway.yaml */ public interface Aeroway extends Layer { double BUFFER_SIZE = 4.0; @@ -822,7 +834,7 @@ final class FieldMappings { * 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 @@ final class Fields { * 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 @@ final class FieldMappings { * location:underground are excluded. * * Generated from - * building.yaml + * building.yaml */ public interface Building extends Layer { double BUFFER_SIZE = 4.0; @@ -1208,7 +1221,7 @@ final class FieldMappings { * from OSM water bodies. Only the most important lakes contain labels. * * Generated from water_name.yaml + * "https://github.com/openmaptiles/openmaptiles/blob/master/layers/water_name/water_name.yaml">water_name.yaml */ public interface WaterName extends Layer { double BUFFER_SIZE = 256.0; @@ -1231,11 +1244,14 @@ final class Fields { 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 @@ final class Fields { /** 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 @@ final class FieldMappings { * 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 @@ final class Fields { *
  • "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 @@ final class FieldValues { 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 @@ final class FieldMappings { * create a text hierarchy. * * Generated from - * place.yaml + * place.yaml */ public interface Place extends Layer { double BUFFER_SIZE = 256.0; @@ -1542,6 +1573,7 @@ final class Fields { *
  • "town" *
  • "village" *
  • "hamlet" + *
  • "borough" *
  • "suburb" *
  • "quarter" *
  • "neighbourhood" @@ -1579,13 +1611,14 @@ final class FieldValues { 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 @@ final class FieldMappings { /** * 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 @@ default String name() { /** 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 @@ final class FieldMappings { * Points of interests containing a of a variety * of OpenStreetMap tags. Mostly contains amenities, sport, shop and tourist POIs. * - * Generated from poi.yaml + * Generated from poi.yaml */ public interface Poi extends Layer { double BUFFER_SIZE = 64.0; @@ -1798,8 +1835,8 @@ final class FieldMappings { "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("town_hall", matchAny("subclass", "townhall", "public_building", "courthouse", "community_centre")), MultiExpression.entry("golf", matchAny("subclass", "golf", "golf_course", "miniature_golf")), @@ -1846,7 +1883,7 @@ final class FieldMappings { * 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 a29e15e7..5effebba 100644 --- a/src/main/java/org/openmaptiles/generated/Tables.java +++ b/src/main/java/org/openmaptiles/generated/Tables.java @@ -50,7 +50,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE /** * OSM element parsers generated from the imposm3 table definitions - * in the OpenMapTiles vector tile + * in the OpenMapTiles vector tile * schema. * * These filter and parse the raw OSM key/value attribute pairs on tags into records with fields that match the columns @@ -94,21 +94,23 @@ public record RowHandlerAndClass ( ) {} /** 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 OsmParkPolygon(SourceFeature source, String mappingKey) { } /** 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 interface Handler { } } /** 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); @@ -521,16 +524,19 @@ public interface Handler { } /** 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 +662,8 @@ public OsmCityPoint(SourceFeature source, String mappingKey) { } /** 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 +674,12 @@ public interface Handler { } } /** 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". */ @@ -731,9 +738,9 @@ public OsmPoiPoint(SourceFeature source, String mappingKey) { "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", @@ -801,9 +808,9 @@ public OsmPoiPolygon(SourceFeature source, String mappingKey) { "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 +892,11 @@ public interface WithBicycle { 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 +977,11 @@ public interface WithFunicular { 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 +1242,11 @@ public interface WithStation { 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 +1267,11 @@ public interface WithTourism { 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/Poi.java b/src/main/java/org/openmaptiles/layers/Poi.java index b12b8307..6885de18 100644 --- a/src/main/java/org/openmaptiles/layers/Poi.java +++ b/src/main/java/org/openmaptiles/layers/Poi.java @@ -48,14 +48,27 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 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.Geometry; +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 +81,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 +114,26 @@ 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 Map AGG_STOP_SUBCLASS_ORDER = Map.ofEntries( + entry("subway", 0), + entry("tram_stop", 1), + entry("bus_station", 2), + entry("bus_stop", 3) + ); + private static final Comparator BY_SUBCLASS = Comparator + .comparingInt(s -> AGG_STOP_SUBCLASS_ORDER.get(s.subclass())); + private static final double LOG2 = Math.log(2); + private static final double SQRT10 = Math.sqrt(10); 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 +154,140 @@ private int minzoom(String subclass, String mappingKey) { return lowZoom ? 12 : 14; } + public static int uniAreaToMinZoom(double areaWorld) { + double oneSideWorld = Math.sqrt(areaWorld); + // full(-er) formula (along with comments) is in PoiTest.testUniAreaToMinZoom(), here is simplified reverse of that + double zoom = -(Math.log(oneSideWorld * SQRT10) / LOG2); + + // Say Z13.01 means bellow threshold, Z13.00 is exactly threshold, Z12.99 is over threshold, + // hence Z13.01 and Z13.00 will be rounded to Z14 and Z12.99 to Z13 (e.g. `floor() + 1`). + // And to accommodate for some precision errors (observed for Z9-Z11) we do also `- 0.1e-10`. + int result = (int) Math.floor(zoom - 0.1e-10) + 1; + + return Math.min(14, Math.max(10, result)); + } + + @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.containsKey(element.subclass())) { + // multiple threads may update this concurrently + synchronized (this) { + aggStops.computeIfAbsent(element.uicRef(), 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()); + + // possible TODO: run in parallel? + for (var aggStopSet : aggStops.values()) { + if (aggStopSet.size() == 1) { + processAggStop(aggStopSet.get(0), featureCollectors, emit, 1); + continue; + } + try { + // find first based on subclass + var firstSubclass = aggStopSet.stream().sorted(BY_SUBCLASS).toArray(Tables.OsmPoiPoint[]::new)[0].subclass(); + var topAggStops = + aggStopSet.stream().filter(s -> firstSubclass.equals(s.subclass())).toArray(Tables.OsmPoiPoint[]::new); + if (topAggStops.length <= 2) { + // one found => straightforward: flag it and emit + // two found => both will be same distance from centroid: pick first one, flag it and emit + processAggStop(topAggStops[0], featureCollectors, emit, 1); + // and emit the rest + aggStopSet.stream() + .filter(s -> !topAggStops[0].equals(s)) + .forEach(s -> processAggStop(s, featureCollectors, emit, null)); + continue; + } + + // easy cases handled, now we need also centroid + List aggStopPoints = new ArrayList<>(aggStopSet.size()); + for (var aggStop : aggStopSet) { + aggStopPoints.add(aggStop.source().worldGeometry().getCentroid()); + } + var aggStopCentroid = GeoUtils.combinePoints(aggStopPoints).getCentroid(); + + // find nearest + double minDistance = Double.MAX_VALUE; + Tables.OsmPoiPoint nearest = null; + for (var aggStop : topAggStops) { + double distance = aggStopCentroid.distance(aggStop.source().worldGeometry()); + if (distance < minDistance) { + minDistance = distance; + nearest = aggStop; + } + } + + // emit nearest + if (nearest != null) { + processAggStop(nearest, featureCollectors, emit, 1); + } else { + stats.dataError("agg_stop_nearest"); + LOGGER.warn("Failed to find nearest stop for agg_stop UIC ref. {}", aggStopSet.get(0).uicRef()); + } + + // emit the rest + final var alreadyDone = nearest; + aggStopSet.stream() + .filter(s -> !s.equals(alreadyDone)) + .forEach(s -> processAggStop(s, featureCollectors, emit, null)); + } catch (GeometryException e) { + e.log(stats, "agg_stop_geometry_1", + "Error getting geometry for some of the stops with UIC ref. " + aggStopSet.get(0).uicRef() + " (agg_stop)"); + // we're not able to calculate agg_stop, so simply dump them as-is + aggStopSet + .forEach(s -> processAggStop(s, featureCollectors, emit, 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"; @@ -178,16 +328,29 @@ private 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.toString(), + RouteNetwork.CA_PROVINCIAL_ARTERIAL.toString(), + RouteNetwork.US_INTERSTATE.toString() + ); + 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) @@ -152,11 +165,13 @@ public class Transportation implements private static final Set ONEWAY_VALUES = Set.of(-1, 1); 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,9 +254,12 @@ private static boolean isBridgeOrPier(String manMade) { @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 + 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")) { synchronized (this) { try { Geometry boundary = feature.polygon().buffer(GeoUtils.metersToPixelAtEquator(0, 10_000) / 256d); @@ -250,6 +268,15 @@ public void processNaturalEarth(String table, SourceFeature feature, LOGGER.error("Failed to get Great Britain Polygon: " + e); } } + } else if (feature.hasTag("iso_a2", "IE")) { + synchronized (this) { + try { + Geometry boundary = feature.polygon().buffer(GeoUtils.metersToPixelAtEquator(0, 10_000) / 256d); + ireland = PreparedGeometryFactory.prepare(boundary); + } catch (GeometryException e) { + LOGGER.error("Failed to get Great Britain Polygon: " + e); + } + } } } @@ -268,6 +295,26 @@ public List preprocessOsmRelation(OsmElement.Relation relation) 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,10 +354,26 @@ List getRouteRelations(Tables.OsmHighwayLinestring element) { 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"; + Transportation.RouteNetwork networkType; + String network; + switch (element.highway()) { + case "motorway" -> { + networkType = Transportation.RouteNetwork.GB_MOTORWAY; + network = "omt-gb-motorway"; + } + case "trunk" -> { + networkType = RouteNetwork.GB_TRUNK; + network = "omt-gb-trunk"; + } + case "primary", "secondary" -> { + networkType = RouteNetwork.GB_PRIMARY; + network = "omt-gb-primary"; + } + default -> { + networkType = null; + network = null; + } + } result.add(new RouteRelation(refMatcher.group(), network, networkType, (byte) -1, 0)); } @@ -320,6 +383,43 @@ List getRouteRelations(Tables.OsmHighwayLinestring element) { } } } + // 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)) { + Transportation.RouteNetwork networkType; + String network; + String highway = coalesce(element.highway(), ""); + switch (highway) { + case "motorway" -> { + networkType = Transportation.RouteNetwork.IE_MOTORWAY; + network = "omt-ie-motorway"; + } + case "trunk", "primary" -> { + networkType = RouteNetwork.IE_NATIONAL; + network = "omt-ie-national"; + } + default -> { + networkType = RouteNetwork.IE_REGIONAL; + network = "omt-ie-regional"; + } + } + result.add(new RouteRelation(refMatcher.group(), 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; @@ -381,7 +481,7 @@ public void process(Tables.OsmHighwayLinestring element, FeatureCollector featur // 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 +515,15 @@ int getMinzoom(Tables.OsmHighwayLinestring element, String highwayClass) { 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) + .map(Enum::toString) + .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); }; } @@ -552,8 +661,14 @@ enum RouteNetwork { US_HIGHWAY("us-highway"), US_STATE("us-state"), CA_TRANSCANADA("ca-transcanada"), + CA_PROVINCIAL_ARTERIAL("ca-provincial-arterial"), + CA_PROVINCIAL("ca-provincial"), GB_MOTORWAY("gb-motorway"), - GB_TRUNK("gb-trunk"); + GB_TRUNK("gb-trunk"), + GB_PRIMARY("gb-primary"), + IE_MOTORWAY("ie-motorway"), + IE_NATIONAL("ie-national"), + IE_REGIONAL("ie-regional"); final String name; diff --git a/src/main/java/org/openmaptiles/layers/WaterName.java b/src/main/java/org/openmaptiles/layers/WaterName.java index 2bc19fc9..18621048 100644 --- a/src/main/java/org/openmaptiles/layers/WaterName.java +++ b/src/main/java/org/openmaptiles/layers/WaterName.java @@ -35,6 +35,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE */ 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 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 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,8 @@ 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 +142,10 @@ public void processNaturalEarth(String table, SourceFeature feature, FeatureColl @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 +163,20 @@ public void process(Tables.OsmMarinePoint element, FeatureCollector features) { 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) { + 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); } @@ -176,6 +189,16 @@ public void process(Tables.OsmWaterPolygon element, FeatureCollector features) { 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) @@ -183,13 +206,15 @@ public void process(Tables.OsmWaterPolygon element, FeatureCollector features) { } 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)); + double area = element.source().area(); + if (place != null && SEA_OR_OCEAN_PLACE.contains(place)) { + minzoom = 0; + } else { + minzoom = areaToMinZoom(area); + } } feature - .setAttr(Fields.CLASS, FieldValues.CLASS_LAKE) + .setAttr(Fields.CLASS, clazz) .setBufferPixels(BUFFER_SIZE) .putAttrs(OmtLanguageUtils.getNames(element.source().tags(), translations)) .setAttr(Fields.INTERMITTENT, element.isIntermittent() ? 1 : 0) @@ -199,4 +224,17 @@ public void process(Tables.OsmWaterPolygon element, FeatureCollector features) { } } } + + public static int areaToMinZoom(double areaWorld) { + double oneSideWorld = Math.sqrt(areaWorld); + // full(-er) formula (along with comments) is in WaterNameTest.testAreaToMinZoom(), here is simplified reverse of that + double zoom = -(Math.log(oneSideWorld) / LOG2) - 1; + + // Say Z13.01 means bellow threshold, Z13.00 is exactly threshold, Z12.99 is over threshold, + // hence Z13.01 and Z13.00 will be rounded to Z14 and Z12.99 to Z13 (e.g. `floor() + 1`). + // And to accommodate for some precision errors (observed for Z9-Z11) we do also `- 0.1e-11`. + int result = (int) Math.floor(zoom - 0.1e-11) + 1; + + return Math.min(14, Math.max(3, result)); + } } diff --git a/src/test/java/org/openmaptiles/layers/PoiTest.java b/src/test/java/org/openmaptiles/layers/PoiTest.java index 11a9a705..f4e10ced 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,159 @@ void testSubway(boolean area) { )))); } + 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 1", + "uic_ref", "1" + )), + pointFeature(Map.of( + "railway", "tram_stop", + "name", "station 2", + "uic_ref", "1" + )) + )); + assertFeatures(14, List.of( + Map.of( + "_layer", "poi", + "name", "station 1", + "class", "railway", + "subclass", "tram_stop", + "agg_stop", 1, + "_minzoom", 14 + ), + Map.of( + "_layer", "poi", + "name", "station 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 1", + "uic_ref", "1" + )), + pointFeature(Map.of( + "highway", "bus_stop", + "name", "station 2", + "uic_ref", "1" + )), + pointFeature(Map.of( + "railway", "tram_stop", + "name", "station 3", + "uic_ref", "1" + )) + )); + assertFeatures(14, List.of( + Map.of( + "_layer", "poi", + "name", "station 3", + "class", "railway", + "subclass", "tram_stop", + "agg_stop", 1, + "_minzoom", 14 + ), + Map.of( + "_layer", "poi", + "name", "station 1", + "class", "bus", + "subclass", "bus_stop", + "agg_stop", "", + "_minzoom", 14 + ), + Map.of( + "_layer", "poi", + "name", "station 2", + "class", "bus", + "subclass", "bus_stop", + "agg_stop", "", + "_minzoom", 14 + ) + ), result); + } + + @Test + void testAggStopThreeWithSameSubclass() { + var result = testAggStops(List.of( + SimpleFeature.create(newPoint(0, 0), Map.of( + "highway", "bus_stop", + "name", "station 1", + "uic_ref", "1" + ), OpenMapTilesProfile.OSM_SOURCE, null, 0), + SimpleFeature.create(newPoint(1, 0), Map.of( + "highway", "bus_stop", + "name", "station 2", + "uic_ref", "1" + ), OpenMapTilesProfile.OSM_SOURCE, null, 1), + SimpleFeature.create(newPoint(2, 0), Map.of( + "highway", "bus_stop", + "name", "station 3", + "uic_ref", "1" + ), OpenMapTilesProfile.OSM_SOURCE, null, 2) + )); + assertFeatures(14, List.of( + Map.of( + "_layer", "poi", + "name", "station 2", + "class", "bus", + "subclass", "bus_stop", + "agg_stop", 1, + "_minzoom", 14 + ), + Map.of( + "_layer", "poi", + "name", "station 1", + "class", "bus", + "subclass", "bus_stop", + "agg_stop", "", + "_minzoom", 14 + ), + Map.of( + "_layer", "poi", + "name", "station 3", + "class", "bus", + "subclass", "bus_stop", + "agg_stop", "", + "_minzoom", 14 + ) + ), result); + } + @ParameterizedTest @ValueSource(booleans = {false, true}) void testPlaceOfWorshipFromReligionTag(boolean area) { @@ -274,4 +433,56 @@ void testParcelLockerCornerCase() { "ref", "Corner Case" )))); } + + private record TestEntry( + SourceFeature feature, + int expectedZoom + ) {} + + private void createUniAreaForMinZoomTest(List testEntries, double side, int expectedZoom, String name) { + double area = Math.pow(side, 2); + var feature = polygonFeatureWithArea(area, Map.of( + "name", name, + "amenity", "university" + )); + testEntries.add(new TestEntry( + feature, + Math.min(14, Math.max(10, expectedZoom)) + )); + } + + @Test + void testUniAreaToMinZoom() throws GeometryException { + // threshold is 1/10 of tile area, hence ... + // ... side is 1/sqrt(10) tile side: from pixels to world coord, for say Z14 ... + //final double PORTION_OF_TILE_SIDE = (256d / Math.sqrt(10)) / Math.pow(2d, 14d + 8d); + // ... and then for some lower zoom: + //double testAreaSide = PORTION_OF_TILE_SIDE * Math.pow(2, 14 - zoom); + // all this then simplified to `testAreaSide` calculation bellow + + final double SQRT10 = Math.sqrt(10); + final List testEntries = new ArrayList<>(); + for (int zoom = 14; zoom >= 0; zoom--) { + double testAreaSide = Math.pow(2, -zoom) / SQRT10; + + // slightly bellow the threshold + createUniAreaForMinZoomTest(testEntries, testAreaSide * 0.999, zoom + 1, "uni-"); + // precisely at the threshold + createUniAreaForMinZoomTest(testEntries, testAreaSide, zoom, "uni="); + // slightly over the threshold + createUniAreaForMinZoomTest(testEntries, testAreaSide * 1.001, zoom, "uni+"); + } + + for (var entry : testEntries) { + assertFeatures(14, List.of(Map.of( + "_layer", "landuse", + "class", "university" + ), Map.of( + "_layer", "poi", + "_type", "point", + "_minzoom", entry.expectedZoom, + "_maxzoom", 14 + )), process(entry.feature)); + } + } } diff --git a/src/test/java/org/openmaptiles/layers/TransportationTest.java b/src/test/java/org/openmaptiles/layers/TransportationTest.java index 5f1e6636..943c10aa 100644 --- a/src/test/java/org/openmaptiles/layers/TransportationTest.java +++ b/src/test/java/org/openmaptiles/layers/TransportationTest.java @@ -3,6 +3,7 @@ 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; @@ -15,6 +16,7 @@ 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; @@ -909,6 +911,316 @@ void testTransCanadaHighway() { )), 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( @@ -972,6 +1284,307 @@ void testGreatBritainHighway() { ))); } + @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); @@ -1315,4 +1928,28 @@ void testIssue86() { "service", "driveway" )))); } + + @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" + )))); + } } diff --git a/src/test/java/org/openmaptiles/layers/WaterNameTest.java b/src/test/java/org/openmaptiles/layers/WaterNameTest.java index 54e0b00d..fee396b1 100644 --- a/src/test/java/org/openmaptiles/layers/WaterNameTest.java +++ b/src/test/java/org/openmaptiles/layers/WaterNameTest.java @@ -5,7 +5,10 @@ import com.onthegomap.planetiler.TestUtils; import com.onthegomap.planetiler.geo.GeoUtils; +import com.onthegomap.planetiler.geo.GeometryException; import com.onthegomap.planetiler.reader.SimpleFeature; +import com.onthegomap.planetiler.reader.SourceFeature; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -27,7 +30,7 @@ void testWaterNamePoint() { "_layer", "water_name", "_type", "point", - "_minzoom", 9, + "_minzoom", 3, "_maxzoom", 14 )), process(polygonFeatureWithArea(1, Map.of( "name", "waterway", @@ -36,7 +39,8 @@ void testWaterNamePoint() { "water", "pond", "intermittent", "1" )))); - double z11area = Math.pow((GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d), 2) * Math.pow(2, 20 - 11); + // 1/4 th of tile area is the threshold, 1/4 = 0.25 => area->side:0.25->0.5 => slightly bigger -> 0.51 + double z11area = Math.pow(0.51d / Math.pow(2, 11), 2); assertFeatures(10, List.of(Map.of( "_layer", "water" ), Map.of( @@ -51,6 +55,23 @@ void testWaterNamePoint() { )))); } + // https://zelonewolf.github.io/openstreetmap-americana/#map=13/41.43989/-71.5716 + @Test + void testWordenPondNamePoint() { + assertFeatures(10, List.of(Map.of( + "_layer", "water" + ), Map.of( + "_layer", "water_name", + "_type", "point", + "_minzoom", 13, + "_maxzoom", 14 + )), process(polygonFeatureWithArea(4.930387948170328E-9, Map.of( + "name", "waterway", + "natural", "water", + "water", "pond" + )))); + } + @Test void testWaterNameLakeline() { assertFeatures(11, List.of(), process(SimpleFeature.create( @@ -71,7 +92,7 @@ void testWaterNameLakeline() { "_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 +142,7 @@ void testWaterNameMultipleLakelines() { 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 +159,40 @@ void testWaterNameMultipleLakelines() { ))); } + @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( @@ -201,4 +256,54 @@ void testMarinePoint() { "place", "sea" )))); } + + private record TestEntry( + SourceFeature feature, + int expectedZoom + ) {} + + private void createAreaForMinZoomTest(List testEntries, double side, int expectedZoom, String name) { + double area = Math.pow(side, 2); + var feature = polygonFeatureWithArea(area, Map.of( + "name", name, + "natural", "water" + )); + testEntries.add(new TestEntry( + feature, + Math.min(14, Math.max(3, expectedZoom)) + )); + } + + @Test + void testAreaToMinZoom() throws GeometryException { + // threshold is 1/4 of tile area, hence ... + // ... side is 1/2 tile side: from pixels to world coord, for say Z14 ... + //final double HALF_OF_TILE_SIDE = 128d / Math.pow(2d, 14d + 8d); + // ... and then for some lower zoom: + //double testAreaSide = HALF_OF_TILE_SIDE * Math.pow(2, 14 - zoom); + // all this then simplified to `testAreaSide` calculation bellow + + final List testEntries = new ArrayList<>(); + for (int zoom = 14; zoom >= 0; zoom--) { + double testAreaSide = Math.pow(2, -zoom - 1); + + // slightly bellow the threshold + createAreaForMinZoomTest(testEntries, testAreaSide * 0.999, zoom + 1, "waterway-"); + // precisely at the threshold + createAreaForMinZoomTest(testEntries, testAreaSide, zoom, "waterway="); + // slightly over the threshold + createAreaForMinZoomTest(testEntries, testAreaSide * 1.001, zoom, "waterway+"); + } + + for (var entry : testEntries) { + assertFeatures(10, List.of(Map.of( + "_layer", "water" + ), Map.of( + "_layer", "water_name", + "_type", "point", + "_minzoom", entry.expectedZoom, + "_maxzoom", 14 + )), process(entry.feature)); + } + } }