Files
boc/quixzoom-capture-pipeline/fleet-intelligence/coordinator.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

628 lines
18 KiB
JavaScript

/**
* 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}${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();
}