""" 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 (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]