Initial commit — Mek-Tech Live Labs Showcase v1.0
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user