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
300 lines
8.4 KiB
JavaScript
300 lines
8.4 KiB
JavaScript
/**
|
|
* 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}`);
|
|
}
|