Files

210 lines
9.6 KiB
JavaScript
Raw Permalink Normal View History

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 };