wind_tool_standortpruefung/server.cjs

514 lines
20 KiB
JavaScript

const express = require('express');
const cors = require('cors');
// Polyfill for shpjs which expects self.DecompressionStream
global.self = global;
const proj4 = require('proj4');
const path = require('path');
const fs = require('fs');
const https = require('https');
const app = express();
const PORT = 3000;
app.use(cors());
app.use(express.static('dist'));
// Define Projections
proj4.defs("EPSG:25832", "+proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs");
proj4.defs("EPSG:4326", "+proj=longlat +datum=WGS84 +no_defs");
const sourceProj = "EPSG:25832";
const destProj = "EPSG:4326";
const ALKIS_WFS_URL = 'https://www.wfs.nrw.de/geobasis/wfs_nw_alkis_vereinfacht';
const SHP_PATH = path.join(__dirname, 'public/Daten/260130_Wohngebäude.shp');
app.get('/api/buildings', async (req, res) => {
let { bbox, limit } = req.query;
// Quick test endpoint via browser simply loading /api/buildings
if (!bbox) return res.send('Worker is ready. Use ?bbox=minLon,minLat,maxLon,maxLat');
const bounds = bbox.split(',').map(Number);
if (bounds.length !== 4) return res.status(400).json({ error: 'Invalid bbox' });
console.time("Query");
// 1. Project Query BBOX to UTM32
const corners = [
[bounds[0], bounds[1]], [bounds[2], bounds[3]],
[bounds[0], bounds[3]], [bounds[2], bounds[1]]
].map(c => proj4(destProj, sourceProj, c));
const minX = Math.min(...corners.map(c => c[0]));
const maxX = Math.max(...corners.map(c => c[0]));
const minY = Math.min(...corners.map(c => c[1]));
const maxY = Math.max(...corners.map(c => c[1]));
const queryBbox = [minX - 50, minY - 50, maxX + 50, maxY + 50];
const maxFeatures = limit ? parseInt(limit) : 2000;
const results = [];
// 2. Buffered Scanning
const BUFFER_SIZE = 10 * 1024 * 1024; // 10MB chunk
// We open the file and read large chunks.
// We maintain a "buffer cursor" and a "file position".
let fd;
try {
fd = fs.openSync(SHP_PATH, 'r');
const fileSize = fs.statSync(SHP_PATH).size;
let buf = Buffer.alloc(BUFFER_SIZE);
// Initial Read: Skip 100 bytes header
let filePos = 100;
let bytesRead = fs.readSync(fd, buf, 0, BUFFER_SIZE, filePos);
let bufPos = 0; // Position within current buffer
while (filePos < fileSize && results.length < maxFeatures && bytesRead > 0) {
// Need at least 8 bytes for Record Header
if (bufPos + 8 > bytesRead) {
// Not enough bytes left in buffer for header.
// Move remaining to start, read more.
// Or simply: Refill logic.
// Simplest: If near end, just read a new chunk from filePos.
// But filePos is tricky if we processed partial buffer.
// Let's track absolute `currentRecordPos` = filePos + bufPos
}
// SIMPLER LOGIC:
// Valid data is in buf[bufPos ... bytesRead]
const remaining = bytesRead - bufPos;
if (remaining < 8) {
// Fetch next chunk
// Determine where we are in file: filePos + bufPos
filePos += bufPos;
bytesRead = fs.readSync(fd, buf, 0, BUFFER_SIZE, filePos);
bufPos = 0;
if (bytesRead < 8) break; // EOF
}
// Read Record Header (8 bytes)
// Record Num (4 bytes BE) - ignore
const recLenWords = buf.readInt32BE(bufPos + 4);
const recLenBytes = recLenWords * 2;
// Check if we have the full record content in buffer
// 8 header + recLenBytes content
const totalRecSize = 8 + recLenBytes;
if (bufPos + totalRecSize > bytesRead) {
// Record spans across buffer boundary (or is larger than buffer remaining)
// Check if it's larger than entire buffer?
if (totalRecSize > BUFFER_SIZE) {
// Huge record. Rare for buildings. Skip or alloc special.
// Just skip logic:
filePos += bufPos + totalRecSize;
// Refill
bytesRead = fs.readSync(fd, buf, 0, BUFFER_SIZE, filePos);
bufPos = 0;
continue;
}
// Move pointer to filePos += bufPos
filePos += bufPos;
// Read fresh chunk starting at this record
bytesRead = fs.readSync(fd, buf, 0, BUFFER_SIZE, filePos);
bufPos = 0;
if (bytesRead < totalRecSize) break; // Should not happen unless EOF
}
const recOffset = filePos + bufPos;
// Now buf[bufPos] is start of header.
// buf[bufPos+8] is start of content.
const contentOffset = bufPos + 8;
// Parse BBox (32 bytes) at Content+4 (Type)
// Type is at contentOffset (4 bytes LE)
const type = buf.readInt32LE(contentOffset);
// BBox: offset + 4
const bXmin = buf.readDoubleLE(contentOffset + 4);
const bYmin = buf.readDoubleLE(contentOffset + 12);
const bXmax = buf.readDoubleLE(contentOffset + 20);
const bYmax = buf.readDoubleLE(contentOffset + 28);
const disjoint = (bXmin > queryBbox[2] || bXmax < queryBbox[0] || bYmin > queryBbox[3] || bYmax < queryBbox[1]);
if (!disjoint) {
// Match! Parse Geometry.
// Only if Type 5 (Polygon)
if (type === 5) {
const numParts = buf.readInt32LE(contentOffset + 36);
const numPoints = buf.readInt32LE(contentOffset + 40);
if (numPoints > 0 && numPoints < 10000) {
const parts = [];
for (let i = 0; i < numParts; i++) {
parts.push(buf.readInt32LE(contentOffset + 44 + i * 4));
}
parts.push(numPoints);
const pointsOffset = contentOffset + 44 + numParts * 4;
const coords = [];
for (let p = 0; p < numParts; p++) {
const start = parts[p];
const end = parts[p + 1];
const ring = [];
for (let k = start; k < end; k++) {
const px = buf.readDoubleLE(pointsOffset + k * 16);
const py = buf.readDoubleLE(pointsOffset + k * 16 + 8);
const [lon, lat] = proj4(sourceProj, destProj, [px, py]);
ring.push([lon, lat]);
}
coords.push(ring);
}
results.push({
type: "Feature",
properties: { id: "SHP_" + recOffset },
geometry: { type: "Polygon", coordinates: coords }
});
}
}
}
// Advance
bufPos += totalRecSize;
}
} catch (e) {
console.error("Fehler beim Scannen des Buffers:", e);
} finally {
if (fd) fs.closeSync(fd);
}
console.timeEnd("Query");
console.log(`BBox-Abfrage fand ${results.length} Gebäude`);
res.json({ type: "FeatureCollection", features: results });
});
/**
* API: Ruft detaillierte ALKIS-Informationen für einen Punkt ab (GML-Parsing)
*/
app.get('/api/alkis-info', async (req, res) => {
const { lat, lng } = req.query;
if (!lat || !lng) return res.status(400).json({ error: 'Koordinaten fehlen' });
const lats = parseFloat(lat);
const lngs = parseFloat(lng);
const size = 0.0001;
const bbox = `${lats - size},${lngs - size},${lats + size},${lngs + size},urn:ogc:def:crs:EPSG::4326`;
const url = `${ALKIS_WFS_URL}?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=ave:Flurstueck&BBOX=${bbox}&COUNT=1`;
https.get(url, (wfsRes) => {
let data = '';
wfsRes.on('data', (chunk) => data += chunk);
wfsRes.on('end', () => {
try {
// Einfaches Regex-basiertes Parsing für GML (da kein schwerer XML-Parser installiert ist)
const getTag = (tag) => {
const match = data.match(new RegExp(`<${tag}[^>]*>(.*?)<\/${tag}>`, 's'));
return match ? match[1].trim() : null;
};
const info = {
gemarkung: getTag('gemarkung'),
gemeinde: getTag('gemeinde'),
kreis: getTag('kreis'),
regbezirk: getTag('regbezirk'),
flur: getTag('flur'),
flstnrzae: getTag('flstnrzae'),
flstnrnen: getTag('flstnrnen'),
flaeche: getTag('flaeche'),
aktualit: getTag('aktualit'),
lagebeztxt: getTag('lagebeztxt'),
flstkennz: getTag('flstkennz')
};
if (!info.flstkennz) {
return res.json({ found: false });
}
res.json({ found: true, properties: info });
} catch (err) {
res.status(500).json({ error: 'Parsing Fehler', details: err.message });
}
});
}).on('error', (e) => {
res.status(500).json({ error: 'WFS Fehler', details: e.message });
});
});
/**
* API: Ruft ALKIS-Nutzungsinformationen für einen Punkt ab
*/
app.get('/api/alkis-usage', async (req, res) => {
const { lat, lng } = req.query;
if (!lat || !lng) return res.status(400).json({ error: 'Koordinaten fehlen' });
const lats = parseFloat(lat);
const lngs = parseFloat(lng);
const size = 0.00001; // Sehr kleiner BBOX für präzise Punkt-Abfrage (~1m)
const bbox = `${lats - size},${lngs - size},${lats + size},${lngs + size},urn:ogc:def:crs:EPSG::4326`;
const url = `${ALKIS_WFS_URL}?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=ave:Nutzung&BBOX=${bbox}&COUNT=1`;
https.get(url, (wfsRes) => {
let data = '';
wfsRes.on('data', (chunk) => data += chunk);
wfsRes.on('end', () => {
try {
// Regex-Parsing für nutzart und bez
const nutzartMatch = data.match(/<nutzart[^>]*>(.*?)<\/nutzart>/s);
const bezMatch = data.match(/<bez[^>]*>(.*?)<\/bez>/s);
const nutzart = nutzartMatch ? nutzartMatch[1].trim() : null;
const bez = bezMatch ? bezMatch[1].trim() : null;
if (!nutzart && !bez) {
return res.json({ found: false });
}
res.json({ found: true, nutzart, bez });
} catch (err) {
res.status(500).json({ error: 'Parsing Fehler', details: err.message });
}
});
}).on('error', (e) => {
res.status(500).json({ error: 'WFS Fehler', details: e.message });
});
});
/**
* API: Ruft Windenergieanlagen in der Nähe ab (WFS Proxy)
*/
app.get('/api/wind-turbines', async (req, res) => {
const { lat, lng } = req.query;
if (!lat || !lng) return res.status(400).json({ error: 'Koordinaten fehlen' });
const lats = parseFloat(lat);
const lngs = parseFloat(lng);
const size = 0.05; // ~5.5km BBOX für absolute Sicherheit
const bbox = `${lats - size},${lngs - size},${lats + size},${lngs + size},urn:ogc:def:crs:EPSG::4326`;
// Wir nutzen WFS 2.0.0 da dieser stabil Daten liefert
const url = `https://www.wfs.nrw.de/umwelt/erneuerbare_energien_wfs?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=erneuerbare_energien_wfs:WFS_Windenergie&BBOX=${bbox}&COUNT=100`;
console.log(`[WindProxy] Fetching: ${url}`);
https.get(url, (wfsRes) => {
console.log(`[WindProxy] Status: ${wfsRes.statusCode}`);
let data = '';
wfsRes.on('data', (chunk) => data += chunk);
wfsRes.on('end', () => {
try {
if (data.includes('ExceptionReport')) {
console.error(`[WindProxy] WFS Exception: ${data.substring(0, 500)}`);
return res.status(500).json({ error: 'WFS Exception', details: 'Der Dienst hat einen Fehler gemeldet.' });
}
const features = [];
// Robustes Splitten (Regex handles attributes in member tag)
const blocks = data.split(/<wfs:member[^>]*>/);
for (let i = 1; i < blocks.length; i++) {
const block = blocks[i];
// Extrahiere UTM Koordinaten aus gml:pos
const posMatch = block.match(/<gml:pos[^>]*>(.*?)<\/gml:pos>/);
const ibjahrMatch = block.match(/<erneuerbare_energien_wfs:ibjahr[^>]*>(.*?)<\/erneuerbare_energien_wfs:ibjahr>/);
if (posMatch) {
const coords = posMatch[1].trim().split(/\s+/).map(Number);
const [lon, lat] = proj4(sourceProj, destProj, [coords[0], coords[1]]);
features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [lon, lat] // [Lon, Lat] in WGS84
},
properties: {
ibjahr: ibjahrMatch ? ibjahrMatch[1].trim() : "Unbekannt"
}
});
}
}
console.log(`[WindProxy] Found ${features.length} turbines for ${lat},${lng}`);
res.json({ features });
} catch (err) {
res.status(500).json({ error: 'Parsing Fehler', details: err.message });
}
});
}).on('error', (e) => {
res.status(500).json({ error: 'WFS Fehler', details: e.message });
});
});
// --- Logik für Potenzialflächen & Regionalplanung ---
const POTENTIAL_ZIP_PATH = path.join(__dirname, 'public/daten/Potenzialkarten-Windenergieanlagen-NRW_EPSG25832_Shape.zip');
const PLANNING_ZIP_PATH = path.join(__dirname, 'public/daten/Windenergiebereiche_NRW.zip');
let potentialFeatures = [];
let potentialLoaded = false;
let planningFeatures = [];
let planningLoaded = false;
let planningError = null;
let turf = null;
/**
* Prüft, ob ein Punkt innerhalb eines Polygons liegt (unter Verwendung von Turf.js)
*/
function checkPointInFeature(pt, feature) {
if (!turf) return false;
if (!feature || !feature.geometry) return false;
try {
const point = turf.point(pt); // pt muss [lng, lat] sein
if (feature.geometry.type === 'Polygon' || feature.geometry.type === 'MultiPolygon') {
return turf.booleanPointInPolygon(point, feature);
}
} catch (e) { }
return false;
}
/**
* Lädt die notwendigen Bibliotheken und Geodaten beim Serverstart
*/
(async () => {
try {
console.log("Lade Bibliotheken & Daten...");
const { default: shp } = await import('shpjs');
const turfModule = await import('@turf/turf');
turf = turfModule.default || turfModule;
// 1. Regionalplanung (Windenergiebereiche) laden
if (fs.existsSync(PLANNING_ZIP_PATH)) {
console.log(`[${new Date().toISOString()}] Lade Planungsdaten von: ${PLANNING_ZIP_PATH}`);
try {
const buffer = fs.readFileSync(PLANNING_ZIP_PATH);
const geojson = await shp(buffer);
if (Array.isArray(geojson)) {
geojson.forEach(fc => { if (fc.features) planningFeatures.push(...fc.features); });
} else {
planningFeatures = geojson.features;
}
planningLoaded = true;
console.log(`[${new Date().toISOString()}] Planungsdaten geladen: ${planningFeatures.length} Features.`);
} catch (err) {
planningError = err.message;
console.warn("Konnte Planungs-ZIP nicht laden:", err);
}
} else {
console.warn("Planungs-ZIP nicht gefunden:", PLANNING_ZIP_PATH);
planningError = "Datei nicht gefunden: " + PLANNING_ZIP_PATH;
}
// 2. Potenzialkarten laden
if (fs.existsSync(POTENTIAL_ZIP_PATH)) {
console.log(`[${new Date().toISOString()}] Lade Potenzialdaten...`);
const buffer = fs.readFileSync(POTENTIAL_ZIP_PATH);
const geojson = await shp(buffer);
if (Array.isArray(geojson)) {
geojson.forEach(fc => { if (fc.features) potentialFeatures.push(...fc.features); });
} else {
potentialFeatures = geojson.features;
}
potentialLoaded = true;
console.log(`[${new Date().toISOString()}] Potenzialdaten geladen: ${potentialFeatures.length} Features.`);
}
} catch (e) {
console.error("Fehler beim Laden der Daten / Libs:", e);
planningError = "Globaler Ladefehler: " + e.message;
}
})();
/**
* API: Prüft, ob ein Standort in einer LANUV-Potenzialfläche liegt
*/
app.get('/api/check-potential', (req, res) => {
const { lat, lng } = req.query;
if (!potentialLoaded) return res.json({ result: 'error', message: 'Daten nicht geladen' });
if (!lat || !lng) return res.json({ result: 'error', message: 'Koordinaten fehlen' });
try {
const pt = [parseFloat(lng), parseFloat(lat)];
if (isNaN(pt[0]) || isNaN(pt[1])) throw new Error("Ungültige Koordinaten");
let found = false;
let props = {};
for (const f of potentialFeatures) {
if (checkPointInFeature(pt, f)) {
found = true;
props = f.properties;
break;
}
}
res.json({
found: found,
properties: found ? props : null
});
} catch (e) {
res.status(500).json({ error: e.toString() });
}
});
/**
* API: Prüft, ob ein Standort in einem Windenergiebereich liegt
*/
app.get('/api/check-planning', (req, res) => {
const { lat, lng } = req.query;
if (!planningLoaded) return res.json({ found: false, message: 'Daten nicht geladen' });
if (!lat || !lng) return res.json({ found: false, message: 'Koordinaten fehlen' });
try {
const pt = [parseFloat(lng), parseFloat(lat)];
let found = false;
let props = {};
for (const f of planningFeatures) {
if (checkPointInFeature(pt, f)) {
found = true;
props = f.properties;
break;
}
}
res.json({ found: found, properties: found ? props : null });
} catch (e) {
res.status(500).json({ error: e.toString() });
}
});
/**
* API: Liefert alle Potenzialflächen als GeoJSON
*/
app.get('/api/potential-data', (req, res) => {
if (!potentialLoaded) return res.status(503).json({ error: 'Daten noch nicht bereit' });
res.json({ type: 'FeatureCollection', features: potentialFeatures });
});
/**
* API: Liefert alle Windenergiebereiche als GeoJSON
*/
app.get('/api/planning-data', (req, res) => {
if (!planningLoaded) {
if (planningError) {
return res.status(500).json({ error: 'Laden der Planungsdaten fehlgeschlagen: ' + planningError });
}
return res.status(503).json({ error: 'Daten werden geladen... bitte erneut versuchen.', retry: true });
}
res.json({ type: 'FeatureCollection', features: planningFeatures });
});
app.listen(PORT, () => {
console.log(`Geodaten-Server läuft auf http://localhost:${PORT} (Buffered Mode)`);
});