/**
* ZPTDataFactory.js - RDF-Ext Dataset Management for ZPT Ontology
*
* This module provides factory methods and utilities for creating, managing,
* and manipulating RDF datasets using the ZPT (Zoom-Pan-Tilt) ontology.
*
* Key responsibilities:
* - Create RDF datasets with ZPT ontology terms
* - Generate navigation views and their states
* - Manage provenance and session tracking
* - Provide utility methods for common ZPT RDF patterns
*/
import rdf from 'rdf-ext';
import { ZPT, RAGNO, PROV, XSD, RDF, RDFS } from './ZPTNamespaces.js';
import logger from 'loglevel';
/**
* Factory for creating ZPT-related RDF data structures
*/
export class ZPTDataFactory {
constructor(options = {}) {
this.baseURI = options.baseURI || 'http://example.org/nav/';
this.navigationGraph = options.navigationGraph || 'http://purl.org/stuff/navigation';
this.datasetFactory = rdf;
// Counter for generating unique IDs
this.viewCounter = 0;
this.sessionCounter = 0;
this.stateCounter = 0;
}
/**
* Create a new RDF dataset for ZPT navigation data
* @returns {Dataset} Empty RDF dataset ready for ZPT data
*/
createDataset() {
return this.datasetFactory.dataset();
}
/**
* Generate a unique URI for navigation entities
* @param {string} type - Entity type (view, session, state)
* @param {string} suffix - Optional suffix
* @returns {NamedNode} RDF NamedNode with unique URI
*/
generateURI(type, suffix = '') {
let counter;
switch (type) {
case 'view':
counter = ++this.viewCounter;
break;
case 'session':
counter = ++this.sessionCounter;
break;
case 'state':
counter = ++this.stateCounter;
break;
default:
counter = Date.now();
}
const id = suffix ? `${type}_${counter}_${suffix}` : `${type}_${counter}`;
return this.datasetFactory.namedNode(`${this.baseURI}${id}`);
}
/**
* Create a navigation session with basic metadata
* @param {Object} options - Session options
* @param {string} options.agentURI - URI of the navigating agent
* @param {Date} options.startTime - Session start time
* @returns {Object} Session data with URI and quads
*/
createNavigationSession(options = {}) {
const sessionURI = this.generateURI('session');
const startTime = options.startTime || new Date();
const agentURI = options.agentURI ? this.datasetFactory.namedNode(options.agentURI) : null;
const purpose = options.purpose || 'Navigation session';
const quads = [];
const graphNode = this.datasetFactory.namedNode(this.navigationGraph);
// Basic session properties
quads.push(this.datasetFactory.quad(
sessionURI,
RDF.type,
ZPT.NavigationSession,
graphNode
));
// Store purpose as ZPT property
quads.push(this.datasetFactory.quad(
sessionURI,
ZPT.hasPurpose,
this.datasetFactory.literal(purpose),
graphNode
));
// Store timestamp as ZPT property
quads.push(this.datasetFactory.quad(
sessionURI,
ZPT.navigationTimestamp,
this.datasetFactory.literal(startTime.toISOString(), XSD.dateTime),
graphNode
));
// Also store PROV properties for compatibility
quads.push(this.datasetFactory.quad(
sessionURI,
PROV.startedAtTime,
this.datasetFactory.literal(startTime.toISOString(), XSD.dateTime),
graphNode
));
// Associate with agent if provided
if (agentURI) {
quads.push(this.datasetFactory.quad(
sessionURI,
PROV.wasAssociatedWith,
agentURI,
graphNode
));
}
return {
uri: sessionURI,
quads: quads,
startTime: startTime,
agentURI: agentURI,
purpose: purpose
};
}
/**
* Create a zoom state with specified level
* @param {string} zoomLevel - ZPT zoom level URI or string
* @returns {Object} Zoom state data with URI and quads
*/
createZoomState(zoomLevel) {
const stateURI = this.generateURI('state', 'zoom');
const zoomLevelNode = this.resolveZoomLevel(zoomLevel);
const quads = [];
const graphNode = this.datasetFactory.namedNode(this.navigationGraph);
quads.push(this.datasetFactory.quad(
stateURI,
RDF.type,
ZPT.ZoomState,
graphNode
));
quads.push(this.datasetFactory.quad(
stateURI,
ZPT.atZoomLevel,
zoomLevelNode,
graphNode
));
return {
uri: stateURI,
quads: quads,
zoomLevel: zoomLevelNode
};
}
/**
* Create a pan state with domain and filtering parameters
* @param {Object} panConfig - Pan configuration
* @param {string|Array} panConfig.domains - Domain URIs or strings
* @param {Object} panConfig.temporal - Temporal constraints
* @param {Array} panConfig.entities - Entity URIs
* @returns {Object} Pan state data with URI and quads
*/
createPanState(panConfig = {}) {
const stateURI = this.generateURI('state', 'pan');
const quads = [];
const graphNode = this.datasetFactory.namedNode(this.navigationGraph);
quads.push(this.datasetFactory.quad(
stateURI,
RDF.type,
ZPT.PanState,
graphNode
));
// Handle domain constraints
if (panConfig.domains) {
const domains = Array.isArray(panConfig.domains) ? panConfig.domains : [panConfig.domains];
domains.forEach(domain => {
const domainNode = this.resolvePanDomain(domain);
quads.push(this.datasetFactory.quad(
stateURI,
ZPT.withPanDomain,
domainNode,
graphNode
));
});
}
// Handle temporal constraints
if (panConfig.temporal) {
const temporalNode = this.createTemporalConstraint(panConfig.temporal);
if (temporalNode.quads.length > 0) {
quads.push(...temporalNode.quads);
quads.push(this.datasetFactory.quad(
stateURI,
ZPT.hasTemporalConstraint,
temporalNode.uri,
graphNode
));
}
}
// Handle entity constraints
if (panConfig.entities && Array.isArray(panConfig.entities)) {
panConfig.entities.forEach(entityURI => {
const entityNode = typeof entityURI === 'string' ?
this.datasetFactory.namedNode(entityURI) : entityURI;
quads.push(this.datasetFactory.quad(
stateURI,
ZPT.constrainedByEntity,
entityNode,
graphNode
));
});
}
return {
uri: stateURI,
quads: quads,
config: panConfig
};
}
/**
* Create a tilt state with projection method
* @param {string} tiltProjection - ZPT tilt projection URI or string
* @returns {Object} Tilt state data with URI and quads
*/
createTiltState(tiltProjection) {
const stateURI = this.generateURI('state', 'tilt');
const projectionNode = this.resolveTiltProjection(tiltProjection);
const quads = [];
const graphNode = this.datasetFactory.namedNode(this.navigationGraph);
quads.push(this.datasetFactory.quad(
stateURI,
RDF.type,
ZPT.TiltState,
graphNode
));
quads.push(this.datasetFactory.quad(
stateURI,
ZPT.withTiltProjection,
projectionNode,
graphNode
));
return {
uri: stateURI,
quads: quads,
projection: projectionNode
};
}
/**
* Create a complete navigation view with all states
* @param {Object} config - Navigation view configuration
* @param {string} config.query - Natural language query
* @param {string} config.zoom - Zoom level
* @param {Object} config.pan - Pan configuration
* @param {string} config.tilt - Tilt projection
* @param {string} config.sessionURI - Parent session URI
* @param {Array} config.selectedCorpuscles - Selected corpuscle URIs
* @returns {Object} Complete navigation view with all quads
*/
createNavigationView(config) {
const viewURI = this.generateURI('view');
const quads = [];
const graphNode = this.datasetFactory.namedNode(this.navigationGraph);
// Basic navigation view properties
quads.push(this.datasetFactory.quad(
viewURI,
RDF.type,
ZPT.NavigationView,
graphNode
));
quads.push(this.datasetFactory.quad(
viewURI,
ZPT.answersQuery,
this.datasetFactory.literal(config.query),
graphNode
));
quads.push(this.datasetFactory.quad(
viewURI,
ZPT.navigationTimestamp,
this.datasetFactory.literal(new Date().toISOString(), XSD.dateTime),
graphNode
));
// Link to session if provided
if (config.sessionURI) {
const sessionNode = typeof config.sessionURI === 'string' ?
this.datasetFactory.namedNode(config.sessionURI) : config.sessionURI;
quads.push(this.datasetFactory.quad(
viewURI,
ZPT.partOfSession,
sessionNode,
graphNode
));
}
// Create and link states
const zoomState = this.createZoomState(config.zoom);
quads.push(...zoomState.quads);
quads.push(this.datasetFactory.quad(
viewURI,
ZPT.hasZoomState,
zoomState.uri,
graphNode
));
const panState = this.createPanState(config.pan || {});
quads.push(...panState.quads);
quads.push(this.datasetFactory.quad(
viewURI,
ZPT.hasPanState,
panState.uri,
graphNode
));
const tiltState = this.createTiltState(config.tilt);
quads.push(...tiltState.quads);
quads.push(this.datasetFactory.quad(
viewURI,
ZPT.hasTiltState,
tiltState.uri,
graphNode
));
// Link to selected corpuscles
if (config.selectedCorpuscles && Array.isArray(config.selectedCorpuscles)) {
config.selectedCorpuscles.forEach(corpuscleURI => {
const corpuscleNode = typeof corpuscleURI === 'string' ?
this.datasetFactory.namedNode(corpuscleURI) : corpuscleURI;
quads.push(this.datasetFactory.quad(
viewURI,
ZPT.selectedCorpuscle,
corpuscleNode,
graphNode
));
});
}
return {
uri: viewURI,
quads: quads,
states: {
zoom: zoomState,
pan: panState,
tilt: tiltState
},
config: config
};
}
/**
* Add optimization metadata to a corpuscle
* @param {string|NamedNode} corpuscleURI - Corpuscle URI
* @param {Object} scores - Optimization scores
* @param {number} scores.optimizationScore - Overall score
* @param {number} scores.zoomRelevance - Zoom relevance score
* @param {number} scores.panCoverage - Pan coverage score
* @param {number} scores.tiltEffectiveness - Tilt effectiveness score
* @returns {Array} Array of quads with optimization metadata
*/
addOptimizationMetadata(corpuscleURI, scores) {
const corpuscleNode = typeof corpuscleURI === 'string' ?
this.datasetFactory.namedNode(corpuscleURI) : corpuscleURI;
const quads = [];
const graphNode = this.datasetFactory.namedNode(this.navigationGraph);
if (typeof scores.optimizationScore === 'number') {
quads.push(this.datasetFactory.quad(
corpuscleNode,
ZPT.optimizationScore,
this.datasetFactory.literal(scores.optimizationScore.toString(), XSD.float),
graphNode
));
}
if (typeof scores.zoomRelevance === 'number') {
quads.push(this.datasetFactory.quad(
corpuscleNode,
ZPT.zoomRelevance,
this.datasetFactory.literal(scores.zoomRelevance.toString(), XSD.float),
graphNode
));
}
if (typeof scores.panCoverage === 'number') {
quads.push(this.datasetFactory.quad(
corpuscleNode,
ZPT.panCoverage,
this.datasetFactory.literal(scores.panCoverage.toString(), XSD.float),
graphNode
));
}
if (typeof scores.tiltEffectiveness === 'number') {
quads.push(this.datasetFactory.quad(
corpuscleNode,
ZPT.tiltEffectiveness,
this.datasetFactory.literal(scores.tiltEffectiveness.toString(), XSD.float),
graphNode
));
}
return quads;
}
/**
* Resolve zoom level string to ZPT ontology URI
* @param {string} zoomLevel - Zoom level string or URI
* @returns {NamedNode} ZPT zoom level URI
*/
resolveZoomLevel(zoomLevel) {
if (typeof zoomLevel !== 'string') {
return zoomLevel; // Assume it's already a NamedNode
}
const zoomMap = {
'entity': ZPT.EntityLevel,
'unit': ZPT.UnitLevel,
'text': ZPT.TextLevel,
'micro': ZPT.TextLevel, // Alias for text level
'community': ZPT.CommunityLevel,
'corpus': ZPT.CorpusLevel
};
return zoomMap[zoomLevel] || this.datasetFactory.namedNode(zoomLevel);
}
/**
* Resolve tilt projection string to ZPT ontology URI
* @param {string} tiltProjection - Tilt projection string or URI
* @returns {NamedNode} ZPT tilt projection URI
*/
resolveTiltProjection(tiltProjection) {
if (typeof tiltProjection !== 'string') {
return tiltProjection; // Assume it's already a NamedNode
}
const tiltMap = {
'keywords': ZPT.KeywordProjection,
'embedding': ZPT.EmbeddingProjection,
'graph': ZPT.GraphProjection,
'temporal': ZPT.TemporalProjection
};
return tiltMap[tiltProjection] || this.datasetFactory.namedNode(tiltProjection);
}
/**
* Resolve pan domain string to ZPT ontology URI
* @param {string} domain - Domain string or URI
* @returns {NamedNode} ZPT pan domain URI
*/
resolvePanDomain(domain) {
if (typeof domain !== 'string') {
return domain; // Assume it's already a NamedNode
}
const domainMap = {
'topic': ZPT.TopicDomain,
'entity': ZPT.EntityDomain,
'temporal': ZPT.TemporalDomain,
'geospatial': ZPT.GeospatialDomain
};
return domainMap[domain] || this.datasetFactory.namedNode(`${this.baseURI}domain/${domain}`);
}
/**
* Create temporal constraint node with date range
* @param {Object} temporal - Temporal configuration
* @param {string} temporal.start - Start date
* @param {string} temporal.end - End date
* @returns {Object} Temporal constraint with URI and quads
*/
createTemporalConstraint(temporal) {
const constraintURI = this.generateURI('constraint', 'temporal');
const quads = [];
const graphNode = this.datasetFactory.namedNode(this.navigationGraph);
if (temporal.start) {
quads.push(this.datasetFactory.quad(
constraintURI,
ZPT.temporalStart,
this.datasetFactory.literal(temporal.start, XSD.dateTime),
graphNode
));
}
if (temporal.end) {
quads.push(this.datasetFactory.quad(
constraintURI,
ZPT.temporalEnd,
this.datasetFactory.literal(temporal.end, XSD.dateTime),
graphNode
));
}
return {
uri: constraintURI,
quads: quads
};
}
/**
* Convert RDF dataset to N-Triples string for debugging
* @param {Dataset} dataset - RDF dataset
* @returns {string} N-Triples representation
*/
datasetToNTriples(dataset) {
const serializer = new (rdf.formats.serializers.get('application/n-triples'))();
const stream = serializer.import(dataset.toStream());
return new Promise((resolve, reject) => {
let result = '';
stream.on('data', chunk => {
result += chunk;
});
stream.on('end', () => {
resolve(result);
});
stream.on('error', reject);
});
}
/**
* Utility method to create a complete dataset with navigation view
* @param {Object} config - Navigation configuration
* @returns {Dataset} Complete RDF dataset
*/
createNavigationDataset(config) {
const dataset = this.createDataset();
const view = this.createNavigationView(config);
// Add all quads to dataset
view.quads.forEach(quad => {
dataset.add(quad);
});
return dataset;
}
}
/**
* Default factory instance for convenience
*/
export const zptDataFactory = new ZPTDataFactory();
/**
* Utility functions for common operations
*/
export const ZPTUtils = {
/**
* Create a simple navigation view with minimal configuration
* @param {string} query - Natural language query
* @param {string} zoom - Zoom level
* @param {string} tilt - Tilt projection
* @returns {Object} Navigation view data
*/
createSimpleView(query, zoom = 'entity', tilt = 'keywords') {
return zptDataFactory.createNavigationView({
query,
zoom,
tilt,
pan: {}
});
},
/**
* Extract navigation parameters from RDF dataset
* @param {Dataset} dataset - RDF dataset containing navigation view
* @param {NamedNode} viewURI - Navigation view URI
* @returns {Object} Extracted navigation parameters
*/
extractNavigationParams(dataset, viewURI) {
const params = {
query: null,
zoom: null,
pan: {},
tilt: null,
timestamp: null
};
// Extract query
for (const quad of dataset.match(viewURI, ZPT.answersQuery)) {
params.query = quad.object.value;
break;
}
// Extract timestamp
for (const quad of dataset.match(viewURI, ZPT.navigationTimestamp)) {
params.timestamp = new Date(quad.object.value);
break;
}
// Extract zoom level
for (const quad of dataset.match(viewURI, ZPT.hasZoomState)) {
const zoomState = quad.object;
for (const zoomQuad of dataset.match(zoomState, ZPT.atZoomLevel)) {
params.zoom = zoomQuad.object.value;
break;
}
break;
}
// Extract tilt projection
for (const quad of dataset.match(viewURI, ZPT.hasTiltState)) {
const tiltState = quad.object;
for (const tiltQuad of dataset.match(tiltState, ZPT.withTiltProjection)) {
params.tilt = tiltQuad.object.value;
break;
}
break;
}
return params;
}
};
export default ZPTDataFactory;