import { describe, expect, it } from 'vitest'; import { leiteAb } from '@/domain/geometrie/ableitung'; import { REGELBREITE } from '@/domain/geometrie/vermessung'; import type { Haltlinie, Lageplan, Planlinie } from '@/domain/geometrie/lageplan'; import { createEmptyProject } from '@/domain/model/factory'; import type { Movement } from '@/domain/rilsa/types'; /** * Vierarmiger Knotenpunkt mit je drei Fahrbeziehungen. * * Genau die Zeichnung, nach der der Anwender gefragt hat: aus jeder Zufahrt * geradeaus, links und rechts. Zwoelf Stroeme, alle an vier Haltlinien. * * Der Fall ist der Massstab fuer die ganze Aenderung. Ohne Haltlinien meldet * das Programm zwoelf Konflikte, die keine sind - je Zufahrt drei Paare, die * sich nur ihren Anfangspunkt teilen. Mit Haltlinien duerfen es null sein, und * kein einziger echter Konflikt darf dabei verlorengehen. * * Massstab: 1 Bildpunkt = 1 m, damit sich die Zahlen von Hand nachrechnen * lassen. Der Knotenpunkt liegt im Ursprung, die Zufahrten enden 8 m davor. */ const FIXED_DATE = new Date('2026-01-01T00:00:00Z'); interface Arm { readonly id: string; readonly name: string; /** Einheitsvektor von der Haltlinie zum Knotenpunktmittelpunkt. */ readonly ein: { x: number; y: number }; } const ARME: readonly Arm[] = [ { id: 'nord', name: 'Nord', ein: { x: 0, y: 1 } }, { id: 'ost', name: 'Ost', ein: { x: -1, y: 0 } }, { id: 'sued', name: 'Sued', ein: { x: 0, y: -1 } }, { id: 'west', name: 'West', ein: { x: 1, y: 0 } }, ]; /** Abstand der Haltlinie vom Mittelpunkt. */ const HALT = 8; /** Wie weit die Fahrlinien ueber den Knotenpunkt hinauslaufen. */ const AUS = 30; function drehe(v: { x: number; y: number }, links: boolean): { x: number; y: number } { // Linksdrehung um 90 Grad im Bildkoordinatensystem. return links ? { x: v.y, y: -v.x } : { x: -v.y, y: v.x }; } /** * Baut einen Knotenpunkt. * `mitHaltlinien` steuert, ob die Stroeme ihrer Zufahrt zugeordnet sind. */ function knotenpunkt(mitHaltlinien: boolean): Lageplan { const haltlinien: Haltlinie[] = []; const linien: Planlinie[] = []; for (const arm of ARME) { const start = { x: -arm.ein.x * HALT, y: -arm.ein.y * HALT }; // Die Haltlinie liegt quer zur Fahrtrichtung, 6 m breit. const quer = drehe(arm.ein, true); const haltlinie: Haltlinie = { id: `hl-${arm.id}`, name: arm.name, art: 'zufahrt', armId: arm.id, von: { x: start.x - quer.x * 3, y: start.y - quer.y * 3 }, bis: { x: start.x + quer.x * 3, y: start.y + quer.y * 3 }, }; haltlinien.push(haltlinie); const beziehungen: readonly { movement: Movement; links: boolean | null }[] = [ { movement: 'geradeaus', links: null }, { movement: 'links', links: true }, { movement: 'rechts', links: false }, ]; beziehungen.forEach((b, index) => { // Alle drei beginnen in der Mitte der Haltlinie - der Regelfall, wenn der // Anwender sie nacheinander auf dieselbe Stelle setzt. const punkte = [start, { x: 0, y: 0 }]; const richtung = b.links === null ? arm.ein : drehe(arm.ein, b.links); punkte.push({ x: richtung.x * AUS, y: richtung.y * AUS }); linien.push({ id: `${arm.id}-${b.movement}`, name: `${arm.name} ${b.movement}`, mode: 'kfz', movement: b.movement, breiteMeter: REGELBREITE.kfz, punkte, haltlinieId: mitHaltlinien ? haltlinie.id : null, startT: 0.5 + index * 0.0001, signalGroupId: null, }); }); } return { arbeitsbereiche: [], signalgeber: [], bild: { datenUrl: 'data:image/png;base64,AAAA', breite: 200, hoehe: 200, herkunft: 'Test', geladenAm: FIXED_DATE.toISOString(), }, // 1 Bildpunkt = 1 m. kalibrierung: { von: { x: 0, y: 0 }, bis: { x: 100, y: 0 }, laengeMeter: 100, herkunft: 'georeferenziert', }, haltlinien, linien, }; } function projektMitArmen() { const leer = createEmptyProject('Vierarmig', FIXED_DATE); return { ...leer, intersection: { ...leer.intersection, arms: ARME.map((a) => ({ id: a.id, name: a.name, direction: a.name, lanes: 1, vZul: 50 })), }, }; } /** Paare, die derselben Zufahrt angehoeren. */ function ausGleicherZufahrt(aId: string, bId: string): boolean { return (aId.split('-')[0] ?? '') === (bId.split('-')[0] ?? ''); } describe('Vierarmiger Knotenpunkt mit je drei Fahrbeziehungen', () => { it('meldet ohne Haltlinien zwoelf Konflikte, die keine sind', () => { // Der Zustand vor der Aenderung - und der Zustand jedes uebernommenen // Projekts. Er wird hier festgehalten, damit der Gewinn messbar bleibt. const ableitung = leiteAb(projektMitArmen(), knotenpunkt(false)); const falsch = ableitung.vertraeglichkeit.filter( (v) => v.feindlich && ausGleicherZufahrt(v.aLinieId, v.bLinieId), ); // Vier Zufahrten mit je drei Stroemen: drei Paare je Zufahrt. expect(falsch).toHaveLength(12); }); it('meldet mit Haltlinien keinen einzigen davon', () => { const ableitung = leiteAb(projektMitArmen(), knotenpunkt(true)); const falsch = ableitung.vertraeglichkeit.filter( (v) => v.feindlich && ausGleicherZufahrt(v.aLinieId, v.bLinieId), ); expect(falsch).toHaveLength(0); }); it('verliert dabei keinen echten Konflikt zwischen den Zufahrten', () => { const ohne = leiteAb(projektMitArmen(), knotenpunkt(false)); const mit = leiteAb(projektMitArmen(), knotenpunkt(true)); const echt = (a: typeof ohne) => a.vertraeglichkeit .filter((v) => v.feindlich && !ausGleicherZufahrt(v.aLinieId, v.bLinieId)) .map((v) => `${v.aLinieId}|${v.bLinieId}`) .sort(); expect(echt(mit)).toEqual(echt(ohne)); // Und es sind wirklich welche da - sonst prueft der Vergleich nichts. expect(echt(mit).length).toBeGreaterThan(10); }); it('weist die unterdrueckten Beruehrungen aus', () => { const ableitung = leiteAb(projektMitArmen(), knotenpunkt(true)); expect(ableitung.unterdrueckt).toHaveLength(12); for (const paar of ableitung.unterdrueckt) { expect(ausGleicherZufahrt(paar.aLinieId, paar.bLinieId)).toBe(true); expect(paar.anzahl).toBeGreaterThan(0); } }); it('erkennt die einstreifige Zufahrt als gemeinsamen Fahrstreifen', () => { // In dieser Zeichnung laufen alle drei Stroeme einer Zufahrt acht Meter auf // demselben Fahrstreifen und trennen sich erst im Knotenpunkt - der // Regelfall einer einstreifigen Zufahrt. Sie sind vertraeglich, muessen aber // denselben Signalgeber bekommen; getrennt signalisieren laesst sich nur, // was einen eigenen Fahrstreifen hat. Genau das sagt die Begruendung. const ableitung = leiteAb(projektMitArmen(), knotenpunkt(true)); const paar = ableitung.vertraeglichkeit.find( (v) => v.aLinieId === 'nord-geradeaus' && v.bLinieId === 'nord-links', ); expect(paar).toBeDefined(); expect(paar?.feindlich).toBe(false); expect(paar?.grund).toBe('gemeinsamer-fahrstreifen'); expect(paar?.begruendung).toContain('dieselbe Signalgruppe'); }); it('schlaegt keine Zwischenzeit zwischen Stroemen derselben Zufahrt vor', () => { const ableitung = leiteAb(projektMitArmen(), knotenpunkt(true)); const falsch = ableitung.wege.filter((w) => ausGleicherZufahrt(w.vonLinieId, w.nachLinieId)); expect(falsch).toHaveLength(0); }); it('warnt, solange Fahrlinien keiner Haltlinie zugeordnet sind', () => { const ableitung = leiteAb(projektMitArmen(), knotenpunkt(false)); const hinweis = ableitung.hinweise.find((h) => h.code === 'haltlinie-fehlt'); expect(hinweis).toBeDefined(); // Die Anzeige blendet blosse Hinweise aus - die Schwere muss hoeher sein. expect(hinweis?.severity).toBe('warnung'); }); });