import { createArm, createConflict, createPhase, createSignalGroup } from '@/domain/model/factory'; import { aendereKonflikt } from '@/domain/model/project'; import { legeProgrammAn, loescheProgramm, wechsleProgramm } from '@/domain/model/programme'; import type { IntersectionArm, Konfliktaenderung, Koordinierungskette, Koordinierungsknoten, Phase, Project, SignalGroup, TrafficDemand, } from '@/domain/model/project'; import { createId } from '@/domain/id'; import type { TrafficMode } from '@/domain/rilsa/types'; import { normalisiere, type Arbeitsbereich, type Signalgeber, type Haltlinie, type Lageplan, type Planlinie, } from '@/domain/geometrie/lageplan'; /** * Aenderungen am Projekt. * * Alle Funktionen sind rein: sie geben ein neues Projekt zurueck und aendern das * uebergebene nicht. Genau daran scheiterte im Altbestand das Rueckgaengig- * machen: dort wurden Signalgruppen mit `splice` an Ort und Stelle geaendert, * sodass der zuvor abgelegte Wiederherstellungspunkt dieselbe Liste * referenzierte und sich mitveraenderte. */ // --- Projektdaten ----------------------------------------------------------- export function setMeta(project: Project, patch: Partial): Project { return { ...project, meta: { ...project.meta, ...patch } }; } export function setIntersection( project: Project, patch: Partial, ): Project { return { ...project, intersection: { ...project.intersection, ...patch } }; } export function setProgram(project: Project, patch: Partial): Project { return { ...project, program: { ...project.program, ...patch } }; } export function setSettings(project: Project, patch: Partial): Project { return { ...project, settings: { ...project.settings, ...patch } }; } // --- Zufahrten -------------------------------------------------------------- export function addArm(project: Project, name: string, direction: string): Project { const arm = createArm(name, direction, 1, project.intersection.vZul); return setIntersection(project, { arms: [...project.intersection.arms, arm] }); } export function updateArm(project: Project, id: string, patch: Partial): Project { return setIntersection(project, { arms: project.intersection.arms.map((a) => (a.id === id ? { ...a, ...patch } : a)), }); } export function removeArm(project: Project, id: string): Project { return { ...setIntersection(project, { arms: project.intersection.arms.filter((a) => a.id !== id) }), // Zuordnungen aufloesen, statt sie ins Leere zeigen zu lassen. signalGroups: project.signalGroups.map((g) => (g.armId === id ? { ...g, armId: null } : g)), // Auch die Haltlinien: Sonst haengt die Zufahrtszugehoerigkeit der ganzen // Familie an einem toten Verweis, und aufgeraeumt wuerde erst beim naechsten // Einlesen - also erst nach dem Speichern. lageplan: { ...project.lageplan, haltlinien: project.lageplan.haltlinien.map((h) => h.armId === id ? { ...h, armId: null } : h, ), }, }; } // --- Signalgruppen ---------------------------------------------------------- export function addSignalGroup(project: Project, mode: TrafficMode): Project { const prefix = mode === 'fuss' ? 'F' : mode === 'rad' ? 'R' : mode === 'oepnv' ? 'B' : 'K'; const used = new Set(project.signalGroups.map((g) => g.name)); let index = 1; while (used.has(`${prefix}${index}`)) index += 1; const group = createSignalGroup({ name: `${prefix}${index}`, mode, vZul: project.intersection.vZul, index: project.signalGroups.length, }); return { ...project, signalGroups: [...project.signalGroups, group] }; } export function updateSignalGroup( project: Project, id: string, patch: Partial, ): Project { return { ...project, signalGroups: project.signalGroups.map((g) => (g.id === id ? { ...g, ...patch } : g)), }; } /** * Loescht eine Signalgruppe samt aller Verweise. * Der Altbestand entfernte nur den Eintrag; Konfliktmatrix und Phasen * verwiesen danach auf eine nicht mehr vorhandene Gruppe, und weil die Matrix * ueber Feldindizes gefuehrt wurde, verschoben sich alle folgenden Zuordnungen. */ export function removeSignalGroup(project: Project, id: string): Project { return { ...project, signalGroups: project.signalGroups.filter((g) => g.id !== id), conflicts: project.conflicts.filter((c) => c.fromId !== id && c.toId !== id), phases: project.phases.map((p) => ({ ...p, signalGroupIds: p.signalGroupIds.filter((sgId) => sgId !== id), })), demands: project.demands.filter((d) => d.signalGroupId !== id), /* * UND DIE VERKEHRSSTAERKEN DER UEBRIGEN TAGESPROGRAMME (Schema 16). Sonst * fuehrt das Nachtprogramm nach dem Loeschen eine Verkehrsstaerke fuer * einen Strom, den es nicht mehr gibt; beim naechsten Umschalten stuende * sie wieder im Projekt, und der Pruefbericht meldete eine Zuordnung ins * Leere - genau der Fehler, an dem der Altbestand mit der Konfliktmatrix * scheiterte. */ programmbibliothek: project.programmbibliothek.map((satz) => ({ ...satz, demands: satz.demands.filter((d) => d.signalGroupId !== id), })), // Neu: Sonst zeigt die gezeichnete Linie nach dem Loeschen ins Leere, und // die Ableitung vergliche gegen eine Gruppe, die es nicht mehr gibt. // Der Signalgeber (Schema 19) geht denselben Weg: Sein Standort bleibt - // er ist die Aussage -, die Verknuepfung faellt. lageplan: { ...project.lageplan, linien: project.lageplan.linien.map((l) => l.signalGroupId === id ? { ...l, signalGroupId: null } : l, ), signalgeber: project.lageplan.signalgeber.map((g) => g.signalGroupId === id ? { ...g, signalGroupId: null } : g, ), }, }; } export function moveSignalGroup(project: Project, id: string, delta: number): Project { const index = project.signalGroups.findIndex((g) => g.id === id); if (index === -1) return project; const target = index + delta; if (target < 0 || target >= project.signalGroups.length) return project; const next = [...project.signalGroups]; const [moved] = next.splice(index, 1); if (moved) next.splice(target, 0, moved); return { ...project, signalGroups: next }; } // --- Konfliktbeziehungen ---------------------------------------------------- /** * Aendert eine Konfliktbeziehung; legt sie an, wenn es sie noch nicht gibt. * * Geschrieben wird ausschliesslich ueber `aendereKonflikt`. Dort haengt der * Herkunftsnachweis der Wege dran, und dort gehoert er auch hin: Wuerde jede * Aufrufstelle selbst mischen, muesste an einem Dutzend Stellen jemand daran * denken, den Nachweis zurueckzusetzen - und beim naechsten neuen Feld wieder. * Vergessen hiesse: Der Ausdruck weist eine Vermessung aus, die es fuer diese * Zahl nie gegeben hat. * * EIN SCHLUESSEL FUER SUCHEN UND SCHREIBEN: Nachgeschlagen wird ueber das Paar * (fromId, toId) - geschrieben wurde ueber `c.id === existing.id`. Zwei * Schluessel fuer dieselbe Entscheidung: Trifft die Kennung ein zweites Mal zu, * schreibt die Funktion an eine Beziehung, die sie nie nachgeschlagen hat, und * entwertet dort auch noch den Herkunftsnachweis. Die Neuvergabe beim Einlesen * entschaerft nur, was durch `parseProject` laeuft. Geschrieben wird * jetzt ueber denselben Schluessel, ueber den gesucht wird - so wie * `removeConflict` es schon tut. */ export function setConflict( project: Project, fromId: string, toId: string, patch: Konfliktaenderung, ): Project { const existing = project.conflicts.some((c) => c.fromId === fromId && c.toId === toId); if (existing) { return { ...project, conflicts: project.conflicts.map((c) => c.fromId === fromId && c.toId === toId ? aendereKonflikt(c, patch) : c, ), }; } return { ...project, conflicts: [...project.conflicts, aendereKonflikt(createConflict(fromId, toId), patch)], }; } /** * Setzt Raeum- und Einfahrweg fuer mehrere Beziehungen zugleich. * * Hilfsmittel fuer den Beginn der Bearbeitung. Die Ansicht hat die Werte * frueher selbst in die Liste geschrieben - und damit an `setConflict` und dem * Herkunftsnachweis vorbei. Genau dieser Weg machte aus einer Uebernahme aus * dem Lageplan stillschweigend eine Massenvorgabe, ueber der im Ausdruck * weiterhin "Aus dem Lageplan vermessen" stand. * * `nurLeere` laesst bereits vermassene Beziehungen unberuehrt - auch ihren * Nachweis, denn an ihnen aendert sich nichts. * * VERMASSEN HEISST: EINER DER BEIDEN WEGE STEHT (Fassung 5.10.0). Geprueft * wurde bis dahin allein der Raeumweg. Raeum- und Einfahrweg sind in der * Ansicht "Zwischenzeiten" zwei voneinander unabhaengige Felder; eine Beziehung * mit eingetragenem Einfahrweg und noch offenem Raeumweg galt deshalb als leer, * und der Sammelwert ueberschrieb den gemessenen Einfahrweg samt seinem * Herkunftsnachweis - entgegen der Beschriftung des Kontrollkaestchens ("Nur * Beziehungen ohne erfasste Wege ändern"). Der Einfahrweg geht mit negativem * Vorzeichen in die Zwischenzeit ein; gegenueber dem gemessenen Wert faellt sie * mit dem groesseren Sammelwert zu kurz aus. Bleibt der Raeumweg dabei bei 0 m, * geht das nicht still durch: Das meldet der Pruefbericht eigens. */ export function setzeAlleWege( project: Project, raeumweg: number, einfahrweg: number, nurLeere: boolean, ): Project { return { ...project, conflicts: project.conflicts.map((c) => nurLeere && (c.clearingDistance > 0 || c.enteringDistance > 0) ? c : aendereKonflikt(c, { clearingDistance: raeumweg, enteringDistance: einfahrweg }), ), }; } export function removeConflict(project: Project, fromId: string, toId: string): Project { return { ...project, conflicts: project.conflicts.filter((c) => !(c.fromId === fromId && c.toId === toId)), }; } /** Legt eine Konfliktbeziehung in beiden Richtungen an. */ export function toggleConflictPair(project: Project, aId: string, bId: string): Project { const exists = project.conflicts.some( (c) => (c.fromId === aId && c.toId === bId) || (c.fromId === bId && c.toId === aId), ); if (exists) { return { ...project, conflicts: project.conflicts.filter( (c) => !((c.fromId === aId && c.toId === bId) || (c.fromId === bId && c.toId === aId)), ), }; } return { ...project, conflicts: [...project.conflicts, createConflict(aId, bId), createConflict(bId, aId)], }; } // --- Phasen ----------------------------------------------------------------- /** * Eine Phase anlegen. * * Sie wird an JEDE Programmreihenfolge angehaengt, nicht nur an die des * aktiven (Schema 16): Eine Phase, die in einem Programm nicht vorkommt, * bekommt dort nie Freigabe - das ist eine Entscheidung des Bearbeiters und * darf nicht die stille Nebenwirkung davon sein, welches Programm beim * Anlegen gerade sichtbar war. */ export function addPhase(project: Project): Project { const phase = createPhase(`Phase ${project.phases.length + 1}`); return { ...project, phases: [...project.phases, phase], program: { ...project.program, phaseOrder: [...project.program.phaseOrder, phase.id] }, programmbibliothek: project.programmbibliothek.map((satz) => ({ ...satz, programm: { ...satz.programm, phaseOrder: [...satz.programm.phaseOrder, phase.id] }, })), }; } export function updatePhase(project: Project, id: string, patch: Partial): Project { return { ...project, phases: project.phases.map((p) => (p.id === id ? { ...p, ...patch } : p)) }; } export function removePhase(project: Project, id: string): Project { return { ...project, phases: project.phases.filter((p) => p.id !== id), program: { ...project.program, phaseOrder: project.program.phaseOrder.filter((p) => p !== id) }, // Aus jeder Reihenfolge und aus jeder festen Freigabezeit (Schema 16): // Was auf eine geloeschte Phase verweist, verweist ins Leere. programmbibliothek: project.programmbibliothek.map((satz) => ({ ...satz, programm: { ...satz.programm, phaseOrder: satz.programm.phaseOrder.filter((p) => p !== id), }, festeFreigabezeiten: satz.festeFreigabezeiten.filter((f) => f.phaseId !== id), })), }; } export function movePhase(project: Project, id: string, delta: number): Project { const order = [...project.program.phaseOrder]; const index = order.indexOf(id); if (index === -1) return project; const target = index + delta; if (target < 0 || target >= order.length) return project; const [moved] = order.splice(index, 1); if (moved !== undefined) order.splice(target, 0, moved); return { ...project, program: { ...project.program, phaseOrder: order } }; } /** * Feindliche Signalgruppen, die der Phase bereits zugeordnet sind. * * Massgebend ist die erfasste Konfliktliste des Projekts - dieselbe Wahrheit, * aus der die Zwischenzeitenmatrix entsteht. Bewusst NICHT die Matrixwerte: * Auch eine feindliche Beziehung mit rechnerischer Zwischenzeit 0 s bleibt * feindlich und gehoert nicht in eine gemeinsame Phase. */ export function feindlichePartnerInPhase( project: Project, phaseId: string, groupId: string, ): readonly SignalGroup[] { const phase = project.phases.find((p) => p.id === phaseId); if (!phase) return []; const feindlich = new Set(); for (const conflict of project.conflicts) { if (conflict.fromId === groupId) feindlich.add(conflict.toId); if (conflict.toId === groupId) feindlich.add(conflict.fromId); } return project.signalGroups.filter( (g) => g.id !== groupId && phase.signalGroupIds.includes(g.id) && feindlich.has(g.id), ); } export function togglePhaseGroup(project: Project, phaseId: string, groupId: string): Project { /* * KORREKTUR (Befund B10): Die Vertraeglichkeitsansicht verspricht, dass nur * vertraegliche Gruppen in dieselbe Phase gelangen - bislang fuegte diese * Funktion jede Gruppe ungeprueft hinzu, und das Versprechen war falsch. * Jetzt wird die Zuordnung einer feindlichen Gruppe abgelehnt (das Projekt * bleibt unveraendert); die Oberflaeche meldet den Grund ueber * feindlichePartnerInPhase. Das Entfernen aus einer Phase bleibt immer * moeglich, auch bei aus Altdaten geladenen Konfliktpaaren. */ const phase = project.phases.find((p) => p.id === phaseId); const wirdEntfernt = phase?.signalGroupIds.includes(groupId) ?? false; if (!wirdEntfernt && feindlichePartnerInPhase(project, phaseId, groupId).length > 0) { return project; } return { ...project, phases: project.phases.map((p) => { if (p.id !== phaseId) return p; const has = p.signalGroupIds.includes(groupId); return { ...p, signalGroupIds: has ? p.signalGroupIds.filter((id) => id !== groupId) : [...p.signalGroupIds, groupId], }; }), }; } /** * Ersetzt die Phaseneinteilung durch eine vorgeschlagene. * * WAS DABEI FORTFAELLT, und es steht in der Rueckfrage der Oberflaeche: die * bisherigen Phasen mit ihren Namen und ihren von Hand vorgegebenen * Freigabezeiten - in JEDEM Tagesprogramm. Sie haengen an Phasenkennungen, und * die neuen Phasen sind neue Phasen. Ein stehengelassener Eintrag zeigte auf * eine Phase, die es nicht mehr gibt. * * Die Reihenfolge ist die des Vorschlags. Sie geht in die Uebergangszeiten * ein: Eine andere Reihenfolge derselben Phasen ergibt eine andere Umlaufzeit, * und die sucht dieses Programm nicht ab. */ export function setzePhasen( project: Project, gruppenJePhase: readonly (readonly string[])[], ): Project { const phasen = gruppenJePhase.map((gruppen, index) => createPhase(`Phase ${String(index + 1)}`, [...gruppen]), ); return { ...project, phases: phasen, program: { ...project.program, phaseOrder: phasen.map((p) => p.id) }, programmbibliothek: project.programmbibliothek.map((satz) => ({ ...satz, programm: { ...satz.programm, phaseOrder: phasen.map((p) => p.id) }, festeFreigabezeiten: [], })), }; } // --- Koordinierung ---------------------------------------------------------- /** * Die Kette eines Projekts, das noch keine hat. * * Sie beginnt LEER und nicht mit einem erfundenen Nachbarn: Ein Knotenpunkt, * den niemand eingetragen hat, waere eine Behauptung ueber die Oertlichkeit. */ const LEERE_KETTE: Koordinierungskette = { progressionsgeschwindigkeit: null, hauptrichtung: null, gegenrichtung: null, eigenerAbstand: 0, nachbarn: [], platz: 0, }; /** Aendert die Kette; legt sie an, wo es noch keine gibt. */ export function setzeKoordinierung(project: Project, patch: Partial): Project { const bisher = project.koordinierung ?? LEERE_KETTE; return { ...project, koordinierung: { ...bisher, ...patch } }; } /** Entfernt die Koordinierung ganz - der Knotenpunkt steht dann wieder allein. */ export function entferneKoordinierung(project: Project): Project { return { ...project, koordinierung: null }; } /** Legt eine Nachbaranlage am Ende der Kette an. */ export function koordinierungsknotenAnlegen(project: Project, name: string): Project { const bisher = project.koordinierung ?? LEERE_KETTE; const knoten: Koordinierungsknoten = { id: createId('knoten'), name, abstand: 0, freigabeBeginn: 0, freigabeDauer: 0, }; return setzeKoordinierung(project, { nachbarn: [...bisher.nachbarn, knoten] }); } /** Aendert eine Nachbaranlage. */ export function koordinierungsknotenAendern( project: Project, id: string, patch: Partial, ): Project { const bisher = project.koordinierung; if (bisher === null) return project; return setzeKoordinierung(project, { nachbarn: bisher.nachbarn.map((n) => (n.id === id ? { ...n, ...patch } : n)), }); } /** * Entfernt eine Nachbaranlage. * * Der eigene Platz wird mitgezogen: Faellt ein Knotenpunkt VOR diesem fort, * rueckt dieser um eine Stelle nach vorn. Ohne das Nachziehen stuende er * danach an einer anderen Stelle der Kette, ohne dass jemand ihn verschoben * haette - und die Abstaende bezoegen sich auf einen anderen Nachbarn. */ export function koordinierungsknotenEntfernen(project: Project, id: string): Project { const bisher = project.koordinierung; if (bisher === null) return project; const index = bisher.nachbarn.findIndex((n) => n.id === id); if (index === -1) return project; const platz = index < bisher.platz ? Math.max(0, bisher.platz - 1) : bisher.platz; return setzeKoordinierung(project, { nachbarn: bisher.nachbarn.filter((n) => n.id !== id), platz, }); } // --- Tagesprogramme --------------------------------------------------------- /* * Die drei Aktionen der Programmbibliothek reichen an den Fachkern durch. Sie * stehen hier, weil die Oberflaeche ihre Aenderungen ausschliesslich ueber * `actions` fuehrt - und nicht, weil sie etwas hinzufuegten. */ /** Auf ein anderes Tagesprogramm umschalten. */ export function programmWechseln(project: Project, programmId: string): Project { return wechsleProgramm(project, programmId); } /** Ein weiteres Tagesprogramm anlegen und darauf umschalten. */ export function programmAnlegen(project: Project, name: string): Project { return legeProgrammAn(project, name); } /** Ein Tagesprogramm entfernen; das letzte bleibt stehen. */ export function programmEntfernen(project: Project, programmId: string): Project { return loescheProgramm(project, programmId); } // --- Lageplan --------------------------------------------------------------- export function setLageplan(project: Project, lageplan: Lageplan): Project { // Einziger Trichter: Alle Schreibstellen der Oberflaeche laufen hierueber. // `normalisiere` zieht den ersten Punkt jeder zugeordneten Fahrlinie auf ihre // Haltlinie - eine verschobene Haltlinie nimmt dadurch alle ihre Stroeme mit. return { ...project, lageplan: normalisiere(lageplan) }; } // --- Haltlinien ------------------------------------------------------------- export function addHaltlinie(project: Project, haltlinie: Haltlinie): Project { return setLageplan(project, { ...project.lageplan, haltlinien: [...project.lageplan.haltlinien, haltlinie], }); } export function setHaltlinie(project: Project, id: string, patch: Partial): Project { return setLageplan(project, { ...project.lageplan, haltlinien: project.lageplan.haltlinien.map((h) => (h.id === id ? { ...h, ...patch } : h)), }); } /** * Loescht eine Haltlinie. * * Ihre Stroeme bleiben erhalten und behalten ihren gezeichneten Verlauf * vollstaendig - `punkte` fuehrt den ganzen Linienzug. Sie sind danach nur * keiner Zufahrt mehr zugeordnet; fuer sie entscheidet dann wieder allein die * Zeichnung. Die Oberflaeche fragt vorher zurueck, weil sich damit die * Vertraeglichkeit dieser Stroeme untereinander aendert. */ export function removeHaltlinie(project: Project, id: string): Project { return setLageplan(project, { ...project.lageplan, haltlinien: project.lageplan.haltlinien.filter((h) => h.id !== id), linien: project.lageplan.linien.map((l) => l.haltlinieId === id ? { ...l, haltlinieId: null } : l, ), }); } export function setPlanlinie(project: Project, id: string, patch: Partial): Project { return setLageplan(project, { ...project.lageplan, linien: project.lageplan.linien.map((l) => (l.id === id ? { ...l, ...patch } : l)), }); } export function addPlanlinie(project: Project, linie: Planlinie): Project { return setLageplan(project, { ...project.lageplan, linien: [...project.lageplan.linien, linie], }); } /** * Loescht eine gezeichnete Linie. * Die verknuepfte Signalgruppe bleibt bestehen - sie kann bereits in Phasen * verwendet sein. Geloescht wird nur die Zeichnung. */ export function removePlanlinie(project: Project, id: string): Project { return setLageplan(project, { ...project.lageplan, linien: project.lageplan.linien.filter((l) => l.id !== id), }); } // --- Arbeitsbereiche -------------------------------------------------------- /* * Die drei Aktionen des Arbeitsbereichs - dieselbe Bauform wie bei der * Fahrlinie und ueber denselben Trichter `setLageplan`. * * KEIN AUFRAEUMEN IN FREMDEN LISTEN, und das ist keine Nachlaessigkeit: Auf * einen Arbeitsbereich verweist nichts. Er traegt weder Signalgruppe noch * Zufahrt, und deshalb beruehrt ihn weder `removeSignalGroup` noch * `removeArm`. Bekaeme er einmal einen Verweis, gehoerte er in beide. */ export function addArbeitsbereich(project: Project, bereich: Arbeitsbereich): Project { return setLageplan(project, { ...project.lageplan, arbeitsbereiche: [...project.lageplan.arbeitsbereiche, bereich], }); } export function setArbeitsbereich( project: Project, id: string, patch: Partial, ): Project { return setLageplan(project, { ...project.lageplan, arbeitsbereiche: project.lageplan.arbeitsbereiche.map((a) => a.id === id ? { ...a, ...patch } : a, ), }); } export function removeArbeitsbereich(project: Project, id: string): Project { return setLageplan(project, { ...project.lageplan, arbeitsbereiche: project.lageplan.arbeitsbereiche.filter((a) => a.id !== id), }); } /** * Die drei Aktionen des Signalgebers - dieselbe Bauform, ein Unterschied. * * ER TRAEGT EINEN VERWEIS, und deshalb gilt hier nicht, was beim * Arbeitsbereich gilt: Wird eine Signalgruppe geloescht, muss der Verweis * fallen, sonst zeigt er ins Leere. Das erledigt `removeSignalGroup` weiter * unten mit - an einer Stelle und nicht hier, damit kein zweiter Weg entsteht. */ export function addSignalgeber(project: Project, geber: Signalgeber): Project { return setLageplan(project, { ...project.lageplan, signalgeber: [...project.lageplan.signalgeber, geber], }); } export function setSignalgeber(project: Project, id: string, patch: Partial): Project { return setLageplan(project, { ...project.lageplan, signalgeber: project.lageplan.signalgeber.map((g) => (g.id === id ? { ...g, ...patch } : g)), }); } export function removeSignalgeber(project: Project, id: string): Project { return setLageplan(project, { ...project.lageplan, signalgeber: project.lageplan.signalgeber.filter((g) => g.id !== id), }); } // --- Verkehrsstaerken ------------------------------------------------------- export function setDemand( project: Project, signalGroupId: string, patch: Partial>, ): Project { // Verkehrsstaerken sind ganzzahlig (Fahrzeuge je Stunde): Nach HBS 2015 // haengt die Stufe F an q > C, und beide Zahlen stehen ganzzahlig im // Ausdruck; eine Verkehrsstaerke 243,4 neben einer Kapazitaet 243 waere // dort nicht nachvollziehbar (Befund B1). const bereinigt: Partial> = patch.volume === undefined || !Number.isFinite(patch.volume) ? patch : { ...patch, volume: Math.round(patch.volume) }; const existing = project.demands.find((d) => d.signalGroupId === signalGroupId); if (existing) { return { ...project, demands: project.demands.map((d) => d.signalGroupId === signalGroupId ? { ...d, ...bereinigt } : d, ), }; } return { ...project, demands: [...project.demands, { signalGroupId, volume: 0, heavyVehicleShare: 0, ...bereinigt }], }; } export function removeDemand(project: Project, signalGroupId: string): Project { return { ...project, demands: project.demands.filter((d) => d.signalGroupId !== signalGroupId) }; } /** * Der Lastzuganteil am Schwerverkehr - eintragen ODER entfernen. * * NEU (Schema 13). Warum das eine eigene Aktion braucht und nicht * ueber `setDemand` laeuft: `lastzugAnteil` ist ein OPTIONALES Feld, und der * Unterschied zwischen "0 % Lastzuege" und "nicht erfasst" ist genau der * Unterschied zwischen HBS-Gleichung 2-5 und 2-6. Ueber `setDemand` liesse er * sich nicht ausdruecken: `{ lastzugAnteil: undefined }` legt beim Ausbreiten * einen Schluessel mit dem Wert `undefined` an, das Feld gilt danach als * gesetzt-aber-leer, und die Unterscheidung haengt daran, welche Stelle als * naechstes darauf sieht. * * `null` heisst deshalb hier ausdruecklich "nicht erfasst" und ENTFERNT das * Feld. Das ist der Rueckweg zum Pauschalwert 1,90 - und es ist der Weg, der * gebraucht wird, wenn sich herausstellt, dass die Zaehlung die Aufteilung * nicht hergibt. * * NICHT BEGRENZT WIRD HIER: Ein Wert ausserhalb von 0 bis 1 bleibt stehen und * wird vom Signalzeitenplan als unbrauchbar gemeldet * (`lastzuganteil-ausserhalb`), statt stillschweigend auf 0 oder 1 gezogen zu * werden. Eine stille Ersetzung an dieser Stelle ergaebe die guenstigste * Aufteilung aus einer Fehleingabe. */ export function setLastzugAnteil( project: Project, signalGroupId: string, anteil: number | null, ): Project { const vorhanden = project.demands.find((d) => d.signalGroupId === signalGroupId); const gesetzt = (demand: TrafficDemand): TrafficDemand => { if (anteil === null) { const { lastzugAnteil: _entfaellt, ...ohne } = demand; return ohne; } return { ...demand, lastzugAnteil: anteil }; }; if (!vorhanden) { // Ohne Verkehrsstaerke gibt es nichts aufzuteilen; ein Eintrag entstuende // dann mit volume 0, und der Plan rechnete fuer diesen Strom ohnehin // keine Auslastung. Erst die Verkehrsstaerke, dann ihre Aufteilung. if (anteil === null) return project; return { ...project, demands: [...project.demands, gesetzt({ signalGroupId, volume: 0, heavyVehicleShare: 0 })], }; } return { ...project, demands: project.demands.map((d) => (d.signalGroupId === signalGroupId ? gesetzt(d) : d)), }; }