/** * QUIXZOOM Coverage Gap Analyzer * * Analyserar täckningsgap och genererar uppdrag. */ class CoverageGapAnalyzer { constructor(datasetManager) { this.manager = datasetManager; } /** * Analysera gap för en stad */ analyzeCity(cityId) { const city = this.manager.getReferenceCity(cityId); if (!city) return null; const report = this.manager.getCoverageReport(cityId); if (!report) return null; const analysis = { city: city.name, overallCoverage: report.overall, criticalGaps: [], warnings: [], opportunities: [], missions: [], }; // Analysera varje kategori for (const [category, coverage] of Object.entries(report.categories)) { const percentage = parseFloat(coverage.percentage); // Critical: < 50% if (percentage < 50) { analysis.criticalGaps.push({ category, coverage: percentage, message: `${category}: Only ${percentage}% coverage. Critical gap.`, }); // Generera uppdrag analysis.missions.push(this.generateMission(city, category, coverage, 'critical')); } // Warning: 50-80% else if (percentage < 80) { analysis.warnings.push({ category, coverage: percentage, message: `${category}: ${percentage}% coverage. Below target.`, }); analysis.missions.push(this.generateMission(city, category, coverage, 'warning')); } // Analysera tidpunkter if (coverage.byTimeOfDay) { const nightCoverage = coverage.byTimeOfDay.night || 0; const totalNight = (coverage.byTimeOfDay.night || 0) + (coverage.byTimeOfDay.day || 0) + (coverage.byTimeOfDay.evening || 0); const nightPercentage = totalNight > 0 ? (nightCoverage / totalNight * 100).toFixed(1) : 0; if (nightPercentage < 20) { analysis.opportunities.push({ category, type: 'night_coverage', message: `${category}: Only ${nightPercentage}% night coverage. Collect night observations.`, potentialValue: 'high', }); analysis.missions.push(this.generateTimeMission(city, category, 'night')); } } // Analysera väder if (coverage.byWeather) { const rainCoverage = coverage.byWeather.rain || 0; if (rainCoverage < 50) { analysis.opportunities.push({ category, type: 'weather_coverage', message: `${category}: Only ${rainCoverage} rain observations. Collect rainy weather data.`, potentialValue: 'medium', }); } } } // Sortera uppdrag efter prioritet analysis.missions.sort((a, b) => { const priorityOrder = { critical: 0, warning: 1, opportunity: 2 }; return priorityOrder[a.priority] - priorityOrder[b.priority]; }); return analysis; } /** * Generera uppdrag för gap */ generateMission(city, category, coverage, priority) { const needed = Math.ceil((100 - parseFloat(coverage.percentage)) * 10); // 10 obs per % return { id: `mission_${city.id}_${category}_${Date.now()}`, city: city.name, category, priority, type: 'coverage_gap', description: `Collect ${needed} ${category} observations in ${city.name}`, target: { count: needed, coverage: 100, }, current: { count: coverage.found, coverage: parseFloat(coverage.percentage), }, conditions: { timeOfDay: this.suggestTimeOfDay(coverage), weather: this.suggestWeather(coverage), season: this.suggestSeason(coverage), }, reward: this.calculateReward(priority, needed), expiresAt: new Date(Date.now() + 7 * 86400000).toISOString(), // 7 dagar }; } /** * Generera tidsbaserat uppdrag */ generateTimeMission(city, category, timeOfDay) { return { id: `mission_${city.id}_${category}_${timeOfDay}_${Date.now()}`, city: city.name, category, priority: 'opportunity', type: 'time_coverage', description: `Collect ${category} observations during ${timeOfDay} in ${city.name}`, target: { count: 50, timeOfDay, }, conditions: { timeOfDay, weather: 'any', }, reward: this.calculateReward('opportunity', 50), expiresAt: new Date(Date.now() + 14 * 86400000).toISOString(), // 14 dagar }; } /** * Föreslå tid på dygnet */ suggestTimeOfDay(coverage) { const times = coverage.byTimeOfDay || {}; const min = Math.min(times.day || 0, times.evening || 0, times.night || 0); if (min === times.night || 0) return 'night'; if (min === times.evening || 0) return 'evening'; return 'day'; } /** * Föreslå väder */ suggestWeather(coverage) { const weather = coverage.byWeather || {}; const min = Math.min(weather.clear || 0, weather.cloudy || 0, weather.rain || 0); if (min === weather.rain || 0) return 'rain'; if (min === weather.cloudy || 0) return 'cloudy'; return 'any'; } /** * Föreslå säsong */ suggestSeason(coverage) { const seasons = coverage.bySeason || {}; const min = Math.min(seasons.summer || 0, seasons.winter || 0, seasons.spring || 0, seasons.fall || 0); if (min === seasons.winter || 0) return 'winter'; if (min === seasons.spring || 0) return 'spring'; if (min === seasons.fall || 0) return 'fall'; return 'any'; } /** * Beräkna belöning */ calculateReward(priority, count) { const baseReward = { critical: 10, warning: 5, opportunity: 3, }; return baseReward[priority] * count; } /** * Generera rapport för alla städer */ generateGlobalReport() { const cities = this.manager.listReferenceCities(); const report = { generatedAt: new Date().toISOString(), totalCities: cities.length, cities: [], globalGaps: [], recommendedMissions: [], }; for (const city of cities) { const analysis = this.analyzeCity(city.id); if (analysis) { report.cities.push(analysis); report.recommendedMissions.push(...analysis.missions); } } // Identifiera globala gap (saknas i alla städer) const categoryCoverage = {}; for (const city of report.cities) { for (const gap of city.criticalGaps) { if (!categoryCoverage[gap.category]) { categoryCoverage[gap.category] = []; } categoryCoverage[gap.category].push(city.city); } } for (const [category, cities] of Object.entries(categoryCoverage)) { if (cities.length === report.totalCities) { report.globalGaps.push({ category, affectedCities: cities, message: `${category} is critically underrepresented in all cities`, }); } } // Sortera uppdrag report.recommendedMissions.sort((a, b) => b.reward - a.reward); return report; } } module.exports = CoverageGapAnalyzer; // Demo if (require.main === module) { const UrbanFoundationDatasetManager = require('./manager'); const manager = new UrbanFoundationDatasetManager(); // Lägg till städer med låg coverage const bangkok = manager.addReferenceCity({ name: 'Bangkok', country: 'TH', climate: 'tropical', type: 'tropical_megacity', location: { lat: 13.7563, lng: 100.5018 }, }); // Lägg till coverage med gap manager.updateCoverage(bangkok.id, 'street_lamp', { total: 1000, found: 940, byTimeOfDay: { day: 500, evening: 300, night: 140 }, }); manager.updateCoverage(bangkok.id, 'traffic_sign', { total: 500, found: 200, byTimeOfDay: { day: 150, evening: 40, night: 10 }, }); manager.updateCoverage(bangkok.id, 'manhole', { total: 300, found: 80, byTimeOfDay: { day: 60, evening: 15, night: 5 }, }); // Analysera const analyzer = new CoverageGapAnalyzer(manager); const analysis = analyzer.analyzeCity(bangkok.id); console.log('\n=== BANGKOK GAP ANALYSIS ==='); console.log(JSON.stringify(analysis, null, 2)); // Generera global rapport const globalReport = analyzer.generateGlobalReport(); console.log('\n=== GLOBAL REPORT ==='); console.log(`Total missions: ${globalReport.recommendedMissions.length}`); console.log(`Top mission: ${globalReport.recommendedMissions[0]?.description}`); }