const { evolveInfrastructure, getCameraById } = require('./smart-city-infra'); const NL_STATIONS = [ { id: 1, name: 'Amsterdam-NO2', city: 'Amsterdam', lat: 52.37, lng: 4.90, no2: 32, pm25: 14, o3: 45, population: 921402 }, { id: 2, name: 'Rotterdam-Centrum', city: 'Rotterdam', lat: 51.92, lng: 4.48, no2: 38, pm25: 18, o3: 42, population: 655468 }, { id: 3, name: 'Den Haag', city: 'Den Haag', lat: 52.07, lng: 4.30, no2: 29, pm25: 12, o3: 48, population: 552995 }, { id: 4, name: 'Utrecht', city: 'Utrecht', lat: 52.09, lng: 5.12, no2: 31, pm25: 13, o3: 46, population: 361924 }, { id: 5, name: 'Eindhoven', city: 'Eindhoven', lat: 51.44, lng: 5.48, no2: 35, pm25: 16, o3: 44, population: 246417 }, { id: 6, name: 'Groningen', city: 'Groningen', lat: 53.22, lng: 6.57, no2: 22, pm25: 9, o3: 52, population: 233273 }, { id: 7, name: 'Maastricht', city: 'Maastricht', lat: 50.85, lng: 5.69, no2: 26, pm25: 11, o3: 50, population: 121565 }, { id: 8, name: 'Nijmegen', city: 'Nijmegen', lat: 51.81, lng: 5.84, no2: 28, pm25: 12, o3: 47, population: 177359 }, { id: 9, name: 'Arnhem', city: 'Arnhem', lat: 51.99, lng: 5.90, no2: 27, pm25: 11, o3: 49, population: 164096 }, { id: 10, name: 'Breda', city: 'Breda', lat: 51.57, lng: 4.77, no2: 30, pm25: 13, o3: 46, population: 184403 }, { id: 11, name: 'Tilburg', city: 'Tilburg', lat: 51.56, lng: 5.09, no2: 33, pm25: 15, o3: 43, population: 224702 }, { id: 12, name: 'Almere', city: 'Almere', lat: 52.35, lng: 5.22, no2: 24, pm25: 10, o3: 51, population: 218096 }, { id: 13, name: 'Haarlem', city: 'Haarlem', lat: 52.39, lng: 4.64, no2: 30, pm25: 13, o3: 47, population: 162543 }, { id: 14, name: 'Enschede', city: 'Enschede', lat: 52.22, lng: 6.89, no2: 25, pm25: 10, o3: 50, population: 159732 }, { id: 15, name: 'Apeldoorn', city: 'Apeldoorn', lat: 52.21, lng: 5.97, no2: 23, pm25: 9, o3: 52, population: 164781 }, { id: 16, name: 'Amersfoort', city: 'Amersfoort', lat: 52.16, lng: 5.39, no2: 26, pm25: 11, o3: 48, population: 158712 }, { id: 17, name: 'Zwolle', city: 'Zwolle', lat: 52.52, lng: 6.08, no2: 24, pm25: 10, o3: 51, population: 130592 }, { id: 18, name: 'Leiden', city: 'Leiden', lat: 52.16, lng: 4.49, no2: 28, pm25: 12, o3: 47, population: 125565 } ]; function jitterInt(v, pct = 0.08) { return Math.max(5, Math.round(v * (1 + (Math.random() - 0.5) * pct * 2))); } function buildHourlyTrend(baseNo2, basePm25) { const hours = []; const now = new Date(); for (let h = 0; h < 24; h++) { const rush = h >= 7 && h <= 9 ? 1.28 : h >= 16 && h <= 19 ? 1.22 : h >= 0 && h <= 5 ? 0.72 : 1; hours.push({ hour: `${String(h).padStart(2, '0')}:00`, no2: Math.round(baseNo2 * rush * (0.92 + Math.random() * 0.16)), pm25: Math.round(basePm25 * (0.88 + Math.random() * 0.2)), traffic: Math.round(rush * 100 * (0.8 + Math.random() * 0.4)), noise: Math.round(45 + rush * 25 + Math.random() * 10) }); } hours[now.getHours()].no2 = jitterInt(hours[now.getHours()].no2, 0.03); return hours; } function buildLiveSeconds(baseNo2) { const pts = []; for (let i = 59; i >= 0; i--) { pts.push({ sec: i, no2: jitterInt(baseNo2 * (0.95 + Math.sin(i / 8) * 0.08), 0.04), cameras: 12, events: Math.random() > 0.92 ? 1 : 0 }); } return pts; } async function getSmartCityData() { const { cameras, lampposts } = evolveInfrastructure(); const stations = NL_STATIONS.map(s => ({ ...s, no2: jitterInt(s.no2), pm25: jitterInt(s.pm25), o3: jitterInt(s.o3), source: 'rivm-sim' })); const avgNo2 = Math.round(stations.reduce((a, s) => a + s.no2, 0) / stations.length); const avgPm25 = Math.round(stations.reduce((a, s) => a + s.pm25, 0) / stations.length); const aboveThreshold = stations.filter(s => s.no2 > 40 || s.pm25 > 25).length; const cityMap = {}; stations.forEach(s => { const c = s.city; if (!cityMap[c]) cityMap[c] = { city: c, no2: 0, pm25: 0, count: 0, lat: s.lat, lng: s.lng }; cityMap[c].no2 += s.no2; cityMap[c].pm25 += s.pm25; cityMap[c].count++; }); const cityRankings = Object.values(cityMap) .map(c => ({ ...c, no2: Math.round(c.no2 / c.count), pm25: Math.round(c.pm25 / c.count) })) .sort((a, b) => b.no2 - a.no2); const statusDistribution = [ { name: 'Goed', value: stations.filter(s => s.no2 <= 28 && s.pm25 <= 15).length, fill: '#22c55e' }, { name: 'Matig', value: stations.filter(s => (s.no2 > 28 && s.no2 <= 40) || (s.pm25 > 15 && s.pm25 <= 25)).length, fill: '#f59e0b' }, { name: 'Onvoldoende', value: stations.filter(s => s.no2 > 40 || s.pm25 > 25).length, fill: '#ef4444' } ]; const hourlyTrend = buildHourlyTrend(avgNo2, avgPm25); const liveSeconds = buildLiveSeconds(avgNo2); const weeklyTrend = ['Ma', 'Di', 'Wo', 'Do', 'Vr', 'Za', 'Zo'].map((d, i) => ({ day: d, no2: Math.round(avgNo2 * (0.9 + Math.sin(i) * 0.15 + Math.random() * 0.1)), pm25: Math.round(avgPm25 * (0.85 + Math.cos(i) * 0.12 + Math.random() * 0.1)) })); const pollutantRadar = [ { pollutant: 'NO₂', value: avgNo2, full: 60 }, { pollutant: 'PM2.5', value: avgPm25, full: 35 }, { pollutant: 'O₃', value: Math.round(stations.reduce((a, s) => a + s.o3, 0) / stations.length), full: 70 }, { pollutant: 'SO₂', value: Math.round(8 + Math.random() * 6), full: 25 }, { pollutant: 'CO', value: Math.round(3 + Math.random() * 4), full: 15 } ]; const cameraStats = { total: cameras.length, online: cameras.filter(c => c.online).length, traffic: cameras.filter(c => c.type === 'traffic').length, security: cameras.filter(c => c.type === 'security').length, avgLatency: Math.round(cameras.reduce((a, c) => a + c.latencyMs, 0) / cameras.length) }; const lamppostStats = { total: lampposts.length, activeMotion: lampposts.filter(l => l.motion).length, avgBrightness: Math.round(lampposts.reduce((a, l) => a + l.brightness, 0) / lampposts.length), energyTodayKwh: +(lampposts.reduce((a, l) => a + l.energyWh, 0) / 1000).toFixed(2) }; const recentEvents = []; if (Math.random() > 0.3) { const cam = cameras[Math.floor(Math.random() * cameras.length)]; recentEvents.push({ ts: new Date().toISOString(), type: ['motion', 'crowd', 'vehicle', 'incident'][Math.floor(Math.random() * 4)], camera: cam.name, severity: ['info', 'warn', 'alert'][Math.floor(Math.random() * 3)] }); } lampposts.filter(l => l.motion).slice(0, 2).forEach(lp => { recentEvents.push({ ts: new Date().toISOString(), type: 'motion-lamp', camera: lp.street, severity: 'info' }); }); const insights = [ { type: 'warn', label: 'Live nu', text: `${cameraStats.online}/${cameraStats.total} camera's online · gem. latency ${cameraStats.avgLatency}ms · refresh 1,5s.` }, { type: 'info', label: 'Piekuren', text: `NO₂ piek ~08:00 (${hourlyTrend[8]?.no2} µg/m³) gekoppeld aan verkeerscamera's A10/IJ-tunnel.` }, { type: 'ok', label: 'Smart lighting', text: `${lamppostStats.activeMotion} lantaarnpalen met motion-detect — ${lamppostStats.energyTodayKwh} kWh vandaag.` }, { type: 'warn', label: 'Stad', text: `${cityRankings[0]?.city}: hoogste NO₂ (${cityRankings[0]?.no2}). Drill-down via kaart → camera feed.` }, { type: 'info', label: 'Integratie', text: 'Productie: koppel gemeente RTSP/ONVIF via edge gateway — zelfde dashboard UI.' }, { type: 'ok', label: 'Compliance', text: 'AVG: anonimiseer video analytics; bewaartermijnen per camera-zone configureerbaar.' } ]; const contextBlocks = [ { title: 'Waarom Smart City dashboards?', body: 'Gemeenten beheren duizenden assets: camera\'s, sensoren, lantaarnpalen, verkeerslichtmasten. Zonder unified view reageer je te laat op incidenten, overshoot je energie-budget en mis je datagedreven beleid.' }, { title: 'Wat u hier ziet', body: 'Een Global Operations Center voor de Randstad: live camera-streams, IoT-lantaarnpalen, luchtkwaliteit en event feed — alles op één kaart met sub-second refresh.' }, { title: 'Technische aanpak Mek-Tech', body: 'Open data (RIVM, KNMI) + RTSP-ingest via edge + SSE/WebSocket naar browser. Geen vendor lock-in; dashboards op maat in dagen i.p.v. maanden.' }, { title: 'ROI voor uw organisatie', body: 'Snellere incident response (−40% meld-naar-actie tijd), energiebesparing smart lighting (−15–25%), betere burgercommunicatie via open kaartlagen.' }, { title: 'Productie-architectuur', body: 'Camera gateway → Kafka/EventHub → time-series DB → dit dashboard. Video blijft on-prem; metadata & analytics in cloud of gemeente-DC.' }, { title: 'Volgende stap', body: 'Pilot op 1 wijk: 20 camera\'s + 50 lantaarnpalen + luchtsensoren. Mek-Tech levert integratie, UI en SLA binnen 4–6 weken.' } ]; return { ts: new Date().toISOString(), refreshMs: 1500, stations, cameras, lampposts, cityRankings, avgNo2, avgPm25, aboveThreshold, statusDistribution, hourlyTrend, liveSeconds, weeklyTrend, pollutantRadar, insights, contextBlocks, cameraStats, lamppostStats, recentEvents, healthIndex: Math.max(0, Math.min(100, 100 - Math.round(avgNo2 * 1.2 + avgPm25 * 1.5))), sensorsOnline: stations.length + cameras.length + lampposts.length, populationCovered: stations.reduce((a, s) => a + (s.population || 50000), 0), rivmLive: false, mapCenter: { lat: 52.37, lng: 4.90, zoom: 11 }, context: { pitch: 'Global Smart City Command Center — camera\'s, lantaarnpalen, lucht & verkeer realtime.', deployment: 'Amsterdam · Rotterdam · Den Haag · Utrecht · Eindhoven' } }; } module.exports = { getSmartCityData, getCameraById };