/** * 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'); }