bae705aa97
- 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
461 lines
15 KiB
JavaScript
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');
|
|
}
|