Files
boc/quixzoom-capture-pipeline/urban-ontology/ontology.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

538 lines
15 KiB
JavaScript

/**
* QUIXZOOM Urban Ontology
*
* Semantiskt lager ovanpå objektigenkänning.
* Gör det möjligt att resonera om relationer mellan objekt.
*
* Exempel:
* - "Den här gatlyktan är släckt. Vilka övergångsställen påverkas?"
* - "Vilka skyltar riskerar att döljas om trädet fortsätter växa?"
* - "Vilka objekt påverkas om detta elskåp tas ur drift?"
*/
class UrbanOntology {
constructor() {
// Relationstyper
this.relationTypes = new Map();
// Objekttyper och deras möjliga relationer
this.objectTypes = new Map();
// Instansierade relationer
this.relations = new Map();
this.initializeOntology();
}
/**
* ============================================================
* INITIALISERA ONTOLOGI
* ============================================================
*/
initializeOntology() {
// Definiera relationstyper
this.defineRelationType('illuminates', {
description: 'Belyser',
directional: true,
domain: ['street_lamp', 'flood_light'],
range: ['road', 'crosswalk', 'sidewalk'],
properties: {
intensity: 'number', // Lux
coverage: 'number', // Meters
},
});
this.defineRelationType('belongs_to', {
description: 'Tillhör',
directional: true,
domain: ['road', 'sidewalk', 'crosswalk'],
range: ['street_network'],
properties: {},
});
this.defineRelationType('crosses', {
description: 'Korsar',
directional: true,
domain: ['crosswalk', 'road'],
range: ['road', 'railway'],
properties: {
hasTrafficLight: 'boolean',
hasSign: 'boolean',
},
});
this.defineRelationType('regulates', {
description: 'Reglerar',
directional: true,
domain: ['traffic_light', 'traffic_sign'],
range: ['crosswalk', 'road', 'intersection'],
properties: {
regulationType: 'string', // 'stop', 'yield', 'speed_limit'
},
});
this.defineRelationType('powers', {
description: 'Försörjer',
directional: true,
domain: ['electrical_cabinet', 'transformer'],
range: ['street_lamp', 'traffic_light', 'flood_light'],
properties: {
voltage: 'number',
capacity: 'number',
},
});
this.defineRelationType('may_obscure', {
description: 'Kan skymma',
directional: true,
domain: ['tree', 'bush'],
range: ['traffic_sign', 'street_lamp', 'traffic_light'],
properties: {
currentObscuration: 'number', // 0-1
growthRate: 'number', // meters/year
projectedObscuration: 'number', // 0-1 in 5 years
},
});
this.defineRelationType('mounted_on', {
description: 'Är monterad på',
directional: true,
domain: ['traffic_sign', 'street_lamp', 'traffic_light'],
range: ['pole', 'wall', 'building'],
properties: {
height: 'number',
mountingType: 'string',
},
});
this.defineRelationType('provides_access_to', {
description: 'Ger tillgång till',
directional: true,
domain: ['manhole', 'drain'],
range: ['sewer', 'cable_duct', 'water_pipe'],
properties: {
accessType: 'string',
depth: 'number',
},
});
this.defineRelationType('connects_to', {
description: 'Ansluter till',
directional: false,
domain: ['drain', 'pipe', 'cable'],
range: ['drain', 'pipe', 'cable', 'sewer', 'water_main'],
properties: {
connectionType: 'string',
diameter: 'number',
},
});
this.defineRelationType('connected_to', {
description: 'Ansluten till',
directional: false,
domain: ['hydrant', 'valve'],
range: ['water_main', 'pipe'],
properties: {
pressure: 'number',
flowRate: 'number',
},
});
this.defineRelationType('adjacent_to', {
description: 'Ligger bredvid',
directional: false,
domain: ['sidewalk', 'road', 'building', 'park'],
range: ['sidewalk', 'road', 'building', 'park'],
properties: {
distance: 'number',
},
});
this.defineRelationType('supports', {
description: 'Stödjer',
directional: true,
domain: ['pole', 'column'],
range: ['traffic_sign', 'street_lamp', 'traffic_light', 'cable'],
properties: {
loadCapacity: 'number',
},
});
this.defineRelationType('located_in', {
description: 'Finns i',
directional: true,
domain: ['street_lamp', 'traffic_sign', 'tree', 'bench'],
range: ['district', 'neighborhood', 'park', 'road'],
properties: {},
});
// Definiera objekttyper och deras relationer
this.defineObjectType('street_lamp', {
canHaveRelations: ['illuminates', 'mounted_on', 'powered_by', 'located_in'],
canBeTargetOf: ['may_obscure', 'adjacent_to'],
});
this.defineObjectType('traffic_sign', {
canHaveRelations: ['regulates', 'mounted_on', 'located_in'],
canBeTargetOf: ['may_obscure', 'adjacent_to'],
});
this.defineObjectType('tree', {
canHaveRelations: ['may_obscure', 'located_in', 'adjacent_to'],
canBeTargetOf: ['adjacent_to'],
});
this.defineObjectType('electrical_cabinet', {
canHaveRelations: ['powers', 'located_in', 'adjacent_to'],
canBeTargetOf: ['adjacent_to'],
});
this.defineObjectType('crosswalk', {
canHaveRelations: ['crosses', 'located_in'],
canBeTargetOf: ['illuminates', 'regulates', 'adjacent_to'],
});
this.defineObjectType('manhole', {
canHaveRelations: ['provides_access_to', 'located_in'],
canBeTargetOf: ['adjacent_to'],
});
}
defineRelationType(name, config) {
this.relationTypes.set(name, config);
}
defineObjectType(name, config) {
this.objectTypes.set(name, config);
}
/**
* ============================================================
* RELATIONSHANtering
* ============================================================
*/
addRelation(fromId, toId, relationType, properties = {}) {
const typeConfig = this.relationTypes.get(relationType);
if (!typeConfig) {
throw new Error(`Unknown relation type: ${relationType}`);
}
const relation = {
id: `${fromId}-${relationType}-${toId}`,
type: relationType,
from: fromId,
to: toId,
properties: {
...properties,
strength: properties.strength || 1.0,
discoveredAt: new Date().toISOString(),
verified: properties.verified || false,
},
};
this.relations.set(relation.id, relation);
return relation;
}
getRelations(objectId, options = {}) {
const relations = [];
for (const relation of this.relations.values()) {
if (relation.from === objectId || relation.to === objectId) {
// Filtrera efter typ
if (options.type && relation.type !== options.type) continue;
// Filtrera efter riktning
if (options.direction === 'outgoing' && relation.from !== objectId) continue;
if (options.direction === 'incoming' && relation.to !== objectId) continue;
relations.push(relation);
}
}
return relations;
}
getRelatedObjects(objectId, relationType) {
const relations = this.getRelations(objectId, { type: relationType });
return relations.map(r => ({
objectId: r.from === objectId ? r.to : r.from,
relation: r,
}));
}
/**
* ============================================================
* SEMANTISKA FRÅGOR (QUERIES)
* ============================================================
*/
/**
* Impact Analysis: Vilka objekt påverkas om ett objekt slutar fungera?
*/
impactAnalysis(objectId, failureType = 'total') {
const impacted = new Set();
const queue = [objectId];
const visited = new Set();
while (queue.length > 0) {
const current = queue.shift();
if (visited.has(current)) continue;
visited.add(current);
// Hitta alla objekt som är beroende av detta
const relations = this.getRelations(current, { direction: 'outgoing' });
for (const relation of relations) {
const targetId = relation.to;
// Beroende på relationstyp och failureType
let shouldImpact = false;
switch (relation.type) {
case 'powers':
shouldImpact = ['total', 'power'].includes(failureType);
break;
case 'illuminates':
shouldImpact = ['total', 'lighting'].includes(failureType);
break;
case 'regulates':
shouldImpact = ['total', 'traffic'].includes(failureType);
break;
case 'supports':
shouldImpact = ['total', 'structural'].includes(failureType);
break;
default:
shouldImpact = failureType === 'total';
}
if (shouldImpact) {
impacted.add(targetId);
queue.push(targetId);
}
}
}
return {
source: objectId,
failureType,
impactedObjects: Array.from(impacted),
impactCount: impacted.size,
};
}
/**
* Growth Risk: Vilka objekt riskerar att skymmas av växande vegetation?
*/
growthRiskAnalysis(vegetationId, timeHorizonYears = 5) {
const vegetation = this.getObject(vegetationId);
if (!vegetation) return null;
const risks = [];
// Hitta objekt som kan skymmas
const relations = this.getRelations(vegetationId, { type: 'may_obscure' });
for (const relation of relations) {
const targetId = relation.to;
const target = this.getObject(targetId);
if (!target) continue;
const currentObscuration = relation.properties.currentObscuration || 0;
const growthRate = relation.properties.growthRate || 0.1; // meters/year
const projectedObscuration = relation.properties.projectedObscuration ||
Math.min(1, currentObscuration + (growthRate * timeHorizonYears * 0.2));
risks.push({
targetId,
targetType: target.type,
currentObscuration,
projectedObscuration,
timeHorizonYears,
riskLevel: projectedObscuration > 0.7 ? 'high' :
projectedObscuration > 0.4 ? 'medium' : 'low',
recommendedAction: projectedObscuration > 0.7 ? 'Prune or remove vegetation' :
projectedObscuration > 0.4 ? 'Monitor growth' : 'No action needed',
});
}
return {
vegetationId,
timeHorizonYears,
risks: risks.sort((a, b) => b.projectedObscuration - a.projectedObscuration),
highRiskCount: risks.filter(r => r.riskLevel === 'high').length,
};
}
/**
* Path Analysis: Hitta väg mellan två objekt
*/
findPath(fromId, toId, maxDepth = 5) {
const queue = [[{ objectId: fromId, relation: null }]];
const visited = new Set();
while (queue.length > 0) {
const path = queue.shift();
const current = path[path.length - 1].objectId;
if (current === toId) {
return {
found: true,
path: path.slice(1), // Remove starting point
length: path.length - 1,
};
}
if (visited.has(current) || path.length >= maxDepth) continue;
visited.add(current);
// Explore neighbors
const relations = this.getRelations(current);
for (const relation of relations) {
const nextId = relation.from === current ? relation.to : relation.from;
if (!visited.has(nextId)) {
queue.push([...path, { objectId: nextId, relation }]);
}
}
}
return {
found: false,
path: [],
length: -1,
};
}
/**
* Network Analysis: Hitta alla objekt i ett nätverk
*/
networkAnalysis(seedObjectId, options = {}) {
const network = new Set();
const queue = [seedObjectId];
const visited = new Set();
const maxDepth = options.maxDepth || 3;
while (queue.length > 0) {
const current = queue.shift();
if (visited.has(current)) continue;
visited.add(current);
network.add(current);
if (visited.size >= maxDepth * 10) break; // Limit network size
const relations = this.getRelations(current);
for (const relation of relations) {
const nextId = relation.from === current ? relation.to : relation.from;
if (!visited.has(nextId)) {
queue.push(nextId);
}
}
}
return {
seedObjectId,
networkSize: network.size,
objects: Array.from(network),
};
}
/**
* ============================================================
* HJÄLPMETODER
* ============================================================
*/
getObject(objectId) {
// I verkligheten: hämta från UKG
// Mock för demo
return {
id: objectId,
type: 'unknown',
location: { lat: 0, lng: 0 },
};
}
getStats() {
return {
relationTypes: this.relationTypes.size,
objectTypes: this.objectTypes.size,
relations: this.relations.size,
};
}
exportOntology() {
return {
relationTypes: Array.from(this.relationTypes.entries()),
objectTypes: Array.from(this.objectTypes.entries()),
relations: Array.from(this.relations.entries()),
};
}
}
module.exports = UrbanOntology;
// Demo
if (require.main === module) {
const ontology = new UrbanOntology();
console.log('=== URBAN ONTOLOGY ===\n');
console.log(`Relation types: ${ontology.getStats().relationTypes}`);
console.log(`Object types: ${ontology.getStats().objectTypes}`);
// Skapa exempel-relationer
ontology.addRelation('lamp_001', 'road_001', 'illuminates', {
intensity: 50,
coverage: 10,
});
ontology.addRelation('tree_001', 'sign_001', 'may_obscure', {
currentObscuration: 0.3,
growthRate: 0.5,
projectedObscuration: 0.8,
});
ontology.addRelation('cabinet_001', 'lamp_001', 'powers', {
voltage: 230,
capacity: 1000,
});
ontology.addRelation('cabinet_001', 'lamp_002', 'powers', {
voltage: 230,
capacity: 1000,
});
console.log('\n=== IMPACT ANALYSIS ===');
const impact = ontology.impactAnalysis('cabinet_001', 'total');
console.log(`Source: ${impact.source}`);
console.log(`Impacted objects: ${impact.impactCount}`);
console.log(`Objects: ${impact.impactedObjects.join(', ')}`);
console.log('\n=== GROWTH RISK ANALYSIS ===');
const risk = ontology.growthRiskAnalysis('tree_001', 5);
console.log(`Vegetation: ${risk.vegetationId}`);
console.log(`Time horizon: ${risk.timeHorizonYears} years`);
console.log(`High risk count: ${risk.highRiskCount}`);
risk.risks.forEach(r => {
console.log(` ${r.targetId}: ${r.riskLevel} (current: ${r.currentObscuration}, projected: ${r.projectedObscuration})`);
});
console.log('\n=== PATH ANALYSIS ===');
const path = ontology.findPath('cabinet_001', 'road_001');
console.log(`Found: ${path.found}`);
console.log(`Length: ${path.length}`);
if (path.found) {
path.path.forEach((step, i) => {
console.log(` Step ${i + 1}: ${step.objectId} via ${step.relation?.type || 'direct'}`);
});
}
console.log('\n=== NETWORK ANALYSIS ===');
const network = ontology.networkAnalysis('cabinet_001', { maxDepth: 2 });
console.log(`Seed: ${network.seedObjectId}`);
console.log(`Network size: ${network.networkSize}`);
console.log(`Objects: ${network.objects.join(', ')}`);
}