import { describe, expect, it } from 'vitest'; import { verbindeMitGruppe } from '@/domain/geometrie/ableitung'; import { lotAuf, type Haltlinie, type Lageplan, type Planlinie } from '@/domain/geometrie/lageplan'; import { REGELBREITE } from '@/domain/geometrie/vermessung'; import { createEmptyProject } from '@/domain/model/factory'; import type { Project, SignalGroup } from '@/domain/model/project'; /** * Zuordnung eines Stroms zu einer bestehenden Signalgruppe. * * Der Fall, den das Programm bis dahin nicht ausdruecken konnte: EIN Signalgeber * fuer geradeaus UND rechts. Eine Signalgruppe ist die Menge gleich schaltender * Signalgeber, kein Verkehrsstrom - sie darf mehrere Stroeme bedienen. * * Die drei Sperren sind der eigentliche Gegenstand dieser Faelle. Jede von ihnen * verhindert eine Zuordnung, die auf den ersten Blick vernuenftig aussieht und * einen Konflikt ungesichert liesse. */ const FIXED_DATE = new Date('2026-01-01T00:00:00Z'); const HALTLINIE = 'hl-nord'; const haltlinie = (id: string, versatz = 0): Haltlinie => ({ id, name: id, art: 'zufahrt', armId: 'arm-nord', von: { x: versatz, y: 0 }, bis: { x: versatz + 70, y: 0 }, }); const linie = ( id: string, punkte: readonly { x: number; y: number }[], patch: Partial = {}, ): Planlinie => ({ id, name: id, mode: 'kfz', movement: 'geradeaus', breiteMeter: REGELBREITE.kfz, punkte, haltlinieId: HALTLINIE, startT: 0.5, signalGroupId: null, ...patch, }); const gruppe = (id: string, patch: Partial = {}): SignalGroup => ({ id, name: id, mode: 'kfz', movement: 'geradeaus', armId: null, vZul: 50, lanes: 1, vehicleClass: 'pkw', minGreenOverride: null, maxGreenOverride: null, reducedMobility: false, color: '#2f6fd0', ...patch, }); /** 1 Bildpunkt = 1 m, damit sich die Masse unmittelbar ablesen lassen. */ function plan(linien: readonly Planlinie[], haltlinien: readonly Haltlinie[]): Lageplan { return { arbeitsbereiche: [], signalgeber: [], bild: null, kalibrierung: { von: { x: 0, y: 0 }, bis: { x: 100, y: 0 }, laengeMeter: 100, herkunft: 'gemessen', }, haltlinien, linien, }; } function projekt(gruppen: readonly SignalGroup[], lageplan: Lageplan): Project { const leer = createEmptyProject('Zuordnung', FIXED_DATE); return { ...leer, signalGroups: gruppen, lageplan }; } // Zwei Stroeme derselben Haltlinie, die auseinanderlaufen. const GERADEAUS = linie('geradeaus', [ { x: 35, y: 0 }, { x: 35, y: 40 }, ]); const RECHTS = linie( 'rechts', [ { x: 35, y: 0 }, { x: 38, y: 12 }, { x: 70, y: 16 }, ], { movement: 'rechts' }, ); describe('Strom einer bestehenden Signalgruppe zuordnen', () => { it('legt geradeaus und rechts auf denselben Signalgeber', () => { const lageplan = plan( [{ ...GERADEAUS, signalGroupId: 'sg-1' }, RECHTS], [haltlinie(HALTLINIE)], ); const p = projekt([gruppe('sg-1')], lageplan); const ergebnis = verbindeMitGruppe(p, lageplan, 'rechts', 'sg-1'); expect(ergebnis.hinweis).toBeNull(); expect(ergebnis.lageplan.linien.find((l) => l.id === 'rechts')?.signalGroupId).toBe('sg-1'); // Und die andere Linie bleibt unberuehrt. expect(ergebnis.lageplan.linien.find((l) => l.id === 'geradeaus')?.signalGroupId).toBe('sg-1'); }); it('loest eine Zuordnung wieder, ohne zu pruefen', () => { const lageplan = plan([{ ...GERADEAUS, signalGroupId: 'sg-1' }], [haltlinie(HALTLINIE)]); const p = projekt([gruppe('sg-1')], lageplan); const ergebnis = verbindeMitGruppe(p, lageplan, 'geradeaus', null); expect(ergebnis.hinweis).toBeNull(); expect(ergebnis.lageplan.linien[0]?.signalGroupId).toBeNull(); }); it('verweigert eine fremde Verkehrsart', () => { // Fussgaenger und Kraftfahrzeuge haben verschiedene Mindestfreigabe- und // Raeumzeiten; eine gemeinsame Gruppe koennte beide nicht einhalten. const furt = linie( 'furt', [ { x: 20, y: 6 }, { x: 60, y: 6 }, ], { mode: 'fuss', movement: 'querung', breiteMeter: REGELBREITE.fuss }, ); const lageplan = plan([{ ...GERADEAUS, signalGroupId: 'sg-1' }, furt], [haltlinie(HALTLINIE)]); const p = projekt([gruppe('sg-1')], lageplan); const ergebnis = verbindeMitGruppe(p, lageplan, 'furt', 'sg-1'); expect(ergebnis.hinweis?.code).toBe('gruppe-fremde-verkehrsart'); expect(ergebnis.lageplan).toBe(lageplan); }); it('verweigert Stroeme verschiedener Haltlinien', () => { // Gleich schaltende Signalgeber stehen an einem Querschnitt. const andere = linie( 'von-sued', [ { x: 35, y: 40 }, { x: 35, y: 5 }, ], { haltlinieId: 'hl-sued' }, ); const lageplan = plan( [{ ...GERADEAUS, signalGroupId: 'sg-1' }, andere], [ haltlinie(HALTLINIE), { ...haltlinie('hl-sued'), von: { x: 0, y: 40 }, bis: { x: 70, y: 40 } }, ], ); const p = projekt([gruppe('sg-1')], lageplan); const ergebnis = verbindeMitGruppe(p, lageplan, 'von-sued', 'sg-1'); expect(ergebnis.hinweis?.code).toBe('gruppe-fremde-haltlinie'); expect(ergebnis.lageplan).toBe(lageplan); }); it('verweigert sich kreuzende Stroeme', () => { // Die entscheidende Sperre: Zwischen Stroemen derselben Gruppe gibt es keine // Zwischenzeit. Wuerden zwei kreuzende Stroeme gebuendelt, erhielten sie // gleichzeitig Freigabe fuer einander feindliche Bewegungen - und niemand // saehe es, weil kein Konflikt mehr gefuehrt wird. const kreuzend = linie( 'kreuzend', [ { x: 60, y: 0 }, { x: 10, y: 30 }, ], { movement: 'links' }, ); const lageplan = plan( [{ ...GERADEAUS, signalGroupId: 'sg-1' }, kreuzend], [haltlinie(HALTLINIE)], ); const p = projekt([gruppe('sg-1')], lageplan); const ergebnis = verbindeMitGruppe(p, lageplan, 'kreuzend', 'sg-1'); expect(ergebnis.hinweis?.code).toBe('gruppe-kreuzende-stroeme'); expect(ergebnis.lageplan).toBe(lageplan); }); it('begruendet jede Verweigerung im Klartext', () => { const furt = linie( 'furt', [ { x: 20, y: 6 }, { x: 60, y: 6 }, ], { mode: 'fuss', movement: 'querung' }, ); const lageplan = plan([{ ...GERADEAUS, signalGroupId: 'sg-1' }, furt], [haltlinie(HALTLINIE)]); const p = projekt([gruppe('sg-1', { name: 'K1' })], lageplan); const hinweis = verbindeMitGruppe(p, lageplan, 'furt', 'sg-1').hinweis; // Der Text muss beide Beteiligten nennen - sonst weiss der Anwender nicht, // welche Zuordnung abgelehnt wurde. expect(hinweis?.message).toContain('K1'); expect(hinweis?.message).toContain('furt'); expect(hinweis?.severity).toBe('warnung'); }); }); describe('Lotfusspunkt auf der Haltlinie', () => { it('findet die naechstgelegene Stelle', () => { // Beim nachtraeglichen Zuordnen wandert der Anfangspunkt auf die Haltlinie. // Genommen wird die Stelle, die der Anwender gemeint hat. const h = haltlinie(HALTLINIE); // von (0,0) bis (70,0) expect(lotAuf(h, { x: 35, y: 12 })).toBeCloseTo(0.5, 6); expect(lotAuf(h, { x: 0, y: 3 })).toBeCloseTo(0, 6); expect(lotAuf(h, { x: 70, y: -3 })).toBeCloseTo(1, 6); }); it('begrenzt auf die Strecke, statt sie zu verlaengern', () => { const h = haltlinie(HALTLINIE); expect(lotAuf(h, { x: -40, y: 0 })).toBe(0); expect(lotAuf(h, { x: 200, y: 0 })).toBe(1); }); it('bleibt bei einer entarteten Haltlinie in der Mitte', () => { const entartet: Haltlinie = { ...haltlinie(HALTLINIE), bis: { x: 0, y: 0 } }; expect(lotAuf(entartet, { x: 10, y: 10 })).toBe(0.5); }); it('faengt einen fehlenden Punkt ab', () => { expect(lotAuf(haltlinie(HALTLINIE), undefined)).toBe(0.5); }); });