Files
geograsim/App/pages/heli-uebersicht.php
T
Adminator 1e51ef7def Nachtrag: alle bisher untracked Ordner + hängende Änderungen mit-committen
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/
- 12 code-workspace-Files
- STATUS-*.md
- viele M/D/R-Änderungen an bereits getrackten Files
- .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-08 02:27:02 +02:00

439 lines
35 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Heli-Uebersichtskarte V2 — vollstaendige Karte mit Quartett-Cards.
*
* Inhalt:
* - 33 Heli-Stuetzpunkte (Notarzt + Bergrettung + Polizei + Bundesheer + Krankenhaeuser)
* mit Quartett-Popup (Heli-Typ-Bild, Daten, Wikipedia-Link)
* - Anzeige rechtssicher ohne Markennamen (Notarzt-Heli, Notarzt-Heli, ARA, Libelle).
* CSS-Klassen oeamtc/ara/police/bh sind interne Konvention fuer Farb-Codierung.
* Siehe sims/heli/data/heli-stations.json + docs/konzept-heli-kennzeichen.md.
* - 16 Staedte > 30k Einwohner mit KI-Bildern + Fakten + Wiki
* - 14 Berggipfel mit Daten + Wiki
* - 25 Mission-Zielmarker (rot, mit ID-Badge) — Spiegel zu ALL_MISSIONS
* - Filter-Pills: Alles / Stuetzpunkte / Staedte / Berge / Missionen
*
* Das war frueher unter /heli.html (V1) und ist jetzt unter /heli-uebersicht.
*/
require_once __DIR__ . '/../php/config/app.php';
require_once __DIR__ . '/../php/config/db.php';
$db = getDB();
// === Mission-Marker aus den 25 Missionen (Spiegel zu ALL_MISSIONS) ===
$missions = [
['id'=>'m1', 'title'=>'Skiunfall Damüls', 'target'=>'bergrettung_muels', 'icon'=>'⛷️'],
['id'=>'m2', 'title'=>'Verkehrsunfall A14', 'target'=>'verkehr_a14', 'icon'=>'🚗'],
['id'=>'m3', 'title'=>'Bergrettung Brand', 'target'=>'brand', 'icon'=>'🧗'],
['id'=>'m4', 'title'=>'Skiunfall Sölden', 'target'=>'skiunfall_soelden', 'icon'=>'⛷️'],
['id'=>'m5', 'title'=>'Seerettung Bregenz', 'target'=>'bregenz', 'icon'=>'🌊'],
['id'=>'m6', 'title'=>'Canyoning Mellau', 'target'=>'mellau', 'icon'=>'🏞️'],
['id'=>'m7', 'title'=>'Wanderunfall Kufstein', 'target'=>'kufstein', 'icon'=>'🥾'],
['id'=>'m8', 'title'=>'Notfall Gaschurn', 'target'=>'gaschurn', 'icon'=>'🚑'],
['id'=>'m9', 'title'=>'Lawineneinsatz Mayrhofen', 'target'=>'mayrhofen', 'icon'=>'❄️'],
['id'=>'m10', 'title'=>'Verkehrsunfall A2 Baden', 'target'=>'verkehr_a2_baden', 'icon'=>'🚗'],
['id'=>'m11', 'title'=>'Sog in der Neuen Donau', 'target'=>'see_donau_wien', 'icon'=>'🌊'],
['id'=>'m12', 'title'=>'Kletterunfall Schneeberg', 'target'=>'berg_schneeberg', 'icon'=>'🧗'],
['id'=>'m13', 'title'=>'Wanderunfall Rax', 'target'=>'berg_rax', 'icon'=>'🥾'],
['id'=>'m14', 'title'=>'Surfer Neusiedler See', 'target'=>'see_neusiedl', 'icon'=>'🌊'],
['id'=>'m15', 'title'=>'Verkehrsunfall A1 Sattledt','target'=>'verkehr_a1_sattledt', 'icon'=>'🚗'],
['id'=>'m16', 'title'=>'Boot in Not Traunsee', 'target'=>'see_traun', 'icon'=>'🌊'],
['id'=>'m17', 'title'=>'LKW-Unfall A10 Tauern', 'target'=>'verkehr_a10_tauern', 'icon'=>'🚗'],
['id'=>'m18', 'title'=>'Seerettung Wolfgangsee', 'target'=>'see_wolfgang', 'icon'=>'🌊'],
['id'=>'m19', 'title'=>'Absturz Großglockner', 'target'=>'berg_glockner', 'icon'=>'❄️'],
['id'=>'m20', 'title'=>'Dachstein Südwand', 'target'=>'berg_dachstein', 'icon'=>'❄️'],
['id'=>'m21', 'title'=>'Alpinunfall Hochschwab', 'target'=>'berg_hochschwab', 'icon'=>'🧗'],
['id'=>'m22', 'title'=>'Unfall A9 Bosruck', 'target'=>'verkehr_a9_pyhrn', 'icon'=>'🚗'],
['id'=>'m23', 'title'=>'Badegast Wörthersee', 'target'=>'see_woerth', 'icon'=>'🌊'],
['id'=>'m24', 'title'=>'Glockner Südseite', 'target'=>'berg_glockner', 'icon'=>'❄️'],
['id'=>'m25', 'title'=>'Unfall A2 Klagenfurt', 'target'=>'verkehr_a2_klagenfurt', 'icon'=>'🚗'],
];
// Mission-Wegpunkt-Koordinaten aus DB ziehen (target → wp_key → lat/lon)
$wpRows = $db->query("SELECT wp_key, name, lat, lon, region FROM geo_waypoints")->fetchAll(PDO::FETCH_ASSOC);
$wpByKey = [];
foreach ($wpRows as $w) $wpByKey[$w['wp_key']] = $w;
$missionMarkers = [];
foreach ($missions as $m) {
if (!isset($wpByKey[$m['target']])) continue;
$wp = $wpByKey[$m['target']];
$missionMarkers[] = [
'id' => $m['id'], 'title' => $m['title'], 'icon' => $m['icon'],
'lat' => (float)$wp['lat'], 'lon' => (float)$wp['lon'],
'wpName' => $wp['name'], 'region' => $wp['region'],
];
}
$mmJson = json_encode($missionMarkers, JSON_UNESCAPED_UNICODE);
$base = BASE_PATH;
// === Bild-Pfad-Aufloesung mit Fallback-Kette ===
// Server-seitig pruefen wir pro Asset den ersten existierenden Pfad.
$appRoot = realpath(__DIR__ . '/..');
function firstExisting(array $relPaths, string $appRoot): string {
foreach ($relPaths as $rel) {
if (file_exists($appRoot . '/' . $rel)) return $rel;
}
return $relPaths[0]; // Letzter Fallback (existiert evtl. nicht, aber Browser meldet 404)
}
$cityKeys = ['wien','graz','linz','salzburg','innsbruck','klagenfurt','villach','wels','stpoelten','dornbirn','wiener_neustadt','steyr','feldkirch','bregenz','leoben','eisenstadt'];
$cityImg = [];
foreach ($cityKeys as $k) {
$cityImg[$k] = firstExisting([
"sims/heli/assets/cities-panorama/$k.png", // 1. Neue Heli-Panoramen (Stil + Wahrzeichen)
"sims/logistik/assets/cities/$k.png", // 2. Logistik-Panoramen (4 Treffer: wien/graz/salzburg/innsbruck)
"sims/heli/assets/cities/$k.png", // 3. Alte Kreis-Bilder (Fallback)
], $appRoot);
}
$heliKeys = ['heli-ec135','heli-h145','heli-aw169','heli-ab212'];
$heliImg = [];
foreach ($heliKeys as $k) {
$heliImg[$k] = firstExisting([
"sims/heli/assets/helis-new/$k.png", // 1. Neue typgetreue Bilder
"sims/heli/assets/$k.png", // 2. Alte Bilder
], $appRoot);
}
$cityImgJson = json_encode($cityImg);
$heliImgJson = json_encode($heliImg);
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>GeoGraSim — Helikopter-Übersichtskarte</title>
<link rel="icon" type="image/png" href="<?= $base ?>/favicon-96x96.png" sizes="96x96">
<link rel="stylesheet" href="<?= $base ?>/assets/vendor/leaflet/leaflet.css">
<link rel="stylesheet" href="<?= $base ?>/assets/fonts/inter.css">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Inter',system-ui,sans-serif;overflow:hidden;height:100dvh;background:#e8e4d8}
#map{width:100%;height:100dvh}
/* === Heli-Marker (V1-Stil) === */
.marker-circle{width:26px;height:26px;background:#fff;border:2.5px solid #1f4e5a;border-radius:50%;box-shadow:0 2px 6px rgba(0,0,0,.3);display:flex;align-items:center;justify-content:center;font-size:13px;line-height:1}
.marker-circle.capital{border-color:#e8833a;width:30px;height:30px;font-size:15px}
.marker-circle.peak{border-color:#5a8a5e;width:24px;height:24px;font-size:12px}
.marker-circle.heli-m{border-color:#d4a017;width:30px;height:30px;font-size:16px}
.marker-circle.heli-m.inactive{width:22px;height:22px;font-size:11px;border-color:#888;opacity:.7}
/* Pills */
.heli-pill{font-family:'Inter',sans-serif;font-weight:600;font-size:11px;white-space:nowrap;padding:3px 8px;border-radius:5px;background:rgba(255,255,255,.92);box-shadow:0 1px 4px rgba(0,0,0,.15);line-height:1.3;max-width:180px}
.heli-pill .pill-name{font-weight:800;display:block}
.heli-pill .pill-org{font-size:8px;opacity:.7;display:block}
.heli-pill.oeamtc{border-left:4px solid #d4a017;color:#7a5f00}
.heli-pill.ara{border-left:4px solid #cc3333;color:#882222}
.heli-pill.police{border-left:4px solid #2255aa;color:#1a3a77}
.heli-pill.bh{border-left:4px solid #228833;color:#145522}
.heli-pill-sm{font-family:'Inter',sans-serif;font-weight:700;font-size:10px;white-space:nowrap;padding:2px 6px;border-radius:3px;background:rgba(255,255,255,.88);box-shadow:0 1px 3px rgba(0,0,0,.15);color:#555;border-left:3px solid #888}
/* Stadt + Berg-Pills */
.city-pill{font-family:'Inter',sans-serif;font-weight:700;font-size:11px;white-space:nowrap;padding:4px 10px;border-radius:5px;background:rgba(218,232,236,.94);box-shadow:0 1px 4px rgba(0,0,0,.15);color:#1f4e5a;border-bottom:2.5px solid #4a7c8a;display:flex;align-items:center;height:22px}
.city-pill.capital{color:#e8833a;background:rgba(232,200,170,.92);border-bottom-color:#e8833a}
.city-pill .city-pop{font-size:8px;font-weight:500;color:#8a8a8a;margin-left:4px}
.peak-pill{font-family:'Inter',sans-serif;font-weight:700;font-size:10px;white-space:nowrap;padding:4px 9px;border-radius:5px;background:rgba(220,234,221,.94);box-shadow:0 1px 4px rgba(0,0,0,.12);color:#3a6b3e;border-bottom:2.5px solid #5a8a5e;display:flex;align-items:center;height:20px}
.peak-pill .peak-ele{font-size:8px;font-weight:500;color:#5a8a5e;margin-left:4px}
/* Quartett-Popup */
.heli-popup .leaflet-popup-content{margin:0;padding:0}
.heli-popup .leaflet-popup-content-wrapper{border-radius:12px;padding:0;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,.15)}
.quartett{width:280px;font-family:'Inter',sans-serif}
/* Heli: 1:1 Bild, contain (Heli komplett sichtbar mit Rand) */
.quartett-img{height:160px;overflow:hidden;background:#f7f6f3;display:flex;align-items:center;justify-content:center}
.quartett-img img{width:100%;height:100%;object-fit:contain}
/* Stadt: 16:9 Bild, cover (Panorama formatfuellend) */
.city-popup .quartett-img,.city-img-box{height:158px;overflow:hidden;background:#f7f6f3}
.city-popup .quartett-img img,.city-img-box img{width:100%;height:100%;object-fit:cover}
.quartett-body{padding:10px 12px 12px}
.quartett-name{font-size:13px;font-weight:800;color:#1f4e5a;margin-bottom:2px}
.quartett-org{font-size:10px;color:#8a8a8a;margin-bottom:8px}
.quartett-stats{display:grid;grid-template-columns:1fr 1fr;gap:4px}
.quartett-stat{background:#f7f6f3;border-radius:5px;padding:4px 6px}
.quartett-stat .qs-label{font-size:8px;color:#8a8a8a;font-weight:500}
.quartett-stat .qs-val{font-size:12px;font-weight:800;color:#1f4e5a}
.quartett-fact{margin-top:8px;font-size:10px;color:#4a4a4a;font-style:italic;line-height:1.4;border-top:1px solid #eee;padding-top:6px}
/* Mission-Zielmarker (rot, mit ID-Badge) */
.marker-mission{width:34px;height:34px;background:#c85c4a;border:3px solid #fff;border-radius:50%;box-shadow:0 3px 10px rgba(200,92,74,.55);display:flex;align-items:center;justify-content:center;font-size:18px;color:#fff;position:relative}
.marker-mission .mid{position:absolute;bottom:-8px;right:-10px;background:#1f4b37;color:#fff;font-size:9px;font-weight:800;padding:1px 5px;border-radius:8px;border:2px solid #fff;line-height:1.2}
.wp-pill-mission{font-family:'Inter',sans-serif;font-weight:700;font-size:11px;white-space:nowrap;padding:3px 9px;border-radius:5px;background:rgba(200,92,74,.95);color:#fff;border-bottom:2.5px solid #8a3a2a;display:flex;align-items:center;height:20px;box-shadow:0 1px 4px rgba(0,0,0,.18)}
/* Header-Pills */
.ueb-sim-pill{position:fixed;top:14px;right:64px;z-index:5000;background:#1f4b37;color:#fff;border-radius:12px;box-shadow:0 4px 14px rgba(0,0,0,.25);padding:10px 18px;font-size:.9rem;font-weight:700;text-decoration:none;display:flex;align-items:center;gap:8px;border:2px solid transparent}
.ueb-sim-pill:hover{background:#2d6e4e}
.ueb-back-pill{position:fixed;top:14px;left:14px;z-index:5000;background:#fff;border-radius:12px;box-shadow:0 4px 12px rgba(0,0,0,.18);padding:8px 14px;font-size:.85rem;font-weight:700;color:#1f4b37;text-decoration:none;display:flex;align-items:center;gap:6px}
.ueb-back-pill:hover{background:#f5f3ea}
/* Filter-Pills */
.filter-bar{position:fixed;top:60px;left:14px;z-index:4900;background:rgba(255,255,255,.95);border-radius:10px;box-shadow:0 3px 10px rgba(0,0,0,.14);padding:6px 8px;display:flex;gap:4px;align-items:center;flex-wrap:wrap;max-width:calc(100vw - 28px)}
.filter-bar button{font-family:inherit;background:transparent;border:1.5px solid transparent;padding:4px 10px;border-radius:6px;font-size:.78rem;font-weight:700;cursor:pointer;color:#1f4b37}
.filter-bar button.on{background:#1f4b37;color:#fff}
.filter-bar button:not(.on):hover{background:#f0eee5}
</style>
</head>
<body>
<a href="<?= $base ?>/sim" class="ueb-back-pill">← Cockpit</a>
<a href="<?= $base ?>/heli-game" class="ueb-sim-pill">🚁 Zur Simulation</a>
<div class="filter-bar" id="filterBar">
<span style="font-size:.7rem;color:#5a5a5a;margin-right:4px">Anzeigen:</span>
<button data-filter="all" class="on">Alles</button>
<button data-filter="bases">Stützpunkte</button>
<button data-filter="cities">Städte</button>
<button data-filter="peaks">Berge</button>
<button data-filter="missions">Missionen</button>
</div>
<div id="map"></div>
<script src="<?= $base ?>/assets/vendor/leaflet/leaflet.js"></script>
<script>
const BASE_PATH = '<?= $base ?>';
const MISSION_MARKERS = <?= $mmJson ?>;
const CITY_IMG = <?= $cityImgJson ?>;
const HELI_IMG = <?= $heliImgJson ?>;
function cityImgUrl(name){ return BASE_PATH + '/' + (CITY_IMG[name] || 'sims/heli/assets/cities/'+name+'.png'); }
function heliImgUrl(name){ return BASE_PATH + '/' + (HELI_IMG[name] || 'sims/heli/assets/'+name+'.png'); }
// === Karte + Tile-Layer (DSGVO-Proxy) ===
var map = L.map('map', {
zoomControl: false, maxBoundsViscosity:1.0, minZoom:7, maxZoom:15
}).setView([47.4, 13.2], 8);
L.tileLayer(BASE_PATH + '/php/tile-proxy.php?z={z}&x={x}&y={y}&p=osm', {
attribution:'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>', maxZoom:18
}).addTo(map);
L.control.zoom({position:'topright'}).addTo(map);
L.control.scale({position:'bottomleft', metric:true, imperial:false, maxWidth:200}).addTo(map);
// === Layer-Gruppen für Filter ===
var L_BASES = L.layerGroup().addTo(map);
var L_CITIES = L.layerGroup().addTo(map);
var L_PEAKS = L.layerGroup().addTo(map);
var L_MISSIONS = L.layerGroup().addTo(map);
// === HUBSCHRAUBER-TYPEN (Quartett-Daten) ===
var HELI_TYPES = {
'EC135': {name:'Eurocopter EC135', img:'sims/heli/assets/heli-ec135.png', engines:2, crew:'1 Pilot + 1 Notarzt + 1 Sanitäter', maxSpeed:254, range:635, ceiling:6096, weight:2910, seats:6, role:'Rettung & Notfall', fact:'Der EC135 ist der meistgenutzte Rettungshubschrauber in Europa.', wiki:'https://de.wikipedia.org/wiki/Eurocopter_EC_135'},
'H145': {name:'Airbus H145', img:'sims/heli/assets/heli-h145.png', engines:2, crew:'12 Piloten + Rettungsteam', maxSpeed:268, range:680, ceiling:5485, weight:3700, seats:8, role:'Rettung & Intensivtransport', fact:'Der H145 kann auch bei Nacht und schlechtem Wetter fliegen (IFR).', wiki:'https://de.wikipedia.org/wiki/Airbus_Helicopters_H145'},
'AW169': {name:'Leonardo AW169', img:'sims/heli/assets/heli-aw169.png', engines:2, crew:'12 Pilot*innen + Besatzung', maxSpeed:280, range:820, ceiling:4572, weight:4600, seats:10, role:'Polizei & Überwachung', fact:'Wird oft bei Polizei-Einsätzen und in der zivilen Suchrettung eingesetzt.', wiki:'https://de.wikipedia.org/wiki/Leonardo_AW169'},
'AB212': {name:'Agusta-Bell AB212', img:'sims/heli/assets/heli-ab212.png', engines:2, crew:'2 Pilot*innen + Besatzung', maxSpeed:240, range:450, ceiling:4630, weight:5070, seats:14, role:'Militär & Katastrophenhilfe', fact:'Das Bundesheer setzt den AB212 seit den 1980er-Jahren ein.', wiki:'https://de.wikipedia.org/wiki/Agusta-Bell_AB_212'},
};
// === ALLE HUBSCHRAUBER-BASEN ===
var bases = [
// Notarzt-Hubschrauber (14 Stuetzpunkte). code = generisches Funkzeichen
// (rechtssicher; echte ist 'Notarzt-Heli N'). name = Standort.
{name:'Innsbruck', code:'N1', org:'Notarzt-Hubschrauber', orgClass:'oeamtc', lat:47.2600, lon:11.3439, active:true, heli:'EC135', region:'Innsbruck / Nordtirol'},
{name:'Krems', code:'N2', org:'Notarzt-Hubschrauber', orgClass:'oeamtc', lat:48.4306, lon:15.6100, active:true, heli:'EC135', region:'Krems / Waldviertel'},
{name:'Wiener Neustadt-Ost', code:'N3', org:'Notarzt-Hubschrauber', orgClass:'oeamtc', lat:47.8419, lon:16.2331, active:true, heli:'EC135', region:'Wiener Neustadt'},
{name:'Reith bei Kitzbühel', code:'N4', org:'Notarzt-Hubschrauber', orgClass:'oeamtc', lat:47.3801, lon:12.3580, active:true, heli:'H145', region:'Reith bei Kitzbühel'},
{name:'Zams', code:'N5', org:'Notarzt-Hubschrauber', orgClass:'oeamtc', lat:47.1597, lon:10.5933, active:true, heli:'H145', region:'Zams / Oberinntal'},
{name:'Salzburg-Flughafen', code:'N6', org:'Notarzt-Hubschrauber', orgClass:'oeamtc', lat:47.7950, lon:13.0044, active:true, heli:'H145', region:'Salzburg'},
{name:'Lienz-Nikolsdorf', code:'N7', org:'Notarzt-Hubschrauber', orgClass:'oeamtc', lat:46.8297, lon:12.7647, active:true, heli:'EC135', region:'Lienz / Osttirol'},
{name:'Nenzing', code:'N8', org:'Notarzt-Hubschrauber', orgClass:'oeamtc', lat:47.1828, lon:9.7072, active:true, heli:'H145', region:'Nenzing / Vorarlberg'},
{name:'Wien-Aspern', code:'N9', org:'Notarzt-Hubschrauber', orgClass:'oeamtc', lat:48.1172, lon:16.5025, active:true, heli:'EC135', region:'Wien'},
{name:'Linz-Hörsching', code:'N10', org:'Notarzt-Hubschrauber', orgClass:'oeamtc', lat:48.2331, lon:14.1875, active:true, heli:'EC135', region:'Linz'},
{name:'Klagenfurt-Flughafen',code:'N11', org:'Notarzt-Hubschrauber', orgClass:'oeamtc', lat:46.6503, lon:14.3372, active:true, heli:'EC135', region:'Klagenfurt'},
{name:'Graz-Thalerhof', code:'N12', org:'Notarzt-Hubschrauber', orgClass:'oeamtc', lat:47.0094, lon:15.4106, active:true, heli:'EC135', region:'Graz'},
{name:'Niederöblarn', code:'N14', org:'Notarzt-Hubschrauber', orgClass:'oeamtc', lat:47.4756, lon:14.0042, active:true, heli:'H145', region:'Niederöblarn'},
{name:'Pinkafeld-Oberwart', code:'N16', org:'Notarzt-Hubschrauber', orgClass:'oeamtc', lat:47.3833, lon:16.1167, active:true, heli:'EC135', region:'Pinkafeld / Burgenland'},
// Bergrettungs-Hubschrauber (3 Stuetzpunkte)
{name:'Ehenbichl-Reutte', code:'B1', org:'Bergrettungs-Hubschrauber', orgClass:'ara', lat:47.4858, lon:10.7181, active:false, heli:'H145', region:'Reutte / Außerfern'},
{name:'Fresach', code:'B2', org:'Bergrettungs-Hubschrauber', orgClass:'ara', lat:46.7325, lon:13.7100, active:false, heli:'H145', region:'Fresach / Kärnten'},
// Polizei-Hubschrauber (7 Standorte). code = generisches Polizei-Funkzeichen.
{name:'Innsbruck', code:'P1', org:'Polizei-Hubschrauber', orgClass:'police', lat:47.2583, lon:11.3556, active:false, heli:'AW169', region:'Innsbruck'},
{name:'Hohenems-Dornbirn', code:'P2', org:'Polizei-Hubschrauber', orgClass:'police', lat:47.38440,lon:9.69930, active:true, heli:'AW169', region:'Hohenems / Rheintal'},
{name:'Wien-Meidling', code:'P3', org:'Polizei-Hubschrauber', orgClass:'police', lat:48.1103, lon:16.5697, active:false, heli:'AW169', region:'Wien'},
{name:'Salzburg', code:'P4', org:'Polizei-Hubschrauber', orgClass:'police', lat:47.7933, lon:13.0033, active:false, heli:'AW169', region:'Salzburg'},
{name:'Klagenfurt', code:'P5', org:'Polizei-Hubschrauber', orgClass:'police', lat:46.6428, lon:14.3375, active:false, heli:'AW169', region:'Klagenfurt'},
{name:'Linz', code:'P6', org:'Polizei-Hubschrauber', orgClass:'police', lat:48.2400, lon:14.1867, active:false, heli:'AW169', region:'Linz'},
{name:'Graz', code:'P7', org:'Polizei-Hubschrauber', orgClass:'police', lat:47.0094, lon:15.4400, active:false, heli:'AW169', region:'Graz'},
// Bundesheer (3 Stuetzpunkte). 'Bundesheer' ist Behoerdenname, kein Markenname.
{name:'Aigen im Ennstal', code:'M1', org:'Bundesheer', orgClass:'bh', lat:47.5236, lon:14.1350, active:false, heli:'AB212', region:'Aigen / Ennstal'},
{name:'Höttinger Au', code:'M2', org:'Bundesheer', orgClass:'bh', lat:47.2722, lon:11.3753, active:false, heli:'AB212', region:'Innsbruck'},
{name:'Langenlebarn', code:'M3', org:'Bundesheer', orgClass:'bh', lat:48.3239, lon:15.9836, active:false, heli:'AB212', region:'Langenlebarn / Tullnerfeld'},
];
function makeHeliPopup(b) {
var t = HELI_TYPES[b.heli];
if (!t) return '<strong>' + b.name + '</strong><br>' + b.org;
// Heli-Bild-Lookup mit Fallback (heli-ec135 → 'heli-ec135')
var imgKey = (t.img || '').replace(/^.*\//,'').replace(/\.png$/,'');
return '<div class="quartett">'
+ '<div class="quartett-img"><img src="' + heliImgUrl(imgKey) + '" alt="' + t.name + '"></div>'
+ '<div class="quartett-body">'
+ '<div class="quartett-name">' + b.code + ' — ' + b.name + '</div>'
+ '<div class="quartett-org">' + b.org + ' · ' + (b.region||'') + '</div>'
+ '<div class="quartett-stats">'
+ '<div class="quartett-stat"><div class="qs-label">Typ</div><div class="qs-val">' + t.name + '</div></div>'
+ '<div class="quartett-stat"><div class="qs-label">Max. Speed</div><div class="qs-val">' + t.maxSpeed + ' km/h</div></div>'
+ '<div class="quartett-stat"><div class="qs-label">Reichweite</div><div class="qs-val">' + t.range + ' km</div></div>'
+ '<div class="quartett-stat"><div class="qs-label">Gipfelhöhe</div><div class="qs-val">' + t.ceiling + ' m</div></div>'
+ '<div class="quartett-stat"><div class="qs-label">Triebwerke</div><div class="qs-val">' + t.engines + '</div></div>'
+ '<div class="quartett-stat"><div class="qs-label">Besatzung</div><div class="qs-val">' + t.crew + '</div></div>'
+ '</div>'
+ '<div class="quartett-fact">💡 ' + t.fact + '</div>'
+ (t.wiki ? '<div style="text-align:center;margin-top:6px"><a href="'+t.wiki+'" target="_blank" style="font-size:10px;color:#4a7c8a;text-decoration:none;font-weight:600">📖 Wikipedia →</a></div>' : '')
+ '</div></div>';
}
bases.forEach(function(b) {
var size = b.active ? 30 : 22;
var circleClass = b.active ? 'marker-circle heli-m' : 'marker-circle heli-m inactive';
L.marker([b.lat, b.lon], {
icon: L.divIcon({className:'', html:'<div class="'+circleClass+'">🚁</div>', iconSize:[size,size], iconAnchor:[size/2,size/2]}),
title: b.name+' ('+b.org+')', zIndexOffset: b.active ? 1000 : 0
}).addTo(L_BASES).bindPopup(makeHeliPopup(b), {maxWidth:280, className:'heli-popup'});
if (b.active) {
L.marker([b.lat, b.lon], {
icon: L.divIcon({className:'heli-pill '+b.orgClass, html:'<span class="pill-name">'+b.code+' '+b.name+'</span><span class="pill-org">'+b.org+'</span>', iconSize:[160,28], iconAnchor:[-20,14]}),
interactive:false, zIndexOffset:900
}).addTo(L_BASES);
} else {
L.marker([b.lat, b.lon], {
icon: L.divIcon({className:'heli-pill-sm', html:b.code, iconSize:[40,14], iconAnchor:[-10,7]}),
interactive:false
}).addTo(L_BASES);
}
});
// === STÄDTE > 30k Einwohner + Bregenz/Eisenstadt ===
var cities = [
{name:'Wien', lat:48.2082, lon:16.3738, pop:1982097, capital:true, land:'Wien', fun:'Hat mehr Einwohner*innen als alle anderen Landeshauptstädte zusammen!', fact:'Wurde 5× zur lebenswertesten Stadt der Welt gewählt.', wiki:'https://de.wikipedia.org/wiki/Wien', img:'sims/heli/assets/cities/wien.png'},
{name:'Graz', lat:47.0707, lon:15.4395, pop:295424, capital:true, land:'Steiermark', fun:'Die Altstadt ist UNESCO-Weltkulturerbe.', fact:'Zweitgrößte Stadt Österreichs, Universitätsstadt mit über 60.000 Studierenden.', wiki:'https://de.wikipedia.org/wiki/Graz', img:'sims/heli/assets/cities/graz.png'},
{name:'Linz', lat:48.3069, lon:14.2858, pop:210118, capital:true, land:'Oberösterreich', fun:'War 2009 Europäische Kulturhauptstadt.', fact:'Wichtigster Industriestandort Österreichs (Stahl, Chemie).', wiki:'https://de.wikipedia.org/wiki/Linz', img:'sims/heli/assets/cities/linz.png'},
{name:'Salzburg', lat:47.8095, lon:13.0550, pop:157745, capital:true, land:'Salzburg', fun:'Geburtsstadt von Wolfgang Amadeus Mozart (1756).', fact:'Die Altstadt gehört zum UNESCO-Weltkulturerbe.', wiki:'https://de.wikipedia.org/wiki/Salzburg', img:'sims/heli/assets/cities/salzburg.png'},
{name:'Innsbruck', lat:47.2654, lon:11.3928, pop:130585, capital:true, land:'Tirol', fun:'Hat schon 3× Olympische Winterspiele ausgerichtet!', fact:'Liegt auf 574 m im Inntal, umgeben von Bergen bis 2.300 m.', wiki:'https://de.wikipedia.org/wiki/Innsbruck', img:'sims/heli/assets/cities/innsbruck.png'},
{name:'Klagenfurt', lat:46.6247, lon:14.3050, pop:103671, capital:true, land:'Kärnten', fun:'Am Wörthersee — einem der wärmsten Badeseen der Alpen.', fact:'Südlichste Landeshauptstadt Österreichs.', wiki:'https://de.wikipedia.org/wiki/Klagenfurt_am_Wörthersee', img:'sims/heli/assets/cities/klagenfurt.png'},
{name:'Villach', lat:46.6111, lon:13.8558, pop:65127, capital:false, land:'Kärnten', fun:'Hat einen eigenen Fasching — den Villacher Fasching!', fact:'Zweitgrößte Stadt Kärntens, wichtiger Verkehrsknotenpunkt Richtung Italien/Slowenien.', wiki:'https://de.wikipedia.org/wiki/Villach', img:'sims/heli/assets/cities/villach.png'},
{name:'Wels', lat:48.1575, lon:14.0289, pop:63539, capital:false, land:'Oberösterreich', fun:'Hier findet die größte Agrarmesse Österreichs statt.', fact:'Zweitgrößte Stadt Oberösterreichs.', wiki:'https://de.wikipedia.org/wiki/Wels_(Stadt)', img:'sims/heli/assets/cities/wels.png'},
{name:'St. Pölten', lat:48.2047, lon:15.6256, pop:57169, capital:true, land:'Niederösterreich', fun:'Jüngste Landeshauptstadt — erst seit 1986!', fact:'Vorher war Wien auch Hauptstadt von Niederösterreich.', wiki:'https://de.wikipedia.org/wiki/St._Pölten', img:'sims/heli/assets/cities/stpoelten.png'},
{name:'Dornbirn', lat:47.4136, lon:9.7424, pop:51222, capital:false, land:'Vorarlberg', fun:'Größte Stadt Vorarlbergs — nicht Bregenz!', fact:'Wichtiger Messestandort und Wirtschaftszentrum im Rheintal.', wiki:'https://de.wikipedia.org/wiki/Dornbirn', img:'sims/heli/assets/cities/dornbirn.png'},
{name:'Wiener Neustadt', lat:47.8133, lon:16.2467, pop:48519, capital:false, land:'Niederösterreich', fun:'Hier wurde die erste Militärakademie der Welt gegründet (1752).', fact:'Wichtiger Standort des Bundesheers.', wiki:'https://de.wikipedia.org/wiki/Wiener_Neustadt', img:'sims/heli/assets/cities/wiener_neustadt.png'},
{name:'Steyr', lat:48.0386, lon:14.4214, pop:38572, capital:false, land:'Oberösterreich', fun:'Die Altstadt sieht aus wie ein Märchen — mit Flüssen und mittelalterlichen Häusern.', fact:'Berühmt für Stahl und Waffen seit dem Mittelalter.', wiki:'https://de.wikipedia.org/wiki/Steyr', img:'sims/heli/assets/cities/steyr.png'},
{name:'Feldkirch', lat:47.2376, lon:9.5982, pop:35793, capital:false, land:'Vorarlberg', fun:'Hat eine echte mittelalterliche Burg — die Schattenburg.', fact:'Grenzstadt zur Schweiz und Liechtenstein.', wiki:'https://de.wikipedia.org/wiki/Feldkirch', img:'sims/heli/assets/cities/feldkirch.png'},
{name:'Bregenz', lat:47.5026, lon:9.7473, pop:29620, capital:true, land:'Vorarlberg', fun:'Die Seebühne auf dem Bodensee ist die größte Freilichtbühne der Welt!', fact:'Kleinste Landeshauptstadt Österreichs.', wiki:'https://de.wikipedia.org/wiki/Bregenz', img:'sims/heli/assets/cities/bregenz.png'},
{name:'Leoben', lat:47.3765, lon:15.0910, pop:32768, capital:false, land:'Steiermark', fun:'Hier wird an der Montanuniversität Bergbau studiert.', fact:'War jahrhundertelang das Zentrum des steirischen Erzbergbaus.', wiki:'https://de.wikipedia.org/wiki/Leoben', img:'sims/heli/assets/cities/leoben.png'},
{name:'Eisenstadt', lat:47.8456, lon:16.5189, pop:15073, capital:true, land:'Burgenland', fun:'Hier lebte der Komponist Joseph Haydn über 30 Jahre.', fact:'Kleinste Landeshauptstadt nach Einwohnern. Schloss Esterházy ist das Wahrzeichen.', wiki:'https://de.wikipedia.org/wiki/Eisenstadt', img:'sims/heli/assets/cities/eisenstadt.png'},
];
cities.forEach(function(c) {
var dotSize = c.capital ? 30 : 26;
var capitalBadge = c.capital ? '<div style="display:inline-block;background:#e8833a;color:#fff;font-size:9px;font-weight:700;padding:2px 8px;border-radius:99px;margin-bottom:6px">★ Landeshauptstadt' + (c.land ? ' von ' + c.land : '') + '</div>' : (c.land ? '<div style="font-size:10px;color:#8a8a8a;margin-bottom:4px">' + c.land + '</div>' : '');
// Stadt-Key aus img-Pfad: 'sims/heli/assets/cities/wien.png' → 'wien'
var cityKey = (c.img||'').replace(/^.*\//,'').replace(/\.png$/,'');
L.marker([c.lat, c.lon], {
icon: L.divIcon({className:'', html:'<div class="marker-circle'+(c.capital?' capital':'')+'">🏙️</div>', iconSize:[dotSize,dotSize], iconAnchor:[dotSize/2,dotSize/2]}),
zIndexOffset: c.capital ? 500 : 100
}).addTo(L_CITIES).bindPopup(
'<div style="font-family:Inter,sans-serif;width:280px;padding:0">'
+ (cityKey ? '<div class="city-img-box" style="border-radius:8px 8px 0 0;margin:-12px -12px 8px"><img src="'+cityImgUrl(cityKey)+'" alt="'+c.name+'"></div>' : '')
+ '<div style="text-align:center">'
+ capitalBadge
+ '<div style="font-size:16px;font-weight:900;color:#1f4e5a;margin-bottom:2px">' + c.name + '</div>'
+ '<div style="font-size:22px;font-weight:900;color:#4a7c8a">' + c.pop.toLocaleString() + '</div>'
+ '<div style="font-size:9px;color:#8a8a8a;margin-bottom:8px">Einwohner*innen</div>'
+ '</div>'
+ (c.fun ? '<div style="font-size:11px;color:#2d5434;background:rgba(90,138,94,.08);border-radius:6px;padding:6px 8px;margin-bottom:6px">🎯 ' + c.fun + '</div>' : '')
+ (c.fact ? '<div style="font-size:11px;color:#4a4a4a;line-height:1.4;margin-bottom:6px">📚 ' + c.fact + '</div>' : '')
+ (c.wiki ? '<div style="text-align:center"><a href="'+c.wiki+'" target="_blank" style="font-size:10px;color:#4a7c8a;text-decoration:none;font-weight:600">📖 Wikipedia →</a></div>' : '')
+ '</div>'
, {maxWidth:280, className:'city-popup'});
var popStr = c.pop >= 1000000 ? (c.pop/1000000).toFixed(1) + ' Mio' : Math.round(c.pop/1000) + 'k';
var star = c.capital ? ' ★' : '';
L.marker([c.lat, c.lon], {
icon: L.divIcon({className:'city-pill'+(c.capital?' capital':''), html:c.name+star+'<span class="city-pop">'+popStr+'</span>', iconSize:[140,22], iconAnchor:[-16,11]}),
interactive:false, zIndexOffset: c.capital ? 400 : 50
}).addTo(L_CITIES);
});
// === BERGGIPFEL ===
var peaks = [
{name:'Großglockner', lat:47.0742, lon:12.6947, ele:3798, fact:'Höchster Berg Österreichs! Die Großglockner Hochalpenstraße führt bis auf 2.504 m.', wiki:'https://de.wikipedia.org/wiki/Großglockner'},
{name:'Wildspitze', lat:46.8853, lon:10.8672, ele:3768, fact:'Höchster Berg Tirols und zweithöchster Österreichs. Hat den größten Gletscher der Ostalpen.', wiki:'https://de.wikipedia.org/wiki/Wildspitze'},
{name:'Weißkugel', lat:46.8439, lon:10.7833, ele:3738, fact:'Dritthöchster Berg Österreichs, direkt an der Grenze zu Italien.', wiki:'https://de.wikipedia.org/wiki/Weißkugel'},
{name:'Großvenediger', lat:47.1092, lon:12.3464, ele:3657, fact:'Wird „Weltalte Majestät" genannt. Einer der schönsten Gletscherberge der Alpen.', wiki:'https://de.wikipedia.org/wiki/Großvenediger'},
{name:'Similaun', lat:46.7578, lon:10.8853, ele:3599, fact:'Hier wurde 1991 die Gletschermumie „Ötzi" gefunden — 5.300 Jahre alt!', wiki:'https://de.wikipedia.org/wiki/Similaun'},
{name:'Zuckerhütl', lat:46.9833, lon:11.1167, ele:3505, fact:'Höchster Berg der Stubaier Alpen. Beliebtes Ziel für Skitouren.', wiki:'https://de.wikipedia.org/wiki/Zuckerhütl'},
{name:'Piz Buin', lat:46.8444, lon:10.1186, ele:3312, fact:'Höchster Berg Vorarlbergs. Die Sonnencreme-Marke wurde nach ihm benannt!', wiki:'https://de.wikipedia.org/wiki/Piz_Buin'},
{name:'Zugspitze', lat:47.4211, lon:10.9856, ele:2962, fact:'Höchster Berg Deutschlands, direkt an der Grenze zu Österreich. Seilbahn fährt bis zum Gipfel.', wiki:'https://de.wikipedia.org/wiki/Zugspitze'},
{name:'Dachstein', lat:47.4753, lon:13.6061, ele:2995, fact:'Höchster Berg der Steiermark. Hat eine Eishöhle und den berühmten Skywalk.', wiki:'https://de.wikipedia.org/wiki/Hoher_Dachstein'},
{name:'Valluga', lat:47.1567, lon:10.2117, ele:2809, fact:'Hausberg von St. Anton am Arlberg, eines der berühmtesten Skigebiete der Welt.', wiki:'https://de.wikipedia.org/wiki/Valluga'},
{name:'Parseierspitze', lat:47.0825, lon:10.5086, ele:3036, fact:'Höchster Berg der Lechtaler Alpen. Nur für erfahrene Bergsteiger*innen!', wiki:'https://de.wikipedia.org/wiki/Parseierspitze'},
{name:'Brenner', lat:47.0422, lon:11.5067, ele:1370, fact:'Der wichtigste Alpenpass zwischen Österreich und Italien. Schon die Römer nutzten ihn.', wiki:'https://de.wikipedia.org/wiki/Brennerpass'},
{name:'Patscherkofel', lat:47.2083, lon:11.4611, ele:2246, fact:'Hausberg von Innsbruck. Hier fanden Olympische Skirennen statt.', wiki:'https://de.wikipedia.org/wiki/Patscherkofel'},
{name:'Arlberg', lat:47.1297, lon:10.2119, ele:1793, fact:'Grenze zwischen Tirol und Vorarlberg. Wiege des alpinen Skilaufs.', wiki:'https://de.wikipedia.org/wiki/Arlberg'},
];
peaks.forEach(function(p) {
L.marker([p.lat, p.lon], {
icon: L.divIcon({className:'', html:'<div class="marker-circle peak">🏔️</div>', iconSize:[24,24], iconAnchor:[12,12]})
}).addTo(L_PEAKS).bindPopup(
'<div style="font-family:Inter,sans-serif;width:220px;padding:2px">'
+ '<div style="text-align:center">'
+ '<div style="font-size:2.5rem">🏔️</div>'
+ '<div style="font-size:15px;font-weight:900;color:#3a6b3e">' + p.name + '</div>'
+ '<div style="font-size:24px;font-weight:900;color:#1f4e5a">' + p.ele.toLocaleString() + ' m</div>'
+ '</div>'
+ (p.fact ? '<div style="font-size:11px;color:#4a4a4a;line-height:1.4;margin:8px 0 4px;background:rgba(90,138,94,.06);border-radius:6px;padding:6px 8px">🌿 ' + p.fact + '</div>' : '')
+ (p.wiki ? '<div style="text-align:center"><a href="'+p.wiki+'" target="_blank" style="font-size:10px;color:#4a7c8a;text-decoration:none;font-weight:600">📖 Wikipedia →</a></div>' : '')
+ '</div>'
, {maxWidth:240});
L.marker([p.lat, p.lon], {
icon: L.divIcon({className:'peak-pill', html:p.name+'<span class="peak-ele">'+p.ele.toLocaleString()+' m</span>', iconSize:[140,20], iconAnchor:[-14,10]}),
interactive:false
}).addTo(L_PEAKS);
});
// === MISSION-ZIELMARKER (Spiegel zu ALL_MISSIONS) ===
// Mehrere Missionen koennen den gleichen Zielpunkt haben (z.B. m19+m24 → Glockner).
var byTarget = {};
MISSION_MARKERS.forEach(function(m){
var k = m.lat.toFixed(4) + ',' + m.lon.toFixed(4);
if (!byTarget[k]) byTarget[k] = { lat:m.lat, lon:m.lon, wpName:m.wpName, region:m.region, missions:[] };
byTarget[k].missions.push(m);
});
Object.keys(byTarget).forEach(function(k){
var grp = byTarget[k];
var first = grp.missions[0];
var idLabel = grp.missions.map(function(m){ return m.id.toUpperCase(); }).join('+');
var iconHtml = '<div class="marker-mission">'+ first.icon
+ '<span class="mid">'+ idLabel +'</span></div>';
var popupHtml = '<div style="font-family:Inter,sans-serif;padding:4px;min-width:200px">'
+ '<div style="font-size:11px;color:#8a3a2a;font-weight:800;letter-spacing:.04em">EINSATZZIEL</div>'
+ '<div style="font-size:14px;font-weight:900;color:#1f4e5a;margin-bottom:4px">'+ grp.wpName +'</div>'
+ '<div style="font-size:10px;color:#8a8a8a;margin-bottom:6px">'+ (grp.region||'') +'</div>'
+ '<ul style="font-size:11px;color:#1f4e5a;padding-left:16px;margin:0">'
+ grp.missions.map(function(m){ return '<li><strong>'+ m.id.toUpperCase() +'</strong> · '+ m.title +'</li>'; }).join('')
+ '</ul></div>';
L.marker([grp.lat, grp.lon], {
icon: L.divIcon({className:'', html:iconHtml, iconSize:[34,34], iconAnchor:[17,17]}),
zIndexOffset: 1500
}).addTo(L_MISSIONS).bindPopup(popupHtml, {maxWidth:280});
L.marker([grp.lat, grp.lon], {
icon: L.divIcon({className:'wp-pill-mission', html: idLabel +' · '+ first.title, iconSize:[160,20], iconAnchor:[-18,10]}),
interactive:false, zIndexOffset: 1400
}).addTo(L_MISSIONS);
});
// === Filter-Pills ===
function setFilter(f) {
document.querySelectorAll('#filterBar button').forEach(function(b){ b.classList.toggle('on', b.getAttribute('data-filter') === f); });
// Alles ab, dann gezielt an
[L_BASES, L_CITIES, L_PEAKS, L_MISSIONS].forEach(function(g){ if (map.hasLayer(g)) map.removeLayer(g); });
if (f === 'all' || f === 'bases') map.addLayer(L_BASES);
if (f === 'all' || f === 'cities') map.addLayer(L_CITIES);
if (f === 'all' || f === 'peaks') map.addLayer(L_PEAKS);
if (f === 'all' || f === 'missions') map.addLayer(L_MISSIONS);
}
document.querySelectorAll('#filterBar button').forEach(function(btn){
btn.addEventListener('click', function(){ setFilter(btn.getAttribute('data-filter')); });
});
</script>
</body>
</html>