/** * 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`); }