lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
| 1 | import { createArm, createConflict, createPhase, createSignalGroup } from '@/domain/model/factory'; |
| 2 | import { aendereKonflikt } from '@/domain/model/project'; |
| 3 | import { legeProgrammAn, loescheProgramm, wechsleProgramm } from '@/domain/model/programme'; |
| 4 | import type { |
| 5 | IntersectionArm, |
| 6 | Konfliktaenderung, |
| 7 | Koordinierungskette, |
| 8 | Koordinierungsknoten, |
| 9 | Phase, |
| 10 | Project, |
| 11 | SignalGroup, |
| 12 | TrafficDemand, |
| 13 | } from '@/domain/model/project'; |
| 14 | import { createId } from '@/domain/id'; |
| 15 | import type { TrafficMode } from '@/domain/rilsa/types'; |
| 16 | import { |
| 17 | normalisiere, |
| 18 | type Arbeitsbereich, |
| 19 | type Signalgeber, |
| 20 | type Haltlinie, |
| 21 | type Lageplan, |
| 22 | type Planlinie, |
| 23 | } from '@/domain/geometrie/lageplan'; |
| 24 | |
| 25 | /** |
| 26 | * Aenderungen am Projekt. |
| 27 | * |
| 28 | * Alle Funktionen sind rein: sie geben ein neues Projekt zurueck und aendern das |
| 29 | * uebergebene nicht. Genau daran scheiterte im Altbestand das Rueckgaengig- |
| 30 | * machen: dort wurden Signalgruppen mit `splice` an Ort und Stelle geaendert, |
| 31 | * sodass der zuvor abgelegte Wiederherstellungspunkt dieselbe Liste |
| 32 | * referenzierte und sich mitveraenderte. |
| 33 | */ |
| 34 | |
| 35 | // --- Projektdaten ----------------------------------------------------------- |
| 36 | |
| 37 | export function setMeta(project: Project, patch: Partial<Project['meta']>): Project { |
| 38 | return { ...project, meta: { ...project.meta, ...patch } }; |
| 39 | } |
| 40 | |
| 41 | export function setIntersection( |
| 42 | project: Project, |
| 43 | patch: Partial<Project['intersection']>, |
| 44 | ): Project { |
| 45 | return { ...project, intersection: { ...project.intersection, ...patch } }; |
| 46 | } |
| 47 | |
| 48 | export function setProgram(project: Project, patch: Partial<Project['program']>): Project { |
| 49 | return { ...project, program: { ...project.program, ...patch } }; |
| 50 | } |
| 51 | |
| 52 | export function setSettings(project: Project, patch: Partial<Project['settings']>): Project { |
| 53 | return { ...project, settings: { ...project.settings, ...patch } }; |
| 54 | } |
| 55 | |
| 56 | // --- Zufahrten -------------------------------------------------------------- |
| 57 | |
| 58 | export function addArm(project: Project, name: string, direction: string): Project { |
| 59 | const arm = createArm(name, direction, 1, project.intersection.vZul); |
| 60 | return setIntersection(project, { arms: [...project.intersection.arms, arm] }); |
| 61 | } |
| 62 | |
| 63 | export function updateArm(project: Project, id: string, patch: Partial<IntersectionArm>): Project { |
| 64 | return setIntersection(project, { |
| 65 | arms: project.intersection.arms.map((a) => (a.id === id ? { ...a, ...patch } : a)), |
| 66 | }); |
| 67 | } |
| 68 | |
| 69 | export function removeArm(project: Project, id: string): Project { |
| 70 | return { |
| 71 | ...setIntersection(project, { arms: project.intersection.arms.filter((a) => a.id !== id) }), |
| 72 | // Zuordnungen aufloesen, statt sie ins Leere zeigen zu lassen. |
| 73 | signalGroups: project.signalGroups.map((g) => (g.armId === id ? { ...g, armId: null } : g)), |
| 74 | // Auch die Haltlinien: Sonst haengt die Zufahrtszugehoerigkeit der ganzen |
| 75 | // Familie an einem toten Verweis, und aufgeraeumt wuerde erst beim naechsten |
| 76 | // Einlesen - also erst nach dem Speichern. |
| 77 | lageplan: { |
| 78 | ...project.lageplan, |
| 79 | haltlinien: project.lageplan.haltlinien.map((h) => |
| 80 | h.armId === id ? { ...h, armId: null } : h, |
| 81 | ), |
| 82 | }, |
| 83 | }; |
| 84 | } |
| 85 | |
| 86 | // --- Signalgruppen ---------------------------------------------------------- |
| 87 | |
| 88 | export function addSignalGroup(project: Project, mode: TrafficMode): Project { |
| 89 | const prefix = mode === 'fuss' ? 'F' : mode === 'rad' ? 'R' : mode === 'oepnv' ? 'B' : 'K'; |
| 90 | const used = new Set(project.signalGroups.map((g) => g.name)); |
| 91 | let index = 1; |
| 92 | while (used.has(`${prefix}${index}`)) index += 1; |
| 93 | |
| 94 | const group = createSignalGroup({ |
| 95 | name: `${prefix}${index}`, |
| 96 | mode, |
| 97 | vZul: project.intersection.vZul, |
| 98 | index: project.signalGroups.length, |
| 99 | }); |
| 100 | return { ...project, signalGroups: [...project.signalGroups, group] }; |
| 101 | } |
| 102 | |
| 103 | export function updateSignalGroup( |
| 104 | project: Project, |
| 105 | id: string, |
| 106 | patch: Partial<SignalGroup>, |
| 107 | ): Project { |
| 108 | return { |
| 109 | ...project, |
| 110 | signalGroups: project.signalGroups.map((g) => (g.id === id ? { ...g, ...patch } : g)), |
| 111 | }; |
| 112 | } |
| 113 | |
| 114 | /** |
| 115 | * Loescht eine Signalgruppe samt aller Verweise. |
| 116 | * Der Altbestand entfernte nur den Eintrag; Konfliktmatrix und Phasen |
| 117 | * verwiesen danach auf eine nicht mehr vorhandene Gruppe, und weil die Matrix |
| 118 | * ueber Feldindizes gefuehrt wurde, verschoben sich alle folgenden Zuordnungen. |
| 119 | */ |
| 120 | export function removeSignalGroup(project: Project, id: string): Project { |
| 121 | return { |
| 122 | ...project, |
| 123 | signalGroups: project.signalGroups.filter((g) => g.id !== id), |
| 124 | conflicts: project.conflicts.filter((c) => c.fromId !== id && c.toId !== id), |
| 125 | phases: project.phases.map((p) => ({ |
| 126 | ...p, |
| 127 | signalGroupIds: p.signalGroupIds.filter((sgId) => sgId !== id), |
| 128 | })), |
| 129 | demands: project.demands.filter((d) => d.signalGroupId !== id), |
| 130 | /* |
| 131 | * UND DIE VERKEHRSSTAERKEN DER UEBRIGEN TAGESPROGRAMME (Schema 16). Sonst |
| 132 | * fuehrt das Nachtprogramm nach dem Loeschen eine Verkehrsstaerke fuer |
| 133 | * einen Strom, den es nicht mehr gibt; beim naechsten Umschalten stuende |
| 134 | * sie wieder im Projekt, und der Pruefbericht meldete eine Zuordnung ins |
| 135 | * Leere - genau der Fehler, an dem der Altbestand mit der Konfliktmatrix |
| 136 | * scheiterte. |
| 137 | */ |
| 138 | programmbibliothek: project.programmbibliothek.map((satz) => ({ |
| 139 | ...satz, |
| 140 | demands: satz.demands.filter((d) => d.signalGroupId !== id), |
| 141 | })), |
| 142 | // Neu: Sonst zeigt die gezeichnete Linie nach dem Loeschen ins Leere, und |
| 143 | // die Ableitung vergliche gegen eine Gruppe, die es nicht mehr gibt. |
| 144 | // Der Signalgeber (Schema 19) geht denselben Weg: Sein Standort bleibt - |
| 145 | // er ist die Aussage -, die Verknuepfung faellt. |
| 146 | lageplan: { |
| 147 | ...project.lageplan, |
| 148 | linien: project.lageplan.linien.map((l) => |
| 149 | l.signalGroupId === id ? { ...l, signalGroupId: null } : l, |
| 150 | ), |
| 151 | signalgeber: project.lageplan.signalgeber.map((g) => |
| 152 | g.signalGroupId === id ? { ...g, signalGroupId: null } : g, |
| 153 | ), |
| 154 | }, |
| 155 | }; |
| 156 | } |
| 157 | |
| 158 | export function moveSignalGroup(project: Project, id: string, delta: number): Project { |
| 159 | const index = project.signalGroups.findIndex((g) => g.id === id); |
| 160 | if (index === -1) return project; |
| 161 | const target = index + delta; |
| 162 | if (target < 0 || target >= project.signalGroups.length) return project; |
| 163 | const next = [...project.signalGroups]; |
| 164 | const [moved] = next.splice(index, 1); |
| 165 | if (moved) next.splice(target, 0, moved); |
| 166 | return { ...project, signalGroups: next }; |
| 167 | } |
| 168 | |
| 169 | // --- Konfliktbeziehungen ---------------------------------------------------- |
| 170 | |
| 171 | /** |
| 172 | * Aendert eine Konfliktbeziehung; legt sie an, wenn es sie noch nicht gibt. |
| 173 | * |
| 174 | * Geschrieben wird ausschliesslich ueber `aendereKonflikt`. Dort haengt der |
| 175 | * Herkunftsnachweis der Wege dran, und dort gehoert er auch hin: Wuerde jede |
| 176 | * Aufrufstelle selbst mischen, muesste an einem Dutzend Stellen jemand daran |
| 177 | * denken, den Nachweis zurueckzusetzen - und beim naechsten neuen Feld wieder. |
| 178 | * Vergessen hiesse: Der Ausdruck weist eine Vermessung aus, die es fuer diese |
| 179 | * Zahl nie gegeben hat. |
| 180 | * |
| 181 | * EIN SCHLUESSEL FUER SUCHEN UND SCHREIBEN: Nachgeschlagen wird ueber das Paar |
| 182 | * (fromId, toId) - geschrieben wurde ueber `c.id === existing.id`. Zwei |
| 183 | * Schluessel fuer dieselbe Entscheidung: Trifft die Kennung ein zweites Mal zu, |
| 184 | * schreibt die Funktion an eine Beziehung, die sie nie nachgeschlagen hat, und |
| 185 | * entwertet dort auch noch den Herkunftsnachweis. Die Neuvergabe beim Einlesen |
| 186 | * entschaerft nur, was durch `parseProject` laeuft. Geschrieben wird |
| 187 | * jetzt ueber denselben Schluessel, ueber den gesucht wird - so wie |
| 188 | * `removeConflict` es schon tut. |
| 189 | */ |
| 190 | export function setConflict( |
| 191 | project: Project, |
| 192 | fromId: string, |
| 193 | toId: string, |
| 194 | patch: Konfliktaenderung, |
| 195 | ): Project { |
| 196 | const existing = project.conflicts.some((c) => c.fromId === fromId && c.toId === toId); |
| 197 | if (existing) { |
| 198 | return { |
| 199 | ...project, |
| 200 | conflicts: project.conflicts.map((c) => |
| 201 | c.fromId === fromId && c.toId === toId ? aendereKonflikt(c, patch) : c, |
| 202 | ), |
| 203 | }; |
| 204 | } |
| 205 | return { |
| 206 | ...project, |
| 207 | conflicts: [...project.conflicts, aendereKonflikt(createConflict(fromId, toId), patch)], |
| 208 | }; |
| 209 | } |
| 210 | |
| 211 | /** |
| 212 | * Setzt Raeum- und Einfahrweg fuer mehrere Beziehungen zugleich. |
| 213 | * |
| 214 | * Hilfsmittel fuer den Beginn der Bearbeitung. Die Ansicht hat die Werte |
| 215 | * frueher selbst in die Liste geschrieben - und damit an `setConflict` und dem |
| 216 | * Herkunftsnachweis vorbei. Genau dieser Weg machte aus einer Uebernahme aus |
| 217 | * dem Lageplan stillschweigend eine Massenvorgabe, ueber der im Ausdruck |
| 218 | * weiterhin "Aus dem Lageplan vermessen" stand. |
| 219 | * |
| 220 | * `nurLeere` laesst bereits vermassene Beziehungen unberuehrt - auch ihren |
| 221 | * Nachweis, denn an ihnen aendert sich nichts. |
| 222 | * |
| 223 | * VERMASSEN HEISST: EINER DER BEIDEN WEGE STEHT (Fassung 5.10.0). Geprueft |
| 224 | * wurde bis dahin allein der Raeumweg. Raeum- und Einfahrweg sind in der |
| 225 | * Ansicht "Zwischenzeiten" zwei voneinander unabhaengige Felder; eine Beziehung |
| 226 | * mit eingetragenem Einfahrweg und noch offenem Raeumweg galt deshalb als leer, |
| 227 | * und der Sammelwert ueberschrieb den gemessenen Einfahrweg samt seinem |
| 228 | * Herkunftsnachweis - entgegen der Beschriftung des Kontrollkaestchens ("Nur |
| 229 | * Beziehungen ohne erfasste Wege ändern"). Der Einfahrweg geht mit negativem |
| 230 | * Vorzeichen in die Zwischenzeit ein; gegenueber dem gemessenen Wert faellt sie |
| 231 | * mit dem groesseren Sammelwert zu kurz aus. Bleibt der Raeumweg dabei bei 0 m, |
| 232 | * geht das nicht still durch: Das meldet der Pruefbericht eigens. |
| 233 | */ |
| 234 | export function setzeAlleWege( |
| 235 | project: Project, |
| 236 | raeumweg: number, |
| 237 | einfahrweg: number, |
| 238 | nurLeere: boolean, |
| 239 | ): Project { |
| 240 | return { |
| 241 | ...project, |
| 242 | conflicts: project.conflicts.map((c) => |
| 243 | nurLeere && (c.clearingDistance > 0 || c.enteringDistance > 0) |
| 244 | ? c |
| 245 | : aendereKonflikt(c, { clearingDistance: raeumweg, enteringDistance: einfahrweg }), |
| 246 | ), |
| 247 | }; |
| 248 | } |
| 249 | |
| 250 | export function removeConflict(project: Project, fromId: string, toId: string): Project { |
| 251 | return { |
| 252 | ...project, |
| 253 | conflicts: project.conflicts.filter((c) => !(c.fromId === fromId && c.toId === toId)), |
| 254 | }; |
| 255 | } |
| 256 | |
| 257 | /** Legt eine Konfliktbeziehung in beiden Richtungen an. */ |
| 258 | export function toggleConflictPair(project: Project, aId: string, bId: string): Project { |
| 259 | const exists = project.conflicts.some( |
| 260 | (c) => (c.fromId === aId && c.toId === bId) || (c.fromId === bId && c.toId === aId), |
| 261 | ); |
| 262 | if (exists) { |
| 263 | return { |
| 264 | ...project, |
| 265 | conflicts: project.conflicts.filter( |
| 266 | (c) => !((c.fromId === aId && c.toId === bId) || (c.fromId === bId && c.toId === aId)), |
| 267 | ), |
| 268 | }; |
| 269 | } |
| 270 | return { |
| 271 | ...project, |
| 272 | conflicts: [...project.conflicts, createConflict(aId, bId), createConflict(bId, aId)], |
| 273 | }; |
| 274 | } |
| 275 | |
| 276 | // --- Phasen ----------------------------------------------------------------- |
| 277 | |
| 278 | /** |
| 279 | * Eine Phase anlegen. |
| 280 | * |
| 281 | * Sie wird an JEDE Programmreihenfolge angehaengt, nicht nur an die des |
| 282 | * aktiven (Schema 16): Eine Phase, die in einem Programm nicht vorkommt, |
| 283 | * bekommt dort nie Freigabe - das ist eine Entscheidung des Bearbeiters und |
| 284 | * darf nicht die stille Nebenwirkung davon sein, welches Programm beim |
| 285 | * Anlegen gerade sichtbar war. |
| 286 | */ |
| 287 | export function addPhase(project: Project): Project { |
| 288 | const phase = createPhase(`Phase ${project.phases.length + 1}`); |
| 289 | return { |
| 290 | ...project, |
| 291 | phases: [...project.phases, phase], |
| 292 | program: { ...project.program, phaseOrder: [...project.program.phaseOrder, phase.id] }, |
| 293 | programmbibliothek: project.programmbibliothek.map((satz) => ({ |
| 294 | ...satz, |
| 295 | programm: { ...satz.programm, phaseOrder: [...satz.programm.phaseOrder, phase.id] }, |
| 296 | })), |
| 297 | }; |
| 298 | } |
| 299 | |
| 300 | export function updatePhase(project: Project, id: string, patch: Partial<Phase>): Project { |
| 301 | return { ...project, phases: project.phases.map((p) => (p.id === id ? { ...p, ...patch } : p)) }; |
| 302 | } |
| 303 | |
| 304 | export function removePhase(project: Project, id: string): Project { |
| 305 | return { |
| 306 | ...project, |
| 307 | phases: project.phases.filter((p) => p.id !== id), |
| 308 | program: { ...project.program, phaseOrder: project.program.phaseOrder.filter((p) => p !== id) }, |
| 309 | // Aus jeder Reihenfolge und aus jeder festen Freigabezeit (Schema 16): |
| 310 | // Was auf eine geloeschte Phase verweist, verweist ins Leere. |
| 311 | programmbibliothek: project.programmbibliothek.map((satz) => ({ |
| 312 | ...satz, |
| 313 | programm: { |
| 314 | ...satz.programm, |
| 315 | phaseOrder: satz.programm.phaseOrder.filter((p) => p !== id), |
| 316 | }, |
| 317 | festeFreigabezeiten: satz.festeFreigabezeiten.filter((f) => f.phaseId !== id), |
| 318 | })), |
| 319 | }; |
| 320 | } |
| 321 | |
| 322 | export function movePhase(project: Project, id: string, delta: number): Project { |
| 323 | const order = [...project.program.phaseOrder]; |
| 324 | const index = order.indexOf(id); |
| 325 | if (index === -1) return project; |
| 326 | const target = index + delta; |
| 327 | if (target < 0 || target >= order.length) return project; |
| 328 | const [moved] = order.splice(index, 1); |
| 329 | if (moved !== undefined) order.splice(target, 0, moved); |
| 330 | return { ...project, program: { ...project.program, phaseOrder: order } }; |
| 331 | } |
| 332 | |
| 333 | /** |
| 334 | * Feindliche Signalgruppen, die der Phase bereits zugeordnet sind. |
| 335 | * |
| 336 | * Massgebend ist die erfasste Konfliktliste des Projekts - dieselbe Wahrheit, |
| 337 | * aus der die Zwischenzeitenmatrix entsteht. Bewusst NICHT die Matrixwerte: |
| 338 | * Auch eine feindliche Beziehung mit rechnerischer Zwischenzeit 0 s bleibt |
| 339 | * feindlich und gehoert nicht in eine gemeinsame Phase. |
| 340 | */ |
| 341 | export function feindlichePartnerInPhase( |
| 342 | project: Project, |
| 343 | phaseId: string, |
| 344 | groupId: string, |
| 345 | ): readonly SignalGroup[] { |
| 346 | const phase = project.phases.find((p) => p.id === phaseId); |
| 347 | if (!phase) return []; |
| 348 | const feindlich = new Set<string>(); |
| 349 | for (const conflict of project.conflicts) { |
| 350 | if (conflict.fromId === groupId) feindlich.add(conflict.toId); |
| 351 | if (conflict.toId === groupId) feindlich.add(conflict.fromId); |
| 352 | } |
| 353 | return project.signalGroups.filter( |
| 354 | (g) => g.id !== groupId && phase.signalGroupIds.includes(g.id) && feindlich.has(g.id), |
| 355 | ); |
| 356 | } |
| 357 | |
| 358 | export function togglePhaseGroup(project: Project, phaseId: string, groupId: string): Project { |
| 359 | /* |
| 360 | * KORREKTUR (Befund B10): Die Vertraeglichkeitsansicht verspricht, dass nur |
| 361 | * vertraegliche Gruppen in dieselbe Phase gelangen - bislang fuegte diese |
| 362 | * Funktion jede Gruppe ungeprueft hinzu, und das Versprechen war falsch. |
| 363 | * Jetzt wird die Zuordnung einer feindlichen Gruppe abgelehnt (das Projekt |
| 364 | * bleibt unveraendert); die Oberflaeche meldet den Grund ueber |
| 365 | * feindlichePartnerInPhase. Das Entfernen aus einer Phase bleibt immer |
| 366 | * moeglich, auch bei aus Altdaten geladenen Konfliktpaaren. |
| 367 | */ |
| 368 | const phase = project.phases.find((p) => p.id === phaseId); |
| 369 | const wirdEntfernt = phase?.signalGroupIds.includes(groupId) ?? false; |
| 370 | if (!wirdEntfernt && feindlichePartnerInPhase(project, phaseId, groupId).length > 0) { |
| 371 | return project; |
| 372 | } |
| 373 | return { |
| 374 | ...project, |
| 375 | phases: project.phases.map((p) => { |
| 376 | if (p.id !== phaseId) return p; |
| 377 | const has = p.signalGroupIds.includes(groupId); |
| 378 | return { |
| 379 | ...p, |
| 380 | signalGroupIds: has |
| 381 | ? p.signalGroupIds.filter((id) => id !== groupId) |
| 382 | : [...p.signalGroupIds, groupId], |
| 383 | }; |
| 384 | }), |
| 385 | }; |
| 386 | } |
| 387 | |
| 388 | /** |
| 389 | * Ersetzt die Phaseneinteilung durch eine vorgeschlagene. |
| 390 | * |
| 391 | * WAS DABEI FORTFAELLT, und es steht in der Rueckfrage der Oberflaeche: die |
| 392 | * bisherigen Phasen mit ihren Namen und ihren von Hand vorgegebenen |
| 393 | * Freigabezeiten - in JEDEM Tagesprogramm. Sie haengen an Phasenkennungen, und |
| 394 | * die neuen Phasen sind neue Phasen. Ein stehengelassener Eintrag zeigte auf |
| 395 | * eine Phase, die es nicht mehr gibt. |
| 396 | * |
| 397 | * Die Reihenfolge ist die des Vorschlags. Sie geht in die Uebergangszeiten |
| 398 | * ein: Eine andere Reihenfolge derselben Phasen ergibt eine andere Umlaufzeit, |
| 399 | * und die sucht dieses Programm nicht ab. |
| 400 | */ |
| 401 | export function setzePhasen( |
| 402 | project: Project, |
| 403 | gruppenJePhase: readonly (readonly string[])[], |
| 404 | ): Project { |
| 405 | const phasen = gruppenJePhase.map((gruppen, index) => |
| 406 | createPhase(`Phase ${String(index + 1)}`, [...gruppen]), |
| 407 | ); |
| 408 | return { |
| 409 | ...project, |
| 410 | phases: phasen, |
| 411 | program: { ...project.program, phaseOrder: phasen.map((p) => p.id) }, |
| 412 | programmbibliothek: project.programmbibliothek.map((satz) => ({ |
| 413 | ...satz, |
| 414 | programm: { ...satz.programm, phaseOrder: phasen.map((p) => p.id) }, |
| 415 | festeFreigabezeiten: [], |
| 416 | })), |
| 417 | }; |
| 418 | } |
| 419 | |
| 420 | // --- Koordinierung ---------------------------------------------------------- |
| 421 | |
| 422 | /** |
| 423 | * Die Kette eines Projekts, das noch keine hat. |
| 424 | * |
| 425 | * Sie beginnt LEER und nicht mit einem erfundenen Nachbarn: Ein Knotenpunkt, |
| 426 | * den niemand eingetragen hat, waere eine Behauptung ueber die Oertlichkeit. |
| 427 | */ |
| 428 | const LEERE_KETTE: Koordinierungskette = { |
| 429 | progressionsgeschwindigkeit: null, |
| 430 | hauptrichtung: null, |
| 431 | gegenrichtung: null, |
| 432 | eigenerAbstand: 0, |
| 433 | nachbarn: [], |
| 434 | platz: 0, |
| 435 | }; |
| 436 | |
| 437 | /** Aendert die Kette; legt sie an, wo es noch keine gibt. */ |
| 438 | export function setzeKoordinierung(project: Project, patch: Partial<Koordinierungskette>): Project { |
| 439 | const bisher = project.koordinierung ?? LEERE_KETTE; |
| 440 | return { ...project, koordinierung: { ...bisher, ...patch } }; |
| 441 | } |
| 442 | |
| 443 | /** Entfernt die Koordinierung ganz - der Knotenpunkt steht dann wieder allein. */ |
| 444 | export function entferneKoordinierung(project: Project): Project { |
| 445 | return { ...project, koordinierung: null }; |
| 446 | } |
| 447 | |
| 448 | /** Legt eine Nachbaranlage am Ende der Kette an. */ |
| 449 | export function koordinierungsknotenAnlegen(project: Project, name: string): Project { |
| 450 | const bisher = project.koordinierung ?? LEERE_KETTE; |
| 451 | const knoten: Koordinierungsknoten = { |
| 452 | id: createId('knoten'), |
| 453 | name, |
| 454 | abstand: 0, |
| 455 | freigabeBeginn: 0, |
| 456 | freigabeDauer: 0, |
| 457 | }; |
| 458 | return setzeKoordinierung(project, { nachbarn: [...bisher.nachbarn, knoten] }); |
| 459 | } |
| 460 | |
| 461 | /** Aendert eine Nachbaranlage. */ |
| 462 | export function koordinierungsknotenAendern( |
| 463 | project: Project, |
| 464 | id: string, |
| 465 | patch: Partial<Koordinierungsknoten>, |
| 466 | ): Project { |
| 467 | const bisher = project.koordinierung; |
| 468 | if (bisher === null) return project; |
| 469 | return setzeKoordinierung(project, { |
| 470 | nachbarn: bisher.nachbarn.map((n) => (n.id === id ? { ...n, ...patch } : n)), |
| 471 | }); |
| 472 | } |
| 473 | |
| 474 | /** |
| 475 | * Entfernt eine Nachbaranlage. |
| 476 | * |
| 477 | * Der eigene Platz wird mitgezogen: Faellt ein Knotenpunkt VOR diesem fort, |
| 478 | * rueckt dieser um eine Stelle nach vorn. Ohne das Nachziehen stuende er |
| 479 | * danach an einer anderen Stelle der Kette, ohne dass jemand ihn verschoben |
| 480 | * haette - und die Abstaende bezoegen sich auf einen anderen Nachbarn. |
| 481 | */ |
| 482 | export function koordinierungsknotenEntfernen(project: Project, id: string): Project { |
| 483 | const bisher = project.koordinierung; |
| 484 | if (bisher === null) return project; |
| 485 | const index = bisher.nachbarn.findIndex((n) => n.id === id); |
| 486 | if (index === -1) return project; |
| 487 | const platz = index < bisher.platz ? Math.max(0, bisher.platz - 1) : bisher.platz; |
| 488 | return setzeKoordinierung(project, { |
| 489 | nachbarn: bisher.nachbarn.filter((n) => n.id !== id), |
| 490 | platz, |
| 491 | }); |
| 492 | } |
| 493 | |
| 494 | // --- Tagesprogramme --------------------------------------------------------- |
| 495 | |
| 496 | /* |
| 497 | * Die drei Aktionen der Programmbibliothek reichen an den Fachkern durch. Sie |
| 498 | * stehen hier, weil die Oberflaeche ihre Aenderungen ausschliesslich ueber |
| 499 | * `actions` fuehrt - und nicht, weil sie etwas hinzufuegten. |
| 500 | */ |
| 501 | |
| 502 | /** Auf ein anderes Tagesprogramm umschalten. */ |
| 503 | export function programmWechseln(project: Project, programmId: string): Project { |
| 504 | return wechsleProgramm(project, programmId); |
| 505 | } |
| 506 | |
| 507 | /** Ein weiteres Tagesprogramm anlegen und darauf umschalten. */ |
| 508 | export function programmAnlegen(project: Project, name: string): Project { |
| 509 | return legeProgrammAn(project, name); |
| 510 | } |
| 511 | |
| 512 | /** Ein Tagesprogramm entfernen; das letzte bleibt stehen. */ |
| 513 | export function programmEntfernen(project: Project, programmId: string): Project { |
| 514 | return loescheProgramm(project, programmId); |
| 515 | } |
| 516 | |
| 517 | // --- Lageplan --------------------------------------------------------------- |
| 518 | |
| 519 | export function setLageplan(project: Project, lageplan: Lageplan): Project { |
| 520 | // Einziger Trichter: Alle Schreibstellen der Oberflaeche laufen hierueber. |
| 521 | // `normalisiere` zieht den ersten Punkt jeder zugeordneten Fahrlinie auf ihre |
| 522 | // Haltlinie - eine verschobene Haltlinie nimmt dadurch alle ihre Stroeme mit. |
| 523 | return { ...project, lageplan: normalisiere(lageplan) }; |
| 524 | } |
| 525 | |
| 526 | // --- Haltlinien ------------------------------------------------------------- |
| 527 | |
| 528 | export function addHaltlinie(project: Project, haltlinie: Haltlinie): Project { |
| 529 | return setLageplan(project, { |
| 530 | ...project.lageplan, |
| 531 | haltlinien: [...project.lageplan.haltlinien, haltlinie], |
| 532 | }); |
| 533 | } |
| 534 | |
| 535 | export function setHaltlinie(project: Project, id: string, patch: Partial<Haltlinie>): Project { |
| 536 | return setLageplan(project, { |
| 537 | ...project.lageplan, |
| 538 | haltlinien: project.lageplan.haltlinien.map((h) => (h.id === id ? { ...h, ...patch } : h)), |
| 539 | }); |
| 540 | } |
| 541 | |
| 542 | /** |
| 543 | * Loescht eine Haltlinie. |
| 544 | * |
| 545 | * Ihre Stroeme bleiben erhalten und behalten ihren gezeichneten Verlauf |
| 546 | * vollstaendig - `punkte` fuehrt den ganzen Linienzug. Sie sind danach nur |
| 547 | * keiner Zufahrt mehr zugeordnet; fuer sie entscheidet dann wieder allein die |
| 548 | * Zeichnung. Die Oberflaeche fragt vorher zurueck, weil sich damit die |
| 549 | * Vertraeglichkeit dieser Stroeme untereinander aendert. |
| 550 | */ |
| 551 | export function removeHaltlinie(project: Project, id: string): Project { |
| 552 | return setLageplan(project, { |
| 553 | ...project.lageplan, |
| 554 | haltlinien: project.lageplan.haltlinien.filter((h) => h.id !== id), |
| 555 | linien: project.lageplan.linien.map((l) => |
| 556 | l.haltlinieId === id ? { ...l, haltlinieId: null } : l, |
| 557 | ), |
| 558 | }); |
| 559 | } |
| 560 | |
| 561 | export function setPlanlinie(project: Project, id: string, patch: Partial<Planlinie>): Project { |
| 562 | return setLageplan(project, { |
| 563 | ...project.lageplan, |
| 564 | linien: project.lageplan.linien.map((l) => (l.id === id ? { ...l, ...patch } : l)), |
| 565 | }); |
| 566 | } |
| 567 | |
| 568 | export function addPlanlinie(project: Project, linie: Planlinie): Project { |
| 569 | return setLageplan(project, { |
| 570 | ...project.lageplan, |
| 571 | linien: [...project.lageplan.linien, linie], |
| 572 | }); |
| 573 | } |
| 574 | |
| 575 | /** |
| 576 | * Loescht eine gezeichnete Linie. |
| 577 | * Die verknuepfte Signalgruppe bleibt bestehen - sie kann bereits in Phasen |
| 578 | * verwendet sein. Geloescht wird nur die Zeichnung. |
| 579 | */ |
| 580 | export function removePlanlinie(project: Project, id: string): Project { |
| 581 | return setLageplan(project, { |
| 582 | ...project.lageplan, |
| 583 | linien: project.lageplan.linien.filter((l) => l.id !== id), |
| 584 | }); |
| 585 | } |
| 586 | |
| 587 | // --- Arbeitsbereiche -------------------------------------------------------- |
| 588 | |
| 589 | /* |
| 590 | * Die drei Aktionen des Arbeitsbereichs - dieselbe Bauform wie bei der |
| 591 | * Fahrlinie und ueber denselben Trichter `setLageplan`. |
| 592 | * |
| 593 | * KEIN AUFRAEUMEN IN FREMDEN LISTEN, und das ist keine Nachlaessigkeit: Auf |
| 594 | * einen Arbeitsbereich verweist nichts. Er traegt weder Signalgruppe noch |
| 595 | * Zufahrt, und deshalb beruehrt ihn weder `removeSignalGroup` noch |
| 596 | * `removeArm`. Bekaeme er einmal einen Verweis, gehoerte er in beide. |
| 597 | */ |
| 598 | export function addArbeitsbereich(project: Project, bereich: Arbeitsbereich): Project { |
| 599 | return setLageplan(project, { |
| 600 | ...project.lageplan, |
| 601 | arbeitsbereiche: [...project.lageplan.arbeitsbereiche, bereich], |
| 602 | }); |
| 603 | } |
| 604 | |
| 605 | export function setArbeitsbereich( |
| 606 | project: Project, |
| 607 | id: string, |
| 608 | patch: Partial<Arbeitsbereich>, |
| 609 | ): Project { |
| 610 | return setLageplan(project, { |
| 611 | ...project.lageplan, |
| 612 | arbeitsbereiche: project.lageplan.arbeitsbereiche.map((a) => |
| 613 | a.id === id ? { ...a, ...patch } : a, |
| 614 | ), |
| 615 | }); |
| 616 | } |
| 617 | |
| 618 | export function removeArbeitsbereich(project: Project, id: string): Project { |
| 619 | return setLageplan(project, { |
| 620 | ...project.lageplan, |
| 621 | arbeitsbereiche: project.lageplan.arbeitsbereiche.filter((a) => a.id !== id), |
| 622 | }); |
| 623 | } |
| 624 | |
| 625 | /** |
| 626 | * Die drei Aktionen des Signalgebers - dieselbe Bauform, ein Unterschied. |
| 627 | * |
| 628 | * ER TRAEGT EINEN VERWEIS, und deshalb gilt hier nicht, was beim |
| 629 | * Arbeitsbereich gilt: Wird eine Signalgruppe geloescht, muss der Verweis |
| 630 | * fallen, sonst zeigt er ins Leere. Das erledigt `removeSignalGroup` weiter |
| 631 | * unten mit - an einer Stelle und nicht hier, damit kein zweiter Weg entsteht. |
| 632 | */ |
| 633 | export function addSignalgeber(project: Project, geber: Signalgeber): Project { |
| 634 | return setLageplan(project, { |
| 635 | ...project.lageplan, |
| 636 | signalgeber: [...project.lageplan.signalgeber, geber], |
| 637 | }); |
| 638 | } |
| 639 | |
| 640 | export function setSignalgeber(project: Project, id: string, patch: Partial<Signalgeber>): Project { |
| 641 | return setLageplan(project, { |
| 642 | ...project.lageplan, |
| 643 | signalgeber: project.lageplan.signalgeber.map((g) => (g.id === id ? { ...g, ...patch } : g)), |
| 644 | }); |
| 645 | } |
| 646 | |
| 647 | export function removeSignalgeber(project: Project, id: string): Project { |
| 648 | return setLageplan(project, { |
| 649 | ...project.lageplan, |
| 650 | signalgeber: project.lageplan.signalgeber.filter((g) => g.id !== id), |
| 651 | }); |
| 652 | } |
| 653 | |
| 654 | // --- Verkehrsstaerken ------------------------------------------------------- |
| 655 | |
| 656 | export function setDemand( |
| 657 | project: Project, |
| 658 | signalGroupId: string, |
| 659 | patch: Partial<Omit<TrafficDemand, 'signalGroupId'>>, |
| 660 | ): Project { |
| 661 | // Verkehrsstaerken sind ganzzahlig (Fahrzeuge je Stunde): Nach HBS 2015 |
| 662 | // haengt die Stufe F an q > C, und beide Zahlen stehen ganzzahlig im |
| 663 | // Ausdruck; eine Verkehrsstaerke 243,4 neben einer Kapazitaet 243 waere |
| 664 | // dort nicht nachvollziehbar (Befund B1). |
| 665 | const bereinigt: Partial<Omit<TrafficDemand, 'signalGroupId'>> = |
| 666 | patch.volume === undefined || !Number.isFinite(patch.volume) |
| 667 | ? patch |
| 668 | : { ...patch, volume: Math.round(patch.volume) }; |
| 669 | const existing = project.demands.find((d) => d.signalGroupId === signalGroupId); |
| 670 | if (existing) { |
| 671 | return { |
| 672 | ...project, |
| 673 | demands: project.demands.map((d) => |
| 674 | d.signalGroupId === signalGroupId ? { ...d, ...bereinigt } : d, |
| 675 | ), |
| 676 | }; |
| 677 | } |
| 678 | return { |
| 679 | ...project, |
| 680 | demands: [...project.demands, { signalGroupId, volume: 0, heavyVehicleShare: 0, ...bereinigt }], |
| 681 | }; |
| 682 | } |
| 683 | |
| 684 | export function removeDemand(project: Project, signalGroupId: string): Project { |
| 685 | return { ...project, demands: project.demands.filter((d) => d.signalGroupId !== signalGroupId) }; |
| 686 | } |
| 687 | |
| 688 | /** |
| 689 | * Der Lastzuganteil am Schwerverkehr - eintragen ODER entfernen. |
| 690 | * |
| 691 | * NEU (Schema 13). Warum das eine eigene Aktion braucht und nicht |
| 692 | * ueber `setDemand` laeuft: `lastzugAnteil` ist ein OPTIONALES Feld, und der |
| 693 | * Unterschied zwischen "0 % Lastzuege" und "nicht erfasst" ist genau der |
| 694 | * Unterschied zwischen HBS-Gleichung 2-5 und 2-6. Ueber `setDemand` liesse er |
| 695 | * sich nicht ausdruecken: `{ lastzugAnteil: undefined }` legt beim Ausbreiten |
| 696 | * einen Schluessel mit dem Wert `undefined` an, das Feld gilt danach als |
| 697 | * gesetzt-aber-leer, und die Unterscheidung haengt daran, welche Stelle als |
| 698 | * naechstes darauf sieht. |
| 699 | * |
| 700 | * `null` heisst deshalb hier ausdruecklich "nicht erfasst" und ENTFERNT das |
| 701 | * Feld. Das ist der Rueckweg zum Pauschalwert 1,90 - und es ist der Weg, der |
| 702 | * gebraucht wird, wenn sich herausstellt, dass die Zaehlung die Aufteilung |
| 703 | * nicht hergibt. |
| 704 | * |
| 705 | * NICHT BEGRENZT WIRD HIER: Ein Wert ausserhalb von 0 bis 1 bleibt stehen und |
| 706 | * wird vom Signalzeitenplan als unbrauchbar gemeldet |
| 707 | * (`lastzuganteil-ausserhalb`), statt stillschweigend auf 0 oder 1 gezogen zu |
| 708 | * werden. Eine stille Ersetzung an dieser Stelle ergaebe die guenstigste |
| 709 | * Aufteilung aus einer Fehleingabe. |
| 710 | */ |
| 711 | export function setLastzugAnteil( |
| 712 | project: Project, |
| 713 | signalGroupId: string, |
| 714 | anteil: number | null, |
| 715 | ): Project { |
| 716 | const vorhanden = project.demands.find((d) => d.signalGroupId === signalGroupId); |
| 717 | const gesetzt = (demand: TrafficDemand): TrafficDemand => { |
| 718 | if (anteil === null) { |
| 719 | const { lastzugAnteil: _entfaellt, ...ohne } = demand; |
| 720 | return ohne; |
| 721 | } |
| 722 | return { ...demand, lastzugAnteil: anteil }; |
| 723 | }; |
| 724 | if (!vorhanden) { |
| 725 | // Ohne Verkehrsstaerke gibt es nichts aufzuteilen; ein Eintrag entstuende |
| 726 | // dann mit volume 0, und der Plan rechnete fuer diesen Strom ohnehin |
| 727 | // keine Auslastung. Erst die Verkehrsstaerke, dann ihre Aufteilung. |
| 728 | if (anteil === null) return project; |
| 729 | return { |
| 730 | ...project, |
| 731 | demands: [...project.demands, gesetzt({ signalGroupId, volume: 0, heavyVehicleShare: 0 })], |
| 732 | }; |
| 733 | } |
| 734 | return { |
| 735 | ...project, |
| 736 | demands: project.demands.map((d) => (d.signalGroupId === signalGroupId ? gesetzt(d) : d)), |
| 737 | }; |
| 738 | } |