Files
geograsim/App/assets/js/glossar-experiments-tourismus.js
Adminator 2e50aaf49f Glossar: 12 neue interaktive Widgets (Tourismus + Geo/Energie/Reise)
tourismus.js: beschneiung (Temp→Schnee), diversifizierung, kapazitaet, saisonalitaet,
uebertourismus, bevoelkerungsdichte
geowelt.js: gletscher, lawinenwarnstufe, lichtgeschwindigkeit, gigawattstunde,
netzspannung, kartenmaßstab
Einbindung in glossar.php (Registry via Object.assign).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 02:36:55 +02:00

555 lines
28 KiB
JavaScript
Raw Permalink 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.
/**
* Glossar-Experimente — Tourismus.
*
* Registrierung wie in glossar-experiments.js:
* window.GlossarExperiments[key_slug] = function (root) { … }
* Der Renderer (glossar.php) hängt einen leeren Container ein und ruft die
* Funktion mit diesem Container als einzigem Argument auf.
*
* Richtlinien:
* - Keine externen Libraries, Vanilla JS + SVG.
* - Mobile-/Tap-freundlich: grosse Buttons, keine Hover-Abhängigkeit.
* - Zahlen quellenbar, kurze Begründung in der UI sichtbar (.ge-note).
* - Rechenzeichen: Malpunkt · und Doppelpunkt : (nie ×, ÷, *).
* - Deutsche Zahlen via toLocaleString('de-AT').
*/
(function () {
'use strict';
const NS = 'http://www.w3.org/2000/svg';
const svgEl = (tag, attrs, parent) => {
const el = document.createElementNS(NS, tag);
if (attrs) for (const k in attrs) el.setAttribute(k, attrs[k]);
if (parent) parent.appendChild(el);
return el;
};
const fmtInt = n => Math.round(n).toLocaleString('de-AT');
const fmtDec = (n, digits) => n.toLocaleString('de-AT', {
minimumFractionDigits: digits, maximumFractionDigits: digits
});
// =====================================================================
// Experiment: Beschneiung — Schneekanone und die Feuchtkugel-Grenze
// =====================================================================
function beschneiung(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>❄ Wann kann die Schneekanone arbeiten?</h3>
<p>Eine Schneekanone verwandelt Wasser nur dann in Schnee, wenn es kalt genug ist.
Schiebe den Regler und beobachte, ab welcher Temperatur echter Schnee entsteht.</p>
</div>
<svg class="ge-gh-svg" viewBox="0 0 600 260" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-slider">
<label for="ge-bs-range">Lufttemperatur: <strong id="ge-bs-temp">5 °C</strong></label>
<input id="ge-bs-range" type="range" min="-8" max="4" value="-5" step="1">
<div class="ge-gh-markers"><span>8 °C</span><span>2 °C</span><span>0 °C</span><span>+4 °C</span></div>
</div>
<div class="ge-gh-result">
<div class="ge-gh-pill" id="ge-bs-state" data-state="good">🟢 Schneekanone AN</div>
<div class="ge-gh-pill" id="ge-bs-snow">Schneehöhe: <strong>—</strong></div>
</div>
<p class="ge-note">Faustregel der Pisten-Technik: Beschneiung gelingt erst unterhalb einer
Feuchtkugeltemperatur von rund 2 °C. Je kälter (und trockener) die Luft, desto mehr Schnee
je Stunde. Über der Grenze fällt das Wasser nur nass herunter. Quelle: Seilbahnen Österreich,
Beschneiungsleitfaden.</p>
</div>
`;
const svg = root.querySelector('.ge-gh-svg');
const rng = root.querySelector('#ge-bs-range');
const tempOut = root.querySelector('#ge-bs-temp');
const stateP = root.querySelector('#ge-bs-state');
const snowP = root.querySelector('#ge-bs-snow');
const THRESHOLD = -2;
// Himmel + Berg
svgEl('rect', { x: 0, y: 0, width: 600, height: 260, fill: '#eff5f6' }, svg);
svgEl('polygon', { points: '360,260 470,90 520,140 600,60 600,260', fill: '#cddbe0' }, svg);
// Piste (Boden)
const PISTE_Y = 210;
svgEl('rect', { x: 0, y: PISTE_Y, width: 600, height: 50, fill: '#8aa39a' }, svg);
// Schneekanone links
svgEl('rect', { x: 60, y: PISTE_Y - 22, width: 16, height: 22, fill: '#4a4a4a', rx: '2' }, svg);
svgEl('rect', { x: 74, y: PISTE_Y - 30, width: 48, height: 18, fill: '#1f4e5a', rx: '6' }, svg);
svgEl('circle', { cx: 122, cy: PISTE_Y - 21, r: 11, fill: '#4a7c8a', stroke: '#1f4e5a', 'stroke-width': '2' }, svg);
svgEl('text', { x: 68, y: PISTE_Y - 40, 'font-size': '11', 'font-weight': '700', fill: '#1f4e5a' }, svg).textContent = 'Schneekanone';
// Schneeband auf der Piste (wächst mit Kälte)
const snowBand = svgEl('rect', { x: 140, y: PISTE_Y, width: 440, height: 0, fill: '#ffffff', stroke: '#b5c9cf', 'stroke-width': '1' }, svg);
// Dynamische Partikel-Gruppe (Schnee oder Tropfen)
const spray = svgEl('g', {}, svg);
// Status-Text in der Szene
const sceneMsg = svgEl('text', { x: 360, y: 40, 'text-anchor': 'middle', 'font-size': '15', 'font-weight': '800', fill: '#1f4e5a' }, svg);
function update() {
const temp = +rng.value;
tempOut.textContent = (temp > 0 ? '+' : '') + temp + ' °C';
spray.innerHTML = '';
const on = temp <= THRESHOLD;
// coldFactor: 0 an der Grenze (2), 1 bei 8
const coldFactor = Math.max(0, Math.min(1, (THRESHOLD - temp) / 6));
if (on) {
stateP.textContent = '🟢 Schneekanone AN';
stateP.dataset.state = 'good';
// Schneeband wächst
const h = 6 + coldFactor * 42;
snowBand.setAttribute('y', PISTE_Y - h);
snowBand.setAttribute('height', h);
// Schneeflocken aus der Kanone
const nFlakes = 10 + Math.round(coldFactor * 30);
for (let i = 0; i < nFlakes; i++) {
const t = i / nFlakes;
const px = 130 + t * 300 + (Math.random() * 30 - 15);
const py = (PISTE_Y - 25) - Math.sin(t * Math.PI) * 70 + (Math.random() * 16 - 8);
svgEl('circle', { cx: px, cy: py, r: 3.2, fill: '#ffffff', stroke: '#cddbe0', 'stroke-width': '.8' }, spray);
}
sceneMsg.textContent = '❄ Echter Schnee entsteht';
const cmPerH = 2 + coldFactor * 10; // anschaulicher Richtwert
snowP.innerHTML = 'Schnee-Leistung: <strong>' + fmtDec(cmPerH, 0) + ' cm je Stunde</strong>';
} else {
stateP.textContent = '🟠 Schneekanone AUS';
stateP.dataset.state = 'warn';
snowBand.setAttribute('height', 0);
// Nasse Tropfen fallen einfach herunter
for (let i = 0; i < 14; i++) {
const px = 130 + Math.random() * 300;
const py = (PISTE_Y - 20) - Math.random() * 40;
svgEl('ellipse', { cx: px, cy: py, rx: 2.4, ry: 4, fill: '#4a7c8a', opacity: '0.75' }, spray);
}
sceneMsg.textContent = '💧 Zu warm — nur nasses Wasser';
snowP.innerHTML = 'Schnee-Leistung: <strong>0 cm — kein Schnee</strong>';
}
}
rng.addEventListener('input', update);
update();
}
// =====================================================================
// Experiment: Diversifizierung — mehrere Standbeine sichern den Umsatz
// =====================================================================
function diversifizierung(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>🌗 Ein Standbein oder mehrere?</h3>
<p>Ein Urlaubsort lebt vom Winter. Stelle ein, wie gut der Winter ausfällt, und schalte
Sommer-Standbeine dazu. Beobachte, was mit dem Jahresumsatz passiert.</p>
</div>
<div class="ge-gh-slider">
<label for="ge-dv-range">Winter: <strong id="ge-dv-winter">guter Winter</strong></label>
<input id="ge-dv-range" type="range" min="0" max="100" value="80" step="5">
<div class="ge-gh-markers"><span>schneearm</span><span></span><span>guter Winter</span></div>
</div>
<div class="ge-controls">
<button class="ge-btn" data-act="summer">☀ Sommer-Standbeine dazuschalten</button>
</div>
<svg class="ge-bal-svg" viewBox="0 0 600 140" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-result">
<div class="ge-gh-pill">Jahresumsatz: <strong id="ge-dv-sum">—</strong></div>
<div class="ge-gh-pill" id="ge-dv-verdict">…</div>
</div>
<p class="ge-note">Modellzahlen: höchstmöglicher Jahresumsatz 500.000 €. Ohne zweites Standbein
hängt alles am Winter — ein schneearmer Winter bricht den Umsatz fast völlig weg. Mit
Sommer-Angeboten (Wandern, Bike, See) bleibt rund die Hälfte des Umsatzes auch ohne Schnee
erhalten. Genau das meint Diversifizierung: das Risiko auf mehrere Standbeine verteilen.</p>
</div>
`;
const rng = root.querySelector('#ge-dv-range');
const winterOut = root.querySelector('#ge-dv-winter');
const sumOut = root.querySelector('#ge-dv-sum');
const verdict = root.querySelector('#ge-dv-verdict');
const btn = root.querySelector('[data-act="summer"]');
const svg = root.querySelector('.ge-bal-svg');
const MAX = 500000, BAR_X = 130, BAR_W = 400, BAR_Y = 40, BAR_H = 34;
let summer = false;
// Balken-Rahmen
svgEl('text', { x: BAR_X - 10, y: BAR_Y + 22, 'text-anchor': 'end', 'font-size': '13', 'font-weight': '700', fill: '#1f4e5a' }, svg).textContent = 'Umsatz';
svgEl('rect', { x: BAR_X, y: BAR_Y, width: BAR_W, height: BAR_H, fill: '#eff5f6', rx: '8' }, svg);
const bar = svgEl('rect', { x: BAR_X, y: BAR_Y, width: 0, height: BAR_H, fill: '#5a8a5e', rx: '8' }, svg);
const barTxt = svgEl('text', { x: BAR_X + 8, y: BAR_Y + 22, 'font-size': '13', 'font-weight': '800', fill: '#1f4e5a' }, svg);
// Winter-Anteil-Markierung (bei summer der Sockel)
svgEl('text', { x: BAR_X, y: 108, 'font-size': '11', fill: '#4a4a4a' }, svg).textContent = '0 €';
svgEl('text', { x: BAR_X + BAR_W, y: 108, 'text-anchor': 'end', 'font-size': '11', fill: '#4a4a4a' }, svg).textContent = '500.000 €';
const sockelLine = svgEl('line', { x1: BAR_X, y1: BAR_Y - 6, x2: BAR_X, y2: BAR_Y + BAR_H + 6, stroke: '#e8833a', 'stroke-width': '2', 'stroke-dasharray': '4 3', opacity: '0' }, svg);
const sockelTxt = svgEl('text', { x: BAR_X, y: BAR_Y - 10, 'text-anchor': 'middle', 'font-size': '10', 'font-weight': '700', fill: '#e8833a', opacity: '0' }, svg);
function update() {
const winter = +rng.value; // 0..100
const wf = winter / 100;
winterOut.textContent = winter >= 66 ? 'guter Winter' : (winter >= 33 ? 'mittlerer Winter' : 'schneearmer Winter');
let umsatz, summerFloor;
if (summer) {
// 40 % feste Sommerbasis + 60 % vom Winter
summerFloor = MAX * 0.4;
umsatz = summerFloor + MAX * 0.6 * wf;
} else {
summerFloor = 0;
umsatz = MAX * wf;
}
const w = (umsatz / MAX) * BAR_W;
bar.setAttribute('width', w);
bar.setAttribute('fill', umsatz < MAX * 0.25 ? '#c85c4a' : (umsatz < MAX * 0.55 ? '#e8833a' : '#5a8a5e'));
barTxt.setAttribute('x', Math.min(BAR_X + w + 8, BAR_X + BAR_W - 92));
barTxt.textContent = fmtInt(umsatz) + ' €';
// Sommer-Sockel-Linie
if (summer) {
const sx = BAR_X + (summerFloor / MAX) * BAR_W;
sockelLine.setAttribute('x1', sx); sockelLine.setAttribute('x2', sx); sockelLine.setAttribute('opacity', '1');
sockelTxt.setAttribute('x', sx); sockelTxt.setAttribute('opacity', '1');
sockelTxt.textContent = 'Sommer-Sockel';
} else {
sockelLine.setAttribute('opacity', '0');
sockelTxt.setAttribute('opacity', '0');
}
sumOut.textContent = fmtInt(umsatz) + ' €';
btn.classList.toggle('ge-btn-primary', summer);
btn.textContent = summer ? '☀ Sommer-Standbeine AN (zum Abschalten tippen)' : '☀ Sommer-Standbeine dazuschalten';
if (summer && winter < 33) {
verdict.textContent = '🟢 Schneearm — trotzdem stabil dank Sommer';
verdict.dataset.state = 'good';
} else if (!summer && winter < 33) {
verdict.textContent = '🔴 Nur Winter — schneearm bricht den Umsatz weg';
verdict.dataset.state = 'bad';
} else if (summer) {
verdict.textContent = '🟢 Zwei Standbeine — sicher aufgestellt';
verdict.dataset.state = 'good';
} else {
verdict.textContent = '🟡 Ein Standbein — alles hängt am Winter';
verdict.dataset.state = 'warn';
}
}
rng.addEventListener('input', update);
btn.addEventListener('click', () => { summer = !summer; update(); });
update();
}
// =====================================================================
// Experiment: Kapazität — 30 Plätze sind die Obergrenze
// =====================================================================
function kapazitaet(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>☕ Das Café hat 30 Plätze</h3>
<p>Ein Bergcafé hat genau 30 Stühle. Stelle ein, wie viele Gäste in einer Stunde kommen —
und beobachte, wer noch Platz findet und wer warten muss.</p>
</div>
<svg class="ge-bal-svg" viewBox="0 0 600 240" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-slider">
<label for="ge-kp-range">Gäste pro Stunde: <strong id="ge-kp-guests">24</strong></label>
<input id="ge-kp-range" type="range" min="0" max="55" value="24" step="1">
<div class="ge-gh-markers"><span>0</span><span>30 = Kapazität</span><span>55</span></div>
</div>
<div class="ge-gh-result">
<div class="ge-gh-pill">Sitzen: <strong id="ge-kp-sit">24</strong> · Warten: <strong id="ge-kp-wait">0</strong></div>
<div class="ge-gh-pill" id="ge-kp-verdict">…</div>
</div>
<p class="ge-note">Die Kapazität ist die feste Obergrenze: Ab dem 31. Gast finden keine weiteren
mehr Platz — sie müssen warten oder bekommen eine Absage. Mehr Nachfrage füllt die vorhandenen
Plätze, erhöht aber nie die Kapazität. Modellbeispiel Bergcafé mit 30 Sitzplätzen.</p>
</div>
`;
const svg = root.querySelector('.ge-bal-svg');
const rng = root.querySelector('#ge-kp-range');
const guestsOut = root.querySelector('#ge-kp-guests');
const sitOut = root.querySelector('#ge-kp-sit');
const waitOut = root.querySelector('#ge-kp-wait');
const verdict = root.querySelector('#ge-kp-verdict');
const CAP = 30;
// Café-Bereich (Stuhl-Raster 6 x 5) links, Warteschlange rechts
svgEl('rect', { x: 14, y: 14, width: 372, height: 212, fill: '#eff5f6', stroke: '#b5c9cf', 'stroke-width': '1.5', rx: '10' }, svg);
svgEl('text', { x: 24, y: 34, 'font-size': '12', 'font-weight': '800', fill: '#1f4e5a' }, svg).textContent = '☕ Café — 30 Plätze';
svgEl('text', { x: 470, y: 34, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '800', fill: '#c85c4a' }, svg).textContent = 'Warteschlange';
const seats = [];
const COLS = 6, ROWS = 5;
for (let i = 0; i < CAP; i++) {
const c = i % COLS, r = Math.floor(i / COLS);
const cx = 46 + c * 58, cy = 66 + r * 34;
// Stuhl-Slot
svgEl('circle', { cx, cy, r: 13, fill: '#ffffff', stroke: '#b5c9cf', 'stroke-width': '1.5' }, svg);
const occ = svgEl('circle', { cx, cy, r: 10, fill: '#5a8a5e', opacity: '0' }, svg);
const head = svgEl('circle', { cx, cy: cy - 4, r: 3.4, fill: '#ffffff', opacity: '0' }, svg);
seats.push({ occ, head });
}
const queueG = svgEl('g', {}, svg);
function update() {
const guests = +rng.value;
const sit = Math.min(guests, CAP);
const wait = Math.max(0, guests - CAP);
guestsOut.textContent = guests;
sitOut.textContent = sit;
waitOut.textContent = wait;
seats.forEach((s, i) => {
const on = i < sit ? '1' : '0';
s.occ.setAttribute('opacity', on);
s.head.setAttribute('opacity', on);
});
// Warteschlange zeichnen (max 25 Figuren sichtbar)
queueG.innerHTML = '';
const shown = Math.min(wait, 25);
for (let i = 0; i < shown; i++) {
const c = i % 5, r = Math.floor(i / 5);
const x = 418 + c * 30, y = 60 + r * 32;
svgEl('circle', { cx: x, cy: y, r: 9, fill: '#c85c4a' }, queueG);
svgEl('circle', { cx: x, cy: y - 3, r: 3, fill: '#ffffff' }, queueG);
}
if (wait > 25) {
svgEl('text', { x: 470, y: 216, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '800', fill: '#c85c4a' }, queueG).textContent = '+ ' + (wait - 25) + ' weitere';
}
if (wait === 0 && guests < CAP) {
verdict.textContent = '🟢 Alle ' + guests + ' Gäste finden Platz';
verdict.dataset.state = 'good';
} else if (wait === 0) {
verdict.textContent = '🟡 Café voll — genau an der Kapazität';
verdict.dataset.state = 'warn';
} else {
verdict.textContent = '🔴 ' + wait + ' Gäste müssen warten oder absagen';
verdict.dataset.state = 'bad';
}
}
rng.addEventListener('input', update);
update();
}
// =====================================================================
// Experiment: Saisonalität — Ganzjahrestourismus glättet die Kurve
// =====================================================================
function saisonalitaet(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>📅 Volle und leere Monate</h3>
<p>Viele Bergorte leben fast nur vom Winter. Schalte Sommer-Angebote dazu und beobachte,
wie sich die Gäste übers Jahr verteilen.</p>
</div>
<svg class="ge-bal-svg" viewBox="0 0 600 220" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-controls">
<button class="ge-btn" data-act="summer">☀ Sommer-Angebote einschalten</button>
</div>
<div class="ge-gh-result">
<div class="ge-gh-pill">Höchster Monat: <strong id="ge-sa-max">—</strong> · Schwächster: <strong id="ge-sa-min">—</strong></div>
<div class="ge-gh-pill" id="ge-sa-verdict">…</div>
</div>
<p class="ge-note">Die starke Ballung auf wenige Monate heißt Saisonalität. Ganzjahrestourismus
(Wandern, Bike, Seen, Kultur im Sommer) verteilt die Nachfrage gleichmäßiger übers Jahr — das
sichert Arbeitsplätze und entlastet die Spitzen. Modellhafte Nachfragewerte, angelehnt an das
Muster alpiner Wintersportorte.</p>
</div>
`;
const svg = root.querySelector('.ge-bal-svg');
const btn = root.querySelector('[data-act="summer"]');
const maxOut = root.querySelector('#ge-sa-max');
const minOut = root.querySelector('#ge-sa-min');
const verdict = root.querySelector('#ge-sa-verdict');
const months = ['Jän', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'];
const winterOnly = [85, 90, 70, 30, 18, 22, 26, 30, 22, 20, 42, 80];
const withSummer = [85, 90, 72, 42, 48, 70, 80, 82, 66, 48, 45, 80];
let summer = false;
const BASE_Y = 180, MAXH = 140, BW = 34, GAP = 12, X0 = 60;
svgEl('line', { x1: 40, y1: BASE_Y, x2: 580, y2: BASE_Y, stroke: '#b5c9cf', 'stroke-width': '1.5' }, svg);
const bars = months.map((m, i) => {
const x = X0 + i * (BW + GAP);
const bar = svgEl('rect', { x, y: BASE_Y, width: BW, height: 0, fill: '#4a7c8a', rx: '4' }, svg);
svgEl('text', { x: x + BW / 2, y: BASE_Y + 16, 'text-anchor': 'middle', 'font-size': '10', 'font-weight': '600', fill: '#4a4a4a' }, svg).textContent = m;
return bar;
});
function update() {
const data = summer ? withSummer : winterOnly;
let maxV = -1, minV = 999, maxI = 0, minI = 0;
data.forEach((v, i) => {
if (v > maxV) { maxV = v; maxI = i; }
if (v < minV) { minV = v; minI = i; }
});
bars.forEach((bar, i) => {
const h = data[i] / 100 * MAXH;
bar.setAttribute('y', BASE_Y - h);
bar.setAttribute('height', h);
// Sommermonate hervorheben, wenn eingeschaltet
const isSummer = i >= 4 && i <= 9;
bar.setAttribute('fill', summer && isSummer ? '#5a8a5e' : '#4a7c8a');
});
maxOut.textContent = months[maxI];
minOut.textContent = months[minI];
btn.classList.toggle('ge-btn-primary', summer);
btn.textContent = summer ? '☀ Sommer-Angebote AN (zum Abschalten tippen)' : '☀ Sommer-Angebote einschalten';
const spread = maxV - minV;
if (summer) {
verdict.textContent = '🟢 Gleichmäßiger — Unterschied nur noch ' + spread + ' Punkte';
verdict.dataset.state = 'good';
} else {
verdict.textContent = '🔴 Winterlastig — Unterschied ' + spread + ' Punkte';
verdict.dataset.state = 'bad';
}
}
btn.addEventListener('click', () => { summer = !summer; update(); });
update();
}
// =====================================================================
// Experiment: Übertourismus — Gäste je Einwohner (Hallstatt)
// =====================================================================
function uebertourismus(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>🏘 Wie viele Gäste verträgt ein kleiner Ort?</h3>
<p>Hallstatt in Oberösterreich hat rund <strong>760 Einwohner:innen</strong>. Schiebe den
Regler und sieh, wie viele Gäste je Einwohner:in an einem Tag im Ort sind.</p>
</div>
<svg class="ge-bal-svg" viewBox="0 0 600 130" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-slider">
<label for="ge-ut-range">Gäste pro Tag: <strong id="ge-ut-guests">3.000</strong></label>
<input id="ge-ut-range" type="range" min="0" max="10000" value="3000" step="100">
<div class="ge-gh-markers"><span>0</span><span>5.000</span><span>10.000</span></div>
</div>
<div class="ge-gh-result">
<div class="ge-gh-pill">Je Einwohner:in: <strong id="ge-ut-ratio">—</strong> Gäste</div>
<div class="ge-gh-pill" id="ge-ut-verdict">…</div>
</div>
<p class="ge-note">Hallstatt: rund 760 Einwohner:innen, an Spitzentagen bis zu 10.000 Gäste —
das sind über 13 Gäste je Einwohner:in. So viel Andrang überlastet Wege, Verkehr und
Alltag der Menschen vor Ort. Das nennt man Übertourismus. Quellen: Gemeinde Hallstatt,
Statistik Austria.</p>
</div>
`;
const svg = root.querySelector('.ge-bal-svg');
const rng = root.querySelector('#ge-ut-range');
const guestsOut = root.querySelector('#ge-ut-guests');
const ratioOut = root.querySelector('#ge-ut-ratio');
const verdict = root.querySelector('#ge-ut-verdict');
const RESIDENTS = 760, MAX_GUESTS = 10000;
const BAR_X = 20, BAR_W = 560, BAR_Y = 44, BAR_H = 40;
svgEl('text', { x: BAR_X, y: 30, 'font-size': '12', 'font-weight': '700', fill: '#1f4e5a' }, svg).textContent = 'Belastung des Ortes';
svgEl('rect', { x: BAR_X, y: BAR_Y, width: BAR_W, height: BAR_H, fill: '#eff5f6', stroke: '#b5c9cf', 'stroke-width': '1', rx: '10' }, svg);
const bar = svgEl('rect', { x: BAR_X, y: BAR_Y, width: 0, height: BAR_H, fill: '#5a8a5e', rx: '10' }, svg);
const barTxt = svgEl('text', { x: BAR_X + BAR_W / 2, y: BAR_Y + 26, 'text-anchor': 'middle', 'font-size': '14', 'font-weight': '800', fill: '#1f4e5a' }, svg);
svgEl('text', { x: BAR_X, y: 108, 'font-size': '11', fill: '#5a8a5e', 'font-weight': '700' }, svg).textContent = '🟢 entspannt';
svgEl('text', { x: BAR_X + BAR_W, y: 108, 'text-anchor': 'end', 'font-size': '11', fill: '#c85c4a', 'font-weight': '700' }, svg).textContent = 'überlastet 🔴';
function update() {
const guests = +rng.value;
const ratio = guests / RESIDENTS;
guestsOut.textContent = fmtInt(guests);
ratioOut.textContent = fmtDec(ratio, 1);
const w = (guests / MAX_GUESTS) * BAR_W;
bar.setAttribute('width', w);
barTxt.textContent = fmtDec(ratio, 1) + ' Gäste je Einwohner:in';
let color, v, state;
if (ratio < 2) { color = '#5a8a5e'; v = '🟢 Entspannt — der Ort verkraftet das gut'; state = 'good'; }
else if (ratio < 6) { color = '#e8833a'; v = '🟡 Viel los — Wege und Verkehr werden eng'; state = 'warn'; }
else { color = '#c85c4a'; v = '🔴 Übertourismus — der Ort ist überlastet'; state = 'bad'; }
bar.setAttribute('fill', color);
verdict.textContent = v;
verdict.dataset.state = state;
}
rng.addEventListener('input', update);
update();
}
// =====================================================================
// Experiment: Bevölkerungsdichte — Menschen je Quadratkilometer
// =====================================================================
function bevoelkerungsdichte(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>🧑‍🤝‍🧑 Wie eng wohnen die Menschen?</h3>
<p>Jedes Feld unten ist genau <strong>1 km²</strong> groß. Wähle einen Ort und sieh, wie viele
Menschen dort auf diesem einen Quadratkilometer leben.</p>
</div>
<div class="ge-albedo-choice" role="tablist">
<button class="ge-btn" data-key="tal">Bergtal</button>
<button class="ge-btn ge-btn-primary" data-key="at">Österreich ⌀</button>
<button class="ge-btn" data-key="wien">Stadt (Wien)</button>
<button class="ge-btn" data-key="monaco">Monaco</button>
</div>
<svg class="ge-bal-svg" viewBox="0 0 600 300" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-result">
<div class="ge-gh-pill">Dichte: <strong id="ge-bd-val">—</strong> Menschen je km²</div>
<div class="ge-gh-pill" id="ge-bd-verdict">…</div>
</div>
<p class="ge-note">Bevölkerungsdichte = Menschen : Fläche. Bergtal rund 5, Österreich im Schnitt
109, Wien rund 4.600 und Monaco rund 26.000 Menschen je km². Ein Punkt steht hier für
50 Menschen. Quellen: Statistik Austria (2024), Statista.</p>
</div>
`;
const svg = root.querySelector('.ge-bal-svg');
const valOut = root.querySelector('#ge-bd-val');
const verdict = root.querySelector('#ge-bd-verdict');
const PER_DOT = 50;
const places = {
tal: { label: 'Bergtal', density: 5, v: '🟢 Sehr dünn besiedelt — viel Platz', state: 'good' },
at: { label: 'Österreich ⌀', density: 109, v: '🟢 Ländlich-durchschnittlich', state: 'good' },
wien: { label: 'Stadt (Wien)', density: 4600, v: '🟡 Dicht — typische Großstadt', state: 'warn' },
monaco: { label: 'Monaco', density: 26000, v: '🔴 Extrem dicht — dichtestes Land der Welt', state: 'bad' }
};
// 1-km²-Feld (Quadrat)
const FX = 175, FY = 20, FS = 260;
svgEl('rect', { x: FX, y: FY, width: FS, height: FS, fill: '#eef4ef', stroke: '#5a8a5e', 'stroke-width': '2', rx: '6' }, svg);
svgEl('text', { x: FX + FS / 2, y: FY + FS + 20, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '700', fill: '#1f4e5a' }, svg).textContent = '1 km² · 1 Punkt = 50 Menschen';
const dotG = svgEl('g', {}, svg);
// Feste Punkt-Positionen (Raster mit Jitter), damit der Vergleich fair bleibt
const CAP = 600; // max sichtbare Punkte (25 x 24)
const positions = [];
const gcols = 25, grows = 24;
const cellW = (FS - 16) / gcols, cellH = (FS - 16) / grows;
for (let r = 0; r < grows; r++) {
for (let c = 0; c < gcols; c++) {
positions.push({
x: FX + 8 + c * cellW + cellW / 2,
y: FY + 8 + r * cellH + cellH / 2
});
}
}
function setPlace(key) {
const p = places[key] || places.at;
const nDots = Math.min(CAP, Math.round(p.density / PER_DOT));
dotG.innerHTML = '';
for (let i = 0; i < nDots; i++) {
const pos = positions[i];
svgEl('circle', { cx: pos.x, cy: pos.y, r: 2.6, fill: '#c85c4a', opacity: '0.9' }, dotG);
}
if (nDots === 0) {
// wenigstens 1 Mensch sichtbar bei sehr geringer Dichte
svgEl('circle', { cx: positions[0].x, cy: positions[0].y, r: 3, fill: '#5a8a5e' }, dotG);
}
valOut.textContent = fmtInt(p.density);
verdict.textContent = p.v;
verdict.dataset.state = p.state;
root.querySelectorAll('.ge-albedo-choice .ge-btn').forEach(b => {
b.classList.toggle('ge-btn-primary', b.dataset.key === key);
});
}
root.querySelectorAll('.ge-albedo-choice .ge-btn').forEach(b => {
b.addEventListener('click', () => setPlace(b.dataset.key));
});
setPlace('at');
}
// Registry — Keys entsprechen key_slug aus der Tabelle `glossar`
window.GlossarExperiments = Object.assign(window.GlossarExperiments || {}, {
'beschneiung': beschneiung,
'diversifizierung': diversifizierung,
'kapazitaet': kapazitaet,
'saisonalitaet': saisonalitaet,
'uebertourismus': uebertourismus,
'bevoelkerungsdichte': bevoelkerungsdichte
});
})();