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>/s); const bezMatch = data.match(/]*>(.*?)<\/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(/]*>/); for (let i = 1; i < blocks.length; i++) { const block = blocks[i]; // Extrahiere UTM Koordinaten aus gml:pos const posMatch = block.match(/]*>(.*?)<\/gml:pos>/); const ibjahrMatch = block.match(/]*>(.*?)<\/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)`); });