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
798 lines
21 KiB
JavaScript
798 lines
21 KiB
JavaScript
/**
|
|
* QUIXZOOM Urban Knowledge Graph (UKG)
|
|
*
|
|
* Centralt datalager — den verkliga "hjärnan" i QUIXZOOM.
|
|
*
|
|
* Tre lager:
|
|
* 1. Observation Layer — Rådata → observationer
|
|
* 2. Evidence Layer — Bevis för varje objekt
|
|
* 3. Temporal Layer — Förändring över tid
|
|
*
|
|
* Varje objekt i staden får:
|
|
* - Globalt objekt-ID
|
|
* - Geografisk position
|
|
* - Typ
|
|
* - Attribut
|
|
* - Observationer
|
|
* - Historik
|
|
* - Evidens
|
|
* - Verifieringsstatus
|
|
* - Osäkerhet
|
|
* - Relationer till andra objekt
|
|
*/
|
|
|
|
const crypto = require('crypto');
|
|
|
|
class UrbanKnowledgeGraph {
|
|
constructor() {
|
|
// Objekt-index
|
|
this.objects = new Map(); // objectId -> UrbanObject
|
|
|
|
// Spatialt index (för snabb geografisk sökning)
|
|
this.spatialIndex = new Map(); // gridKey -> Set(objectIds)
|
|
|
|
// Typ-index
|
|
this.typeIndex = new Map(); // type -> Set(objectIds)
|
|
|
|
// Observationer
|
|
this.observations = new Map(); // observationId -> Observation
|
|
|
|
// Evidens
|
|
this.evidence = new Map(); // evidenceId -> Evidence
|
|
|
|
// Temporal
|
|
this.temporal = new Map(); // objectId -> [TemporalRecord]
|
|
|
|
// Relationer
|
|
this.relations = new Map(); // objectId -> [Relation]
|
|
|
|
// Grid-storlek för spatialt index (m)
|
|
this.gridSize = 10;
|
|
}
|
|
|
|
// ============================================================
|
|
// OBJEKT-HANTERING
|
|
// ============================================================
|
|
|
|
createObject(config) {
|
|
const objectId = config.id || this.generateObjectId(config);
|
|
|
|
const obj = {
|
|
id: objectId,
|
|
type: config.type, // street_lamp, pothole, crosswalk, etc.
|
|
subtype: config.subtype,
|
|
|
|
// Geografi
|
|
location: {
|
|
lat: config.location.lat,
|
|
lng: config.location.lng,
|
|
accuracy: config.location.accuracy || 5,
|
|
altitude: config.location.altitude,
|
|
},
|
|
|
|
// Attribut (kan ändras över tid)
|
|
attributes: config.attributes || {},
|
|
|
|
// Metadata
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
createdBy: config.createdBy,
|
|
|
|
// Status
|
|
status: 'active', // active, removed, modified
|
|
verificationStatus: config.verificationStatus || 'unverified',
|
|
|
|
// Osäkerhet
|
|
confidence: config.confidence || 0,
|
|
uncertainty: config.uncertainty || 1.0,
|
|
|
|
// Referenser
|
|
observationIds: [],
|
|
evidenceIds: [],
|
|
temporalIds: [],
|
|
relationIds: [],
|
|
};
|
|
|
|
this.objects.set(objectId, obj);
|
|
|
|
// Uppdatera index
|
|
this.addToSpatialIndex(objectId, obj.location);
|
|
this.addToTypeIndex(objectId, obj.type);
|
|
|
|
console.log(`[UKG] Object created: ${objectId} (${obj.type})`);
|
|
|
|
return obj;
|
|
}
|
|
|
|
getObject(objectId) {
|
|
return this.objects.get(objectId);
|
|
}
|
|
|
|
updateObject(objectId, updates) {
|
|
const obj = this.objects.get(objectId);
|
|
if (!obj) return null;
|
|
|
|
// Spara gammalt tillstånd i temporal layer
|
|
this.addTemporalRecord(objectId, {
|
|
timestamp: Date.now(),
|
|
attributes: { ...obj.attributes },
|
|
verificationStatus: obj.verificationStatus,
|
|
confidence: obj.confidence,
|
|
});
|
|
|
|
// Uppdatera objekt
|
|
if (updates.attributes) {
|
|
obj.attributes = { ...obj.attributes, ...updates.attributes };
|
|
}
|
|
if (updates.location) {
|
|
// Ta bort från gammalt spatialt index
|
|
this.removeFromSpatialIndex(objectId, obj.location);
|
|
// Uppdatera plats
|
|
obj.location = { ...obj.location, ...updates.location };
|
|
// Lägg till i nytt spatialt index
|
|
this.addToSpatialIndex(objectId, obj.location);
|
|
}
|
|
if (updates.verificationStatus) {
|
|
obj.verificationStatus = updates.verificationStatus;
|
|
}
|
|
if (updates.confidence !== undefined) {
|
|
obj.confidence = updates.confidence;
|
|
}
|
|
|
|
obj.updatedAt = Date.now();
|
|
|
|
console.log(`[UKG] Object updated: ${objectId}`);
|
|
|
|
return obj;
|
|
}
|
|
|
|
// ============================================================
|
|
// OBSERVATION LAYER
|
|
// ============================================================
|
|
|
|
addObservation(config) {
|
|
const observationId = config.id || this.generateId('obs');
|
|
|
|
const observation = {
|
|
id: observationId,
|
|
timestamp: config.timestamp || Date.now(),
|
|
|
|
// Källa
|
|
source: {
|
|
type: config.sourceType, // quixzoom, youtube_cc, inspector, municipality
|
|
id: config.sourceId,
|
|
operator: config.operator,
|
|
},
|
|
|
|
// Media
|
|
media: {
|
|
type: config.mediaType, // video, image
|
|
url: config.mediaUrl,
|
|
frameNumber: config.frameNumber,
|
|
timestamp: config.mediaTimestamp,
|
|
},
|
|
|
|
// Vad som observerades
|
|
observedObjects: config.observedObjects || [],
|
|
|
|
// Plats
|
|
location: config.location,
|
|
|
|
// Kvalitet
|
|
quality: config.quality || {},
|
|
|
|
// AI-analys
|
|
aiAnalysis: config.aiAnalysis || {},
|
|
|
|
// Metadata
|
|
metadata: config.metadata || {},
|
|
};
|
|
|
|
this.observations.set(observationId, observation);
|
|
|
|
// Koppla till objekt
|
|
for (const observed of observation.observedObjects) {
|
|
if (observed.objectId) {
|
|
const obj = this.objects.get(observed.objectId);
|
|
if (obj) {
|
|
obj.observationIds.push(observationId);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`[UKG] Observation added: ${observationId}`);
|
|
|
|
return observation;
|
|
}
|
|
|
|
getObservation(observationId) {
|
|
return this.observations.get(observationId);
|
|
}
|
|
|
|
// ============================================================
|
|
// EVIDENCE LAYER
|
|
// ============================================================
|
|
|
|
addEvidence(config) {
|
|
const evidenceId = config.id || this.generateId('ev');
|
|
|
|
const evidence = {
|
|
id: evidenceId,
|
|
objectId: config.objectId,
|
|
observationId: config.observationId,
|
|
|
|
// Bevis
|
|
proof: {
|
|
type: config.proofType, // image, video, lidar, manual_inspection
|
|
url: config.proofUrl,
|
|
frameNumber: config.frameNumber,
|
|
timestamp: config.proofTimestamp,
|
|
},
|
|
|
|
// Vem som lade till beviset
|
|
source: {
|
|
type: config.sourceType, // quixzoom, inspector, municipality, ai
|
|
id: config.sourceId,
|
|
operator: config.operator,
|
|
},
|
|
|
|
// Kvalitet
|
|
quality: config.quality || {},
|
|
reliability: config.reliability || 0,
|
|
|
|
// Metadata
|
|
timestamp: config.timestamp || Date.now(),
|
|
metadata: config.metadata || {},
|
|
};
|
|
|
|
this.evidence.set(evidenceId, evidence);
|
|
|
|
// Koppla till objekt
|
|
const obj = this.objects.get(config.objectId);
|
|
if (obj) {
|
|
obj.evidenceIds.push(evidenceId);
|
|
}
|
|
|
|
console.log(`[UKG] Evidence added: ${evidenceId} for object ${config.objectId}`);
|
|
|
|
return evidence;
|
|
}
|
|
|
|
getEvidenceForObject(objectId) {
|
|
const obj = this.objects.get(objectId);
|
|
if (!obj) return [];
|
|
|
|
return obj.evidenceIds.map(id => this.evidence.get(id)).filter(Boolean);
|
|
}
|
|
|
|
// ============================================================
|
|
// TEMPORAL LAYER
|
|
// ============================================================
|
|
|
|
addTemporalRecord(objectId, record) {
|
|
const temporalId = this.generateId('tmp');
|
|
|
|
const temporalRecord = {
|
|
id: temporalId,
|
|
objectId,
|
|
timestamp: record.timestamp || Date.now(),
|
|
|
|
// Tillstånd vid denna tidpunkt
|
|
state: {
|
|
attributes: record.attributes,
|
|
verificationStatus: record.verificationStatus,
|
|
confidence: record.confidence,
|
|
status: record.status,
|
|
},
|
|
|
|
// Vad som ändrades
|
|
changes: record.changes || [],
|
|
|
|
// Orsak till ändring
|
|
reason: record.reason,
|
|
source: record.source,
|
|
};
|
|
|
|
if (!this.temporal.has(objectId)) {
|
|
this.temporal.set(objectId, []);
|
|
}
|
|
|
|
this.temporal.get(objectId).push(temporalRecord);
|
|
|
|
// Koppla till objekt
|
|
const obj = this.objects.get(objectId);
|
|
if (obj) {
|
|
obj.temporalIds.push(temporalId);
|
|
}
|
|
|
|
return temporalRecord;
|
|
}
|
|
|
|
getTemporalHistory(objectId) {
|
|
return this.temporal.get(objectId) || [];
|
|
}
|
|
|
|
getObjectAtTime(objectId, timestamp) {
|
|
const history = this.getTemporalHistory(objectId);
|
|
if (history.length === 0) return null;
|
|
|
|
// Hitta senaste record före timestamp
|
|
let closest = null;
|
|
for (const record of history) {
|
|
if (record.timestamp <= timestamp) {
|
|
closest = record;
|
|
}
|
|
}
|
|
|
|
return closest;
|
|
}
|
|
|
|
// ============================================================
|
|
// RELATIONER
|
|
// ============================================================
|
|
|
|
addRelation(config) {
|
|
const relationId = this.generateId('rel');
|
|
|
|
const relation = {
|
|
id: relationId,
|
|
fromObjectId: config.fromObjectId,
|
|
toObjectId: config.toObjectId,
|
|
type: config.relationType, // connected_to, part_of, near, supports, etc.
|
|
attributes: config.attributes || {},
|
|
confidence: config.confidence || 0,
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
if (!this.relations.has(config.fromObjectId)) {
|
|
this.relations.set(config.fromObjectId, []);
|
|
}
|
|
this.relations.get(config.fromObjectId).push(relation);
|
|
|
|
// Koppla till objekt
|
|
const fromObj = this.objects.get(config.fromObjectId);
|
|
if (fromObj) {
|
|
fromObj.relationIds.push(relationId);
|
|
}
|
|
|
|
console.log(`[UKG] Relation added: ${relationId} (${config.fromObjectId} -> ${config.toObjectId})`);
|
|
|
|
return relation;
|
|
}
|
|
|
|
getRelations(objectId) {
|
|
return this.relations.get(objectId) || [];
|
|
}
|
|
|
|
// ============================================================
|
|
// SPATIAL SÖKNING
|
|
// ============================================================
|
|
|
|
findNearbyObjects(location, radius = 50) {
|
|
const nearby = new Set();
|
|
const gridKeys = this.getGridKeysInRadius(location, radius);
|
|
|
|
for (const key of gridKeys) {
|
|
const objectIds = this.spatialIndex.get(key);
|
|
if (objectIds) {
|
|
for (const objectId of objectIds) {
|
|
const obj = this.objects.get(objectId);
|
|
if (obj) {
|
|
const distance = this.calculateDistance(location, obj.location);
|
|
if (distance <= radius) {
|
|
nearby.add(obj);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return Array.from(nearby);
|
|
}
|
|
|
|
findObjectsByType(type, area = null) {
|
|
const objectIds = this.typeIndex.get(type);
|
|
if (!objectIds) return [];
|
|
|
|
const objects = Array.from(objectIds)
|
|
.map(id => this.objects.get(id))
|
|
.filter(Boolean);
|
|
|
|
if (area) {
|
|
return objects.filter(obj => this.isInArea(obj.location, area));
|
|
}
|
|
|
|
return objects;
|
|
}
|
|
|
|
// ============================================================
|
|
// MERGE-OBJEKT (hundra videor → ett objekt)
|
|
// ============================================================
|
|
|
|
mergeObjects(targetId, sourceIds) {
|
|
const target = this.objects.get(targetId);
|
|
if (!target) return null;
|
|
|
|
for (const sourceId of sourceIds) {
|
|
const source = this.objects.get(sourceId);
|
|
if (!source || source.id === targetId) continue;
|
|
|
|
// Flytta observationer
|
|
for (const obsId of source.observationIds) {
|
|
if (!target.observationIds.includes(obsId)) {
|
|
target.observationIds.push(obsId);
|
|
}
|
|
}
|
|
|
|
// Flytta evidens
|
|
for (const evId of source.evidenceIds) {
|
|
if (!target.evidenceIds.includes(evId)) {
|
|
target.evidenceIds.push(evId);
|
|
}
|
|
|
|
// Uppdatera evidens-referens
|
|
const ev = this.evidence.get(evId);
|
|
if (ev) {
|
|
ev.objectId = targetId;
|
|
}
|
|
}
|
|
|
|
// Flytta temporal
|
|
for (const tmpId of source.temporalIds) {
|
|
if (!target.temporalIds.includes(tmpId)) {
|
|
target.temporalIds.push(tmpId);
|
|
}
|
|
}
|
|
|
|
// Ta bort källa
|
|
this.objects.delete(sourceId);
|
|
this.removeFromSpatialIndex(sourceId, source.location);
|
|
this.removeFromTypeIndex(sourceId, source.type);
|
|
|
|
console.log(`[UKG] Merged ${sourceId} into ${targetId}`);
|
|
}
|
|
|
|
// Uppdatera konfidens
|
|
target.confidence = Math.min(1.0, target.confidence + 0.1 * sourceIds.length);
|
|
target.verificationStatus = 'verified';
|
|
|
|
return target;
|
|
}
|
|
|
|
// ============================================================
|
|
// STATISTIK OCH ANALYS
|
|
// ============================================================
|
|
|
|
getStatistics() {
|
|
return {
|
|
totalObjects: this.objects.size,
|
|
totalObservations: this.observations.size,
|
|
totalEvidence: this.evidence.size,
|
|
totalTemporalRecords: Array.from(this.temporal.values()).reduce((sum, arr) => sum + arr.length, 0),
|
|
totalRelations: Array.from(this.relations.values()).reduce((sum, arr) => sum + arr.length, 0),
|
|
|
|
byType: this.getTypeStatistics(),
|
|
byVerification: this.getVerificationStatistics(),
|
|
};
|
|
}
|
|
|
|
getTypeStatistics() {
|
|
const stats = new Map();
|
|
|
|
for (const obj of this.objects.values()) {
|
|
const count = stats.get(obj.type) || 0;
|
|
stats.set(obj.type, count + 1);
|
|
}
|
|
|
|
return Object.fromEntries(stats);
|
|
}
|
|
|
|
getVerificationStatistics() {
|
|
const stats = { verified: 0, unverified: 0, disputed: 0 };
|
|
|
|
for (const obj of this.objects.values()) {
|
|
stats[obj.verificationStatus] = (stats[obj.verificationStatus] || 0) + 1;
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
// ============================================================
|
|
// EXPORT
|
|
// ============================================================
|
|
|
|
exportObject(objectId) {
|
|
const obj = this.objects.get(objectId);
|
|
if (!obj) return null;
|
|
|
|
return {
|
|
...obj,
|
|
observations: obj.observationIds.map(id => this.observations.get(id)).filter(Boolean),
|
|
evidence: obj.evidenceIds.map(id => this.evidence.get(id)).filter(Boolean),
|
|
temporal: this.getTemporalHistory(objectId),
|
|
relations: this.getRelations(objectId),
|
|
};
|
|
}
|
|
|
|
exportArea(area) {
|
|
const objects = [];
|
|
|
|
for (const obj of this.objects.values()) {
|
|
if (this.isInArea(obj.location, area)) {
|
|
objects.push(this.exportObject(obj.id));
|
|
}
|
|
}
|
|
|
|
return {
|
|
area,
|
|
statistics: this.getStatistics(),
|
|
objects,
|
|
};
|
|
}
|
|
|
|
// ============================================================
|
|
// HJÄLPMETODER
|
|
// ============================================================
|
|
|
|
generateObjectId(config) {
|
|
// Deterministiskt ID baserat på plats och typ
|
|
const lat = config.location.lat.toFixed(6);
|
|
const lng = config.location.lng.toFixed(6);
|
|
const type = config.type;
|
|
|
|
const hash = crypto.createHash('md5')
|
|
.update(`${type}_${lat}_${lng}`)
|
|
.digest('hex')
|
|
.substring(0, 12);
|
|
|
|
return `${type}_${hash}`;
|
|
}
|
|
|
|
generateId(prefix) {
|
|
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
}
|
|
|
|
getGridKey(location) {
|
|
const x = Math.floor(location.lng * 1000 / this.gridSize);
|
|
const y = Math.floor(location.lat * 1000 / this.gridSize);
|
|
return `${x},${y}`;
|
|
}
|
|
|
|
getGridKeysInRadius(location, radius) {
|
|
const keys = [];
|
|
const radiusInGrid = Math.ceil(radius / this.gridSize);
|
|
|
|
const centerX = Math.floor(location.lng * 1000 / this.gridSize);
|
|
const centerY = Math.floor(location.lat * 1000 / this.gridSize);
|
|
|
|
for (let dx = -radiusInGrid; dx <= radiusInGrid; dx++) {
|
|
for (let dy = -radiusInGrid; dy <= radiusInGrid; dy++) {
|
|
keys.push(`${centerX + dx},${centerY + dy}`);
|
|
}
|
|
}
|
|
|
|
return keys;
|
|
}
|
|
|
|
addToSpatialIndex(objectId, location) {
|
|
const key = this.getGridKey(location);
|
|
if (!this.spatialIndex.has(key)) {
|
|
this.spatialIndex.set(key, new Set());
|
|
}
|
|
this.spatialIndex.get(key).add(objectId);
|
|
}
|
|
|
|
removeFromSpatialIndex(objectId, location) {
|
|
const key = this.getGridKey(location);
|
|
const set = this.spatialIndex.get(key);
|
|
if (set) {
|
|
set.delete(objectId);
|
|
}
|
|
}
|
|
|
|
addToTypeIndex(objectId, type) {
|
|
if (!this.typeIndex.has(type)) {
|
|
this.typeIndex.set(type, new Set());
|
|
}
|
|
this.typeIndex.get(type).add(objectId);
|
|
}
|
|
|
|
removeFromTypeIndex(objectId, type) {
|
|
const set = this.typeIndex.get(type);
|
|
if (set) {
|
|
set.delete(objectId);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
isInArea(location, area) {
|
|
return location.lat >= area.minLat &&
|
|
location.lat <= area.maxLat &&
|
|
location.lng >= area.minLng &&
|
|
location.lng <= area.maxLng;
|
|
}
|
|
}
|
|
|
|
// Exportera
|
|
module.exports = UrbanKnowledgeGraph;
|
|
|
|
// Demo
|
|
if (require.main === module) {
|
|
const ukg = new UrbanKnowledgeGraph();
|
|
|
|
console.log('=== URBAN KNOWLEDGE GRAPH DEMO ===\n');
|
|
|
|
// Skapa objekt: Street Lamp #1293
|
|
const lamp = ukg.createObject({
|
|
type: 'street_lamp',
|
|
location: { lat: 13.725, lng: 100.555, accuracy: 2 },
|
|
attributes: {
|
|
material: 'steel',
|
|
height: 7.8,
|
|
paint: 'grey',
|
|
rust: false,
|
|
lean: 0,
|
|
light: 'working',
|
|
},
|
|
confidence: 0.94,
|
|
createdBy: 'quixzoom_system',
|
|
});
|
|
|
|
console.log(`Created object: ${lamp.id}`);
|
|
console.log(`Attributes:`, lamp.attributes);
|
|
console.log();
|
|
|
|
// Lägg till observation
|
|
const obs1 = ukg.addObservation({
|
|
sourceType: 'quixzoom',
|
|
sourceId: 'zoomer_001',
|
|
mediaType: 'video',
|
|
mediaUrl: 'https://r2.landvex.com/videos/bangkok_001.mp4',
|
|
frameNumber: 342,
|
|
observedObjects: [{
|
|
objectId: lamp.id,
|
|
confidence: 0.94,
|
|
bbox: [100, 200, 150, 300],
|
|
}],
|
|
location: { lat: 13.725, lng: 100.555 },
|
|
quality: { resolution: 1080, sharpness: 0.9 },
|
|
aiAnalysis: { objectDetection: 0.94, condition: 'good' },
|
|
});
|
|
|
|
console.log(`Added observation: ${obs1.id}`);
|
|
console.log();
|
|
|
|
// Lägg till evidens från flera källor
|
|
const ev1 = ukg.addEvidence({
|
|
objectId: lamp.id,
|
|
observationId: obs1.id,
|
|
proofType: 'video',
|
|
proofUrl: 'https://r2.landvex.com/videos/bangkok_001.mp4#frame=342',
|
|
frameNumber: 342,
|
|
sourceType: 'quixzoom',
|
|
sourceId: 'zoomer_001',
|
|
reliability: 0.85,
|
|
});
|
|
|
|
const ev2 = ukg.addEvidence({
|
|
objectId: lamp.id,
|
|
proofType: 'video',
|
|
proofUrl: 'https://r2.landvex.com/videos/bangkok_002.mp4#frame=91',
|
|
frameNumber: 91,
|
|
sourceType: 'quixzoom',
|
|
sourceId: 'zoomer_002',
|
|
reliability: 0.80,
|
|
});
|
|
|
|
const ev3 = ukg.addEvidence({
|
|
objectId: lamp.id,
|
|
proofType: 'manual_inspection',
|
|
sourceType: 'inspector',
|
|
sourceId: 'inspector_001',
|
|
reliability: 0.95,
|
|
});
|
|
|
|
console.log(`Added ${lamp.evidenceIds.length} evidence items`);
|
|
console.log();
|
|
|
|
// Temporal: Förändring över tid
|
|
console.log('=== TEMPORAL HISTORY ===');
|
|
|
|
ukg.addTemporalRecord(lamp.id, {
|
|
timestamp: new Date('2026-06-01').getTime(),
|
|
attributes: { ...lamp.attributes, rust: false, lean: 0, light: 'working' },
|
|
verificationStatus: 'verified',
|
|
confidence: 0.95,
|
|
reason: 'Initial observation',
|
|
});
|
|
|
|
ukg.addTemporalRecord(lamp.id, {
|
|
timestamp: new Date('2026-08-12').getTime(),
|
|
attributes: { ...lamp.attributes, rust: false, lean: 2, light: 'working' },
|
|
verificationStatus: 'verified',
|
|
confidence: 0.90,
|
|
reason: 'Tilt detected',
|
|
changes: ['lean: 0 -> 2'],
|
|
});
|
|
|
|
ukg.addTemporalRecord(lamp.id, {
|
|
timestamp: new Date('2026-09-20').getTime(),
|
|
attributes: { ...lamp.attributes, rust: true, lean: 4, light: 'flickering' },
|
|
verificationStatus: 'verified',
|
|
confidence: 0.85,
|
|
reason: 'Corrosion increasing',
|
|
changes: ['rust: false -> true', 'lean: 2 -> 4', 'light: working -> flickering'],
|
|
});
|
|
|
|
ukg.addTemporalRecord(lamp.id, {
|
|
timestamp: new Date('2026-10-11').getTime(),
|
|
attributes: { ...lamp.attributes, rust: true, lean: 4, light: 'broken' },
|
|
verificationStatus: 'verified',
|
|
confidence: 0.95,
|
|
reason: 'Light broken',
|
|
changes: ['light: flickering -> broken'],
|
|
});
|
|
|
|
ukg.addTemporalRecord(lamp.id, {
|
|
timestamp: new Date('2026-11-02').getTime(),
|
|
attributes: { ...lamp.attributes, rust: false, lean: 0, light: 'working' },
|
|
verificationStatus: 'verified',
|
|
confidence: 0.98,
|
|
reason: 'Replaced',
|
|
changes: ['status: repaired'],
|
|
});
|
|
|
|
const history = ukg.getTemporalHistory(lamp.id);
|
|
for (const record of history) {
|
|
const date = new Date(record.timestamp).toISOString().split('T')[0];
|
|
console.log(` ${date}: ${record.reason}`);
|
|
if (record.changes.length > 0) {
|
|
console.log(` Changes: ${record.changes.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
console.log();
|
|
|
|
// Visa evidens
|
|
console.log('=== EVIDENCE ===');
|
|
const evidence = ukg.getEvidenceForObject(lamp.id);
|
|
for (const ev of evidence) {
|
|
console.log(` ${ev.source.type}: ${ev.proofType} (reliability: ${ev.reliability})`);
|
|
}
|
|
|
|
console.log();
|
|
|
|
// Statistik
|
|
console.log('=== STATISTICS ===');
|
|
const stats = ukg.getStatistics();
|
|
console.log(`Total objects: ${stats.totalObjects}`);
|
|
console.log(`Total observations: ${stats.totalObservations}`);
|
|
console.log(`Total evidence: ${stats.totalEvidence}`);
|
|
console.log(`Total temporal records: ${stats.totalTemporalRecords}`);
|
|
|
|
console.log();
|
|
|
|
// Exportera objekt
|
|
console.log('=== EXPORTED OBJECT ===');
|
|
const exported = ukg.exportObject(lamp.id);
|
|
console.log(`ID: ${exported.id}`);
|
|
console.log(`Type: ${exported.type}`);
|
|
console.log(`Observations: ${exported.observations.length}`);
|
|
console.log(`Evidence: ${exported.evidence.length}`);
|
|
console.log(`Temporal records: ${exported.temporal.length}`);
|
|
}
|