From 9af1fdba26fb3323251baf8c9d75f65c13e4b7cb Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 13 Jun 2026 21:59:08 +0200 Subject: [PATCH] feat(map): card-validators tile layer Capture OSM student-card validators (amenity=vending_machine, vending=student_card_validation) into the pois table as indoor='card_validator' in style.lua, expose them through a new floor-independent card_validators MVT function, register it in the Martin config, add a hidden card-validators symbol layer with a validator sprite to the basemap style. The webclient overlay toggle, POI-entry removal, and /view redirects follow in separate PRs. Co-Authored-By: Claude Opus 4.8 (1M context) --- map/martin/config.yaml | 6 +++ .../sprites/navigatum/card_validator.svg | 4 ++ map/martin/styles/navigatum-basemap.json | 32 ++++++++++++ map/osm2pgsql/style.lua | 24 +++++++++ .../20260613120000_card_validators_mvt.sql | 49 +++++++++++++++++++ 5 files changed, 115 insertions(+) create mode 100644 map/martin/sprites/navigatum/card_validator.svg create mode 100644 server/migrations/20260613120000_card_validators_mvt.sql diff --git a/map/martin/config.yaml b/map/martin/config.yaml index 56fee7a6b..665037e5b 100644 --- a/map/martin/config.yaml +++ b/map/martin/config.yaml @@ -74,3 +74,9 @@ postgres: minzoom: 16 maxzoom: 30 bounds: [ 5.98865807458, 47.3024876979, 15.0169958839, 54.983104153 ] # germany + card_validators: + schema: public + function: card_validators + minzoom: 13 + maxzoom: 30 + bounds: [ 5.98865807458, 47.3024876979, 15.0169958839, 54.983104153 ] # germany diff --git a/map/martin/sprites/navigatum/card_validator.svg b/map/martin/sprites/navigatum/card_validator.svg new file mode 100644 index 000000000..0e1607e97 --- /dev/null +++ b/map/martin/sprites/navigatum/card_validator.svg @@ -0,0 +1,4 @@ + + + + diff --git a/map/martin/styles/navigatum-basemap.json b/map/martin/styles/navigatum-basemap.json index cdec58c3f..46838eca2 100644 --- a/map/martin/styles/navigatum-basemap.json +++ b/map/martin/styles/navigatum-basemap.json @@ -82,6 +82,11 @@ "url": "https://nav.tum.de/martin/events_active", "minzoom": 13 }, + "card_validators": { + "type": "vector", + "url": "https://nav.tum.de/martin/card_validators", + "minzoom": 13 + }, "nav": { "type": "vector", "url": "https://nav.tum.de/martin/shortbread" @@ -6369,6 +6374,33 @@ "text-halo-color": "#ffffff", "text-halo-width": 1.2 } + }, + { + "id": "card-validators", + "type": "symbol", + "source": "card_validators", + "source-layer": "card_validators", + "minzoom": 13, + "layout": { + "visibility": "none", + "icon-image": "card_validator", + "icon-size": ["interpolate", ["linear"], ["zoom"], 13, 0.9, 18, 1.5], + "icon-allow-overlap": true, + "text-field": ["get", "name"], + "text-font": ["Roboto Regular"], + "text-size": 11, + "text-anchor": "top", + "text-offset": [0, 1.1], + "text-optional": true + }, + "paint": { + "icon-color": "#3070b3", + "icon-halo-color": "#ffffff", + "icon-halo-width": 1, + "text-color": "#3070b3", + "text-halo-color": "#ffffff", + "text-halo-width": 1.2 + } } ], "layout": { diff --git a/map/osm2pgsql/style.lua b/map/osm2pgsql/style.lua index cf83a1cd4..6fcbdf901 100644 --- a/map/osm2pgsql/style.lua +++ b/map/osm2pgsql/style.lua @@ -292,6 +292,10 @@ function osm2pgsql.process_node(object) object.tags.indoor = "shower" end end + -- Student-card validators are vending machines in OSM; lift them into their own poi category. + if object.tags.amenity == "vending_machine" and object.tags.vending == "student_card_validation" then + object.tags.indoor = "card_validator" + end if object.tags.indoor == "door" then -- pois should not need layers. Using them is likely a bug object.tags.layer = nil @@ -334,6 +338,26 @@ function osm2pgsql.process_node(object) } ) end + elseif object.tags.indoor == "card_validator" then + -- Unlike toilets, these render as named markers, so carry name + ref:tum into the poi. + for _, level in ipairs(SantiseLevel(object.tags.level)) do + tables.pois:insert( + { + indoor = object.tags.indoor, + name = object.tags.name, + ref = object.tags["ref:tum"], + students_have_access = true, + is_male_toilet = false, + is_female_toilet = false, + is_unisex_toilet = false, + is_wheelchair_toilet = false, + area = 0, + level_min = level.min, + level_max = level.max, + geom = object:as_point() + } + ) + end end end diff --git a/server/migrations/20260613120000_card_validators_mvt.sql b/server/migrations/20260613120000_card_validators_mvt.sql new file mode 100644 index 000000000..0f0f430f0 --- /dev/null +++ b/server/migrations/20260613120000_card_validators_mvt.sql @@ -0,0 +1,49 @@ +-- Student-card validators are a campus-wide overlay, not an indoor layer: they +-- must stay visible regardless of the selected floor. So unlike `indoor_pois`, +-- this function does not filter on `query_params->>'level'`. osm2pgsql captures +-- the OSM `vending_machine`/`student_card_validation` nodes into `pois` with +-- `indoor = 'card_validator'`, carrying their `name` and `ref` (from `ref:tum`). +CREATE OR REPLACE + FUNCTION card_validators(z integer, x integer, y integer) + RETURNS bytea AS $$ +DECLARE + mvt bytea; +BEGIN + SELECT INTO mvt ST_AsMVT(tile, 'card_validators', 4096, 'geom') + FROM ( + SELECT + ST_AsMVTGeom( + geom, + ST_TileEnvelope(z, x, y), + 4096, 64, true) AS geom, + name, + ref + FROM pois + WHERE indoor = 'card_validator' AND + geom && ST_TileEnvelope(z, x, y) + ) as tile + WHERE geom IS NOT NULL; + + RETURN mvt; +END +$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE; + +DO $do$ BEGIN + EXECUTE 'COMMENT ON FUNCTION card_validators IS $tj$' || $$ + { + "description": "student-card validation machines", + "attribution": "© OpenStreetMap contributors", + "vector_layers": [ + { + "id": "card_validators", + "fields": { + "name": "String", + "ref": "String" + }, + "maxzoom": 30, + "minzoom": 13 + } + ] + } + $$::json || '$tj$'; +END $do$;