lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain umlaufzeitempfehlung.test.ts
| 1 | import { readFileSync, readdirSync } from 'node:fs'; |
| 2 | import path from 'node:path'; |
| 3 | import { describe, expect, it } from 'vitest'; |
| 4 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 5 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 6 | import type { Project } from '@/domain/model/project'; |
| 7 | |
| 8 | /* |
| 9 | * Befund 49 (Fassung 5.10.0). |
| 10 | * |
| 11 | * Die Unterlagen stellten der 4.x-Empfehlung ("addierte Objekte auf eine Zahl |
| 12 | * -> immer NaN") fuer Version 5 eine "begruendete Empfehlung" gegenueber und |
| 13 | * sagten zu, dass die Aenderungen "andere Zahlen" liefern - also eine Ausgabe, |
| 14 | * nicht bloss eine vorhandene Formel. |
| 15 | * |
| 16 | * `compareCycleTimeMethods` rechnete sie und war geprueft, hatte aber im ganzen |
| 17 | * `src/`-Baum keinen Aufrufer. Der Signalzeitenplan ging ueber |
| 18 | * `resolveCycleTime` und rechnete genau das eine gewaehlte Verfahren; die |
| 19 | * Auswahlliste der Phasenansicht kannte keine Empfehlung, und weder Pruefbericht |
| 20 | * noch Ausdruck noch Tabellenausgabe fuehrten einen Empfehlungstext. |
| 21 | * |
| 22 | * DER FALL IST GEFALLEN, UND ZWAR PLANMAESSIG (Fassung 5.29.0). Der Vergleich |
| 23 | * hat seit dieser Fassung einen Aufrufer: Der Planaufbau rechnet ihn und legt |
| 24 | * ihn als `cycleComparison` in den Plan, die Phasenansicht stellt die vier |
| 25 | * Ansaetze gegenueber, und die Planunterlage druckt sie in einem eigenen, |
| 26 | * abwaehlbaren Abschnitt. Die Faelle messen die Aufruferlage am Baum, nicht an |
| 27 | * einer Behauptung, und verlangen jetzt den Aufrufer. |
| 28 | */ |
| 29 | |
| 30 | /** Alle Quelldateien unter src/. */ |
| 31 | function quelldateien(verzeichnis: string): string[] { |
| 32 | const gefunden: string[] = []; |
| 33 | for (const eintrag of readdirSync(verzeichnis, { withFileTypes: true })) { |
| 34 | const pfad = path.join(verzeichnis, eintrag.name); |
| 35 | if (eintrag.isDirectory()) gefunden.push(...quelldateien(pfad)); |
| 36 | else if (eintrag.name.endsWith('.ts')) gefunden.push(pfad); |
| 37 | } |
| 38 | return gefunden; |
| 39 | } |
| 40 | |
| 41 | /** |
| 42 | * Dateien unter src/, die den Namen nennen - ohne die Stelle der Definition |
| 43 | * und ohne Kommentare. |
| 44 | * |
| 45 | * KOMMENTARE ZAEHLEN NICHT (Fassung 5.28.0): Die Frage lautet "gibt es einen |
| 46 | * Aufrufer", und ein Satz ueber die Funktion ist keiner. Seit der Behebung |
| 47 | * erklaert der Kopfkommentar von `compareCycleTimeMethods`, warum die |
| 48 | * Empfehlung ein Wert und kein Verfahren ist, und nennt dabei den eigenen |
| 49 | * Namen; die Pruefung schlug daraufhin an, ohne dass ein Aufruf hinzugekommen |
| 50 | * waere. Ein Waechter, der sich an der eigenen Begruendung stoert, treibt dazu, |
| 51 | * die Begruendung wegzulassen. |
| 52 | */ |
| 53 | function nennungenAusserDerDefinition(name: string): string[] { |
| 54 | const gefunden: string[] = []; |
| 55 | for (const datei of quelldateien('src')) { |
| 56 | const inhalt = readFileSync(datei, 'utf8') |
| 57 | .replace(/\/\*[\s\S]*?\*\//g, '') |
| 58 | .replace(/\/\/[^\n]*/g, ''); |
| 59 | const ohneDefinition = inhalt.replace(`export function ${name}(`, ''); |
| 60 | if (ohneDefinition.includes(name)) gefunden.push(datei); |
| 61 | } |
| 62 | return gefunden; |
| 63 | } |
| 64 | |
| 65 | const DATUM = new Date('2026-01-01T00:00:00Z'); |
| 66 | |
| 67 | /** |
| 68 | * Ein Knotenpunkt, an dem NUR das HBS-Verfahren scheitert. |
| 69 | * |
| 70 | * Gemessen: 900 Kfz/h je Signalgruppe ergeben Y = 0,90 bei einer Verlustzeit |
| 71 | * von 24 s. Die Auslastungssumme liegt damit ueber dem Ziel-Auslastungsgrad |
| 72 | * des HBS-Verfahrens (0,85) und unter der Grenze 1 der wartezeitminimalen |
| 73 | * Ansaetze: Der Vergleich meldet fuer HBS 'uebersaettigt', Webster dagegen |
| 74 | * 'maximum' - rechenbar, aber ueber dem Hoechstwert. |
| 75 | * |
| 76 | * Genau diese Lage zeigt, ob fremde Meldungen in den Pruefbericht laufen: Der |
| 77 | * Kode 'uebersaettigt' entsteht ausschliesslich im HBS- und im HCM-Zweig, und |
| 78 | * gewaehlt ist keiner von beiden. |
| 79 | */ |
| 80 | function uebersaettigtesHbs(): Project { |
| 81 | const roh = createStandardIntersectionProject('Nur HBS uebersaettigt', DATUM); |
| 82 | return { |
| 83 | ...roh, |
| 84 | program: { ...roh.program, method: 'webster' }, |
| 85 | demands: roh.signalGroups |
| 86 | .filter((g) => g.mode === 'kfz') |
| 87 | .map((g) => ({ signalGroupId: g.id, volume: 900, heavyVehicleShare: 0 })), |
| 88 | }; |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * Eine einstreifige Verkehrsfuehrung mit einer Umlaufzeit ueber 120 s. |
| 93 | * |
| 94 | * Der lange Raeumweg treibt die Zwischenzeiten und damit die Verlustzeit; die |
| 95 | * Anlagenart laesst bis 300 s zu. Gemessen mit 60 m Raeumweg je Beziehung und |
| 96 | * 300 Kfz/h: 230 s nach Webster, ohne Grenzfall. Am Knotenpunkt waere derselbe |
| 97 | * Wert bei 120 s gekappt - daran zeigt sich, mit welchen Schranken der |
| 98 | * Vergleich rechnet. |
| 99 | */ |
| 100 | function langeEngstelle(): Project { |
| 101 | const roh = createStandardIntersectionProject('Lange Engstelle', DATUM); |
| 102 | return { |
| 103 | ...roh, |
| 104 | anlagenart: 'einstreifig', |
| 105 | conflicts: roh.conflicts.map((c) => ({ ...c, clearingDistance: 60 })), |
| 106 | demands: roh.signalGroups |
| 107 | .filter((g) => g.mode === 'kfz') |
| 108 | .map((g) => ({ signalGroupId: g.id, volume: 300, heavyVehicleShare: 0 })), |
| 109 | }; |
| 110 | } |
| 111 | |
| 112 | describe('Umlaufzeitempfehlung - der Sachstand', () => { |
| 113 | it('hat compareCycleTimeMethods jetzt einen Aufrufer im src-Baum', () => { |
| 114 | // Umgedreht mit Fassung 5.29.0: Der Planaufbau ruft den Vergleich, legt ihn als |
| 115 | // `cycleComparison` in den Plan, und Ansicht wie Ausdruck lesen ihn von |
| 116 | // dort. Faellt der Aufrufer wieder weg, gehoeren die Vermerke unten |
| 117 | // zurueck in die Unterlagen - dieser Fall sagt es dann. |
| 118 | expect(nennungenAusserDerDefinition('compareCycleTimeMethods')).toContain( |
| 119 | path.join('src', 'domain', 'plan', 'signalPlan.ts'), |
| 120 | ); |
| 121 | }); |
| 122 | |
| 123 | it('nimmt der Plan den Vergleich mit, ohne fremde Meldungen einzusammeln', () => { |
| 124 | /* |
| 125 | * Die entscheidende Nebenbedingung des Ausbaus: Die drei nicht gewaehlten |
| 126 | * Verfahren rechnen an einer Umlaufzeit, die niemand schaltet. Ihre |
| 127 | * Beanstandungen duerfen den Pruefbericht dieser Planung nicht fuellen. |
| 128 | * |
| 129 | * Der Fall waehlt Webster bei einer Auslastungssumme, bei der das |
| 130 | * HBS-Verfahren uebersaettigt ist - die gemessenen Zahlen stehen am |
| 131 | * Projektaufbau `uebersaettigtesHbs`. |
| 132 | */ |
| 133 | const plan = buildSignalPlan(uebersaettigtesHbs()); |
| 134 | expect(plan.cycleComparison, 'der Plan traegt den Vergleich').not.toBeNull(); |
| 135 | expect(plan.cycleComparison?.results.hbs.bounded).toBe('uebersaettigt'); |
| 136 | expect( |
| 137 | plan.notes.some((n) => n.code === 'uebersaettigt'), |
| 138 | 'die Uebersaettigung des HBS-Verfahrens steht nicht im Pruefbericht', |
| 139 | ).toBe(false); |
| 140 | expect(plan.cycleResult?.method, 'gerechnet wird das gewaehlte Verfahren').toBe('webster'); |
| 141 | }); |
| 142 | |
| 143 | it('nimmt das gewaehlte Ergebnis aus dem Vergleich, statt es zweimal zu rechnen', () => { |
| 144 | // Sonst koennten Tafel und Signalzeitenplan verschiedene Zahlen zeigen. |
| 145 | const plan = buildSignalPlan(uebersaettigtesHbs()); |
| 146 | expect(plan.cycleResult).not.toBeNull(); |
| 147 | expect(plan.cycleComparison?.results.webster.cycleTime).toBe(plan.cycleResult?.cycleTime); |
| 148 | }); |
| 149 | |
| 150 | it('rechnet den Vergleich mit den Schranken der Anlagenart', () => { |
| 151 | /* |
| 152 | * DIE STELLE, AN DER ES STILL SCHIEFGEHT. `resolveCycleTime` baut sich aus |
| 153 | * den Vorgaben und der Anlagenart einen eigenen Kennwertsatz |
| 154 | * (`cycleDefaults`); der Vergleich muss denselben bekommen. Wer ihn |
| 155 | * stattdessen mit RILSA_DEFAULTS rechnet, bekommt die Grenzen des |
| 156 | * Knotenpunkts - und an einer einstreifigen Verkehrsfuehrung, wo bis 300 s |
| 157 | * zulaessig sind, staende in der Tafel ein auf 120 s gekapptes Ergebnis |
| 158 | * neben einer Umlaufzeit von 150 s im Plan. |
| 159 | * |
| 160 | * Gemessen: Die Grenzen der einstreifigen Fuehrung sind 30 bis 300 s, die |
| 161 | * des Knotenpunkts 30 bis 120 s. |
| 162 | */ |
| 163 | const plan = buildSignalPlan(langeEngstelle()); |
| 164 | expect(plan.anlagenart).toBe('einstreifig'); |
| 165 | const webster = plan.cycleComparison?.results.webster; |
| 166 | expect(webster, 'der Vergleich ist da').toBeDefined(); |
| 167 | expect( |
| 168 | webster?.cycleTime, |
| 169 | 'mit den Knotenpunktgrenzen waere hier bei 120 s gekappt worden', |
| 170 | ).toBeGreaterThan(120); |
| 171 | expect(webster?.bounded, 'unter 300 s ist das kein Grenzfall').toBe('keine'); |
| 172 | expect(webster?.cycleTime, 'dieselbe Zahl wie im Plan').toBe(plan.cycleResult?.cycleTime); |
| 173 | }); |
| 174 | }); |