Files
boc/quixzoom-capture-pipeline/test-data/simulator.js
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

461 lines
15 KiB
JavaScript

/**
* QUIXZOOM City Data Simulator
*
* Simulerar en hel stad med realistisk data:
* - 50 000 gatlyktor
* - 25 000 trafikskyltar
* - 12 000 brunnar
* - 6 000 träd
* - 4 000 elskåp
* - 300 000 observationer
* - 2 000 000 evidensposter
* - 10 000 Zoomers
*/
const crypto = require('crypto');
class CitySimulator {
constructor(cityName, bounds) {
this.cityName = cityName;
this.bounds = bounds; // { north, south, east, west }
this.objects = new Map();
this.observations = [];
this.zoomers = [];
this.evidence = [];
this.events = [];
// Objekttyper och deras förekomst
this.objectTypes = {
street_lamp: { count: 50000, density: 'high' },
traffic_sign: { count: 25000, density: 'medium' },
manhole: { count: 12000, density: 'medium' },
tree: { count: 6000, density: 'low' },
utility_box: { count: 4000, density: 'low' },
};
}
/**
* ============================================================
* GENERERA STAD
* ============================================================
*/
generate() {
console.log(`[SIM] Generating ${this.cityName}...`);
const startTime = Date.now();
// 1. Generera Zoomers
this.generateZoomers(10000);
console.log(`[SIM] Generated ${this.zoomers.length} Zoomers`);
// 2. Generera objekt
for (const [type, config] of Object.entries(this.objectTypes)) {
this.generateObjects(type, config.count);
}
console.log(`[SIM] Generated ${this.objects.size} objects`);
// 3. Generera observationer (300 000)
this.generateObservations(300000);
console.log(`[SIM] Generated ${this.observations.length} observations`);
// 4. Generera evidens (2 000 000)
this.generateEvidence(2000000);
console.log(`[SIM] Generated ${this.evidence.length} evidence records`);
// 5. Generera events
this.generateEvents();
console.log(`[SIM] Generated ${this.events.length} events`);
const duration = Date.now() - startTime;
console.log(`[SIM] Complete in ${duration}ms`);
return this.getStats();
}
/**
* ============================================================
* ZOOMERS
* ============================================================
*/
generateZoomers(count) {
const firstNames = ['Anna', 'Erik', 'Maria', 'Johan', 'Lisa', 'Anders', 'Emma', 'Lars', 'Sara', 'Karl'];
const lastNames = ['Andersson', 'Johansson', 'Karlsson', 'Nilsson', 'Eriksson', 'Larsson', 'Olsson', 'Persson'];
const cities = ['Stockholm', 'Göteborg', 'Malmö', 'Uppsala', 'Linköping'];
for (let i = 0; i < count; i++) {
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)];
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)];
this.zoomers.push({
id: `zoomer_${i.toString().padStart(5, '0')}`,
name: `${firstName} ${lastName}`,
email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}${i}@example.com`,
city: cities[Math.floor(Math.random() * cities.length)],
level: this.randomLevel(),
rating: 3.5 + Math.random() * 1.5,
missionsCompleted: Math.floor(Math.random() * 500),
joinedAt: this.randomDate(2024, 2026),
device: this.randomDevice(),
status: Math.random() > 0.7 ? 'active' : 'inactive',
});
}
}
randomLevel() {
const r = Math.random();
if (r < 0.6) return 'supplementary';
if (r < 0.9) return 'active';
return 'professional';
}
randomDevice() {
const devices = [
{ type: 'iPhone', model: 'iPhone 15 Pro', os: 'iOS 17' },
{ type: 'iPhone', model: 'iPhone 14', os: 'iOS 16' },
{ type: 'Android', model: 'Samsung S24', os: 'Android 14' },
{ type: 'Android', model: 'Pixel 8', os: 'Android 14' },
];
return devices[Math.floor(Math.random() * devices.length)];
}
/**
* ============================================================
* OBJEKT
* ============================================================
*/
generateObjects(type, count) {
for (let i = 0; i < count; i++) {
const location = this.randomLocation();
const objectId = `${type}_${i.toString().padStart(6, '0')}`;
const obj = {
id: objectId,
type,
location,
altitude: this.randomAltitude(),
attributes: this.generateAttributes(type),
firstSeen: this.randomDate(2023, 2026),
lastSeen: this.randomDate(2025, 2026),
observationCount: 0,
health: 0.7 + Math.random() * 0.3,
status: Math.random() > 0.9 ? 'needs_attention' : 'ok',
};
this.objects.set(objectId, obj);
}
}
randomLocation() {
const lat = this.bounds.south + Math.random() * (this.bounds.north - this.bounds.south);
const lng = this.bounds.west + Math.random() * (this.bounds.east - this.bounds.west);
return { lat, lng };
}
randomAltitude() {
return 5 + Math.random() * 50;
}
generateAttributes(type) {
const attributes = {};
switch (type) {
case 'street_lamp':
attributes.material = Math.random() > 0.5 ? 'steel' : 'aluminum';
attributes.height = 6 + Math.random() * 4;
attributes.paint = ['grey', 'black', 'green'][Math.floor(Math.random() * 3)];
attributes.rust = Math.random() > 0.8;
attributes.lean = Math.random() > 0.9 ? Math.random() * 5 : 0;
attributes.light = Math.random() > 0.9 ? 'broken' : 'working';
break;
case 'traffic_sign':
attributes.type = ['speed_limit', 'stop', 'yield', 'pedestrian', 'parking'][Math.floor(Math.random() * 5)];
attributes.material = 'aluminum';
attributes.size = ['small', 'medium', 'large'][Math.floor(Math.random() * 3)];
attributes.reflective = Math.random() > 0.1;
attributes.damaged = Math.random() > 0.95;
break;
case 'manhole':
attributes.material = ['cast_iron', 'concrete', 'steel'][Math.floor(Math.random() * 3)];
attributes.diameter = 0.5 + Math.random() * 0.3;
attributes.cover_type = ['round', 'square'][Math.floor(Math.random() * 2)];
attributes.condition = Math.random() > 0.8 ? 'damaged' : 'good';
break;
case 'tree':
attributes.species = ['oak', 'pine', 'birch', 'maple', 'elm'][Math.floor(Math.random() * 5)];
attributes.height = 3 + Math.random() * 15;
attributes.diameter = 0.2 + Math.random() * 1.0;
attributes.health = Math.random() > 0.9 ? 'poor' : 'good';
break;
case 'utility_box':
attributes.type = ['electric', 'telecom', 'traffic'][Math.floor(Math.random() * 3)];
attributes.material = 'steel';
attributes.color = ['grey', 'green', 'brown'][Math.floor(Math.random() * 3)];
attributes.condition = Math.random() > 0.85 ? 'damaged' : 'good';
break;
}
return attributes;
}
/**
* ============================================================
* OBSERVATIONER
* ============================================================
*/
generateObservations(count) {
const objectIds = Array.from(this.objects.keys());
for (let i = 0; i < count; i++) {
const objectId = objectIds[Math.floor(Math.random() * objectIds.length)];
const obj = this.objects.get(objectId);
const zoomer = this.zoomers[Math.floor(Math.random() * this.zoomers.length)];
const observation = {
id: `obs_${i.toString().padStart(7, '0')}`,
objectId,
objectType: obj.type,
zoomerId: zoomer.id,
location: {
lat: obj.location.lat + (Math.random() - 0.5) * 0.0001,
lng: obj.location.lng + (Math.random() - 0.5) * 0.0001,
},
altitude: obj.altitude + (Math.random() - 0.5) * 2,
gpsAccuracy: 2 + Math.random() * 8,
timestamp: this.randomDate(2025, 2026),
attributes: { ...obj.attributes },
quality: this.randomQuality(),
weather: this.randomWeather(),
device: zoomer.device,
};
this.observations.push(observation);
obj.observationCount++;
}
}
randomQuality() {
return {
blur: Math.random() * 0.3,
exposure: 0.5 + Math.random() * 0.5,
angle: Math.random() * 30,
distance: 2 + Math.random() * 10,
};
}
randomWeather() {
const conditions = ['clear', 'cloudy', 'rain', 'fog', 'snow'];
return conditions[Math.floor(Math.random() * conditions.length)];
}
/**
* ============================================================
* EVIDENS
* ============================================================
*/
generateEvidence(count) {
for (let i = 0; i < count; i++) {
const observation = this.observations[Math.floor(Math.random() * this.observations.length)];
this.evidence.push({
id: `ev_${i.toString().padStart(8, '0')}`,
observationId: observation.id,
objectId: observation.objectId,
type: ['image', 'video', 'sensor', 'ai_analysis'][Math.floor(Math.random() * 4)],
source: observation.zoomerId,
timestamp: observation.timestamp,
confidence: 0.5 + Math.random() * 0.5,
metadata: {
resolution: ['1080p', '4K', '720p'][Math.floor(Math.random() * 3)],
format: ['jpg', 'png', 'mp4'][Math.floor(Math.random() * 3)],
size: Math.floor(1024 + Math.random() * 10240),
},
});
}
}
/**
* ============================================================
* EVENTS
* ============================================================
*/
generateEvents() {
// Generera events baserat på observationer
for (const observation of this.observations.slice(0, 100000)) {
this.events.push({
id: `evt_${this.events.length.toString().padStart(8, '0')}`,
type: 'observation_created',
timestamp: observation.timestamp,
payload: {
observationId: observation.id,
objectId: observation.objectId,
zoomerId: observation.zoomerId,
},
});
}
// Generera några identity-match events
for (let i = 0; i < 50000; i++) {
this.events.push({
id: `evt_${this.events.length.toString().padStart(8, '0')}`,
type: 'identity_matched',
timestamp: this.randomDate(2025, 2026),
payload: {
observationId: `obs_${Math.floor(Math.random() * 300000).toString().padStart(7, '0')}`,
objectId: `street_lamp_${Math.floor(Math.random() * 50000).toString().padStart(6, '0')}`,
confidence: 0.8 + Math.random() * 0.2,
},
});
}
}
/**
* ============================================================
* HJÄLPMETODER
* ============================================================
*/
randomDate(startYear, endYear) {
const start = new Date(startYear, 0, 1).getTime();
const end = new Date(endYear, 11, 31).getTime();
return new Date(start + Math.random() * (end - start));
}
getStats() {
return {
city: this.cityName,
objects: {
total: this.objects.size,
byType: this.getObjectCountsByType(),
},
observations: this.observations.length,
zoomers: this.zoomers.length,
evidence: this.evidence.length,
events: this.events.length,
health: this.getHealthStats(),
};
}
getObjectCountsByType() {
const counts = {};
for (const obj of this.objects.values()) {
counts[obj.type] = (counts[obj.type] || 0) + 1;
}
return counts;
}
getHealthStats() {
const healths = Array.from(this.objects.values()).map(o => o.health);
const avg = healths.reduce((a, b) => a + b, 0) / healths.length;
const min = Math.min(...healths);
const max = Math.max(...healths);
return { average: avg.toFixed(3), min: min.toFixed(3), max: max.toFixed(3) };
}
/**
* ============================================================
* EXPORT TILL POSTGRESQL
* ============================================================
*/
async exportToPostgres(pool) {
console.log('[SIM] Exporting to PostgreSQL...');
// Zoomers
const zoomerValues = this.zoomers.map(z =>
`('${z.id}', '${z.name}', '${z.email}', '${z.city}', '${z.level}', ${z.rating}, ${z.missionsCompleted}, '${z.joinedAt.toISOString()}', '${JSON.stringify(z.device)}', '${z.status}')`
).join(',');
await pool.query(`
INSERT INTO zoomers (id, name, email, city, level, rating, missions_completed, joined_at, device, status)
VALUES ${zoomerValues}
`);
// Objects
const batchSize = 1000;
const objectList = Array.from(this.objects.values());
for (let i = 0; i < objectList.length; i += batchSize) {
const batch = objectList.slice(i, i + batchSize);
const values = batch.map(o =>
`('${o.id}', '${o.type}', ST_SetSRID(ST_MakePoint(${o.location.lng}, ${o.location.lat}), 4326), ${o.altitude}, '${JSON.stringify(o.attributes)}', '${o.firstSeen.toISOString()}', '${o.lastSeen.toISOString()}', ${o.observationCount}, ${o.health}, '${o.status}')`
).join(',');
await pool.query(`
INSERT INTO objects (id, type, location, altitude, attributes, first_seen, last_seen, observation_count, health, status)
VALUES ${values}
`);
console.log(`[SIM] Exported ${Math.min(i + batchSize, objectList.length)}/${objectList.length} objects`);
}
// Observations
for (let i = 0; i < this.observations.length; i += batchSize) {
const batch = this.observations.slice(i, i + batchSize);
const values = batch.map(o =>
`('${o.id}', '${o.objectId}', '${o.zoomerId}', ST_SetSRID(ST_MakePoint(${o.location.lng}, ${o.location.lat}), 4326), ${o.altitude}, ${o.gpsAccuracy}, '${o.timestamp.toISOString()}', '${JSON.stringify(o.attributes)}', '${JSON.stringify(o.quality)}', '${o.weather}')`
).join(',');
await pool.query(`
INSERT INTO observations (id, object_id, zoomer_id, location, altitude, gps_accuracy, timestamp, attributes, quality, weather)
VALUES ${values}
`);
console.log(`[SIM] Exported ${Math.min(i + batchSize, this.observations.length)}/${this.observations.length} observations`);
}
console.log('[SIM] PostgreSQL export complete');
}
}
// Exportera
module.exports = CitySimulator;
// Demo
if (require.main === module) {
const simulator = new CitySimulator('Stockholm', {
north: 59.4,
south: 59.2,
east: 18.2,
west: 17.9,
});
console.log('=== CITY DATA SIMULATOR ===\n');
const stats = simulator.generate();
console.log('\n=== STATISTICS ===');
console.log(`City: ${stats.city}`);
console.log(`Objects: ${stats.objects.total.toLocaleString()}`);
for (const [type, count] of Object.entries(stats.objects.byType)) {
console.log(` ${type}: ${count.toLocaleString()}`);
}
console.log(`Observations: ${stats.observations.toLocaleString()}`);
console.log(`Zoomers: ${stats.zoomers.toLocaleString()}`);
console.log(`Evidence: ${stats.evidence.toLocaleString()}`);
console.log(`Events: ${stats.events.toLocaleString()}`);
console.log(`Health: avg=${stats.health.average}, min=${stats.health.min}, max=${stats.health.max}`);
// Spara till fil
const fs = require('fs');
const data = {
city: simulator.cityName,
zoomers: simulator.zoomers.slice(0, 100), // Spara bara 100 för demo
objects: Array.from(simulator.objects.values()).slice(0, 100),
observations: simulator.observations.slice(0, 1000),
evidence: simulator.evidence.slice(0, 1000),
events: simulator.events.slice(0, 1000),
};
fs.writeFileSync('/tmp/stockholm-sim.json', JSON.stringify(data, null, 2));
console.log('\n[SIM] Saved sample data to /tmp/stockholm-sim.json');
}