Files
boc/quixzoom-capture-pipeline/area-knowledge-graph/graph.js
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- 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
2026-06-29 16:24:48 +00:00

564 lines
16 KiB
JavaScript

/**
* QUIXZOOM Area Knowledge Graph
*
* Hierarkisk struktur:
* Area (Bangkok)
* └── District (Sukhumvit)
* └── Road (Sukhumvit Road)
* └── Segment (Soi 22)
* └── Object (Streetlight)
* └── Observation
* └── Confidence
* └── Last Seen
* └── Needs Verification
* └── Knowledge Gap
*
* Bangkok blir bara den första regionen.
* Samma modell fungerar i Stockholm, London, Tokyo.
*/
class AreaKnowledgeGraph {
constructor() {
this.areas = new Map(); // areaId -> Area
this.objects = new Map(); // objectId -> Object (global index)
this.observations = new Map(); // observationId -> Observation
}
// ============================================================
// AREA
// ============================================================
createArea(config) {
const area = {
id: config.id || this.generateId(),
name: config.name,
bounds: config.bounds,
districts: new Map(),
statistics: {
totalObservations: 0,
totalObjects: 0,
coverage: 0,
lastUpdated: Date.now(),
},
};
this.areas.set(area.id, area);
return area;
}
getArea(areaId) {
return this.areas.get(areaId);
}
// ============================================================
// DISTRICT
// ============================================================
createDistrict(areaId, config) {
const area = this.areas.get(areaId);
if (!area) throw new Error(`Area not found: ${areaId}`);
const district = {
id: config.id || this.generateId(),
name: config.name,
areaId,
bounds: config.bounds,
roads: new Map(),
statistics: {
totalObservations: 0,
totalObjects: 0,
coverage: 0,
},
};
area.districts.set(district.id, district);
return district;
}
getDistrict(areaId, districtId) {
const area = this.areas.get(areaId);
return area?.districts.get(districtId);
}
// ============================================================
// ROAD
// ============================================================
createRoad(areaId, districtId, config) {
const district = this.getDistrict(areaId, districtId);
if (!district) throw new Error(`District not found: ${districtId}`);
const road = {
id: config.id || this.generateId(),
name: config.name,
areaId,
districtId,
type: config.type || 'street', // street, avenue, highway, alley
segments: new Map(),
statistics: {
totalObservations: 0,
totalObjects: 0,
length: config.length || 0,
},
};
district.roads.set(road.id, road);
return road;
}
// ============================================================
// SEGMENT
// ============================================================
createSegment(areaId, districtId, roadId, config) {
const road = this.getRoad(areaId, districtId, roadId);
if (!road) throw new Error(`Road not found: ${roadId}`);
const segment = {
id: config.id || this.generateId(),
name: config.name,
areaId,
districtId,
roadId,
bounds: config.bounds,
objects: new Map(),
observations: [],
statistics: {
totalObservations: 0,
totalObjects: 0,
coverage: 0,
quality: 0,
},
};
road.segments.set(segment.id, segment);
return segment;
}
getSegment(areaId, districtId, roadId, segmentId) {
const road = this.getRoad(areaId, districtId, roadId);
return road?.segments.get(segmentId);
}
// ============================================================
// OBJECT
// ============================================================
createObject(config) {
const object = {
id: config.id || this.generateId(),
type: config.type, // streetlight, pothole, crosswalk, etc.
class: config.class, // infrastructure, vehicle, vegetation, etc.
areaId: config.areaId,
districtId: config.districtId,
roadId: config.roadId,
segmentId: config.segmentId,
location: config.location, // { lat, lng }
firstSeen: Date.now(),
lastSeen: Date.now(),
observations: [],
confidence: config.confidence || 0,
verificationStatus: config.verificationStatus || 'unverified',
needsVerification: config.needsVerification || false,
attributes: config.attributes || {},
};
this.objects.set(object.id, object);
// Lägg till i segment
if (config.segmentId) {
const segment = this.getSegment(config.areaId, config.districtId, config.roadId, config.segmentId);
if (segment) {
segment.objects.set(object.id, object);
}
}
return object;
}
getObject(objectId) {
return this.objects.get(objectId);
}
// ============================================================
// OBSERVATION
// ============================================================
addObservation(config) {
const observation = {
id: config.id || this.generateId(),
objectId: config.objectId,
sourceType: config.sourceType, // quixzoom, youtube_cc, etc.
sourceId: config.sourceId,
timestamp: config.timestamp || Date.now(),
location: config.location,
quality: config.quality || {},
aiAnalysis: config.aiAnalysis || {},
reliability: config.reliability || 0,
verificationStatus: config.verificationStatus || 'unverified',
mediaUrl: config.mediaUrl,
metadata: config.metadata || {},
};
this.observations.set(observation.id, observation);
// Lägg till i objekt
const object = this.objects.get(config.objectId);
if (object) {
object.observations.push(observation.id);
object.lastSeen = observation.timestamp;
// Uppdatera konfidens
const allObservations = object.observations.map(id => this.observations.get(id));
const avgConfidence = allObservations.reduce((sum, obs) => sum + (obs?.reliability || 0), 0) / allObservations.length;
object.confidence = avgConfidence;
// Markera som verifierad om tillräckligt många observationer
if (allObservations.length >= 3 && avgConfidence > 0.7) {
object.verificationStatus = 'verified';
object.needsVerification = false;
}
}
// Uppdatera statistik
this.updateStatistics(config.areaId);
return observation;
}
// ============================================================
// KNOWLEDGE GAPS
// ============================================================
identifyKnowledgeGaps(areaId) {
const area = this.areas.get(areaId);
if (!area) return [];
const gaps = [];
// 1. Spatiala gap — områden utan observationer
const spatialGaps = this.findSpatialGaps(area);
gaps.push(...spatialGaps);
// 2. Temporala gap — tider utan data
const temporalGaps = this.findTemporalGaps(area);
gaps.push(...temporalGaps);
// 3. Objekt-typer med få exempel
const categoricalGaps = this.findCategoricalGaps(area);
gaps.push(...categoricalGaps);
// 4. Områden med låg konfidens
const confidenceGaps = this.findConfidenceGaps(area);
gaps.push(...confidenceGaps);
// 5. Objekt som behöver verifiering
const verificationGaps = this.findVerificationGaps(area);
gaps.push(...verificationGaps);
return gaps.sort((a, b) => b.priority - a.priority);
}
findSpatialGaps(area) {
const gaps = [];
const gridSize = 0.001; // ~100m
for (const district of area.districts.values()) {
for (const road of district.roads.values()) {
for (const segment of road.segments.values()) {
if (segment.observations.length < 5) {
gaps.push({
type: 'spatial',
subtype: 'insufficient_coverage',
location: segment.bounds,
description: `${segment.name} har få observationer`,
priority: 0.7,
suggestedAction: 'Dokumentera segmentet mer noggrant',
});
}
}
}
}
return gaps;
}
findTemporalGaps(area) {
const gaps = [];
const hourCounts = new Array(24).fill(0);
for (const observation of this.observations.values()) {
if (observation.location && this.isInArea(observation.location, area)) {
const hour = new Date(observation.timestamp).getHours();
hourCounts[hour]++;
}
}
const avgCount = this.observations.size / 24;
for (let hour = 0; hour < 24; hour++) {
if (hourCounts[hour] < avgCount * 0.3) {
gaps.push({
type: 'temporal',
subtype: 'missing_time_period',
timeOfDay: hour,
description: `Få observationer kl ${hour}:00-${hour + 1}:00`,
priority: 0.5,
suggestedAction: `Dokumentera området kl ${hour}:00`,
});
}
}
return gaps;
}
findCategoricalGaps(area) {
const gaps = [];
const objectTypes = new Map();
for (const object of this.objects.values()) {
if (object.areaId === area.id) {
const count = objectTypes.get(object.type) || 0;
objectTypes.set(object.type, count + 1);
}
}
const avgCount = this.objects.size / objectTypes.size;
for (const [type, count] of objectTypes) {
if (count < avgCount * 0.2) {
gaps.push({
type: 'categorical',
subtype: 'rare_object_type',
objectType: type,
description: `Få exempel av ${type} (${count} observationer)`,
priority: 0.6,
suggestedAction: `Dokumentera fler ${type}`,
});
}
}
return gaps;
}
findConfidenceGaps(area) {
const gaps = [];
for (const object of this.objects.values()) {
if (object.areaId === area.id && object.confidence < 0.5) {
gaps.push({
type: 'confidence',
subtype: 'low_confidence_object',
objectId: object.id,
description: `${object.type} har låg konfidens (${(object.confidence * 100).toFixed(0)}%)`,
priority: 0.8,
suggestedAction: 'Verifiera objektet från annan vinkel',
});
}
}
return gaps;
}
findVerificationGaps(area) {
const gaps = [];
for (const object of this.objects.values()) {
if (object.areaId === area.id && object.needsVerification) {
gaps.push({
type: 'verification',
subtype: 'needs_verification',
objectId: object.id,
description: `${object.type} behöver verifiering`,
priority: 0.9,
suggestedAction: 'Dokumentera samma objekt från annan vinkel eller tid',
});
}
}
return gaps;
}
// ============================================================
// STATISTICS
// ============================================================
updateStatistics(areaId) {
const area = this.areas.get(areaId);
if (!area) return;
let totalObservations = 0;
let totalObjects = 0;
for (const district of area.districts.values()) {
for (const road of district.roads.values()) {
for (const segment of road.segments.values()) {
totalObservations += segment.observations.length;
totalObjects += segment.objects.size;
}
}
}
area.statistics = {
totalObservations,
totalObjects,
coverage: this.calculateCoverage(area),
lastUpdated: Date.now(),
};
}
calculateCoverage(area) {
const uniqueLocations = new Set();
for (const observation of this.observations.values()) {
if (observation.location && this.isInArea(observation.location, area)) {
const key = `${observation.location.lat.toFixed(4)},${observation.location.lng.toFixed(4)}`;
uniqueLocations.add(key);
}
}
// Förenklad: anta att 1000 unika platser är 100% täckning
return Math.min(1.0, uniqueLocations.size / 1000);
}
isInArea(location, area) {
const bounds = area.bounds;
return location.lat >= bounds.minLat &&
location.lat <= bounds.maxLat &&
location.lng >= bounds.minLng &&
location.lng <= bounds.maxLng;
}
// ============================================================
// EXPORT
// ============================================================
exportToJSON(areaId) {
const area = this.areas.get(areaId);
if (!area) return null;
const districts = [];
for (const district of area.districts.values()) {
const roads = [];
for (const road of district.roads.values()) {
const segments = [];
for (const segment of road.segments.values()) {
const objects = [];
for (const object of segment.objects.values()) {
const observations = object.observations.map(id => this.observations.get(id));
objects.push({
...object,
observations,
});
}
segments.push({
...segment,
objects,
});
}
roads.push({
...road,
segments,
});
}
districts.push({
...district,
roads,
});
}
return {
...area,
districts,
};
}
// ============================================================
// HJÄLPMETODER
// ============================================================
generateId() {
return `id_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
getRoad(areaId, districtId, roadId) {
const district = this.getDistrict(areaId, districtId);
return district?.roads.get(roadId);
}
}
// Exportera
module.exports = AreaKnowledgeGraph;
// Demo
if (require.main === module) {
const graph = new AreaKnowledgeGraph();
console.log('=== AREA KNOWLEDGE GRAPH DEMO ===\n');
// Skapa Bangkok
const bangkok = graph.createArea({
name: 'Bangkok',
bounds: { minLat: 13.5, maxLat: 14.0, minLng: 100.3, maxLng: 100.9 },
});
console.log(`Created area: ${bangkok.name} (${bangkok.id})`);
// Skapa distrikt
const sukhumvit = graph.createDistrict(bangkok.id, {
name: 'Sukhumvit',
bounds: { minLat: 13.7, maxLat: 13.8, minLng: 100.5, maxLng: 100.6 },
});
console.log(`Created district: ${sukhumvit.name}`);
// Skapa väg
const sukhumvitRoad = graph.createRoad(bangkok.id, sukhumvit.id, {
name: 'Sukhumvit Road',
type: 'avenue',
length: 4000,
});
console.log(`Created road: ${sukhumvitRoad.name}`);
// Skapa segment
const soi22 = graph.createSegment(bangkok.id, sukhumvit.id, sukhumvitRoad.id, {
name: 'Soi 22',
bounds: { minLat: 13.72, maxLat: 13.73, minLng: 100.55, maxLng: 100.56 },
});
console.log(`Created segment: ${soi22.name}`);
// Skapa objekt
const streetlight = graph.createObject({
type: 'streetlight',
class: 'infrastructure',
areaId: bangkok.id,
districtId: sukhumvit.id,
roadId: sukhumvitRoad.id,
segmentId: soi22.id,
location: { lat: 13.725, lng: 100.555 },
confidence: 0.8,
attributes: { condition: 'broken', height: 5 },
});
console.log(`Created object: ${streetlight.type} (${streetlight.id})`);
// Lägg till observation
const observation = graph.addObservation({
objectId: streetlight.id,
sourceType: 'quixzoom',
location: { lat: 13.725, lng: 100.555 },
quality: { sharpness: 0.9, exposure: 0.8 },
aiAnalysis: { objectDetection: 0.92, condition: 'broken' },
reliability: 0.85,
});
console.log(`Added observation: ${observation.id}`);
// Identifiera kunskapsgap
const gaps = graph.identifyKnowledgeGaps(bangkok.id);
console.log(`\nKnowledge gaps found: ${gaps.length}`);
for (const gap of gaps) {
console.log(` - ${gap.type}: ${gap.description} (priority: ${gap.priority})`);
}
// Exportera
const exported = graph.exportToJSON(bangkok.id);
console.log(`\nExported area with ${exported.districts.length} districts`);
}