import { describe, expect, it } from 'vitest'; import { blattstreckeInMeter, planblatt, planblattMoeglich } from '@/domain/geometrie/planblatt'; import { massstabsbalken } from '@/domain/geometrie/vermessung'; import type { Lageplan } from '@/domain/geometrie/lageplan'; /** * Fassung 5.32.0 - das massstaebliche Lageplanblatt. * * DIE ZUSAGE, AN DER ALLES HAENGT: Was auf dem Blatt steht, muss sich abgreifen * lassen. Ein Blatt, dessen Massstab nicht stimmt, ist schlimmer als keines - * wer daraus einen zu kurzen Raeumweg abliest, bekommt eine zu kurze * Zwischenzeit, und das ist die gefaehrliche Richtung. */ /** Ein Lageplan mit bekanntem Massstab: 100 Bildpunkte sind 20 m. */ function lageplan(mitLinien = true): Lageplan { return { arbeitsbereiche: [], signalgeber: [], bild: null, kalibrierung: { von: { x: 0, y: 0 }, bis: { x: 100, y: 0 }, laengeMeter: 20, herkunft: 'gemessen', }, haltlinien: mitLinien ? [ { id: 'h1', name: 'Nord', art: 'zufahrt', armId: null, von: { x: 0, y: 0 }, bis: { x: 0, y: 100 }, }, ] : [], linien: mitLinien ? [ { id: 'l1', name: 'K1', mode: 'kfz', movement: 'geradeaus', breiteMeter: 3, punkte: [ { x: 0, y: 50 }, { x: 200, y: 50 }, ], haltlinieId: 'h1', startT: 0.5, signalGroupId: null, }, ] : [], }; } describe('Planblatt - der Massstab stimmt', () => { it('bildet eine gezeichnete Strecke maßstäblich ab', () => { /* * DIE KERNZUSAGE. Die Fahrlinie ist 200 Bildpunkte lang, und 100 * Bildpunkte sind 20 m - also 40 m. Was immer das Blatt aus ihr macht: * Zurueckgerechnet mit `meterJeBlattpunkt` müssen wieder 40 m * herauskommen. */ const blatt = planblatt(lageplan(), 400, 300); expect(blatt, 'ein Blatt entsteht').not.toBeNull(); const linie = blatt!.linien[0]!; expect(blattstreckeInMeter(blatt!, linie.punkte[0]!, linie.punkte[1]!)).toBeCloseTo(40, 2); }); it('nimmt in beiden Richtungen denselben Faktor', () => { /* * Ein Blatt mit zwei Massstaeben ist keines: Ein Raeumweg quer zur * Fahrtrichtung liesse sich daraus nicht abgreifen. * * GEPRUEFT AN BEIDEN BLATTFORMEN, und das ist noetig: Die Zeichnung ist * 200 auf 100 Bildpunkte gross. Auf einem breiten Blatt (600 x 200) * begrenzt die Hoehe, auf einem hohen (400 x 300) die Breite. Nur wo die * beiden Kandidaten AUSEINANDERGEHEN, faellt eine getrennte Stauchung von * x und y auf - mit nur einer Blattform bliebe sie unbemerkt. */ for (const [breite, hoehe] of [ [600, 200], [400, 300], ] as const) { const blatt = planblatt(lageplan(), breite, hoehe)!; const halt = blatt.haltlinien[0]!; const linie = blatt.linien[0]!; const form = `${String(breite)}x${String(hoehe)}`; // Haltlinie: 100 Bildpunkte senkrecht = 20 m. Fahrlinie: 200 waagerecht = 40 m. expect(blattstreckeInMeter(blatt, halt.von, halt.bis), form).toBeCloseTo(20, 2); expect(blattstreckeInMeter(blatt, linie.punkte[0]!, linie.punkte[1]!), form).toBeCloseTo( 40, 2, ); } }); it('hält die Zeichnung innerhalb des Blattes', () => { const blatt = planblatt(lageplan(), 400, 300)!; for (const linie of blatt.linien) { for (const p of linie.punkte) { expect(p.x).toBeGreaterThanOrEqual(0); expect(p.x).toBeLessThanOrEqual(400); expect(p.y).toBeGreaterThanOrEqual(0); expect(p.y).toBeLessThanOrEqual(300); } } }); it('liefert kein Blatt ohne Maßstab oder ohne Zeichnung', () => { const ohneMassstab: Lageplan = { ...lageplan(), kalibrierung: null }; expect(planblatt(ohneMassstab, 400, 300)).toBeNull(); expect(planblattMoeglich(ohneMassstab)).toBe(false); expect(planblatt(lageplan(false), 400, 300)).toBeNull(); expect(planblattMoeglich(lageplan(false))).toBe(false); expect(planblattMoeglich(lageplan())).toBe(true); // Und kein Blatt, das kleiner ist als sein eigener Rand. expect(planblatt(lageplan(), 20, 300)).toBeNull(); }); }); describe('Planblatt - der Balken ist die maßgebende Angabe', () => { it('wählt eine runde Meterzahl', () => { const blatt = planblatt(lageplan(), 400, 300)!; expect([1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]).toContain(blatt.balken.meter); }); it('ist so lang, wie er sagt', () => { /* * Der Balken behauptet eine Länge in Metern. Nimmt man seine Länge in * Blattpunkten und rechnet sie mit dem Blattmaßstab zurück, muss genau * diese Zahl herauskommen - sonst ist er eine Lüge auf dem Papier. */ const blatt = planblatt(lageplan(), 400, 300)!; expect(blatt.balken.laenge * blatt.meterJeBlattpunkt).toBeCloseTo(blatt.balken.meter, 6); }); it('nimmt dieselbe Stufenwahl wie der Bildschirm', () => { // Eine zweite Fassung in der Leinwand liefe auseinander; die Funktion // steht deshalb im Fachkern und wird von beiden Seiten gerufen. const blatt = planblatt(lageplan(), 400, 300)!; const geteilt = massstabsbalken(blatt.meterJeBlattpunkt, (400 - 2 * 18) / 3); expect(blatt.balken).toEqual(geteilt); }); it('nennt einen runden Blattmaßstab und rundet ihn nach oben', () => { /* * "1:250" kann ein Prüfer mit dem Maßstabsdreieck nachmessen, "1:247" * nicht. Aufgerundet wird, weil der genannte Maßstab dann kleiner ist als * der wirkliche: Wer daraus abgreift, bekommt eher zu wenig als zu viel. */ const blatt = planblatt(lageplan(), 400, 300)!; expect(blatt.massstabsnenner).not.toBeNull(); const wirklich = (blatt.meterJeBlattpunkt * 72 * 1000) / 25.4; expect(blatt.massstabsnenner!).toBeGreaterThanOrEqual(wirklich); expect([50, 100, 200, 250, 500, 1000, 2000, 2500, 5000, 10000]).toContain( blatt.massstabsnenner, ); }); }); describe('Massstabsbalken - die geteilte Stufenwahl', () => { it('bleibt bei unbrauchbaren Angaben stumm', () => { for (const wert of [0, -1, Number.NaN, Number.POSITIVE_INFINITY]) { expect(massstabsbalken(wert, 120), String(wert)).toEqual({ meter: 0, laenge: 0 }); } }); it('trifft die nächstgrößere runde Stufe', () => { // 120 Punkte * 0,1 m = 12 m roh -> die Stufe 20. expect(massstabsbalken(0.1, 120).meter).toBe(20); // 120 * 0,04 = 4,8 m -> 5. expect(massstabsbalken(0.04, 120).meter).toBe(5); // Und die Länge gehört zur Zahl. expect(massstabsbalken(0.04, 120).laenge).toBeCloseTo(5 / 0.04, 6); }); });