Compare commits

...

4 Commits

Author SHA1 Message Date
Michael Stangl 6ee33eb085 Beispiel-/Doku-Ausgaben neu erzeugt
Regenerierte SVG-Doku-Bilder und die hochzeit-Beispielausgaben
(TischePersonen.txt / PersonenTische.csv) nach den aktuellen Aenderungen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 10:11:38 +02:00
Michael Stangl cded317a40 Doku: Farbprofile (--farben, cfg/farben.cfg, libs/Farben.py)
CLAUDE.md dokumentiert das neue SVG-Farbprofil-Feature: den Schalter
--farben, die Profile in cfg/farben.cfg (palette / kategorien) und das
Modul libs/Farben.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 10:11:25 +02:00
Michael Stangl c6e0b95d81 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>
2026-07-27 10:10:41 +02:00
Michael Stangl d19913a53d SVG-Farbprofile: --farben mit palette- und kategorien-Modus
Neues Modul libs/Farben.py mit Farbschema, das Gruppen Farben zuordnet,
gesteuert durch Profile aus cfg/farben.cfg. Zwei Modi: "palette" (Farbliste
zyklisch je Gruppe, Profil grosse_gruppen) und "kategorien" (Hochzeit:
Familie/Verein/VIP getrennt ueber Farbverlaeufe, Profil hochzeit).

