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
538 lines
15 KiB
JavaScript
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(', ')}`);
|
|
}
|