Abstandsstrafe fuer getrennte Gruppen + Verbindungslinien-Optionen

- strafen.cfg: neuer optionaler Abschnitt [Abstand] mit Faktor je
  normierter Abstandseinheit (0..100, bezogen auf die max. Ausdehnung
  der Tischanordnung)
- Strukturdaten.py: Strafliste.AbstandFaktor, gibPunkte('Abstand',...),
  bewerten() bestraft den max. Tisch-Tisch-Abstand getrennter Gruppen;
  abwaertskompatibel (fehlt der Abschnitt -> Faktor 0)
- platz.py: --show-/--hide-connections steuern die Verbindungslinien
  getrennter Gruppen in der SVG
- work/*, doc/images/*: Beispiele neu durchgerechnet

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Michael Stangl
2026-07-11 13:47:27 +02:00
parent 98b1dc4f36
commit 4f87895303
18 changed files with 1540 additions and 1379 deletions
+110 -6
View File
@@ -369,25 +369,34 @@ class Plaetze:
# Strafliste. Was ist wie schlecht?
class Strafliste:
"""enthaelt die Informationen zur Vergabe der Wertigkeit einer Sitzplatzverteilung"""
def __init__(self, GruppenTrennen={}, NichtNachbar=-5, Allein=-20, TischWert={}):
def __init__(self, GruppenTrennen={}, NichtNachbar=-5, Allein=-20, TischWert={}, AbstandFaktor=0.0):
self.GruppenTrennen = GruppenTrennen
self.NichtNachbar = NichtNachbar
self.Allein = Allein
self.TischWert = TischWert
# Strafpunkte je normierter Abstandseinheit (0..100) zwischen
# getrennten Teilen einer Gruppe; 0 = Abstand wird nicht bestraft.
self.AbstandFaktor = AbstandFaktor
def __repr__(self):
"""Drucke alle Bestrafungen auf der Konsole aus"""
return ("+ Strafliste:\n"
"Trennen von Gruppen (Groesse:Strafe): %s\n"
"Strafe kein Gruppenmitglied am Nachbartisch : %s\n"
"Strafe fuer eine Person allein: %s"
"Strafe fuer eine Person allein: %s\n"
"Straffaktor je Abstandseinheit getrennter Gruppen: %s"
% ( self.GruppenTrennen,
self.NichtNachbar,
self.Allein) )
def gibPunkte(self, Typ, Gruppengroesse=0, N_Teilungen=0, freiePlaetze=0):
self.Allein,
self.AbstandFaktor) )
def gibPunkte(self, Typ, Gruppengroesse=0, N_Teilungen=0, freiePlaetze=0, Abstand=0.0):
if Typ == 'allein':
return self.Allein
elif Typ == 'nichtNachbar':
return self.NichtNachbar
elif Typ == 'Abstand':
# Abstand ist bereits auf 0..100 normiert (siehe bewerten());
# je groesser der Abstand, desto mehr Strafe.
return self.AbstandFaktor * Abstand
elif Typ == 'Tischbesetzung':
try:
return self.TischWert[freiePlaetze]
@@ -417,6 +426,10 @@ class Strafliste:
self.NichtNachbar = ast.literal_eval(config.get( 'Globals', 'NichtNachbar' ))
self.Allein = ast.literal_eval(config.get( 'Globals', 'Allein' ))
self.TischWert = ast.literal_eval( config.get( 'Globals', 'TischWertigkeiten' ) )
# Abschnitt [Abstand] ist optional; fehlt er, wird der Abstand
# getrennter Gruppen nicht bestraft (Faktor 0).
if config.has_section( 'Abstand' ):
self.AbstandFaktor = ast.literal_eval(config.get( 'Abstand', 'Faktor' ))
for (k,v) in config.items( 'Trennung' ):
self.GruppenTrennen[int(k)] = ast.literal_eval(v)
@@ -548,7 +561,7 @@ class Sitzplatzverteilung:
'#42d4f4', '#f032e6', '#bfef45', '#fabed4', '#469990',
'#dcbeff', '#9a6324', '#800000', '#aaffc3', '#000075' ]
def alsSVG(self, OutputSVG, PersonenRadius=12, Puffer=10):
def alsSVG(self, OutputSVG, PersonenRadius=12, Puffer=10, Verbindungen=True):
"""Zeichnet die Sitzplatzverteilung als SVG:
Tische werden als weisse Kreise dargestellt, Personen als
@@ -556,6 +569,10 @@ class Sitzplatzverteilung:
Das Koordinatenraster der Tische wird so skaliert, dass alle
Personenkreise in ihrem Tischkreis Platz haben und sich die
Tischkreise untereinander nicht ueberschneiden.
Ist Verbindungen=True (Vorgabe), wird jede ueber mehrere Tische
getrennte Gruppe zusaetzlich mit einer Linie in ihrer Gruppenfarbe
verbunden, so dass die Zusammengehoerigkeit sichtbar bleibt.
"""
# Tischradius je Tisch berechnen: gross genug, damit alle
# Personenkreise (auf einem inneren Kreis gleichmaessig verteilt)
@@ -613,6 +630,32 @@ class Sitzplatzverteilung:
GidZuFarbe[G.Id] = self.SVGGruppenFarben[Index]
return GidZuFarbe[G.Id]
# Fuer jede Gruppe pro Tisch den Mittelpunkt (Zentroid) ihrer
# an diesem Tisch sitzenden Personenkreise sammeln. Die Reihenfolge
# der Iteration (Tische, darin Personen) stimmt mit der spaeteren
# Zeichenschleife ueberein, damit die per FarbeVonGruppe() lazy
# vergebenen Farben konsistent sind.
GidZuTischZentroiden = {}
for T in self.Tische:
Mx, My = TischMitte(T)
Plaetze = list(T)
n = len(Plaetze)
SitzRadius = SitzRadiusVon( T.Plaetze )
GidZuSummen = {}
for i, P in enumerate(Plaetze):
Winkel = 2 * math.pi * i / max(n, 1) - math.pi / 2
Px = Mx + SitzRadius * math.cos(Winkel)
Py = My + SitzRadius * math.sin(Winkel)
G = GruppeVonPerson(P)
if G is None:
continue
FarbeVonGruppe(G) # Farbe frueh (aber in Zeichenreihenfolge) festlegen
sx, sy, cnt = GidZuSummen.get( G.Id, (0.0, 0.0, 0) )
GidZuSummen[G.Id] = (sx + Px, sy + Py, cnt + 1)
for Gid, (sx, sy, cnt) in GidZuSummen.items():
GidZuTischZentroiden.setdefault( Gid, [] ).append(
(sx / cnt, sy / cnt) )
Teile = []
Teile.append( '<?xml version="1.0" encoding="UTF-8"?>\n' )
Teile.append(
@@ -620,6 +663,42 @@ class Sitzplatzverteilung:
'viewBox="0 0 %g %g">\n' % (Breite, Hoehe, Breite, Hoehe) )
Teile.append( '<rect width="100%" height="100%" fill="white"/>\n' )
# Getrennte Gruppen (an mehr als einem Tisch vertreten) mit einer
# Linie in ihrer Gruppenfarbe verbinden, damit die Zusammengehoerigkeit
# sichtbar bleibt. Die Linie laeuft durch die Tisch-Zentroiden der
# Gruppe (Mittelpunkte ihrer Personenkreise je Tisch); bei mehr als
# zwei Tischen als durchgehender Streckenzug entlang der laengsten
# Achse. Die Linienbreite entspricht dem halben Sitzdurchmesser
# (= Sitzradius = PersonenRadius). Vor den Tischen gezeichnet, damit
# sie darunter liegt. Nur wenn Verbindungen aktiv sind.
LinienBreite = PersonenRadius
for Gid, Zentroiden in ( GidZuTischZentroiden.items() if Verbindungen
else [] ):
if len(Zentroiden) < 2:
continue
# Zentroiden entlang der am weitesten auseinanderliegenden Achse
# ordnen, damit der Streckenzug sie ohne Ueberkreuzungen durchlaeuft
F1, F2 = Zentroiden[0], Zentroiden[1]
MaxDist2 = -1.0
for i in range(len(Zentroiden)):
for j in range(i + 1, len(Zentroiden)):
ax, ay = Zentroiden[i]
bx, by = Zentroiden[j]
d2 = (ax - bx) ** 2 + (ay - by) ** 2
if d2 > MaxDist2:
MaxDist2 = d2
F1, F2 = Zentroiden[i], Zentroiden[j]
Dx, Dy = F2[0] - F1[0], F2[1] - F1[1]
Geordnet = sorted( Zentroiden,
key=lambda P: (P[0] - F1[0]) * Dx + (P[1] - F1[1]) * Dy )
Punkte = ' '.join( '%g,%g' % (px, py) for (px, py) in Geordnet )
Farbe = FarbeVonGruppe( self.Gruppen.vonId(Gid) )
Teile.append(
'<polyline points="%s" fill="none" stroke="%s" '
'stroke-width="%g" stroke-linecap="round" '
'stroke-linejoin="round" stroke-opacity="0.5"/>\n'
% (Punkte, Farbe, LinienBreite) )
for T in self.Tische:
Mx, My = TischMitte(T)
TR = TidZuRadius[T.Id]
@@ -689,12 +768,37 @@ class Sitzplatzverteilung:
#self.GruppenWertAdd( Gruppe, self.Strafen.gibPunkte( 'nichtNachbar') )
# Bezugsgroesse fuer den Abstand getrennter Gruppen: maximale
# Ausdehnung der Tischanordnung (groessere von Breite/Laenge im
# Koordinatenraster). Ein Abstand von dieser Groesse entspricht dem
# normierten Wert 100 (Gruppenteile an gegenueberliegenden Enden).
Xs = [ T.X_Koordinate for T in self.Tische ]
Ys = [ T.Y_Koordinate for T in self.Tische ]
MaxAusdehnung = max( max(Xs) - min(Xs), max(Ys) - min(Ys) ) if Xs else 0
# schauen wie oft eine Gruppe getrennt wurde
for G in self.Gruppen:
NTeilungen = self.GruppenTeilungGet(G)
Strafpunkte = self.Strafen.gibPunkte( 'Trennung', G.Anzahl(), NTeilungen )
self.GruppenWertSet( G, Strafpunkte )
self.GruppenWertSet( G, Strafpunkte )
self.GesamtWertAdd( Strafpunkte )
# Bei getrennten Gruppen zusaetzlich den raeumlichen Abstand
# zwischen den belegten Tischen bestrafen: groesser = schlechter.
if NTeilungen >= 1 and MaxAusdehnung > 0:
Tische = [ self.Tische.vonId(Tid) for Tid in self.Gid_Tid[G.Id] ]
MaxDist = 0.0
for i in range(len(Tische)):
for j in range(i + 1, len(Tische)):
dx = Tische[i].X_Koordinate - Tische[j].X_Koordinate
dy = Tische[i].Y_Koordinate - Tische[j].Y_Koordinate
d = math.sqrt( dx * dx + dy * dy )
if d > MaxDist:
MaxDist = d
Abstand = MaxDist / MaxAusdehnung * 100
AbstandStrafe = self.Strafen.gibPunkte( 'Abstand', Abstand=Abstand )
self.GruppenWertAdd( G, AbstandStrafe )
self.GesamtWertAdd( AbstandStrafe )
# schauen ob Leute an einem Tisch allein sind
Strafpunkte = self.Strafen.gibPunkte( 'allein' )