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
598 lines
17 KiB
JavaScript
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`);
|
|
});
|
|
}
|