Phase 6: alles, was einen Ort hat — Loeschwasser, Objekte mit Feuerwehrplan, die Lagekarte eines Einsatzes und die geplante Foerderstrecke. Keine Kacheln aus dem Netz. Wo eine Wehr ausrueckt und wo ihre Hydranten liegen, ist niemandes Geschaeft; der Kartenserver laeuft selbst (tileserver-gl im Compose-Profil "karte"). Ohne ihn zeigt die Karte einen leeren Grund und sagt das auch — Punkte, Entfernungen und Umkreissuche funktionieren trotzdem. Dasselbe gilt fuer einen eingetragenen, aber nicht erreichbaren Server. Die Umkreissuche laeuft ueber PostGIS: ST_DWithin auf der Geografie mit GiST-Index rechnet in Metern und nutzt den Index. Gefragt wird waehrend der Fahrt, und bei tausend Hydranten waere alles andere ein vollstaendiger Tabellendurchlauf. Der Import aus GeoJSON und Stadtwerke-CSV ist nachsichtig, aber ehrlich: er erkennt "DN 100", deutsche Kommazahlen, Datumsangaben und vertauschte Achsen — raet aber nichts. Fehlt die Position, wird die Zeile uebersprungen; ohne Statusangabe steht "ungeprueft", nicht "einsatzbereit". 22 taktische Zeichen nach FwDV 102 selbst gezeichnet, die Lagekarte gleicht ueber denselben Ereignisstrom ab wie die Atemschutzueberwachung. Die Wasserfoerderung rechnet Reibung, Hoehe und Pumpenzahl; das Ergebnis wird nie gespeichert, sondern bei jeder Anzeige neu gerechnet. Geht eine Strecke nicht auf, sagt die Ausgabe das. Behoben: die Reibungsbeiwerte lagen im ersten Wurf um den Faktor zehn daneben. Aufgefallen ist es nur, weil die Tests gegen die Tabellenwerte der Ausbildung pruefen und nicht gegen die eigene Implementierung. Dazu ein Abruf fuer abgelegte Dateien (/api/dateien/[id]) — bis hierher gab es nur den Upload. 59 neue Tests (48 Unit, 11 E2E je Browser). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
271 lines
8.5 KiB
TypeScript
271 lines
8.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
entfernung,
|
|
foerderplan,
|
|
hoehenverlust,
|
|
reibungsverlust,
|
|
type Streckenpunkt,
|
|
} from "@/lib/karte/wasserfoerderung";
|
|
|
|
/**
|
|
* Wasserförderung über lange Wegstrecke.
|
|
*
|
|
* Physik mit klaren Formeln — und deshalb der Teil von Phase 6, der sich
|
|
* vollständig ohne Karte, ohne Datenbank und ohne Browser prüfen lässt.
|
|
* Geprüft wird nicht, ob eine Zahl herauskommt, sondern ob die Rechnung die
|
|
* Tabellenwerte der Ausbildung trifft und ob sie zugibt, wenn eine Strecke
|
|
* nicht zu fördern ist.
|
|
*/
|
|
|
|
/** Ein Punkt bei Musterhausen. */
|
|
function punkt(breitengrad: number, laengengrad: number, hoehe?: number): Streckenpunkt {
|
|
return { breitengrad, laengengrad, hoehe: hoehe ?? null };
|
|
}
|
|
|
|
describe("Entfernung", () => {
|
|
it("rechnet eine kurze Strecke auf den Meter", () => {
|
|
// Ein Zehntausendstel Grad Breite sind rund elf Meter.
|
|
const meter = entfernung(punkt(48.5, 12.15), punkt(48.5001, 12.15));
|
|
expect(meter).toBeGreaterThanOrEqual(10);
|
|
expect(meter).toBeLessThanOrEqual(12);
|
|
});
|
|
|
|
it("rechnet eine Strecke über einen Kilometer", () => {
|
|
// 0,01 Grad Breite sind rund 1,11 km.
|
|
const meter = entfernung(punkt(48.5, 12.15), punkt(48.51, 12.15));
|
|
expect(meter).toBeGreaterThan(1100);
|
|
expect(meter).toBeLessThan(1120);
|
|
});
|
|
|
|
it("kennt keine Richtung — hin ist so weit wie zurück", () => {
|
|
const hin = entfernung(punkt(48.5, 12.15), punkt(48.52, 12.19));
|
|
const zurueck = entfernung(punkt(48.52, 12.19), punkt(48.5, 12.15));
|
|
expect(hin).toBe(zurueck);
|
|
});
|
|
|
|
it("ist null zwischen einem Punkt und sich selbst", () => {
|
|
expect(entfernung(punkt(48.5, 12.15), punkt(48.5, 12.15))).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("Druckverluste", () => {
|
|
it("trifft den Tabellenwert für den B-Schlauch", () => {
|
|
// 800 l/min im B-Schlauch: rund 1,5 bar je 100 Meter.
|
|
const verlust = reibungsverlust(100, 800, 75);
|
|
expect(verlust).toBeGreaterThanOrEqual(1.3);
|
|
expect(verlust).toBeLessThanOrEqual(1.7);
|
|
});
|
|
|
|
it("trifft den Tabellenwert für den A-Schlauch", () => {
|
|
// Derselbe Durchfluss im A-Schlauch kostet einen Bruchteil.
|
|
const verlust = reibungsverlust(100, 800, 110);
|
|
expect(verlust).toBeGreaterThan(0);
|
|
expect(verlust).toBeLessThan(0.4);
|
|
});
|
|
|
|
it("steigt mit dem Quadrat der Fördermenge", () => {
|
|
const einfach = reibungsverlust(100, 400, 75);
|
|
const doppelt = reibungsverlust(100, 800, 75);
|
|
|
|
// Doppelte Menge, vierfacher Verlust — das ist der Grund, warum man
|
|
// lieber einen Schlauch mehr legt als schneller fördert.
|
|
expect(doppelt / einfach).toBeGreaterThan(3.5);
|
|
expect(doppelt / einfach).toBeLessThan(4.5);
|
|
});
|
|
|
|
it("wächst gleichmäßig mit der Länge", () => {
|
|
const hundert = reibungsverlust(100, 800, 75);
|
|
const dreihundert = reibungsverlust(300, 800, 75);
|
|
|
|
expect(dreihundert).toBeCloseTo(hundert * 3, 1);
|
|
});
|
|
|
|
it("rechnet ohne Wasser keinen Verlust", () => {
|
|
expect(reibungsverlust(500, 0, 75)).toBe(0);
|
|
expect(reibungsverlust(0, 800, 75)).toBe(0);
|
|
});
|
|
|
|
it("rechnet zehn Höhenmeter als ein bar", () => {
|
|
expect(hoehenverlust(10)).toBe(1);
|
|
expect(hoehenverlust(35)).toBe(3.5);
|
|
});
|
|
|
|
it("gibt bergab den Druck zurück", () => {
|
|
expect(hoehenverlust(-20)).toBe(-2);
|
|
});
|
|
});
|
|
|
|
describe("Förderplan", () => {
|
|
it("kommt bei kurzer Strecke mit einer Pumpe aus", () => {
|
|
const plan = foerderplan({
|
|
punkte: [
|
|
{ ...punkt(48.5, 12.15, 400), bezeichnung: "Weiher" },
|
|
{ ...punkt(48.502, 12.15, 402), bezeichnung: "Einsatzstelle" },
|
|
],
|
|
durchmesser: 75,
|
|
foerdermenge: 800,
|
|
ausgangsdruck: 8,
|
|
eingangsdruck: 1.5,
|
|
});
|
|
|
|
expect(plan.machbar).toBe(true);
|
|
expect(plan.pumpen).toBe(1);
|
|
expect(plan.abschnitte).toHaveLength(1);
|
|
expect(plan.enddruck).toBeLessThan(8);
|
|
expect(plan.hinweise.join(" ")).toContain("Eine Pumpe genügt");
|
|
});
|
|
|
|
it("setzt eine Verstärkerpumpe, sobald der Druck nicht mehr reicht", () => {
|
|
// Ein Kilometer eben — das schafft eine Pumpe im B-Schlauch bei 800 l/min
|
|
// nicht: sie kommt rund 430 Meter weit.
|
|
const plan = foerderplan({
|
|
punkte: [
|
|
{ ...punkt(48.5, 12.15, 400), bezeichnung: "Entnahme" },
|
|
{ ...punkt(48.503, 12.15, 400), bezeichnung: "Feldweg" },
|
|
{ ...punkt(48.506, 12.15, 400), bezeichnung: "Kreuzung" },
|
|
{ ...punkt(48.509, 12.15, 400), bezeichnung: "Einsatzstelle" },
|
|
],
|
|
durchmesser: 75,
|
|
foerdermenge: 800,
|
|
ausgangsdruck: 8,
|
|
eingangsdruck: 1.5,
|
|
});
|
|
|
|
expect(plan.machbar).toBe(true);
|
|
expect(plan.pumpen).toBeGreaterThan(1);
|
|
expect(plan.abschnitte.some((abschnitt) => abschnitt.pumpeNoetig)).toBe(true);
|
|
expect(plan.hinweise.join(" ")).toMatch(/Verstärker/);
|
|
});
|
|
|
|
it("braucht im A-Schlauch weniger Pumpen als im B-Schlauch", () => {
|
|
const strecke = [
|
|
{ ...punkt(48.5, 12.15, 400), bezeichnung: "Entnahme" },
|
|
{ ...punkt(48.503, 12.15, 400), bezeichnung: "Mitte" },
|
|
{ ...punkt(48.506, 12.15, 400), bezeichnung: "Einsatzstelle" },
|
|
];
|
|
|
|
const b = foerderplan({
|
|
punkte: strecke,
|
|
durchmesser: 75,
|
|
foerdermenge: 800,
|
|
ausgangsdruck: 8,
|
|
eingangsdruck: 1.5,
|
|
});
|
|
const a = foerderplan({
|
|
punkte: strecke,
|
|
durchmesser: 110,
|
|
foerdermenge: 800,
|
|
ausgangsdruck: 8,
|
|
eingangsdruck: 1.5,
|
|
});
|
|
|
|
expect(a.pumpen).toBeLessThan(b.pumpen);
|
|
});
|
|
|
|
it("rechnet die Steigung mit", () => {
|
|
const eben = foerderplan({
|
|
punkte: [punkt(48.5, 12.15, 400), punkt(48.503, 12.15, 400)],
|
|
durchmesser: 75,
|
|
foerdermenge: 800,
|
|
ausgangsdruck: 8,
|
|
eingangsdruck: 1.5,
|
|
});
|
|
|
|
const bergauf = foerderplan({
|
|
punkte: [punkt(48.5, 12.15, 400), punkt(48.503, 12.15, 460)],
|
|
durchmesser: 75,
|
|
foerdermenge: 800,
|
|
ausgangsdruck: 8,
|
|
eingangsdruck: 1.5,
|
|
});
|
|
|
|
// 60 Höhenmeter kosten 6 bar — das ist mehr als die Reibung auf der
|
|
// Strecke und dreht das Ergebnis.
|
|
expect(bergauf.enddruck).toBeLessThan(eben.enddruck - 5);
|
|
expect(bergauf.hoehenunterschied).toBe(60);
|
|
});
|
|
|
|
it("gewinnt bergab Druck zurück", () => {
|
|
const plan = foerderplan({
|
|
punkte: [punkt(48.5, 12.15, 460), punkt(48.503, 12.15, 400)],
|
|
durchmesser: 75,
|
|
foerdermenge: 800,
|
|
ausgangsdruck: 8,
|
|
eingangsdruck: 1.5,
|
|
});
|
|
|
|
expect(plan.hoehenunterschied).toBe(-60);
|
|
expect(plan.hinweise.join(" ")).toContain("fällt deutlich ab");
|
|
});
|
|
|
|
it("sagt es, wenn ein Abschnitt selbst mit voller Pumpe nicht zu schaffen ist", () => {
|
|
// Über einen Kilometer am Stück im C-Schlauch: das geht nicht, und die
|
|
// Ausgabe soll das sagen statt eine Zahl zu erfinden.
|
|
const plan = foerderplan({
|
|
punkte: [
|
|
{ ...punkt(48.5, 12.15, 400), bezeichnung: "Entnahme" },
|
|
{ ...punkt(48.51, 12.15, 400), bezeichnung: "Einsatzstelle" },
|
|
],
|
|
durchmesser: 52,
|
|
foerdermenge: 400,
|
|
ausgangsdruck: 8,
|
|
eingangsdruck: 1.5,
|
|
});
|
|
|
|
expect(plan.machbar).toBe(false);
|
|
expect(plan.hinweise.join(" ")).toContain("zu lang für eine Pumpe");
|
|
});
|
|
|
|
it("zählt Schlauchlängen auf, statt Meter zu nennen", () => {
|
|
const plan = foerderplan({
|
|
punkte: [punkt(48.5, 12.15, 400), punkt(48.502, 12.15, 400)],
|
|
durchmesser: 75,
|
|
foerdermenge: 800,
|
|
ausgangsdruck: 8,
|
|
eingangsdruck: 1.5,
|
|
});
|
|
|
|
// Rund 222 Meter — das sind zwölf Längen, nicht elf komma eins.
|
|
expect(plan.laengen).toBe(Math.ceil(plan.gesamtlaenge / 20));
|
|
expect(Number.isInteger(plan.laengen)).toBe(true);
|
|
});
|
|
|
|
it("verlangt mindestens zwei Punkte", () => {
|
|
const plan = foerderplan({
|
|
punkte: [punkt(48.5, 12.15)],
|
|
durchmesser: 75,
|
|
foerdermenge: 800,
|
|
ausgangsdruck: 8,
|
|
eingangsdruck: 1.5,
|
|
});
|
|
|
|
expect(plan.machbar).toBe(false);
|
|
expect(plan.hinweise.join(" ")).toContain("Mindestens zwei Punkte");
|
|
});
|
|
|
|
it("rechnet ohne Höhenangaben mit ebenem Gelände", () => {
|
|
const plan = foerderplan({
|
|
punkte: [punkt(48.5, 12.15), punkt(48.502, 12.15)],
|
|
durchmesser: 75,
|
|
foerdermenge: 800,
|
|
ausgangsdruck: 8,
|
|
eingangsdruck: 1.5,
|
|
});
|
|
|
|
expect(plan.hoehenunterschied).toBe(0);
|
|
expect(plan.machbar).toBe(true);
|
|
});
|
|
|
|
it("sagt in jedem Fall dazu, dass es eine Planungshilfe ist", () => {
|
|
const plan = foerderplan({
|
|
punkte: [punkt(48.5, 12.15), punkt(48.502, 12.15)],
|
|
durchmesser: 75,
|
|
foerdermenge: 800,
|
|
ausgangsdruck: 8,
|
|
eingangsdruck: 1.5,
|
|
});
|
|
|
|
expect(plan.hinweise.at(-1)).toContain("Planungshilfe");
|
|
});
|
|
});
|