in der Roademap weitergemacht

This commit is contained in:
Michael Stangl
2026-07-12 17:02:14 +02:00
parent dd84293c36
commit f880ae1c97
8 changed files with 752 additions and 74 deletions
+472
View File
@@ -0,0 +1,472 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sitz &amp; Platz — Tisch-Editor (M1)</title>
<style>
:root {
--akzent: #2f6f4f;
--akzent-hell: #e6f0ea;
--rand: #d9d4c8;
--hintergrund: #faf8f3;
}
* { box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
margin: 0; background: var(--hintergrund); color: #2a2a24; line-height: 1.5;
}
header { background: var(--akzent); color: white; padding: 1rem 1.5rem; }
header h1 { margin: 0; font-size: 1.3rem; }
header p { margin: .2rem 0 0; opacity: .85; font-size: .85rem; }
header a { color: #d6eade; }
main { max-width: 68rem; margin: 0 auto; padding: 1rem 1.5rem 4rem; }
.werkzeug {
display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
background: white; border: 1px solid var(--rand); border-radius: 8px;
padding: .6rem .75rem; margin-bottom: 1rem;
}
.werkzeug .trenner { width: 1px; height: 1.6rem; background: var(--rand); margin: 0 .3rem; }
.werkzeug label { font-size: .82rem; color: #6b6557; display: inline-flex; align-items: center; gap: .3rem; }
button {
background: var(--akzent); color: white; border: 0; border-radius: 4px;
padding: .42rem .8rem; font-size: .88rem; cursor: pointer;
}
button.sekundaer { background: #8b8577; }
button.geist { background: white; color: var(--akzent); border: 1px solid var(--rand); }
button:disabled { background: #cfcabc; color: #f3f0e8; cursor: not-allowed; }
.buehne {
display: grid; grid-template-columns: 1fr 15rem; gap: 1rem; align-items: start;
}
@media (max-width: 48rem) { .buehne { grid-template-columns: 1fr; } }
#leinwand {
background: white; border: 1px solid var(--rand); border-radius: 8px;
overflow: auto; touch-action: none; min-height: 24rem;
}
#leinwand svg { display: block; }
.circle-tisch { cursor: grab; }
.circle-tisch:active { cursor: grabbing; }
aside {
background: white; border: 1px solid var(--rand); border-radius: 8px;
padding: 1rem; font-size: .9rem;
}
aside h2 { font-size: 1rem; margin: 0 0 .6rem; color: var(--akzent); }
aside .feld { margin: .5rem 0; }
aside input[type=number] { width: 4rem; }
.zahl { display: inline-block; background: #f1eee6; border-radius: 4px;
padding: .25rem .6rem; margin: .1rem .3rem .1rem 0; font-size: .85rem; }
.zahl b { color: var(--akzent); }
.status { font-size: .85rem; margin-top: .6rem; min-height: 1.2rem; }
.status.ok { color: var(--akzent); }
.status.fehler { color: #a03030; white-space: pre-wrap; }
.hinweis { font-size: .8rem; color: #6b6557; }
kbd { background: #f1eee6; border: 1px solid var(--rand); border-bottom-width: 2px;
border-radius: 3px; padding: 0 .3rem; font-size: .78rem; }
</style>
</head>
<body>
<header>
<h1>Sitz &amp; Platz <small style="font-weight:normal">— Interaktiver Tisch-Editor (Meilenstein 1)</small></h1>
<p>Tische per Drag &amp; Drop platzieren; Nachbarschaft wird aus der Nähe abgeleitet.
Sitzung: <code id="sitzungsId"></code> · weiter zu <a href="/">Gäste &amp; Berechnung</a></p>
</header>
<main>
<div class="werkzeug">
<button id="btnUndo" title="Rückgängig (Strg+Z)" disabled>↶ Rückgängig</button>
<button id="btnRedo" title="Wiederholen (Strg+Y)" disabled>↷ Wiederholen</button>
<span class="trenner"></span>
<button id="btnAdd" class="geist">+ Tisch</button>
<button id="btnReihe" class="sekundaer">Preset: 4er-Reihe</button>
<button id="btnRaster" class="sekundaer">Preset: 3×3-Raster</button>
<span class="trenner"></span>
<label>Nachbar-Radius
<input type="range" id="radius" min="0.5" max="3" step="0.1" value="1.5">
<b id="radiusWert">1,5</b>
</label>
<label>Raster
<select id="raster">
<option value="0.5" selected>0,5</option>
<option value="1">1</option>
<option value="1.5">1,5</option>
<option value="2">2</option>
</select>
</label>
<span class="trenner"></span>
<label>tische.ini <input type="file" id="iniDatei" accept=".ini,text/plain"></label>
<button id="btnIni" class="geist">laden</button>
<span class="trenner"></span>
<button id="btnSpeichern">Layout speichern</button>
</div>
<div class="buehne">
<div id="leinwand"></div>
<aside>
<h2>Ausgewählter Tisch</h2>
<div id="inspektor"><p class="hinweis">Kein Tisch ausgewählt. Auf einen Tisch
klicken, um Plätze zu ändern oder ihn zu löschen. Ziehen zum Verschieben.</p></div>
<hr style="border:0;border-top:1px solid var(--rand);margin:.8rem 0">
<div id="zahlen"></div>
<p class="hinweis" style="margin-top:.6rem">Tastatur: <kbd>Strg</kbd>+<kbd>Z</kbd> /
<kbd>Strg</kbd>+<kbd>Y</kbd> Undo/Redo · <kbd>Entf</kbd> löscht den
ausgewählten Tisch.</p>
</aside>
</div>
<div class="status" id="status"></div>
</main>
<script>
'use strict';
/* ============================================================ Zustand
state.tische ist die einzige Wahrheit; das SVG wird daraus gezeichnet
(state-getrieben, nie umgekehrt). Der Nachbar-Radius ist bewusst KEIN
Teil der Undo-History (Undo/Redo betrifft Tischgeometrie/-kapazität,
nicht diese Ableitungs-Einstellung), wird aber mitgespeichert. */
const state = { tische: [] };
let radius = 1.5;
let rasterSchritt = 0.5;
const ZIEH_TEMPO = 0.5; // Ziehgeschwindigkeit: Tisch folgt dem Zeiger halb so schnell
let selectedId = null;
let sitzung = null;
/* Undo/Redo: Schnappschuss-Stapel. Ein Schnappschuss ist ein tiefer Klon
von state.tische. Vor jeder verändernden Aktion wird der aktuelle
Zustand auf undoStack gelegt und redoStack geleert. */
const undoStack = [];
const redoStack = [];
const HIST_MAX = 200;
const klon = (t) => JSON.parse(JSON.stringify(t));
function pushHistory() {
undoStack.push(klon(state.tische));
if (undoStack.length > HIST_MAX) undoStack.shift();
redoStack.length = 0;
knoepfe();
}
function undo() {
if (!undoStack.length) return;
redoStack.push(klon(state.tische));
state.tische = undoStack.pop();
nachZustandswechsel();
}
function redo() {
if (!redoStack.length) return;
undoStack.push(klon(state.tische));
state.tische = redoStack.pop();
nachZustandswechsel();
}
function nachZustandswechsel() {
if (selectedId !== null && !tischVonId(selectedId)) selectedId = null;
knoepfe(); zeichne(); inspektor();
}
function knoepfe() {
document.getElementById('btnUndo').disabled = undoStack.length === 0;
document.getElementById('btnRedo').disabled = redoStack.length === 0;
}
/* ============================================================ Helfer */
const tischVonId = (id) => state.tische.find(t => t.id === id);
function neueId() {
return state.tische.reduce((m, t) => Math.max(m, t.id), 0) + 1;
}
function snap(v) { return Math.round( Math.round(v / rasterSchritt) * rasterSchritt * 100 ) / 100; }
function radiusPx(plaetze) {
// Kreisgroesse waechst mit der Platzzahl (analog alsSVG im Kern)
return Math.max(18, Math.min(48, 15 + plaetze * 1.5));
}
/* ============================================================ Geometrie
Grid-Koordinaten (wie tische.ini) <-> Pixel. Bei jeder Zeichnung neu
bestimmt und in Modulvariablen abgelegt, damit pointerGrid() umrechnen
kann. */
const SCALE = 72, MARGIN = 34, PAD = 1;
let viewMinX = 0, viewMinY = 0, curW = 600, curH = 400;
function grenzen() {
if (!state.tische.length) return { minX: 0, maxX: 3, minY: 0, maxY: 2 };
const xs = state.tische.map(t => t.x), ys = state.tische.map(t => t.y);
return { minX: Math.min(...xs), maxX: Math.max(...xs),
minY: Math.min(...ys), maxY: Math.max(...ys) };
}
const px = (gx, min) => MARGIN + (gx - min) * SCALE;
function pointerGrid(e) {
const svg = document.querySelector('#leinwand svg');
const r = svg.getBoundingClientRect();
const sx = curW / r.width, sy = curH / r.height;
const X = (e.clientX - r.left) * sx, Y = (e.clientY - r.top) * sy;
return { x: (X - MARGIN) / SCALE + viewMinX, y: (Y - MARGIN) / SCALE + viewMinY };
}
/* ============================================================ Zeichnen */
function nachbarPaare() {
const paare = [];
for (let i = 0; i < state.tische.length; i++)
for (let j = i + 1; j < state.tische.length; j++) {
const a = state.tische[i], b = state.tische[j];
if (Math.hypot(a.x - b.x, a.y - b.y) <= radius) paare.push([a, b]);
}
return paare;
}
function zeichne() {
const g = grenzen();
viewMinX = g.minX - PAD; viewMinY = g.minY - PAD;
curW = Math.max(480, MARGIN * 2 + (g.maxX - g.minX + 2 * PAD) * SCALE);
curH = Math.max(320, MARGIN * 2 + (g.maxY - g.minY + 2 * PAD) * SCALE);
let s = `<svg xmlns="http://www.w3.org/2000/svg" width="${curW}" height="${curH}" `
+ `viewBox="0 0 ${curW} ${curH}" font-family="sans-serif">`;
s += `<rect width="100%" height="100%" fill="white"/>`;
// feines Grid an ganzzahligen Rasterlinien
for (let gx = Math.ceil(viewMinX); px(gx, viewMinX) <= curW; gx++)
s += `<line x1="${px(gx, viewMinX)}" y1="0" x2="${px(gx, viewMinX)}" y2="${curH}" stroke="#f0ede5"/>`;
for (let gy = Math.ceil(viewMinY); px(gy, viewMinY) <= curH; gy++)
s += `<line x1="0" y1="${px(gy, viewMinY)}" x2="${curW}" y2="${px(gy, viewMinY)}" stroke="#f0ede5"/>`;
// Nachbar-Linien (aus der Naehe abgeleitet, wie im Backend)
for (const [a, b] of nachbarPaare())
s += `<line x1="${px(a.x, viewMinX)}" y1="${px(a.y, viewMinY)}" `
+ `x2="${px(b.x, viewMinX)}" y2="${px(b.y, viewMinY)}" `
+ `stroke="#bcd3c6" stroke-width="2"/>`;
// Tische als Kreise
for (const t of state.tische) {
const cx = px(t.x, viewMinX), cy = px(t.y, viewMinY), r = radiusPx(t.plaetze);
const gewaehlt = t.id === selectedId;
s += `<g class="circle-tisch" data-id="${t.id}">`;
if (gewaehlt)
s += `<circle cx="${cx}" cy="${cy}" r="${r + 5}" fill="none" `
+ `stroke="#2f6f4f" stroke-width="2" stroke-dasharray="4 3"/>`;
s += `<circle cx="${cx}" cy="${cy}" r="${r}" `
+ `fill="${gewaehlt ? '#e6f0ea' : 'white'}" stroke="#2f6f4f" stroke-width="2"/>`;
s += `<text x="${cx}" y="${cy - 1}" text-anchor="middle" font-size="15" `
+ `font-weight="bold" fill="#2f6f4f">${t.id}</text>`;
s += `<text x="${cx}" y="${cy + 13}" text-anchor="middle" font-size="10" `
+ `fill="#6b6557">${t.plaetze} Pl.</text>`;
s += `</g>`;
}
s += `</svg>`;
document.getElementById('leinwand').innerHTML = s;
// Kennzahlen
const plaetze = state.tische.reduce((m, t) => m + t.plaetze, 0);
document.getElementById('zahlen').innerHTML =
`<span class="zahl">Tische: <b>${state.tische.length}</b></span>` +
`<span class="zahl">Plätze: <b>${plaetze}</b></span>` +
`<span class="zahl">Nachbar-Paare: <b>${nachbarPaare().length}</b></span>`;
}
/* ============================================================ Inspektor */
function inspektor() {
const el = document.getElementById('inspektor');
const t = selectedId !== null ? tischVonId(selectedId) : null;
if (!t) {
el.innerHTML = '<p class="hinweis">Kein Tisch ausgewählt. Auf einen Tisch ' +
'klicken, um Plätze zu ändern oder ihn zu löschen. Ziehen zum Verschieben.</p>';
return;
}
el.innerHTML =
`<div class="feld"><b>Tisch ${t.id}</b></div>` +
`<div class="feld">Plätze: ` +
`<button class="geist" id="plMinus"></button> ` +
`<input type="number" id="plaetze" min="1" max="100" value="${t.plaetze}"> ` +
`<button class="geist" id="plPlus">+</button></div>` +
`<div class="feld hinweis">Position: x=${t.x}, y=${t.y}</div>` +
`<div class="feld"><button class="sekundaer" id="loeschen">Tisch löschen</button></div>`;
document.getElementById('plMinus').onclick = () => plaetzeSetzen(t.plaetze - 1);
document.getElementById('plPlus').onclick = () => plaetzeSetzen(t.plaetze + 1);
document.getElementById('plaetze').onchange = (e) => plaetzeSetzen(parseInt(e.target.value, 10));
document.getElementById('loeschen').onclick = tischLoeschen;
}
function plaetzeSetzen(wert) {
const t = tischVonId(selectedId);
if (!t || isNaN(wert)) return;
wert = Math.max(1, Math.min(100, wert));
if (wert === t.plaetze) return;
pushHistory(); t.plaetze = wert; zeichne(); inspektor();
}
/* ============================================================ Auswahl */
function auswaehlen(id) { selectedId = id; zeichne(); inspektor(); }
/* ============================================================ Aktionen */
function tischHinzufuegen() {
const g = grenzen();
const x = state.tische.length ? snap((g.minX + g.maxX) / 2) : 0;
const y = state.tische.length ? snap((g.minY + g.maxY) / 2) : 0;
pushHistory();
const id = neueId();
state.tische.push({ id, plaetze: 8, x, y });
auswaehlen(id);
melde('Tisch ' + id + ' hinzugefügt.', 'ok');
}
function tischLoeschen() {
if (selectedId === null) return;
pushHistory();
state.tische = state.tische.filter(t => t.id !== selectedId);
const weg = selectedId; selectedId = null;
zeichne(); inspektor();
melde('Tisch ' + weg + ' gelöscht.', 'ok');
}
function presetReihe() {
pushHistory();
state.tische = [1, 2, 3, 4].map(i => ({ id: i, plaetze: 5, x: i - 1, y: 0 }));
selectedId = null; zeichne(); inspektor();
}
function presetRaster() {
pushHistory();
state.tische = [];
let id = 1;
for (let y = 0; y < 3; y++)
for (let x = 0; x < 3; x++) state.tische.push({ id: id++, plaetze: 8, x, y });
selectedId = null; zeichne(); inspektor();
}
/* ============================================================ Drag & Drop */
let dragId = null, dragMoved = false, dragStart = null;
let ankerPtr = null, ankerPos = null; // Zeiger- und Tischposition bei Zugbeginn
document.getElementById('leinwand').addEventListener('pointerdown', (e) => {
const el = e.target.closest('[data-id]');
if (!el) { if (selectedId !== null) auswaehlen(null); return; }
const id = parseInt(el.dataset.id, 10);
auswaehlen(id);
const gpt = pointerGrid(e), t = tischVonId(id);
dragId = id; dragMoved = false; dragStart = klon(state.tische);
// Anker merken: der Tisch wandert ab hier um ZIEH_TEMPO * Zeigerweg
ankerPtr = { x: gpt.x, y: gpt.y };
ankerPos = { x: t.x, y: t.y };
e.preventDefault();
});
window.addEventListener('pointermove', (e) => {
if (dragId === null) return;
const gpt = pointerGrid(e);
// halbe Geschwindigkeit: Verschiebung des Tisches = halber Zeigerweg
const nx = snap(ankerPos.x + (gpt.x - ankerPtr.x) * ZIEH_TEMPO);
const ny = snap(ankerPos.y + (gpt.y - ankerPtr.y) * ZIEH_TEMPO);
const t = tischVonId(dragId);
if (t && (t.x !== nx || t.y !== ny)) { t.x = nx; t.y = ny; dragMoved = true; zeichne(); inspektor(); }
});
window.addEventListener('pointerup', () => {
if (dragId === null) return;
if (dragMoved) {
// den ganzen Zug als EINEN Undo-Schritt ablegen (Vor-Zustand)
undoStack.push(dragStart);
if (undoStack.length > HIST_MAX) undoStack.shift();
redoStack.length = 0; knoepfe();
}
dragId = null; dragStart = null;
});
/* ============================================================ Tastatur */
window.addEventListener('keydown', (e) => {
const tippt = /^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName);
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z' && !e.shiftKey) {
e.preventDefault(); undo();
} else if ((e.ctrlKey || e.metaKey) && (e.key.toLowerCase() === 'y' ||
(e.key.toLowerCase() === 'z' && e.shiftKey))) {
e.preventDefault(); redo();
} else if ((e.key === 'Delete' || e.key === 'Backspace') && selectedId !== null && !tippt) {
e.preventDefault(); tischLoeschen();
}
});
/* ============================================================ Backend */
function melde(text, klasse) {
const el = document.getElementById('status');
el.textContent = text; el.className = 'status ' + (klasse || '');
}
async function api(pfad, optionen) {
const antwort = await fetch(pfad, optionen);
const daten = await antwort.json().catch(() => null);
if (!antwort.ok) {
const d = daten && daten.detail
? (typeof daten.detail === 'string' ? daten.detail : JSON.stringify(daten.detail))
: antwort.statusText;
throw new Error(d);
}
return daten;
}
async function sitzungHolen() {
// Session mit der Testoberflaeche (index.html) ueber localStorage teilen
let id = localStorage.getItem('platzSitzung');
if (id) {
const ok = await fetch('/sitzung/' + id).then(r => r.ok).catch(() => false);
if (ok) return id;
}
const r = await api('/sitzung', { method: 'POST' });
localStorage.setItem('platzSitzung', r.id);
return r.id;
}
async function layoutSpeichern() {
if (!state.tische.length) { melde('Mindestens ein Tisch nötig.', 'fehler'); return; }
const tische = state.tische.map(t => ({ id: t.id, plaetze: t.plaetze, x: t.x, y: t.y }));
try {
const r = await api('/sitzung/' + sitzung + '/tische', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tische, nachbarRadius: radius }),
});
melde('✓ ' + r.tische.length + ' Tische, ' + r.plaetzeGesamt +
' Plätze gespeichert. Weiter mit „Gäste & Berechnung“.', 'ok');
} catch (e) { melde('Fehler: ' + e.message, 'fehler'); }
}
async function iniLaden() {
const eingabe = document.getElementById('iniDatei');
if (!eingabe.files.length) { melde('Bitte zuerst eine tische.ini auswählen.', 'fehler'); return; }
const form = new FormData(); form.append('datei', eingabe.files[0]);
try {
const r = await api('/sitzung/' + sitzung + '/tische-datei', { method: 'POST', body: form });
pushHistory();
state.tische = r.tische.map(t => ({ id: t.id, plaetze: t.plaetze, x: t.x, y: t.y }));
selectedId = null; zeichne(); inspektor();
melde('✓ ' + r.tische.length + ' Tische aus Datei geladen.', 'ok');
} catch (e) { melde('Fehler: ' + e.message, 'fehler'); }
}
async function layoutLaden() {
// bereits in der (geteilten) Session gespeichertes Layout uebernehmen
try {
const r = await api('/sitzung/' + sitzung + '/tische');
if (r.tische && r.tische.length) {
state.tische = r.tische.map(t => ({ id: t.id, plaetze: t.plaetze, x: t.x, y: t.y }));
radius = r.nachbarRadius || 1.5;
document.getElementById('radius').value = radius;
document.getElementById('radiusWert').textContent = radius.toFixed(1).replace('.', ',');
return true;
}
} catch (e) { /* neue Session ohne Layout — Preset zeigen */ }
return false;
}
/* ============================================================ Verdrahtung */
document.getElementById('btnUndo').onclick = undo;
document.getElementById('btnRedo').onclick = redo;
document.getElementById('btnAdd').onclick = tischHinzufuegen;
document.getElementById('btnReihe').onclick = presetReihe;
document.getElementById('btnRaster').onclick = presetRaster;
document.getElementById('btnSpeichern').onclick = layoutSpeichern;
document.getElementById('btnIni').onclick = iniLaden;
document.getElementById('raster').onchange = (e) => { rasterSchritt = parseFloat(e.target.value); };
const radiusInput = document.getElementById('radius');
radiusInput.oninput = (e) => {
radius = parseFloat(e.target.value);
document.getElementById('radiusWert').textContent = radius.toFixed(1).replace('.', ',');
zeichne();
};
/* ============================================================ Start */
window.addEventListener('DOMContentLoaded', async () => {
try {
sitzung = await sitzungHolen();
document.getElementById('sitzungsId').textContent = sitzung;
} catch (e) {
document.getElementById('sitzungsId').textContent = 'Fehler: ' + e.message;
}
const geladen = sitzung ? await layoutLaden() : false;
if (!geladen) state.tische = [1, 2, 3, 4].map(i => ({ id: i, plaetze: 5, x: i - 1, y: 0 }));
knoepfe(); zeichne(); inspektor();
});
</script>
</body>
</html>
+77 -23
View File
@@ -53,12 +53,23 @@
padding: .3rem .7rem; margin: .15rem .3rem .15rem 0; font-size: .9rem; }
.kennzahl b { color: var(--akzent); }
.hinweis { font-size: .8rem; color: #6b6557; }
.varianten { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .6rem; }
.variante {
border: 1px solid var(--rand); border-radius: 6px; background: white;
padding: .5rem .8rem; cursor: pointer; font-size: .85rem; text-align: left;
color: #2a2a24; min-width: 11rem;
}
.variante.aktiv { border-color: var(--akzent); background: #e6f0ea;
box-shadow: 0 0 0 1px var(--akzent); }
.variante b { color: var(--akzent); font-size: 1rem; }
.variante .zeile { display: block; color: #6b6557; }
</style>
</head>
<body>
<header>
<h1>Sitz &amp; Platz <small style="font-weight:normal">— Testoberfläche (Meilenstein 1)</small></h1>
<p>Session-basiert, nichts wird dauerhaft gespeichert (TTL 24 h). Sitzung: <code id="sitzungsId">wird angelegt…</code></p>
<p>Session-basiert, nichts wird dauerhaft gespeichert (TTL 24 h). Sitzung: <code id="sitzungsId">wird angelegt…</code>
· <a href="/editor.html" style="color:#d6eade">grafischer Tisch-Editor</a></p>
</header>
<main>
@@ -104,12 +115,15 @@
</section>
<section>
<h2>3. Sitzordnung berechnen</h2>
<p class="hinweis">Jeder Klick startet den genetischen Algorithmus neu und liefert
eine neue Variante — die Wertigkeit (0 = perfekt, negativer = schlechter)
macht Varianten vergleichbar.</p>
<h2>3. Sitzordnung berechnen &amp; Varianten vergleichen</h2>
<p class="hinweis">Ein Lauf liefert bis zu drei <em>unterschiedliche</em> gute
Sitzordnungen zum Vergleich — die Wertigkeit (0 = perfekt, negativer = schlechter),
die Zahl getrennter Gruppen und allein sitzende Personen machen sie vergleichbar.
Variante anklicken, um Tische und Raumplan umzuschalten; jeder weitere Klick auf
„Berechnen“ startet einen frischen Lauf.</p>
<button type="button" id="berechnenKnopf" onclick="berechnen()" disabled>Sitzordnung berechnen</button>
<div class="status" id="rechnenStatus"></div>
<div class="varianten" id="variantenListe"></div>
<div id="kennzahlen"></div>
<div id="ergebnisTabellen"></div>
<img id="svgBild" alt="" style="display:none">
@@ -266,20 +280,22 @@ async function berechnenFreigeben() {
} catch (e) { /* Session ggf. abgelaufen — beim Berechnen gemeldet */ }
}
let letzterLauf = null; // Antwort des letzten Berechnen-Laufs (mit Varianten)
async function berechnen() {
const knopf = document.getElementById('berechnenKnopf');
knopf.disabled = true;
status('rechnenStatus', 'Der genetische Algorithmus läuft…');
try {
const r = await api('/sitzung/' + sitzung + '/berechnen', { method: 'POST' });
status('rechnenStatus', '✓ Variante ' + r.variante + ' berechnet.', 'ok');
knopf.textContent = 'Neue Variante berechnen';
zeigeErgebnis(r);
const svg = await fetch('/sitzung/' + sitzung + '/ergebnis.svg');
const svgText = await svg.text();
const bild = document.getElementById('svgBild');
bild.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgText);
bild.style.display = 'block';
const r = await api('/sitzung/' + sitzung + '/berechnen?varianten=3', { method: 'POST' });
letzterLauf = r;
status('rechnenStatus', '✓ Lauf ' + r.lauf + ': ' + r.varianten.length +
(r.varianten.length === 1
? ' Variante gefunden (der Lauf konvergierte auf eine Sitzordnung).'
: ' unterschiedliche Varianten zum Vergleich.'), 'ok');
knopf.textContent = 'Neuen Lauf berechnen';
zeigeVariantenListe(r);
await zeigeVariante(0);
} catch (e) {
status('rechnenStatus', 'Fehler: ' + e.message, 'fehler');
} finally {
@@ -287,15 +303,31 @@ async function berechnen() {
}
}
function zeigeErgebnis(r) {
const getrennte = r.gruppen.filter(g => g.teilungen > 0).length;
function zeigeVariantenListe(r) {
document.getElementById('variantenListe').innerHTML = r.varianten.map((v, i) =>
'<button type="button" class="variante" data-i="' + i + '" onclick="zeigeVariante(' + i + ')">' +
'<b>Variante ' + v.nr + '</b>' +
'<span class="zeile">Wertigkeit: ' + v.kennzahlen.wertigkeit + '</span>' +
'<span class="zeile">getrennte Gruppen: ' + v.kennzahlen.getrennteGruppen +
' (' + v.kennzahlen.teilungenGesamt + ' Teilungen)</span>' +
'<span class="zeile">allein sitzend: ' + v.kennzahlen.alleinePersonen + '</span>' +
'</button>').join('');
}
async function zeigeVariante(i) {
if (!letzterLauf || !letzterLauf.varianten[i]) return;
const v = letzterLauf.varianten[i];
document.querySelectorAll('#variantenListe .variante').forEach(el =>
el.classList.toggle('aktiv', parseInt(el.dataset.i, 10) === i));
document.getElementById('kennzahlen').innerHTML =
'<span class="kennzahl">Wertigkeit: <b>' + r.wertigkeit + '</b></span>' +
'<span class="kennzahl">Variante: <b>' + r.variante + '</b></span>' +
'<span class="kennzahl">getrennte Gruppen: <b>' + getrennte + '</b></span>';
'<span class="kennzahl">Variante: <b>' + v.nr + '</b> (Lauf ' + letzterLauf.lauf + ')</span>' +
'<span class="kennzahl">Wertigkeit: <b>' + v.kennzahlen.wertigkeit + '</b></span>' +
'<span class="kennzahl">getrennte Gruppen: <b>' + v.kennzahlen.getrennteGruppen + '</b></span>' +
'<span class="kennzahl">allein sitzend: <b>' + v.kennzahlen.alleinePersonen + '</b></span>';
let html = '<table><thead><tr><th>Tisch</th><th>Belegung</th><th>Gäste (Gruppe)</th></tr></thead><tbody>';
for (const t of r.tische) {
for (const t of v.tische) {
const gaeste = t.personen.map(p =>
((p.titel ? p.titel + ' ' : '') + p.vorname + ' ' + p.nachname +
' <span class="hinweis">(' + p.gruppe + ')</span>')).join(', ');
@@ -306,10 +338,19 @@ function zeigeErgebnis(r) {
html += '<h2 style="margin-top:1rem">Gruppen</h2>' +
'<table><thead><tr><th>Gruppe</th><th>Personen</th><th>Teilungen</th><th>Strafpunkte</th></tr></thead><tbody>' +
r.gruppen.map(g => '<tr><td>' + g.name + '</td><td>' + g.anzahl + '</td><td>' +
v.gruppen.map(g => '<tr><td>' + g.name + '</td><td>' + g.anzahl + '</td><td>' +
g.teilungen + '</td><td>' + g.wert + '</td></tr>').join('') +
'</tbody></table>';
document.getElementById('ergebnisTabellen').innerHTML = html;
// Raumplan der gewaehlten Variante
try {
const svg = await fetch('/sitzung/' + sitzung + '/ergebnis.svg?variante=' + v.nr);
const svgText = await svg.text();
const bild = document.getElementById('svgBild');
bild.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgText);
bild.style.display = 'block';
} catch (e) { /* SVG optional — Tabellen stehen schon da */ }
}
/* ------------------------------------------------ Start */
@@ -317,13 +358,26 @@ window.addEventListener('DOMContentLoaded', async () => {
document.getElementById('csvText').value = beispielCSV;
presetReihe();
try {
const r = await api('/sitzung', { method: 'POST' });
sitzung = r.id;
sitzung = await sitzungHolen();
document.getElementById('sitzungsId').textContent = sitzung;
} catch (e) {
document.getElementById('sitzungsId').textContent = 'Fehler: ' + e.message;
}
});
// Session mit dem grafischen Editor (editor.html) ueber localStorage teilen:
// eine bestehende, noch gueltige Sitzung wiederverwenden statt jedes Mal
// eine neue anzulegen — so wirkt ein im Editor gebautes Layout hier weiter.
async function sitzungHolen() {
let id = localStorage.getItem('platzSitzung');
if (id) {
const ok = await fetch('/sitzung/' + id).then(r => r.ok).catch(() => false);
if (ok) return id;
}
const r = await api('/sitzung', { method: 'POST' });
localStorage.setItem('platzSitzung', r.id);
return r.id;
}
</script>
</body>
</html>