import { describe, expect, it } from 'vitest'; import { leiteAb, uebernimmWege, wegSchluessel } from '@/domain/geometrie/ableitung'; import type { Haltlinie, Lageplan, Planlinie } from '@/domain/geometrie/lageplan'; import { REGELBREITE } from '@/domain/geometrie/vermessung'; import { createEmptyProject } from '@/domain/model/factory'; import { WEGNACHWEIS_HANDEINGABE, type Conflict, type Project } from '@/domain/model/project'; import type { SignalGroup } from '@/domain/model/project'; /* * Befund 43, behoben in Fassung 5.11.0. * * `wegSchluessel` klebte den Schluessel eines Wegevorschlags aus den beiden * PLANLINIENKENNUNGEN zusammen: `${vonLinieId}->${nachLinieId}`. Dieselbe * Bauart hat Befund 61 (Fassung 5.10.0) an drei Stellen beseitigt; die * Folge dazu (schluesselAusKennungen.test.ts) sah dabei nur die * Signalgruppenkennungen an. * * Planlinienkennungen sind noch schwaecher geprueft als Signalgruppenkennungen: * `parseLageplan` vergibt eine doppelte zwar neu, prueft aber kein * Zeichenrepertoire - eine Kennung mit "->" ueberlebt das Einlesen unveraendert. * Damit fielen ("a->b", "c") und ("a", "b->c") auf denselben Schluessel * "a->b->c". * * WAS DARAN SCHADET: `uebernimmWege` filtert mit * `if (!schluessel.has(wegSchluessel(weg))) continue;`. Der Anwender hakt EIN * Paar an, uebernommen werden ZWEI. Fuer das nicht angehakte Paar werden Raeum- * und Einfahrweg ueberschrieben - ein von Hand eingetragener, bewusst laengerer * Raeumweg wird durch den gemessenen ersetzt, und die Zwischenzeit kann dadurch * KUERZER werden. Dazu bekommt es den Herkunftsnachweis "aus dem Lageplan" mit * dem heutigen Datum, obwohl der Kopfkommentar von `uebernimmWege` zusagt, das * sei die einzige Stelle, die ihn vergibt, "und sie vergibt ihn nur fuer die * Beziehungen, deren Wege sie im selben Zug schreibt". * * GEGEN DEN ALTSTAND schlagen die beiden Faelle mit "->" in der Kennung fehl; * die Gegenprobe mit gewoehnlichen Kennungen besteht dort ebenso. */ const DATUM = new Date('2026-01-01T00:00:00Z'); /** 1 Bildpunkt = 1 m, damit sich die Masse unmittelbar ablesen lassen. */ const KALIBRIERUNG = { von: { x: 0, y: 0 }, bis: { x: 400, y: 0 }, laengeMeter: 400, herkunft: 'gemessen', } as const; /** Zufahrt und Furt einer Kreuzung, um `versatz` Meter nach Osten geschoben. */ function haltlinienPaar(nummer: number, versatz: number): Haltlinie[] { return [ { id: `hl-zufahrt-${nummer}`, name: `Zufahrt ${nummer}`, art: 'zufahrt', armId: null, von: { x: versatz, y: 0 }, bis: { x: versatz + 20, y: 0 }, }, { id: `hl-furt-${nummer}`, name: `Furt ${nummer}`, art: 'querung', armId: null, von: { x: versatz, y: 30 }, bis: { x: versatz + 30, y: 30 }, }, ]; } /** Geradeausstrom und kreuzende Furt; sie schneiden sich bei (versatz+10 | 30). */ function linienPaar( nummer: number, versatz: number, kennungGerade: string, kennungFurt: string, ): Planlinie[] { return [ { id: kennungGerade, name: `L${nummer} geradeaus`, mode: 'kfz', movement: 'geradeaus', breiteMeter: REGELBREITE.kfz, punkte: [ { x: versatz + 10, y: 0 }, { x: versatz + 10, y: 60 }, ], haltlinieId: `hl-zufahrt-${nummer}`, startT: 0.5, signalGroupId: `sg-k${nummer}`, }, { id: kennungFurt, name: `L${nummer} Furt`, mode: 'fuss', movement: 'querung', breiteMeter: REGELBREITE.fuss, punkte: [ { x: versatz, y: 30 }, { x: versatz + 30, y: 30 }, ], haltlinieId: `hl-furt-${nummer}`, startT: 0, signalGroupId: `sg-f${nummer}`, }, ]; } 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, }); /** Die von Hand gepflegte Beziehung der ZWEITEN Kreuzung - 99 m Raeumweg. */ const VON_HAND: Conflict = { id: 'cf-hand', fromId: 'sg-k2', toId: 'sg-f2', clearingDistance: 99, enteringDistance: 4, clearingSpeedOverride: null, enteringSpeedOverride: null, crossingTimeOverride: null, manualIntergreen: null, wegnachweis: WEGNACHWEIS_HANDEINGABE, note: 'Von Hand nachgemessen.', }; /** * Zwei getrennte Kreuzungen in EINEM Lageplan, mit frei waehlbaren * Planlinienkennungen. */ function aufbau(kennungen: readonly (readonly [string, string])[]): { project: Project; lageplan: Lageplan; } { const haltlinien = kennungen.flatMap((_, i) => haltlinienPaar(i + 1, i * 100)); const linien = kennungen.flatMap(([gerade, furt], i) => linienPaar(i + 1, i * 100, gerade, furt)); const lageplan: Lageplan = { bild: null, kalibrierung: KALIBRIERUNG, haltlinien, linien, arbeitsbereiche: [], signalgeber: [], }; const leer = createEmptyProject('Planlinienkennungen mit Pfeil', DATUM); return { project: { ...leer, signalGroups: kennungen.flatMap((_, i) => [ gruppe(`sg-k${i + 1}`), gruppe(`sg-f${i + 1}`, { mode: 'fuss', movement: 'querung', vehicleClass: 'keine' }), ]), conflicts: [VON_HAND], lageplan, }, lageplan, }; } /** Der Weg der ERSTEN Kreuzung, vom Geradeausstrom auf seine Furt. */ function wegDerErstenKreuzung(lageplan: Lageplan, ableitung: ReturnType): string[] { const erste = lageplan.linien[0]!.id; const zweite = lageplan.linien[1]!.id; const weg = ableitung.wege.find((w) => w.vonLinieId === erste && w.nachLinieId === zweite); // Vorbedingung jedes Falls: Ohne diesen Weg prueft nichts darunter etwas. expect(weg, 'die Ströme der ersten Kreuzung schneiden sich im Plan nicht').toBeDefined(); return [wegSchluessel(weg!)]; } describe('Befund 43 - Wegeuebernahme bei "->" in der Planlinienkennung', () => { it('haelt zwei Wege auseinander, die denselben zusammengeklebten Schluessel ergaeben', () => { const { project, lageplan } = aufbau([ ['a->b', 'c'], ['a', 'b->c'], ]); const ableitung = leiteAb(project, lageplan); const erster = ableitung.wege.find((w) => w.vonLinieId === 'a->b' && w.nachLinieId === 'c'); const zweiter = ableitung.wege.find((w) => w.vonLinieId === 'a' && w.nachLinieId === 'b->c'); expect(erster, 'Weg der ersten Kreuzung').toBeDefined(); expect(zweiter, 'Weg der zweiten Kreuzung').toBeDefined(); // Der Kern: zwei sachlich verschiedene Wege, zwei verschiedene Schluessel. expect(wegSchluessel(erster!)).not.toBe(wegSchluessel(zweiter!)); }); it('uebernimmt nur die angehakte Beziehung und laesst die andere unangetastet', () => { const { project, lageplan } = aufbau([ ['a->b', 'c'], ['a', 'b->c'], ]); const ableitung = leiteAb(project, lageplan); const ergebnis = uebernimmWege( project, lageplan, ableitung, wegDerErstenKreuzung(lageplan, ableitung), DATUM, ); expect(ergebnis.uebernommen).toBe(1); // Die nicht angehakte Beziehung behaelt ihren von Hand gepflegten, // laengeren Raeumweg - und ihren Herkunftsnachweis. const zweite = ergebnis.project.conflicts.find( (c) => c.fromId === 'sg-k2' && c.toId === 'sg-f2', ); expect(zweite?.clearingDistance).toBe(99); expect(zweite?.wegnachweis.herkunft).toBe('handeingabe'); expect(zweite?.note).toBe('Von Hand nachgemessen.'); // Und die angehakte ist wirklich geschrieben worden. const erste = ergebnis.project.conflicts.find( (c) => c.fromId === 'sg-k1' && c.toId === 'sg-f1', ); expect(erste?.wegnachweis.herkunft).toBe('lageplan'); }); it('uebernimmt bei gewoehnlichen Kennungen ebenfalls nur die angehakte (Gegenprobe)', () => { const { project, lageplan } = aufbau([ ['l-gerade-1', 'l-furt-1'], ['l-gerade-2', 'l-furt-2'], ]); const ableitung = leiteAb(project, lageplan); const ergebnis = uebernimmWege( project, lageplan, ableitung, wegDerErstenKreuzung(lageplan, ableitung), DATUM, ); expect(ergebnis.uebernommen).toBe(1); const zweite = ergebnis.project.conflicts.find( (c) => c.fromId === 'sg-k2' && c.toId === 'sg-f2', ); expect(zweite?.clearingDistance).toBe(99); expect(zweite?.wegnachweis.herkunft).toBe('handeingabe'); }); });