Atlas: Tile-Proxy + Leaflet lokal — DSGVO-Sanierung fuer Kartendienste

- Leaflet 1.9.4 (CSS + JS + 5 Marker-Images) lokal unter
  App/assets/vendor/leaflet/ statt via unpkg.com/Cloudflare
- Neuer PHP-Endpunkt App/php/tile-proxy.php mit Disk-Cache, liefert
  OSM- und CARTO-Kacheln serverseitig aus; Browser kontaktiert keinen
  Drittanbieter mehr
- Whitelist (osm, carto), Zoom 3-18, 30 Tage Browser-Cache, ETag +
  If-Modified-Since, sauberer User-Agent fuer OSM-Policy
- Cache-Pfad: APP_ROOT/data/tiles lokal, via .env TILE_CACHE_DIR
  auf Produktions-Volume /var/www/html/geograsim/data/tiles
- Heli (heli.php + heli-game.php), Logistik (logistik.php), Routes-
  Dev-Tool und admin-levels.html alle auf Proxy umgestellt
- Datenschutzerklaerung: alter Google-AdSense-Passus (falsch, nicht
  im Einsatz) entfernt, neuer Abschnitt zur serverseitigen
  Tile-Auslieferung mit Rechtsgrundlage Art. 6 Abs. 1 lit. f DSGVO
