lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain ersatzbelegeRichtungUndGroessenordnung.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import * as actions from '@/app/actions'; |
| 3 | import { |
| 4 | leiteAb, |
| 5 | uebernimmSignalgruppen, |
| 6 | uebernimmWege, |
| 7 | wegSchluessel, |
| 8 | } from '@/domain/geometrie/ableitung'; |
| 9 | import type { Lageplan, Planlinie } from '@/domain/geometrie/lageplan'; |
| 10 | import { REGELBREITE } from '@/domain/geometrie/vermessung'; |
| 11 | import { createEmptyProject, createStandardIntersectionProject } from '@/domain/model/factory'; |
| 12 | import { parseProject } from '@/domain/model/schema'; |
| 13 | import type { Project } from '@/domain/model/project'; |
| 14 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 15 | import { schwerverkehrsfaktor } from '@/domain/rilsa/capacity'; |
| 16 | import { HBS_PKW_GLEICHWERTE, RILSA_DEFAULTS } from '@/domain/rilsa/constants'; |
| 17 | import { computeIntergreen, engerRadiusWirktAn } from '@/domain/rilsa/intergreen'; |
| 18 | import { QUELLEN } from '@/domain/rilsa/quellen'; |
| 19 | import { resolveRilsaSettings } from '@/domain/rilsa/settings'; |
| 20 | import type { ClearingStream, EnteringStream } from '@/domain/rilsa/types'; |
| 21 | import { validateProject } from '@/domain/validation/engine'; |
| 22 | import { schwerverkehrSatz } from '@/services/export/bewertung'; |
| 23 | |
| 24 | /** |
| 25 | * Richtung, Groessenordnung und Herleitung der Ersatzbelege - die Befunde B1 |
| 26 | * bis B6 und F1 bis F7 der Fassung 5.5.0; die Kennungen in dieser Datei |
| 27 | * meinen diese Befunde. |
| 28 | * |
| 29 | * Alle Faelle hier schlagen gegen den Stand vom 15.08.2026 fehl. Der rote |
| 30 | * Faden ist derselbe wie in allen Tests zu den Ersatzbelegen: Keine Ausgabe |
| 31 | * darf eine Richtung, eine Groessenordnung oder eine Herleitung behaupten, die |
| 32 | * die Rechnung daneben nicht hergibt. |
| 33 | * |
| 34 | * B1 Der enge Innenradius VERKUERZTE die Zwischenzeit, sobald die |
| 35 | * Raeumgeschwindigkeit des Abbiegers unter 5 m/s abgesenkt war - die |
| 36 | * Feldschranke misst gegen den Regelwert 7,0 m/s, nicht gegen den |
| 37 | * wirksamen Wert. |
| 38 | * B2 Eine von Hand eingetragene Raeumgeschwindigkeit gewinnt gegen das |
| 39 | * Merkmal. Das stand nirgends, war nirgends geprueft, und drei Ausgaben |
| 40 | * behaupteten das Gegenteil. |
| 41 | * B3 Ein unlesbares `engerRadius` fiel ohne Meldung auf die unsichere Seite. |
| 42 | * B4 Die Uebernahme aus dem Lageplan waehlte den massgebenden Weg nach einer |
| 43 | * anderen Rechnung als der Plan. |
| 44 | * B5 Fest eingeschriebene Zahlen im Fehlertext ("3 s geradeaus, 2 s |
| 45 | * abbiegend"), obwohl beide Werte Vorgabenfelder sind. |
| 46 | * F1 "bis zu 2,3 %" war keine Obergrenze - der Zuwachs waechst monoton bis |
| 47 | * 5,3 %. |
| 48 | * F2 Die groesste unsichere Annahme (keine Aufteilung des Schwerverkehrs) |
| 49 | * stand ohne Fehlerrichtung in der gedruckten Unterlage. |
| 50 | * F6 Ein Referat war als Zitat ausgegeben. |
| 51 | */ |
| 52 | |
| 53 | const DATUM = new Date('2026-01-01T00:00:00Z'); |
| 54 | const UEBERNAHME = new Date('2026-08-15T09:30:00Z'); |
| 55 | |
| 56 | function raeumenderAbbieger(rest: Partial<ClearingStream> = {}): ClearingStream { |
| 57 | return { |
| 58 | mode: 'kfz', |
| 59 | movement: 'rechts', |
| 60 | clearingDistance: 25, |
| 61 | vehicleClass: 'pkw', |
| 62 | vZul: 50, |
| 63 | ...rest, |
| 64 | }; |
| 65 | } |
| 66 | |
| 67 | const EINFAHREND: EnteringStream = { mode: 'kfz', movement: 'geradeaus', enteringDistance: 0 }; |
| 68 | |
| 69 | // --- B1: Das Merkmal darf nie verkuerzen ----------------------------------- |
| 70 | |
| 71 | describe('B1 - enger Innenradius gegen abgesenkte Raeumgeschwindigkeit des Abbiegers', () => { |
| 72 | it('kappt die Vorgabe auf die wirksame Raeumgeschwindigkeit des Abbiegers und meldet es', () => { |
| 73 | // Beide Werte lagen einzeln in ihren Schranken: 4 m/s (Feld 3 bis 10) und |
| 74 | // 5 m/s (Feld 2 bis 7). Zusammen ergaben sie einen "engen Innenradius", |
| 75 | // der SCHNELLER raeumt als der Regelfall. |
| 76 | const { defaults, notes, deviations } = resolveRilsaSettings({ |
| 77 | clearingSpeedKfzTurning: 4, |
| 78 | clearingSpeedKfzTurningEngerRadius: 5, |
| 79 | }); |
| 80 | |
| 81 | expect(defaults.clearingSpeed.kfzTurningEngerRadius).toBe(4); |
| 82 | const meldung = notes.find((n) => n.code.endsWith('clearingSpeedKfzTurningEngerRadius')); |
| 83 | expect(meldung?.severity).toBe('fehler'); |
| 84 | expect(meldung?.message).toContain('verkürzte'); |
| 85 | |
| 86 | // Die Abweichungsliste fuehrt den ANGESETZTEN Wert - sonst druckte der |
| 87 | // Ausdruck 5 m/s neben einer Rechnung mit 4 m/s (Befund C21). |
| 88 | const abweichung = deviations.find((d) => d.key === 'clearingSpeedKfzTurningEngerRadius'); |
| 89 | expect(abweichung?.appliedValue).toBe(4); |
| 90 | }); |
| 91 | |
| 92 | it('laesst die Zwischenzeit mit dem Merkmal nie kuerzer ausfallen als ohne', () => { |
| 93 | const { defaults } = resolveRilsaSettings({ |
| 94 | clearingSpeedKfzTurning: 4, |
| 95 | clearingSpeedKfzTurningEngerRadius: 5, |
| 96 | }); |
| 97 | const ohne = computeIntergreen(raeumenderAbbieger(), EINFAHREND, defaults).intergreen; |
| 98 | const mit = computeIntergreen( |
| 99 | raeumenderAbbieger({ engerRadius: true }), |
| 100 | EINFAHREND, |
| 101 | defaults, |
| 102 | ).intergreen; |
| 103 | |
| 104 | // Gegen den Altstand: ohne 11 s, mit 10 s - das Merkmal verkuerzte um 1 s. |
| 105 | expect(ohne).toBe(11); |
| 106 | expect(mit).toBeGreaterThanOrEqual(ohne); |
| 107 | }); |
| 108 | |
| 109 | it('meldet den Widerspruch auch, wenn ein Kennwertsatz ihn von Hand mitbringt', () => { |
| 110 | // Zweiter Riegel: Wer computeIntergreen mit einem selbst gebauten |
| 111 | // Kennwertsatz aufruft, laeuft nicht an resolveRilsaSettings vorbei. |
| 112 | const kennwerte = { |
| 113 | ...RILSA_DEFAULTS, |
| 114 | clearingSpeed: { |
| 115 | ...RILSA_DEFAULTS.clearingSpeed, |
| 116 | kfzTurning: 4, |
| 117 | kfzTurningEngerRadius: 5, |
| 118 | }, |
| 119 | }; |
| 120 | const ergebnis = computeIntergreen( |
| 121 | raeumenderAbbieger({ engerRadius: true }), |
| 122 | EINFAHREND, |
| 123 | kennwerte, |
| 124 | ); |
| 125 | expect(ergebnis.clearingSpeed).toBe(4); |
| 126 | expect(ergebnis.notes.map((n) => n.code)).toContain('enger-radius-schneller-als-regelfall'); |
| 127 | }); |
| 128 | |
| 129 | it('nennt in der Schrankenbegruendung keine feste Zahl mehr', () => { |
| 130 | // Die Schranke gilt gegen den eingetragenen Nachbarwert, nicht gegen 7 m/s. |
| 131 | const { notes } = resolveRilsaSettings({ clearingSpeedKfzTurningEngerRadius: 9 }); |
| 132 | const meldung = notes.find((n) => n.code.endsWith('clearingSpeedKfzTurningEngerRadius')); |
| 133 | expect(meldung?.message).toContain('höchstens 7 m/s'); |
| 134 | expect(meldung?.message).not.toContain('ohne dieses Merkmal (7 m/s)'); |
| 135 | }); |
| 136 | }); |
| 137 | |
| 138 | // --- B2: Wer gewinnt - Merkmal oder Handeingabe? --------------------------- |
| 139 | |
| 140 | describe('B2 - Vorrang der eingetragenen Raeumgeschwindigkeit vor dem Merkmal', () => { |
| 141 | it('beantwortet die Frage an einer Stelle des Fachkerns', () => { |
| 142 | const basis = { mode: 'kfz', movement: 'rechts', engerRadius: true } as const; |
| 143 | expect(engerRadiusWirktAn(basis)).toBe(true); |
| 144 | expect(engerRadiusWirktAn({ ...basis, clearingSpeedOverride: 9 })).toBe(false); |
| 145 | // Ein ungueltiger Eintrag faellt auf den Regelwert zurueck - dann wirkt |
| 146 | // das Merkmal weiter, genau wie in resolveClearingSpeed. |
| 147 | expect(engerRadiusWirktAn({ ...basis, clearingSpeedOverride: 0 })).toBe(true); |
| 148 | expect(engerRadiusWirktAn({ ...basis, movement: 'geradeaus' })).toBe(false); |
| 149 | expect(engerRadiusWirktAn({ ...basis, engerRadius: false })).toBe(false); |
| 150 | }); |
| 151 | |
| 152 | it('vermerkt den Vorrang im Rechenweg der Beziehung', () => { |
| 153 | const ergebnis = computeIntergreen( |
| 154 | raeumenderAbbieger({ engerRadius: true, clearingSpeedOverride: 9 }), |
| 155 | EINFAHREND, |
| 156 | ); |
| 157 | expect(ergebnis.clearingSpeed).toBe(9); |
| 158 | const notiz = ergebnis.notes.find((n) => n.code === 'enger-radius-durch-vorgabe-ersetzt'); |
| 159 | expect(notiz?.severity).toBe('warnung'); |
| 160 | expect(notiz?.message).toContain('Vorrang'); |
| 161 | expect(notiz?.message).toContain('KÜRZER'); |
| 162 | }); |
| 163 | |
| 164 | it('meldet im Pruefbericht den Vorrang statt einer Verlaengerung', () => { |
| 165 | const roh = createStandardIntersectionProject('Vorrang', DATUM); |
| 166 | const k1 = roh.signalGroups.find((g) => g.name === 'K1')!; |
| 167 | const f2 = roh.signalGroups.find((g) => g.name === 'F2')!; |
| 168 | const project: Project = { |
| 169 | ...roh, |
| 170 | signalGroups: roh.signalGroups.map((g) => |
| 171 | g.id === k1.id ? { ...g, movement: 'rechts' } : g, |
| 172 | ), |
| 173 | conflicts: roh.conflicts.map((c) => |
| 174 | c.fromId === k1.id && c.toId === f2.id |
| 175 | ? { ...c, engerRadius: true, clearingSpeedOverride: 9 } |
| 176 | : c, |
| 177 | ), |
| 178 | }; |
| 179 | const bericht = validateProject(project, buildSignalPlan(project), DATUM); |
| 180 | |
| 181 | // Der Hinweis "die Zwischenzeit faellt laenger aus" darf hier NICHT stehen. |
| 182 | expect(bericht.findings.map((f) => f.rule)).not.toContain('zwischenzeiten.enger-radius'); |
| 183 | const warnung = bericht.findings.find( |
| 184 | (f) => f.rule === 'zwischenzeiten.enger-radius-ohne-wirkung', |
| 185 | ); |
| 186 | expect(warnung?.severity).toBe('warnung'); |
| 187 | expect(warnung?.message).toContain('Vorrang'); |
| 188 | expect(warnung?.suggestion).toContain('Entfernen Sie eines von beiden'); |
| 189 | }); |
| 190 | }); |
| 191 | |
| 192 | // --- B3: Unlesbare Merkmale -------------------------------------------------- |
| 193 | |
| 194 | describe('B3 - unlesbare Wahrheitswerte fallen nicht mehr stumm auf die unsichere Seite', () => { |
| 195 | function mitRohemMerkmal(feld: string, wert: unknown): ReturnType<typeof parseProject> { |
| 196 | const basis = createStandardIntersectionProject('Unlesbar', DATUM); |
| 197 | const roh = JSON.parse(JSON.stringify(basis)) as Record<string, unknown>; |
| 198 | const konflikte = roh['conflicts'] as Record<string, unknown>[]; |
| 199 | konflikte[0]![feld] = wert; |
| 200 | return parseProject(roh); |
| 201 | } |
| 202 | |
| 203 | it('meldet ein unlesbares engerRadius', () => { |
| 204 | const { project, issues } = mitRohemMerkmal('engerRadius', 'ja'); |
| 205 | expect(project.conflicts[0]?.engerRadius).toBe(false); |
| 206 | const meldung = issues.find((i) => i.path.endsWith('.engerRadius')); |
| 207 | expect(meldung?.severity).toBe('warnung'); |
| 208 | expect(meldung?.message).toContain('kürzer'); |
| 209 | }); |
| 210 | |
| 211 | it('meldet ein unlesbares haltVorKnoten', () => { |
| 212 | const { issues } = mitRohemMerkmal('haltVorKnoten', 'vielleicht'); |
| 213 | expect(issues.find((i) => i.path.endsWith('.haltVorKnoten'))?.severity).toBe('warnung'); |
| 214 | }); |
| 215 | |
| 216 | it('schweigt weiterhin zum FEHLENDEN Feld', () => { |
| 217 | // Eine Datei bis Schema 11 fuehrt das Feld nicht. Das ist keine |
| 218 | // Unlesbarkeit, sondern die Aussage "nicht gesetzt". |
| 219 | const basis = createStandardIntersectionProject('Ohne Feld', DATUM); |
| 220 | const { project, issues } = parseProject(JSON.parse(JSON.stringify(basis))); |
| 221 | expect(project.conflicts[0]?.engerRadius).toBeUndefined(); |
| 222 | expect(issues.filter((i) => i.path.includes('engerRadius'))).toHaveLength(0); |
| 223 | }); |
| 224 | }); |
| 225 | |
| 226 | // --- B4: Uebernahme aus dem Lageplan --------------------------------------- |
| 227 | |
| 228 | describe('B4 - die Uebernahme rechnet mit den Ansaetzen des Projekts', () => { |
| 229 | /** Zwei kreuzende Linien; die raeumende biegt ab. */ |
| 230 | function lageplanMitAbbieger(): Lageplan { |
| 231 | const linie = ( |
| 232 | id: string, |
| 233 | name: string, |
| 234 | movement: Planlinie['movement'], |
| 235 | punkte: readonly { x: number; y: number }[], |
| 236 | ): Planlinie => ({ |
| 237 | id, |
| 238 | name, |
| 239 | mode: 'kfz', |
| 240 | movement, |
| 241 | breiteMeter: REGELBREITE.kfz, |
| 242 | punkte, |
| 243 | haltlinieId: null, |
| 244 | startT: 0.5, |
| 245 | signalGroupId: null, |
| 246 | }); |
| 247 | |
| 248 | return { |
| 249 | arbeitsbereiche: [], |
| 250 | signalgeber: [], |
| 251 | haltlinien: [], |
| 252 | bild: { |
| 253 | datenUrl: 'data:image/png;base64,AAAA', |
| 254 | breite: 400, |
| 255 | hoehe: 400, |
| 256 | herkunft: 'Test', |
| 257 | geladenAm: DATUM.toISOString(), |
| 258 | }, |
| 259 | kalibrierung: { |
| 260 | von: { x: 0, y: 0 }, |
| 261 | bis: { x: 100, y: 0 }, |
| 262 | laengeMeter: 50, |
| 263 | herkunft: 'gemessen', |
| 264 | }, |
| 265 | linien: [ |
| 266 | linie('l-w', 'K1', 'rechts', [ |
| 267 | { x: 0, y: 100 }, |
| 268 | { x: 200, y: 100 }, |
| 269 | ]), |
| 270 | linie('l-s', 'K2', 'geradeaus', [ |
| 271 | { x: 100, y: 0 }, |
| 272 | { x: 100, y: 200 }, |
| 273 | ]), |
| 274 | ], |
| 275 | }; |
| 276 | } |
| 277 | |
| 278 | /** Die uebernommene Zwischenzeit steht in der Bemerkung der Beziehung. */ |
| 279 | function uebernommeneZwischenzeit(project: Project): number { |
| 280 | const { project: mitGruppen, lageplan } = uebernimmSignalgruppen( |
| 281 | project, |
| 282 | lageplanMitAbbieger(), |
| 283 | ['l-w', 'l-s'], |
| 284 | ); |
| 285 | const ableitung = leiteAb(mitGruppen, lageplan); |
| 286 | const weg = ableitung.wege.find((w) => w.vonLinieId === 'l-w'); |
| 287 | expect(weg).toBeDefined(); |
| 288 | const ergebnis = uebernimmWege( |
| 289 | mitGruppen, |
| 290 | lageplan, |
| 291 | ableitung, |
| 292 | [wegSchluessel(weg!)], |
| 293 | UEBERNAHME, |
| 294 | ).project; |
| 295 | const konflikt = ergebnis.conflicts.find((c) => c.note.includes('maßgebend')); |
| 296 | expect(konflikt, 'Keine Beziehung mit Uebernahmevermerk').toBeDefined(); |
| 297 | const treffer = /\((\d+) s\)/.exec(konflikt!.note); |
| 298 | expect(treffer, `Keine Zwischenzeit im Vermerk: ${konflikt!.note}`).not.toBeNull(); |
| 299 | return Number(treffer![1]); |
| 300 | } |
| 301 | |
| 302 | it('rechnet den Vergleich mit dem gewaehlten Ueberfahrzeit-Ansatz', () => { |
| 303 | const basis = createEmptyProject('Uebernahme', DATUM); |
| 304 | const mitGelbzeit = uebernommeneZwischenzeit(basis); |
| 305 | const mitFest = uebernommeneZwischenzeit({ |
| 306 | ...basis, |
| 307 | settings: { ...basis.settings, ueberfahrzeitAnsatz: 'fest' }, |
| 308 | }); |
| 309 | |
| 310 | // Abbiegend: Gelbzeit 3 s gegen feste Ueberfahrzeit 2 s. Gegen den |
| 311 | // Altstand sind beide Zahlen gleich - die Uebernahme kannte den Ansatz |
| 312 | // nicht und rechnete immer mit der Gelbzeit. |
| 313 | expect(mitFest).toBe(mitGelbzeit - 1); |
| 314 | }); |
| 315 | |
| 316 | it('rechnet den Vergleich mit dem Merkmal der vorhandenen Beziehung', () => { |
| 317 | const basis = createEmptyProject('Uebernahme Radius', DATUM); |
| 318 | const ohne = uebernommeneZwischenzeit(basis); |
| 319 | |
| 320 | // Dieselbe Uebernahme ein zweites Mal, nachdem die Beziehung das Merkmal |
| 321 | // traegt: Der Abbieger raeumt jetzt mit 5,0 statt 7,0 m/s. |
| 322 | const { project: mitGruppen, lageplan } = uebernimmSignalgruppen(basis, lageplanMitAbbieger(), [ |
| 323 | 'l-w', |
| 324 | 'l-s', |
| 325 | ]); |
| 326 | const ableitung = leiteAb(mitGruppen, lageplan); |
| 327 | const weg = ableitung.wege.find((w) => w.vonLinieId === 'l-w')!; |
| 328 | const einmal = uebernimmWege( |
| 329 | mitGruppen, |
| 330 | lageplan, |
| 331 | ableitung, |
| 332 | [wegSchluessel(weg)], |
| 333 | UEBERNAHME, |
| 334 | ).project; |
| 335 | const mitMerkmal: Project = { |
| 336 | ...einmal, |
| 337 | conflicts: einmal.conflicts.map((c) => |
| 338 | c.note.includes('maßgebend') ? { ...c, engerRadius: true } : c, |
| 339 | ), |
| 340 | }; |
| 341 | const nachher = uebernimmWege( |
| 342 | mitMerkmal, |
| 343 | lageplan, |
| 344 | leiteAb(mitMerkmal, lageplan), |
| 345 | [wegSchluessel(weg)], |
| 346 | UEBERNAHME, |
| 347 | ).project; |
| 348 | const treffer = /\((\d+) s\)/.exec( |
| 349 | nachher.conflicts.find((c) => c.note.includes('maßgebend'))!.note, |
| 350 | ); |
| 351 | expect(Number(treffer![1])).toBeGreaterThan(ohne); |
| 352 | }); |
| 353 | }); |
| 354 | |
| 355 | // --- B5 und F3: keine Zahl aus dem Satz ------------------------------------ |
| 356 | |
| 357 | describe('B5/F3 - der Fehlertext zur festen Ueberfahrzeit nennt die eingetragenen Werte', () => { |
| 358 | it('widerspricht sich nicht mehr, wenn die festen Werte geaendert sind', () => { |
| 359 | const { defaults } = resolveRilsaSettings({ |
| 360 | crossingTimeKfzGeradeaus: 5, |
| 361 | crossingTimeKfzAbbiegend: 4, |
| 362 | }); |
| 363 | const ergebnis = computeIntergreen( |
| 364 | raeumenderAbbieger({ ueberfahrzeitAnsatz: 'fest', crossingTimeOverride: 1 }), |
| 365 | EINFAHREND, |
| 366 | defaults, |
| 367 | ); |
| 368 | const meldung = ergebnis.notes.find((n) => n.code === 'ueberfahrzeit-unter-gelbzeitschranke'); |
| 369 | expect(meldung?.severity).toBe('fehler'); |
| 370 | expect(meldung?.message).toContain('Regelwert von 4 s'); |
| 371 | expect(meldung?.message).toContain('5 s geradeaus, 4 s abbiegend'); |
| 372 | // Genau das stand vorher da - und widersprach dem Satzanfang. |
| 373 | expect(meldung?.message).not.toContain('(3 s geradeaus, 2 s abbiegend)'); |
| 374 | }); |
| 375 | }); |
| 376 | |
| 377 | // --- F1 und F2: Fehlerrichtungen des Schwerverkehrs ------------------------ |
| 378 | |
| 379 | describe('F1/F2 - die Groessenordnungen der Schwerverkehrsrechnung', () => { |
| 380 | it('bestaetigt, dass der Zuwachs gegenueber dem Altstand monoton bis 5,3 % waechst', () => { |
| 381 | const zuwachs = (pSV: number): number => |
| 382 | 2000 / schwerverkehrsfaktor(pSV) / (2000 / (1 + pSV)) - 1; |
| 383 | // Die Zahl, die als Obergrenze gedruckt wurde, ist keine. |
| 384 | expect(zuwachs(1)).toBeGreaterThan(0.05); |
| 385 | expect(zuwachs(1) * 100).toBeCloseTo(5.263, 3); |
| 386 | expect(zuwachs(0.3) * 100).toBeCloseTo(2.362, 3); |
| 387 | expect(zuwachs(0.05) * 100).toBeCloseTo(0.478, 3); |
| 388 | // Streng monoton wachsend - "bis zu" gilt am oberen Rand, nicht bei 30 %. |
| 389 | let vorher = -1; |
| 390 | for (let pSV = 0; pSV <= 1.0001; pSV += 0.05) { |
| 391 | const wert = zuwachs(Math.min(pSV, 1)); |
| 392 | expect(wert).toBeGreaterThan(vorher); |
| 393 | vorher = wert; |
| 394 | } |
| 395 | }); |
| 396 | |
| 397 | it('haelt die Fehlerrichtung der fehlenden Aufteilung fest', () => { |
| 398 | const lastzug = (p: number): number => |
| 399 | schwerverkehrsfaktor(0, { lkwUndBus: 0, lkwMitAnhaenger: p }) / schwerverkehrsfaktor(p) - 1; |
| 400 | expect(lastzug(0.2) * 100).toBeCloseTo(10.17, 2); |
| 401 | expect(lastzug(1) * 100).toBeCloseTo(31.58, 2); |
| 402 | expect(HBS_PKW_GLEICHWERTE.lkwMitAnhaenger).toBe(2.5); |
| 403 | }); |
| 404 | |
| 405 | it('druckt Richtung und Groessenordnung in der Planunterlage', () => { |
| 406 | const satz = schwerverkehrSatz(); |
| 407 | expect(satz).toContain('ZU GROSS'); |
| 408 | expect(satz).toContain('10,2 %'); |
| 409 | expect(satz).toContain('31,6 %'); |
| 410 | expect(satz).toContain('1.695 Fz/h'); |
| 411 | expect(satz).toContain('1.538 Fz/h'); |
| 412 | }); |
| 413 | |
| 414 | it('nennt im Fundstellenverzeichnis beide Fehlerrichtungen mit richtigen Grenzen', () => { |
| 415 | const hinweis = QUELLEN['hbs-kapazitaet'].hinweis ?? ''; |
| 416 | expect(hinweis).toContain('5,3 %'); |
| 417 | expect(hinweis).toContain('2,4 %'); |
| 418 | expect(hinweis).toContain('31,6 %'); |
| 419 | expect(hinweis).not.toContain('bis zu 2,3 %'); |
| 420 | }); |
| 421 | }); |
| 422 | |
| 423 | // --- F6: Referat statt Zitat ------------------------------------------------ |
| 424 | |
| 425 | describe('F6 - der erschlossene Schluss ist durch das Original ersetzt', () => { |
| 426 | it('nennt die Fundstelle statt des erschlossenen Umkehrschlusses', () => { |
| 427 | /* |
| 428 | * F6 verlangte, den Hamburger Leitfaden zu REFERIEREN und den daraus |
| 429 | * gezogenen Schluss ("die RiLSA kennt keinen Sonderwert") als erschlossen |
| 430 | * zu kennzeichnen. Das war richtig, solange nur der Leitfaden vorlag. Seit |
| 431 | * 5.27.0 liegt die RiLSA vor, und der Schluss war falsch: Abschnitt 2.5.2, |
| 432 | * Fall 2 nennt die 5 m/s samt Radiusschwelle. Ein erschlossener Schluss |
| 433 | * neben der Fundstelle waere jetzt der Fehler. |
| 434 | */ |
| 435 | const quelle = QUELLEN['praxis-enger-radius']; |
| 436 | const hinweis = quelle.hinweis ?? ''; |
| 437 | expect(hinweis).not.toContain('erschlossen, nicht ausgesprochen'); |
| 438 | expect(quelle.fundstelle).toContain('2.5.2'); |
| 439 | expect(quelle.pruefstand).toBe('gesichert'); |
| 440 | /* |
| 441 | * Die alte Aussage bleibt im Text stehen, aber nur noch als Bericht ueber |
| 442 | * den frueheren Stand: Sie muss HINTER dem Satz kommen, der sie widerruft. |
| 443 | * Stuende sie davor, behauptete die Unterlage wieder, der Wert sei nicht |
| 444 | * nachweisbar. |
| 445 | */ |
| 446 | expect(hinweis).toContain('ist widerlegt'); |
| 447 | expect(hinweis.indexOf('nicht nachweisbar')).toBeGreaterThan( |
| 448 | hinweis.indexOf('Bis zur Fassung 5.26.0'), |
| 449 | ); |
| 450 | }); |
| 451 | }); |
| 452 | |
| 453 | // --- B6: gleiche Auskunft fuer beide bedingten Kennwerte ------------------- |
| 454 | |
| 455 | describe('B6 - Wirkungsvermerk auch fuer den engen Innenradius', () => { |
| 456 | it('meldet die Abweichung als wirkungslos, solange keine Beziehung das Merkmal traegt', () => { |
| 457 | const basis = createStandardIntersectionProject('Wirkungslos', DATUM); |
| 458 | const project: Project = actions.setSettings(basis, { |
| 459 | rilsa: { clearingSpeedKfzTurningEngerRadius: 4 }, |
| 460 | }); |
| 461 | const bericht = validateProject(project, buildSignalPlan(project), DATUM); |
| 462 | const befund = bericht.findings.find( |
| 463 | (f) => f.rule === 'vorgaben.abweichung.clearingSpeedKfzTurningEngerRadius', |
| 464 | ); |
| 465 | expect(befund?.message).toContain('keine Beziehung dieses Merkmal'); |
| 466 | expect(befund?.suggestion).not.toContain('in den Planunterlagen zu begründen'); |
| 467 | }); |
| 468 | }); |