import { describe, expect, it } from 'vitest'; import { REGELBREITE, kreuzenSich, schneide, schneideMitBefund, ueberlappung, vermesseWege, type Fahrlinie, type Strombezug, } from '@/domain/geometrie/vermessung'; /** * Stroeme aus derselben Zufahrt. * * Anlass ist eine Frage des Anwenders: "Ich kann doch von einer Richtung * geradeaus fahren oder auch links abbiegen, wie werden solche Zustaende * eingezeichnet?" Die Antwort lautet: als zwei Fahrlinien mit gemeinsamer * Haltlinie. Genau dabei fiel auf, dass die Vermassung diesen gemeinsamen * Anfangspunkt als Kreuzung wertete und damit zwei Stroeme als feindlich * meldete, die gleichzeitig Freigabe erhalten muessen. * * Diese Faelle sichern die Ausnahme ab UND ihre Grenzen. Jeder Fall, der * "feindlich" erwartet, verteidigt einen echten Konflikt gegen die Ausnahme - * das ist der wichtigere Teil. */ const HALTLINIE = 'hl-nord'; function mitBezug(linie: Fahrlinie, bezug: Partial = {}): Fahrlinie { return { ...linie, bezug: { haltlinieId: HALTLINIE, haltlinienArt: 'zufahrt', mode: 'kfz', startT: 0.5, ...bezug, }, }; } /** Aus der Nordzufahrt geradeaus. */ const GERADEAUS: Fahrlinie = { id: 'nord-geradeaus', punkte: [ { x: 0, y: 0 }, { x: 0, y: 40 }, ], breite: REGELBREITE.kfz, }; /** Aus derselben Zufahrt links abbiegend, im Bogen. */ const LINKS: Fahrlinie = { id: 'nord-links', punkte: [ { x: 0, y: 0 }, { x: 5, y: 18 }, { x: 40, y: 22 }, ], breite: REGELBREITE.kfz, }; describe('Stroeme derselben Zufahrt', () => { it('wertet den gemeinsamen Haltlinienpunkt nicht als Kreuzung', () => { const a = mitBezug(GERADEAUS); const b = mitBezug(LINKS); expect(schneide(a, b)).toHaveLength(0); expect(kreuzenSich(a, b)).toBe(false); expect(vermesseWege(a, b)).toBeNull(); }); it('weist die unterdrueckte Beruehrung aus, statt sie zu verschweigen', () => { const befund = schneideMitBefund(mitBezug(GERADEAUS), mitBezug(LINKS)); expect(befund.unterdrueckt).toBe(1); expect(befund.schnitte).toHaveLength(0); }); it('verhaelt sich ohne Zufahrtsangabe wie bisher', () => { // Uebernommene Projekte haben keine Haltlinien. Sie duerfen sich nicht // stillschweigend anders verhalten; ohne Zuordnung entscheidet allein die // Zeichnung, und das ist die sichere Seite. expect(kreuzenSich(GERADEAUS, LINKS)).toBe(true); }); it('haengt nicht an Millimetern im Startpunkt', () => { // Der eigentliche Gewinn gegenueber einer Abstandstoleranz: Die Entscheidung // faellt ueber die Zugehoerigkeit, nicht ueber die Lage. Ein Millimeter // Versatz beim Klicken darf sie nicht kippen. for (const versatz of [-0.001, 0, 0.001]) { const verschoben: Fahrlinie = { ...LINKS, punkte: [{ x: versatz, y: versatz }, ...LINKS.punkte.slice(1)], }; expect(kreuzenSich(mitBezug(GERADEAUS), mitBezug(verschoben))).toBe(false); } }); it('greift nur bei ein und derselben Haltlinie', () => { // Versetzte Haltlinien, aufgeweiteter Radaufstellstreifen, vorgelagerter // Teilknoten: zwei Objekte, also kein gemeinsamer Aufstellbereich. expect(kreuzenSich(mitBezug(GERADEAUS), mitBezug(LINKS, { haltlinieId: 'hl-nord-2' }))).toBe( true, ); }); it('greift nicht bei fehlender Zuordnung einer der beiden Linien', () => { expect(kreuzenSich(mitBezug(GERADEAUS), mitBezug(LINKS, { haltlinieId: null }))).toBe(true); }); it('schuetzt den Radstrom derselben Zufahrt', () => { // Der Rechtsabbieger gegen den parallel gefuehrten Radverkehr ist einer der // sicherheitskritischsten Konflikte an einer Lichtsignalanlage. Er darf nie // ausgeblendet werden, auch nicht bei gemeinsamer Haltlinie. const rad = mitBezug({ ...LINKS, breite: REGELBREITE.rad }, { mode: 'rad' }); expect(kreuzenSich(mitBezug(GERADEAUS), rad)).toBe(true); }); it('nimmt Furten aus, die am selben Arm liegen', () => { // Eine Furt gehoert zum Knotenpunktarm, nicht zu dessen Zufahrt. Sie ist // gegenueber den einfahrenden Stroemen ihres eigenen Arms feindlich. const furt = mitBezug({ ...LINKS, breite: REGELBREITE.fuss }, { haltlinienArt: 'querung' }); const strom = mitBezug(GERADEAUS, { haltlinienArt: 'querung' }); expect(kreuzenSich(strom, furt)).toBe(true); }); it('erkennt den Linksabbieger vom rechten Fahrstreifen als echte Kreuzung', () => { // Er beginnt rechts neben dem Geradeausstrom und schneidet dessen Bahn zwei // Meter hinter der Haltlinie. Das ist ein echter Konflikt, auch bei // gemeinsamer Haltlinie - die Ausnahme greift nur im gemeinsamen Ursprung. const linksVonRechts = mitBezug({ id: 'nord-links-von-rechts', punkte: [ { x: 3, y: 0 }, { x: -6, y: 6 }, { x: -40, y: 8 }, ], breite: REGELBREITE.kfz, }); expect(kreuzenSich(mitBezug(GERADEAUS), linksVonRechts)).toBe(true); }); it('unterdrueckt nichts, was einen halben Meter hinter der Haltlinie liegt', () => { // Die Grenze der Ausnahme, ausdruecklich festgehalten: Zwei Stroeme, die // an derselben Haltlinie beginnen und sich erst dahinter schneiden, bleiben // feindlich. Die Entscheidung darueber gehoert dem Planer, nicht der // Geometrie - die Ableitung legt sie ihm einzeln vor. const kreuzendNahDahinter = mitBezug({ id: 'nord-kreuzend', punkte: [ { x: 2.5, y: 0 }, { x: -2.5, y: 5 }, { x: -30, y: 8 }, ], breite: REGELBREITE.kfz, }); expect(kreuzenSich(mitBezug(GERADEAUS), kreuzendNahDahinter)).toBe(true); }); it('blendet nur den gemeinsamen Anfang aus, nicht spaetere Kreuzungen', () => { // Ein Abbieger, der weiter im Knotenpunkt in die Bahn seines Nachbarn // zurueckschwenkt, ist ein Konflikt. const zurueckSchwenkend = mitBezug({ id: 'nord-links-zurueck', punkte: [ { x: 0, y: 0 }, { x: 12, y: 15 }, { x: -12, y: 30 }, ], breite: REGELBREITE.kfz, }); expect(kreuzenSich(mitBezug(GERADEAUS), zurueckSchwenkend)).toBe(true); }); it('behandelt die Zusammenfuehrung in eine Ausfahrt weiterhin als Konflikt', () => { // Gemeinsamer ENDpunkt: zwei Stroeme laufen in dieselbe Ausfahrt ein. Eine // pauschale Regel "Beruehrung im Endpunkt zaehlt nie" verschluckte das. const vonWesten: Fahrlinie = { id: 'west', punkte: [ { x: -40, y: 60 }, { x: 0, y: 40 }, ], breite: REGELBREITE.kfz, }; expect(kreuzenSich(mitBezug(GERADEAUS), mitBezug(vonWesten, { haltlinieId: 'hl-west' }))).toBe( true, ); }); }); describe('Doppelzaehlung und Ueberlagerung', () => { it('zaehlt einen Schnitt auf einem geteilten Stuetzpunkt nur einmal', () => { const knick: Fahrlinie = { id: 'knick', punkte: [ { x: -10, y: -10 }, { x: 0, y: 0 }, { x: 10, y: -10 }, ], breite: REGELBREITE.kfz, }; const quer: Fahrlinie = { id: 'quer', punkte: [ { x: -20, y: 0 }, { x: 20, y: 0 }, ], breite: REGELBREITE.kfz, }; const befund = schneideMitBefund(knick, quer); expect(befund.schnitte).toHaveLength(1); expect(befund.doppelt).toBe(1); }); it('findet zwei Querungen derselben Furt als zwei Schnitte', () => { // Hier darf NICHT ueber den geometrischen Punkt entdoppelt werden: Der Strom // durchfaehrt die Furt an zwei verschiedenen Stellen seines Wegs. const wende: Fahrlinie = { id: 'wende', punkte: [ { x: -10, y: -5 }, { x: -10, y: 5 }, { x: 10, y: 5 }, { x: 10, y: -5 }, ], breite: REGELBREITE.kfz, }; const furt: Fahrlinie = { id: 'furt', punkte: [ { x: -20, y: 0 }, { x: 20, y: 0 }, ], breite: REGELBREITE.fuss, }; expect(schneide(wende, furt)).toHaveLength(2); }); it('findet zwei Stroeme, die sich einen Fahrstreifen teilen', () => { // Bisher der einzige Fall, in dem die Vermassung in die UNSICHERE Richtung // falsch lag: Gleichlaufende Segmente liefern keinen Punktschnitt, die // Stroeme galten damit als vertraeglich. const vorne: Fahrlinie = { id: 'vorne', punkte: [ { x: 0, y: 0 }, { x: 40, y: 0 }, ], breite: REGELBREITE.kfz, }; const hinten: Fahrlinie = { id: 'hinten', punkte: [ { x: 10, y: 0 }, { x: 50, y: 0 }, ], breite: REGELBREITE.kfz, }; const ueber = ueberlappung(vorne, hinten); expect(ueber).not.toBeNull(); expect(ueber?.laenge).toBeCloseTo(30, 3); const wege = vermesseWege(vorne, hinten); expect(wege).not.toBeNull(); expect(wege?.konfliktbereich.ausUeberlappung).toBe(true); }); it('sieht keine Ueberlagerung bei getrennt gefuehrten Nachbarstroemen', () => { // Zwei benachbarte Fahrstreifen: 3,5 m Achsabstand bei 3,25 m Korridorbreite - // die Korridore beruehren sich nicht. const links: Fahrlinie = { id: 'fs-links', punkte: [ { x: 0, y: 0 }, { x: 40, y: 0 }, ], breite: REGELBREITE.kfz, }; const rechts: Fahrlinie = { id: 'fs-rechts', punkte: [ { x: 0, y: 3.5 }, { x: 40, y: 3.5 }, ], breite: REGELBREITE.kfz, }; expect(ueberlappung(links, rechts)).toBeNull(); }); it('erkennt eine Furt, die laengs einer Fahrlinie verlaeuft', () => { // Gegenlaeufig gezeichnet - die Richtung darf keine Rolle spielen. const fahrlinie: Fahrlinie = { id: 'kfz', punkte: [ { x: 0, y: 0 }, { x: 40, y: 0 }, ], breite: REGELBREITE.kfz, }; const furt: Fahrlinie = { id: 'furt-laengs', punkte: [ { x: 30, y: 1 }, { x: 5, y: 1 }, ], breite: REGELBREITE.fuss, }; const ueber = ueberlappung(fahrlinie, furt); expect(ueber).not.toBeNull(); expect(ueber?.laenge).toBeCloseTo(25, 3); }); it('weist den Winkel des massgebenden Schnitts aus, nicht den des ersten', () => { // Ein Strom wird zuerst spitz, dann rechtwinklig gekreuzt. Fuer den Raeumweg // zaehlt der hintere Schnitt - und damit dessen Winkel. const raeumend: Fahrlinie = { id: 'raeumend', punkte: [ { x: 0, y: 0 }, { x: 60, y: 0 }, ], breite: REGELBREITE.kfz, }; const einfahrend: Fahrlinie = { id: 'einfahrend', punkte: [ { x: 5, y: -2 }, { x: 25, y: 2 }, { x: 45, y: -20 }, { x: 45, y: 20 }, ], breite: REGELBREITE.kfz, }; const wege = vermesseWege(raeumend, einfahrend); expect(wege).not.toBeNull(); expect(wege?.winkelGrad).toBeCloseTo(90, 0); }); });