/** * QUIXZOOM Data Value Score * * Varje observation får ett värde baserat på: * - Hur mycket ny information den tillför * - Hur sällsynt datan är * - Hur mycket den förbättrar modellen * - Teknisk kvalitet * * Ersättning baseras på Data Value Score, inte bara volym. */ class DataValueCalculator { constructor(options = {}) { // Vikter för olika värdekomponenter this.weights = { novelty: options.noveltyWeight || 0.25, // Ny information rarity: options.rarityWeight || 0.20, // Sällsynthet verification: options.verificationWeight || 0.20, // Verifiering coverage: options.coverageWeight || 0.15, // Täckning quality: options.qualityWeight || 0.15, // Kvalitet diversity: options.diversityWeight || 0.05, // Diversitet }; // Historik för att beräkna novelty this.observationHistory = new Map(); // area -> observations this.objectHistory = new Map(); // objectType -> count // Basbelopp per poäng this.baseReward = options.baseReward || 1.0; // USD per poäng } // ============================================================ // HUVUDBERÄKNING // ============================================================ calculateValue(observation, context = {}) { const scores = { novelty: this.calculateNovelty(observation, context), rarity: this.calculateRarity(observation, context), verification: this.calculateVerificationValue(observation, context), coverage: this.calculateCoverageValue(observation, context), quality: this.calculateQualityValue(observation, context), diversity: this.calculateDiversityValue(observation, context), }; // Beräkna viktat medelvärde let totalValue = 0; for (const [key, score] of Object.entries(scores)) { totalValue += score * this.weights[key]; } // Normalisera till 0-100 const normalizedValue = Math.min(100, Math.max(0, totalValue * 100)); // Beräkna ersättning const reward = normalizedValue * this.baseReward; return { totalValue: normalizedValue, reward, currency: 'USD', breakdown: scores, weights: this.weights, factors: this.explainValue(observation, scores, context), }; } // ============================================================ // NOVELTY — Hur mycket ny information // ============================================================ calculateNovelty(observation, context) { const area = context.area; const areaObservations = this.observationHistory.get(area) || []; if (areaObservations.length === 0) { // Första observationen i området — mycket värdefull return 1.0; } // Jämför med tidigare observationer let maxSimilarity = 0; for (const prev of areaObservations.slice(-100)) { // Senaste 100 const similarity = this.calculateSimilarity(observation, prev); maxSimilarity = Math.max(maxSimilarity, similarity); } // Ju mindre lik, desto mer novel return 1 - maxSimilarity; } calculateSimilarity(a, b) { // Jämför plats const locationSim = this.locationSimilarity(a.location, b.location); // Jämför objekt const objectSim = this.objectSimilarity(a.objects, b.objects); // Jämför tid const timeSim = this.timeSimilarity(a.timestamp, b.timestamp); // Viktat medelvärde return locationSim * 0.5 + objectSim * 0.3 + timeSim * 0.2; } locationSimilarity(a, b) { if (!a || !b) return 0; const distance = this.calculateDistance(a, b); // Exponential decay — 0 similarity at 100m return Math.exp(-distance / 100); } objectSimilarity(a, b) { if (!a || !b) return 0; const aTypes = new Set(a.map(o => o.type)); const bTypes = new Set(b.map(o => o.type)); const intersection = new Set([...aTypes].filter(x => bTypes.has(x))); const union = new Set([...aTypes, ...bTypes]); return intersection.size / union.size; } timeSimilarity(a, b) { if (!a || !b) return 0; const diff = Math.abs(a - b) / 1000; // sekunder // Exponential decay — 0 similarity after 1 hour return Math.exp(-diff / 3600); } // ============================================================ // RARITY — Sällsynta objekttyper // ============================================================ calculateRarity(observation, context) { const objects = observation.objects || []; if (objects.length === 0) return 0.5; let totalRarity = 0; for (const obj of objects) { const count = this.objectHistory.get(obj.type) || 0; const totalObjects = Array.from(this.objectHistory.values()).reduce((a, b) => a + b, 0); if (totalObjects === 0) { totalRarity += 1.0; // Första av denna typ } else { // Ju färre, desto mer sällsynt const frequency = count / totalObjects; totalRarity += 1 - frequency; } } return totalRarity / objects.length; } // ============================================================ // VERIFICATION — Verifiera osäkra observationer // ============================================================ calculateVerificationValue(observation, context) { const objects = observation.objects || []; if (objects.length === 0) return 0.5; let totalValue = 0; for (const obj of objects) { if (obj.needsVerification) { // Verifiering av osäker observation — högt värde totalValue += 1.0; } else if (obj.verificationStatus === 'verified') { // Redan verifierad — lägre värde totalValue += 0.3; } else { // Ingen verifieringsstatus — medelvärde totalValue += 0.6; } } return totalValue / objects.length; } // ============================================================ // COVERAGE — Förbättra spatial täckning // ============================================================ calculateCoverageValue(observation, context) { const area = context.area; const areaObservations = this.observationHistory.get(area) || []; if (areaObservations.length === 0) { return 1.0; // Första observationen } // Beräkna täckning före och efter const coverageBefore = this.calculateCoverage(areaObservations); const newObservations = [...areaObservations, observation]; const coverageAfter = this.calculateCoverage(newObservations); // Värdet är ökningen i täckning return Math.min(1.0, (coverageAfter - coverageBefore) * 10); } calculateCoverage(observations) { const uniqueLocations = new Set(); for (const obs of observations) { if (obs.location) { const key = `${obs.location.lat.toFixed(4)},${obs.location.lng.toFixed(4)}`; uniqueLocations.add(key); } } // Förenklad: anta att 1000 unika platser är 100% return Math.min(1.0, uniqueLocations.size / 1000); } // ============================================================ // QUALITY — Teknisk kvalitet // ============================================================ calculateQualityValue(observation, context) { const quality = observation.quality || {}; let score = 0; // Upplösning score += (quality.resolution || 0.5) * 0.2; // Skärpa score += (quality.sharpness || 0.5) * 0.2; // Exponering score += (quality.exposure || 0.5) * 0.15; // Stabilisering score += (quality.stabilization ? 1.0 : 0.0) * 0.15; // GPS-accuracy score += Math.max(0, 1 - (quality.gpsAccuracy || 50) / 50) * 0.15; // Komplett metadata const metadataScore = this.calculateMetadataScore(observation); score += metadataScore * 0.15; return score; } calculateMetadataScore(observation) { const required = ['timestamp', 'location', 'source']; const optional = ['device', 'camera', 'weather', 'operator']; let score = 0; for (const field of required) { if (observation[field]) score += 0.15; } for (const field of optional) { if (observation[field]) score += 0.05; } return Math.min(1.0, score); } // ============================================================ // DIVERSITY — Ny vinkel, tid, etc. // ============================================================ calculateDiversityValue(observation, context) { const area = context.area; const areaObservations = this.observationHistory.get(area) || []; if (areaObservations.length === 0) { return 1.0; } // Kontrollera om detta är en ny vinkel const hasNewAngle = this.hasNewAngle(observation, areaObservations); // Kontrollera om detta är en ny tid const hasNewTime = this.hasNewTime(observation, areaObservations); // Kontrollera om detta är nytt väder const hasNewWeather = this.hasNewWeather(observation, areaObservations); return (hasNewAngle ? 0.4 : 0) + (hasNewTime ? 0.35 : 0) + (hasNewWeather ? 0.25 : 0); } hasNewAngle(observation, previous) { // Jämför kompassriktning if (!observation.compass) return false; for (const prev of previous.slice(-10)) { if (prev.compass) { const diff = Math.abs(observation.compass - prev.compass); if (diff < 30) return false; // Liknande vinkel } } return true; } hasNewTime(observation, previous) { if (!observation.timestamp) return false; const hour = new Date(observation.timestamp).getHours(); for (const prev of previous) { if (prev.timestamp) { const prevHour = new Date(prev.timestamp).getHours(); if (Math.abs(hour - prevHour) < 3) return false; // Liknande tid } } return true; } hasNewWeather(observation, previous) { if (!observation.weather) return false; for (const prev of previous.slice(-5)) { if (prev.weather === observation.weather) return false; } return true; } // ============================================================ // FÖRKLARING // ============================================================ explainValue(observation, scores, context) { const factors = []; if (scores.novelty > 0.7) { factors.push('Första observationen av detta område/objekt'); } else if (scores.novelty > 0.4) { factors.push('Ny information jämfört med tidigare observationer'); } if (scores.rarity > 0.7) { factors.push('Sällsynt objekttyp'); } if (scores.verification > 0.7) { factors.push('Verifierar osäker tidigare observation'); } if (scores.coverage > 0.5) { factors.push('Förbättrar spatial täckning'); } if (scores.quality > 0.8) { factors.push('Mycket hög teknisk kvalitet'); } if (scores.diversity > 0.5) { factors.push('Ny vinkel, tid eller väderförhållande'); } return factors; } // ============================================================ // HISTORIK // ============================================================ addToHistory(observation, context) { const area = context.area; // Lägg till i area-historik if (!this.observationHistory.has(area)) { this.observationHistory.set(area, []); } this.observationHistory.get(area).push(observation); // Uppdatera objekt-historik for (const obj of observation.objects || []) { const count = this.objectHistory.get(obj.type) || 0; this.objectHistory.set(obj.type, count + 1); } } // ============================================================ // HJÄLPMETODER // ============================================================ calculateDistance(a, b) { const R = 6371e3; const φ1 = a.lat * Math.PI / 180; const φ2 = b.lat * Math.PI / 180; const Δφ = (b.lat - a.lat) * Math.PI / 180; const Δλ = (b.lng - a.lng) * Math.PI / 180; const x = Math.sin(Δφ/2) * Math.sin(Δφ/2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ/2) * Math.sin(Δλ/2); const c = 2 * Math.atan2(Math.sqrt(x), Math.sqrt(1-x)); return R * c; } // ============================================================ // EXEMPEL // ============================================================ static examples() { const calculator = new DataValueCalculator({ baseReward: 0.1 }); console.log('=== DATA VALUE SCORE EXAMPLES ===\n'); // Exempel 1: Första observationen i ett område const obs1 = { location: { lat: 13.75, lng: 100.50 }, timestamp: Date.now(), objects: [{ type: 'streetlight', condition: 'broken' }], quality: { resolution: 1.0, sharpness: 0.9, stabilization: true }, compass: 90, weather: 'sunny', }; const result1 = calculator.calculateValue(obs1, { area: 'bangkok_sukhumvit' }); calculator.addToHistory(obs1, { area: 'bangkok_sukhumvit' }); console.log('Observation 1: Första i området'); console.log(` Total Value: ${result1.totalValue.toFixed(1)}/100`); console.log(` Reward: $${result1.reward.toFixed(2)}`); console.log(` Factors: ${result1.factors.join(', ')}`); console.log(` Breakdown:`, result1.breakdown); console.log(); // Exempel 2: Liknande observation (lågt värde) const obs2 = { location: { lat: 13.7501, lng: 100.5001 }, timestamp: Date.now() + 1000, objects: [{ type: 'streetlight', condition: 'broken' }], quality: { resolution: 1.0, sharpness: 0.9, stabilization: true }, compass: 95, weather: 'sunny', }; const result2 = calculator.calculateValue(obs2, { area: 'bangkok_sukhumvit' }); console.log('Observation 2: Liknande plats och objekt'); console.log(` Total Value: ${result2.totalValue.toFixed(1)}/100`); console.log(` Reward: $${result2.reward.toFixed(2)}`); console.log(` Factors: ${result2.factors.join(', ') || 'Inga särskilda faktorer'}`); console.log(); // Exempel 3: Verifiering av osäker observation const obs3 = { location: { lat: 13.76, lng: 100.51 }, timestamp: Date.now() + 2000, objects: [{ type: 'pothole', needsVerification: true }], quality: { resolution: 0.8, sharpness: 0.7, stabilization: false }, compass: 180, weather: 'cloudy', }; const result3 = calculator.calculateValue(obs3, { area: 'bangkok_sukhumvit' }); calculator.addToHistory(obs3, { area: 'bangkok_sukhumvit' }); console.log('Observation 3: Verifiering av osäker observation'); console.log(` Total Value: ${result3.totalValue.toFixed(1)}/100`); console.log(` Reward: $${result3.reward.toFixed(2)}`); console.log(` Factors: ${result3.factors.join(', ')}`); console.log(); // Exempel 4: Sällsynt objekt const obs4 = { location: { lat: 13.77, lng: 100.52 }, timestamp: Date.now() + 3000, objects: [{ type: 'historic_marker', condition: 'good' }], quality: { resolution: 1.0, sharpness: 0.95, stabilization: true }, compass: 270, weather: 'sunny', }; const result4 = calculator.calculateValue(obs4, { area: 'bangkok_sukhumvit' }); calculator.addToHistory(obs4, { area: 'bangkok_sukhumvit' }); console.log('Observation 4: Sällsynt objekt (historic_marker)'); console.log(` Total Value: ${result4.totalValue.toFixed(1)}/100`); console.log(` Reward: $${result4.reward.toFixed(2)}`); console.log(` Factors: ${result4.factors.join(', ')}`); } } // Exportera module.exports = DataValueCalculator; // Kör exempel if (require.main === module) { DataValueCalculator.examples(); }