lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests ui winkelanzeige.test.ts

5,7 KB Rohdatei
tests/ui/winkelanzeige.test.ts — 201 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import type { Haltlinie, Lageplan, Planlinie } from '@/domain/geometrie/lageplan';
4 import { REGELBREITE } from '@/domain/geometrie/vermessung';
5 import { createEmptyProject } from '@/domain/model/factory';
6 import type { Project } from '@/domain/model/project';
7 import { lageplanView } from '@/ui/views/lageplanView';
8
9 /**
10 * Die Spalte "Winkel" gab `winkelGrad` stur als Gradzahl aus.
11 *
12 * WAS DER ANWENDER SONST ERLEBT
13 *
14 * Zwei Stroeme laufen ein Stueck nebeneinander her, statt sich in einem Punkt
15 * zu kreuzen - eine Furt entlang einer Fahrlinie, zwei Fahrbeziehungen auf
16 * derselben Flaeche. Der Konfliktbereich stammt dann aus dem Laengslauf, und
17 * `Konfliktbereich.winkel` traegt 0: Das heisst "laengs", nicht "null Grad"
18 * (siehe vermessung.ts). Die Tabelle druckte daraus "0°" - also einen Schnitt
19 * unter null Grad und damit die Aussage, die beiden Linien kreuzten sich gar
20 * nicht. Genau daneben stehen Raeumweg und Einfahrweg derselben Beziehung.
21 *
22 * `winkelGrad` geht in keine Zwischenzeit ein; es ist kein Rechenfehler,
23 * sondern eine irrefuehrende Anzeige.
24 */
25
26 const FIXED_DATE = new Date('2026-01-01T00:00:00Z');
27
28 /** 1 Bildpunkt = 1 m, damit sich die Masse unmittelbar ablesen lassen. */
29 const KALIBRIERUNG = {
30 von: { x: 0, y: 0 },
31 bis: { x: 100, y: 0 },
32 laengeMeter: 100,
33 herkunft: 'gemessen',
34 } as const;
35
36 const HL_NORD = 'hl-nord';
37 const HL_OST = 'hl-ost';
38
39 const HALTLINIEN: readonly Haltlinie[] = [
40 {
41 id: HL_NORD,
42 name: 'Zufahrt Nord',
43 art: 'zufahrt',
44 armId: 'arm-nord',
45 von: { x: 0, y: 0 },
46 bis: { x: 20, y: 0 },
47 },
48 {
49 id: HL_OST,
50 name: 'Zufahrt Ost',
51 art: 'zufahrt',
52 armId: 'arm-ost',
53 von: { x: 0, y: 10 },
54 bis: { x: 20, y: 10 },
55 },
56 ];
57
58 const linie = (
59 id: string,
60 name: string,
61 punkte: readonly { x: number; y: number }[],
62 patch: Partial<Planlinie> = {},
63 ): Planlinie => ({
64 id,
65 name,
66 mode: 'kfz',
67 movement: 'geradeaus',
68 breiteMeter: REGELBREITE.kfz,
69 punkte,
70 haltlinieId: HL_NORD,
71 startT: 0.5,
72 signalGroupId: null,
73 ...patch,
74 });
75
76 /**
77 * Zwei Stroeme verschiedener Zufahrten, die 2 m nebeneinander herlaufen.
78 *
79 * Bei 3,25 m Korridorbreite je Strom ueberlappen sich die Korridore auf ganzer
80 * Laenge; einen Punktschnitt gibt es nicht.
81 */
82 const LAENGS_A = linie('l-a', 'Nord geradeaus', [
83 { x: 10, y: 0 },
84 { x: 10, y: 60 },
85 ]);
86
87 const LAENGS_B = linie(
88 'l-b',
89 'Ost geradeaus',
90 [
91 { x: 12, y: 10 },
92 { x: 12, y: 60 },
93 ],
94 { haltlinieId: HL_OST },
95 );
96
97 /** Gegenprobe: ein echter Punktschnitt, quer zur Nordzufahrt. */
98 const QUER = linie(
99 'l-quer',
100 'Querung Ost',
101 [
102 { x: 0, y: 30 },
103 { x: 40, y: 30 },
104 ],
105 {
106 mode: 'fuss',
107 movement: 'querung',
108 breiteMeter: REGELBREITE.fuss,
109 haltlinieId: HL_OST,
110 startT: 0,
111 },
112 );
113
114 function plan(linien: readonly Planlinie[]): Lageplan {
115 return {
116 bild: null,
117 kalibrierung: KALIBRIERUNG,
118 haltlinien: HALTLINIEN,
119 linien,
120 arbeitsbereiche: [],
121 signalgeber: [],
122 };
123 }
124
125 function projekt(lageplan: Lageplan): Project {
126 return { ...createEmptyProject('Winkel', FIXED_DATE), lageplan };
127 }
128
129 interface Ansicht {
130 readonly wurzel: HTMLElement;
131 readonly aufraeumen: () => void;
132 }
133
134 function baueAnsicht(lageplan: Lageplan): Ansicht {
135 const store = new ProjectStore(projekt(lageplan));
136 const wurzel = document.createElement('div');
137 document.body.append(wurzel);
138 const schliessen = lageplanView.render(wurzel, {
139 store,
140 refresh: () => {},
141 navigate: () => {},
142 target: null,
143 });
144
145 return {
146 wurzel,
147 aufraeumen: () => {
148 schliessen();
149 wurzel.remove();
150 },
151 };
152 }
153
154 afterEach(() => {
155 document.body.replaceChildren();
156 });
157
158 /** Die Spalte "Winkel" aller Zeilen der Tabelle "Vermessene Beziehungen". */
159 function winkelspalte(ansicht: Ansicht): string[] {
160 const tabelle = [...ansicht.wurzel.querySelectorAll('table')].find((t) =>
161 (t.querySelector('thead')?.textContent ?? '').includes('Winkel'),
162 );
163 if (tabelle === undefined) throw new Error('Keine Tabelle mit einer Spalte "Winkel"');
164 const kopf = [...(tabelle.querySelector('thead tr')?.children ?? [])];
165 const spalte = kopf.findIndex((z) => (z.textContent ?? '').trim() === 'Winkel');
166 if (spalte < 0) throw new Error('Keine Spalte "Winkel"');
167 return [...tabelle.querySelectorAll('tbody tr')].map(
168 (zeile) => zeile.children[spalte]?.textContent?.trim() ?? '',
169 );
170 }
171
172 describe('Spalte „Winkel“ der vermessenen Beziehungen', () => {
173 it('weist ein Bereichsende aus dem Längslauf als „längs“ aus statt als 0 Grad', () => {
174 const ansicht = baueAnsicht(plan([LAENGS_A, LAENGS_B]));
175 try {
176 const winkel = winkelspalte(ansicht);
177 // Grundlage: Die beiden Stroeme sind ueberhaupt als feindlich vermessen.
178 expect(winkel.length, 'die Überlagerung wurde nicht vermessen').toBeGreaterThan(0);
179 // Der Kern der Aenderung: "0°" behauptet einen Schnitt unter null Grad -
180 // also gar keine Kreuzung - neben einem Räum- und einem Einfahrweg.
181 expect(winkel).not.toContain('0°');
182 expect(winkel.every((w) => w === 'längs')).toBe(true);
183 } finally {
184 ansicht.aufraeumen();
185 }
186 });
187
188 it('gibt einen echten Punktschnitt weiter in Grad an', () => {
189 // Gegenprobe gegen die Ueberkorrektur: Wo ein Schnitt den Raeumweg
190 // bestimmt, gehoert seine Gradzahl weiter in die Spalte.
191 const ansicht = baueAnsicht(plan([LAENGS_A, QUER]));
192 try {
193 const winkel = winkelspalte(ansicht);
194 expect(winkel.length, 'die Kreuzung wurde nicht vermessen').toBeGreaterThan(0);
195 expect(winkel.every((w) => w.endsWith('°'))).toBe(true);
196 expect(winkel).not.toContain('längs');
197 } finally {
198 ansicht.aufraeumen();
199 }
200 });
201 });