Files
mek-tech-showcase/server/lib/sources/smart-city.js
T

210 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (1525%), 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 46 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 };