- .gitignore: App/data/tiles ausgeschlossen
- .env.example: TILE_CACHE_DIR + OpenAI/ElevenLabs dokumentiert

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 00:53:43 +02:00
parent 3885c83294
commit eac686ee1a
15 changed files with 1380 additions and 8 deletions
+502
View File
@@ -0,0 +1,502 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoGraSim — Heli-Routen Vorarlberg</title>
<link rel="icon" type="image/png" href="../../favicon-96x96.png">
<link rel="stylesheet" href="../../assets/vendor/leaflet/leaflet.css">
<link rel="stylesheet" href="../../assets/fonts/inter.css">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Inter',sans-serif;background:#f7f6f3;height:100vh;display:flex;flex-direction:column}
.topbar{background:#fff;padding:.5rem 1rem;display:flex;align-items:center;gap:.8rem;border-bottom:1px solid rgba(0,0,0,.06);flex-shrink:0;flex-wrap:wrap}
.topbar h1{font-size:.9rem;font-weight:800;color:#1f4e5a}
.topbar select{padding:.3rem .5rem;border:1.5px solid rgba(0,0,0,.1);border-radius:6px;font-size:.75rem;font-family:inherit;font-weight:600;max-width:350px}
.topbar .info{font-size:.68rem;color:#8a8a8a;flex:1}
.badge{display:inline-block;font-size:.6rem;font-weight:700;padding:1px 6px;border-radius:4px;margin-left:4px}
.badge-easy{background:#dceadd;color:#3a6b3e}
.badge-medium{background:#f2eacc;color:#8a7020}
.badge-hard{background:#f2ddd8;color:#8a3a2a}
#map{flex:1}
.wp-marker{width:22px;height:22px;background:#fff;border:2px solid #1f4e5a;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:800;color:#1f4e5a;box-shadow:0 1px 4px rgba(0,0,0,.25)}
.wp-marker.start{background:#e8833a;border-color:#c06020;color:#fff;width:28px;height:28px;font-size:12px}
.wp-marker.end{background:#c85c4a;border-color:#a04030;color:#fff;width:28px;height:28px;font-size:12px}
.wp-label{font-family:'Inter',sans-serif;font-weight:700;font-size:9px;color:#1f4e5a;white-space:nowrap;background:rgba(255,255,255,.88);padding:1px 5px;border-radius:3px;box-shadow:0 1px 2px rgba(0,0,0,.1)}
.route-desc{position:absolute;bottom:12px;left:12px;right:12px;background:rgba(255,255,255,.95);border-radius:10px;padding:10px 14px;font-size:.75rem;color:#333;box-shadow:0 4px 16px rgba(0,0,0,.12);z-index:1000;line-height:1.5;max-height:30vh;overflow-y:auto}
.route-desc h3{font-size:.85rem;font-weight:800;color:#1f4e5a;margin-bottom:4px}
.route-desc .meta{font-size:.65rem;color:#8a8a8a}
</style>
</head>
<body>
<div class="topbar">
<h1>🚁 Heli-Routen</h1>
<select id="region-select" onchange="filterRoutes()" style="padding:.3rem .5rem;border:1.5px solid rgba(0,0,0,.1);border-radius:6px;font-size:.75rem;font-family:inherit;font-weight:600">
<option value="all">Alle Regionen</option>
<option value="vlbg">Vorarlberg</option>
<option value="tirol">Tirol</option>
</select>
<select id="route-select" onchange="showRoute()"></select>
<div class="info" id="route-info">60 Routen · Vorarlberg + Tirol</div>
</div>
<div id="map" style="position:relative">
<div class="route-desc" id="route-desc"></div>
</div>
<script src="../../assets/vendor/leaflet/leaflet.js"></script>
<script>
var map = L.map('map', {zoomControl:false}).setView([47.2, 11.0], 9);
L.tileLayer('../../php/tile-proxy.php?z={z}&x={x}&y={y}&p=osm', {
attribution:'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>', maxZoom:16
}).addTo(map);
L.control.zoom({position:'topright'}).addTo(map);
L.control.scale({position:'bottomleft',metric:true,imperial:false}).addTo(map);
// Waypoints: echte Vorarlberger Orte
var WP = {
// Basen
nenzing: {lat:47.1828, lon:9.7072, name:'C8 Nenzing (ÖAMTC)'},
hohenems: {lat:47.3650, lon:9.6892, name:'Hohenems (Polizei)'},
// Rheintal
feldkirch: {lat:47.2376, lon:9.5982, name:'Feldkirch'},
rankweil: {lat:47.2725, lon:9.6450, name:'Rankweil'},
goetzis: {lat:47.3333, lon:9.6333, name:'Götzis'},
dornbirn: {lat:47.4136, lon:9.7424, name:'Dornbirn'},
bregenz: {lat:47.5026, lon:9.7473, name:'Bregenz'},
lustenau: {lat:47.4267, lon:9.6583, name:'Lustenau'},
// Walgau / Klostertal
bludenz: {lat:47.1546, lon:9.8222, name:'Bludenz'},
buers: {lat:47.1500, lon:9.8000, name:'Bürs'},
nüziders: {lat:47.1700, lon:9.8000, name:'Nüziders'},
innerbraz: {lat:47.1347, lon:9.9208, name:'Innerbraz'},
kloeserle: {lat:47.1283, lon:10.0750, name:'Klösterle'},
stuben: {lat:47.1300, lon:10.1900, name:'Stuben am Arlberg'},
langen: {lat:47.1283, lon:10.1167, name:'Langen am Arlberg'},
// Montafon
schruns: {lat:47.0800, lon:9.9167, name:'Schruns'},
tschagguns: {lat:47.0750, lon:9.9000, name:'Tschagguns'},
stgallenk: {lat:47.0500, lon:9.8833, name:'St. Gallenkirch'},
gaschurn: {lat:47.0167, lon:10.0167, name:'Gaschurn'},
partenen: {lat:47.0000, lon:10.0667, name:'Partenen'},
silvretta: {lat:46.9167, lon:10.0833, name:'Silvretta-Stausee'},
// Großes Walsertal
thueringen: {lat:47.2200, lon:9.7600, name:'Thüringen'},
zwischenwasser:{lat:47.2300, lon:9.8000, name:'Zwischenwasser'},
laterns: {lat:47.2500, lon:9.8700, name:'Laterns'},
damuels: {lat:47.2833, lon:9.8917, name:'Damüls'},
fontanella: {lat:47.2600, lon:9.9400, name:'Fontanella'},
sonntag: {lat:47.2300, lon:9.9200, name:'Sonntag'},
// Bregenzerwald
egg: {lat:47.4333, lon:9.8917, name:'Egg'},
bezau: {lat:47.3833, lon:9.9000, name:'Bezau'},
mellau: {lat:47.3500, lon:9.8833, name:'Mellau'},
au: {lat:47.3333, lon:9.9667, name:'Au'},
schoppernau:{lat:47.3167, lon:10.0167, name:'Schoppernau'},
warth: {lat:47.2533, lon:10.1883, name:'Warth'},
schroecken: {lat:47.2583, lon:10.0917, name:'Schröcken'},
lech: {lat:47.2083, lon:10.1417, name:'Lech am Arlberg'},
zuers: {lat:47.1700, lon:10.1583, name:'Zürs'},
// Brandnertal / Rätikon
brand: {lat:47.1000, lon:9.7333, name:'Brand'},
// Kleinwalsertal
riezlern: {lat:47.3583, lon:10.1917, name:'Riezlern'},
mittelberg: {lat:47.3333, lon:10.1500, name:'Mittelberg'},
// Pfänder / Bodensee
pfaender: {lat:47.5100, lon:9.7800, name:'Pfänder (1064 m)'},
// Kanisfluh
kanisfluh: {lat:47.3600, lon:9.9400, name:'Kanisfluh (2044 m)'},
// Spezielle Einsatzorte
damuels_ski:{lat:47.2900, lon:9.8950, name:'Skigebiet Damüls'},
lech_ski: {lat:47.2150, lon:10.1500, name:'Skigebiet Lech-Zürs'},
silvretta_bielerhöhe:{lat:46.9200, lon:10.0900, name:'Bielerhöhe'},
brandnergletscher:{lat:47.0800, lon:9.7500, name:'Brandner Gletscher'},
// Einsatzorte
verkehr_a14:{lat:47.2800, lon:9.6600, name:'Unfall A14 Rankweil'},
bergrettung_muels:{lat:47.2700, lon:9.8800, name:'Skiunfall Damüls-Mellau'},
vermisst_silvretta:{lat:46.9500, lon:10.0700, name:'Vermisste Person Silvretta'},
hochwasser_ill:{lat:47.1200, lon:9.8500, name:'Hochwasser Ill-Tal'},
// === TIROL ===
// Basen
innsbruck_c1:{lat:47.2600, lon:11.3439, name:'C1 Innsbruck (ÖAMTC)'},
zams_c5: {lat:47.1597, lon:10.5933, name:'C5 Zams (ÖAMTC)'},
kitzbuehel_c4:{lat:47.3801, lon:12.3580, name:'C4 Kitzbühel (ÖAMTC)'},
lienz_c7: {lat:46.8297, lon:12.7647, name:'C7 Lienz (ÖAMTC)'},
// Inntal
hall: {lat:47.2894, lon:11.5083, name:'Hall in Tirol'},
schwaz: {lat:47.3517, lon:11.7100, name:'Schwaz'},
woergl: {lat:47.4873, lon:12.0638, name:'Wörgl'},
kufstein: {lat:47.5836, lon:12.1647, name:'Kufstein'},
telfs: {lat:47.3128, lon:11.0697, name:'Telfs'},
imst: {lat:47.2450, lon:10.7397, name:'Imst'},
landeck: {lat:47.1397, lon:10.5669, name:'Landeck'},
reutte: {lat:47.4858, lon:10.7181, name:'Reutte'},
// Stubaital
fulpmes: {lat:47.1558, lon:11.3533, name:'Fulpmes'},
neustift: {lat:47.1083, lon:11.3117, name:'Neustift im Stubaital'},
stubaier_gl: {lat:46.9972, lon:11.1167, name:'Stubaier Gletscher'},
// Ötztal
oetz: {lat:47.2083, lon:10.8933, name:'Ötz'},
soelden: {lat:46.9667, lon:10.8750, name:'Sölden'},
obergurgl: {lat:46.8700, lon:11.0300, name:'Obergurgl'},
// Zillertal
jenbach: {lat:47.3917, lon:11.7750, name:'Jenbach'},
zell_ziller: {lat:47.2286, lon:11.8822, name:'Zell am Ziller'},
mayrhofen: {lat:47.1592, lon:11.8614, name:'Mayrhofen'},
hintertux: {lat:47.0833, lon:11.6500, name:'Hintertux'},
// Wipptal / Brenner
steinach: {lat:47.0886, lon:11.4672, name:'Steinach am Brenner'},
brenner_ort: {lat:47.0422, lon:11.5067, name:'Brenner'},
// Osttirol
matrei: {lat:47.0000, lon:12.5333, name:'Matrei in Osttirol'},
kals: {lat:47.0083, lon:12.6417, name:'Kals am Großglockner'},
sillian: {lat:46.7519, lon:12.4219, name:'Sillian'},
// Arlberg
st_anton: {lat:47.1286, lon:10.2653, name:'St. Anton am Arlberg'},
// Kitzbühel
kitzbuehel_ort:{lat:47.4464, lon:12.3922, name:'Kitzbühel'},
st_johann: {lat:47.3528, lon:12.4247, name:'St. Johann in Tirol'},
// Seefeld / Karwendel
seefeld: {lat:47.3300, lon:11.1900, name:'Seefeld'},
scharnitz: {lat:47.3889, lon:11.2647, name:'Scharnitz'},
// Einsatzorte Tirol
lawine_arlberg:{lat:47.1400, lon:10.2200, name:'Lawine St. Anton'},
skiunfall_soelden:{lat:46.9800, lon:10.8600, name:'Skiunfall Sölden'},
bergunfall_glockner:{lat:47.0750, lon:12.6950, name:'Bergunfall Großglockner'},
vermisst_karwendel:{lat:47.3600, lon:11.3900, name:'Vermisste Person Karwendel'},
verkehr_inntal:{lat:47.3000, lon:11.6000, name:'Unfall A12 Inntal'},
hochwasser_inn:{lat:47.2700, lon:10.8500, name:'Hochwasser Inn bei Imst'},
};
// === 30 ROUTEN ===
var ROUTES = [
// --- VON NENZING (C8) ---
{id:1, name:'Nenzing → Damüls (Skiunfall)', start:'nenzing', end:'bergrettung_muels', difficulty:'medium',
waypoints:['nenzing','thueringen','zwischenwasser','laterns','damuels','bergrettung_muels'],
desc:'Skiunfall im Gebiet Damüls-Mellau. Route über das Laternsertal, da Direktflug über den Grat bei Wind nicht möglich.', reason:'Gebirgsumgehung über Talroute'},
{id:2, name:'Nenzing → Lech (Lawinenunfall)', start:'nenzing', end:'lech', difficulty:'hard',
waypoints:['nenzing','bludenz','kloeserle','stuben','zuers','lech'],
desc:'Lawinenunfall bei Lech. Klassische Route durch das Klostertal und über den Arlberg.', reason:'Standardroute durch Klostertal'},
{id:3, name:'Nenzing → Silvretta (Vermisste)', start:'nenzing', end:'vermisst_silvretta', difficulty:'hard',
waypoints:['nenzing','bludenz','schruns','stgallenk','gaschurn','partenen','silvretta','vermisst_silvretta'],
desc:'Vermisste Wandergruppe im Silvrettagebiet. Langer Flug durch das gesamte Montafon bis zur Bielerhöhe.', reason:'Einzige Zugangsroute durch das enge Montafon'},
{id:4, name:'Nenzing → Bregenz (Seerettung)', start:'nenzing', end:'bregenz', difficulty:'easy',
waypoints:['nenzing','feldkirch','rankweil','goetzis','dornbirn','bregenz'],
desc:'Notfall am Bodensee. Einfacher Rheintal-Flug nach Norden.', reason:'Flaches Rheintal, keine Hindernisse'},
{id:5, name:'Nenzing → Brand (Bergrettung)', start:'nenzing', end:'brand', difficulty:'medium',
waypoints:['nenzing','buers','brand'],
desc:'Kletterunfall im Brandnertal (Rätikon). Kurzer, aber enger Talflug.', reason:'Kurze Distanz, enges Tal'},
{id:6, name:'Nenzing → Schoppernau (Verkehr)', start:'nenzing', end:'schoppernau', difficulty:'medium',
waypoints:['nenzing','feldkirch','rankweil','goetzis','egg','bezau','au','schoppernau'],
desc:'Schwerer Verkehrsunfall bei Schoppernau. Route über Rheintal und Bregenzerwald.', reason:'Umweg über Rheintal nötig — Direktflug über Gebirge bei Wetter nicht möglich'},
{id:7, name:'Nenzing → Gaschurn (Hochwasser)', start:'nenzing', end:'hochwasser_ill', difficulty:'medium',
waypoints:['nenzing','bludenz','schruns','tschagguns','stgallenk','hochwasser_ill'],
desc:'Hochwasser an der Ill. Evakuierung im vorderen Montafon.', reason:'Flusstal-Route'},
{id:8, name:'Nenzing → Warth (Skiunfall)', start:'nenzing', end:'warth', difficulty:'hard',
waypoints:['nenzing','bludenz','kloeserle','langen','stuben','zuers','lech','warth'],
desc:'Skiunfall bei Warth-Schröcken. Lange Route über Klostertal und Arlberg.', reason:'Einzige wettersichere Route über den Arlberg'},
{id:9, name:'Nenzing → A14 Rankweil (Verkehr)', start:'nenzing', end:'verkehr_a14', difficulty:'easy',
waypoints:['nenzing','thueringen','rankweil','verkehr_a14'],
desc:'Massenkarambolage auf der A14. Kürzester Weg.', reason:'Direktflug, minimale Distanz'},
{id:10, name:'Nenzing → Fontanella (Großes Walsertal)', start:'nenzing', end:'fontanella', difficulty:'hard',
waypoints:['nenzing','thueringen','zwischenwasser','sonntag','fontanella'],
desc:'Akuter Notfall im hinteren Großen Walsertal. Enges, winkeliges Tal.', reason:'Große Walsertal nur durch Taleingang erreichbar'},
{id:11, name:'Nenzing → Dornbirn (Industrieunfall)', start:'nenzing', end:'dornbirn', difficulty:'easy',
waypoints:['nenzing','feldkirch','rankweil','goetzis','hohenems','dornbirn'],
desc:'Chemieunfall in Dornbirn. Rheintal-Direktflug.', reason:'Flaches Gelände, gute Sicht'},
{id:12, name:'Nenzing → Kleinwalsertal', start:'nenzing', end:'mittelberg', difficulty:'hard',
waypoints:['nenzing','feldkirch','rankweil','goetzis','egg','bezau','au','schroecken','warth','riezlern','mittelberg'],
desc:'Notfall im Kleinwalsertal. Nur über den Bregenzerwald erreichbar (keine Straße nach Österreich!).', reason:'Kleinwalsertal ist nur von Deutschland oder per Luft erreichbar'},
{id:13, name:'Nenzing → Zürs (Piste)', start:'nenzing', end:'zuers', difficulty:'medium',
waypoints:['nenzing','bludenz','innerbraz','kloeserle','langen','stuben','zuers'],
desc:'Verletzung auf der Skipiste in Zürs. Route durch Klostertal.', reason:'Klassische Arlberg-Zufahrt'},
{id:14, name:'Nenzing → Pfänder (Wanderunfall)', start:'nenzing', end:'pfaender', difficulty:'easy',
waypoints:['nenzing','feldkirch','rankweil','goetzis','dornbirn','bregenz','pfaender'],
desc:'Wanderunfall am Pfänder über Bregenz. Einfacher Rheintal-Flug mit kurzem Anstieg.', reason:'Pfänder ist niedrig und gut erreichbar'},
{id:15, name:'Nenzing → Mellau (Canyoning)', start:'nenzing', end:'mellau', difficulty:'medium',
waypoints:['nenzing','rankweil','goetzis','egg','bezau','mellau'],
desc:'Canyoning-Unfall bei Mellau. Route über Bregenzerwald.', reason:'Bregenzerwald-Route'},
// --- VON HOHENEMS (Polizei) ---
{id:16, name:'Hohenems → Damüls (Skiunfall)', start:'hohenems', end:'bergrettung_muels', difficulty:'medium',
waypoints:['hohenems','goetzis','rankweil','zwischenwasser','laterns','damuels','bergrettung_muels'],
desc:'Polizeieinsatz nach Skiunfall Damüls. Route über Götzis und Laternsertal.', reason:'Umgehung des Gebirges über Talroute'},
{id:17, name:'Hohenems → Bregenz (Demonstration)', start:'hohenems', end:'bregenz', difficulty:'easy',
waypoints:['hohenems','dornbirn','bregenz'],
desc:'Polizeieinsatz bei Großveranstaltung in Bregenz. Kurzer Rheintal-Flug.', reason:'Minimale Distanz'},
{id:18, name:'Hohenems → Lech (Sucheinsatz)', start:'hohenems', end:'lech', difficulty:'hard',
waypoints:['hohenems','dornbirn','egg','bezau','au','schroecken','warth','lech'],
desc:'Vermisstensuche bei Lech. Route über den gesamten Bregenzerwald.', reason:'Bregenzerwald-Route vermeidet hohe Pässe'},
{id:19, name:'Hohenems → Schruns (Fahndung)', start:'hohenems', end:'schruns', difficulty:'medium',
waypoints:['hohenems','rankweil','nenzing','bludenz','schruns'],
desc:'Fahndungseinsatz in Schruns. Route über Rheintal und Walgau.', reason:'Standardroute ins Montafon'},
{id:20, name:'Hohenems → Feldkirch (Amtshilfe)', start:'hohenems', end:'feldkirch', difficulty:'easy',
waypoints:['hohenems','rankweil','feldkirch'],
desc:'Polizeieinsatz in Feldkirch. Sehr kurzer Flug.', reason:'Direkte Linie im Rheintal'},
{id:21, name:'Hohenems → Lustenau (Grenze)', start:'hohenems', end:'lustenau', difficulty:'easy',
waypoints:['hohenems','lustenau'],
desc:'Grenzeinsatz bei Lustenau (Schweizer Grenze). Kürzeste Route.', reason:'Nachbarort, Sichtflug'},
{id:22, name:'Hohenems → Egg (Bregenzerwald)', start:'hohenems', end:'egg', difficulty:'easy',
waypoints:['hohenems','dornbirn','egg'],
desc:'Einsatz im vorderen Bregenzerwald. Route über Dornbirn.', reason:'Einfache Talroute'},
{id:23, name:'Hohenems → Gaschurn (Großeinsatz)', start:'hohenems', end:'gaschurn', difficulty:'hard',
waypoints:['hohenems','rankweil','nenzing','bludenz','schruns','stgallenk','gaschurn'],
desc:'Großeinsatz im hinteren Montafon. Langer Flug durch mehrere Täler.', reason:'Montafon nur von Bludenz aus erreichbar'},
{id:24, name:'Hohenems → Brand (Kletterunfall)', start:'hohenems', end:'brand', difficulty:'medium',
waypoints:['hohenems','rankweil','nenzing','buers','brand'],
desc:'Kletterunfall im Brandnertal. Route über Walgau.', reason:'Brandnertal-Zugang über Bürs'},
{id:25, name:'Hohenems → Sonntag (Walsertal)', start:'hohenems', end:'sonntag', difficulty:'hard',
waypoints:['hohenems','rankweil','thueringen','zwischenwasser','sonntag'],
desc:'Notfall im Großen Walsertal. Enges Tal, schwieriger Zugang.', reason:'Talenge erfordert präzise Navigation'},
{id:26, name:'Hohenems → Bezau (Verkehr)', start:'hohenems', end:'bezau', difficulty:'easy',
waypoints:['hohenems','dornbirn','egg','bezau'],
desc:'Verkehrsunfall bei Bezau. Route über den vorderen Bregenzerwald.', reason:'Einfache Bregenzerwald-Route'},
{id:27, name:'Hohenems → Stuben (Arlberg)', start:'hohenems', end:'stuben', difficulty:'hard',
waypoints:['hohenems','rankweil','nenzing','bludenz','innerbraz','kloeserle','stuben'],
desc:'Einsatz am Arlberg. Langer Flug durch Walgau und Klostertal.', reason:'Arlberg nur über Klostertal erreichbar'},
{id:28, name:'Hohenems → Silvretta', start:'hohenems', end:'silvretta', difficulty:'hard',
waypoints:['hohenems','rankweil','nenzing','bludenz','schruns','gaschurn','partenen','silvretta'],
desc:'Bergunfall an der Silvretta. Längste Route — komplettes Montafon.', reason:'Silvretta nur über Montafon erreichbar'},
{id:29, name:'Hohenems → Kanisfluh (Sucheinsatz)', start:'hohenems', end:'kanisfluh', difficulty:'medium',
waypoints:['hohenems','dornbirn','egg','bezau','mellau','kanisfluh'],
desc:'Vermisstensuche am Kanisfluh. Route über den Bregenzerwald, dann Anflug zum Bergmassiv.', reason:'Kanisfluh ist isoliertes Massiv im Bregenzerwald'},
{id:30, name:'Hohenems → Riezlern (Kleinwalsertal)', start:'hohenems', end:'riezlern', difficulty:'hard',
waypoints:['hohenems','dornbirn','egg','bezau','au','schroecken','warth','riezlern'],
desc:'Einsatz im Kleinwalsertal. Das Tal ist nur von Deutschland oder per Luft erreichbar — eine der anspruchsvollsten Routen Vorarlbergs.', reason:'Funktionale Exklave — Luftweg einzige österreichische Verbindung', region:'vlbg'},
// === TIROL-ROUTEN (3160) ===
// Von C1 Innsbruck
{id:31, name:'Innsbruck → Sölden (Skiunfall)', start:'innsbruck_c1', end:'skiunfall_soelden', difficulty:'medium', region:'tirol',
waypoints:['innsbruck_c1','telfs','oetz','soelden','skiunfall_soelden'],
desc:'Skiunfall im Skigebiet Sölden. Route durch das Inntal und Ötztal.', reason:'Ötztal nur von Norden erreichbar'},
{id:32, name:'Innsbruck → Großglockner (Bergunfall)', start:'innsbruck_c1', end:'bergunfall_glockner', difficulty:'hard', region:'tirol',
waypoints:['innsbruck_c1','hall','schwaz','jenbach','woergl','kitzbuehel_ort','st_johann','matrei','kals','bergunfall_glockner'],
desc:'Kletterunfall am Großglockner. Längste Route — quer durch Tirol bis Osttirol.', reason:'Großglockner liegt an der Grenze Tirol/Kärnten, nur über Felbertauern oder Inntal erreichbar'},
{id:33, name:'Innsbruck → Stubaier Gletscher', start:'innsbruck_c1', end:'stubaier_gl', difficulty:'medium', region:'tirol',
waypoints:['innsbruck_c1','fulpmes','neustift','stubaier_gl'],
desc:'Notfall am Stubaier Gletscher. Kurzer, aber hochalpiner Flug.', reason:'Stubaital ist Sackgasse — ein Weg rein, ein Weg raus'},
{id:34, name:'Innsbruck → Karwendel (Vermisste)', start:'innsbruck_c1', end:'vermisst_karwendel', difficulty:'medium', region:'tirol',
waypoints:['innsbruck_c1','hall','scharnitz','vermisst_karwendel'],
desc:'Vermisstensuche im Karwendelgebirge. Unwegsames Gelände nördlich von Innsbruck.', reason:'Karwendel ist das größte Naturschutzgebiet der Nordalpen'},
{id:35, name:'Innsbruck → Kufstein (Verkehr)', start:'innsbruck_c1', end:'kufstein', difficulty:'easy', region:'tirol',
waypoints:['innsbruck_c1','hall','schwaz','jenbach','woergl','kufstein'],
desc:'Schwerer Verkehrsunfall auf der A12 bei Kufstein. Einfacher Inntal-Flug.', reason:'Flaches Inntal, gute Sicht'},
{id:36, name:'Innsbruck → Brenner', start:'innsbruck_c1', end:'brenner_ort', difficulty:'easy', region:'tirol',
waypoints:['innsbruck_c1','steinach','brenner_ort'],
desc:'Einsatz am Brennerpass. Kurzer Flug durchs Wipptal.', reason:'Direkte Talroute nach Süden'},
{id:37, name:'Innsbruck → Obergurgl', start:'innsbruck_c1', end:'obergurgl', difficulty:'hard', region:'tirol',
waypoints:['innsbruck_c1','telfs','oetz','soelden','obergurgl'],
desc:'Notfall in Obergurgl. Tief ins hintere Ötztal — hochalpin.', reason:'Obergurgl liegt auf 1.930 m, eine der höchsten Dauersiedlungen Österreichs'},
{id:38, name:'Innsbruck → Mayrhofen', start:'innsbruck_c1', end:'mayrhofen', difficulty:'medium', region:'tirol',
waypoints:['innsbruck_c1','hall','schwaz','jenbach','zell_ziller','mayrhofen'],
desc:'Canyoning-Unfall bei Mayrhofen. Route durch Inntal und Zillertal.', reason:'Zillertal-Zugang über Jenbach'},
{id:39, name:'Innsbruck → Hintertux', start:'innsbruck_c1', end:'hintertux', difficulty:'hard', region:'tirol',
waypoints:['innsbruck_c1','hall','schwaz','jenbach','zell_ziller','mayrhofen','hintertux'],
desc:'Gletscherunfall am Hintertuxer Gletscher. Langer Flug bis ans Talende.', reason:'Tuxertal ist Sackgasse, Gletscher auf 3.250 m'},
{id:40, name:'Innsbruck → Seefeld', start:'innsbruck_c1', end:'seefeld', difficulty:'easy', region:'tirol',
waypoints:['innsbruck_c1','seefeld'],
desc:'Wanderunfall am Seefelder Plateau. Kurzer Flug.', reason:'Seefeld liegt nur 20 km nordwestlich von Innsbruck'},
{id:41, name:'Innsbruck → St. Anton (Lawine)', start:'innsbruck_c1', end:'lawine_arlberg', difficulty:'hard', region:'tirol',
waypoints:['innsbruck_c1','telfs','imst','landeck','st_anton','lawine_arlberg'],
desc:'Lawinenabgang bei St. Anton. Langer Flug durch das gesamte Oberinntal.', reason:'Arlberg nur über Inntal-Route erreichbar'},
{id:42, name:'Innsbruck → Reutte', start:'innsbruck_c1', end:'reutte', difficulty:'medium', region:'tirol',
waypoints:['innsbruck_c1','telfs','imst','reutte'],
desc:'Notfall im Außerfern. Route über den Fernpass.', reason:'Außerfern ist vom Rest Tirols durch Gebirge getrennt'},
{id:43, name:'Innsbruck → Inntal A12 (Verkehr)', start:'innsbruck_c1', end:'verkehr_inntal', difficulty:'easy', region:'tirol',
waypoints:['innsbruck_c1','hall','verkehr_inntal'],
desc:'Massenkarambolage auf der A12. Sehr kurzer Einsatz.', reason:'Direktflug im Inntal'},
{id:44, name:'Innsbruck → Hochwasser Inn', start:'innsbruck_c1', end:'hochwasser_inn', difficulty:'medium', region:'tirol',
waypoints:['innsbruck_c1','telfs','imst','hochwasser_inn'],
desc:'Hochwasser am Inn bei Imst. Evakuierungseinsatz.', reason:'Inntal-Route, Fluss folgen'},
{id:45, name:'Innsbruck → Kitzbühel', start:'innsbruck_c1', end:'kitzbuehel_ort', difficulty:'medium', region:'tirol',
waypoints:['innsbruck_c1','hall','schwaz','jenbach','woergl','kitzbuehel_ort'],
desc:'Einsatz in Kitzbühel. Inntal-Route über Wörgl.', reason:'Kitzbühel liegt südlich des Inntals'},
// Von C5 Zams
{id:46, name:'Zams → St. Anton (Lawine)', start:'zams_c5', end:'lawine_arlberg', difficulty:'medium', region:'tirol',
waypoints:['zams_c5','landeck','st_anton','lawine_arlberg'],
desc:'Lawinenunfall bei St. Anton. Kurzer Flug von Zams über Landeck.', reason:'C5 ist die nächste Basis zum Arlberg'},
{id:47, name:'Zams → Sölden', start:'zams_c5', end:'soelden', difficulty:'medium', region:'tirol',
waypoints:['zams_c5','imst','oetz','soelden'],
desc:'Skiunfall in Sölden. Route über Imst ins Ötztal.', reason:'Ötztal-Zugang über Imst'},
{id:48, name:'Zams → Ischgl', start:'zams_c5', end:'sillian', difficulty:'hard', region:'tirol',
waypoints:['zams_c5','landeck','sillian'],
desc:'Notfall im Paznauntal. Enges Hochgebirgstal.', reason:'Paznauntal ist eine der engsten Talrouten Tirols'},
{id:49, name:'Zams → Reutte', start:'zams_c5', end:'reutte', difficulty:'medium', region:'tirol',
waypoints:['zams_c5','imst','reutte'],
desc:'Einsatz im Außerfern. Route über Imst und Fernpass.', reason:'Außerfern-Zugang'},
{id:50, name:'Zams → Innsbruck', start:'zams_c5', end:'innsbruck_c1', difficulty:'easy', region:'tirol',
waypoints:['zams_c5','imst','telfs','innsbruck_c1'],
desc:'Patiententransport nach Innsbruck. Inntal-Flug.', reason:'Inntal-Standardroute'},
// Von C4 Kitzbühel
{id:51, name:'Kitzbühel → Mayrhofen', start:'kitzbuehel_c4', end:'mayrhofen', difficulty:'medium', region:'tirol',
waypoints:['kitzbuehel_c4','woergl','jenbach','zell_ziller','mayrhofen'],
desc:'Bergunfall bei Mayrhofen. Route über das Inntal ins Zillertal.', reason:'Zillertal-Zugang über Jenbach'},
{id:52, name:'Kitzbühel → Kufstein', start:'kitzbuehel_c4', end:'kufstein', difficulty:'easy', region:'tirol',
waypoints:['kitzbuehel_c4','woergl','kufstein'],
desc:'Verkehrsunfall bei Kufstein. Kurzer Inntal-Flug.', reason:'Direkter Inntal-Flug'},
{id:53, name:'Kitzbühel → St. Johann', start:'kitzbuehel_c4', end:'st_johann', difficulty:'easy', region:'tirol',
waypoints:['kitzbuehel_c4','st_johann'],
desc:'Notfall in St. Johann. Kürzeste Route.', reason:'Nachbarort'},
{id:54, name:'Kitzbühel → Hintertux', start:'kitzbuehel_c4', end:'hintertux', difficulty:'hard', region:'tirol',
waypoints:['kitzbuehel_c4','woergl','jenbach','zell_ziller','mayrhofen','hintertux'],
desc:'Gletscherunfall Hintertux. Lange Route durchs gesamte Zillertal.', reason:'Tuxertal nur über Zillertal erreichbar'},
// Von C7 Lienz
{id:55, name:'Lienz → Großglockner', start:'lienz_c7', end:'bergunfall_glockner', difficulty:'hard', region:'tirol',
waypoints:['lienz_c7','matrei','kals','bergunfall_glockner'],
desc:'Kletterunfall am Großglockner. Route durch das Iseltal.', reason:'Kürzeste Route zum Glockner von Süden'},
{id:56, name:'Lienz → Sillian', start:'lienz_c7', end:'sillian', difficulty:'easy', region:'tirol',
waypoints:['lienz_c7','sillian'],
desc:'Einsatz bei Sillian nahe der italienischen Grenze. Kurzer Drautal-Flug.', reason:'Drautal-Route Richtung Osten'},
{id:57, name:'Lienz → Matrei', start:'lienz_c7', end:'matrei', difficulty:'easy', region:'tirol',
waypoints:['lienz_c7','matrei'],
desc:'Notfall in Matrei. Direkter Flug ins Iseltal.', reason:'Iseltal-Route'},
{id:58, name:'Lienz → Kals (Bergrettung)', start:'lienz_c7', end:'kals', difficulty:'medium', region:'tirol',
waypoints:['lienz_c7','matrei','kals'],
desc:'Bergrettung in Kals am Großglockner. Route über Matrei.', reason:'Kalser Tal nur über Matrei erreichbar'},
{id:59, name:'Lienz → Kitzbühel (Transport)', start:'lienz_c7', end:'kitzbuehel_ort', difficulty:'hard', region:'tirol',
waypoints:['lienz_c7','matrei','st_johann','kitzbuehel_ort'],
desc:'Patiententransport nach Kitzbühel. Route über den Felbertauern.', reason:'Felbertauernroute verbindet Ost- und Nordtirol'},
{id:60, name:'Lienz → Innsbruck (Intensivtransport)', start:'lienz_c7', end:'innsbruck_c1', difficulty:'hard', region:'tirol',
waypoints:['lienz_c7','matrei','st_johann','kitzbuehel_ort','woergl','jenbach','schwaz','hall','innsbruck_c1'],
desc:'Intensivtransport nach Innsbruck. Die längste Route — quer durch ganz Tirol.', reason:'Osttirol hat kein eigenes Traumazentrum'},
];
// Farben nach Schwierigkeit
var COLORS = {easy:'#5a8a5e', medium:'#c4a35a', hard:'#c85c4a'};
var routeLayers = [];
function buildSelect(region) {
var sel = document.getElementById('route-select');
sel.innerHTML = '';
var filtered = region === 'all' ? ROUTES : ROUTES.filter(function(r){return r.region===region;});
filtered.forEach(function(r) {
var opt = document.createElement('option');
opt.value = r.id;
var badge = r.difficulty === 'easy' ? '🟢' : r.difficulty === 'medium' ? '🟡' : '🔴';
opt.textContent = badge + ' #' + r.id + ' ' + r.name;
sel.appendChild(opt);
});
}
function showRoute() {
var id = parseInt(document.getElementById('route-select').value);
var r = ROUTES.find(function(x){return x.id===id;});
if (!r) return;
// Clear old
routeLayers.forEach(function(l){map.removeLayer(l);});
routeLayers = [];
// Build points
var latlngs = r.waypoints.map(function(wpId){
var wp = WP[wpId];
return wp ? [wp.lat, wp.lon] : null;
}).filter(Boolean);
// Fit map
if (latlngs.length > 0) map.fitBounds(L.latLngBounds(latlngs).pad(0.15));
// Draw line
var line = L.polyline(latlngs, {color:COLORS[r.difficulty], weight:4, opacity:0.8, dashArray:r.difficulty==='hard'?'8,6':null}).addTo(map);
routeLayers.push(line);
// Waypoint markers
r.waypoints.forEach(function(wpId, i) {
var wp = WP[wpId];
if (!wp) return;
var isStart = i === 0;
var isEnd = i === r.waypoints.length - 1;
var cls = isStart ? 'wp-marker start' : isEnd ? 'wp-marker end' : 'wp-marker';
var label = isStart ? '🚁' : isEnd ? '🎯' : (i);
var size = (isStart||isEnd) ? 28 : 22;
var m = L.marker([wp.lat, wp.lon], {
icon: L.divIcon({className:'', html:'<div class="'+cls+'">'+label+'</div>', iconSize:[size,size], iconAnchor:[size/2,size/2]}),
zIndexOffset: (isStart||isEnd) ? 1000 : 500
}).addTo(map).bindPopup('<strong>'+wp.name+'</strong><br><span style="font-size:10px;color:#8a8a8a">Waypoint '+(i+1)+' von '+r.waypoints.length+'</span>');
routeLayers.push(m);
// Label
var lbl = L.marker([wp.lat, wp.lon], {
icon: L.divIcon({className:'wp-label', html:wp.name, iconSize:[120,14], iconAnchor:[-14,7]}),
interactive:false
}).addTo(map);
routeLayers.push(lbl);
});
// Description
var diffLabel = r.difficulty==='easy'?'Einfach':r.difficulty==='medium'?'Mittel':'Schwierig';
var diffBadge = '<span class="badge badge-'+r.difficulty+'">'+diffLabel+'</span>';
document.getElementById('route-desc').innerHTML =
'<h3>#'+r.id+' '+r.name+' '+diffBadge+'</h3>'
+'<p>'+r.desc+'</p>'
+'<div class="meta">Grund für diese Route: <strong>'+r.reason+'</strong> · '+r.waypoints.length+' Wegpunkte</div>';
}
function filterRoutes() {
var region = document.getElementById('region-select').value;
buildSelect(region);
showRoute();
}
buildSelect('all');
showRoute();
</script>
</body>
</html>