glam/frontend/src/lib/schema-custodian-mapping.ts
2025-12-26 14:30:31 +01:00

356 lines
13 KiB
TypeScript

/**
* schema-custodian-mapping.ts - Maps LinkML schema elements to CustodianTypes
*
* This module provides mappings between schema elements (classes, slots, enums)
* and the GLAMORCUBESFIXPHDNT custodian types they primarily relate to.
*
* Used by the CustodianTypeIndicator component to show which type(s) a schema
* element is most relevant to.
*
* PRIORITY ORDER:
* 1. Read `custodian_types` annotation from LinkML schema (via linkmlSchemaService)
* 2. Fall back to static mapping (CLASS_TO_CUSTODIAN_TYPE, etc.)
* 3. Default to all 19 types (universal)
*/
import type { CustodianTypeCode } from './custodian-types';
import { linkmlSchemaService } from './linkml/linkml-schema-service';
/**
* Mapping of schema class names to relevant custodian types
*
* Key: Class name (as it appears in LinkML schema)
* Value: Array of CustodianTypeCode(s) the class relates to
*
* Most classes relate to ALL types (universal), but some are type-specific.
*/
export const CLASS_TO_CUSTODIAN_TYPE: Record<string, CustodianTypeCode[]> = {
// Universal classes (apply to all custodian types)
'CustodianObservation': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'],
'CustodianName': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'],
'CustodianReconstruction': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'],
'Location': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'],
'GHCID': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'],
'Provenance': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'],
// Place-related classes
'FeaturePlace': ['F'], // Features (monuments, statues)
'FeaturePlaceClass': ['F'],
// Collection-related classes
'Collection': ['G', 'L', 'A', 'M', 'B', 'H'], // Galleries, Libraries, Archives, Museums, Botanical, Holy sites
'CollectionItem': ['G', 'L', 'A', 'M', 'B', 'H'],
// Digital platform classes
'DigitalPlatform': ['D'], // Digital platforms
'DigitalPlatformClass': ['D'],
'WebObservation': ['D'],
'WebClaim': ['D'],
// Archive-specific
'ArchivalFonds': ['A'], // Archives
'ArchivalSeries': ['A'],
'ArchivalRecord': ['A'],
// Library-specific
'BibliographicRecord': ['L'], // Libraries
'Catalog': ['L'],
// Museum-specific
'Exhibition': ['M', 'G'], // Museums, Galleries
'MuseumObject': ['M'],
// Research-related
'ResearchProject': ['R'], // Research centers
'Publication': ['R', 'L'], // Research centers, Libraries
// Education-related
'Course': ['E'], // Education providers
'LearningResource': ['E', 'D'], // Education, Digital platforms
// Religious heritage
'ReligiousCollection': ['H'], // Holy sites
'LiturgicalObject': ['H'],
// Botanical/Zoo
'LivingCollection': ['B'], // Botanical gardens/zoos
'Specimen': ['B'],
// Intangible heritage
'IntangibleHeritage': ['I'], // Intangible heritage groups
'Performance': ['I'],
'Tradition': ['I'],
// Organizational
'StaffRole': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'E', 'S', 'H', 'N'],
'OrganizationalChange': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'I', 'X', 'P', 'H', 'D', 'N', 'T'],
// Personal collections
'PersonalCollection': ['P'], // Personal collections
'PrivateArchive': ['P'],
// Corporate
'CorporateCollection': ['C'], // Corporations
'CorporateArchive': ['C'],
// Society-related
'SocietyMembership': ['S'], // Collecting societies
'HeemkundigeKring': ['S'],
// Taste/Smell heritage
'CulinaryHeritage': ['T'], // Taste/smell heritage
'Recipe': ['T'],
'Formulation': ['T'],
// NGO-specific
'AdvocacyOrganization': ['N'], // NGOs
'HeritageInitiative': ['N'],
};
/**
* Mapping of schema slot names to relevant custodian types
*/
export const SLOT_TO_CUSTODIAN_TYPE: Record<string, CustodianTypeCode[]> = {
// Universal slots
'custodian_name': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'],
'location': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'],
'ghcid': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'],
'provenance': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'],
// Archive-specific slots
'fonds': ['A'],
'finding_aid': ['A'],
'archival_hierarchy': ['A'],
// Library-specific slots
'call_number': ['L'],
'bibliographic_record': ['L'],
'catalog_entry': ['L'],
// Museum-specific slots
'accession_number': ['M'],
'exhibition_history': ['M', 'G'],
'conservation_status': ['M', 'G'],
// Digital platform slots
'platform_url': ['D'],
'api_endpoint': ['D'],
'metadata_format': ['D', 'L', 'A', 'M'],
// Religious heritage slots
'denomination': ['H'],
'consecration_date': ['H'],
'liturgical_calendar': ['H'],
// Botanical/Zoo slots
'species': ['B'],
'habitat': ['B'],
'conservation_program': ['B'],
// Intangible heritage slots
'tradition_type': ['I'],
'transmission_method': ['I'],
'practitioners': ['I'],
// Taste/Smell slots
'recipe_origin': ['T'],
'ingredients': ['T'],
'preparation_method': ['T'],
};
/**
* Mapping of enum names to relevant custodian types
*/
export const ENUM_TO_CUSTODIAN_TYPE: Record<string, CustodianTypeCode[]> = {
// Universal enums
'CustodianTypeEnum': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'],
'DataTierEnum': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'],
'DataSourceEnum': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'],
'CountryCodeEnum': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'],
'LanguageCodeEnum': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'],
// Type-specific enums
'ArchivalLevelEnum': ['A'],
'BibliographicFormatEnum': ['L'],
'ExhibitionTypeEnum': ['M', 'G'],
'DigitalPlatformTypeEnum': ['D'],
'ReligiousDenominationEnum': ['H'],
'SpeciesClassificationEnum': ['B'],
'IntangibleHeritageTypeEnum': ['I'],
'CulinaryHeritageTypeEnum': ['T'],
'StaffRoleTypeEnum': ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'E', 'S', 'H', 'N'],
};
/**
* Default types for elements not explicitly mapped (universal)
*/
export const DEFAULT_CUSTODIAN_TYPES: CustodianTypeCode[] = [
'G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'
];
/**
* Get custodian types for a schema class
*/
export function getCustodianTypesForClass(className: string): CustodianTypeCode[] {
return CLASS_TO_CUSTODIAN_TYPE[className] || DEFAULT_CUSTODIAN_TYPES;
}
/**
* Get custodian types for a schema slot
*/
export function getCustodianTypesForSlot(slotName: string): CustodianTypeCode[] {
return SLOT_TO_CUSTODIAN_TYPE[slotName] || DEFAULT_CUSTODIAN_TYPES;
}
/**
* Get custodian types for a schema enum
*/
export function getCustodianTypesForEnum(enumName: string): CustodianTypeCode[] {
return ENUM_TO_CUSTODIAN_TYPE[enumName] || DEFAULT_CUSTODIAN_TYPES;
}
/**
* Check if a schema element is universal (applies to all types)
*/
export function isUniversalElement(types: CustodianTypeCode[]): boolean {
return types.length >= 19; // All 19 types
}
/**
* Get the primary custodian type for a schema element
* Returns the first type, or 'U' (Unknown) if empty
*/
export function getPrimaryCustodianType(types: CustodianTypeCode[]): CustodianTypeCode {
if (types.length === 0) return 'U';
// For universal elements, return the most common types first
if (isUniversalElement(types)) {
return 'M'; // Museum as default primary for universal
}
return types[0];
}
/**
* Get a compact representation of custodian types
* For universal: returns "ALL"
* For few types: returns joined string (e.g., "MAL")
* For many types: returns abbreviated (e.g., "MA+5")
*/
export function getCompactTypeRepresentation(types: CustodianTypeCode[]): string {
if (types.length === 0) return '?';
if (isUniversalElement(types)) return 'ALL';
if (types.length <= 4) return types.join('');
return types.slice(0, 2).join('') + `+${types.length - 2}`;
}
// ============================================================================
// ASYNC FUNCTIONS - Read from LinkML schema annotations with static fallback
// ============================================================================
/**
* Validate that a string array contains valid CustodianTypeCodes
*/
function validateCustodianTypes(types: string[]): CustodianTypeCode[] {
const validCodes: CustodianTypeCode[] = ['G', 'L', 'A', 'M', 'O', 'R', 'C', 'U', 'B', 'E', 'S', 'F', 'I', 'X', 'P', 'H', 'D', 'N', 'T'];
return types.filter((t): t is CustodianTypeCode => validCodes.includes(t as CustodianTypeCode));
}
/**
* Get custodian types for a schema class (async version)
*
* Priority:
* 1. Read from LinkML schema annotations (custodian_types)
* 2. Fall back to static mapping
* 3. Default to EMPTY ARRAY (no types assigned) - cube will show no letters
*
* NOTE: We return [] instead of DEFAULT_CUSTODIAN_TYPES when no annotation exists
* because classes without explicit custodian_types annotations should NOT display
* all 19 letters on the cube. Only classes with explicit annotations should show letters.
*/
export async function getCustodianTypesForClassAsync(className: string): Promise<CustodianTypeCode[]> {
try {
// Try to get from schema annotations first
const annotationTypes = await linkmlSchemaService.getClassCustodianTypes(className);
if (annotationTypes && annotationTypes.length > 0) {
const validated = validateCustodianTypes(annotationTypes);
if (validated.length > 0) {
return validated;
}
}
} catch (error) {
console.warn(`[CustodianMapping] Error reading annotations for class ${className}:`, error);
}
// Fall back to static mapping, or empty array if no mapping exists
// Empty array means "no custodian types assigned" - cube will show no letters
return CLASS_TO_CUSTODIAN_TYPE[className] || [];
}
/**
* Get custodian types for a schema slot (async version)
*
* Priority:
* 1. Read from slot's own LinkML schema annotations (custodian_types)
* 2. Inherit from parent class(es) that use this slot
* 3. Fall back to static mapping
* 4. Return empty array (no types assigned - cube shows no letters)
*/
export async function getCustodianTypesForSlotAsync(slotName: string): Promise<CustodianTypeCode[]> {
try {
// 1. Try slot's own annotation first
const annotationTypes = await linkmlSchemaService.getSlotCustodianTypes(slotName);
if (annotationTypes && annotationTypes.length > 0) {
const validated = validateCustodianTypes(annotationTypes);
if (validated.length > 0) {
return validated;
}
}
// 2. Try to inherit from parent class(es) that use this slot
const parentClasses = await linkmlSchemaService.getClassesUsingSlot(slotName);
if (parentClasses.length > 0) {
const inheritedTypes = new Set<CustodianTypeCode>();
for (const className of parentClasses) {
const classTypes = await linkmlSchemaService.getClassCustodianTypes(className);
if (classTypes && classTypes.length > 0) {
const validated = validateCustodianTypes(classTypes);
validated.forEach(t => inheritedTypes.add(t));
}
}
if (inheritedTypes.size > 0) {
return Array.from(inheritedTypes);
}
}
} catch (error) {
console.warn(`[CustodianMapping] Error reading annotations for slot ${slotName}:`, error);
}
// 3. Fall back to static mapping, or empty array if no mapping exists
// Empty array means "no custodian types assigned" - cube will show no letters
return SLOT_TO_CUSTODIAN_TYPE[slotName] || [];
}
/**
* Get custodian types for a schema enum (async version)
*
* Priority:
* 1. Read from enum's LinkML schema annotations (custodian_types)
* 2. Fall back to static mapping
* 3. Return empty array (no types assigned - cube shows no letters)
*/
export async function getCustodianTypesForEnumAsync(enumName: string): Promise<CustodianTypeCode[]> {
try {
const annotationTypes = await linkmlSchemaService.getEnumCustodianTypes(enumName);
if (annotationTypes && annotationTypes.length > 0) {
const validated = validateCustodianTypes(annotationTypes);
if (validated.length > 0) {
return validated;
}
}
} catch (error) {
console.warn(`[CustodianMapping] Error reading annotations for enum ${enumName}:`, error);
}
// Fall back to static mapping, or empty array if no mapping exists
// Empty array means "no custodian types assigned" - cube will show no letters
return ENUM_TO_CUSTODIAN_TYPE[enumName] || [];
}