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
628 lines
18 KiB
JavaScript
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} 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();
|
|
}
|