bwsamern-ohne_standortplaner/server.js

286 lines
11 KiB
JavaScript

require('dotenv').config();
const express = require('express');
const { Pool } = require('pg');
const bodyParser = require('body-parser');
const cors = require('cors');
const path = require('path');
const app = express();
const port = process.env.PORT || 3000;
app.use(cors());
app.use(bodyParser.json());
// Serve static files from the root directory
app.use(express.static(path.join(__dirname, './')));
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD
});
// In-memory logs for remote debugging
const serverLogs = [];
function log(msg) {
const entry = `[${new Date().toISOString()}] ${msg}`;
console.log(entry);
serverLogs.push(entry);
if (serverLogs.length > 50) serverLogs.shift();
}
// Status-Endpunkt für Diagnose
app.get('/api/status', async (req, res) => {
try {
const result = await pool.query('SELECT NOW()');
res.json({
status: 'online',
database: 'connected',
time: result.rows[0].now,
schema: process.env.DB_SCHEMA || 'geodaten'
});
} catch (err) {
res.status(500).json({ status: 'error', database: 'disconnected', error: err.message });
}
});
app.get('/api/logs', (req, res) => {
res.type('text/plain').send(serverLogs.join('\n'));
});
// API to save turbines
app.post('/api/wea', async (req, res) => {
const { projekt_id, turbines } = req.body;
const targetProject = projekt_id || 'BWSamern-Ohne';
const schema = process.env.DB_SCHEMA || 'geodaten';
log(`Empfange Save-Request für Projekt: ${targetProject} (${turbines?.length || 0} WEAs)`);
const client = await pool.connect();
try {
await client.query('BEGIN');
// 1. Bestehende WEAs für dieses Projekt löschen
const delRes = await client.query(
`DELETE FROM ${schema}.wea_standorte WHERE projekt_id = $1`,
[targetProject]
);
log(`Gelöscht: ${delRes.rowCount} bestehende Anlagen.`);
// 2. Neue WEAs einfügen
if (turbines && turbines.length > 0) {
for (const t of turbines) {
log(`Füge WEA ein: Nr=${t.nr}, Typ=${t.type}, Pos=${t.latlng?.lat},${t.latlng?.lng}`);
await client.query(
`INSERT INTO ${schema}.wea_standorte
(projekt_id, wea_nummer, anlagentyp, nabenhoehe, rotordurchmesser, ksf_drehung, variante, geom)
VALUES ($1, $2, $3, $4, $5, $6, $7, ST_Transform(ST_SetSRID(ST_MakePoint($8, $9), 4326), 25832))`,
[targetProject, t.nr, t.type, t.hh, t.rd, t.ksfAngle, t.variant, t.latlng.lng, t.latlng.lat]
);
}
}
await client.query('COMMIT');
log(`Erfolgreich gespeichert: ${turbines?.length || 0} WEAs.`);
res.json({ message: `${turbines?.length || 0} WEAs erfolgreich in Datenbank gespeichert.` });
} catch (err) {
await client.query('ROLLBACK');
log(`FEHLER beim Speichern: ${err.message}`);
res.status(500).json({ error: 'Datenbankfehler beim Speichern', details: err.message });
} finally {
client.release();
}
});
// API to load turbines
app.get('/api/wea/:projekt_id', async (req, res) => {
const { projekt_id } = req.params;
log(`Lade WEAs für Projekt: ${projekt_id}`);
try {
const result = await pool.query(
`SELECT wea_nummer as nr, anlagentyp as type, nabenhoehe as hh, rotordurchmesser as rd, ksf_drehung as ksfAngle, variante as variant,
ST_X(ST_Transform(geom, 4326)) as lng, ST_Y(ST_Transform(geom, 4326)) as lat
FROM geodaten.wea_standorte WHERE projekt_id = $1`,
[projekt_id]
);
log(`Geladen: ${result.rowCount} Anlagen.`);
res.status(200).json(result.rows);
} catch (e) {
log(`FEHLER beim Laden: ${e.message}`);
res.status(500).json({ error: e.message });
}
});
// Hilfsfunktion zur Auflösung von Projekt-ID oder Name
async function resolveProjectId(client, input) {
if (!input) return null;
// Prüfen, ob input eine valide UUID ist
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (uuidRegex.test(input)) return input;
// Normalisierte Suche (Kleinschreibung, alle Bindestriche und Unterstriche entfernen)
const normalized = input.toLowerCase().replace(/[-_]/g, '');
const res = await client.query(
'SELECT id FROM geodaten.projekte WHERE LOWER(REPLACE(REPLACE(name, \'-\', \'\'), \'_\', \'\')) = $1',
[normalized]
);
return res.rows.length > 0 ? res.rows[0].id : null;
}
// API zur Flächensicherung von Eigentümern
app.post('/api/sicherung', async (req, res) => {
const { nachname, vorname, projekt_id, status, notiz } = req.body;
const targetStatus = status || 'Gesichert';
const schema = process.env.DB_SCHEMA || 'geodaten';
log(`Sicherungs-Request: ${vorname} ${nachname} Status='${targetStatus}' Notiz='${notiz}'`);
const client = await pool.connect();
try {
await client.query('BEGIN');
// Suche nach dem Eigentümer in flaecheneigentuemer_alkis und aktualisiere status und notiz
const searchNachname = (nachname || '').trim();
const searchVorname = (vorname || '').trim();
// Normalisierte Suche (TRIM und COALESCE für ALKIS-Daten)
const updateRes = await client.query(
`UPDATE ${schema}.flaecheneigentuemer_alkis
SET status = $1, notiz = $2
WHERE
(TRIM(COALESCE("GNA", '')) = TRIM(COALESCE($3, '')) AND TRIM(COALESCE("VNA", '')) = TRIM(COALESCE($4, ''))) OR
(TRIM(COALESCE("VNA", '')) = TRIM(COALESCE($5, '')) AND TRIM(COALESCE("GNA", '')) = '') OR
(TRIM(COALESCE("GNA", '')) = TRIM(COALESCE($5, '')) AND TRIM(COALESCE("VNA", '')) = '') OR
(LOWER(TRIM(COALESCE("GNA", ''))) = LOWER(TRIM(COALESCE($3, ''))) AND LOWER(TRIM(COALESCE("VNA", ''))) = LOWER(TRIM(COALESCE($4, ''))))`,
[targetStatus, notiz, searchNachname, searchVorname, `${searchVorname} ${searchNachname}`.trim()]
);
if (updateRes.rowCount === 0) {
log(`Kein Eigentümer für ${vorname} ${nachname} gefunden.`);
await client.query('ROLLBACK');
return res.status(404).json({ error: `Eigentümer '${vorname} ${nachname}' nicht in der ALKIS-Tabelle gefunden.` });
}
await client.query('COMMIT');
log(`Erfolgreich: ${updateRes.rowCount || 1} Einträge in ALKIS aktualisiert.`);
res.json({
message: `Eigentümer ${vorname} ${nachname} erfolgreich aktualisiert.`,
count: updateRes.rowCount
});
} catch (err) {
await client.query('ROLLBACK');
log(`FEHLER bei Sicherung: ${err.message}`);
res.status(500).json({ error: 'Fehler bei der Flächensicherung', details: err.message });
} finally {
client.release();
}
});
// API zum Laden der Sicherungsstände
app.get('/api/sicherung/:projekt_id', async (req, res) => {
const { projekt_id } = req.params;
log(`Lade Sicherungsstände für Projekt: ${projekt_id}`);
const client = await pool.connect();
try {
// Wir laden nun direkt aus der ALKIS Tabelle, wie vom User gewünscht
const result = await client.query(
`SELECT "VNA" as vorname, "GNA" as nachname, status, notiz
FROM geodaten.flaecheneigentuemer_alkis
WHERE status IS NOT NULL OR notiz IS NOT NULL`
);
log(`Geladen: ${result.rowCount} Status-Einträge aus ALKIS.`);
res.json(result.rows);
} catch (e) {
log(`FEHLER beim Laden der Stände: ${e.message}`);
res.status(500).json({ error: e.message });
} finally {
client.release();
}
});
// NEU: API zum Laden des ALKIS-Layers als GeoJSON
app.get('/api/layers/alkis', async (req, res) => {
log("Lade ALKIS-Layer aus Datenbank...");
try {
const result = await pool.query(
`SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', jsonb_agg(features.feature)
)
FROM (
SELECT jsonb_build_object(
'type', 'Feature',
'id', id,
'geometry', ST_AsGeoJSON(ST_Transform(geom, 4326))::jsonb,
'properties', jsonb_build_object(
'id', id,
'VNA', "VNA",
'GNA', "GNA",
'FSK', "FSK",
'PLZ', "PLZ",
'ORP', "ORP",
'STR', "STR",
'status', status,
'notiz', notiz
)
) AS feature
FROM geodaten.flaecheneigentuemer_alkis
) features`
);
res.json(result.rows[0].jsonb_build_object);
} catch (err) {
log(`FEHLER beim Laden des ALKIS-Layers: ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// API für Projekt-Statistiken (Fortschrittsanzeige)
app.get('/api/stats/:projekt_id', async (req, res) => {
const { projekt_id } = req.params;
log(`Lade Statistiken für Projekt: ${projekt_id}`);
const client = await pool.connect();
try {
const resolvedPid = await resolveProjectId(client, projekt_id);
if (!resolvedPid) {
throw new Error(`Projekt '${projekt_id}' konnte nicht gefunden werden.`);
}
// Verwende die vom User vorgeschlagene Logik, aber mit resolvedPid
// Wir gruppieren nach aktueller_status (da dies in der View v_projekt_sicherung so heißt)
const result = await client.query(
`SELECT
aktueller_status as status,
count(*) as anzahl,
ROUND(SUM(ST_Area(geom)) / 10000, 2) as hektar
FROM geodaten.v_projekt_sicherung
WHERE projekt_id = $1
GROUP BY aktueller_status`,
[resolvedPid]
);
log(`Statistiken berechnet: ${result.rows.length} Status-Gruppen.`);
res.json(result.rows);
} catch (e) {
log(`FEHLER beim Laden der Statistiken: ${e.message}`);
res.status(500).json({ error: e.message });
} finally {
client.release();
}
});
// Catch-all route to serve index.html for any other request (optional, good for SPAs)
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
app.listen(port, '0.0.0.0', () => {
console.log(`Server running at http://0.0.0.0:${port}`);
});