Kartenmodule ergaenzen
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e1480c0c3e
commit
1c1e141666
+279
-1
@@ -13,7 +13,9 @@
|
||||
* Gerätehaus und Schutzkleidung in der Reinigung — damit die Terminalübersicht
|
||||
* beim ersten Start nicht leer ist. Ab Phase 5 dazu: G26-Untersuchungen und
|
||||
* Belastungsübungen der Atemschutzgeräteträger und eine abgeschlossene
|
||||
* Überwachung mit vollständigem Protokoll.
|
||||
* Überwachung mit vollständigem Protokoll. Ab Phase 6 dazu: Koordinaten für
|
||||
* die Standorte, 24 Löschwasserstellen im Ausrückegebiet und drei Objekte mit
|
||||
* Feuerwehrplan.
|
||||
*
|
||||
* Das Skript läuft über DIRECT_DATABASE_URL, also mit dem Eigentümer-Zugang,
|
||||
* der Row Level Security umgehen darf. Anders ginge es nicht: der Seed legt
|
||||
@@ -52,6 +54,9 @@ import {
|
||||
QualifikationsArt,
|
||||
RolleSchluessel,
|
||||
SignaturRolle,
|
||||
HydrantStatus,
|
||||
HydrantTyp,
|
||||
ObjektKategorie,
|
||||
StandortArt,
|
||||
TruppStatus,
|
||||
UebungsArt,
|
||||
@@ -2479,6 +2484,276 @@ async function atemschutzAnlegen(organisationId: string) {
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Phase 6 — Kartenmodule
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/** Der Ortskern von Musterhausen — frei erfunden, aber in Niederbayern plausibel. */
|
||||
const ORTSMITTE = { breitengrad: 48.5, laengengrad: 12.15 };
|
||||
|
||||
/**
|
||||
* Löschwasser und Objekte.
|
||||
*
|
||||
* Die Hydranten liegen in einem Raster um den Ortskern, mit den Zuständen, die
|
||||
* eine Wehr tatsächlich vorfindet: die meisten in Ordnung, einige ungeprüft,
|
||||
* zwei defekt. Dazu ein Weiher und eine Zisterne am Ortsrand — im Außenbereich
|
||||
* gibt es keine Leitung.
|
||||
*
|
||||
* Die drei Objekte sind die, für die eine Wehr zuerst einen Plan schreibt:
|
||||
* Schule, Pflegeheim, Aussiedlerhof.
|
||||
*/
|
||||
async function kartendatenAnlegen(organisationId: string) {
|
||||
const vorhanden = await prisma.hydrant.count({ where: { organisationId } });
|
||||
if (vorhanden > 0) {
|
||||
log(`Karte: bereits ${vorhanden} Löschwasserstellen vorhanden`);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Standorte auf die Karte holen ---------------------------------------
|
||||
//
|
||||
// Ohne Koordinaten am Gerätehaus wüsste die Karte nicht, wohin sie schauen
|
||||
// soll. Die Standorte gibt es seit Phase 1, die Dezimalgrade seit Phase 6.
|
||||
const standorte = await prisma.standort.findMany({
|
||||
where: { organisationId, geloeschtAm: null },
|
||||
select: { id: true },
|
||||
orderBy: { erstelltAm: "asc" },
|
||||
});
|
||||
|
||||
for (const [index, standort] of standorte.entries()) {
|
||||
await prisma.standort.update({
|
||||
where: { id: standort.id },
|
||||
data: {
|
||||
breitengrad: ORTSMITTE.breitengrad + index * 0.004,
|
||||
laengengrad: ORTSMITTE.laengengrad + index * 0.006,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --- Löschwasserstellen ---------------------------------------------------
|
||||
const STRASSEN = [
|
||||
"Hauptstraße",
|
||||
"Bahnhofstraße",
|
||||
"Schulstraße",
|
||||
"Kirchplatz",
|
||||
"Am Anger",
|
||||
"Industriestraße",
|
||||
];
|
||||
|
||||
const hydranten: Array<{
|
||||
bezeichnung: string;
|
||||
typ: HydrantTyp;
|
||||
status: HydrantStatus;
|
||||
nennweite: number | null;
|
||||
durchfluss: number | null;
|
||||
strasse: string;
|
||||
breitengrad: number;
|
||||
laengengrad: number;
|
||||
letztePruefung: Date | null;
|
||||
hinweis: string | null;
|
||||
}> = [];
|
||||
|
||||
const heute = new Date();
|
||||
|
||||
function vorTagen(tage: number): Date {
|
||||
const datum = new Date(heute);
|
||||
datum.setUTCDate(datum.getUTCDate() - tage);
|
||||
datum.setUTCHours(0, 0, 0, 0);
|
||||
return datum;
|
||||
}
|
||||
|
||||
for (let nummer = 1; nummer <= 20; nummer += 1) {
|
||||
const strasse = STRASSEN[nummer % STRASSEN.length]!;
|
||||
// Ein Raster von rund 120 Metern Kantenlänge um die Ortsmitte.
|
||||
const reihe = Math.floor((nummer - 1) / 5);
|
||||
const spalte = (nummer - 1) % 5;
|
||||
|
||||
const ungeprueft = nummer % 7 === 0;
|
||||
const defekt = nummer === 9 || nummer === 17;
|
||||
|
||||
hydranten.push({
|
||||
bezeichnung: `H-${String(nummer).padStart(4, "0")}`,
|
||||
typ: nummer % 6 === 0 ? HydrantTyp.UEBERFLUR : HydrantTyp.UNTERFLUR,
|
||||
status: defekt
|
||||
? HydrantStatus.DEFEKT
|
||||
: ungeprueft
|
||||
? HydrantStatus.UNBEKANNT
|
||||
: HydrantStatus.EINSATZBEREIT,
|
||||
nennweite: nummer % 4 === 0 ? 150 : nummer % 3 === 0 ? 80 : 100,
|
||||
durchfluss: nummer % 4 === 0 ? 1600 : nummer % 3 === 0 ? 400 : 800,
|
||||
strasse,
|
||||
breitengrad: ORTSMITTE.breitengrad + reihe * 0.0011 - 0.002,
|
||||
laengengrad: ORTSMITTE.laengengrad + spalte * 0.0016 - 0.003,
|
||||
letztePruefung: ungeprueft ? null : vorTagen(60 + nummer * 9),
|
||||
hinweis: defekt
|
||||
? "Deckel verzogen, Standrohr passt nicht. Stadtwerke gemeldet."
|
||||
: nummer === 3
|
||||
? "Im Winter oft zugeschoben — Schneepflug fährt hier durch."
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
// Im Außenbereich gibt es keine Leitung.
|
||||
hydranten.push(
|
||||
{
|
||||
bezeichnung: "Weiher Kiesgrube",
|
||||
typ: HydrantTyp.OFFENES_GEWAESSER,
|
||||
status: HydrantStatus.EINSATZBEREIT,
|
||||
nennweite: null,
|
||||
durchfluss: 2400,
|
||||
strasse: "Kiesgrubenweg",
|
||||
breitengrad: ORTSMITTE.breitengrad - 0.008,
|
||||
laengengrad: ORTSMITTE.laengengrad + 0.012,
|
||||
letztePruefung: vorTagen(200),
|
||||
hinweis: "Zufahrt für LF geeignet, im Winter vereist. Saughöhe rund 3 m.",
|
||||
},
|
||||
{
|
||||
bezeichnung: "Zisterne Aussiedlerhof",
|
||||
typ: HydrantTyp.ZISTERNE,
|
||||
status: HydrantStatus.EINSATZBEREIT,
|
||||
nennweite: null,
|
||||
durchfluss: 800,
|
||||
strasse: "Hofweg 2",
|
||||
breitengrad: ORTSMITTE.breitengrad + 0.011,
|
||||
laengengrad: ORTSMITTE.laengengrad + 0.018,
|
||||
letztePruefung: vorTagen(120),
|
||||
hinweis: "50 m³, Saugstutzen an der Nordseite der Scheune.",
|
||||
},
|
||||
{
|
||||
bezeichnung: "Saugstelle Mühlbach",
|
||||
typ: HydrantTyp.SAUGSTELLE,
|
||||
status: HydrantStatus.EINGESCHRAENKT,
|
||||
nennweite: null,
|
||||
durchfluss: 600,
|
||||
strasse: "Mühlweg",
|
||||
breitengrad: ORTSMITTE.breitengrad - 0.004,
|
||||
laengengrad: ORTSMITTE.laengengrad - 0.009,
|
||||
letztePruefung: vorTagen(400),
|
||||
hinweis: "Im Sommer zu wenig Wasser. Nur nach Regen brauchbar.",
|
||||
},
|
||||
{
|
||||
bezeichnung: "H-0021",
|
||||
typ: HydrantTyp.UEBERFLUR,
|
||||
status: HydrantStatus.EINSATZBEREIT,
|
||||
nennweite: 150,
|
||||
durchfluss: 1600,
|
||||
strasse: "Industriestraße",
|
||||
breitengrad: ORTSMITTE.breitengrad + 0.006,
|
||||
laengengrad: ORTSMITTE.laengengrad + 0.008,
|
||||
letztePruefung: vorTagen(45),
|
||||
hinweis: "Am Werkstor, ergiebigster Hydrant im Ort.",
|
||||
},
|
||||
);
|
||||
|
||||
for (const hydrant of hydranten) {
|
||||
await prisma.hydrant.create({
|
||||
data: {
|
||||
organisationId,
|
||||
...hydrant,
|
||||
betreiber: "Stadtwerke Musterhausen",
|
||||
quelle: "seed",
|
||||
externeNummer: `SW-${hydrant.bezeichnung}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --- Objekte mit Feuerwehrplan -------------------------------------------
|
||||
const OBJEKTE: Array<{
|
||||
name: string;
|
||||
kategorie: ObjektKategorie;
|
||||
strasse: string;
|
||||
hausnummer: string;
|
||||
breitengrad: number;
|
||||
laengengrad: number;
|
||||
personenzahl: number;
|
||||
zufahrt: string;
|
||||
gefahren: string;
|
||||
schluesseldepot: string;
|
||||
loeschwasser: string;
|
||||
kontakte: Array<{ name: string; funktion: string; telefon: string; jederzeit: boolean }>;
|
||||
}> = [
|
||||
{
|
||||
name: "Grundschule Musterhausen",
|
||||
kategorie: ObjektKategorie.SCHULE,
|
||||
strasse: "Schulstraße",
|
||||
hausnummer: "4",
|
||||
breitengrad: ORTSMITTE.breitengrad + 0.0015,
|
||||
laengengrad: ORTSMITTE.laengengrad + 0.0022,
|
||||
personenzahl: 180,
|
||||
zufahrt:
|
||||
"Aufstellfläche auf dem Pausenhof, Zufahrt über die Schulstraße. Poller mit Feuerwehrschlüssel, Durchfahrt 3,10 m.",
|
||||
gefahren:
|
||||
"Photovoltaik auf dem Süddach (Gleichstrom bis zum Wechselrichter im Keller). Chemieraum im 1. OG Nordseite.",
|
||||
schluesseldepot: "FSD rechts neben dem Haupteingang, Schlüssel für alle Türen.",
|
||||
loeschwasser: "Wandhydranten in jedem Geschoss, Steigleitung trocken im Treppenhaus B.",
|
||||
kontakte: [
|
||||
{ name: "Ursula Berger", funktion: "Schulleitung", telefon: "08721 4711", jederzeit: false },
|
||||
{ name: "Karl Steininger", funktion: "Hausmeister", telefon: "0170 1234567", jederzeit: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Seniorenheim St. Anna",
|
||||
kategorie: ObjektKategorie.PFLEGEHEIM,
|
||||
strasse: "Am Anger",
|
||||
hausnummer: "11",
|
||||
breitengrad: ORTSMITTE.breitengrad - 0.0018,
|
||||
laengengrad: ORTSMITTE.laengengrad + 0.0009,
|
||||
personenzahl: 84,
|
||||
zufahrt:
|
||||
"Anleiterbare Seite nur nach Norden. Aufstellfläche für DL im Innenhof, Einfahrt an der Ostseite.",
|
||||
gefahren:
|
||||
"62 Bewohner, davon 28 bettlägerig. Sauerstoffflaschen in den Zimmern 12, 14 und 31. Sammelplatz auf dem Parkplatz an der Ostseite.",
|
||||
schluesseldepot: "FSD links vom Haupteingang, dazu Objektschlüssel in der Pforte.",
|
||||
loeschwasser: "Wandhydranten je Geschoss, nächster Überflurhydrant 40 m westlich.",
|
||||
kontakte: [
|
||||
{ name: "Pflegedienstleitung", funktion: "Nachtdienst", telefon: "08721 5522", jederzeit: true },
|
||||
{ name: "Hermann Falk", funktion: "Heimleitung", telefon: "0171 9876543", jederzeit: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Aussiedlerhof Huber",
|
||||
kategorie: ObjektKategorie.LANDWIRTSCHAFT,
|
||||
strasse: "Hofweg",
|
||||
hausnummer: "2",
|
||||
breitengrad: ORTSMITTE.breitengrad + 0.0108,
|
||||
laengengrad: ORTSMITTE.laengengrad + 0.0178,
|
||||
personenzahl: 6,
|
||||
zufahrt:
|
||||
"Feldweg ab der Kreisstraße, 600 m unbefestigt. Für TLF befahrbar, bei Nässe nur mit Allrad.",
|
||||
gefahren:
|
||||
"Silo mit Gärgasen, Biogasanlage 80 m nordöstlich. 60 Rinder im Stall, Notausgang Südseite. PV auf allen Dächern.",
|
||||
schluesseldepot: "Kein FSD. Familie wohnt auf dem Hof, Schlüssel beim Betriebsleiter.",
|
||||
loeschwasser: "Zisterne 50 m³ an der Scheune, sonst kein Löschwasser im Umkreis von 1,5 km.",
|
||||
kontakte: [
|
||||
{ name: "Josef Huber", funktion: "Betriebsleiter", telefon: "0175 3344556", jederzeit: true },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for (const vorlage of OBJEKTE) {
|
||||
const { kontakte, ...felder } = vorlage;
|
||||
|
||||
await prisma.objekt.create({
|
||||
data: {
|
||||
organisationId,
|
||||
...felder,
|
||||
plz: "84359",
|
||||
ort: "Musterhausen",
|
||||
kontakte: {
|
||||
create: kontakte.map((kontakt, index) => ({
|
||||
organisationId,
|
||||
...kontakt,
|
||||
reihenfolge: index,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
log(
|
||||
`Karte: ${hydranten.length} Löschwasserstellen, ${OBJEKTE.length} Objekte mit Ansprechpartnern, ${standorte.length} Standorte verortet`,
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Ablauf
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -2516,6 +2791,9 @@ async function main(): Promise<void> {
|
||||
// Phase 5 — Atemschutz nach FwDV 7
|
||||
await atemschutzAnlegen(organisation.id);
|
||||
|
||||
// Phase 6 — Kartenmodule
|
||||
await kartendatenAnlegen(organisation.id);
|
||||
|
||||
log("Fertig.");
|
||||
log("");
|
||||
log("Anmeldung für den Erststart:");
|
||||
|
||||
Reference in New Issue
Block a user