Sitzplatzverteilung.alsSVG bekommt Parameter Farbschema; Farben werden nun
fest fuer die vollstaendige Gruppenliste (nach Id) vergeben statt lazy in
Zeichenreihenfolge, damit jede Gruppe variantenuebergreifend dieselbe Farbe
hat. platz.py bekommt Schalter --farben (Vorgabe grosse_gruppen).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 10:10:23 +02:00
12 changed files with 522 additions and 185 deletions
+15 -2
View File
@@ -73,8 +73,9 @@ the same layout:
The GA cycle and penalty scheme are read directly from `$PLATZ_CFG/zyklus.cfg` and The GA cycle and penalty scheme are read directly from `$PLATZ_CFG/zyklus.cfg` and
`$PLATZ_CFG/strafen.cfg` (independent of `--indir`). There is no `platz.cfg` indirection any `$PLATZ_CFG/strafen.cfg` (independent of `--indir`). There is no `platz.cfg` indirection any
more — it was removed; `zyklus.cfg`/`strafen.cfg` are loaded straight from `$PLATZ_CFG`, the more — it was removed; `zyklus.cfg`/`strafen.cfg` are loaded straight from `$PLATZ_CFG`, the
GA profile is chosen by the `--zyklus-art <name>` switch (default `Adaptiv`), and the penalty GA profile is chosen by the `--zyklus-art <name>` switch (default `Adaptiv`), the penalty
profile by the `--strafen <name>` switch (default `default`). profile by the `--strafen <name>` switch (default `default`), and the SVG colour profile by
the `--farben <name>` switch (default `grosse_gruppen`).
- `cfg/zyklus.cfg` — defines the GA profiles. Two flavours: **static** profiles (`Easy`, - `cfg/zyklus.cfg` — defines the GA profiles. Two flavours: **static** profiles (`Easy`,
`Simple`) give an explicit sequence of actions (`e`=erzeugen/create, `s`=selektieren/ `Simple`) give an explicit sequence of actions (`e`=erzeugen/create, `s`=selektieren/
@@ -97,6 +98,18 @@ profile by the `--strafen <name>` switch (default `default`).
between split-off parts; absent = distance not penalised). `Strafliste.laden(FileName, between split-off parts; absent = distance not penalised). `Strafliste.laden(FileName,
Sektion='default')` parses one section; the old `[Trennung]`/`[Globals]`/`[Abstand]` Sektion='default')` parses one section; the old `[Trennung]`/`[Globals]`/`[Abstand]`
three-section layout is gone. three-section layout is gone.
- `cfg/farben.cfg` — SVG colour profiles, chosen by `--farben <section>` (default
`grosse_gruppen`) / env `PLATZ_FARBENART`, parsed by `Farben.Farbschema.laden()` (in
`libs/Farben.py`) and passed into `Sitzplatzverteilung.alsSVG(..., Farbschema=...)`. Two
modes: `Modus = palette` (a `Palette` colour list cycled per group by sorted group id — the
`grosse_gruppen` profile) and `Modus = kategorien` (the `hochzeit` profile — splits groups
into **Familie** = members mostly share a surname, coloured along `FamilienVerlaeufe`
gradients blue→green / yellow→orange, one gradient bucket per surname; **Verein** = any
other named group, along `VereinVerlauf` violet→grey; **VIP** = fixed-table people with no
GA group, the start of `VIPVerlauf`, red). Gradient syntax `"start -> end"`. Anonymous bulk
bookings (all members identical, e.g. an `anzahl`-expanded "Gast Musikverein") are *not*
treated as a family. Absent/invalid config falls back to the built-in palette
(`Farbschema=None`), so the SVG always renders.
`tische.ini`/`bestellung.json`/`TischePersonen.txt`/`PersonenTische.csv` are fixed filenames `tische.ini`/`bestellung.json`/`TischePersonen.txt`/`PersonenTische.csv` are fixed filenames
resolved as `os.path.join(indir, ...)` in `platz.py`. resolved as `os.path.join(indir, ...)` in `platz.py`.
+44
View File
@@ -0,0 +1,44 @@
# Farbprofile fuer die SVG-Ausgabe (Sitzplatzverteilung.alsSVG).
#
# Jede Sektion ist ein Farbprofil, waehlbar per --farben <Sektion> (libs/platz.py,
# Vorgabe: grosse_gruppen) bzw. ueber PLATZ_FARBENART (Web). So laesst sich je
# Anlass ein anderes Farbschema vorwaehlen.
#
# Modus = palette
# Eine Farbliste (Palette), die zyklisch je Gruppe (nach Gruppen-Id sortiert)
# vergeben wird. Gut fuer viele/grosse Gruppen, wo es nur auf klare
# Unterscheidbarkeit ankommt.
#
# Modus = kategorien
# Trennt die Gruppen nach Art und faerbt sie unterschiedlich ein - gedacht
# fuer Hochzeiten:
# - Familie: eine Gruppe, deren Mitglieder mehrheitlich denselben
# Nachnamen tragen. Jeder Nachname bekommt einen Farbverlauf
# (abwechselnd aus FamilienVerlaeufe), Untergruppen desselben
# Nachnamens laufen den Verlauf entlang (z.B. Blau -> Gruen).
# - Verein: jede sonstige benannte Gruppe (Freunde, Kollegen, Vereine);
# laeuft ueber VereinVerlauf (z.B. Violett -> Grau).
# - VIP: fest gesetzte Gaeste ohne GA-Gruppe (Ehrentafel, Kindertisch);
# bekommen die Startfarbe von VIPVerlauf (Rot).
#
# Farbverlauf-Syntax: "Startfarbe -> Endfarbe"; mehrere Verlaeufe kommagetrennt.
# Fuer grosse Veranstaltungen mit vielen Gruppen: reine Palette, zyklisch.
[grosse_gruppen]
Modus = palette
Palette = #e6194b, #3cb44b, #4363d8, #f58231, #911eb4, #42d4f4, #f032e6,
#bfef45, #fabed4, #469990, #dcbeff, #9a6324, #800000, #aaffc3,
#000075, #808000, #ffd8b1, #f58231, #46f0f0, #d2f53c
# Fuer Hochzeiten: nach Familie / Verein / VIP getrennt.
[hochzeit]
Modus = kategorien
# Familien: abwechselnd Blau -> Gruen und Gelb -> Orange (je Nachname ein
# Verlauf, damit sich verschiedene Familien unterscheiden).
FamilienVerlaeufe = #1f4e9c -> #2fae5f, #f2c200 -> #e8590c
# Vereine / gemischte Gruppen: Violett -> Grau.
VereinVerlauf = #7b2fbf -> #9aa0a6
# VIPs (feste Platzierung): Rot.
VIPVerlauf = #e63946 -> #8b0000
+16 -16
View File
@@ -1,43 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="251.2" height="412" viewBox="0 0 251.2 412"> <svg xmlns="http://www.w3.org/2000/svg" width="251.2" height="412" viewBox="0 0 251.2 412">
<rect width="100%" height="100%" fill="white"/> <rect width="100%" height="100%" fill="white"/>
<polyline points="125.6,353.6 206,286.4" fill="none" stroke="#e6194b" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="0.5"/> <polyline points="125.6,353.6 206,286.4" fill="none" stroke="#f58231" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="0.5"/>
<polyline points="125.6,380 45.2,286.4" fill="none" stroke="#3cb44b" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="0.5"/> <polyline points="125.6,380 45.2,286.4" fill="none" stroke="#4363d8" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="0.5"/>
<polyline points="206,125.6 125.6,32" fill="none" stroke="#4363d8" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="0.5"/> <polyline points="206,125.6 125.6,32" fill="none" stroke="#e6194b" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="0.5"/>
<polyline points="125.6,58.4 45.2,125.6" fill="none" stroke="#f58231" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="0.5"/> <polyline points="125.6,58.4 45.2,125.6" fill="none" stroke="#3cb44b" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="0.5"/>
<circle cx="125.6" cy="366.8" r="35.2" fill="white" stroke="black" stroke-width="1.5"/> <circle cx="125.6" cy="366.8" r="35.2" fill="white" stroke="black" stroke-width="1.5"/>
<circle class="person" data-gid="4" data-gruppe="Gruppe 4" data-name="Vorname10 Nachname10" cx="125.6" cy="353.6" r="12" fill="#e6194b" stroke="black" stroke-width="1"><title>Vorname10 Nachname10 — Gruppe 4</title></circle> <circle class="person" data-gid="4" data-gruppe="Gruppe 4" data-name="Vorname10 Nachname10" cx="125.6" cy="353.6" r="12" fill="#f58231" stroke="black" stroke-width="1"><title>Vorname10 Nachname10 — Gruppe 4</title></circle>
<circle class="person" data-gid="3" data-gruppe="Gruppe 3" data-name="Vorname7 Nachname7" cx="125.6" cy="380" r="12" fill="#3cb44b" stroke="black" stroke-width="1"><title>Vorname7 Nachname7 — Gruppe 3</title></circle> <circle class="person" data-gid="3" data-gruppe="Gruppe 3" data-name="Vorname7 Nachname7" cx="125.6" cy="380" r="12" fill="#4363d8" stroke="black" stroke-width="1"><title>Vorname7 Nachname7 — Gruppe 3</title></circle>
<ellipse cx="125.6" cy="366.8" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/> <ellipse cx="125.6" cy="366.8" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/>
<text x="125.6" y="362" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text> <text x="125.6" y="362" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text>
<text x="125.6" y="375.2" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text> <text x="125.6" y="375.2" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text>
<circle cx="206" cy="286.4" r="35.2" fill="white" stroke="black" stroke-width="1.5"/> <circle cx="206" cy="286.4" r="35.2" fill="white" stroke="black" stroke-width="1.5"/>
<circle class="person" data-gid="4" data-gruppe="Gruppe 4" data-name="Vorname11 Nachname11" cx="206" cy="273.2" r="12" fill="#e6194b" stroke="black" stroke-width="1"><title>Vorname11 Nachname11 — Gruppe 4</title></circle> <circle class="person" data-gid="4" data-gruppe="Gruppe 4" data-name="Vorname11 Nachname11" cx="206" cy="273.2" r="12" fill="#f58231" stroke="black" stroke-width="1"><title>Vorname11 Nachname11 — Gruppe 4</title></circle>
<circle class="person" data-gid="4" data-gruppe="Gruppe 4" data-name="Vorname12 Nachname12" cx="206" cy="299.6" r="12" fill="#e6194b" stroke="black" stroke-width="1"><title>Vorname12 Nachname12 — Gruppe 4</title></circle> <circle class="person" data-gid="4" data-gruppe="Gruppe 4" data-name="Vorname12 Nachname12" cx="206" cy="299.6" r="12" fill="#f58231" stroke="black" stroke-width="1"><title>Vorname12 Nachname12 — Gruppe 4</title></circle>
<ellipse cx="206" cy="286.4" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/> <ellipse cx="206" cy="286.4" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/>
<text x="206" y="281.6" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text> <text x="206" y="281.6" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text>
<text x="206" y="294.8" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text> <text x="206" y="294.8" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text>
<circle cx="206" cy="125.6" r="35.2" fill="white" stroke="black" stroke-width="1.5"/> <circle cx="206" cy="125.6" r="35.2" fill="white" stroke="black" stroke-width="1.5"/>
<circle class="person" data-gid="1" data-gruppe="Gruppe 1" data-name="Vorname2 Nachname2" cx="206" cy="112.4" r="12" fill="#4363d8" stroke="black" stroke-width="1"><title>Vorname2 Nachname2 — Gruppe 1</title></circle> <circle class="person" data-gid="1" data-gruppe="Gruppe 1" data-name="Vorname2 Nachname2" cx="206" cy="112.4" r="12" fill="#e6194b" stroke="black" stroke-width="1"><title>Vorname2 Nachname2 — Gruppe 1</title></circle>
<circle class="person" data-gid="1" data-gruppe="Gruppe 1" data-name="Vorname3 Nachname3" cx="206" cy="138.8" r="12" fill="#4363d8" stroke="black" stroke-width="1"><title>Vorname3 Nachname3 — Gruppe 1</title></circle> <circle class="person" data-gid="1" data-gruppe="Gruppe 1" data-name="Vorname3 Nachname3" cx="206" cy="138.8" r="12" fill="#e6194b" stroke="black" stroke-width="1"><title>Vorname3 Nachname3 — Gruppe 1</title></circle>
<ellipse cx="206" cy="125.6" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/> <ellipse cx="206" cy="125.6" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/>
<text x="206" y="120.8" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text> <text x="206" y="120.8" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text>
<text x="206" y="134" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text> <text x="206" y="134" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text>
<circle cx="125.6" cy="45.2" r="35.2" fill="white" stroke="black" stroke-width="1.5"/> <circle cx="125.6" cy="45.2" r="35.2" fill="white" stroke="black" stroke-width="1.5"/>
<circle class="person" data-gid="1" data-gruppe="Gruppe 1" data-name="Vorname1 Nachname1" cx="125.6" cy="32" r="12" fill="#4363d8" stroke="black" stroke-width="1"><title>Vorname1 Nachname1 — Gruppe 1</title></circle> <circle class="person" data-gid="1" data-gruppe="Gruppe 1" data-name="Vorname1 Nachname1" cx="125.6" cy="32" r="12" fill="#e6194b" stroke="black" stroke-width="1"><title>Vorname1 Nachname1 — Gruppe 1</title></circle>
<circle class="person" data-gid="2" data-gruppe="Gruppe 2" data-name="Vorname4 Nachname4" cx="125.6" cy="58.4" r="12" fill="#f58231" stroke="black" stroke-width="1"><title>Vorname4 Nachname4 — Gruppe 2</title></circle> <circle class="person" data-gid="2" data-gruppe="Gruppe 2" data-name="Vorname4 Nachname4" cx="125.6" cy="58.4" r="12" fill="#3cb44b" stroke="black" stroke-width="1"><title>Vorname4 Nachname4 — Gruppe 2</title></circle>
<ellipse cx="125.6" cy="45.2" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/> <ellipse cx="125.6" cy="45.2" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/>
<text x="125.6" y="40.4" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text> <text x="125.6" y="40.4" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text>
<text x="125.6" y="53.6" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text> <text x="125.6" y="53.6" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text>
<circle cx="45.2" cy="125.6" r="35.2" fill="white" stroke="black" stroke-width="1.5"/> <circle cx="45.2" cy="125.6" r="35.2" fill="white" stroke="black" stroke-width="1.5"/>
<circle class="person" data-gid="2" data-gruppe="Gruppe 2" data-name="Vorname5 Nachname5" cx="45.2" cy="112.4" r="12" fill="#f58231" stroke="black" stroke-width="1"><title>Vorname5 Nachname5 — Gruppe 2</title></circle> <circle class="person" data-gid="2" data-gruppe="Gruppe 2" data-name="Vorname5 Nachname5" cx="45.2" cy="112.4" r="12" fill="#3cb44b" stroke="black" stroke-width="1"><title>Vorname5 Nachname5 — Gruppe 2</title></circle>
<circle class="person" data-gid="2" data-gruppe="Gruppe 2" data-name="Vorname6 Nachname6" cx="45.2" cy="138.8" r="12" fill="#f58231" stroke="black" stroke-width="1"><title>Vorname6 Nachname6 — Gruppe 2</title></circle> <circle class="person" data-gid="2" data-gruppe="Gruppe 2" data-name="Vorname6 Nachname6" cx="45.2" cy="138.8" r="12" fill="#3cb44b" stroke="black" stroke-width="1"><title>Vorname6 Nachname6 — Gruppe 2</title></circle>
<ellipse cx="45.2" cy="125.6" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/> <ellipse cx="45.2" cy="125.6" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/>
<text x="45.2" y="120.8" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text> <text x="45.2" y="120.8" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text>
<text x="45.2" y="134" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text> <text x="45.2" y="134" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text>
<circle cx="45.2" cy="286.4" r="35.2" fill="white" stroke="black" stroke-width="1.5"/> <circle cx="45.2" cy="286.4" r="35.2" fill="white" stroke="black" stroke-width="1.5"/>
<circle class="person" data-gid="3" data-gruppe="Gruppe 3" data-name="Vorname8 Nachname8" cx="45.2" cy="273.2" r="12" fill="#3cb44b" stroke="black" stroke-width="1"><title>Vorname8 Nachname8 — Gruppe 3</title></circle> <circle class="person" data-gid="3" data-gruppe="Gruppe 3" data-name="Vorname8 Nachname8" cx="45.2" cy="273.2" r="12" fill="#4363d8" stroke="black" stroke-width="1"><title>Vorname8 Nachname8 — Gruppe 3</title></circle>
<circle class="person" data-gid="3" data-gruppe="Gruppe 3" data-name="Vorname9 Nachname9" cx="45.2" cy="299.6" r="12" fill="#3cb44b" stroke="black" stroke-width="1"><title>Vorname9 Nachname9 — Gruppe 3</title></circle> <circle class="person" data-gid="3" data-gruppe="Gruppe 3" data-name="Vorname9 Nachname9" cx="45.2" cy="299.6" r="12" fill="#4363d8" stroke="black" stroke-width="1"><title>Vorname9 Nachname9 — Gruppe 3</title></circle>
<ellipse cx="45.2" cy="286.4" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/> <ellipse cx="45.2" cy="286.4" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/>
<text x="45.2" y="281.6" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text> <text x="45.2" y="281.6" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text>
<text x="45.2" y="294.8" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text> <text x="45.2" y="294.8" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

+10 -10
View File
@@ -1,29 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="331.6" height="90.4" viewBox="0 0 331.6 90.4"> <svg xmlns="http://www.w3.org/2000/svg" width="331.6" height="90.4" viewBox="0 0 331.6 90.4">
<rect width="100%" height="100%" fill="white"/> <rect width="100%" height="100%" fill="white"/>
<polyline points="45.2,45.2 125.6,45.2" fill="none" stroke="#e6194b" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="0.5"/> <polyline points="45.2,45.2 125.6,45.2" fill="none" stroke="#3cb44b" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="0.5"/>
<polyline points="206,45.2 286.4,45.2" fill="none" stroke="#3cb44b" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="0.5"/> <polyline points="206,45.2 286.4,45.2" fill="none" stroke="#e6194b" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="0.5"/>
<circle cx="45.2" cy="45.2" r="35.2" fill="white" stroke="black" stroke-width="1.5"/> <circle cx="45.2" cy="45.2" r="35.2" fill="white" stroke="black" stroke-width="1.5"/>
<circle class="person" data-gid="2" data-gruppe="Gruppe 2" data-name="Vorname8 Nachname8" cx="45.2" cy="32" r="12" fill="#e6194b" stroke="black" stroke-width="1"><title>Vorname8 Nachname8 — Gruppe 2</title></circle> <circle class="person" data-gid="2" data-gruppe="Gruppe 2" data-name="Vorname8 Nachname8" cx="45.2" cy="32" r="12" fill="#3cb44b" stroke="black" stroke-width="1"><title>Vorname8 Nachname8 — Gruppe 2</title></circle>
<circle class="person" data-gid="2" data-gruppe="Gruppe 2" data-name="Vorname5 Nachname5" cx="45.2" cy="58.4" r="12" fill="#e6194b" stroke="black" stroke-width="1"><title>Vorname5 Nachname5 — Gruppe 2</title></circle> <circle class="person" data-gid="2" data-gruppe="Gruppe 2" data-name="Vorname5 Nachname5" cx="45.2" cy="58.4" r="12" fill="#3cb44b" stroke="black" stroke-width="1"><title>Vorname5 Nachname5 — Gruppe 2</title></circle>
<ellipse cx="45.2" cy="45.2" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/> <ellipse cx="45.2" cy="45.2" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/>
<text x="45.2" y="40.4" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text> <text x="45.2" y="40.4" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text>
<text x="45.2" y="53.6" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text> <text x="45.2" y="53.6" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text>
<circle cx="125.6" cy="45.2" r="35.2" fill="white" stroke="black" stroke-width="1.5"/> <circle cx="125.6" cy="45.2" r="35.2" fill="white" stroke="black" stroke-width="1.5"/>
<circle class="person" data-gid="2" data-gruppe="Gruppe 2" data-name="Vorname6 Nachname6" cx="125.6" cy="32" r="12" fill="#e6194b" stroke="black" stroke-width="1"><title>Vorname6 Nachname6 — Gruppe 2</title></circle> <circle class="person" data-gid="2" data-gruppe="Gruppe 2" data-name="Vorname6 Nachname6" cx="125.6" cy="32" r="12" fill="#3cb44b" stroke="black" stroke-width="1"><title>Vorname6 Nachname6 — Gruppe 2</title></circle>
<circle class="person" data-gid="2" data-gruppe="Gruppe 2" data-name="Vorname7 Nachname7" cx="125.6" cy="58.4" r="12" fill="#e6194b" stroke="black" stroke-width="1"><title>Vorname7 Nachname7 — Gruppe 2</title></circle> <circle class="person" data-gid="2" data-gruppe="Gruppe 2" data-name="Vorname7 Nachname7" cx="125.6" cy="58.4" r="12" fill="#3cb44b" stroke="black" stroke-width="1"><title>Vorname7 Nachname7 — Gruppe 2</title></circle>
<ellipse cx="125.6" cy="45.2" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/> <ellipse cx="125.6" cy="45.2" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/>
<text x="125.6" y="40.4" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text> <text x="125.6" y="40.4" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text>
<text x="125.6" y="53.6" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text> <text x="125.6" y="53.6" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text>
<circle cx="206" cy="45.2" r="35.2" fill="white" stroke="black" stroke-width="1.5"/> <circle cx="206" cy="45.2" r="35.2" fill="white" stroke="black" stroke-width="1.5"/>
<circle class="person" data-gid="1" data-gruppe="Gruppe 1" data-name="Vorname3 Nachname3" cx="206" cy="32" r="12" fill="#3cb44b" stroke="black" stroke-width="1"><title>Vorname3 Nachname3 — Gruppe 1</title></circle> <circle class="person" data-gid="1" data-gruppe="Gruppe 1" data-name="Vorname3 Nachname3" cx="206" cy="32" r="12" fill="#e6194b" stroke="black" stroke-width="1"><title>Vorname3 Nachname3 — Gruppe 1</title></circle>
<circle class="person" data-gid="1" data-gruppe="Gruppe 1" data-name="Vorname4 Nachname4" cx="206" cy="58.4" r="12" fill="#3cb44b" stroke="black" stroke-width="1"><title>Vorname4 Nachname4 — Gruppe 1</title></circle> <circle class="person" data-gid="1" data-gruppe="Gruppe 1" data-name="Vorname4 Nachname4" cx="206" cy="58.4" r="12" fill="#e6194b" stroke="black" stroke-width="1"><title>Vorname4 Nachname4 — Gruppe 1</title></circle>
<ellipse cx="206" cy="45.2" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/> <ellipse cx="206" cy="45.2" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/>
<text x="206" y="40.4" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text> <text x="206" y="40.4" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text>
<text x="206" y="53.6" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text> <text x="206" y="53.6" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text>
<circle cx="286.4" cy="45.2" r="35.2" fill="white" stroke="black" stroke-width="1.5"/> <circle cx="286.4" cy="45.2" r="35.2" fill="white" stroke="black" stroke-width="1.5"/>
<circle class="person" data-gid="1" data-gruppe="Gruppe 1" data-name="Vorname1 Nachname1" cx="286.4" cy="32" r="12" fill="#3cb44b" stroke="black" stroke-width="1"><title>Vorname1 Nachname1 — Gruppe 1</title></circle> <circle class="person" data-gid="1" data-gruppe="Gruppe 1" data-name="Vorname1 Nachname1" cx="286.4" cy="32" r="12" fill="#e6194b" stroke="black" stroke-width="1"><title>Vorname1 Nachname1 — Gruppe 1</title></circle>
<circle class="person" data-gid="1" data-gruppe="Gruppe 1" data-name="Vorname2 Nachname2" cx="286.4" cy="58.4" r="12" fill="#3cb44b" stroke="black" stroke-width="1"><title>Vorname2 Nachname2 — Gruppe 1</title></circle> <circle class="person" data-gid="1" data-gruppe="Gruppe 1" data-name="Vorname2 Nachname2" cx="286.4" cy="58.4" r="12" fill="#e6194b" stroke="black" stroke-width="1"><title>Vorname2 Nachname2 — Gruppe 1</title></circle>
<ellipse cx="286.4" cy="45.2" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/> <ellipse cx="286.4" cy="45.2" rx="12" ry="15.6" fill="white" fill-opacity="0.75"/>
<text x="286.4" y="40.4" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text> <text x="286.4" y="40.4" text-anchor="middle" dominant-baseline="central" font-size="13.2" font-weight="bold" fill="black">0</text>
<text x="286.4" y="53.6" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text> <text x="286.4" y="53.6" text-anchor="middle" dominant-baseline="central" font-size="7.44" fill="#333">2 Pl.</text>

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

+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
+165
View File
@@ -0,0 +1,165 @@
"""
Farbschema fuer die SVG-Ausgabe (Sitzplatzverteilung.alsSVG).
Laedt ein Farbprofil aus cfg/farben.cfg und ordnet jeder Gruppe eine Farbe zu.
Zwei Modi (siehe cfg/farben.cfg fuer Details):
- 'palette': eine Farbliste, zyklisch je Gruppe (nach Gruppen-Id sortiert)
- 'kategorien': trennt nach Art (Hochzeit) in Familie / Verein / VIP und
faerbt sie ueber je einen Farbverlauf ein.
Waehlbar per --farben <Sektion> (libs/platz.py) bzw. PLATZ_FARBENART (web).
"""
__author__ = "Michael Stangl"
__copyright__ = "Copyright (c) 2026 Michael Stangl"
__license__ = "Python"
import configparser
import os
from collections import Counter, defaultdict
def _hex_zu_rgb(h):
h = h.strip().lstrip('#')
return tuple( int(h[i:i+2], 16) for i in (0, 2, 4) )
def _rgb_zu_hex(rgb):
return '#%02x%02x%02x' % tuple( max(0, min(255, int(round(c)))) for c in rgb )
def _verlauf(start, ende, t):
"""Farbe im linearen RGB-Verlauf start->ende an Position t in [0,1]"""
a, b = _hex_zu_rgb(start), _hex_zu_rgb(ende)
return _rgb_zu_hex( tuple( a[i] + (b[i] - a[i]) * t for i in range(3) ) )
def _verlauf_liste(start, ende, n):
"""n Farben gleichmaessig ueber den Verlauf start->ende (n>=1)"""
if n <= 1:
return [ _verlauf(start, ende, 0.0) ]
return [ _verlauf(start, ende, j / (n - 1)) for j in range(n) ]
class Farbschema:
"""ordnet Gruppen Farben zu, gesteuert durch ein Profil aus farben.cfg"""
# Rueckfall-Palette, falls keine Config / kein gueltiges Profil geladen wurde
Standard = [
'#e6194b', '#3cb44b', '#4363d8', '#f58231', '#911eb4',
'#42d4f4', '#f032e6', '#bfef45', '#fabed4', '#469990',
'#dcbeff', '#9a6324', '#800000', '#aaffc3', '#000075' ]
def __init__(self):
self.Modus = 'palette'
self.Palette = list(self.Standard)
self.FamilienVerlaeufe = [ ('#1f4e9c', '#2fae5f'), ('#f2c200', '#e8590c') ]
self.VereinVerlauf = ('#7b2fbf', '#9aa0a6')
self.VIPVerlauf = ('#e63946', '#8b0000')
def laden(self, FileName, Sektion='grosse_gruppen'):
config = configparser.ConfigParser()
config.read( os.path.expanduser(FileName) )
if not config.has_section( Sektion ):
raise ValueError(
"Farbprofil '%s' nicht in %s gefunden (vorhanden: %s)"
% ( Sektion, FileName, ', '.join(config.sections()) or '-' ) )
self.Modus = config.get( Sektion, 'Modus', fallback='palette' ).strip()
if config.has_option( Sektion, 'Palette' ):
self.Palette = self._farbliste( config.get( Sektion, 'Palette' ) )
if config.has_option( Sektion, 'FamilienVerlaeufe' ):
self.FamilienVerlaeufe = self._verlaeufe(
config.get( Sektion, 'FamilienVerlaeufe' ) )
if config.has_option( Sektion, 'VereinVerlauf' ):
self.VereinVerlauf = self._verlaeufe( config.get( Sektion, 'VereinVerlauf' ) )[0]
if config.has_option( Sektion, 'VIPVerlauf' ):
self.VIPVerlauf = self._verlaeufe( config.get( Sektion, 'VIPVerlauf' ) )[0]
return self
@staticmethod
def _farbliste(text):
return [ f.strip() for f in text.replace('\n', ',').split(',') if f.strip() ]
@staticmethod
def _verlaeufe(text):
"""'A -> B, C -> D' -> [(A,B),(C,D)]; ein einzelner Wert wird zu (v,v)"""
ergebnis = []
for teil in text.replace('\n', ',').split(','):
teil = teil.strip()
if not teil:
continue
if '->' in teil:
a, b = teil.split('->', 1)
ergebnis.append( (a.strip(), b.strip()) )
else:
ergebnis.append( (teil, teil) )
return ergebnis or [ ('#888888', '#888888') ]
def farben(self, Gruppen):
"""ordnet den Gruppen Farben zu.
Rueckgabe: (GidZuFarbe {Gruppen-Id: hexfarbe}, VIPFarbe hexfarbe).
VIPFarbe gilt fuer Personen ohne GA-Gruppe (feste Platzierung)."""
GruppenListe = sorted( Gruppen, key=lambda g: g.Id )
if self.Modus == 'kategorien':
return self._kategorien( GruppenListe )
return self._palette( GruppenListe )
def _palette(self, GruppenListe):
pal = self.Palette or self.Standard
GidZuFarbe = { G.Id: pal[i % len(pal)] for i, G in enumerate(GruppenListe) }
return GidZuFarbe, '#888888'
@staticmethod
def _nachname(G):
"""dominanter Nachname der Gruppe, wenn ihn die Mehrheit traegt (bei
>= 2 Mitgliedern) - sonst None (dann gilt die Gruppe als Verein/
gemischt). Anonyme Sammelbuchungen (alle Mitglieder derselbe Name,
z.B. 13x 'Gast Musikverein') sind keine Familie, sondern gelten als
Verein - daher werden mindestens zwei VERSCHIEDENE Personen verlangt."""
personen = list( G )
if len(personen) < 2:
return None
if len({ (P._Vorname, P._Name) for P in personen }) < 2:
return None # anonymer Block (alle identisch) -> kein Familienname
namen = [ P._Name for P in personen ]
name, anzahl = Counter(namen).most_common(1)[0]
return name if anzahl * 2 > len(namen) else None
def _kategorien(self, GruppenListe):
# 1. klassifizieren: Familien (nach Nachname) vs. Vereine
familien = defaultdict(list) # Nachname -> [Gruppen]
vereine = []
for G in GruppenListe:
nn = self._nachname( G )
if nn is not None:
familien[nn].append( G )
else:
vereine.append( G )
GidZuFarbe = {}
# 2. Familien: jeder Nachname kommt (alphabetisch, abwechselnd) in einen
# Farbbereich - Blau->Gruen ODER Gelb->Orange. Innerhalb eines
# Bereichs werden ALLE zugehoerigen Familiengruppen ueber den Verlauf
# gespreizt, damit verschiedene Familien verschiedene Toene haben;
# Gruppen desselben Nachnamens bleiben benachbart (ein zusammen-
# haengendes Band). So laeuft z.B. eine mehrteilige Familie Brandt
# ueber benachbarte Gelb->Orange-Toene, waehrend Familie Peters einen
# anderen Blau->Gruen-Ton bekommt.
Nachnamen = sorted( familien )
Bereich = { nn: i % len(self.FamilienVerlaeufe)
for i, nn in enumerate(Nachnamen) }
GruppenImBereich = defaultdict(list)
for nn in Nachnamen:
for G in sorted( familien[nn], key=lambda g: g.Id ):
GruppenImBereich[ Bereich[nn] ].append( G )
for b, gruppen in GruppenImBereich.items():
start, ende = self.FamilienVerlaeufe[b]
for farbe, G in zip( _verlauf_liste(start, ende, len(gruppen)), gruppen ):
GidZuFarbe[G.Id] = farbe
# 3. Vereine: ein Verlauf (z.B. Violett -> Grau) ueber alle Vereine
vstart, vende = self.VereinVerlauf
for farbe, G in zip( _verlauf_liste(vstart, vende, len(vereine)),
sorted(vereine, key=lambda g: g.Id) ):
GidZuFarbe[G.Id] = farbe
# 4. VIP-Farbe: Startfarbe des VIP-Verlaufs (i.d.R. Rot)
return GidZuFarbe, self.VIPVerlauf[0]
+21 -13
View File
@@ -608,7 +608,7 @@ class Sitzplatzverteilung:
'#dcbeff', '#9a6324', '#800000', '#aaffc3', '#000075' ] '#dcbeff', '#9a6324', '#800000', '#aaffc3', '#000075' ]
def alsSVG(self, OutputSVG, PersonenRadius=12, Puffer=10, Verbindungen=True, def alsSVG(self, OutputSVG, PersonenRadius=12, Puffer=10, Verbindungen=True,
GruppenNamen=None): GruppenNamen=None, Farbschema=None):
"""Zeichnet die Sitzplatzverteilung als SVG: """Zeichnet die Sitzplatzverteilung als SVG:
Tische werden als weisse Kreise dargestellt, Personen als Tische werden als weisse Kreise dargestellt, Personen als
@@ -632,7 +632,7 @@ class Sitzplatzverteilung:
def GruppenName(G): def GruppenName(G):
if G is None: if G is None:
return 'VIP / feste Platzierung' return 'fixiert'
return GruppenNamen.get( G.Id, 'Gruppe %s' % G.Id ) return GruppenNamen.get( G.Id, 'Gruppe %s' % G.Id )
def PersonName(P): def PersonName(P):
@@ -685,20 +685,29 @@ class Sitzplatzverteilung:
except KeyError: except KeyError:
return None return None
GidZuFarbe = {} # Feste Gruppenfarben: einmal fuer die vollstaendige Gruppenliste (nach
# Gruppen-Id) vergeben - NICHT lazy nach der Sitz-Reihenfolge. Da alle
# Varianten dieselbe Gruppenmenge mit denselben Ids teilen, hat so jede
# Gruppe in jeder Variante (und ueber Laeufe hinweg) dieselbe Farbe.
# Mit Farbschema (aus farben.cfg) kommen die Farben von dort (z.B. nach
# Familie/Verein/VIP getrennt); ohne dient die eingebaute Palette als
# Rueckfall (zyklisch je Gruppe).
if Farbschema is not None:
GidZuFarbe, VIPFarbe = Farbschema.farben( self.Gruppen )
else:
GidZuFarbe = {}
for Index, G in enumerate( sorted( self.Gruppen, key=lambda g: g.Id ) ):
GidZuFarbe[G.Id] = self.SVGGruppenFarben[
Index % len(self.SVGGruppenFarben) ]
VIPFarbe = '#888888'
def FarbeVonGruppe(G): def FarbeVonGruppe(G):
if G is None: if G is None:
return '#888888' return VIPFarbe # VIP / feste Platzierung (ohne GA-Gruppe)
if G.Id not in GidZuFarbe: return GidZuFarbe.get( G.Id, VIPFarbe )
Index = len(GidZuFarbe) % len(self.SVGGruppenFarben)
GidZuFarbe[G.Id] = self.SVGGruppenFarben[Index]
return GidZuFarbe[G.Id]
# Fuer jede Gruppe pro Tisch den Mittelpunkt (Zentroid) ihrer # Fuer jede Gruppe pro Tisch den Mittelpunkt (Zentroid) ihrer
# an diesem Tisch sitzenden Personenkreise sammeln. Die Reihenfolge # an diesem Tisch sitzenden Personenkreise sammeln (fuer die
# der Iteration (Tische, darin Personen) stimmt mit der spaeteren # Verbindungslinien getrennter Gruppen).
# Zeichenschleife ueberein, damit die per FarbeVonGruppe() lazy
# vergebenen Farben konsistent sind.
GidZuTischZentroiden = {} GidZuTischZentroiden = {}
for T in self.Tische: for T in self.Tische:
Mx, My = TischMitte(T) Mx, My = TischMitte(T)
@@ -713,7 +722,6 @@ class Sitzplatzverteilung:
G = GruppeVonPerson(P) G = GruppeVonPerson(P)
if G is None: if G is None:
continue continue
FarbeVonGruppe(G) # Farbe frueh (aber in Zeichenreihenfolge) festlegen
sx, sy, cnt = GidZuSummen.get( G.Id, (0.0, 0.0, 0) ) sx, sy, cnt = GidZuSummen.get( G.Id, (0.0, 0.0, 0) )
GidZuSummen[G.Id] = (sx + Px, sy + Py, cnt + 1) GidZuSummen[G.Id] = (sx + Px, sy + Py, cnt + 1)
for Gid, (sx, sy, cnt) in GidZuSummen.items(): for Gid, (sx, sy, cnt) in GidZuSummen.items():
+13 -1
View File
@@ -11,6 +11,7 @@ __license__ = "Python"
from ga import * from ga import *
from Strukturdaten import * from Strukturdaten import *
from Farben import Farbschema
import optparse import optparse
import random import random
@@ -55,6 +56,10 @@ if __name__ == '__main__':
help="Name des Strafpunkte-Profils (Sektion in strafen.cfg; " help="Name des Strafpunkte-Profils (Sektion in strafen.cfg; "
"Vorgabe: default). Erlaubt mehrere Bewertungs-Vorgehen in einer " "Vorgabe: default). Erlaubt mehrere Bewertungs-Vorgehen in einer "
"Datei, z.B. --strafen locker" ) "Datei, z.B. --strafen locker" )
parser.add_option( "--farben", dest="farben", default="grosse_gruppen",
help="Name des Farbprofils fuer die SVG-Ausgabe (Sektion in "
"farben.cfg; Vorgabe: grosse_gruppen). Fuer Hochzeiten: "
"--farben hochzeit (Familien/Vereine/VIPs getrennt)" )
parser.add_option( "--seed", dest="seed", type="int", default=None, parser.add_option( "--seed", dest="seed", type="int", default=None,
help="Zufalls-Startwert (int) fuer random.seed(), macht den Lauf " help="Zufalls-Startwert (int) fuer random.seed(), macht den Lauf "
"reproduzierbar. Ohne Angabe laeuft der GA nichtdeterministisch. " "reproduzierbar. Ohne Angabe laeuft der GA nichtdeterministisch. "
@@ -88,6 +93,8 @@ if __name__ == '__main__':
Zyklusart = options.zyklusart Zyklusart = options.zyklusart
Strafenconfig = os.path.join( CfgDir, 'strafen.cfg' ) Strafenconfig = os.path.join( CfgDir, 'strafen.cfg' )
Strafenart = options.strafen Strafenart = options.strafen
Farbenconfig = os.path.join( CfgDir, 'farben.cfg' )
Farbenart = options.farben
# lade Eingabedateien # lade Eingabedateien
Tischconfig = os.path.join( InDir, 'tische.ini' ) Tischconfig = os.path.join( InDir, 'tische.ini' )
@@ -109,6 +116,11 @@ if __name__ == '__main__':
except ValueError as Fehler: except ValueError as Fehler:
parser.error( str(Fehler) ) parser.error( str(Fehler) )
print(SL) print(SL)
FS = Farbschema()
try:
FS.laden( Farbenconfig, Farbenart )
except ValueError as Fehler:
parser.error( str(Fehler) )
TE = Tische() TE = Tische()
TE.laden( Tischconfig ) TE.laden( Tischconfig )
print(TE) print(TE)
@@ -128,7 +140,7 @@ if __name__ == '__main__':
S.speichern( AusgabeTP, AusgabePT ) S.speichern( AusgabeTP, AusgabePT )
if options.tosvg: if options.tosvg:
S.alsSVG( AusgabeSVG, Verbindungen=options.showconn, S.alsSVG( AusgabeSVG, Verbindungen=options.showconn,
GruppenNamen=GruppenNamen ) GruppenNamen=GruppenNamen, Farbschema=FS )
if options.showdev: if options.showdev:
F1.Entwicklung.alsSVG( AusgabeEntwicklung ) F1.Entwicklung.alsSVG( AusgabeEntwicklung )
print( "Entwicklungsverlauf geschrieben:", AusgabeEntwicklung ) print( "Entwicklungsverlauf geschrieben:", AusgabeEntwicklung )
+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;
+57 -57
View File
@@ -1,72 +1,72 @@
Aksoy, Rainer, = Hof 1, Tisch 14, Nummer 3 Aksoy, Rainer, = Hof 1, Tisch 1, Nummer 13
Aksoy, Renate, = Hof 1, Tisch 14, Nummer 4 Aksoy, Renate, = Hof 1, Tisch 1, Nummer 10
Aydin, Horst, = Hof 1, Tisch 6, Nummer 2 Aydin, Horst, = Hof 1, Tisch 15, Nummer 4
Aydin, Ursula, = Hof 1, Tisch 6, Nummer 3 Aydin, Ursula, = Hof 1, Tisch 15, Nummer 5
Barth, Hannelore, = Hof 1, Tisch 8, Nummer 3 Barth, Hannelore, = Hof 1, Tisch 9, Nummer 3
Berger, Sergej, = Hof 1, Tisch 1, Nummer 11 Berger, Sergej, = Hof 1, Tisch 8, Nummer 3
Brandt, Andreas, = Hof 1, Tisch 15, Nummer 2 Brandt, Andreas, = Hof 1, Tisch 14, Nummer 2
Brandt, Anja, = Hof 1, Tisch 15, Nummer 6 Brandt, Anja, = Hof 1, Tisch 14, Nummer 6
Brandt, Anton, = Hof 1, Tisch 16, Nummer 9 Brandt, Anton, = Hof 1, Tisch 16, Nummer 9
Brandt, Ben, = Hof 1, Tisch 17, Nummer 2 Brandt, Ben, = Hof 1, Tisch 17, Nummer 2
Brandt, Daniel, = Hof 1, Tisch 12, Nummer 8 Brandt, Daniel, = Hof 1, Tisch 11, Nummer 8
Brandt, Felix, = Hof 1, Tisch 16, Nummer 7 Brandt, Felix, = Hof 1, Tisch 16, Nummer 7
Brandt, Frieda, = Hof 1, Tisch 16, Nummer 10 Brandt, Frieda, = Hof 1, Tisch 16, Nummer 10
Brandt, Greta, = Hof 1, Tisch 16, Nummer 12 Brandt, Greta, = Hof 1, Tisch 16, Nummer 12
Brandt, Helga, = Hof 1, Tisch 1, Nummer 5 Brandt, Helga, = Hof 1, Tisch 1, Nummer 5
Brandt, Julia, = Hof 1, Tisch 1, Nummer 2 Brandt, Julia, = Hof 1, Tisch 1, Nummer 2
Brandt, Kathrin, = Hof 1, Tisch 12, Nummer 9 Brandt, Kathrin, = Hof 1, Tisch 11, Nummer 9
Brandt, Lena, = Hof 1, Tisch 15, Nummer 3 Brandt, Lena, = Hof 1, Tisch 14, Nummer 3
Brandt, Milan, = Hof 1, Tisch 15, Nummer 5 Brandt, Milan, = Hof 1, Tisch 14, Nummer 5
Brandt, Paul, = Hof 1, Tisch 17, Nummer 3 Brandt, Paul, = Hof 1, Tisch 17, Nummer 3
Brandt, Sofia, = Hof 1, Tisch 15, Nummer 7 Brandt, Sofia, = Hof 1, Tisch 14, Nummer 7
Brandt, Theo, = Hof 1, Tisch 15, Nummer 4 Brandt, Theo, = Hof 1, Tisch 14, Nummer 4
Brandt, Werner, = Hof 1, Tisch 1, Nummer 4 Brandt, Werner, = Hof 1, Tisch 1, Nummer 4
Busch, Stefanie, = Hof 1, Tisch 3, Nummer 7 Busch, Stefanie, = Hof 1, Tisch 8, Nummer 7
Demir, Elif, = Hof 1, Tisch 10, Nummer 2 Demir, Elif, = Hof 1, Tisch 10, Nummer 2
Demir, Hannelore, = Hof 1, Tisch 3, Nummer 4 Demir, Hannelore, = Hof 1, Tisch 13, Nummer 4
Demir, Ingrid, = Hof 1, Tisch 3, Nummer 2 Demir, Ingrid, = Hof 1, Tisch 13, Nummer 2
Demir, Monika, = Hof 1, Tisch 3, Nummer 3 Demir, Monika, = Hof 1, Tisch 13, Nummer 3
Dietrich, Jennifer, = Hof 1, Tisch 13, Nummer 2 Dietrich, Jennifer, = Hof 1, Tisch 2, Nummer 5
Dietrich, Nicole, = Hof 1, Tisch 12, Nummer 7 Dietrich, Nicole, = Hof 1, Tisch 12, Nummer 7
Erdem, Erika, = Hof 1, Tisch 2, Nummer 7 Erdem, Erika, = Hof 1, Tisch 15, Nummer 7
Erdem, Gerhard, = Hof 1, Tisch 8, Nummer 5 Erdem, Gerhard, = Hof 1, Tisch 9, Nummer 5
Erdem, Rainer, = Hof 1, Tisch 8, Nummer 6 Erdem, Rainer, = Hof 1, Tisch 9, Nummer 6
Esposito, Christian, = Hof 1, Tisch 12, Nummer 4 Esposito, Christian, = Hof 1, Tisch 12, Nummer 4
Esposito, Gisela, = Hof 1, Tisch 8, Nummer 7 Esposito, Gisela, = Hof 1, Tisch 9, Nummer 7
Esposito, Julia, = Hof 1, Tisch 3, Nummer 9 Esposito, Julia, = Hof 1, Tisch 2, Nummer 4
Fischer, Heinz, = Hof 1, Tisch 2, Nummer 4 Fischer, Heinz, = Hof 1, Tisch 3, Nummer 3
Fischer, Helga, = Hof 1, Tisch 2, Nummer 5 Fischer, Helga, = Hof 1, Tisch 3, Nummer 2
Fischer, Nicole, = Hof 1, Tisch 12, Nummer 2 Fischer, Nicole, = Hof 1, Tisch 12, Nummer 2
Fischer, Sven, = Hof 1, Tisch 4, Nummer 9 Fischer, Sven, = Hof 1, Tisch 4, Nummer 9
Frank, Erika, = Hof 1, Tisch 8, Nummer 4 Frank, Erika, = Hof 1, Tisch 9, Nummer 4
Frank, Nadine, = Hof 1, Tisch 11, Nummer 2 Frank, Nadine, = Hof 1, Tisch 11, Nummer 2
Frank, Timo, = Hof 1, Tisch 10, Nummer 6 Frank, Timo, = Hof 1, Tisch 10, Nummer 6
Frank, Tobias, = Hof 1, Tisch 1, Nummer 9 Frank, Tobias, = Hof 1, Tisch 1, Nummer 9
Franke, Brigitte, = Hof 1, Tisch 8, Nummer 8 Franke, Brigitte, = Hof 1, Tisch 9, Nummer 8
Hansen, Olga, = Hof 1, Tisch 11, Nummer 5 Hansen, Olga, = Hof 1, Tisch 11, Nummer 5
Hansen, Wolfgang, = Hof 1, Tisch 8, Nummer 2 Hansen, Wolfgang, = Hof 1, Tisch 9, Nummer 2
Hoffmann, Lena, = Hof 1, Tisch 12, Nummer 5 Hoffmann, Lena, = Hof 1, Tisch 12, Nummer 5
Horn, Daniel, = Hof 1, Tisch 11, Nummer 6 Horn, Daniel, = Hof 1, Tisch 11, Nummer 6
Horn, Svenja, = Hof 1, Tisch 13, Nummer 4 Horn, Svenja, = Hof 1, Tisch 2, Nummer 7
Klein, Stefan, = Hof 1, Tisch 10, Nummer 5 Klein, Stefan, = Hof 1, Tisch 10, Nummer 5
Koch, Andreas, = Hof 1, Tisch 3, Nummer 6 Koch, Andreas, = Hof 1, Tisch 8, Nummer 8
Koch, Giovanna, = Hof 1, Tisch 3, Nummer 8 Koch, Giovanna, = Hof 1, Tisch 2, Nummer 3
Kovac, Giovanna, = Hof 1, Tisch 1, Nummer 10 Kovac, Giovanna, = Hof 1, Tisch 8, Nummer 4
Kraus, Dennis, = Hof 1, Tisch 5, Nummer 8 Kraus, Dennis, = Hof 1, Tisch 12, Nummer 8
Kraus, Matteo, = Hof 1, Tisch 16, Nummer 8 Kraus, Matteo, = Hof 1, Tisch 16, Nummer 8
Kraus, Melanie, = Hof 1, Tisch 5, Nummer 9 Kraus, Melanie, = Hof 1, Tisch 12, Nummer 9
Kuhn, Leni, = Hof 1, Tisch 16, Nummer 2 Kuhn, Leni, = Hof 1, Tisch 16, Nummer 2
Kuhn, Sarah, = Hof 1, Tisch 15, Nummer 9 Kuhn, Sarah, = Hof 1, Tisch 14, Nummer 9
Kuhn, Stefan, = Hof 1, Tisch 13, Nummer 6 Kuhn, Stefan, = Hof 1, Tisch 2, Nummer 9
Kuhn, Timo, = Hof 1, Tisch 15, Nummer 8 Kuhn, Timo, = Hof 1, Tisch 14, Nummer 8
Lange, Milan, = Hof 1, Tisch 11, Nummer 7 Lange, Milan, = Hof 1, Tisch 11, Nummer 7
Meyer, Anton, = Hof 1, Tisch 17, Nummer 4 Meyer, Anton, = Hof 1, Tisch 17, Nummer 4
Meyer, Christian, = Hof 1, Tisch 13, Nummer 3 Meyer, Christian, = Hof 1, Tisch 2, Nummer 6
Meyer, Ida, = Hof 1, Tisch 17, Nummer 5 Meyer, Ida, = Hof 1, Tisch 17, Nummer 5
Meyer, Marco, = Hof 1, Tisch 14, Nummer 6 Meyer, Marco, = Hof 1, Tisch 5, Nummer 8
Meyer, Mila, = Hof 1, Tisch 16, Nummer 11 Meyer, Mila, = Hof 1, Tisch 16, Nummer 11
Meyer, Sandra, = Hof 1, Tisch 14, Nummer 7 Meyer, Sandra, = Hof 1, Tisch 5, Nummer 9
Meyer, Wolfgang, = Hof 1, Tisch 8, Nummer 9 Meyer, Wolfgang, = Hof 1, Tisch 9, Nummer 9
Musikverein, Gast, = Hof 1, Tisch 4, Nummer 2 Musikverein, Gast, = Hof 1, Tisch 4, Nummer 2
Musikverein, Gast, = Hof 1, Tisch 4, Nummer 3 Musikverein, Gast, = Hof 1, Tisch 4, Nummer 3
Musikverein, Gast, = Hof 1, Tisch 4, Nummer 4 Musikverein, Gast, = Hof 1, Tisch 4, Nummer 4
@@ -80,40 +80,40 @@ Musikverein, Gast, = Hof 1, Tisch 5, Nummer 4
Musikverein, Gast, = Hof 1, Tisch 5, Nummer 5 Musikverein, Gast, = Hof 1, Tisch 5, Nummer 5
Musikverein, Gast, = Hof 1, Tisch 5, Nummer 6 Musikverein, Gast, = Hof 1, Tisch 5, Nummer 6
Musikverein, Gast, = Hof 1, Tisch 5, Nummer 7 Musikverein, Gast, = Hof 1, Tisch 5, Nummer 7
Peters, Melanie, = Hof 1, Tisch 11, Nummer 9 Peters, Melanie, = Hof 1, Tisch 8, Nummer 9
Peters, Yusuf, = Hof 1, Tisch 11, Nummer 8 Peters, Yusuf, = Hof 1, Tisch 8, Nummer 2
Polat, Matthias, = Hof 1, Tisch 12, Nummer 3 Polat, Matthias, = Hof 1, Tisch 12, Nummer 3
Roth, Stefanie, = Hof 1, Tisch 13, Nummer 5 Roth, Stefanie, = Hof 1, Tisch 2, Nummer 8
Schmidt, Dennis, = Hof 1, Tisch 1, Nummer 12 Schmidt, Dennis, = Hof 1, Tisch 8, Nummer 5
Schmitt, Markus, = Hof 1, Tisch 11, Nummer 3 Schmitt, Markus, = Hof 1, Tisch 11, Nummer 3
Schwarz, Elias, = Hof 1, Tisch 16, Nummer 6 Schwarz, Elias, = Hof 1, Tisch 16, Nummer 6
Schwarz, Guenter, = Hof 1, Tisch 14, Nummer 2 Schwarz, Guenter, = Hof 1, Tisch 1, Nummer 12
Schwarz, Liam, = Hof 1, Tisch 16, Nummer 5 Schwarz, Liam, = Hof 1, Tisch 16, Nummer 5
Schwarz, Sarah, = Hof 1, Tisch 2, Nummer 3 Schwarz, Sarah, = Hof 1, Tisch 15, Nummer 3
Schwarz, Yusuf, = Hof 1, Tisch 2, Nummer 2 Schwarz, Yusuf, = Hof 1, Tisch 15, Nummer 2
Simon, Jennifer, = Hof 1, Tisch 10, Nummer 3 Simon, Jennifer, = Hof 1, Tisch 10, Nummer 3
Simon, Patrick, = Hof 1, Tisch 1, Nummer 13 Simon, Patrick, = Hof 1, Tisch 8, Nummer 6
Simon, Stefanie, = Hof 1, Tisch 10, Nummer 8 Simon, Stefanie, = Hof 1, Tisch 10, Nummer 8
Sommer, Daniel, = Hof 1, Tisch 1, Nummer 3 Sommer, Daniel, = Hof 1, Tisch 1, Nummer 3
Sommer, Dennis, = Hof 1, Tisch 13, Nummer 7 Sommer, Dennis, = Hof 1, Tisch 13, Nummer 7
Sommer, Dennis, = Hof 1, Tisch 2, Nummer 8 Sommer, Dennis, = Hof 1, Tisch 15, Nummer 8
Sommer, Dieter, = Hof 1, Tisch 1, Nummer 6 Sommer, Dieter, = Hof 1, Tisch 1, Nummer 6
Sommer, Emil, = Hof 1, Tisch 16, Nummer 3 Sommer, Emil, = Hof 1, Tisch 16, Nummer 3
Sommer, Ida, = Hof 1, Tisch 16, Nummer 4 Sommer, Ida, = Hof 1, Tisch 16, Nummer 4
Sommer, Jennifer, = Hof 1, Tisch 2, Nummer 9 Sommer, Jennifer, = Hof 1, Tisch 15, Nummer 9
Sommer, Lina, = Hof 1, Tisch 13, Nummer 9 Sommer, Lina, = Hof 1, Tisch 13, Nummer 9
Sommer, Max, = Hof 1, Tisch 16, Nummer 14 Sommer, Max, = Hof 1, Tisch 16, Nummer 14
Sommer, Nicole, = Hof 1, Tisch 13, Nummer 8 Sommer, Nicole, = Hof 1, Tisch 13, Nummer 8
Sommer, Renate, = Hof 1, Tisch 1, Nummer 7 Sommer, Renate, = Hof 1, Tisch 1, Nummer 7
Sommer, Theo, = Hof 1, Tisch 16, Nummer 13 Sommer, Theo, = Hof 1, Tisch 16, Nummer 13
Stein, Andreas, = Hof 1, Tisch 10, Nummer 4 Stein, Andreas, = Hof 1, Tisch 10, Nummer 4
Stein, Werner, = Hof 1, Tisch 14, Nummer 5 Stein, Werner, = Hof 1, Tisch 1, Nummer 11
Tran, Heinz, = Hof 1, Tisch 2, Nummer 6 Tran, Heinz, = Hof 1, Tisch 15, Nummer 6
Vogel, Anton, = Hof 1, Tisch 17, Nummer 6 Vogel, Anton, = Hof 1, Tisch 17, Nummer 6
Vogel, Max, = Hof 1, Tisch 17, Nummer 7 Vogel, Max, = Hof 1, Tisch 17, Nummer 7
Vogel, Sandra, = Hof 1, Tisch 14, Nummer 9 Vogel, Sandra, = Hof 1, Tisch 13, Nummer 6
Vogel, Svenja, = Hof 1, Tisch 3, Nummer 5 Vogel, Svenja, = Hof 1, Tisch 2, Nummer 2
Vogel, Yusuf, = Hof 1, Tisch 14, Nummer 8 Vogel, Yusuf, = Hof 1, Tisch 13, Nummer 5
Wagner, Daniel, = Hof 1, Tisch 10, Nummer 7 Wagner, Daniel, = Hof 1, Tisch 10, Nummer 7
Wagner, Timo, = Hof 1, Tisch 12, Nummer 6 Wagner, Timo, = Hof 1, Tisch 12, Nummer 6
Winkler, Sarah, = Hof 1, Tisch 1, Nummer 8 Winkler, Sarah, = Hof 1, Tisch 1, Nummer 8
1 Aksoy Rainer = Hof 1 Tisch 14 Tisch 1 Nummer 3 Nummer 13
2 Aksoy Renate = Hof 1 Tisch 14 Tisch 1 Nummer 4 Nummer 10
3 Aydin Horst = Hof 1 Tisch 6 Tisch 15 Nummer 2 Nummer 4
4 Aydin Ursula = Hof 1 Tisch 6 Tisch 15 Nummer 3 Nummer 5
5 Barth Hannelore = Hof 1 Tisch 8 Tisch 9 Nummer 3 Nummer 3
6 Berger Sergej = Hof 1 Tisch 1 Tisch 8 Nummer 11 Nummer 3
7 Brandt Andreas = Hof 1 Tisch 15 Tisch 14 Nummer 2 Nummer 2
8 Brandt Anja = Hof 1 Tisch 15 Tisch 14 Nummer 6 Nummer 6
9 Brandt Anton = Hof 1 Tisch 16 Tisch 16 Nummer 9 Nummer 9
10 Brandt Ben = Hof 1 Tisch 17 Tisch 17 Nummer 2 Nummer 2
11 Brandt Daniel = Hof 1 Tisch 12 Tisch 11 Nummer 8 Nummer 8
12 Brandt Felix = Hof 1 Tisch 16 Tisch 16 Nummer 7 Nummer 7
13 Brandt Frieda = Hof 1 Tisch 16 Tisch 16 Nummer 10 Nummer 10
14 Brandt Greta = Hof 1 Tisch 16 Tisch 16 Nummer 12 Nummer 12
15 Brandt Helga = Hof 1 Tisch 1 Tisch 1 Nummer 5 Nummer 5
16 Brandt Julia = Hof 1 Tisch 1 Tisch 1 Nummer 2 Nummer 2
17 Brandt Kathrin = Hof 1 Tisch 12 Tisch 11 Nummer 9 Nummer 9
18 Brandt Lena = Hof 1 Tisch 15 Tisch 14 Nummer 3 Nummer 3
19 Brandt Milan = Hof 1 Tisch 15 Tisch 14 Nummer 5 Nummer 5
20 Brandt Paul = Hof 1 Tisch 17 Tisch 17 Nummer 3 Nummer 3
21 Brandt Sofia = Hof 1 Tisch 15 Tisch 14 Nummer 7 Nummer 7
22 Brandt Theo = Hof 1 Tisch 15 Tisch 14 Nummer 4 Nummer 4
23 Brandt Werner = Hof 1 Tisch 1 Tisch 1 Nummer 4 Nummer 4
24 Busch Stefanie = Hof 1 Tisch 3 Tisch 8 Nummer 7 Nummer 7
25 Demir Elif = Hof 1 Tisch 10 Tisch 10 Nummer 2 Nummer 2
26 Demir Hannelore = Hof 1 Tisch 3 Tisch 13 Nummer 4 Nummer 4
27 Demir Ingrid = Hof 1 Tisch 3 Tisch 13 Nummer 2 Nummer 2
28 Demir Monika = Hof 1 Tisch 3 Tisch 13 Nummer 3 Nummer 3
29 Dietrich Jennifer = Hof 1 Tisch 13 Tisch 2 Nummer 2 Nummer 5
30 Dietrich Nicole = Hof 1 Tisch 12 Tisch 12 Nummer 7 Nummer 7
31 Erdem Erika = Hof 1 Tisch 2 Tisch 15 Nummer 7 Nummer 7
32 Erdem Gerhard = Hof 1 Tisch 8 Tisch 9 Nummer 5 Nummer 5
33 Erdem Rainer = Hof 1 Tisch 8 Tisch 9 Nummer 6 Nummer 6
34 Esposito Christian = Hof 1 Tisch 12 Tisch 12 Nummer 4 Nummer 4
35 Esposito Gisela = Hof 1 Tisch 8 Tisch 9 Nummer 7 Nummer 7
36 Esposito Julia = Hof 1 Tisch 3 Tisch 2 Nummer 9 Nummer 4
37 Fischer Heinz = Hof 1 Tisch 2 Tisch 3 Nummer 4 Nummer 3
38 Fischer Helga = Hof 1 Tisch 2 Tisch 3 Nummer 5 Nummer 2
39 Fischer Nicole = Hof 1 Tisch 12 Tisch 12 Nummer 2 Nummer 2
40 Fischer Sven = Hof 1 Tisch 4 Tisch 4 Nummer 9 Nummer 9
41 Frank Erika = Hof 1 Tisch 8 Tisch 9 Nummer 4 Nummer 4
42 Frank Nadine = Hof 1 Tisch 11 Tisch 11 Nummer 2 Nummer 2
43 Frank Timo = Hof 1 Tisch 10 Tisch 10 Nummer 6 Nummer 6
44 Frank Tobias = Hof 1 Tisch 1 Tisch 1 Nummer 9 Nummer 9
45 Franke Brigitte = Hof 1 Tisch 8 Tisch 9 Nummer 8 Nummer 8
46 Hansen Olga = Hof 1 Tisch 11 Tisch 11 Nummer 5 Nummer 5
47 Hansen Wolfgang = Hof 1 Tisch 8 Tisch 9 Nummer 2 Nummer 2
48 Hoffmann Lena = Hof 1 Tisch 12 Tisch 12 Nummer 5 Nummer 5
49 Horn Daniel = Hof 1 Tisch 11 Tisch 11 Nummer 6 Nummer 6
50 Horn Svenja = Hof 1 Tisch 13 Tisch 2 Nummer 4 Nummer 7
51 Klein Stefan = Hof 1 Tisch 10 Tisch 10 Nummer 5 Nummer 5
52 Koch Andreas = Hof 1 Tisch 3 Tisch 8 Nummer 6 Nummer 8
53 Koch Giovanna = Hof 1 Tisch 3 Tisch 2 Nummer 8 Nummer 3
54 Kovac Giovanna = Hof 1 Tisch 1 Tisch 8 Nummer 10 Nummer 4
55 Kraus Dennis = Hof 1 Tisch 5 Tisch 12 Nummer 8 Nummer 8
56 Kraus Matteo = Hof 1 Tisch 16 Tisch 16 Nummer 8 Nummer 8
57 Kraus Melanie = Hof 1 Tisch 5 Tisch 12 Nummer 9 Nummer 9
58 Kuhn Leni = Hof 1 Tisch 16 Tisch 16 Nummer 2 Nummer 2
59 Kuhn Sarah = Hof 1 Tisch 15 Tisch 14 Nummer 9 Nummer 9
60 Kuhn Stefan = Hof 1 Tisch 13 Tisch 2 Nummer 6 Nummer 9
61 Kuhn Timo = Hof 1 Tisch 15 Tisch 14 Nummer 8 Nummer 8
62 Lange Milan = Hof 1 Tisch 11 Tisch 11 Nummer 7 Nummer 7
63 Meyer Anton = Hof 1 Tisch 17 Tisch 17 Nummer 4 Nummer 4
64 Meyer Christian = Hof 1 Tisch 13 Tisch 2 Nummer 3 Nummer 6
65 Meyer Ida = Hof 1 Tisch 17 Tisch 17 Nummer 5 Nummer 5
66 Meyer Marco = Hof 1 Tisch 14 Tisch 5 Nummer 6 Nummer 8
67 Meyer Mila = Hof 1 Tisch 16 Tisch 16 Nummer 11 Nummer 11
68 Meyer Sandra = Hof 1 Tisch 14 Tisch 5 Nummer 7 Nummer 9
69 Meyer Wolfgang = Hof 1 Tisch 8 Tisch 9 Nummer 9 Nummer 9
70 Musikverein Gast = Hof 1 Tisch 4 Tisch 4 Nummer 2 Nummer 2
71 Musikverein Gast = Hof 1 Tisch 4 Tisch 4 Nummer 3 Nummer 3
72 Musikverein Gast = Hof 1 Tisch 4 Tisch 4 Nummer 4 Nummer 4
80 Musikverein Gast = Hof 1 Tisch 5 Tisch 5 Nummer 5 Nummer 5
81 Musikverein Gast = Hof 1 Tisch 5 Tisch 5 Nummer 6 Nummer 6
82 Musikverein Gast = Hof 1 Tisch 5 Tisch 5 Nummer 7 Nummer 7
83 Peters Melanie = Hof 1 Tisch 11 Tisch 8 Nummer 9 Nummer 9
84 Peters Yusuf = Hof 1 Tisch 11 Tisch 8 Nummer 8 Nummer 2
85 Polat Matthias = Hof 1 Tisch 12 Tisch 12 Nummer 3 Nummer 3
86 Roth Stefanie = Hof 1 Tisch 13 Tisch 2 Nummer 5 Nummer 8
87 Schmidt Dennis = Hof 1 Tisch 1 Tisch 8 Nummer 12 Nummer 5
88 Schmitt Markus = Hof 1 Tisch 11 Tisch 11 Nummer 3 Nummer 3
89 Schwarz Elias = Hof 1 Tisch 16 Tisch 16 Nummer 6 Nummer 6
90 Schwarz Guenter = Hof 1 Tisch 14 Tisch 1 Nummer 2 Nummer 12
91 Schwarz Liam = Hof 1 Tisch 16 Tisch 16 Nummer 5 Nummer 5
92 Schwarz Sarah = Hof 1 Tisch 2 Tisch 15 Nummer 3 Nummer 3
93 Schwarz Yusuf = Hof 1 Tisch 2 Tisch 15 Nummer 2 Nummer 2
94 Simon Jennifer = Hof 1 Tisch 10 Tisch 10 Nummer 3 Nummer 3
95 Simon Patrick = Hof 1 Tisch 1 Tisch 8 Nummer 13 Nummer 6
96 Simon Stefanie = Hof 1 Tisch 10 Tisch 10 Nummer 8 Nummer 8
97 Sommer Daniel = Hof 1 Tisch 1 Tisch 1 Nummer 3 Nummer 3
98 Sommer Dennis = Hof 1 Tisch 13 Tisch 13 Nummer 7 Nummer 7
99 Sommer Dennis = Hof 1 Tisch 2 Tisch 15 Nummer 8 Nummer 8
100 Sommer Dieter = Hof 1 Tisch 1 Tisch 1 Nummer 6 Nummer 6
101 Sommer Emil = Hof 1 Tisch 16 Tisch 16 Nummer 3 Nummer 3
102 Sommer Ida = Hof 1 Tisch 16 Tisch 16 Nummer 4 Nummer 4
103 Sommer Jennifer = Hof 1 Tisch 2 Tisch 15 Nummer 9 Nummer 9
104 Sommer Lina = Hof 1 Tisch 13 Tisch 13 Nummer 9 Nummer 9
105 Sommer Max = Hof 1 Tisch 16 Tisch 16 Nummer 14 Nummer 14
106 Sommer Nicole = Hof 1 Tisch 13 Tisch 13 Nummer 8 Nummer 8
107 Sommer Renate = Hof 1 Tisch 1 Tisch 1 Nummer 7 Nummer 7
108 Sommer Theo = Hof 1 Tisch 16 Tisch 16 Nummer 13 Nummer 13
109 Stein Andreas = Hof 1 Tisch 10 Tisch 10 Nummer 4 Nummer 4
110 Stein Werner = Hof 1 Tisch 14 Tisch 1 Nummer 5 Nummer 11
111 Tran Heinz = Hof 1 Tisch 2 Tisch 15 Nummer 6 Nummer 6
112 Vogel Anton = Hof 1 Tisch 17 Tisch 17 Nummer 6 Nummer 6
113 Vogel Max = Hof 1 Tisch 17 Tisch 17 Nummer 7 Nummer 7
114 Vogel Sandra = Hof 1 Tisch 14 Tisch 13 Nummer 9 Nummer 6
115 Vogel Svenja = Hof 1 Tisch 3 Tisch 2 Nummer 5 Nummer 2
116 Vogel Yusuf = Hof 1 Tisch 14 Tisch 13 Nummer 8 Nummer 5
117 Wagner Daniel = Hof 1 Tisch 10 Tisch 10 Nummer 7 Nummer 7
118 Wagner Timo = Hof 1 Tisch 12 Tisch 12 Nummer 6 Nummer 6
119 Winkler Sarah = Hof 1 Tisch 1 Tisch 1 Nummer 8 Nummer 8
+57 -57
View File
@@ -7,30 +7,24 @@
Renate Sommer = Hof 1, Tisch 1, Nummer 6 Renate Sommer = Hof 1, Tisch 1, Nummer 6
Sarah Winkler = Hof 1, Tisch 1, Nummer 7 Sarah Winkler = Hof 1, Tisch 1, Nummer 7
Tobias Frank = Hof 1, Tisch 1, Nummer 8 Tobias Frank = Hof 1, Tisch 1, Nummer 8
Giovanna Kovac = Hof 1, Tisch 1, Nummer 9 Renate Aksoy = Hof 1, Tisch 1, Nummer 9
Sergej Berger = Hof 1, Tisch 1, Nummer 10 Werner Stein = Hof 1, Tisch 1, Nummer 10
Dennis Schmidt = Hof 1, Tisch 1, Nummer 11 Guenter Schwarz = Hof 1, Tisch 1, Nummer 11
Patrick Simon = Hof 1, Tisch 1, Nummer 12 Rainer Aksoy = Hof 1, Tisch 1, Nummer 12
[2] [2]
Yusuf Schwarz = Hof 1, Tisch 2, Nummer 1 Svenja Vogel = Hof 1, Tisch 2, Nummer 1
Sarah Schwarz = Hof 1, Tisch 2, Nummer 2 Giovanna Koch = Hof 1, Tisch 2, Nummer 2
Heinz Fischer = Hof 1, Tisch 2, Nummer 3 Julia Esposito = Hof 1, Tisch 2, Nummer 3
Helga Fischer = Hof 1, Tisch 2, Nummer 4 Jennifer Dietrich = Hof 1, Tisch 2, Nummer 4
Heinz Tran = Hof 1, Tisch 2, Nummer 5 Christian Meyer = Hof 1, Tisch 2, Nummer 5
Erika Erdem = Hof 1, Tisch 2, Nummer 6 Svenja Horn = Hof 1, Tisch 2, Nummer 6
Dennis Sommer = Hof 1, Tisch 2, Nummer 7 Stefanie Roth = Hof 1, Tisch 2, Nummer 7
Jennifer Sommer = Hof 1, Tisch 2, Nummer 8 Stefan Kuhn = Hof 1, Tisch 2, Nummer 8
[3] [3]
Ingrid Demir = Hof 1, Tisch 3, Nummer 1 Helga Fischer = Hof 1, Tisch 3, Nummer 1
Monika Demir = Hof 1, Tisch 3, Nummer 2 Heinz Fischer = Hof 1, Tisch 3, Nummer 2
Hannelore Demir = Hof 1, Tisch 3, Nummer 3
Svenja Vogel = Hof 1, Tisch 3, Nummer 4
Andreas Koch = Hof 1, Tisch 3, Nummer 5
Stefanie Busch = Hof 1, Tisch 3, Nummer 6
Giovanna Koch = Hof 1, Tisch 3, Nummer 7
Julia Esposito = Hof 1, Tisch 3, Nummer 8
[4] [4]
Gast Musikverein = Hof 1, Tisch 4, Nummer 1 Gast Musikverein = Hof 1, Tisch 4, Nummer 1
@@ -49,26 +43,32 @@
Gast Musikverein = Hof 1, Tisch 5, Nummer 4 Gast Musikverein = Hof 1, Tisch 5, Nummer 4
Gast Musikverein = Hof 1, Tisch 5, Nummer 5 Gast Musikverein = Hof 1, Tisch 5, Nummer 5
Gast Musikverein = Hof 1, Tisch 5, Nummer 6 Gast Musikverein = Hof 1, Tisch 5, Nummer 6
Dennis Kraus = Hof 1, Tisch 5, Nummer 7 Marco Meyer = Hof 1, Tisch 5, Nummer 7
Melanie Kraus = Hof 1, Tisch 5, Nummer 8 Sandra Meyer = Hof 1, Tisch 5, Nummer 8
[6] [6]
Horst Aydin = Hof 1, Tisch 6, Nummer 1
Ursula Aydin = Hof 1, Tisch 6, Nummer 2
[7] [7]
[8] [8]
Wolfgang Hansen = Hof 1, Tisch 8, Nummer 1 Yusuf Peters = Hof 1, Tisch 8, Nummer 1
Hannelore Barth = Hof 1, Tisch 8, Nummer 2 Sergej Berger = Hof 1, Tisch 8, Nummer 2
Erika Frank = Hof 1, Tisch 8, Nummer 3 Giovanna Kovac = Hof 1, Tisch 8, Nummer 3
Gerhard Erdem = Hof 1, Tisch 8, Nummer 4 Dennis Schmidt = Hof 1, Tisch 8, Nummer 4
Rainer Erdem = Hof 1, Tisch 8, Nummer 5 Patrick Simon = Hof 1, Tisch 8, Nummer 5
Gisela Esposito = Hof 1, Tisch 8, Nummer 6 Stefanie Busch = Hof 1, Tisch 8, Nummer 6
Brigitte Franke = Hof 1, Tisch 8, Nummer 7 Andreas Koch = Hof 1, Tisch 8, Nummer 7
Wolfgang Meyer = Hof 1, Tisch 8, Nummer 8 Melanie Peters = Hof 1, Tisch 8, Nummer 8
[9] [9]
Wolfgang Hansen = Hof 1, Tisch 9, Nummer 1
Hannelore Barth = Hof 1, Tisch 9, Nummer 2
Erika Frank = Hof 1, Tisch 9, Nummer 3
Gerhard Erdem = Hof 1, Tisch 9, Nummer 4
Rainer Erdem = Hof 1, Tisch 9, Nummer 5
Gisela Esposito = Hof 1, Tisch 9, Nummer 6
Brigitte Franke = Hof 1, Tisch 9, Nummer 7
Wolfgang Meyer = Hof 1, Tisch 9, Nummer 8
[10] [10]
Elif Demir = Hof 1, Tisch 10, Nummer 1 Elif Demir = Hof 1, Tisch 10, Nummer 1
@@ -86,8 +86,8 @@
Olga Hansen = Hof 1, Tisch 11, Nummer 4 Olga Hansen = Hof 1, Tisch 11, Nummer 4
Daniel Horn = Hof 1, Tisch 11, Nummer 5 Daniel Horn = Hof 1, Tisch 11, Nummer 5
Milan Lange = Hof 1, Tisch 11, Nummer 6 Milan Lange = Hof 1, Tisch 11, Nummer 6
Yusuf Peters = Hof 1, Tisch 11, Nummer 7 Daniel Brandt = Hof 1, Tisch 11, Nummer 7
Melanie Peters = Hof 1, Tisch 11, Nummer 8 Kathrin Brandt = Hof 1, Tisch 11, Nummer 8
[12] [12]
Nicole Fischer = Hof 1, Tisch 12, Nummer 1 Nicole Fischer = Hof 1, Tisch 12, Nummer 1
@@ -96,38 +96,38 @@
Lena Hoffmann = Hof 1, Tisch 12, Nummer 4 Lena Hoffmann = Hof 1, Tisch 12, Nummer 4
Timo Wagner = Hof 1, Tisch 12, Nummer 5 Timo Wagner = Hof 1, Tisch 12, Nummer 5
Nicole Dietrich = Hof 1, Tisch 12, Nummer 6 Nicole Dietrich = Hof 1, Tisch 12, Nummer 6
Daniel Brandt = Hof 1, Tisch 12, Nummer 7 Dennis Kraus = Hof 1, Tisch 12, Nummer 7
Kathrin Brandt = Hof 1, Tisch 12, Nummer 8 Melanie Kraus = Hof 1, Tisch 12, Nummer 8
[13] [13]
Jennifer Dietrich = Hof 1, Tisch 13, Nummer 1 Ingrid Demir = Hof 1, Tisch 13, Nummer 1
Christian Meyer = Hof 1, Tisch 13, Nummer 2 Monika Demir = Hof 1, Tisch 13, Nummer 2
Svenja Horn = Hof 1, Tisch 13, Nummer 3 Hannelore Demir = Hof 1, Tisch 13, Nummer 3
Stefanie Roth = Hof 1, Tisch 13, Nummer 4 Yusuf Vogel = Hof 1, Tisch 13, Nummer 4
Stefan Kuhn = Hof 1, Tisch 13, Nummer 5 Sandra Vogel = Hof 1, Tisch 13, Nummer 5
Dennis Sommer = Hof 1, Tisch 13, Nummer 6 Dennis Sommer = Hof 1, Tisch 13, Nummer 6
Nicole Sommer = Hof 1, Tisch 13, Nummer 7 Nicole Sommer = Hof 1, Tisch 13, Nummer 7
Lina Sommer = Hof 1, Tisch 13, Nummer 8 Lina Sommer = Hof 1, Tisch 13, Nummer 8
[14] [14]
Guenter Schwarz = Hof 1, Tisch 14, Nummer 1 Andreas Brandt = Hof 1, Tisch 14, Nummer 1
Rainer Aksoy = Hof 1, Tisch 14, Nummer 2 Lena Brandt = Hof 1, Tisch 14, Nummer 2
Renate Aksoy = Hof 1, Tisch 14, Nummer 3 Theo Brandt = Hof 1, Tisch 14, Nummer 3
Werner Stein = Hof 1, Tisch 14, Nummer 4 Milan Brandt = Hof 1, Tisch 14, Nummer 4
Marco Meyer = Hof 1, Tisch 14, Nummer 5 Anja Brandt = Hof 1, Tisch 14, Nummer 5
Sandra Meyer = Hof 1, Tisch 14, Nummer 6 Sofia Brandt = Hof 1, Tisch 14, Nummer 6
Yusuf Vogel = Hof 1, Tisch 14, Nummer 7 Timo Kuhn = Hof 1, Tisch 14, Nummer 7
Sandra Vogel = Hof 1, Tisch 14, Nummer 8 Sarah Kuhn = Hof 1, Tisch 14, Nummer 8
[15] [15]
Andreas Brandt = Hof 1, Tisch 15, Nummer 1 Yusuf Schwarz = Hof 1, Tisch 15, Nummer 1
Lena Brandt = Hof 1, Tisch 15, Nummer 2 Sarah Schwarz = Hof 1, Tisch 15, Nummer 2
Theo Brandt = Hof 1, Tisch 15, Nummer 3 Horst Aydin = Hof 1, Tisch 15, Nummer 3
Milan Brandt = Hof 1, Tisch 15, Nummer 4 Ursula Aydin = Hof 1, Tisch 15, Nummer 4
Anja Brandt = Hof 1, Tisch 15, Nummer 5 Heinz Tran = Hof 1, Tisch 15, Nummer 5
Sofia Brandt = Hof 1, Tisch 15, Nummer 6 Erika Erdem = Hof 1, Tisch 15, Nummer 6
Timo Kuhn = Hof 1, Tisch 15, Nummer 7 Dennis Sommer = Hof 1, Tisch 15, Nummer 7
Sarah Kuhn = Hof 1, Tisch 15, Nummer 8 Jennifer Sommer = Hof 1, Tisch 15, Nummer 8
[16] [16]
Leni Kuhn = Hof 1, Tisch 16, Nummer 1 Leni Kuhn = Hof 1, Tisch 16, Nummer 1