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
503 lines
15 KiB
JavaScript
503 lines
15 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|