/** * QUIXZOOM Fleet Intelligence — Nivå 5 * * AI styr inte bara en Zoomer utan hela nätverket. * * Exempel: * - Zoomer A dokumenterar Sukhumvit * - Zoomer B är redan nära en misstänkt skada och får verifieringsuppdrag * - Zoomer C får i uppdrag att filma samma plats nattetid * - Zoomer D skickas för att fånga en annan kameravinkel * * AI optimerar hela flottan istället för en enskild användare. */ class FleetCoordinator { constructor(options = {}) { this.zoomers = new Map(); // zoomerId -> Zoomer this.areas = new Map(); // areaId -> Area this.missions = new Map(); // missionId -> Mission // Optimeringsparametrar this.optimizationInterval = options.optimizationInterval || 10000; // 10s this.maxZoomersPerArea = options.maxZoomersPerArea || 10; this.minZoomersPerArea = options.minZoomersPerArea || 2; // Statistik this.statistics = { totalMissions: 0, completedMissions: 0, activeZoomers: 0, coverageImprovement: 0, }; this.optimizationTimer = null; } // ============================================================ // ZOOMER MANAGEMENT // ============================================================ registerZoomer(zoomerConfig) { const zoomer = { id: zoomerConfig.id || this.generateId(), name: zoomerConfig.name, status: 'available', // available, active, paused, offline location: zoomerConfig.location, capabilities: zoomerConfig.capabilities || ['video', 'photo'], currentMission: null, history: [], statistics: { totalMissions: 0, totalObservations: 0, averageQuality: 0, }, }; this.zoomers.set(zoomer.id, zoomer); console.log(`[FLEET] Zoomer registered: ${zoomer.name} (${zoomer.id})`); return zoomer; } updateZoomerLocation(zoomerId, location) { const zoomer = this.zoomers.get(zoomerId); if (!zoomer) return; zoomer.location = location; zoomer.lastUpdate = Date.now(); } updateZoomerStatus(zoomerId, status) { const zoomer = this.zoomers.get(zoomerId); if (!zoomer) return; zoomer.status = status; console.log(`[FLEET] Zoomer ${zoomerId} status: ${status}`); } // ============================================================ // AREA MANAGEMENT // ============================================================ registerArea(areaConfig) { const area = { id: areaConfig.id || this.generateId(), name: areaConfig.name, bounds: areaConfig.bounds, priority: areaConfig.priority || 0.5, coverage: 0, knowledgeGaps: [], assignedZoomers: new Set(), missions: [], }; this.areas.set(area.id, area); console.log(`[FLEET] Area registered: ${area.name} (${area.id})`); return area; } updateAreaCoverage(areaId, coverage) { const area = this.areas.get(areaId); if (!area) return; area.coverage = coverage; } updateAreaGaps(areaId, gaps) { const area = this.areas.get(areaId); if (!area) return; area.knowledgeGaps = gaps; } // ============================================================ // MISSION MANAGEMENT // ============================================================ createMission(missionConfig) { const mission = { id: missionConfig.id || this.generateId(), type: missionConfig.type, // verify, explore, document, diversify areaId: missionConfig.areaId, targetLocation: missionConfig.targetLocation, targetObject: missionConfig.targetObject, priority: missionConfig.priority || 0.5, status: 'pending', // pending, assigned, active, completed, failed assignedZoomer: null, createdAt: Date.now(), completedAt: null, instructions: [], }; this.missions.set(mission.id, mission); this.statistics.totalMissions++; console.log(`[FLEET] Mission created: ${mission.type} (${mission.id})`); // Försök tilldela direkt this.assignMission(mission.id); return mission; } assignMission(missionId) { const mission = this.missions.get(missionId); if (!mission || mission.status !== 'pending') return; // Hitta bästa zoomer för uppdraget const bestZoomer = this.findBestZoomerForMission(mission); if (!bestZoomer) { console.log(`[FLEET] No available zoomer for mission ${missionId}`); return; } // Tilldela uppdrag mission.assignedZoomer = bestZoomer.id; mission.status = 'assigned'; bestZoomer.currentMission = mission.id; bestZoomer.status = 'active'; // Generera instruktioner const instructions = this.generateMissionInstructions(mission, bestZoomer); mission.instructions = instructions; console.log(`[FLEET] Mission ${missionId} assigned to ${bestZoomer.name}`); // Skicka instruktioner till zoomer this.sendInstructionsToZoomer(bestZoomer.id, instructions); return mission; } findBestZoomerForMission(mission) { const availableZoomers = Array.from(this.zoomers.values()) .filter(z => z.status === 'available'); if (availableZoomers.length === 0) return null; // Score varje zoomer baserat på: // 1. Närhet till mål // 2. Kapacitet // 3. Historik (diversitet) // 4. Aktuell belastning const scored = availableZoomers.map(zoomer => { let score = 0; // Närhet (vikt: 40%) if (mission.targetLocation && zoomer.location) { const distance = this.calculateDistance(zoomer.location, mission.targetLocation); score += Math.max(0, 1 - distance / 1000) * 0.4; // Max 1km } // Kapacitet (vikt: 20%) const hasVideo = zoomer.capabilities.includes('video'); const hasPhoto = zoomer.capabilities.includes('photo'); score += (hasVideo ? 0.1 : 0) + (hasPhoto ? 0.1 : 0); // Historik-diversitet (vikt: 20%) const recentAreas = new Set(zoomer.history.slice(-5).map(h => h.areaId)); if (!recentAreas.has(mission.areaId)) { score += 0.2; // Bonus för nytt område } // Belastning (vikt: 20%) const load = zoomer.statistics.totalMissions / 10; // Normalisera score += Math.max(0, 1 - load) * 0.2; return { zoomer, score }; }); // Sortera efter score scored.sort((a, b) => b.score - a.score); return scored[0]?.zoomer; } generateMissionInstructions(mission, zoomer) { const instructions = []; switch (mission.type) { case 'verify': instructions.push({ text: `Verifiera ${mission.targetObject?.type} på ${mission.targetLocation?.lat}, ${mission.targetLocation?.lng}`, priority: 'high', reason: 'Previous observation needs verification', }); instructions.push({ text: 'Filma från annan vinkel än tidigare observation', priority: 'medium', reason: 'Diversify viewing angles', }); break; case 'explore': instructions.push({ text: `Utforska området kring ${mission.targetLocation?.lat}, ${mission.targetLocation?.lng}`, priority: 'medium', reason: 'Knowledge gap in this area', }); instructions.push({ text: 'Dokumentera alla synliga objekt', priority: 'low', reason: 'Build area knowledge', }); break; case 'document': instructions.push({ text: `Dokumentera ${mission.targetObject?.type}: ${mission.targetObject?.description}`, priority: 'high', reason: 'Specific documentation needed', }); break; case 'diversify': instructions.push({ text: `Filma samma plats men vid ${mission.targetTime || 'annan tid'}`, priority: 'medium', reason: 'Temporal diversity needed', }); break; default: instructions.push({ text: 'Dokumentera området enligt standardprotokoll', priority: 'medium', reason: 'General documentation', }); } return instructions; } completeMission(missionId, result) { const mission = this.missions.get(missionId); if (!mission) return; mission.status = 'completed'; mission.completedAt = Date.now(); mission.result = result; // Uppdatera zoomer if (mission.assignedZoomer) { const zoomer = this.zoomers.get(mission.assignedZoomer); if (zoomer) { zoomer.status = 'available'; zoomer.currentMission = null; zoomer.statistics.totalMissions++; zoomer.history.push({ missionId, areaId: mission.areaId, completedAt: Date.now(), }); } } this.statistics.completedMissions++; console.log(`[FLEET] Mission completed: ${missionId}`); // Utvärdera om nytt uppdrag behövs this.evaluateAreaNeeds(mission.areaId); } // ============================================================ // FLEET OPTIMIZATION // ============================================================ startOptimization() { this.optimizationTimer = setInterval(() => { this.optimizeFleet(); }, this.optimizationInterval); console.log('[FLEET] Optimization started'); } stopOptimization() { if (this.optimizationTimer) { clearInterval(this.optimizationTimer); this.optimizationTimer = null; } } optimizeFleet() { console.log('[FLEET] Running fleet optimization...'); for (const area of this.areas.values()) { // 1. Kontrollera täckning if (area.coverage < 0.8) { this.createCoverageMissions(area); } // 2. Kontrollera kunskapsgap if (area.knowledgeGaps.length > 0) { this.createGapMissions(area); } // 3. Balansera zoomer-fördelning this.balanceZoomers(area); // 4. Diversifiera observationer this.diversifyObservations(area); } // Uppdatera statistik this.updateStatistics(); } createCoverageMissions(area) { // Skapa uppdrag för att förbättra täckning const uncoveredRegions = this.findUncoveredRegions(area); for (const region of uncoveredRegions) { this.createMission({ type: 'explore', areaId: area.id, targetLocation: region.center, priority: 0.7, }); } } createGapMissions(area) { // Skapa uppdrag för att stänga kunskapsgap for (const gap of area.knowledgeGaps.slice(0, 5)) { // Max 5 åt gången this.createMission({ type: gap.type === 'verification' ? 'verify' : 'explore', areaId: area.id, targetLocation: gap.location, targetObject: gap.objectId ? { type: gap.objectType, id: gap.objectId } : null, priority: gap.priority, }); } } balanceZoomers(area) { const assignedZoomers = Array.from(area.assignedZoomers) .map(id => this.zoomers.get(id)) .filter(Boolean); // Om för få zoomer, begär förstärkning if (assignedZoomers.length < this.minZoomersPerArea) { const needed = this.minZoomersPerArea - assignedZoomers.length; console.log(`[FLEET] Area ${area.name} needs ${needed} more zoomers`); // Skapa uppdrag för att locka zoomer till området this.createMission({ type: 'explore', areaId: area.id, targetLocation: this.findAttractiveLocation(area), priority: 0.6, }); } // Om för många, omfördela if (assignedZoomers.length > this.maxZoomersPerArea) { const excess = assignedZoomers.length - this.maxZoomersPerArea; const toReassign = assignedZoomers.slice(0, excess); for (const zoomer of toReassign) { // Hitta annat område som behöver zoomer const otherArea = this.findAreaNeedingZoomers(); if (otherArea) { area.assignedZoomers.delete(zoomer.id); otherArea.assignedZoomers.add(zoomer.id); this.sendInstructionsToZoomer(zoomer.id, [{ text: `Flytta till ${otherArea.name} — området behöver täckning`, priority: 'medium', reason: 'Fleet rebalancing', }]); } } } } diversifyObservations(area) { // Hitta objekt med få observationer const objectsNeedingDiversity = this.findObjectsNeedingDiversity(area); for (const obj of objectsNeedingDiversity) { // Skapa uppdrag för temporal diversitet this.createMission({ type: 'diversify', areaId: area.id, targetLocation: obj.location, targetObject: obj, targetTime: 'night', priority: 0.5, }); // Skapa uppdrag för spatial diversitet this.createMission({ type: 'verify', areaId: area.id, targetLocation: obj.location, targetObject: obj, priority: 0.5, }); } } // ============================================================ // HJÄLPMETODER // ============================================================ findUncoveredRegions(area) { // Förenklad implementation return [{ center: { lat: (area.bounds.minLat + area.bounds.maxLat) / 2, lng: (area.bounds.minLng + area.bounds.maxLng) / 2, }, radius: 500, }]; } findAttractiveLocation(area) { // Hitta intressant plats i området return { lat: (area.bounds.minLat + area.bounds.maxLat) / 2, lng: (area.bounds.minLng + area.bounds.maxLng) / 2, }; } findAreaNeedingZoomers() { for (const area of this.areas.values()) { if (area.assignedZoomers.size < this.minZoomersPerArea) { return area; } } return null; } findObjectsNeedingDiversity(area) { // Förenklad implementation return []; } calculateDistance(a, b) { const R = 6371e3; const φ1 = a.lat * Math.PI / 180; const φ2 = b.lat * Math.PI / 180; const Δφ = (b.lat - a.lat) * Math.PI / 180; const Δλ = (b.lng - a.lng) * Math.PI / 180; const x = Math.sin(Δφ/2) * Math.sin(Δφ/2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ/2) * Math.sin(Δλ/2); const c = 2 * Math.atan2(Math.sqrt(x), Math.sqrt(1-x)); return R * c; } sendInstructionsToZoomer(zoomerId, instructions) { // I praktiken: skicka via WebSocket console.log(`[FLEET] Instructions to ${zoomerId}:`, instructions); } evaluateAreaNeeds(areaId) { const area = this.areas.get(areaId); if (!area) return; // Utvärdera om fler uppdrag behövs if (area.knowledgeGaps.length > 0) { this.createGapMissions(area); } } updateStatistics() { this.statistics.activeZoomers = Array.from(this.zoomers.values()) .filter(z => z.status === 'active').length; } generateId() { return `fleet_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } // ============================================================ // EXEMPEL SCENARIER // ============================================================ runScenario() { console.log('\n=== FLEET INTELLIGENCE SCENARIO ===\n'); // Registrera område const bangkok = this.registerArea({ name: 'Bangkok', bounds: { minLat: 13.5, maxLat: 14.0, minLng: 100.3, maxLng: 100.9 }, priority: 0.8, }); // Registrera zoomers const zoomerA = this.registerZoomer({ name: 'Zoomer A', location: { lat: 13.75, lng: 100.50 }, capabilities: ['video', 'photo'], }); const zoomerB = this.registerZoomer({ name: 'Zoomer B', location: { lat: 13.76, lng: 100.51 }, capabilities: ['video', 'photo'], }); const zoomerC = this.registerZoomer({ name: 'Zoomer C', location: { lat: 13.74, lng: 100.49 }, capabilities: ['video'], }); const zoomerD = this.registerZoomer({ name: 'Zoomer D', location: { lat: 13.77, lng: 100.52 }, capabilities: ['video', 'photo'], }); console.log(`Registered ${this.zoomers.size} zoomers\n`); // Skapa uppdrag console.log('Creating missions...\n'); // Zoomer A dokumenterar Sukhumvit const mission1 = this.createMission({ type: 'explore', areaId: bangkok.id, targetLocation: { lat: 13.75, lng: 100.50 }, priority: 0.7, }); // Zoomer B är nära misstänkt skada const mission2 = this.createMission({ type: 'verify', areaId: bangkok.id, targetLocation: { lat: 13.76, lng: 100.51 }, targetObject: { type: 'pothole', description: 'Large pothole on main road' }, priority: 0.9, }); // Zoomer C får nattuppdrag const mission3 = this.createMission({ type: 'diversify', areaId: bangkok.id, targetLocation: { lat: 13.74, lng: 100.49 }, targetTime: 'night', priority: 0.6, }); // Zoomer D får annan vinkel const mission4 = this.createMission({ type: 'verify', areaId: bangkok.id, targetLocation: { lat: 13.77, lng: 100.52 }, targetObject: { type: 'streetlight', description: 'Broken streetlight' }, priority: 0.8, }); console.log('\n=== MISSION ASSIGNMENTS ===\n'); for (const mission of this.missions.values()) { console.log(`${mission.type.toUpperCase()}: ${mission.assignedZoomer ? `Assigned to ${mission.assignedZoomer}` : 'Unassigned'}`); } console.log('\n=== FLEET OPTIMIZATION ===\n'); this.startOptimization(); // Simulera optimering setTimeout(() => { console.log('\n=== STATISTICS ==='); console.log(`Total missions: ${this.statistics.totalMissions}`); console.log(`Completed missions: ${this.statistics.completedMissions}`); console.log(`Active zoomers: ${this.statistics.activeZoomers}`); this.stopOptimization(); }, 2000); } } // Exportera module.exports = FleetCoordinator; // Demo if (require.main === module) { const coordinator = new FleetCoordinator(); coordinator.runScenario(); }