import { deflateSync } from 'node:zlib'; import { mkdirSync, writeFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { createEmptyProject } from '../src/domain/model/factory'; import { leiteAb, uebernimmWege, wegSchluessel } from '../src/domain/geometrie/ableitung'; import { buildSignalPlan } from '../src/domain/plan/signalPlan'; import { validateProject } from '../src/domain/validation/engine'; import { REGELBREITE } from '../src/domain/geometrie/vermessung'; import type { WegeVorschlag } from '../src/domain/geometrie/ableitung'; import type { Haltlinie, Lageplan, Planlinie } from '../src/domain/geometrie/lageplan'; import type { Movement } from '../src/domain/rilsa/types'; import type { IntersectionArm, Project } from '../src/domain/model/project'; /** * Erzeugt einen vollstaendigen vierarmigen Knotenpunkt als Pruefstueck. * * WARUM ERZEUGT UND NICHT GEKLICKT * * Sechzehn Fahrlinien und acht Haltlinien von Hand in die Oberflaeche zu * klicken waere weder wiederholbar noch nachrechenbar. Hier steht die Geometrie * dagegen als Formel: Jede Zufahrt entsteht aus derselben Vorschrift, nur * gedreht. Damit laesst sich jede Zahl des Ergebnisses von Hand nachpruefen - * und das ist der Sinn einer Kontrolle. * * MASSE * * Rechtsverkehr, 90 m im Quadrat, 1 Bildpunkt = 0,1 m. * Fahrbahn 14 m breit, je Richtung 7 m mit zwei Fahrstreifen zu 3,5 m. * Haltlinie 12 m vor Knotenpunktmitte, Furt 9 m davor - Fahrzeuge halten vor der Furt. */ // --- Masse in Metern -------------------------------------------------------- const KANTE = 90; const PX_JE_METER = 10; const MITTE = { x: 45, y: 45 }; /** Halbe Fahrbahnbreite - so weit reicht eine Fahrtrichtung von der Achse. */ const HALB = 7; /** * Fahrstreifenachsen je Richtung: zwei Fahrstreifen zu 3,5 m. * Der innere fuehrt links ab, der aeussere geradeaus und rechts - der Regelfall. */ const ACHSE_INNEN = 1.75; const ACHSE_AUSSEN = 5.25; /** * Reihenfolge von aussen nach innen: Haltlinie, dann Furt, dann Knotenpunkt. * Fahrzeuge halten VOR der Furt - genau das macht die Furt zum Konflikt. */ /** Halbmesser der Abbiegeboegen. */ const R_LINKS = 12; const R_RECHTS = 6; const HALTLINIE_VOR = 12; const FURT_VOR = 9; const AUSLAUF = 40; interface Arm { readonly id: string; readonly name: string; /** Fahrtrichtung der einfahrenden Stroeme, Bildkoordinaten (y nach unten). */ readonly f: { x: number; y: number }; } /** Rechts des Fahrers - bei y nach unten ist das (-fy, fx). */ function rechts(v: { x: number; y: number }): { x: number; y: number } { return { x: -v.y, y: v.x }; } function plus( a: { x: number; y: number }, ...teile: readonly { v: { x: number; y: number }; s: number }[] ): { x: number; y: number } { return teile.reduce((p, t) => ({ x: p.x + t.v.x * t.s, y: p.y + t.v.y * t.s }), a); } /** * Kreisbogen zwischen zwei Punkten um einen Mittelpunkt. * * Abbiegevorgaenge als Eckzug zu zeichnen erzeugt Schnittpunkte, die es in * Wirklichkeit nicht gibt: Zwei gegenueberliegende Linksabbieger fahren * hintereinander um die Knotenpunktmitte herum, ihre Sehnen aber kreuzen sich. * Deshalb wird der Bogen abgetastet - hier ist Naeherung angebracht, denn die * Fahrlinie selbst ist ein Linienzug. */ function bogen( mittelpunkt: { x: number; y: number }, von: { x: number; y: number }, bis: { x: number; y: number }, stuecke = 8, ): { x: number; y: number }[] { const radius = Math.hypot(von.x - mittelpunkt.x, von.y - mittelpunkt.y); const a0 = Math.atan2(von.y - mittelpunkt.y, von.x - mittelpunkt.x); let a1 = Math.atan2(bis.y - mittelpunkt.y, bis.x - mittelpunkt.x); // Immer den kurzen Weg nehmen - ein Abbiegevorgang ist hoechstens ein // Viertelkreis. while (a1 - a0 > Math.PI) a1 -= 2 * Math.PI; while (a0 - a1 > Math.PI) a1 += 2 * Math.PI; const punkte: { x: number; y: number }[] = []; for (let i = 0; i <= stuecke; i += 1) { const a = a0 + ((a1 - a0) * i) / stuecke; punkte.push({ x: mittelpunkt.x + radius * Math.cos(a), y: mittelpunkt.y + radius * Math.sin(a), }); } return punkte; } /** Meter in Bildpunkte. */ function px(p: { x: number; y: number }): { x: number; y: number } { return { x: p.x * PX_JE_METER, y: p.y * PX_JE_METER }; } const ARME: readonly Arm[] = [ { id: 'arm-nord', name: 'Nord', f: { x: 0, y: 1 } }, { id: 'arm-ost', name: 'Ost', f: { x: -1, y: 0 } }, { id: 'arm-sued', name: 'Sued', f: { x: 0, y: -1 } }, { id: 'arm-west', name: 'West', f: { x: 1, y: 0 } }, ]; // --- Hintergrundbild -------------------------------------------------------- /** * Zeichnet einen schematischen Knotenpunkt als PNG. * * Ohne Fremdbibliothek: Die Bildpunkte werden unmittelbar gesetzt und mit der * in Node enthaltenen Deflate-Kodierung verpackt. Das Bild dient nur als * Unterlage - gerechnet wird ausschliesslich mit der Geometrie. */ function baueBild(): string { const groesse = KANTE * PX_JE_METER; const daten = Buffer.alloc(groesse * (groesse * 3 + 1)); const setze = (x: number, y: number, r: number, g: number, b: number): void => { const zeile = y * (groesse * 3 + 1); daten[zeile] = 0; daten[zeile + 1 + x * 3] = r; daten[zeile + 2 + x * 3] = g; daten[zeile + 3 + x * 3] = b; }; const halbPx = HALB * PX_JE_METER; const mittePx = px(MITTE); for (let y = 0; y < groesse; y += 1) { for (let x = 0; x < groesse; x += 1) { const dx = Math.abs(x - mittePx.x); const dy = Math.abs(y - mittePx.y); const aufFahrbahn = dx <= halbPx || dy <= halbPx; if (!aufFahrbahn) { // Umfeld setze(x, y, 214, 220, 210); continue; } // Asphalt let r = 96; let g = 99; let b = 104; // Fahrbahnachse als unterbrochene Linie, nur ausserhalb des Knotens. const imKnoten = dx <= halbPx && dy <= halbPx; if (!imKnoten) { const aufAchseX = dx <= 1 && Math.floor(y / 20) % 2 === 0; const aufAchseY = dy <= 1 && Math.floor(x / 20) % 2 === 0; // Fahrstreifenbegrenzung zwischen den beiden Fahrstreifen je Richtung. const trennerX = Math.abs(dx - ACHSE_AUSSEN * PX_JE_METER) <= 1 && Math.floor(y / 16) % 2 === 0; const trennerY = Math.abs(dy - ACHSE_AUSSEN * PX_JE_METER) <= 1 && Math.floor(x / 16) % 2 === 0; if (aufAchseX || aufAchseY || trennerX || trennerY) { r = 236; g = 236; b = 232; } } setze(x, y, r, g, b); } } return `data:image/png;base64,${kodierePng(groesse, groesse, daten).toString('base64')}`; } function kodierePng(breite: number, hoehe: number, roh: Buffer): Buffer { const teil = (typ: string, inhalt: Buffer): Buffer => { const laenge = Buffer.alloc(4); laenge.writeUInt32BE(inhalt.length, 0); const koerper = Buffer.concat([Buffer.from(typ, 'ascii'), inhalt]); const pruef = Buffer.alloc(4); pruef.writeUInt32BE(crc32(koerper), 0); return Buffer.concat([laenge, koerper, pruef]); }; const kopf = Buffer.alloc(13); kopf.writeUInt32BE(breite, 0); kopf.writeUInt32BE(hoehe, 4); kopf[8] = 8; // Bittiefe kopf[9] = 2; // Echtfarbe kopf[10] = 0; kopf[11] = 0; kopf[12] = 0; return Buffer.concat([ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), teil('IHDR', kopf), teil('IDAT', deflateSync(roh, { level: 9 })), teil('IEND', Buffer.alloc(0)), ]); } const CRC_TABELLE = (() => { const t = new Uint32Array(256); for (let n = 0; n < 256; n += 1) { let c = n; for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; t[n] = c >>> 0; } return t; })(); function crc32(daten: Buffer): number { let c = 0xffffffff; for (const wert of daten) c = (CRC_TABELLE[(c ^ wert) & 0xff] as number) ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; } // --- Knotenpunkt ------------------------------------------------------------ function baueLageplan(): Lageplan { const haltlinien: Haltlinie[] = []; const linien: Planlinie[] = []; for (const arm of ARME) { const f = arm.f; const r = rechts(f); const rr = rechts(r); // Haltlinie der Zufahrt: von der Fahrbahnachse bis zum Bordstein. const aufAchse = plus(MITTE, { v: f, s: -HALTLINIE_VOR }); const haltlinie: Haltlinie = { id: `hl-${arm.id}`, name: arm.name, art: 'zufahrt', armId: arm.id, von: px(aufAchse), bis: px(plus(aufAchse, { v: r, s: HALB })), }; haltlinien.push(haltlinie); // Zwei Fahrstreifen: innen der Linksabbieger, aussen geradeaus und rechts. const startAussen = plus(aufAchse, { v: r, s: ACHSE_AUSSEN }); const startInnen = plus(aufAchse, { v: r, s: ACHSE_INNEN }); const stroeme: readonly { movement: Movement; start: { x: number; y: number }; startT: number; punkte: readonly { x: number; y: number }[]; }[] = [ { // Geradeaus: bleibt auf dem aeusseren Fahrstreifen. movement: 'geradeaus', start: startAussen, startT: ACHSE_AUSSEN / HALB, punkte: [ startAussen, plus(MITTE, { v: f, s: FURT_VOR }, { v: r, s: ACHSE_AUSSEN }), plus(MITTE, { v: f, s: AUSLAUF }, { v: r, s: ACHSE_AUSSEN }), ], }, { // Rechts: vom selben Fahrstreifen, enger Bogen in die Ausfahrt. // Bogenmitte rechts der Zufahrt, Halbmesser R_RECHTS. movement: 'rechts', start: startAussen, startT: ACHSE_AUSSEN / HALB, punkte: [ startAussen, ...bogen( plus( MITTE, { v: f, s: -(ACHSE_AUSSEN + R_RECHTS) }, { v: r, s: ACHSE_AUSSEN + R_RECHTS }, ), plus(MITTE, { v: f, s: -(ACHSE_AUSSEN + R_RECHTS) }, { v: r, s: ACHSE_AUSSEN }), plus(MITTE, { v: r, s: ACHSE_AUSSEN + R_RECHTS }, { v: rr, s: ACHSE_AUSSEN }), ), plus(MITTE, { v: r, s: AUSLAUF }, { v: rr, s: ACHSE_AUSSEN }), ], }, { // Links: eigener Fahrstreifen, weiter Bogen um die Knotenpunktmitte. // // Bogenmitte auf der Seite, zu der abgebogen wird. Dadurch fahren die // gegenueberliegenden Linksabbieger hintereinander umeinander herum - // als Eckzug gezeichnet schnitten ihre Sehnen sich, und die Vermassung // meldete das dann voellig zu Recht als Konflikt. movement: 'links', start: startInnen, startT: ACHSE_INNEN / HALB, punkte: [ startInnen, ...bogen( plus(MITTE, { v: f, s: ACHSE_INNEN - R_LINKS }, { v: r, s: ACHSE_INNEN - R_LINKS }), plus(MITTE, { v: f, s: ACHSE_INNEN - R_LINKS }, { v: r, s: ACHSE_INNEN }), plus(MITTE, { v: f, s: ACHSE_INNEN }, { v: r, s: ACHSE_INNEN - R_LINKS }), ), plus(MITTE, { v: r, s: -AUSLAUF }, { v: rr, s: -ACHSE_INNEN }), ], }, ]; for (const strom of stroeme) { linien.push({ id: `${arm.id}-${strom.movement}`, name: `${arm.name} ${strom.movement}`, mode: 'kfz', movement: strom.movement, breiteMeter: REGELBREITE.kfz, punkte: strom.punkte.map(px), haltlinieId: haltlinie.id, startT: strom.startT, signalGroupId: null, }); } // Fussgaengerfurt ueber diesen Arm, vor dem Knotenpunkt. const furtAchse = plus(MITTE, { v: f, s: -FURT_VOR }); const bordA = plus(furtAchse, { v: r, s: HALB }); const bordB = plus(furtAchse, { v: r, s: -HALB }); const furtHalt: Haltlinie = { id: `hl-furt-${arm.id}`, name: `Furt ${arm.name}`, art: 'querung', armId: arm.id, von: px(plus(bordA, { v: f, s: -2 })), bis: px(plus(bordA, { v: f, s: 2 })), }; haltlinien.push(furtHalt); linien.push({ id: `${arm.id}-furt`, name: `Furt ${arm.name}`, mode: 'fuss', movement: 'querung', breiteMeter: REGELBREITE.fuss, punkte: [px(bordA), px(bordB)], haltlinieId: furtHalt.id, startT: 0.5, signalGroupId: null, }); } return { arbeitsbereiche: [], signalgeber: [], bild: { datenUrl: baueBild(), breite: KANTE * PX_JE_METER, hoehe: KANTE * PX_JE_METER, herkunft: 'Schematischer vierarmiger Knotenpunkt (Pruefstueck)', geladenAm: new Date(0).toISOString(), }, /* * GEMESSEN, NICHT GEOREFERENZIERT (Fassung 5.11.0, Befund 34). * * 'georeferenziert' heisst: Das Programm hat den Ausschnitt selbst beim * Kartendienst angefordert und kennt seine Kantenlaenge deshalb exakt * (`Kalibrierung` in src/domain/geometrie/lageplan.ts). Hier zeichnet * `baueBild()` das Bild selbst; ein Kartendienst ist nicht beteiligt. Der * Ausdruck druckte aus dem alten Wert die Zeile "Herkunft des Maszstabs: * aus dem Kartendienst übernommen (georeferenzierter Ausschnitt, nicht * nachgemessen)" - zwei Zeilen unter der wahren Bildherkunft in derselben * Tabelle -, und die Seitenleiste des Lageplans sagte "Er muss nicht * nachgemessen werden". Beides nahm dem Pruefer genau den Arbeitsschritt * ab, der bei einem nicht georeferenzierten Bild die einzige Absicherung * des Maszstabs ist. * * Mit 'gemessen' druckt der Ausdruck stattdessen "an einer Strecke * bekannter Länge abgegriffen: 90,0 m auf 900 Bildpunkten" - beide Zahlen * stehen unten und sind nachpruefbar. Der Maszstab selbst aendert sich * nicht. */ kalibrierung: { von: { x: 0, y: 0 }, bis: { x: KANTE * PX_JE_METER, y: 0 }, laengeMeter: KANTE, herkunft: 'gemessen', }, haltlinien, linien, }; } function baueProjekt(): Project { const leer = createEmptyProject('Vierarmiger Knotenpunkt (Kontrolle)', new Date(0)); const arms: IntersectionArm[] = ARME.map((a) => ({ id: a.id, name: a.name, direction: a.name, lanes: 2, vZul: 50, })); return { ...leer, meta: { ...leer.meta, location: 'Musterkreuzung', variant: 'Kontrolle', // AUS DEN KONSTANTEN GEBILDET, nicht abgeschrieben (Fassung 5.11.0, // Befund 33). Hier stand "Haltlinie 9 m vor Knotenpunktmitte" - das ist // FURT_VOR, also die Lage der Furt. Der Satz steht woertlich in der // ausgelieferten .lsap und wird von src/services/export/pdf.ts unter // "Bemerkungen" in die Planunterlage gedruckt; wer die Vermassung anhand // dieser Angabe nachprueft, misst am falschen Querschnitt. Mit der // Schablone kann er bei einer Massaenderung nicht erneut // auseinanderlaufen. description: `Erzeugtes Pruefstueck: vier Zufahrten mit je geradeaus, links und rechts, dazu vier ` + `Fussgaengerfurten. Fahrbahn ${2 * HALB} m, Haltlinie ${HALTLINIE_VOR} m vor ` + `Knotenpunktmitte, Furt ${FURT_VOR} m davor.`, }, intersection: { ...leer.intersection, name: 'Musterkreuzung', arms, vZul: 50 }, lageplan: baueLageplan(), }; } // --- Auswertung ------------------------------------------------------------- const projekt = baueProjekt(); const ableitung = leiteAb(projekt, projekt.lageplan); const zufahrtVon = (id: string): string => (id.startsWith('arm-') ? (id.split('-')[1] ?? '') : ''); const istFurt = (id: string): boolean => id.endsWith('-furt'); const innerhalb = ableitung.vertraeglichkeit.filter( (v) => zufahrtVon(v.aLinieId) === zufahrtVon(v.bLinieId) && !istFurt(v.aLinieId) && !istFurt(v.bLinieId), ); const furtGegenEigenenArm = ableitung.vertraeglichkeit.filter( (v) => zufahrtVon(v.aLinieId) === zufahrtVon(v.bLinieId) && (istFurt(v.aLinieId) || istFurt(v.bLinieId)), ); const zwischenZufahrten = ableitung.vertraeglichkeit.filter( (v) => zufahrtVon(v.aLinieId) !== zufahrtVon(v.bLinieId), ); console.log('=== Vierarmiger Knotenpunkt ==='); console.log(`Haltlinien ${projekt.lageplan.haltlinien.length}`); console.log(`Fahrlinien ${projekt.lageplan.linien.length}`); console.log(`Paare insgesamt ${ableitung.vertraeglichkeit.length}`); console.log(''); console.log('--- Stroeme DERSELBEN Zufahrt (muessen vertraeglich sein) ---'); console.log(` Paare ${innerhalb.length}`); console.log(` davon feindlich ${innerhalb.filter((v) => v.feindlich).length}`); const gruende = new Map(); for (const v of innerhalb) gruende.set(v.grund, (gruende.get(v.grund) ?? 0) + 1); for (const [grund, anzahl] of gruende) console.log(` ${grund.padEnd(24)} ${anzahl}`); console.log(''); console.log('--- Furt gegen Stroeme ihres EIGENEN Arms (muessen feindlich sein) ---'); console.log(` Paare ${furtGegenEigenenArm.length}`); console.log(` davon feindlich ${furtGegenEigenenArm.filter((v) => v.feindlich).length}`); console.log(''); console.log('--- Zwischen verschiedenen Zufahrten ---'); console.log(` Paare ${zwischenZufahrten.length}`); console.log(` davon feindlich ${zwischenZufahrten.filter((v) => v.feindlich).length}`); console.log(''); console.log(`Unterdrueckte Beruehrungen ${ableitung.unterdrueckt.length}`); console.log(`Vermessene Beziehungen ${ableitung.wege.length}`); console.log(''); /** * Eine Stichprobenzeile zum Nachrechnen - Namen, beide Wege, Schnittwinkel. * * Als eigene Funktion herausgezogen, damit der SATZ pruefbar ist und nicht nur * seine Bausteine: Hier entsteht die Angabe, und hier hing der Fehler. * * Der Winkel 0 heisst "laengs" und nicht "null Grad": Der Konfliktbereich * fuehrt den Winkel des Schnitts mit, der sein Ende bestimmt hat. Stammt das * Ende aus einem gemeinsamen Stueck - zwei Stroeme laufen nebeneinander her, * oder es gibt gar keinen Punktschnitt -, gehoert dazu kein Schnittwinkel, und * `vermessung.ts` traegt 0 ein (siehe `Konfliktbereich.winkel`). "0.0 Grad" * behauptete daraus einen Schnitt unter null Grad und damit, die Linien * kreuzten sich nicht - neben Raeumweg und Einfahrweg derselben Beziehung. * Dieselbe Unterscheidung trifft die Ansicht (lageplanView.ts); hier steht sie * in Umschrift wie die uebrige Ausgabe dieses Werkzeugs. * * Der Text landet in keiner erzeugten Datei - er wird nur ausgegeben. */ export function stichprobenzeile(weg: WegeVorschlag): string { const winkel = weg.winkelGrad === 0 ? 'laengs' : `${weg.winkelGrad.toFixed(1)} Grad`; return ( ` ${weg.vonName.padEnd(18)} raeumt vor ${weg.nachName.padEnd(18)} ` + `Raeumweg ${weg.raeumweg.toFixed(2).padStart(6)} m ` + `Einfahrweg ${weg.einfahrweg.toFixed(2).padStart(6)} m ` + `Winkel ${winkel}` ); } console.log('--- Stichproben zum Nachrechnen ---'); const stichproben = [ ['arm-nord-geradeaus', 'arm-west-geradeaus'], ['arm-west-geradeaus', 'arm-nord-geradeaus'], ['arm-nord-links', 'arm-sued-geradeaus'], ['arm-nord-geradeaus', 'arm-nord-furt'], ]; for (const [von, nach] of stichproben) { const weg = ableitung.wege.find((w) => w.vonLinieId === von && w.nachLinieId === nach); if (weg === undefined) { console.log(` ${von} raeumt / ${nach} faehrt ein: keine Beziehung`); continue; } console.log(stichprobenzeile(weg)); } for (const hinweis of ableitung.hinweise) { console.log(`HINWEIS [${hinweis.severity}] ${hinweis.message}`); } // --- Vom Lageplan zum fertigen Signalzeitenplan ----------------------------- /** * Buendelt die Stroeme zu Signalgruppen, uebernimmt die vermessenen Wege und * bildet ein uebliches Vierphasenprogramm. * * Damit wird die ganze Kette geprueft, nicht nur die Geometrie: Zwoelf Stroeme * werden zu acht Kfz-Signalgruppen gebuendelt - geradeaus und rechts teilen * sich je Zufahrt einen Signalgeber, der Linksabbieger bekommt einen eigenen. * Genau dieser Fall, mehrere Stroeme an einer Gruppe, war vorher nicht * abbildbar. */ function baueSignalprogramm(basis: Project): Project { let p = basis; const gruppeVon = new Map(); const lege = ( name: string, mode: 'kfz' | 'fuss', armId: string, linien: readonly string[], ): void => { const id = `sg-${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`; p = { ...p, signalGroups: [ ...p.signalGroups, { id, name, mode, movement: mode === 'fuss' ? 'querung' : linien.some((l) => l.endsWith('links')) ? 'links' : 'geradeaus', armId, vZul: 50, // Geradeaus und rechts teilen sich EINEN Fahrstreifen - deshalb einer, // nicht einer je Strom. Die Saettigungsverkehrsstaerke haengt am // Fahrstreifen, nicht an der Zahl der Fahrbeziehungen. lanes: 1, // Fussgaenger haben keine Fahrzeuglaenge - dafuer steht 'keine'. vehicleClass: mode === 'fuss' ? 'keine' : 'pkw', minGreenOverride: null, maxGreenOverride: null, reducedMobility: false, color: '#2f6fd0', }, ], }; for (const l of linien) gruppeVon.set(l, id); }; for (const arm of ARME) { // Ein Signalgeber fuer geradeaus UND rechts - sie teilen den Fahrstreifen. lege(`K ${arm.name}`, 'kfz', arm.id, [`${arm.id}-geradeaus`, `${arm.id}-rechts`]); lege(`K ${arm.name} links`, 'kfz', arm.id, [`${arm.id}-links`]); lege(`F ${arm.name}`, 'fuss', arm.id, [`${arm.id}-furt`]); } p = { ...p, lageplan: { ...p.lageplan, linien: p.lageplan.linien.map((l) => ({ ...l, signalGroupId: gruppeVon.get(l.id) ?? null })), }, }; // Wege uebernehmen - alle, die die Ableitung ermittelt hat. const neu = leiteAb(p, p.lageplan); const ergebnis = uebernimmWege(p, p.lageplan, neu, neu.wege.map(wegSchluessel)); p = ergebnis.project; // Uebliches Vierphasenprogramm. // Eigene Fussgaengerphase statt paralleler Freigabe zu den Rechtsabbiegern. // // Der Grund ist fachlich und wird von der Pruefung erzwungen: Geradeaus und // rechts teilen sich hier einen Fahrstreifen und muessen deshalb denselben // Signalgeber bekommen. Der Rechtsabbieger kreuzt aber die Furt des Arms, in // den er einbiegt. Bei zweiwertiger Vertraeglichkeit - und mehr kann dieser // Stand - bleibt nur, die Furten in eine eigene Phase zu legen. // // In der Praxis loest man das ueber die bedingte Vertraeglichkeit: Der // Rechtsabbieger erhaelt gleichzeitig Freigabe und muss warten. Solange das // Programm das nicht abbilden kann, waere es unredlich, so zu tun als ob. const phasen = [ { name: 'Phase 1 - Nord/Sued geradeaus und rechts', gruppen: ['sg-k-nord', 'sg-k-sued'] }, { name: 'Phase 2 - Nord/Sued links', gruppen: ['sg-k-nord-links', 'sg-k-sued-links'] }, { name: 'Phase 3 - Ost/West geradeaus und rechts', gruppen: ['sg-k-ost', 'sg-k-west'] }, { name: 'Phase 4 - Ost/West links', gruppen: ['sg-k-ost-links', 'sg-k-west-links'] }, { name: 'Phase 5 - Fussgaenger', gruppen: ['sg-f-nord', 'sg-f-ost', 'sg-f-sued', 'sg-f-west'] }, ].map((ph, i) => ({ id: `ph-${i + 1}`, name: ph.name, signalGroupIds: ph.gruppen, manualGreen: null, })); return { ...p, phases: phasen, program: { ...p.program, phaseOrder: phasen.map((ph) => ph.id) }, demands: p.signalGroups.map((g) => ({ signalGroupId: g.id, volume: g.mode === 'fuss' ? 120 : 160, heavyVehicleShare: g.mode === 'fuss' ? 0 : 0.05, })), }; } const fertig = baueSignalprogramm(projekt); const plan = buildSignalPlan(fertig); const bericht = validateProject(fertig, plan, new Date(0)); console.log(''); console.log('=== Weiter bis zum Signalzeitenplan ==='); console.log(`Signalgruppen ${fertig.signalGroups.length}`); console.log(`Konfliktbeziehungen ${fertig.conflicts.length}`); console.log(`Phasen ${fertig.phases.length}`); console.log(`Umlaufzeit ${plan.cycleTime} s`); console.log(`Fehler ${bericht.errorCount}`); console.log(`Warnungen ${bericht.warningCount}`); console.log(`Ausgabe gesperrt ${bericht.exportBlocked ? 'ja' : 'nein'}`); for (const eintrag of bericht.findings.slice(0, 10)) { console.log(` [${eintrag.severity}] ${eintrag.message}`); } if (bericht.findings.length > 10) console.log(` ... und ${bericht.findings.length - 10} weitere`); console.log(''); console.log('--- Groesste Zwischenzeiten ---'); const groesste = [...plan.intergreens.entries()] .map(([schluessel, wert]) => { const [vonId, nachId] = schluessel.split('|'); return { von: fertig.signalGroups.find((g) => g.id === vonId)?.name ?? vonId ?? '?', nach: fertig.signalGroups.find((g) => g.id === nachId)?.name ?? nachId ?? '?', wert: wert.value, }; }) .sort((a, b) => b.wert - a.wert) .slice(0, 6); for (const z of groesste) { console.log(` ${z.von.padEnd(16)} -> ${z.nach.padEnd(16)} ${String(z.wert).padStart(3)} s`); } // --- Bild des Plans --------------------------------------------------------- /** * Zeichnet den Plan als SVG, damit sich das Ergebnis ansehen laesst. * * Bewusst nicht die Leinwand der Anwendung: Die haengt am Browser. Hier geht es * darum, das erzeugte Pruefstueck sichtbar zu machen - dieselben Punkte, nur * anders ausgegeben. * * NUR DIE PUNKTE, KEINE AUSWERTUNG (Fassung 5.11.0, Befund 61): * Hier stand unter der Ueberschrift "Feindliche Beruehrungen" eine Schleife * ueber alle feindlichen Paare, die beide Planlinien heraussuchte und danach * nichts erzeugte - kein `teile.push`. Das Bild zeigte also keine einzige * Beruehrung, obwohl der Kommentar sie ankuendigte, und allein dieser toten * Schleife wegen wurde `leiteAb` ein drittes Mal ueber den Knotenpunkt * gerechnet. Die Ankuendigung ist gefallen, nicht die Zeichnung ergaenzt: Der * Satz darueber sagt, wofuer dieses Bild da ist, und das sind die Punkte. */ function baueSvg(p: Project): string { // Die Lageplankoordinaten stehen in Dezimetern (PX_JE_METER = 10, siehe // `px`); das Teilen durch 10 fuehrt sie hier in Meter. Der Kommentar nannte // vor der Fassung 5.11.0 (Befund 62) die Eingangs- statt der Ausgangseinheit; // wer im Bild nachmisst und ihm folgt, liegt um den Faktor zehn daneben. Dass // eine Einheit ein Meter ist, zeigt die viewBox: 90 Einheiten fuer 90 m // Kantenlaenge - bei Dezimetern stuenden dort 900. Auf Meter ausgelegt sind // auch die Schriftgroessen 2.4 und 2.6 und die Strichbreiten 0.7 und 1.6. const s = (KANTE * PX_JE_METER) / 10; // 1 Einheit = 1 m -> handliche Zahlen const m = (w: { x: number; y: number }) => `${(w.x / 10).toFixed(1)},${(w.y / 10).toFixed(1)}`; const farbe: Record = { geradeaus: '#2f6fd0', links: '#a05ec4', rechts: '#4a9e6a', querung: '#e07b39', }; const teile: string[] = []; teile.push(``); // Fahrbahnen const halb = (HALB * PX_JE_METER) / 10; const mitte = (MITTE.x * PX_JE_METER) / 10; teile.push(``); teile.push(``); // Korridore for (const linie of p.lageplan.linien) { const breite = (linie.breiteMeter * PX_JE_METER) / 10; teile.push( `', ); } // Mittellinien for (const linie of p.lageplan.linien) { teile.push( ``, ); } // Haltlinien for (const h of p.lageplan.haltlinien) { teile.push( ``, ); teile.push( `' + `${h.name}`, ); } return ( `` + teile.join('') + `` + `${p.lageplan.linien.length} Stroeme an ${p.lageplan.haltlinien.length} Haltlinien` + '' ); } writeFileSync(resolve('beispiele/vierarmiger-knotenpunkt.svg'), baueSvg(fertig), 'utf-8'); // Projektdatei ablegen, damit sie sich in der Anwendung oeffnen laesst. const ziel = resolve(process.argv[2] ?? 'beispiele/vierarmiger-knotenpunkt.lsap'); mkdirSync(dirname(ziel), { recursive: true }); writeFileSync(ziel, JSON.stringify(fertig, null, 2), 'utf-8'); console.log(''); console.log(`Projektdatei: ${ziel}`);