From cc39c445efdd43e8979f3a30af5e7a4c418fa018 Mon Sep 17 00:00:00 2001 From: David Weber Date: Wed, 22 Jul 2026 09:06:30 +0200 Subject: [PATCH 1/2] Expand aerialway tile support beyond cable cars Before/after behavior: | Feature | Before | After | | --- | --- | --- | | aerialway=cable_car ways | Exported as kind=aerialway | Unchanged | | Other documented aerialway ways | Dropped | All 11 are exported | | kind_detail for aerialway ways | Hardcoded to cable_car | Preserves the source aerialway value | | Explicit station nodes | Dropped | kind=station, kind_detail=aerialway | | Named station areas | Dropped | Point-on-surface POI with name | | Untagged line endpoints | Not inferred | Unchanged | | aerialway=pylon | Dropped | Unchanged | The 11 newly supported line values are gondola, chair_lift, mixed_lift, drag_lift, t-bar, j-bar, platter, rope_tow, magic_carpet, zip_line, and goods. Their minimum zooms are tiered by visual importance. Stations use the existing POI zoom behavior. References: | Subject | Link | | --- | --- | | OSM aerialway values | https://wiki.openstreetmap.org/wiki/Key:aerialway | | OSM station tagging | https://wiki.openstreetmap.org/wiki/Tag:aerialway%3Dstation | | Schauinslandbahn example | https://www.openstreetmap.org/way/4040490 | Tests: | Coverage | Result | | --- | --- | | Existing cable_car and 11 added line values | kind and original kind_detail verified | | Explicit station node | POI schema and min_zoom verified | | Named station area | Point-on-surface name and schema verified | Baden-Wuerttemberg comparison from the same Geofabrik extract: | Metric | Before | After | | --- | ---: | ---: | | Unique aerialway ways | 0 | 345 | | Aerialway stations | 0 | 620 | | Archive size | 664,928,838 bytes | 664,947,675 bytes | | Size increase | | 18,837 bytes (0.0028%) | Schauinslandbahn way 4040490 is now emitted as kind=aerialway and kind_detail=gondola. AI assistance: this change and its tests were prepared with help from OpenAI Codex. --- .../com/protomaps/basemap/layers/Pois.java | 9 ++++++- .../com/protomaps/basemap/layers/Roads.java | 22 +++++++++++++-- .../protomaps/basemap/layers/PoisTest.java | 27 +++++++++++++++++++ .../protomaps/basemap/layers/RoadsTest.java | 24 +++++++++++++---- 4 files changed, 74 insertions(+), 8 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 21a420ec..aab8e681 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -85,6 +85,7 @@ static Integer parseElevation(String elevation) { rule( Expression.or( with("aeroway", "aerodrome"), + with("aerialway", "station"), with("amenity"), with("attraction"), with("boundary", "national_park", "protected_area"), @@ -190,7 +191,13 @@ static Integer parseElevation(String elevation) { rule(with("sport"), use("pm:kindDetail", fromTag("sport"))), rule(with("religion"), use("pm:kindDetail", fromTag("religion"))), - rule(with("cuisine"), use("pm:kindDetail", fromTag("cuisine"))) + rule(with("cuisine"), use("pm:kindDetail", fromTag("cuisine"))), + // Keep explicit aerialway stations ahead of generic railway classification on dual-tagged features. + rule( + with("aerialway", "station"), + use("pm:kind", fromTag("aerialway")), + use("pm:kindDetail", "aerialway") + ) )).index(); diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Roads.java b/tiles/src/main/java/com/protomaps/basemap/layers/Roads.java index 03be0498..69963778 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Roads.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Roads.java @@ -129,11 +129,29 @@ public Roads(CountryCoder countryCoder) { use("minZoom", 13) ), rule( - with("aerialway", "cable_car"), + with("aerialway", "cable_car", "gondola", "mixed_lift"), use("kind", "aerialway"), - use("kindDetail", "cable_car"), + use("kindDetail", fromTag("aerialway")), use("minZoom", 11) ), + rule( + with("aerialway", "chair_lift"), + use("kind", "aerialway"), + use("kindDetail", fromTag("aerialway")), + use("minZoom", 12) + ), + rule( + with("aerialway", "drag_lift", "t-bar", "j-bar", "platter", "rope_tow"), + use("kind", "aerialway"), + use("kindDetail", fromTag("aerialway")), + use("minZoom", 13) + ), + rule( + with("aerialway", "magic_carpet", "zip_line", "goods"), + use("kind", "aerialway"), + use("kindDetail", fromTag("aerialway")), + use("minZoom", 14) + ), rule( with("man_made", "pier"), use("kind", "path"), diff --git a/tiles/src/test/java/com/protomaps/basemap/layers/PoisTest.java b/tiles/src/test/java/com/protomaps/basemap/layers/PoisTest.java index 26e41745..e1550123 100644 --- a/tiles/src/test/java/com/protomaps/basemap/layers/PoisTest.java +++ b/tiles/src/test/java/com/protomaps/basemap/layers/PoisTest.java @@ -1083,6 +1083,33 @@ void kind_station_generic() { ))); } + @Test + void aerialwayStationNode() { + assertFeatures(15, + List.of(Map.of("kind", "station", "kind_detail", "aerialway", "min_zoom", 16)), + process(SimpleFeature.create( + newPoint(1, 1), + new HashMap<>(Map.of("aerialway", "station")), + "osm", null, 0 + ))); + } + + @Test + void aerialwayStationArea() { + assertFeatures(15, + List.of(Map.of( + "kind", "station", + "kind_detail", "aerialway", + "name", "Valley station", + "min_zoom", 16 + )), + process(SimpleFeature.create( + AREA_127_SQ_M, + new HashMap<>(Map.of("aerialway", "station", "name", "Valley station")), + "osm", null, 0 + ))); + } + @Test void kind_bakery_generic() { assertFeatures(15, diff --git a/tiles/src/test/java/com/protomaps/basemap/layers/RoadsTest.java b/tiles/src/test/java/com/protomaps/basemap/layers/RoadsTest.java index f39aadef..444ed574 100644 --- a/tiles/src/test/java/com/protomaps/basemap/layers/RoadsTest.java +++ b/tiles/src/test/java/com/protomaps/basemap/layers/RoadsTest.java @@ -459,16 +459,30 @@ void testRailwayDisused() { ); } - @Test - void testAerialwayCableCar() { + @ParameterizedTest + @CsvSource({ + "cable_car, 11", + "gondola, 11", + "mixed_lift, 11", + "chair_lift, 12", + "drag_lift, 13", + "t-bar, 13", + "j-bar, 13", + "platter, 13", + "rope_tow, 13", + "magic_carpet, 14", + "zip_line, 14", + "goods, 14" + }) + void testAerialways(String aerialway, int minZoom) { assertFeatures(12, List.of(Map.of("kind", "aerialway", - "kind_detail", "cable_car", - "_minzoom", 11 + "kind_detail", aerialway, + "_minzoom", minZoom )), processWithRelationAndCoords("", 0, 0, 1, 1, - "aerialway", "cable_car" + "aerialway", aerialway ) ); } From 777eb4f52e7d14ca20a5898b7f4df8c783d51908 Mon Sep 17 00:00:00 2001 From: David Weber Date: Wed, 22 Jul 2026 09:06:38 +0200 Subject: [PATCH 2/2] Render aerialways in the bundled styles Before/after behavior: | Feature | Before | After | | --- | --- | --- | | roads kind=aerialway | Present for cable cars but not rendered | Rendered with a dashed line | | Aerialway names | Not rendered | Localized line labels | | Aerialway sprite artwork | Present but disabled | Enabled in light and dark manifests | | Aerialway station icon | Not available | Used for station + aerialway POIs | | Railway station icon | train_station | Unchanged | Aerialway lines use the same color, opacity, and zoom-dependent width as rail infrastructure. A longer dash pattern distinguishes them without adding more visual weight. The station icon expression selects aerialway only when kind=station and kind_detail=aerialway. Other stations continue to use train_station. References: | Subject | Link | | --- | --- | | OSM aerialway tagging | https://wiki.openstreetmap.org/wiki/Key:aerialway | | Schauinslandbahn example | https://www.openstreetmap.org/way/4040490 | Verification: | Check | Result | | --- | --- | | Style tests | Passed | | TypeScript | Passed | | Biome | Passed | | Style package build | Passed | | Local Schauinslandbahn rendering | Visually inspected | AI assistance: this change was prepared with help from OpenAI Codex. --- sprites/flavors/dark.json | 3 +- sprites/flavors/light.json | 3 +- styles/src/base_layers.ts | 57 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/sprites/flavors/dark.json b/sprites/flavors/dark.json index 212eed4b..498823e8 100644 --- a/sprites/flavors/dark.json +++ b/sprites/flavors/dark.json @@ -39,6 +39,7 @@ "nature_reserve": "green", "aerodrome": "lapis", "train_station": "lapis", + "aerialway": "lapis", "bus_stop": "lapis", "ferry_terminal": "lapis", "stadium": "slategray", @@ -65,4 +66,4 @@ "theatre": "pink", "artwork": "pink" } -} \ No newline at end of file +} diff --git a/sprites/flavors/light.json b/sprites/flavors/light.json index 888f71d2..c11fb973 100644 --- a/sprites/flavors/light.json +++ b/sprites/flavors/light.json @@ -39,6 +39,7 @@ "nature_reserve": "green", "aerodrome": "lapis", "train_station": "lapis", + "aerialway": "lapis", "bus_stop": "lapis", "ferry_terminal": "lapis", "stadium": "slategray", @@ -65,4 +66,4 @@ "theatre": "pink", "artwork": "pink" } -} \ No newline at end of file +} diff --git a/styles/src/base_layers.ts b/styles/src/base_layers.ts index 65980120..d8f6cdff 100644 --- a/styles/src/base_layers.ts +++ b/styles/src/base_layers.ts @@ -1061,6 +1061,29 @@ export function nolabels_layers( ], }, }, + { + id: "roads_aerialway", + type: "line", + source: source, + "source-layer": "roads", + filter: ["==", "kind", "aerialway"], + paint: { + "line-color": t.railway, + "line-dasharray": [4, 1], + "line-opacity": 0.5, + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 3, + 0, + 6, + 0.15, + 18, + 9, + ], + }, + }, { id: "boundaries_country", type: "line", @@ -1438,6 +1461,29 @@ export function labels_layers( "symbol-spacing": 100, }, }, + { + id: "roads_labels_aerialway", + type: "symbol", + source: source, + "source-layer": "roads", + minzoom: 13, + filter: ["==", "kind", "aerialway"], + layout: { + "symbol-placement": "line", + "text-font": [t.regular || "Noto Sans Regular"], + "text-field": get_multiline_name( + lang, + script, + t.regular, + ) as DataDrivenPropertyValueSpecification, + "text-size": 12, + }, + paint: { + "text-color": t.roads_label_minor, + "text-halo-color": t.roads_label_minor_halo, + "text-halo-width": 1, + }, + }, { id: "roads_labels_minor", type: "symbol", @@ -1662,9 +1708,14 @@ export function labels_layers( ], layout: { "icon-image": [ - "match", - ["get", "kind"], - "station", + "case", + [ + "all", + ["==", ["get", "kind"], "station"], + ["==", ["get", "kind_detail"], "aerialway"], + ], + "aerialway", + ["==", ["get", "kind"], "station"], "train_station", ["get", "kind"], ],