lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain signalgeber.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { createEmptyProject } from '@/domain/model/factory'; |
| 3 | import * as actions from '@/app/actions'; |
| 4 | import { parseProject } from '@/domain/model/schema'; |
| 5 | import { serializeProject } from '@/services/storage'; |
| 6 | import { CURRENT_SCHEMA_VERSION, type Project } from '@/domain/model/project'; |
| 7 | import { fahrlinienVon } from '@/domain/geometrie/lageplan'; |
| 8 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 9 | import type { Signalgeber } from '@/domain/geometrie/lageplan'; |
| 10 | |
| 11 | /* |
| 12 | * STUFE 3 DES SIGNALLAGEPLAN-ENTWURFS: DER SIGNALGEBER WIRD GEZEICHNET UND |
| 13 | * RECHNET NICHT MIT. |
| 14 | * |
| 15 | * Der zweite Teil des Satzes ist der wichtigere, und er ist die Bedingung, |
| 16 | * unter der es das Objekt ueberhaupt gibt. Aus einem Signalgeberstandort |
| 17 | * LIESSE sich rechnen - der Abstand zur Haltlinie, eine Sichtweite, ein |
| 18 | * Raeumweganfang -, und jede dieser Groessen beruehrt die Zwischenzeit. Was |
| 19 | * Zwischenzeiten verkuerzt, ist die gefaehrliche Richtung; solange die |
| 20 | * Regelwerke zu Aufstellung und Abstaenden nicht im Original vorliegen, wird |
| 21 | * gezeichnet und nicht gerechnet. |
| 22 | * |
| 23 | * Diese Datei haelt das fest, damit es nicht spaeter still in eine Ableitung |
| 24 | * wandert. Dasselbe tut `tests/domain/arbeitsbereich.test.ts` fuer das Band - |
| 25 | * und dort war es die richtige Entscheidung, wie der Vergleich zeigt. |
| 26 | */ |
| 27 | |
| 28 | const DATUM = new Date('2026-09-12T10:00:00Z'); |
| 29 | |
| 30 | const GEBER: Signalgeber = { |
| 31 | id: 'sg-1', |
| 32 | name: 'K1', |
| 33 | punkt: { x: 120, y: 80 }, |
| 34 | ausrichtung: 90, |
| 35 | signalGroupId: null, |
| 36 | }; |
| 37 | |
| 38 | function projektMitGeber(geber: Signalgeber = GEBER): Project { |
| 39 | const basis = createEmptyProject('Arbeitsstelle', DATUM, 'einstreifig'); |
| 40 | return actions.addSignalgeber(basis, geber); |
| 41 | } |
| 42 | |
| 43 | describe('Signalgeber im Lageplan', () => { |
| 44 | it('legt an, ändert und löscht über reine Funktionen', () => { |
| 45 | const mit = projektMitGeber(); |
| 46 | expect(mit.lageplan.signalgeber).toHaveLength(1); |
| 47 | |
| 48 | const umbenannt = actions.setSignalgeber(mit, 'sg-1', { name: 'K2' }); |
| 49 | expect(umbenannt.lageplan.signalgeber[0]!.name).toBe('K2'); |
| 50 | // Nichts wird an Ort und Stelle veraendert. |
| 51 | expect(mit.lageplan.signalgeber[0]!.name).toBe('K1'); |
| 52 | |
| 53 | expect(actions.removeSignalgeber(mit, 'sg-1').lageplan.signalgeber).toHaveLength(0); |
| 54 | }); |
| 55 | |
| 56 | it('erscheint in keiner Fahrlinie und erzeugt keine Konfliktbeziehung', () => { |
| 57 | /* |
| 58 | * DIE ENTSCHEIDENDE PRUEFUNG. `fahrlinienVon` ist die Stelle, an der aus |
| 59 | * der Zeichnung Vermessung wird - Kreuzungspunkte, Konfliktbereiche, |
| 60 | * Raeum- und Einfahrwege. Was dort auftaucht, rechnet mit. |
| 61 | */ |
| 62 | const projekt = projektMitGeber(); |
| 63 | expect(fahrlinienVon(projekt.lageplan)).toHaveLength(0); |
| 64 | expect(projekt.conflicts).toHaveLength(0); |
| 65 | }); |
| 66 | |
| 67 | it('ändert keine einzige Zahl des Signalzeitenplans', () => { |
| 68 | const ohne = createEmptyProject('Arbeitsstelle', DATUM, 'einstreifig'); |
| 69 | const mit = actions.addSignalgeber(ohne, GEBER); |
| 70 | const planOhne = buildSignalPlan(ohne); |
| 71 | const planMit = buildSignalPlan(mit); |
| 72 | expect(planMit.cycleTime).toBe(planOhne.cycleTime); |
| 73 | expect(planMit.notes.map((n) => n.code)).toEqual(planOhne.notes.map((n) => n.code)); |
| 74 | }); |
| 75 | |
| 76 | it('überlebt Speichern und Einlesen unverändert', () => { |
| 77 | const projekt = projektMitGeber({ ...GEBER, ausrichtung: 217 }); |
| 78 | const gelesen = parseProject(JSON.parse(serializeProject(projekt))); |
| 79 | expect(gelesen.project.schemaVersion).toBe(CURRENT_SCHEMA_VERSION); |
| 80 | expect(gelesen.project.lageplan.signalgeber).toEqual(projekt.lageplan.signalgeber); |
| 81 | expect(gelesen.issues).toEqual([]); |
| 82 | }); |
| 83 | |
| 84 | it('zieht die Ausrichtung beim Einlesen auf 0 bis unter 360 Grad', () => { |
| 85 | // Ein Wert von 400 oder -20 aus fremder Datei ist kein Fehler, sondern |
| 86 | // dieselbe Richtung anders geschrieben. Eine Meldung darueber waere Laerm. |
| 87 | const roh = JSON.parse(serializeProject(projektMitGeber())) as Record<string, unknown>; |
| 88 | const lageplan = (roh['lageplan'] as Record<string, unknown>) ?? {}; |
| 89 | (lageplan['signalgeber'] as Record<string, unknown>[])[0]!['ausrichtung'] = 400; |
| 90 | const gelesen = parseProject(roh); |
| 91 | expect(gelesen.project.lageplan.signalgeber[0]!.ausrichtung).toBe(40); |
| 92 | }); |
| 93 | |
| 94 | it('behält den Standort, wenn die Signalgruppe gelöscht wird', () => { |
| 95 | /* |
| 96 | * Der Standort IST die Aussage des Objekts, die Verknuepfung nur seine |
| 97 | * Beschriftung. Faellt die Gruppe fort, faellt der Verweis - nicht der |
| 98 | * gezeichnete Punkt, den jemand vermessen hat. |
| 99 | */ |
| 100 | const basis = createEmptyProject('Knoten', DATUM); |
| 101 | const mitGruppe = actions.addSignalGroup(basis, 'kfz'); |
| 102 | const gruppe = mitGruppe.signalGroups[0]!; |
| 103 | const mitGeber = actions.addSignalgeber(mitGruppe, { ...GEBER, signalGroupId: gruppe.id }); |
| 104 | |
| 105 | const ohneGruppe = actions.removeSignalGroup(mitGeber, gruppe.id); |
| 106 | expect(ohneGruppe.lageplan.signalgeber).toHaveLength(1); |
| 107 | expect(ohneGruppe.lageplan.signalgeber[0]!.punkt).toEqual(GEBER.punkt); |
| 108 | expect(ohneGruppe.lageplan.signalgeber[0]!.signalGroupId).toBeNull(); |
| 109 | }); |
| 110 | |
| 111 | it('hebt einen Verweis auf eine nicht vorhandene Signalgruppe beim Einlesen auf', () => { |
| 112 | const roh = JSON.parse(serializeProject(projektMitGeber())) as Record<string, unknown>; |
| 113 | const lageplan = (roh['lageplan'] as Record<string, unknown>) ?? {}; |
| 114 | (lageplan['signalgeber'] as Record<string, unknown>[])[0]!['signalGroupId'] = 'gibt-es-nicht'; |
| 115 | const gelesen = parseProject(roh); |
| 116 | expect(gelesen.project.lageplan.signalgeber[0]!.signalGroupId).toBeNull(); |
| 117 | expect(gelesen.project.lageplan.signalgeber[0]!.punkt).toEqual(GEBER.punkt); |
| 118 | expect(gelesen.issues.some((i) => i.path.includes('signalgeber'))).toBe(true); |
| 119 | }); |
| 120 | |
| 121 | it('lässt einen Signalgeber ohne brauchbaren Standort aus und sagt es', () => { |
| 122 | // Ein Punkt ohne Koordinaten ist kein Standort. Ihn auf 0,0 zu setzen |
| 123 | // hiesse, eine Aufstellung an der Bildecke zu behaupten. |
| 124 | const roh = JSON.parse(serializeProject(projektMitGeber())) as Record<string, unknown>; |
| 125 | const lageplan = (roh['lageplan'] as Record<string, unknown>) ?? {}; |
| 126 | (lageplan['signalgeber'] as Record<string, unknown>[])[0]!['punkt'] = { x: 'weit', y: null }; |
| 127 | const gelesen = parseProject(roh); |
| 128 | expect(gelesen.project.lageplan.signalgeber).toHaveLength(0); |
| 129 | expect(gelesen.issues.some((i) => i.message.includes('Standort'))).toBe(true); |
| 130 | }); |
| 131 | |
| 132 | it('gibt einer Altdatei aus Schema 18 eine leere Liste', () => { |
| 133 | // Das ist die Wahrheit ueber sie: Dort war nichts erfasst. Ein Nachtrag |
| 134 | // waere eine erfundene Aufstellung. |
| 135 | const roh = JSON.parse(serializeProject(createEmptyProject('Alt', DATUM))) as Record< |
| 136 | string, |
| 137 | unknown |
| 138 | >; |
| 139 | roh['schemaVersion'] = 18; |
| 140 | delete (roh['lageplan'] as Record<string, unknown>)['signalgeber']; |
| 141 | const gelesen = parseProject(roh); |
| 142 | expect(gelesen.project.lageplan.signalgeber).toEqual([]); |
| 143 | }); |
| 144 | }); |