lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain variationenBewertungUndVerkehrsart.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { buildSignalPlan, intergreenKey, type SignalPlan } from '@/domain/plan/signalPlan'; |
| 3 | import { validateProject } from '@/domain/validation'; |
| 4 | import { defaultVehicleClass } from '@/domain/model/factory'; |
| 5 | import { ASSISTENT_VORGABE, projektAusAngaben, type Anlagenform } from '@/ui/assistent'; |
| 6 | import type { Project } from '@/domain/model/project'; |
| 7 | import type { ValidationFinding } from '@/domain/validation'; |
| 8 | |
| 9 | /** |
| 10 | * Zwei Ecken der Variationsmatrix, die bisher leer standen. |
| 11 | * |
| 12 | * DIE LAGE, GEMESSEN: Der Fachkern ist dicht bewacht, aber fast alles davon an |
| 13 | * EINER Achse - dem Knotenpunkt. Das Bewertungsverfahren HCM wurde in keinem |
| 14 | * Test an einer anderen Anlagenart GERECHNET, und eine OePNV-Signalgruppe |
| 15 | * stand in keinem Projekt, das kein Knotenpunkt war. (Beide Kombinationen |
| 16 | * kommen in `tests/services/projektdatei.test.ts` vor - dort aber nur als |
| 17 | * Rundreise durch Datei und Einlesen, ohne Plan, Bericht oder Ausdruck.) |
| 18 | * |
| 19 | * Erreichbar sind beide Einstellungen ueberall: Das Auswahlfeld |
| 20 | * "Bewertungsverfahren" steht in der Projektansicht ohne jeden Bezug zur |
| 21 | * Anlagenart, und die Schaltflaeche "+ ÖPNV" steht in der |
| 22 | * Signalgruppentabelle jeder Anlage. |
| 23 | * |
| 24 | * Was hier geprueft wird, ist deshalb nicht "rechnet es ueberhaupt" - das tut |
| 25 | * es -, sondern WAS dabei herauskommt. In beiden Faellen ist das Ergebnis |
| 26 | * bemerkenswert genug, um es festzuhalten. |
| 27 | */ |
| 28 | |
| 29 | const STICHTAG = new Date('2026-01-01T12:00:00Z'); |
| 30 | |
| 31 | /** Das Projekt, das der gefuehrte Einstieg mit seinen Vorgaben anlegt. */ |
| 32 | function ausDemAssistenten( |
| 33 | form: Anlagenform, |
| 34 | teil: Partial<typeof ASSISTENT_VORGABE> = {}, |
| 35 | ): Project { |
| 36 | return projektAusAngaben({ ...ASSISTENT_VORGABE, form, ...teil }, 'Pruefstueck', STICHTAG); |
| 37 | } |
| 38 | |
| 39 | function mitVerfahren(project: Project, verfahren: 'HBS' | 'HCM'): Project { |
| 40 | return { ...project, settings: { ...project.settings, serviceLevelScale: verfahren } }; |
| 41 | } |
| 42 | |
| 43 | /** Plan und Bericht in einem Zug - wie die Oberflaeche sie bildet. */ |
| 44 | function gerechnet(project: Project): { |
| 45 | readonly plan: SignalPlan; |
| 46 | readonly regeln: readonly string[]; |
| 47 | /** Nur die Befunde zur Leistungsfaehigkeit, mit dem Ziel dahinter. */ |
| 48 | readonly bewertung: readonly string[]; |
| 49 | } { |
| 50 | const plan = buildSignalPlan(project); |
| 51 | const bericht = validateProject(project, plan, STICHTAG); |
| 52 | return { |
| 53 | plan, |
| 54 | regeln: bericht.findings.map((f) => f.rule).sort(), |
| 55 | bewertung: bericht.findings |
| 56 | .filter((f) => f.rule.startsWith('leistungsfaehigkeit.')) |
| 57 | .map((f) => `${f.rule.replace('leistungsfaehigkeit.', '')}@${f.target?.label ?? '-'}`) |
| 58 | .sort(), |
| 59 | }; |
| 60 | } |
| 61 | |
| 62 | /** Die Signalgruppe dieses Namens im gerechneten Plan. */ |
| 63 | function gruppe(plan: SignalPlan, name: string): SignalPlan['groups'][number] { |
| 64 | const treffer = plan.groups.find((g) => g.name === name); |
| 65 | expect(treffer, `keine Signalgruppe ${name} im Plan`).toBeDefined(); |
| 66 | return treffer!; |
| 67 | } |
| 68 | |
| 69 | /** Der Befund dieser Regel im Pruefbericht - hoechstens einer je Regel hier. */ |
| 70 | function leistungsbefund(project: Project, regel: string): ValidationFinding | undefined { |
| 71 | const plan = buildSignalPlan(project); |
| 72 | return validateProject(project, plan, STICHTAG).findings.find((f) => f.rule === regel); |
| 73 | } |
| 74 | |
| 75 | /** Die Zwischenzeit, in der die eine Gruppe raeumt und die andere einfaehrt. */ |
| 76 | function zwischenzeit(plan: SignalPlan, raeumt: string, faehrtEin: string): number { |
| 77 | const schluessel = intergreenKey(gruppe(plan, raeumt).groupId, gruppe(plan, faehrtEin).groupId); |
| 78 | const eintrag = plan.intergreens.get(schluessel); |
| 79 | expect(eintrag, `keine Zwischenzeit: ${raeumt} raeumt, ${faehrtEin} faehrt ein`).toBeDefined(); |
| 80 | return eintrag!.value; |
| 81 | } |
| 82 | |
| 83 | describe('Fussgaengerschutzanlage mit dem Bewertungsverfahren HCM', () => { |
| 84 | /* |
| 85 | * ZWEI SCHRANKEN TREFFEN AUFEINANDER, und die vier Faelle hier trennen sie: |
| 86 | * die Anlagenart, die an einer Fussgaengerschutzanlage die Kfz-Leistungs- |
| 87 | * faehigkeit ausser Betracht laesst, und das Verfahren HCM, das fuer |
| 88 | * Fussgaenger und Radverkehr keine Stufentafel kennt. Jede fuer sich ist |
| 89 | * bewacht; was sie ZUSAMMEN ergeben, stand nirgends. |
| 90 | * |
| 91 | * Aufgebaut als Vierfeldertafel - Knotenpunkt gegen Fussgaengerschutzanlage, |
| 92 | * HBS gegen HCM -, weil sich nur so zeigen laesst, welche Schranke welchen |
| 93 | * Befund wegnimmt. Ein einzelner Fall koennte beide Wirkungen nicht |
| 94 | * auseinanderhalten. |
| 95 | * |
| 96 | * DIE ANLAGE IST BEWUSST UEBERLASTET. Die Vorgabe des gefuehrten Einstiegs |
| 97 | * traegt gar keine Verkehrsstaerke - ohne sie gaebe es an der Fahrbahn |
| 98 | * ueberhaupt nichts zu bewerten, und ein Fall, der dann "es bleibt nichts" |
| 99 | * feststellt, haette nur die fehlende Eingabe gemessen und keine der beiden |
| 100 | * Schranken. Erst 6000 Fz/h auf den zwei Fahrstreifen bringen den |
| 101 | * Auslastungsgrad auf 1,79 und beide Bewertungen ueberhaupt in Reichweite. |
| 102 | */ |
| 103 | const belastet = ausDemAssistenten('fussgaengerschutzanlage', { verkehrsstaerke: 6000 }); |
| 104 | |
| 105 | /** Dieselbe Planung, nur als Knotenpunkt beurteilt. */ |
| 106 | const alsKnotenpunkt: Project = { ...belastet, anlagenart: 'knotenpunkt' }; |
| 107 | |
| 108 | it('meldet am Knotenpunkt beides: die ueberlastete Fahrbahn und beide Stufen', () => { |
| 109 | // Der Vergleichsfall. Ohne ihn waere von den drei Faellen darunter nicht |
| 110 | // zu unterscheiden, was die Anlagenart wegnimmt und was ohnehin fehlte. |
| 111 | expect(gerechnet(mitVerfahren(alsKnotenpunkt, 'HBS')).bewertung).toEqual([ |
| 112 | 'qualitaetsstufe@F1', |
| 113 | 'qualitaetsstufe@K1', |
| 114 | 'ueberlastet@K1', |
| 115 | ]); |
| 116 | }); |
| 117 | |
| 118 | it('nimmt die Anlagenart der Fahrbahn ihre Bewertung', () => { |
| 119 | /* |
| 120 | * Die erste Schranke, fuer sich. An einer Fussgaengerschutzanlage ist |
| 121 | * nicht die Leistungsfaehigkeit der Fahrbahn das Mass, sondern die |
| 122 | * Wartezeit der Querenden - der Bericht schweigt dort zum Auslastungsgrad |
| 123 | * und zur Kfz-Stufe. GERECHNET wird beides trotzdem: Der Plan fuehrt fuer |
| 124 | * K1 denselben Auslastungsgrad und dieselbe Stufe F wie am Knotenpunkt. |
| 125 | * Weggenommen ist nur die Meldung. |
| 126 | */ |
| 127 | const { plan, bewertung } = gerechnet(mitVerfahren(belastet, 'HBS')); |
| 128 | expect(gruppe(plan, 'K1').delay?.serviceLevel?.grade, 'gerechnet wird sie doch').toBe('F'); |
| 129 | expect(bewertung).toEqual(['qualitaetsstufe@F1']); |
| 130 | }); |
| 131 | |
| 132 | it('nimmt das Verfahren HCM der Furt ihre Stufe', () => { |
| 133 | /* |
| 134 | * Die zweite Schranke, fuer sich - am Knotenpunkt, wo die erste nicht |
| 135 | * greift. Das HCM hat fuer Fussgaenger und Radverkehr keine Stufentafel; |
| 136 | * die Wartezeit selbst bleibt, was sie war. "Nicht bewertet" ist keine |
| 137 | * schlechte Bewertung, und genau das sagt der Bericht auch. |
| 138 | */ |
| 139 | const { plan, bewertung } = gerechnet(mitVerfahren(alsKnotenpunkt, 'HCM')); |
| 140 | const furt = gruppe(plan, 'F1'); |
| 141 | expect(furt.delay?.maximumDelay, 'die Wartezeit selbst aendert sich nicht').toBe(115); |
| 142 | expect(furt.delay?.serviceLevel ?? null, 'das HCM hat fuer Furten keine Tafel').toBeNull(); |
| 143 | expect(bewertung).toEqual([ |
| 144 | 'fuss-rad-nicht-bewertet@Bewertungsverfahren', |
| 145 | 'qualitaetsstufe@K1', |
| 146 | 'ueberlastet@K1', |
| 147 | ]); |
| 148 | }); |
| 149 | |
| 150 | it('zusammen bleibt an der Fussgaengerschutzanlage keine Bewertung uebrig', () => { |
| 151 | /* |
| 152 | * DER FALL, DEN NIEMAND GERECHNET HATTE. Jede der beiden Schranken ist |
| 153 | * fuer sich richtig und fuer sich bewacht. Zusammen ergeben sie etwas |
| 154 | * Drittes: An einer Anlage, deren Zweck die Querung ist, steht bei einer |
| 155 | * zu 179 % ausgelasteten Fahrbahn und 115 s Wartezeit an der Furt keine |
| 156 | * einzige Bewertung mehr. |
| 157 | * |
| 158 | * Bis 5.20.0 stand dort nur der allgemeine Hinweis, die Furt bekomme nach |
| 159 | * HCM keine Stufe - und der verschwieg die andere Haelfte. Seither sagt |
| 160 | * die Regel `leistungsfaehigkeit.verfahren-ohne-bewertung`, dass gar |
| 161 | * nichts mehr bewertet wird, und sie ersetzt den allgemeinen Hinweis: |
| 162 | * zwei Meldungen zu derselben Sache waeren eine zu viel. |
| 163 | */ |
| 164 | const { plan, bewertung } = gerechnet(mitVerfahren(belastet, 'HCM')); |
| 165 | expect( |
| 166 | gruppe(plan, 'K1').capacity?.degreeOfSaturation, |
| 167 | 'die Fahrbahn ist ueberlastet', |
| 168 | ).toBeGreaterThan(1); |
| 169 | expect(gruppe(plan, 'F1').delay?.maximumDelay, 'die Furt wartet lange').toBe(115); |
| 170 | expect(bewertung).toEqual(['verfahren-ohne-bewertung@Bewertungsverfahren']); |
| 171 | }); |
| 172 | |
| 173 | it('nennt die Meldung beide Haelften und den Weg heraus', () => { |
| 174 | const befund = leistungsbefund( |
| 175 | mitVerfahren(belastet, 'HCM'), |
| 176 | 'leistungsfaehigkeit.verfahren-ohne-bewertung', |
| 177 | ); |
| 178 | expect(befund, 'die Regel schweigt').toBeDefined(); |
| 179 | expect(befund?.severity, 'eine Unterlage ganz ohne Bewertung ist mehr als ein Hinweis').toBe( |
| 180 | 'warnung', |
| 181 | ); |
| 182 | expect(befund?.message, 'die Anlagenartschranke fehlt').toContain('Fahrbahn'); |
| 183 | expect(befund?.message, 'die Verfahrensschranke fehlt').toContain('Furt'); |
| 184 | expect(befund?.suggestion, 'der Weg heraus fehlt').toContain('HBS'); |
| 185 | }); |
| 186 | |
| 187 | it('schweigt sie, wo eine der beiden Schranken fehlt', () => { |
| 188 | // Nach HBS bewertet die Anlage ihre Furt - dann bleibt etwas uebrig. |
| 189 | expect( |
| 190 | leistungsbefund( |
| 191 | mitVerfahren(belastet, 'HBS'), |
| 192 | 'leistungsfaehigkeit.verfahren-ohne-bewertung', |
| 193 | ), |
| 194 | 'nach HBS meldet sie trotzdem', |
| 195 | ).toBeUndefined(); |
| 196 | |
| 197 | // Und am Knotenpunkt bleibt unter HCM die Fahrbahn bewertet. |
| 198 | expect( |
| 199 | leistungsbefund( |
| 200 | mitVerfahren(alsKnotenpunkt, 'HCM'), |
| 201 | 'leistungsfaehigkeit.verfahren-ohne-bewertung', |
| 202 | ), |
| 203 | 'am Knotenpunkt meldet sie', |
| 204 | ).toBeUndefined(); |
| 205 | expect( |
| 206 | leistungsbefund( |
| 207 | mitVerfahren(alsKnotenpunkt, 'HCM'), |
| 208 | 'leistungsfaehigkeit.fuss-rad-nicht-bewertet', |
| 209 | ), |
| 210 | 'der allgemeine Hinweis ist am Knotenpunkt verschwunden', |
| 211 | ).toBeDefined(); |
| 212 | }); |
| 213 | }); |
| 214 | |
| 215 | describe('Einstreifige Verkehrsfuehrung mit dem Bewertungsverfahren HCM', () => { |
| 216 | /* |
| 217 | * Die ergiebigere der beiden Kombinationen: Hier greift die Ausblendung der |
| 218 | * Fussgaengerschutzanlage nicht, beide Signalgruppen sind Kfz-Stroeme, und |
| 219 | * der Verfahrenswechsel schlaegt bis in die Stufe durch. |
| 220 | * |
| 221 | * Der Hebel ist die Abflusszeit tA: Nach HBS 2015 ist sie die Freigabezeit |
| 222 | * zuzueglich einer Sekunde je Freigabezeitfenster - der Abfluss laeuft in |
| 223 | * die Gelbzeit hinein -, nach HCM die Freigabezeit selbst. Die Kapazitaet |
| 224 | * C = qS * tA/tU faellt damit unter HCM, und der Auslastungsgrad steigt. |
| 225 | */ |
| 226 | const engstelle = ausDemAssistenten('einstreifig', { verkehrsstaerke: 400 }); |
| 227 | |
| 228 | it('rechnet unter HCM eine kleinere Kapazitaet und einen hoeheren Auslastungsgrad', () => { |
| 229 | const hbs = gruppe(gerechnet(mitVerfahren(engstelle, 'HBS')).plan, 'K1').capacity; |
| 230 | const hcm = gruppe(gerechnet(mitVerfahren(engstelle, 'HCM')).plan, 'K1').capacity; |
| 231 | expect(hbs, 'ohne Kapazitaet sagt der Vergleich nichts').not.toBeNull(); |
| 232 | expect(hcm, 'ohne Kapazitaet sagt der Vergleich nichts').not.toBeNull(); |
| 233 | |
| 234 | expect(hbs!.verfahren).toBe('HBS'); |
| 235 | expect(hcm!.verfahren).toBe('HCM'); |
| 236 | expect(hcm!.abflusszeit, 'der Abflusszuschlag des HBS steht noch in der Rechnung').toBeLessThan( |
| 237 | hbs!.abflusszeit, |
| 238 | ); |
| 239 | expect(hcm!.capacity).toBeLessThan(hbs!.capacity); |
| 240 | expect(hcm!.degreeOfSaturation!).toBeGreaterThan(hbs!.degreeOfSaturation!); |
| 241 | }); |
| 242 | |
| 243 | it('gibt derselben Planung je nach Verfahren eine andere Qualitaetsstufe', () => { |
| 244 | /* |
| 245 | * DIE ZAHL, DIE DEN FALL TRAEGT. Derselbe Plan, dieselbe Verkehrsstaerke, |
| 246 | * dieselbe Engstelle - und die Stufe springt von B auf C, allein weil in |
| 247 | * der Projektansicht ein anderes Verfahren steht. Wer eine Planunterlage |
| 248 | * nach der Stufe beurteilt, muss wissen, wonach sie gebildet wurde; das |
| 249 | * Feld heisst deshalb seit Fassung 5.4.0 "Bewertungsverfahren" und |
| 250 | * nicht mehr "Bewertungsskala". |
| 251 | */ |
| 252 | expect( |
| 253 | gruppe(gerechnet(mitVerfahren(engstelle, 'HBS')).plan, 'K1').delay?.serviceLevel?.grade, |
| 254 | ).toBe('B'); |
| 255 | expect( |
| 256 | gruppe(gerechnet(mitVerfahren(engstelle, 'HCM')).plan, 'K1').delay?.serviceLevel?.grade, |
| 257 | ).toBe('C'); |
| 258 | }); |
| 259 | }); |
| 260 | |
| 261 | describe('OePNV in einer einstreifigen Verkehrsfuehrung', () => { |
| 262 | /* |
| 263 | * DIE GEFAEHRLICHE RICHTUNG - GEMESSEN UND FESTGEHALTEN, NICHT BEHOBEN. |
| 264 | * |
| 265 | * Ein Bus faehrt durch die Baustelle. Wer dafuer in der Signalgruppen- |
| 266 | * tabelle die Verkehrsart einer Richtung auf "ÖPNV" stellt - die |
| 267 | * Schaltflaeche steht dort, nichts hindert daran -, bekommt eine KUERZERE |
| 268 | * Zwischenzeit als fuer dieselbe Richtung als Kfz-Strom. Der Grund steht im |
| 269 | * Regelwerk und ist fuer sich richtig: Die Raeumgeschwindigkeit des OePNV |
| 270 | * ist Vmax/3,6 - bei 50 km/h also 13,89 m/s. Der Kraftfahrzeugverkehr raeumt |
| 271 | * an einer Engstelle seit 5.27.0 mit der Staffel des Abschnitts 5.2, bei |
| 272 | * 50 km/h also mit 40 km/h = 11,11 m/s (bis 5.26.0 mit festen 10,0 m/s). |
| 273 | * Der raeumende OePNV-Strom ist rechnerisch schneller aus der Engstelle |
| 274 | * heraus, also darf die Gegenrichtung frueher einfahren. |
| 275 | * |
| 276 | * Die Voraussetzung dieses Ansatzes prueft das Programm nirgends nach: Er |
| 277 | * meint den gesondert signalisierten OePNV, der den Konfliktbereich mit |
| 278 | * seiner Hoechstgeschwindigkeit durchfaehrt. In einer einstreifigen |
| 279 | * Verkehrsfuehrung faehrt der Bus auf demselben Fahrstreifen wie alles |
| 280 | * andere, hinter demselben Baustellenverkehr, und raeumt gerade nicht mit |
| 281 | * Vmax. Bei der Vorgabe des Assistenten - 120 m Engstelle, 50 km/h - fallen |
| 282 | * beide Verfahren seit 5.27.0 zufaellig auf dieselbe Sekunde; bei 400 m |
| 283 | * treten sie um fuenf Sekunden auseinander. |
| 284 | * |
| 285 | * DIE RECHNUNG BLEIBT, DER BERICHT SAGT ES JETZT. Der Kennwert wird nicht |
| 286 | * angetastet - er ist der Regelwert, und ihn zu aendern hiesse, das Regelwerk |
| 287 | * zu korrigieren. Was sich geaendert hat, ist das Schweigen daneben: Die |
| 288 | * Regel `einstreifig.oepnv-raeumgeschwindigkeit` nennt an dieser Anlagenart |
| 289 | * die Voraussetzung des Ansatzes und die beiden Geschwindigkeiten, die |
| 290 | * auseinanderfallen. Die Faelle unten halten beides gegeneinander - was |
| 291 | * gerechnet wird und was daraufhin im Bericht steht. |
| 292 | */ |
| 293 | const alsKfz = ausDemAssistenten('einstreifig'); |
| 294 | |
| 295 | /** Dieselbe Anlage, eine Richtung als OePNV - so, wie es die Tabelle tut. */ |
| 296 | function mitBus(project: Project): Project { |
| 297 | return { |
| 298 | ...project, |
| 299 | signalGroups: project.signalGroups.map((g) => |
| 300 | g.name === 'K1' |
| 301 | ? { ...g, mode: 'oepnv' as const, vehicleClass: defaultVehicleClass('oepnv') } |
| 302 | : g, |
| 303 | ), |
| 304 | }; |
| 305 | } |
| 306 | |
| 307 | it('verkuerzt die Zwischenzeit, statt sie zu verlaengern', () => { |
| 308 | const ohne = gerechnet(alsKfz).plan; |
| 309 | const mit = gerechnet(mitBus(alsKfz)).plan; |
| 310 | |
| 311 | /* |
| 312 | * NACHGEZOGEN MIT 5.27.0: Bei der Vorgabe des Assistenten - 120 m, 50 km/h |
| 313 | * - fallen beide Verfahren auf dieselbe Sekunde. Kfz nach Abschnitt 5.2: |
| 314 | * 4 + 120/40 * 3,6 = 14,8 -> 15 s. OePNV nach Abschnitt 2: 5 s |
| 315 | * Ueberfahrzeit + 126 m / 13,89 m/s = 14,07 -> 15 s. Bis 5.26.0 stand der |
| 316 | * Kfz-Wert bei 16 s und der Unterschied betrug eine Sekunde. |
| 317 | */ |
| 318 | expect(zwischenzeit(ohne, 'K1', 'K2'), 'Kfz raeumt 120 m mit 40 km/h, tue 4 s').toBe(15); |
| 319 | expect(zwischenzeit(mit, 'K1', 'K2'), 'der Bus raeumt nach Abschnitt 2').toBe(15); |
| 320 | |
| 321 | // Die Gegenrichtung bleibt, wo sie war - sie raeumt weiter als Kfz. Der |
| 322 | // Einfahrweg ist 0, die Verkehrsart des EINFAHRENDEN Stroms also ohne |
| 323 | // Wirkung; nur der raeumende zaehlt. |
| 324 | expect(zwischenzeit(mit, 'K2', 'K1'), 'die Gegenrichtung hat sich mitverschoben').toBe( |
| 325 | zwischenzeit(ohne, 'K2', 'K1'), |
| 326 | ); |
| 327 | }); |
| 328 | |
| 329 | it('traegt die Verkuerzung an einer laengeren Engstelle offen zutage', () => { |
| 330 | /* |
| 331 | * DER FALL, DEN DIE 120 m VERDECKEN. Bei 400 m Engstelle rechnet der |
| 332 | * Kraftfahrzeugverkehr nach Abschnitt 5.2 mit 4 + 400/40 * 3,6 = 40 s, der |
| 333 | * OePNV nach Abschnitt 2 mit 5 + 406/13,89 = 34,2 -> 35 s. Fuenf Sekunden |
| 334 | * kuerzer, allein weil in der Signalgruppentabelle eine Verkehrsart |
| 335 | * umgestellt wurde. |
| 336 | */ |
| 337 | const lang = ausDemAssistenten('einstreifig', { engstellenlaenge: 400 }); |
| 338 | expect(zwischenzeit(gerechnet(lang).plan, 'K1', 'K2')).toBe(40); |
| 339 | expect(zwischenzeit(gerechnet(mitBus(lang)).plan, 'K1', 'K2')).toBe(35); |
| 340 | }); |
| 341 | |
| 342 | it('meldet der Pruefbericht die Voraussetzung des Ansatzes', () => { |
| 343 | /* |
| 344 | * DER BEFUND, DER DIESE REGEL AUSGELOEST HAT: Bis 5.20.0 war die |
| 345 | * Befundliste mit und ohne OePNV-Gruppe ZEICHENGLEICH. Wer die Verkehrsart |
| 346 | * umstellte, verkuerzte eine Zwischenzeit und sah am Bericht nichts davon. |
| 347 | * |
| 348 | * Gemeldet wird nicht der Kennwert - der ist richtig -, sondern seine |
| 349 | * Voraussetzung: Der Ansatz vr = Vmax/3,6 gilt dem GESONDERT |
| 350 | * signalisierten OePNV. Ob der Bus hier auf eigenem Gleiskoerper faehrt, |
| 351 | * kann das Programm nicht wissen; an einer einstreifigen Verkehrsfuehrung |
| 352 | * kann es die Frage aber stellen, denn dort gibt es nur einen Fahrstreifen. |
| 353 | */ |
| 354 | const befund = leistungsbefund(mitBus(alsKfz), 'einstreifig.oepnv-raeumgeschwindigkeit'); |
| 355 | expect(befund, 'die Regel schweigt').toBeDefined(); |
| 356 | expect(befund?.severity, 'die verkuerzte Zwischenzeit ist mehr als ein Hinweis').toBe( |
| 357 | 'warnung', |
| 358 | ); |
| 359 | expect(befund?.target?.label, 'der Befund zeigt nicht auf die Signalgruppe').toBe('K1'); |
| 360 | |
| 361 | // Beide Geschwindigkeiten stehen darin - sonst muesste der Anwender |
| 362 | // nachschlagen, worin der Unterschied besteht. |
| 363 | expect(befund?.message, 'die Raeumgeschwindigkeit des OePNV fehlt').toContain('13,9 m/s'); |
| 364 | expect(befund?.message, 'die des Kfz fehlt').toContain('10,0 m/s'); |
| 365 | expect(befund?.message, 'die Voraussetzung des Ansatzes fehlt').toContain( |
| 366 | 'gesondert signalisiert', |
| 367 | ); |
| 368 | }); |
| 369 | |
| 370 | it('schweigt sie, wo ihre Voraussetzung nicht in Frage steht', () => { |
| 371 | /* |
| 372 | * Drei Gegenproben, und jede einzeln noetig - ohne sie waere eine Regel |
| 373 | * haltbar, die einfach immer meldet. |
| 374 | */ |
| 375 | expect( |
| 376 | leistungsbefund(alsKfz, 'einstreifig.oepnv-raeumgeschwindigkeit'), |
| 377 | 'ohne OePNV-Gruppe meldet sie trotzdem', |
| 378 | ).toBeUndefined(); |
| 379 | |
| 380 | // Am Knotenpunkt kann der OePNV sehr wohl gesondert signalisiert sein - |
| 381 | // ein eigener Gleiskoerper oder ein Sonderfahrstreifen ist dort der |
| 382 | // Regelfall. Die Frage stellt sich nur, wo es einen Fahrstreifen gibt. |
| 383 | const knoten: Project = { ...mitBus(alsKfz), anlagenart: 'knotenpunkt' }; |
| 384 | expect( |
| 385 | leistungsbefund(knoten, 'einstreifig.oepnv-raeumgeschwindigkeit'), |
| 386 | 'am Knotenpunkt meldet sie', |
| 387 | ).toBeUndefined(); |
| 388 | |
| 389 | /* |
| 390 | * Und unterhalb von 36 km/h raeumt der OePNV LANGSAMER als der Kfz-Strom |
| 391 | * (10,0 m/s): Die Zwischenzeit wird dann laenger, nicht kuerzer - die |
| 392 | * sichere Richtung. Gemessen bei 30 km/h: 8,3 m/s. |
| 393 | */ |
| 394 | const langsam = ausDemAssistenten('einstreifig', { vZul: 30 }); |
| 395 | expect( |
| 396 | leistungsbefund(mitBus(langsam), 'einstreifig.oepnv-raeumgeschwindigkeit'), |
| 397 | 'auch in der sicheren Richtung meldet sie', |
| 398 | ).toBeUndefined(); |
| 399 | }); |
| 400 | |
| 401 | it('nimmt der Richtung das Rot-Gelb und tauscht Gelb gegen das Uebergangssignal', () => { |
| 402 | /* |
| 403 | * Die zweite Folge desselben Handgriffs, und sie faellt an einer |
| 404 | * Baustellenampel mit gewoehnlichen Dreikammersignalen sofort auf: Ein |
| 405 | * OePNV-Signal ist ein Sondersignal und hat kein Rot-Gelb. |
| 406 | * |
| 407 | * NACHGEZOGEN MIT 5.27.0: Bis dahin erwartete dieser Fall auch beim Gelb |
| 408 | * eine Null. Ein Sondersignal zeigt kein GELB, wohl aber ein |
| 409 | * Uebergangssignal nach Par. 51 BOStrab; die RiLSA 2015 staffelt seine |
| 410 | * Dauer in Abschnitt 2.4 nach der betrieblich zugelassenen |
| 411 | * Hoechstgeschwindigkeit - bei 50 km/h sind es 6 s. Die Zahl steht in |
| 412 | * `times.yellow`, weil dort die Uebergangszeit vor Rot gefuehrt wird. |
| 413 | */ |
| 414 | const vorher = gruppe(gerechnet(alsKfz).plan, 'K1').times; |
| 415 | expect([vorher.yellow, vorher.redYellow], 'als Kfz-Strom Gelb und Rot-Gelb').toEqual([3, 1]); |
| 416 | |
| 417 | const bus = gruppe(gerechnet(mitBus(alsKfz)).plan, 'K1').times; |
| 418 | expect([bus.yellow, bus.redYellow], 'Uebergangssignal ja, Rot-Gelb nein').toEqual([6, 0]); |
| 419 | }); |
| 420 | }); |