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$;