/** * 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`); }); }