Web-GUI: fixierte Gruppen anzeigen, Strafprofil waehlen, Farben nutzen

CSVConfig.HandleGaesteliste liefert zusaetzlich FixierteGruppen (Name ->
Tisch/Anzahl), damit fest platzierte Gaeste in der Gaesteliste sichtbar
(grau hinterlegt) erscheinen. Durchgaengig "VIP" -> "fixiert" umbenannt.

Backend: Strafprofile werden gecacht geladen, neuer Endpoint /strafprofile
und Query-Parameter strafen an /berechnen, damit sich Profile (default vs.
locker) in der GUI vergleichen lassen. SVG-Ausgabe nutzt das Farbschema aus
farben.cfg (Rueckfall auf Palette).

Frontend: tische.ini und CSV werden bei Auswahl sofort geladen, Auswahl der
fixierten Gruppen und des Optimierungsprofils in der Oberflaeche.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Stangl
2026-07-27 10:10:41 +02:00
parent d19913a53d
commit c6e0b95d81
3 changed files with 124 additions and 29 deletions
+14 -3
View File
@@ -84,6 +84,11 @@ class CSVConfig:
VIPGruppe = Gruppe( 0 ) VIPGruppe = Gruppe( 0 )
VipHash = {} VipHash = {}
GruppenNamen = {} GruppenNamen = {}
# fest platzierte ("fixierte") Gruppen fuer die Anzeige: Name ->
# {'tisch': Tischnummer, 'anzahl': Personenzahl}. VIPs laufen nicht
# durch die GA-Platzierung, sollen in der Gaesteliste aber sichtbar
# (grau hinterlegt) erscheinen.
FixierteGruppen = {}
# Gruppen entstehen beim ersten Auftreten ihres Namens; # Gruppen entstehen beim ersten Auftreten ihres Namens;
# Einzelpersonen (leerer Gruppenname) bekommen eine eigene Gruppe # Einzelpersonen (leerer Gruppenname) bekommen eine eigene Gruppe
@@ -126,9 +131,13 @@ class CSVConfig:
PID = PID + 1 PID = PID + 1
P = Person( PID, Vorname, Nachname, Titel ) P = Person( PID, Vorname, Nachname, Titel )
if TischNr is not None: if TischNr is not None:
# VIP: fester Tisch, keine GA-Platzierung # fixiert: fester Tisch, keine GA-Platzierung
VipHash[ PID ] = TischNr VipHash[ PID ] = TischNr
VIPGruppe.Person_dazu( P ) VIPGruppe.Person_dazu( P )
Name = GruppenName if GruppenName else 'Fixiert Tisch %d' % TischNr
Eintrag = FixierteGruppen.setdefault(
Name, { 'tisch': TischNr, 'anzahl': 0 } )
Eintrag['anzahl'] = Eintrag['anzahl'] + 1
else: else:
PN.PersonAdd( P ) PN.PersonAdd( P )
if GruppenName == '': if GruppenName == '':
@@ -155,7 +164,7 @@ class CSVConfig:
if G.Anzahl() > 0: if G.Anzahl() > 0:
GN.Gruppe_dazu( G ) GN.Gruppe_dazu( G )
return [ PN, GN, VIPGruppe, VipHash, GruppenNamen ] return [ PN, GN, VIPGruppe, VipHash, GruppenNamen, FixierteGruppen ]
def laden(self, FileName): def laden(self, FileName):
"""lese alle Eingangsdaten aus einer CSV-Datei ein""" """lese alle Eingangsdaten aus einer CSV-Datei ein"""
@@ -179,12 +188,14 @@ Einzel;Gast;;;;
""" """
print("\n-- Parse Beispiel-CSV:") print("\n-- Parse Beispiel-CSV:")
C = CSVConfig() C = CSVConfig()
[ PN, GN, VIPGruppe, VipHash, GruppenNamen ] = C.HandleGaesteliste( Beispiel ) [ PN, GN, VIPGruppe, VipHash, GruppenNamen, FixierteGruppen ] = \
C.HandleGaesteliste( Beispiel )
print(PN) print(PN)
print(GN) print(GN)
print(VIPGruppe) print(VIPGruppe)
print(VipHash) print(VipHash)
print(GruppenNamen) print(GruppenNamen)
print(FixierteGruppen)
else: else:
pass pass
+64 -9
View File
@@ -44,6 +44,7 @@ from pydantic import BaseModel, Field
from ga import Farm, Zyklus from ga import Farm, Zyklus
from CSVConfig import CSVConfig from CSVConfig import CSVConfig
from Farben import Farbschema
from Strukturdaten import Sitzplatzverteilung, Strafliste, Tisch, Tische from Strukturdaten import Sitzplatzverteilung, Strafliste, Tisch, Tische
app = FastAPI( app = FastAPI(
@@ -62,11 +63,43 @@ ZYKLUSDATEI = os.path.join( CFGDIR, 'zyklus.cfg' )
STRAFENDATEI = os.path.join( CFGDIR, 'strafen.cfg' ) STRAFENDATEI = os.path.join( CFGDIR, 'strafen.cfg' )
ZYKLUSART = os.environ.get( 'PLATZ_ZYKLUSART', 'Adaptiv' ) ZYKLUSART = os.environ.get( 'PLATZ_ZYKLUSART', 'Adaptiv' )
STRAFENART = os.environ.get( 'PLATZ_STRAFENART', 'default' ) STRAFENART = os.environ.get( 'PLATZ_STRAFENART', 'default' )
FARBENDATEI = os.path.join( CFGDIR, 'farben.cfg' )
FARBENART = os.environ.get( 'PLATZ_FARBENART', 'grosse_gruppen' )
GAZyklus = Zyklus() GAZyklus = Zyklus()
GAZyklus.laden( ZYKLUSDATEI, ZYKLUSART ) GAZyklus.laden( ZYKLUSDATEI, ZYKLUSART )
Strafen = Strafliste()
Strafen.laden( STRAFENDATEI, STRAFENART ) # Strafprofile werden bei Bedarf geladen und gecacht, damit sich in der GUI
# verschiedene Profile (z.B. 'default' vs 'locker') auswaehlen und vergleichen
# lassen. Das Default-Profil wird schon beim Start geladen (faellt frueh auf,
# falls die Datei/Sektion kaputt ist).
StrafProfile = {}
def strafprofil(Name):
"""liefert (gecacht) die Strafliste einer Sektion aus strafen.cfg;
wirft ValueError bei unbekannter Sektion"""
if Name not in StrafProfile:
SL = Strafliste()
SL.laden( STRAFENDATEI, Name )
StrafProfile[Name] = SL
return StrafProfile[Name]
def strafprofil_namen():
"""alle in strafen.cfg vorhandenen Profilnamen (fuer die GUI-Auswahl)"""
cfg = configparser.ConfigParser()
cfg.read( STRAFENDATEI )
return cfg.sections()
strafprofil( STRAFENART ) # Default vorwaermen / frueh scheitern
# Farbprofil fuer die SVG-Ausgabe; faellt bei fehlender Datei/Sektion auf die
# eingebaute Palette zurueck (Farbschema=None), damit der Server trotzdem laeuft.
try:
Farben = Farbschema().laden( FARBENDATEI, FARBENART )
except (ValueError, OSError):
Farben = None
# ---------------------------------------------------------------- Sessions # ---------------------------------------------------------------- Sessions
SESSION_TTL_SEKUNDEN = 24 * 3600 SESSION_TTL_SEKUNDEN = 24 * 3600
@@ -160,7 +193,7 @@ def _VarianteJSON(SV, GruppenNamen, Nr):
G = SV.Gruppen.GruppeVonPid( P.Id ) G = SV.Gruppen.GruppeVonPid( P.Id )
return GruppenNamen.get( G.Id, 'Gruppe %s' % G.Id ) return GruppenNamen.get( G.Id, 'Gruppe %s' % G.Id )
except KeyError: except KeyError:
return 'VIP' return 'fixiert'
TischListe = [] TischListe = []
for T in SV.Tische: for T in SV.Tische:
@@ -220,6 +253,7 @@ def _ErgebnisJSON(S):
GruppenNamen = S.get( 'gruppenNamen', {} ) GruppenNamen = S.get( 'gruppenNamen', {} )
return { return {
'lauf': S['laeufe'], 'lauf': S['laeufe'],
'strafen': S.get( 'strafen', STRAFENART ), # verwendetes Strafprofil
'varianten': [ _VarianteJSON( SV, GruppenNamen, Nr + 1 ) 'varianten': [ _VarianteJSON( SV, GruppenNamen, Nr + 1 )
for Nr, SV in enumerate( S['ergebnisse'] ) ], for Nr, SV in enumerate( S['ergebnisse'] ) ],
} }
@@ -389,7 +423,7 @@ def PersonenHochladen(SitzungsId: str, datei: UploadFile = File(...)):
Text = Rohdaten.decode( 'cp1252' ) # Excel/Windows-Fallback Text = Rohdaten.decode( 'cp1252' ) # Excel/Windows-Fallback
try: try:
[ PN, GN, VIPGruppe, VipHash, GruppenNamen ] = \ [ PN, GN, VIPGruppe, VipHash, GruppenNamen, FixierteGruppen ] = \
CSVConfig().HandleGaesteliste( Text ) CSVConfig().HandleGaesteliste( Text )
except ValueError as Fehler: except ValueError as Fehler:
raise HTTPException( status_code=400, detail=str(Fehler) ) raise HTTPException( status_code=400, detail=str(Fehler) )
@@ -407,12 +441,15 @@ def PersonenHochladen(SitzungsId: str, datei: UploadFile = File(...)):
return { return {
'personen': GN.NLeute, 'personen': GN.NLeute,
'vips': VIPGruppe.Anzahl(), 'fixiert': VIPGruppe.Anzahl(),
'gruppen': [ { 'gruppen': [ {
'id': G.Id, 'id': G.Id,
'name': GruppenNamen.get( G.Id, 'Gruppe %s' % G.Id ), 'name': GruppenNamen.get( G.Id, 'Gruppe %s' % G.Id ),
'anzahl': G.Anzahl(), 'anzahl': G.Anzahl(),
} for G in GN ], } for G in GN ],
# fest platzierte Gruppen (fuer die grau hinterlegte Anzeige)
'fixierte': [ { 'name': Name, 'tisch': Info['tisch'], 'anzahl': Info['anzahl'] }
for Name, Info in FixierteGruppen.items() ],
} }
@@ -420,9 +457,14 @@ def PersonenHochladen(SitzungsId: str, datei: UploadFile = File(...)):
def Berechnen(SitzungsId: str, def Berechnen(SitzungsId: str,
varianten: int = Query( default=3, ge=1, le=5, varianten: int = Query( default=3, ge=1, le=5,
description='wieviele unterschiedliche Varianten der Lauf ' description='wieviele unterschiedliche Varianten der Lauf '
'zurueckgeben soll (aus Farm.TopN, dedupliziert)' )): 'zurueckgeben soll (aus Farm.TopN, dedupliziert)' ),
strafen: str = Query( default=STRAFENART,
description='Name des Strafpunkte-Profils (Sektion in strafen.cfg), '
'z.B. default oder locker - zum Vergleich waehlbar' )):
"""startet die GA-Optimierung; jeder Aufruf ist ein neuer Lauf und """startet die GA-Optimierung; jeder Aufruf ist ein neuer Lauf und
liefert bis zu 'varianten' unterschiedliche gute Sitzordnungen""" liefert bis zu 'varianten' unterschiedliche gute Sitzordnungen. Das
Strafpunkte-Profil ('strafen') steuert die Bewertung waehrend der
Optimierung - so lassen sich z.B. default und locker vergleichen."""
S = _GibSession( SitzungsId ) S = _GibSession( SitzungsId )
if S['tische'] is None: if S['tische'] is None:
raise HTTPException( status_code=400, raise HTTPException( status_code=400,
@@ -454,9 +496,14 @@ def Berechnen(SitzungsId: str,
detail='Tisch %d hat %d Plaetze, aber %d VIPs sind ' detail='Tisch %d hat %d Plaetze, aber %d VIPs sind '
'dort fest gesetzt' % (Tid, T.Plaetze, AnzahlVIPs) ) 'dort fest gesetzt' % (Tid, T.Plaetze, AnzahlVIPs) )
try:
SL = strafprofil( strafen )
except ValueError as Fehler:
raise HTTPException( status_code=400, detail=str(Fehler) )
try: try:
GAFarm = Farm( GAZyklus, Sitzplatzverteilung, GAFarm = Farm( GAZyklus, Sitzplatzverteilung,
Tische=TE, Gruppen=P['GN'], Strafen=Strafen, Tische=TE, Gruppen=P['GN'], Strafen=SL,
VIPListe=P['VipHash'], VIPs=P['VIPGruppe'] ) VIPListe=P['VipHash'], VIPs=P['VIPGruppe'] )
except IndexError as Fehler: except IndexError as Fehler:
raise HTTPException( status_code=400, detail=str(Fehler) ) raise HTTPException( status_code=400, detail=str(Fehler) )
@@ -481,9 +528,17 @@ def Berechnen(SitzungsId: str,
S['ergebnisse'] = Ergebnisse S['ergebnisse'] = Ergebnisse
S['laeufe'] = S['laeufe'] + 1 S['laeufe'] = S['laeufe'] + 1
S['strafen'] = strafen # fuer die Anzeige, welches Profil den Lauf ergab
return _ErgebnisJSON( S ) return _ErgebnisJSON( S )
@app.get( '/strafprofile' )
def Strafprofile():
"""verfuegbare Strafpunkte-Profile (Sektionen in strafen.cfg) fuer die
Auswahl in der GUI; 'vorgabe' ist das voreingestellte Profil."""
return { 'profile': strafprofil_namen(), 'vorgabe': STRAFENART }
@app.get( '/sitzung/{SitzungsId}/ergebnis' ) @app.get( '/sitzung/{SitzungsId}/ergebnis' )
def Ergebnis(SitzungsId: str): def Ergebnis(SitzungsId: str):
"""alle Varianten des letzten Laufs als JSON (beste zuerst)""" """alle Varianten des letzten Laufs als JSON (beste zuerst)"""
@@ -513,7 +568,7 @@ def ErgebnisSVG(SitzungsId: str,
os.close( Handle ) os.close( Handle )
try: try:
S['ergebnisse'][variante - 1].alsSVG( TempPfad, S['ergebnisse'][variante - 1].alsSVG( TempPfad,
GruppenNamen=S.get( 'gruppenNamen', {} ) ) GruppenNamen=S.get( 'gruppenNamen', {} ), Farbschema=Farben )
fsock = open( TempPfad, 'r' ) fsock = open( TempPfad, 'r' )
SVG = fsock.read() SVG = fsock.read()
fsock.close() fsock.close()
+46 -17
View File
@@ -61,6 +61,7 @@
padding: .3rem .7rem; margin: .15rem .3rem .15rem 0; font-size: .9rem; } padding: .3rem .7rem; margin: .15rem .3rem .15rem 0; font-size: .9rem; }
.kennzahl b { color: var(--akzent); } .kennzahl b { color: var(--akzent); }
.hinweis { font-size: .8rem; color: #6b6557; } .hinweis { font-size: .8rem; color: #6b6557; }
#gruppenUebersicht tr.fixiert td { background: #e6e3dc; color: #555; }
.varianten { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .6rem; } .varianten { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .6rem; }
.variante { .variante {
border: 1px solid var(--rand); border-radius: 6px; background: white; border: 1px solid var(--rand); border-radius: 6px; background: white;
@@ -97,10 +98,9 @@
<button type="button" class="sekundaer" onclick="presetReihe()">Preset: 4er-Reihe</button> <button type="button" class="sekundaer" onclick="presetReihe()">Preset: 4er-Reihe</button>
<button type="button" class="sekundaer" onclick="presetRaster()">Preset: 3×3-Raster</button> <button type="button" class="sekundaer" onclick="presetRaster()">Preset: 3×3-Raster</button>
<br> <br>
<label class="hinweis">Oder eine bestehende <code>tische.ini</code> laden <label class="hinweis">Oder eine bestehende <code>tische.ini</code> auswählen
(z.&nbsp;B. aus <code>work/hochzeit</code>):</label> (z.&nbsp;B. aus <code>work/hochzeit</code>) — wird sofort geladen:</label>
<input type="file" id="tischDatei" accept=".ini,text/plain"> <input type="file" id="tischDatei" accept=".ini,text/plain" onchange="tischDateiLaden()">
<button type="button" class="sekundaer" onclick="tischDateiLaden()">tische.ini laden</button>
<br> <br>
<button type="button" onclick="tischeSpeichern()">Tische speichern</button> <button type="button" onclick="tischeSpeichern()">Tische speichern</button>
<div class="status" id="tischStatus"></div> <div class="status" id="tischStatus"></div>
@@ -110,9 +110,10 @@
<h2>2. Gästeliste (CSV)</h2> <h2>2. Gästeliste (CSV)</h2>
<p class="hinweis">Spalten: <code>Vorname;Nachname;Titel;Gruppe;Tisch;Anzahl</code> <p class="hinweis">Spalten: <code>Vorname;Nachname;Titel;Gruppe;Tisch;Anzahl</code>
<code>Gruppe</code> hält Personen zusammen (leer = Einzelgast), <code>Gruppe</code> hält Personen zusammen (leer = Einzelgast),
<code>Tisch</code> setzt VIPs fest (z.&nbsp;B. Brautpaar an Tisch 1), <code>Tisch</code> fixiert Gäste an einem Tisch (z.&nbsp;B. Brautpaar an Tisch 1),
<code>Anzahl</code> expandiert eine Zeile in mehrere Buchungen <code>Anzahl</code> expandiert eine Zeile in mehrere Buchungen
(anonyme Sammelbuchung). Datei hochladen <em>oder</em> direkt hier bearbeiten:</p> (anonyme Sammelbuchung). Eine Datei auswählen (wird sofort übernommen)
<em>oder</em> hier direkt bearbeiten und übernehmen:</p>
<textarea id="csvText"></textarea> <textarea id="csvText"></textarea>
<br> <br>
<input type="file" id="csvDatei" accept=".csv,text/csv"> <input type="file" id="csvDatei" accept=".csv,text/csv">
@@ -128,7 +129,11 @@
Sitzordnungen zum Vergleich — die Wertigkeit (0 = perfekt, negativer = schlechter), Sitzordnungen zum Vergleich — die Wertigkeit (0 = perfekt, negativer = schlechter),
die Zahl getrennter Gruppen und allein sitzende Personen machen sie vergleichbar. die Zahl getrennter Gruppen und allein sitzende Personen machen sie vergleichbar.
Variante anklicken, um Tische und Raumplan umzuschalten; jeder weitere Klick auf Variante anklicken, um Tische und Raumplan umzuschalten; jeder weitere Klick auf
„Berechnen“ startet einen frischen Lauf.</p> „Berechnen“ startet einen frischen Lauf. Über das <em>Optimierungsprofil</em> lassen sich
Bewertungs-Vorgehen vergleichen (z.&nbsp;B. <code>default</code> vs.
<code>locker</code>): Profil wählen und neu berechnen.</p>
<label class="hinweis">Optimierungsprofil:
<select id="strafenWahl"></select></label>
<button type="button" id="berechnenKnopf" onclick="berechnen()" disabled>Sitzordnung berechnen</button> <button type="button" id="berechnenKnopf" onclick="berechnen()" disabled>Sitzordnung berechnen</button>
<div class="status" id="rechnenStatus"></div> <div class="status" id="rechnenStatus"></div>
<div class="varianten" id="variantenListe"></div> <div class="varianten" id="variantenListe"></div>
@@ -259,6 +264,7 @@ async function tischDateiLaden() {
'„Tische speichern“ ist nur nach Änderungen nötig.', 'ok'); '„Tische speichern“ ist nur nach Änderungen nötig.', 'ok');
berechnenFreigeben(); berechnenFreigeben();
} catch (e) { status('tischStatus', 'Fehler: ' + e.message, 'fehler'); } } catch (e) { status('tischStatus', 'Fehler: ' + e.message, 'fehler'); }
finally { eingabe.value = ''; } // damit dieselbe Datei erneut auswählbar ist
} }
/* ------------------------------------------------ 2. Gäste */ /* ------------------------------------------------ 2. Gäste */
@@ -293,11 +299,10 @@ async function csvDateiGewaehlt(ev) {
} }
text = text.replace(/^/, ''); text = text.replace(/^/, '');
document.getElementById('csvText').value = text; document.getElementById('csvText').value = text;
// stale Ausgabe eines frueheren Uploads entfernen ev.target.value = ''; // damit dieselbe Datei erneut auswählbar ist
document.getElementById('gruppenUebersicht').innerHTML = ''; // direkt uebernehmen: die ausgewaehlte Datei wird sofort geladen (die
status('gaesteStatus', 'Datei „' + datei.name + '“ geladen — zum Übernehmen ' + // Textarea ist bereits befuellt und dient gaesteHochladen als Quelle)
'„Gästeliste übernehmen“ klicken.', 'ok'); await gaesteHochladen();
ev.target.value = ''; // Textarea ist ab jetzt die alleinige Quelle
} }
async function gaesteHochladen() { async function gaesteHochladen() {
@@ -308,16 +313,35 @@ async function gaesteHochladen() {
try { try {
const r = await api('/sitzung/' + sitzung + '/personen', { method: 'POST', body: form }); const r = await api('/sitzung/' + sitzung + '/personen', { method: 'POST', body: form });
status('gaesteStatus', '✓ ' + r.personen + ' Personen in ' + r.gruppen.length + status('gaesteStatus', '✓ ' + r.personen + ' Personen in ' + r.gruppen.length +
' Gruppen, dazu ' + r.vips + ' VIPs mit festem Tisch.', 'ok'); ' Gruppen' + (r.fixiert ? ', dazu ' + r.fixiert +
' fixierte Personen an festen Tischen' : '') + '.', 'ok');
// regulaere Gruppen, danach die fest platzierten ("fixierten") Gruppen
// grau hinterlegt
const regulaer = r.gruppen.map(g =>
'<tr><td>' + g.name + '</td><td>' + g.anzahl + '</td></tr>').join('');
const fixiert = (r.fixierte || []).map(f =>
'<tr class="fixiert"><td>' + f.name +
' <span class="hinweis">(fixiert an Tisch ' + f.tisch + ')</span></td><td>' +
f.anzahl + '</td></tr>').join('');
const html = '<table><thead><tr><th>Gruppe</th><th>Personen</th></tr></thead><tbody>' + const html = '<table><thead><tr><th>Gruppe</th><th>Personen</th></tr></thead><tbody>' +
r.gruppen.map(g => '<tr><td>' + g.name + '</td><td>' + g.anzahl + '</td></tr>').join('') + regulaer + fixiert + '</tbody></table>';
'</tbody></table>';
document.getElementById('gruppenUebersicht').innerHTML = html; document.getElementById('gruppenUebersicht').innerHTML = html;
berechnenFreigeben(); berechnenFreigeben();
} catch (e) { status('gaesteStatus', 'Fehler: ' + e.message, 'fehler'); } } catch (e) { status('gaesteStatus', 'Fehler: ' + e.message, 'fehler'); }
} }
/* ------------------------------------------------ 3. Berechnen */ /* ------------------------------------------------ 3. Berechnen */
async function strafprofileLaden() {
// Auswahl der Strafprofile aus strafen.cfg fuellen (z.B. default, locker)
try {
const r = await api('/strafprofile');
const sel = document.getElementById('strafenWahl');
sel.innerHTML = r.profile.map(p =>
'<option value="' + p + '"' + (p === r.vorgabe ? ' selected' : '') +
'>' + p + '</option>').join('');
} catch (e) { /* Auswahl bleibt leer -> Backend nutzt sein Default */ }
}
async function berechnenFreigeben() { async function berechnenFreigeben() {
try { try {
const s = await api('/sitzung/' + sitzung); const s = await api('/sitzung/' + sitzung);
@@ -332,9 +356,12 @@ async function berechnen() {
knopf.disabled = true; knopf.disabled = true;
status('rechnenStatus', 'Der genetische Algorithmus läuft…'); status('rechnenStatus', 'Der genetische Algorithmus läuft…');
try { try {
const r = await api('/sitzung/' + sitzung + '/berechnen?varianten=3', { method: 'POST' }); const profil = document.getElementById('strafenWahl').value || 'default';
const r = await api('/sitzung/' + sitzung + '/berechnen?varianten=3&strafen=' +
encodeURIComponent(profil), { method: 'POST' });
letzterLauf = r; letzterLauf = r;
status('rechnenStatus', '✓ Lauf ' + r.lauf + ': ' + r.varianten.length + status('rechnenStatus', '✓ Lauf ' + r.lauf + ' (Optimierungsprofil „' + r.strafen + '“): ' +
r.varianten.length +
(r.varianten.length === 1 (r.varianten.length === 1
? ' Variante gefunden (der Lauf konvergierte auf eine Sitzordnung).' ? ' Variante gefunden (der Lauf konvergierte auf eine Sitzordnung).'
: ' unterschiedliche Varianten zum Vergleich.'), 'ok'); : ' unterschiedliche Varianten zum Vergleich.'), 'ok');
@@ -367,6 +394,7 @@ async function zeigeVariante(i) {
document.getElementById('kennzahlen').innerHTML = document.getElementById('kennzahlen').innerHTML =
'<span class="kennzahl">Variante: <b>' + v.nr + '</b> (Lauf ' + letzterLauf.lauf + ')</span>' + '<span class="kennzahl">Variante: <b>' + v.nr + '</b> (Lauf ' + letzterLauf.lauf + ')</span>' +
'<span class="kennzahl">Optimierungsprofil: <b>' + letzterLauf.strafen + '</b></span>' +
'<span class="kennzahl">Wertigkeit: <b>' + v.kennzahlen.wertigkeit + '</b></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">getrennte Gruppen: <b>' + v.kennzahlen.getrennteGruppen + '</b></span>' +
'<span class="kennzahl">allein sitzend: <b>' + v.kennzahlen.alleinePersonen + '</b></span>'; '<span class="kennzahl">allein sitzend: <b>' + v.kennzahlen.alleinePersonen + '</b></span>';
@@ -439,6 +467,7 @@ function gruppeInteraktion(svg) {
window.addEventListener('DOMContentLoaded', async () => { window.addEventListener('DOMContentLoaded', async () => {
document.getElementById('csvText').value = beispielCSV; document.getElementById('csvText').value = beispielCSV;
document.getElementById('csvDatei').addEventListener('change', csvDateiGewaehlt); document.getElementById('csvDatei').addEventListener('change', csvDateiGewaehlt);
strafprofileLaden();
try { try {
sitzung = await sitzungHolen(); sitzung = await sitzungHolen();
document.getElementById('sitzungsId').textContent = sitzung; document.getElementById('sitzungsId').textContent = sitzung;