Files
boc/quixzoom-capture-pipeline/mission-planner/planner.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

598 lines
17 KiB
JavaScript

/**
* QUIXZOOM Urban Intelligence Mission Planner
*
* Bestämmer nästa uppdrag baserat på:
* - Coverage Gap
* - Information Gain
* - Customer Demand
* - Infrastructure Criticality
* - Prediction Uncertainty
* - Temporal Freshness
* - Data Quality
*
* Tre nivåer: Mikro (sekunder), Lokal (minuter), Regional (dagar)
*/
const crypto = require('crypto');
const UrbanFoundationDatasetManager = require('../dataset-manager/manager');
class UrbanIntelligenceMissionPlanner {
constructor(config = {}) {
this.config = {
maxMissionsPerRegion: config.maxMissionsPerRegion || 100,
minMissionScore: config.minMissionScore || 10,
...config,
};
this.datasetManager = new UrbanFoundationDatasetManager();
this.missions = new Map();
this.completedMissions = [];
// Kritikalitetsnivåer för infrastruktur
this.criticalityLevels = {
traffic_light: 1.0,
street_lamp: 0.9,
traffic_sign: 0.85,
hydrant: 0.8,
manhole: 0.7,
electrical_cabinet: 0.75,
drain: 0.6,
tree: 0.5,
bench: 0.3,
trash_can: 0.2,
bike_rack: 0.25,
};
}
/**
* ============================================================
* MISSION SCORE
* ============================================================
*/
calculateMissionScore(mission) {
const coverageGap = this.calculateCoverageGap(mission);
const informationGain = this.calculateInformationGain(mission);
const customerDemand = this.calculateCustomerDemand(mission);
const infrastructureCriticality = this.calculateInfrastructureCriticality(mission);
const predictionUncertainty = this.calculatePredictionUncertainty(mission);
const temporalFreshness = this.calculateTemporalFreshness(mission);
const dataQuality = this.calculateDataQuality(mission);
const score =
coverageGap *
informationGain *
customerDemand *
infrastructureCriticality *
predictionUncertainty *
temporalFreshness *
dataQuality;
return {
total: Math.round(score * 100),
breakdown: {
coverageGap: Math.round(coverageGap * 100),
informationGain: Math.round(informationGain * 100),
customerDemand: Math.round(customerDemand * 100),
infrastructureCriticality: Math.round(infrastructureCriticality * 100),
predictionUncertainty: Math.round(predictionUncertainty * 100),
temporalFreshness: Math.round(temporalFreshness * 100),
dataQuality: Math.round(dataQuality * 100),
},
};
}
calculateCoverageGap(mission) {
const city = this.datasetManager.getReferenceCity(mission.cityId);
if (!city || !city.coverage[mission.category]) return 1.0;
const coverage = parseFloat(city.coverage[mission.category].percentage);
return Math.max(0.1, 1 - (coverage / 100));
}
calculateInformationGain(mission) {
// Nytt objekt = högre gain
if (mission.objectId) {
const obj = this.datasetManager.objects.get(mission.objectId);
if (obj) {
const obsCount = obj.observations?.length || 0;
return Math.max(0.1, 1 / Math.sqrt(obsCount + 1));
}
}
return 1.0;
}
calculateCustomerDemand(mission) {
// Om en kund har efterfrågat detta område/typ
const demand = mission.customerDemand || 0;
return Math.max(0.5, 1 + (demand / 100));
}
calculateInfrastructureCriticality(mission) {
return this.criticalityLevels[mission.category] || 0.5;
}
calculatePredictionUncertainty(mission) {
// Högre osäkerhet = högre score
const uncertainty = mission.predictionUncertainty || 0.5;
return Math.max(0.5, uncertainty * 2);
}
calculateTemporalFreshness(mission) {
// Ju äldre senaste observation, desto högre score
const lastObserved = mission.lastObserved;
if (!lastObserved) return 1.0;
const daysSince = (Date.now() - new Date(lastObserved).getTime()) / (1000 * 60 * 60 * 24);
if (daysSince < 7) return 0.3;
if (daysSince < 30) return 0.5;
if (daysSince < 90) return 0.7;
if (daysSince < 365) return 0.9;
return 1.0;
}
calculateDataQuality(mission) {
// Om befintliga observationer är av låg kvalitet
const quality = mission.existingQuality || 0.5;
return Math.max(0.5, 1 - quality);
}
/**
* ============================================================
* MISSION GENERATION
* ============================================================
*/
generateMissions(cityId, options = {}) {
const city = this.datasetManager.getReferenceCity(cityId);
if (!city) return [];
const missions = [];
const coverage = city.coverage || {};
// Generera uppdrag för varje kategori med låg täckning
for (const [category, cov] of Object.entries(coverage)) {
const percentage = parseFloat(cov.percentage);
if (percentage < 90 || options.forceAll) {
// Mikro-uppdrag: Specifika objekt
const microMissions = this.generateMicroMissions(cityId, category, cov);
missions.push(...microMissions);
// Lokal-uppdrag: Områden
const localMissions = this.generateLocalMissions(cityId, category, cov);
missions.push(...localMissions);
}
}
// Regionala uppdrag: Stora gap
const regionalMissions = this.generateRegionalMissions(cityId);
missions.push(...regionalMissions);
// Sortera efter Mission Score
missions.sort((a, b) => b.score.total - a.score.total);
return missions.slice(0, this.config.maxMissionsPerRegion);
}
generateMicroMissions(cityId, category, coverage) {
const missions = [];
// Hitta objekt med få observationer eller låg kvalitet
const objects = Array.from(this.datasetManager.objects.values())
.filter(o => o.cityId === cityId && o.type === category);
for (const obj of objects) {
const obsCount = obj.observations?.length || 0;
const lastObserved = obj.lastSeen;
if (obsCount < 3 || this.calculateTemporalFreshness({ lastObserved }) > 0.7) {
const mission = {
id: `micro_${crypto.randomBytes(4).toString('hex')}`,
cityId,
category,
objectId: obj.id,
type: 'micro',
// Specifika instruktioner
instructions: this.generateMicroInstructions(obj, coverage),
// Position
location: obj.location,
// Metadata
existingObservations: obsCount,
lastObserved,
// Scoring
predictionUncertainty: obj.confidence < 0.7 ? 0.8 : 0.5,
existingQuality: obj.quality || 0.5,
};
mission.score = this.calculateMissionScore(mission);
if (mission.score.total >= this.config.minMissionScore) {
missions.push(mission);
}
}
}
return missions;
}
generateMicroInstructions(object, coverage) {
const instructions = [];
// Tidsbaserade instruktioner
if (coverage.byTimeOfDay) {
const nightCoverage = coverage.byTimeOfDay.night || 0;
const eveningCoverage = coverage.byTimeOfDay.evening || 0;
if (nightCoverage < 20) {
instructions.push('Ta bild vid nattetid (efter 22:00)');
} else if (eveningCoverage < 40) {
instructions.push('Ta bild vid kvällstid (18:00-22:00)');
}
}
// Väderbaserade instruktioner
if (coverage.byWeather) {
const rainCoverage = coverage.byWeather.rain || 0;
if (rainCoverage < 10) {
instructions.push('Ta bild i regnigt väder');
}
}
// Vinkelbaserade instruktioner
const obsCount = object.observations?.length || 0;
if (obsCount === 1) {
instructions.push('Ta bild från motsatt vinkel');
} else if (obsCount === 2) {
instructions.push('Ta bild från sidan (90°)');
}
// Avstånd
if (obsCount < 2) {
instructions.push('Gå närmare (inom 2 meter)');
}
return instructions;
}
generateLocalMissions(cityId, category, coverage) {
const missions = [];
// Hitta områden med låg täckning
const areas = this.identifyLowCoverageAreas(cityId, category);
for (const area of areas) {
const mission = {
id: `local_${crypto.randomBytes(4).toString('hex')}`,
cityId,
category,
type: 'local',
instructions: [
`Dokumentera alla ${category} i området`,
`Gå längs ${area.streetName || 'gatan'}`,
`Fotografera från båda sidor`,
],
location: area.center,
radius: area.radius,
estimatedObjects: area.estimatedObjects,
estimatedTime: area.estimatedTime,
predictionUncertainty: 0.7,
};
mission.score = this.calculateMissionScore(mission);
if (mission.score.total >= this.config.minMissionScore) {
missions.push(mission);
}
}
return missions;
}
identifyLowCoverageAreas(cityId, category) {
// Simplified: returnera mock-områden
return [
{
center: { lat: 13.7563, lng: 100.5018 },
radius: 500,
streetName: 'Sukhumvit Road',
estimatedObjects: 50,
estimatedTime: 30,
},
{
center: { lat: 13.7580, lng: 100.5030 },
radius: 300,
streetName: 'Silom Road',
estimatedObjects: 30,
estimatedTime: 20,
},
];
}
generateRegionalMissions(cityId) {
const missions = [];
const city = this.datasetManager.getReferenceCity(cityId);
if (!city) return missions;
// Nattinventering
missions.push({
id: `regional_${crypto.randomBytes(4).toString('hex')}`,
cityId,
type: 'regional',
category: 'all',
instructions: [
'Nattinventering av hela staden',
'Fokus på gatlyktor och trafikljus',
'Dokumentera belysningsstatus',
],
estimatedTime: 480, // 8 timmar
estimatedObjects: 1000,
predictionUncertainty: 0.9,
temporalFreshness: 1.0,
});
// Verifiering av gamla observationer
missions.push({
id: `regional_${crypto.randomBytes(4).toString('hex')}`,
cityId,
type: 'regional',
category: 'all',
instructions: [
'Verifiera alla objekt äldre än 12 månader',
'Notera förändringar och skador',
],
estimatedTime: 240, // 4 timmar
estimatedObjects: 500,
predictionUncertainty: 0.8,
temporalFreshness: 1.0,
});
// Score alla regionala uppdrag
for (const mission of missions) {
mission.score = this.calculateMissionScore(mission);
}
return missions.filter(m => m.score.total >= this.config.minMissionScore);
}
/**
* ============================================================
* ERSÄTTNINGSMODELL
* ============================================================
*/
calculateCompensation(mission) {
const baseRates = {
street_lamp: 2,
traffic_sign: 3,
traffic_light: 5,
manhole: 15,
electrical_cabinet: 10,
hydrant: 8,
tree: 1,
bench: 1,
trash_can: 1,
bike_rack: 2,
};
const baseRate = baseRates[mission.category] || 2;
// Multiplikatorer (säkra mot undefined)
const breakdown = mission.score?.breakdown || {};
const gapMultiplier = 1 + ((breakdown.coverageGap || 0) / 100);
const criticalityMultiplier = 1 + ((breakdown.infrastructureCriticality || 50) / 100);
const uncertaintyMultiplier = 1 + ((breakdown.predictionUncertainty || 50) / 100);
const freshnessMultiplier = 1 + ((breakdown.temporalFreshness || 50) / 100);
const compensation = Math.round(
baseRate *
gapMultiplier *
criticalityMultiplier *
uncertaintyMultiplier *
freshnessMultiplier
);
return {
base: baseRate,
final: compensation,
multipliers: {
gap: gapMultiplier.toFixed(2),
criticality: criticalityMultiplier.toFixed(2),
uncertainty: uncertaintyMultiplier.toFixed(2),
freshness: freshnessMultiplier.toFixed(2),
},
};
}
/**
* ============================================================
* MARKETPLACE
* ============================================================
*/
createMarketplaceRequest(request) {
const marketplaceRequest = {
id: `market_${crypto.randomBytes(4).toString('hex')}`,
customerId: request.customerId,
customerType: request.customerType, // municipality, property_owner, energy_company, etc.
// Krav
requirements: {
area: request.requirements?.area || request.area,
categories: request.requirements?.categories || request.categories || [],
conditions: request.requirements?.conditions || request.conditions || {},
priority: request.requirements?.priority || request.priority || 'normal',
},
// Ersättning
budget: request.budget,
maxCompensation: request.maxCompensation,
// Status
status: 'open',
createdAt: new Date().toISOString(),
};
// Generera uppdrag från förfrågan
const missions = this.generateMissionsFromRequest(marketplaceRequest);
marketplaceRequest.missions = missions;
return marketplaceRequest;
}
generateMissionsFromRequest(request) {
const missions = [];
const categories = request.requirements.categories || [];
for (const category of categories) {
const mission = {
id: `mission_${crypto.randomBytes(4).toString('hex')}`,
type: 'marketplace',
category,
cityId: request.cityId || 'unknown',
instructions: [
`Kund: ${request.customerType}`,
`Område: ${request.requirements.area.name}`,
`Prioritet: ${request.requirements.priority}`,
],
location: request.requirements.area.center,
radius: request.requirements.area.radius,
customerDemand: 100, // Hög efterfrågan från kund
existingQuality: 0.3,
predictionUncertainty: 0.8,
};
mission.score = this.calculateMissionScore(mission);
mission.compensation = this.calculateCompensation(mission);
missions.push(mission);
}
return missions;
}
/**
* ============================================================
* API
* ============================================================
*/
getTopMissions(cityId, limit = 10) {
const missions = this.generateMissions(cityId);
return missions.slice(0, limit).map(m => ({
...m,
compensation: this.calculateCompensation(m),
}));
}
getMissionDetails(missionId) {
return this.missions.get(missionId);
}
completeMission(missionId, result) {
const mission = this.missions.get(missionId);
if (!mission) return null;
mission.status = 'completed';
mission.completedAt = new Date().toISOString();
mission.result = result;
this.completedMissions.push(mission);
this.missions.delete(missionId);
return mission;
}
}
module.exports = UrbanIntelligenceMissionPlanner;
// Demo
if (require.main === module) {
const planner = new UrbanIntelligenceMissionPlanner();
// Lägg till Bangkok
const datasetManager = new UrbanFoundationDatasetManager();
const bangkok = datasetManager.addReferenceCity({
name: 'Bangkok',
country: 'TH',
region: 'Southeast Asia',
climate: 'tropical',
type: 'tropical_megacity',
location: { lat: 13.7563, lng: 100.5018 },
timezone: 'Asia/Bangkok',
});
// Uppdatera coverage
datasetManager.updateCoverage(bangkok.id, 'street_lamp', {
total: 1000,
found: 940,
byTimeOfDay: { day: 500, evening: 300, night: 140 },
});
datasetManager.updateCoverage(bangkok.id, 'traffic_sign', {
total: 500,
found: 200,
byTimeOfDay: { day: 150, evening: 40, night: 10 },
});
datasetManager.updateCoverage(bangkok.id, 'manhole', {
total: 1000,
found: 270,
byTimeOfDay: { day: 200, evening: 50, night: 20 },
});
// Generera uppdrag
console.log('=== TOP MISSIONS FOR BANGKOK ===\n');
const missions = planner.getTopMissions(bangkok.id, 5);
missions.forEach((mission, i) => {
console.log(`${i + 1}. ${mission.type.toUpperCase()} MISSION`);
console.log(` Score: ${mission.score.total}`);
console.log(` Category: ${mission.category}`);
console.log(` Compensation: ${mission.compensation.final} kr (base: ${mission.compensation.base})`);
console.log(` Instructions:`);
mission.instructions.forEach(inst => console.log(` - ${inst}`));
console.log('');
});
// Skapa marketplace-förfrågan
console.log('=== MARKETPLACE REQUEST ===\n');
const request = planner.createMarketplaceRequest({
customerId: 'municipality_bangkok',
customerType: 'municipality',
requirements: {
area: { name: 'Sukhumvit', center: { lat: 13.7563, lng: 100.5018 }, radius: 1000 },
categories: ['street_lamp', 'traffic_sign'],
conditions: { night: true },
priority: 'high',
},
budget: 10000,
maxCompensation: 100,
});
console.log(`Request: ${request.id}`);
console.log(`Missions: ${request.missions.length}`);
request.missions.forEach(m => {
console.log(` - ${m.category}: ${m.compensation.final} kr`);
});
}