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:
+110
-6
@@ -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' )
|
||||
|
||||
Reference in New Issue
Block a user