2e50aaf49f
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>
555 lines
28 KiB
JavaScript
555 lines
28 KiB
JavaScript
/**
|
||
* 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
|
||
});
|
||
})();
|