/**
* Maps zoom levels to specific Ragno element types and selection strategies
*/
export default class ZoomLevelMapper {
constructor(options = {}) {
this.ragnoNamespace = options.ragnoNamespace || 'http://purl.org/stuff/ragno/';
this.initializeZoomMappings();
this.initializeSelectionStrategies();
}
/**
* Initialize zoom level to RDF type mappings
*/
initializeZoomMappings() {
this.zoomMappings = {
'corpus': {
primaryTypes: ['ragno:Corpus'],
secondaryTypes: [],
granularity: 1,
scope: 'global',
description: 'Entire corpus overview and metadata',
typicalResultCount: 1,
aggregationLevel: 'corpus'
},
'community': {
primaryTypes: ['ragno:Community'],
secondaryTypes: ['ragno:CommunityCluster', 'ragno:Topic'],
granularity: 2,
scope: 'thematic',
description: 'Thematic communities and topic clusters',
typicalResultCount: 50,
aggregationLevel: 'community'
},
'unit': {
primaryTypes: ['ragno:SemanticUnit', 'ragno:Unit'],
secondaryTypes: ['ragno:Chunk', 'ragno:Segment'],
granularity: 3,
scope: 'semantic',
description: 'Semantic units and meaningful segments',
typicalResultCount: 200,
aggregationLevel: 'unit'
},
'entity': {
primaryTypes: ['ragno:Entity'],
secondaryTypes: ['ragno:Concept', 'ragno:NamedEntity'],
granularity: 4,
scope: 'conceptual',
description: 'Individual entities and concepts',
typicalResultCount: 500,
aggregationLevel: 'entity'
},
'text': {
primaryTypes: ['ragno:TextElement', 'ragno:Text'],
secondaryTypes: ['ragno:Sentence', 'ragno:Paragraph', 'ragno:Document'],
granularity: 5,
scope: 'textual',
description: 'Raw text elements and documents',
typicalResultCount: 1000,
aggregationLevel: 'text'
}
};
}
/**
* Initialize selection strategies for each zoom level
*/
initializeSelectionStrategies() {
this.selectionStrategies = {
'corpus': {
primaryStrategy: 'metadata_aggregation',
scoringFactors: ['completeness', 'recency', 'size'],
weights: { completeness: 0.5, recency: 0.3, size: 0.2 },
requiresAggregation: true,
maxResults: 5,
sortBy: 'created_desc'
},
'community': {
primaryStrategy: 'community_detection',
scoringFactors: ['cohesion', 'diversity', 'size', 'relevance'],
weights: { cohesion: 0.3, diversity: 0.25, size: 0.2, relevance: 0.25 },
requiresAggregation: true,
maxResults: 50,
sortBy: 'cohesion_desc'
},
'unit': {
primaryStrategy: 'semantic_similarity',
scoringFactors: ['relevance', 'completeness', 'connectivity'],
weights: { relevance: 0.5, completeness: 0.3, connectivity: 0.2 },
requiresAggregation: false,
maxResults: 200,
sortBy: 'relevance_desc'
},
'entity': {
primaryStrategy: 'entity_ranking',
scoringFactors: ['relevance', 'importance', 'connectivity', 'frequency'],
weights: { relevance: 0.4, importance: 0.3, connectivity: 0.2, frequency: 0.1 },
requiresAggregation: false,
maxResults: 500,
sortBy: 'importance_desc'
},
'text': {
primaryStrategy: 'text_retrieval',
scoringFactors: ['relevance', 'recency', 'quality'],
weights: { relevance: 0.6, recency: 0.25, quality: 0.15 },
requiresAggregation: false,
maxResults: 1000,
sortBy: 'relevance_desc'
}
};
}
/**
* Get mapping configuration for a zoom level
* @param {string} zoomLevel - The zoom level (corpus, community, unit, entity, text)
* @returns {Object} Zoom level configuration
*/
getZoomMapping(zoomLevel) {
const mapping = this.zoomMappings[zoomLevel];
if (!mapping) {
throw new Error(`Unsupported zoom level: ${zoomLevel}`);
}
return { ...mapping };
}
/**
* Get selection strategy for a zoom level
* @param {string} zoomLevel - The zoom level
* @returns {Object} Selection strategy configuration
*/
getSelectionStrategy(zoomLevel) {
const strategy = this.selectionStrategies[zoomLevel];
if (!strategy) {
throw new Error(`No selection strategy defined for zoom level: ${zoomLevel}`);
}
return { ...strategy };
}
/**
* Get all RDF types for a zoom level (primary + secondary)
* @param {string} zoomLevel - The zoom level
* @returns {Array<string>} Array of RDF type URIs
*/
getAllTypes(zoomLevel) {
const mapping = this.getZoomMapping(zoomLevel);
return [...mapping.primaryTypes, ...mapping.secondaryTypes];
}
/**
* Get primary RDF types for a zoom level
* @param {string} zoomLevel - The zoom level
* @returns {Array<string>} Array of primary RDF type URIs
*/
getPrimaryTypes(zoomLevel) {
const mapping = this.getZoomMapping(zoomLevel);
return [...mapping.primaryTypes];
}
/**
* Build type filter clause for SPARQL queries
* @param {string} zoomLevel - The zoom level
* @param {boolean} includePrimary - Include primary types (default: true)
* @param {boolean} includeSecondary - Include secondary types (default: false)
* @returns {string} SPARQL type filter clause
*/
buildTypeFilter(zoomLevel, includePrimary = true, includeSecondary = false) {
const mapping = this.getZoomMapping(zoomLevel);
const types = [];
if (includePrimary) {
types.push(...mapping.primaryTypes);
}
if (includeSecondary) {
types.push(...mapping.secondaryTypes);
}
if (types.length === 0) {
return '';
}
if (types.length === 1) {
return `?uri a ${types[0]} .`;
}
const typeUnion = types.map(type => `{ ?uri a ${type} }`).join(' UNION ');
return `{ ${typeUnion} }`;
}
/**
* Get optimal result limit for a zoom level
* @param {string} zoomLevel - The zoom level
* @param {number} tokenBudget - Available token budget
* @returns {number} Recommended result limit
*/
getOptimalResultLimit(zoomLevel, tokenBudget = 4000) {
const mapping = this.getZoomMapping(zoomLevel);
const strategy = this.getSelectionStrategy(zoomLevel);
// Estimate tokens per result based on zoom level
const tokensPerResult = this.estimateTokensPerResult(zoomLevel);
const budgetBasedLimit = Math.floor(tokenBudget * 0.8 / tokensPerResult);
// Use the smaller of strategy max or budget-based limit
return Math.min(strategy.maxResults, budgetBasedLimit, mapping.typicalResultCount);
}
/**
* Estimate tokens per result for a zoom level
* @param {string} zoomLevel - The zoom level
* @returns {number} Estimated tokens per result
*/
estimateTokensPerResult(zoomLevel) {
const tokenEstimates = {
'corpus': 200, // High-level metadata
'community': 100, // Community summaries
'unit': 80, // Semantic unit content
'entity': 30, // Entity information
'text': 150 // Text content
};
return tokenEstimates[zoomLevel] || 50;
}
/**
* Get aggregation requirements for a zoom level
* @param {string} zoomLevel - The zoom level
* @returns {Object} Aggregation configuration
*/
getAggregationConfig(zoomLevel) {
const strategy = this.getSelectionStrategy(zoomLevel);
const mapping = this.getZoomMapping(zoomLevel);
return {
required: strategy.requiresAggregation,
level: mapping.aggregationLevel,
strategy: strategy.primaryStrategy,
groupBy: this.getAggregationGroupBy(zoomLevel),
metrics: this.getAggregationMetrics(zoomLevel)
};
}
/**
* Get GROUP BY clause for aggregation
* @param {string} zoomLevel - The zoom level
* @returns {Array<string>} Fields to group by
*/
getAggregationGroupBy(zoomLevel) {
const groupByMap = {
'corpus': ['?corpus'],
'community': ['?community', '?topic'],
'unit': [], // No aggregation
'entity': [], // No aggregation
'text': [] // No aggregation
};
return groupByMap[zoomLevel] || [];
}
/**
* Get aggregation metrics for a zoom level
* @param {string} zoomLevel - The zoom level
* @returns {Array<Object>} Aggregation metrics configuration
*/
getAggregationMetrics(zoomLevel) {
const metricsMap = {
'corpus': [
{ name: 'totalEntities', function: 'COUNT(?entity)', alias: 'entityCount' },
{ name: 'totalUnits', function: 'COUNT(?unit)', alias: 'unitCount' },
{ name: 'avgQuality', function: 'AVG(?quality)', alias: 'avgQuality' },
{ name: 'lastModified', function: 'MAX(?modified)', alias: 'lastModified' }
],
'community': [
{ name: 'memberCount', function: 'COUNT(?member)', alias: 'memberCount' },
{ name: 'avgCohesion', function: 'AVG(?cohesion)', alias: 'avgCohesion' },
{ name: 'topicDiversity', function: 'COUNT(DISTINCT ?topic)', alias: 'topicCount' },
{ name: 'totalConnections', function: 'SUM(?connections)', alias: 'connectionCount' }
],
'unit': [],
'entity': [],
'text': []
};
return metricsMap[zoomLevel] || [];
}
/**
* Determine if zoom level supports a specific tilt representation
* @param {string} zoomLevel - The zoom level
* @param {string} tiltRepresentation - The tilt representation
* @returns {boolean} Whether the combination is supported
*/
supportsTilt(zoomLevel, tiltRepresentation) {
const supportMatrix = {
'corpus': ['keywords', 'temporal', 'graph'],
'community': ['keywords', 'graph', 'embedding'],
'unit': ['keywords', 'embedding', 'temporal', 'graph'],
'entity': ['keywords', 'embedding', 'graph'],
'text': ['keywords', 'embedding', 'temporal']
};
const supportedTilts = supportMatrix[zoomLevel] || [];
return supportedTilts.includes(tiltRepresentation);
}
/**
* Get recommended tilt representation for a zoom level
* @param {string} zoomLevel - The zoom level
* @returns {string} Recommended tilt representation
*/
getRecommendedTilt(zoomLevel) {
const recommendations = {
'corpus': 'keywords',
'community': 'graph',
'unit': 'embedding',
'entity': 'embedding',
'text': 'keywords'
};
return recommendations[zoomLevel] || 'keywords';
}
/**
* Build zoom-specific SPARQL SELECT clause
* @param {string} zoomLevel - The zoom level
* @param {string} tiltRepresentation - The tilt representation
* @returns {string} SPARQL SELECT clause
*/
buildSelectClause(zoomLevel, tiltRepresentation) {
const baseFields = this.getBaseFields(zoomLevel);
const tiltFields = this.getTiltFields(tiltRepresentation);
const aggregationFields = this.getAggregationFields(zoomLevel);
const allFields = [...baseFields, ...tiltFields, ...aggregationFields];
return `SELECT DISTINCT ${allFields.join(' ')}`;
}
/**
* Get base fields for a zoom level
* @param {string} zoomLevel - The zoom level
* @returns {Array<string>} Base SPARQL fields
*/
getBaseFields(zoomLevel) {
const fieldMap = {
'corpus': ['?uri', '?label', '?description', '?created', '?modified'],
'community': ['?uri', '?label', '?description', '?memberCount', '?cohesion'],
'unit': ['?uri', '?content', '?entity', '?unit', '?created'],
'entity': ['?uri', '?label', '?type', '?prefLabel', '?importance'],
'text': ['?uri', '?text', '?source', '?position', '?created']
};
return fieldMap[zoomLevel] || ['?uri', '?label'];
}
/**
* Get tilt-specific fields
* @param {string} tiltRepresentation - The tilt representation
* @returns {Array<string>} Tilt-specific SPARQL fields
*/
getTiltFields(tiltRepresentation) {
const fieldMap = {
'embedding': ['?embedding', '?similarity'],
'keywords': ['?keywords', '?keywordScore'],
'graph': ['?connections', '?centrality'],
'temporal': ['?created', '?modified', '?sequence']
};
return fieldMap[tiltRepresentation] || [];
}
/**
* Get aggregation fields for a zoom level
* @param {string} zoomLevel - The zoom level
* @returns {Array<string>} Aggregation SPARQL fields
*/
getAggregationFields(zoomLevel) {
const metrics = this.getAggregationMetrics(zoomLevel);
return metrics.map(metric => `(${metric.function} AS ?${metric.alias})`);
}
/**
* Validate zoom level compatibility with parameters
* @param {string} zoomLevel - The zoom level
* @param {Object} panFilters - Pan filter parameters
* @param {string} tiltRepresentation - Tilt representation
* @returns {Object} Validation result
*/
validateCompatibility(zoomLevel, panFilters, tiltRepresentation) {
const issues = [];
const warnings = [];
// Check tilt compatibility
if (!this.supportsTilt(zoomLevel, tiltRepresentation)) {
issues.push(`Tilt '${tiltRepresentation}' not optimal for zoom level '${zoomLevel}'`);
warnings.push(`Consider using '${this.getRecommendedTilt(zoomLevel)}' instead`);
}
// Check filter compatibility
if (zoomLevel === 'corpus' && panFilters.entity) {
warnings.push('Entity filters may not be effective at corpus zoom level');
}
if (zoomLevel === 'text' && panFilters.geographic) {
warnings.push('Geographic filters may have limited effect on text elements');
}
// Check result count expectations
const mapping = this.getZoomMapping(zoomLevel);
if (panFilters.topic && mapping.typicalResultCount > 500) {
warnings.push('Topic filters may return many results at this zoom level');
}
return {
valid: issues.length === 0,
issues,
warnings,
recommendations: this.getRecommendations(zoomLevel, panFilters, tiltRepresentation)
};
}
/**
* Get optimization recommendations
* @param {string} zoomLevel - The zoom level
* @param {Object} panFilters - Pan filter parameters
* @param {string} tiltRepresentation - Tilt representation
* @returns {Array<string>} Optimization recommendations
*/
getRecommendations(zoomLevel, panFilters, tiltRepresentation) {
const recommendations = [];
if (zoomLevel === 'entity' && tiltRepresentation === 'temporal') {
recommendations.push('Consider using embedding or graph tilt for better entity analysis');
}
if (zoomLevel === 'corpus' && Object.keys(panFilters).length > 2) {
recommendations.push('Simplify filters for corpus-level analysis');
}
if (zoomLevel === 'text' && !panFilters.topic) {
recommendations.push('Add topic filter to improve text relevance');
}
return recommendations;
}
/**
* Get zoom level metadata for API documentation
* @returns {Object} Complete zoom level documentation
*/
getZoomLevelDocumentation() {
const documentation = {};
for (const [level, mapping] of Object.entries(this.zoomMappings)) {
const strategy = this.selectionStrategies[level];
documentation[level] = {
description: mapping.description,
granularity: mapping.granularity,
scope: mapping.scope,
types: {
primary: mapping.primaryTypes,
secondary: mapping.secondaryTypes
},
strategy: {
name: strategy.primaryStrategy,
factors: strategy.scoringFactors,
maxResults: strategy.maxResults
},
supportedTilts: Object.keys(this.selectionStrategies).filter(tilt =>
this.supportsTilt(level, tilt)
),
recommendedTilt: this.getRecommendedTilt(level),
typicalResultCount: mapping.typicalResultCount,
estimatedTokensPerResult: this.estimateTokensPerResult(level)
};
}
return documentation;
}
}