lipdjs
Version:
A JavaScript library for reading and writing LiPD (Linked Paleo Data) files
1,894 lines • 68.8 kB
text/typescript
/**
* The LipdToRDF class helps in converting a LiPD file to an RDF Graph.
* It uses the SCHEMA object (from globals/schema.ts) to do the conversion
*/
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import AdmZip from 'adm-zip';
import * as Papa from 'papaparse';
import { Store } from 'n3';
import { DataFactory } from 'n3';
import { Logger } from '../utils/logger';
import { ONTONS, NAMESPACES, NSURL, DATAURL } from '../globals/urls';
import { SCHEMA } from '../globals/schema';
import { SYNONYMS } from '../globals/synonyms';
import { uniqid, sanitizeId, ucfirst, lcfirst, camelCase, escape, serializeStore } from './utils';
import { BLACKLIST } from '../globals/blacklist';
import { ChangeLog } from '../classes/changelog';
import JSZip from 'jszip';
import { isBrowser } from './env';
// Get the logger instance
const logger = Logger.getInstance();
const DF = DataFactory
function expandSchema(schema: any): any {
// Clone schema to avoid modifying the original
const expandedSchema = JSON.parse(JSON.stringify(schema));
// Expand schema by adding alternate keys
for (const key in expandedSchema) {
for (const lipdKey in expandedSchema[key]) {
const pdetails = expandedSchema[key][lipdKey];
// Skip if not an object
if (typeof pdetails !== 'object' || pdetails === null) {
continue;
}
// Add alternates if they exist
if (pdetails.alternates && Array.isArray(pdetails.alternates)) {
for (const altKey of pdetails.alternates) {
expandedSchema[key][altKey] = { ...pdetails };
}
}
}
}
// Mark as expanded
expandedSchema.__expanded = true;
return expandedSchema;
}
export class LipdToRDF {
public store: Store;
public graphUrl: string;
public namespaces: {
ont: any;
rdf: any;
rdfs: any;
xsd: any;
owl: any;
wgs84: any;
};
private lipdCsvs: { [key: string]: any[][] } = {};
private standardize: boolean;
private addLabels: boolean;
private schema: any;
private namespace: string;
constructor(standardize: boolean = true, addLabels: boolean = true) {
this.store = new Store();
this.graphUrl = NSURL;
this.namespace = NSURL + "/"
// Define namespaces using DataFactory
this.namespaces = {
ont: DF.namedNode(ONTONS),
rdf: DF.namedNode(NAMESPACES.rdf),
rdfs: DF.namedNode(NAMESPACES.rdfs),
xsd: DF.namedNode(NAMESPACES.xsd),
owl: DF.namedNode(NAMESPACES.owl),
wgs84: DF.namedNode(NAMESPACES.wgs84)
};
this.standardize = standardize;
this.addLabels = addLabels;
this.schema = expandSchema(JSON.parse(JSON.stringify(SCHEMA)));
logger.debug('LipdToRDF instance created with standardize=%s, addLabels=%s', standardize, addLabels);
}
/**
* Convert LiPD file to RDF Graph
* @param lipdPath Path to LiPD file (can be a local file or URL)
*/
public async convert(lipdPath: string): Promise<void> {
logger.debug('Starting conversion of LiPD file: %s', lipdPath);
if (isBrowser()) {
await this._convertBrowser(lipdPath);
logger.debug('Browser conversion completed');
return;
}
// Reset graph
for (const quad of this.store.getQuads(null, null, null, null)) {
this.store.removeQuad(quad);
}
// Get the base name of the LiPD file
const lpdName = path.basename(lipdPath).replace('.lpd', '').replace(/\?.+$/, '');
this.graphUrl = NSURL + "/" + lpdName;
logger.debug('Set graph URL to: %s', this.graphUrl);
// Create a temporary directory
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lipd_to_rdf_'));
logger.debug('Created temporary directory: %s', tmpDir);
try {
// Unzip the LiPD file
logger.debug('Unzipping LiPD file to temporary directory');
this._unzipLipdFile(lipdPath, tmpDir);
// Find JSON-LD files
logger.debug('Looking for JSON-LD files in the extracted content');
const jsons = this._findFilesWithExtension(tmpDir, 'jsonld');
logger.debug('Found %d JSON-LD files', jsons.length);
// Process each JSON-LD file
for (const [jsonPath, jsonName] of jsons) {
logger.debug('Processing JSON-LD file: %s', jsonName);
const jsonDir = path.dirname(jsonPath);
// Find CSV files
logger.debug('Looking for CSV files in %s', jsonDir);
const csvs = this._findFilesWithExtension(jsonDir, 'csv');
logger.debug('Found %d CSV files', csvs.length);
// Reset the CSV cache
this.lipdCsvs = {};
// Load each CSV file
for (const [csvPath, csvName] of csvs) {
try {
logger.debug('Processing CSV file: %s', csvName);
// Read the CSV file
const csvData = fs.readFileSync(csvPath, 'utf8');
// Parse the CSV
const parsedCsv = Papa.parse(csvData, { header: false });
if (parsedCsv.data && Array.isArray(parsedCsv.data)) {
this.lipdCsvs[csvName] = parsedCsv.data as any[][];
logger.debug('Successfully loaded CSV file: %s', csvName);
}
} catch (error) {
logger.warn('CSV file %s might have inconsistent columns: %s', csvName, error instanceof Error ? error.message : String(error));
// Try to detect columns and load
this.lipdCsvs[csvName] = this._detectColumnsAndLoad(csvPath);
}
}
// Process the JSON file
logger.debug('Loading JSON-LD data into RDF graph');
this._loadLipdJsonToGraph(jsonPath);
}
logger.debug('Conversion completed successfully');
} catch (error) {
logger.error('Error during conversion: %s', error instanceof Error ? error.message : String(error));
throw error;
} finally {
// Clean up the temporary directory
try {
logger.debug('Cleaning up temporary directory: %s', tmpDir);
fs.rmSync(tmpDir, { recursive: true });
} catch (error) {
logger.error('Error cleaning up temporary directory: %s', error instanceof Error ? error.message : String(error));
}
}
}
/**
* Load LiPD file from a File object (for browser file input)
* @param file File object from HTML5 file input
*/
public async loadFromFile(file: File): Promise<void> {
if (!isBrowser()) {
throw new Error('loadFromFile() is only available in browser environments');
}
logger.debug('Loading LiPD file from File object: %s', file.name);
try {
// Reset graph
for (const quad of this.store.getQuads(null, null, null, null)) {
this.store.removeQuad(quad);
}
// Read file as ArrayBuffer
const arrayBuffer = await file.arrayBuffer();
// Load with JSZip
const zip = await JSZip.loadAsync(arrayBuffer);
// Set graph URL based on filename
const lpdName = file.name.replace('.lpd', '').replace(/\?.+$/, '');
this.graphUrl = NSURL + "/" + sanitizeId(lpdName);
logger.debug('Set graph URL to: %s', this.graphUrl);
// Reset CSV cache
this.lipdCsvs = {};
// Separate CSV and JSON entries for two-pass processing
const csvEntries: Array<[string, JSZip.JSZipObject]> = [];
const jsonEntries: JSZip.JSZipObject[] = [];
for (const fileName of Object.keys(zip.files)) {
const zipFile = zip.files[fileName];
if (zipFile.dir) continue;
if (fileName.endsWith('.csv')) {
csvEntries.push([fileName, zipFile]);
} else if (fileName.endsWith('.jsonld')) {
jsonEntries.push(zipFile);
}
}
// Pass 1: cache CSVs
for (const [fileName, zipFile] of csvEntries) {
const csvContent = await zipFile.async('string');
const parsedCsv = Papa.parse(csvContent, { header: false });
this.lipdCsvs[fileName] = parsedCsv.data as any[][];
const baseName = fileName.split('/').pop();
if (baseName) {
this.lipdCsvs[baseName] = parsedCsv.data as any[][];
}
logger.debug(`Loaded CSV '${fileName}' (${parsedCsv.data.length}×${((parsedCsv.data as any[])[0] as any[]).length || 0})`);
}
// Pass 2: process JSON after CSVs are ready
for (const zipFile of jsonEntries) {
const jsonContent = await zipFile.async('string');
this._loadLipdJsonString(jsonContent);
}
logger.debug('File loading completed successfully');
} catch (error) {
logger.error('Error loading LiPD file from File object: %s', error instanceof Error ? error.message : String(error));
throw error;
}
}
/**
* Detect the number of columns in a CSV and load it
* @param filePath Path to the CSV file
* @returns Parsed CSV data as array of arrays
*/
private _detectColumnsAndLoad(filePath: string): any[][] {
// Detect number of columns
let numColumns = 0;
const lines = fs.readFileSync(filePath, 'utf8').split('\n');
for (const line of lines) {
const num = line.split(',').length;
if (num > numColumns) {
numColumns = num;
}
}
// Parse the CSV with the detected number of columns
const csvData = fs.readFileSync(filePath, 'utf8');
const parsedCsv = Papa.parse(csvData, { header: false });
return parsedCsv.data as any[][];
}
/**
* Write LiPD RDF Graph to a file
* @param toPath Path to output file
* @param type Output format ('json', 'turtle', 'n3', 'ntriples', etc.)
*/
public async serialize(toPath: string, type: string = 'turtle'): Promise<void> {
logger.debug('Serializing graph to %s in %s format', toPath, type);
if (this.store) {
try {
const serialized = await serializeStore(this.store, type, logger);
fs.writeFileSync(toPath, serialized);
logger.debug('Successfully wrote graph to: %s', toPath);
} catch (error) {
logger.error('Error serializing graph: %s', error instanceof Error ? error.message : String(error));
throw new Error(`Failed to serialize graph: ${error instanceof Error ? error.message : String(error)}`);
}
} else {
logger.error('Cannot serialize: Graph is null or undefined');
throw new Error('Cannot serialize: Graph is null or undefined');
}
}
/**
* Write LiPD RDF Graph to a file
* @param toPath Path to output file
* @param type Output format ('json', 'turtle', 'n3', 'ntriples', etc.)
*/
public async toString(type: string = 'turtle'): Promise<string> {
if (this.store) {
try {
let serialized: string | undefined;
serialized = await serializeStore(this.store, type, logger);
return serialized || '';
} catch (error) {
logger.error('Error serializing graph:', error);
throw new Error(`Failed to serialize graph: ${error instanceof Error ? error.message : String(error)}`);
}
}
return '';
}
/**
* Unzip a LiPD file to a directory
* @param lipdFile Path to the LiPD file
* @param unzipDir Directory to extract to
*/
private _unzipLipdFile(lipdFile: string, unzipDir: string): void {
try {
if (lipdFile.startsWith('http')) {
// If this is a URL, fetch it first (would use fetch API in a full implementation)
throw new Error('URL-based LiPD files not yet supported in this implementation');
} else {
// If this is a local file, unzip it
logger.debug('Unzipping local file: %s to %s', lipdFile, unzipDir);
const zip = new AdmZip(lipdFile);
zip.extractAllTo(unzipDir, true);
logger.debug('Unzipping completed successfully');
}
} catch (error) {
logger.error('Error unzipping LiPD file: %s', error instanceof Error ? error.message : String(error));
throw new Error(`Failed to unzip LiPD file: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Find files with a specific extension in a directory (recursively)
* @param directory Directory to search in
* @param extension File extension to look for
* @returns Array of [filePath, fileName] tuples
*/
private _findFilesWithExtension(directory: string, extension: string): Array<[string, string]> {
const regex = new RegExp(`\\.${extension}$`);
const results: Array<[string, string]> = [];
try {
const entries = fs.readdirSync(directory, { withFileTypes: true });
for (const entry of entries) {
const entryPath = path.join(directory, entry.name);
if (entry.isFile() && regex.test(entry.name)) {
results.push([entryPath, entry.name]);
} else if (entry.isDirectory()) {
// Recursively search subdirectories
const subResults = this._findFilesWithExtension(entryPath, extension);
results.push(...subResults);
}
}
} catch (error) {
logger.error(`Cannot access ${directory}. Probably a permissions error:`, error);
}
return results;
}
/**
* Load LiPD JSON data into the RDF graph
* @param jsonPath Path to the JSON file
* @param url Optional URL of the LiPD file
*/
private _loadLipdJsonToGraph(jsonPath: string, url?: string): void {
logger.debug('Loading JSON file to graph: %s', jsonPath);
for (const quad of this.store.getQuads(null, null, null, null)) {
this.store.removeQuad(quad);
}
try {
// Read and parse the JSON file
const jsonContent = fs.readFileSync(jsonPath, 'utf8');
const obj = JSON.parse(jsonContent);
logger.debug('JSON file parsed successfully');
// Set the graph URL based on dataset name if available
if (obj.dataSetName) {
this.graphUrl = NSURL + "/" + sanitizeId(obj.dataSetName);
logger.debug('Updated graph URL based on dataset name: %s', this.graphUrl);
}
// Map the LiPD data to RDF
logger.debug('Mapping LiPD data to RDF structure');
const objHash: Record<string, any> = {};
this._mapLipdToJson(obj, null, null, 'Dataset', 'Dataset', objHash);
// Set URL if provided
if (url) {
objHash[obj['@id']].hasUrl = url;
logger.debug('Set URL from parameter: %s', url);
} else if (obj['@id']) {
objHash[obj['@id']].hasUrl = DATAURL + "/" + obj['@id'] + '.lpd';
logger.debug('Set derived URL: %s', DATAURL + "/" + obj['@id'] + '.lpd');
}
// Create individuals for all objects in the hash
logger.debug('Creating RDF individuals for %d objects', Object.keys(objHash).length);
for (const [key, item] of Object.entries(objHash)) {
this._createIndividualFull(item);
}
logger.debug('Successfully loaded JSON data into RDF graph');
} catch (error) {
logger.error('Error loading JSON to graph: %s', error instanceof Error ? error.message : String(error));
throw new Error(`Failed to load JSON to graph: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Map LiPD JSON data to a structured format suitable for RDF conversion
* @param obj The JSON object to map
* @param parent Parent object
* @param index Index in parent's array
* @param category Category of the object
* @param schemaName Schema name to use
* @param hash Object hash for storing objects by ID
* @returns ID of the created object
*/
private _mapLipdToJson(obj: any, parent: any, index: any, category: string, schemaName: string, hash: Record<string, any>): string {
const schema = this.schema[schemaName] ? this.schema[schemaName] : {};
if (typeof obj !== 'object' || obj === null) {
return obj;
}
obj['@parent'] = parent;
obj['@index'] = index;
obj['@schema'] = schemaName;
let objId = this.getObjectId(obj, category, schema);
if ('@id' in obj) {
objId = obj['@id'];
}
if (objId in hash) {
return objId;
}
obj['@id'] = objId;
[obj, hash] = this.modifyStructureIfNeeded(obj, hash, schema);
if ('@category' in obj) {
category = obj['@category'];
}
hash[objId] = {
'@id': objId,
'@category': category,
'@schema': schemaName
};
const item = hash[objId];
if (typeof obj === 'object') {
for (const [propKey, value] of Object.entries(obj)) {
if (propKey[0] === '@') {
continue;
}
if (propKey in BLACKLIST) {
continue;
}
let details: any = {};
let pname = propKey;
if (propKey in schema) {
details = schema[propKey];
pname = details['name'] ? details['name'] : propKey;
}
const dtype = details['type'] ? details['type'] : null;
let cat = details['category'] ? details['category'] : null;
let sch = details['schema'] ? details['schema'] : null;
const fromJson = details['fromJson'] ? details['fromJson'] : null;
if (sch && !cat) {
cat = sch;
}
if (fromJson) {
const fn = this[fromJson as keyof this] as Function;
const processedValue = fn.call(this, value, obj);
if (!processedValue) {
continue;
}
if (pname) {
if (Array.isArray(processedValue)) {
let idx = 1;
for (const subValue of processedValue) {
if (typeof subValue === 'object') {
if (!(propKey in item)) {
item[propKey] = [];
}
item[propKey].push(this._mapLipdToJson(subValue, obj, idx, cat, sch, hash));
idx++;
}
}
} else if (typeof processedValue === 'object') {
item[propKey] = this._mapLipdToJson(processedValue, obj, null, cat, sch, hash);
} else {
item[propKey] = processedValue;
}
} else if (typeof processedValue === 'object') {
for (const [subPropKey, subValue] of Object.entries(processedValue)) {
item[subPropKey] = subValue;
}
}
continue;
}
if (!pname) {
continue;
}
if (Array.isArray(value)) {
let idx = 1;
for (const subValue of value) {
if (!(propKey in item)) {
item[propKey] = [];
}
item[propKey].push(this._mapLipdToJson(subValue, obj, idx, cat, sch, hash));
idx++;
}
} else if (typeof value === 'object') {
if (!(propKey in item)) {
item[propKey] = [];
}
item[propKey].push(this._mapLipdToJson(value, obj, null, cat, sch, hash));
} else {
if (dtype === 'Individual') {
item[propKey] = value;
if (!(String(value) in hash)) {
hash[String(value)] = {
'@id': value,
'@category': cat,
'@schema': sch
};
}
} else {
item[propKey] = value;
}
}
}
}
hash[objId] = item;
return objId;
}
/**
* Get compound key ID from an object
* @param compoundKey Array of keys to traverse the object
* @param obj Object to extract value from
* @returns The value at the end of the key path or null if not found
*/
private getCompoundKeyId(compoundKey: string[], obj: any): any {
let tobj = obj;
for (const key of compoundKey) {
if (typeof tobj === 'object' && tobj !== null && key in tobj) {
tobj = tobj[key];
} else {
return null;
}
}
if (typeof tobj !== 'object' || tobj === null) {
return tobj;
}
return null;
}
/**
* Get binding key ID from an object
* @param key Key or compound key (separated by dots) or alternative keys (separated by pipes)
* @param obj Object to extract value from
* @returns The value found or a unique ID if not found
*/
private getBindingKeyId(key: string, obj: any): string {
const keyOptions = key.split('|');
for (const optKey of keyOptions) {
const compoundKey = optKey.split('.');
const keyId = this.getCompoundKeyId(compoundKey, obj);
if (keyId) {
return String(keyId);
}
}
return uniqid();
}
/**
* Apply a function to a key ID
* @param fn Function name to apply
* @param arg Argument for the function
* @param curObjId Current object ID
* @returns Modified object ID
*/
private getFunctionKeyId(fn: string, arg: string, curObjId: string): string {
if (fn === 'trunc') {
return curObjId.substring(0, curObjId.length - parseInt(arg));
} else if (fn === 'uniqid') {
return String(curObjId) + uniqid(arg);
}
return curObjId;
}
/**
* Create an ID from a pattern
* @param pattern Array of pattern parts
* @param obj Object to extract values from
* @returns Generated ID string
*/
private createIdFromPattern(pattern: string[], obj: any): string {
let objId = '';
for (const key of pattern) {
const bindingMatch = key.match(/{(.+)}/);
if (bindingMatch && bindingMatch.length > 1) {
objId += String(this.getBindingKeyId(bindingMatch[1], obj));
} else {
const funcMatch = key.match(/_(.+)\((.*)\)/);
if (funcMatch && funcMatch.length > 2) {
const fn = funcMatch[1];
const arg = funcMatch[2];
objId = String(this.getFunctionKeyId(fn, arg, objId));
} else {
objId += String(key);
}
}
}
return objId;
}
/**
* Fix title by replacing problematic characters
* @param titleId Title ID to fix
* @returns Fixed title ID
*/
private fixTitle(titleId: string): string {
return titleId.replace(/@\\x{FFFD}@u/g, '_');
}
/**
* Get object ID based on schema and category
* @param obj Object to generate ID for
* @param category Category of the object
* @param schema Schema definition
* @returns Generated object ID
*/
private getObjectId(obj: any, category: string, schema: any): string {
let objId: string;
if (typeof obj === 'object' && obj !== null) {
objId = "Unknown." + uniqid(category);
} else {
objId = ucfirst(String(obj)).replace(/\s/g, '_');
}
if (schema && '@id' in schema) {
objId = this.createIdFromPattern(schema['@id'], obj);
}
return this.fixTitle(objId);
}
/**
* Modify the object structure if needed based on schema
* @param obj Object to modify
* @param hash Object hash
* @param schema Schema definition
* @returns [modified object, modified hash]
*/
private modifyStructureIfNeeded(obj: any, hash: any, schema: any): [any, any] {
if (schema['@fromJson']) {
for (const func of schema['@fromJson']) {
if (func in this) {
const fn = this[func as keyof LipdToRDF];
if (typeof fn === 'function') {
const result = (fn as Function).call(this, obj, hash);
if (Array.isArray(result) && result.length >= 2) {
[obj, hash] = result;
}
}
}
}
}
return [obj, hash];
}
/**
* Guess the data value type based on string pattern
* @param val Value to analyze
* @returns Detected data type
*/
private _guessDataValueType(val: any): string {
const value = String(val);
if (/^-?\d+$/.test(value)) {
return "float"; // "integer"
}
if (/^-?\d+\.\d+$/.test(value)) {
return "float";
}
if (/^[2][0-9]{3}[-][0-1][0-9][-][0-3][0-9]( |T)[0-9]{2}:[0-9]{2}:[0-9]{2}/.test(value)) {
return "datetime";
}
if (/^[2][0-9]{3}[-][0-1][0-9][-][0-3][0-9]/.test(value)) {
return "date";
}
if (/^(true|false)$/i.test(value)) {
return "boolean";
}
if (/^http/.test(value)) {
return "url";
}
// if (/^.+@.+\..+/.test(value)) {
// return "Email";
// }
if (/^".+"$/.test(value)) {
return "string";
}
if (/^'.+'$/.test(value)) {
return "string";
}
return "string";
}
/**
* Guess the value type for any kind of value
* @param value Value to analyze
* @returns Detected data type
*/
private _guessValueType(value: any): string {
if (value) {
if (Array.isArray(value)) {
for (const subvalue of value) {
return this._guessValueType(subvalue);
}
} else if (typeof value === 'object' && value !== null) {
return "Individual";
} else {
const valtype = this._guessDataValueType(value);
return valtype;
}
}
return "string";
}
/**
* Get property details from schema and value
* @param key Property key
* @param schema Schema definition
* @param value Property value
* @returns Property details object
*/
private getPropertyDetails(key: string, schema: any, value: any): any {
let pname = key;
const details: Record<string, any> = {
"name": pname
};
if (key in schema && "@@processed" in schema[key]) {
return schema[key];
}
// Get details from schema
if (key in schema) {
for (const [skey, svalue] of Object.entries(schema[key])) {
details[skey] = svalue;
}
}
if ("schema" in details) {
details["type"] = "Individual";
}
pname = lcfirst(details["name"]);
if (!("type" in details)) {
details["type"] = this._guessValueType(value);
if (!("type" in details)) {
details["type"] = "string";
}
}
details["@@processed"] = true;
schema[key] = details;
return details;
}
/**
* Create an individual
* @param objId ID of the individual
* @returns Fully qualified URI for the individual
*/
private createIndividual(objId: string): string {
return this.namespace + sanitizeId(objId);
}
/**
* Create a class
* @param category Category name
* @returns Fully qualified URI for the class
*/
private createClass(category: string): string {
return ONTONS + sanitizeId(category);
}
/**
* Create a property
* @param prop Property name
* @param dtype Data type
* @param cat Category
* @param multiple Whether the property can have multiple values
* @returns [property URI, data type, category, multiple flag]
*/
private createProperty(prop: string, dtype: string, cat: string, multiple: boolean): [string, string, string, boolean] {
const nsProp = prop.split(':', 2);
let ns = ONTONS;
if (nsProp.length > 1) {
const prefix = nsProp[0];
if (prefix in NAMESPACES) {
ns = NAMESPACES[prefix];
}
prop = nsProp[1];
}
return [ns + lcfirst(sanitizeId(prop)), dtype, cat, multiple];
}
/**
* Set individual classes
* @param objId ID of the individual
* @param category Primary category
* @param extraCats Additional categories
*/
private setIndividualClasses(objId: string, category: string | null, extraCats: string[]): void {
if (objId && category) {
this.store.addQuad(
DF.quad(
DF.namedNode(objId),
DF.namedNode(NAMESPACES.rdf + 'type'),
DF.namedNode(category),
DF.namedNode(this.graphUrl)
)
);
}
for (const ecat of extraCats) {
if (objId && ecat) {
this.store.addQuad(
DF.quad(
DF.namedNode(objId),
DF.namedNode(NAMESPACES.rdf + 'type'),
DF.namedNode(this.createClass(ecat)),
DF.namedNode(this.graphUrl)
)
);
}
}
}
/**
* Set object label
* @param objId ID of the object
* @param label Label to set
*/
private setObjectLabel(objId: string, label: string): void {
if (objId && label) {
DF.quad
this.store.addQuad(
DF.quad(
DF.namedNode(objId),
DF.namedNode(NAMESPACES.rdfs + 'label'),
DF.literal(label),
DF.namedNode(this.graphUrl)
)
);
}
}
/**
* Set property value
* @param objId ID of the object
* @param prop Property details (from _createProperty)
* @param value Value to set
*/
private setPropertyValue(objId: string, prop: [string, string, string, boolean], value: any): void {
if (Array.isArray(value)) {
for (const subValue of value) {
this.setPropertyValue(objId, prop, subValue);
}
return;
}
const [propId, dtype, cat, multiple] = prop;
if (!objId || value === null || value === undefined) {
return;
}
let objItem = null;
// Handle special values for numeric types
if (dtype === 'float' || dtype === 'integer') {
if (String(value).toLowerCase().includes('nan')) return;
if (String(value).toLowerCase().includes('na')) return;
}
// Escape string values
if (typeof value === 'string') {
value = escape(value);
}
// Convert to appropriate type
if (dtype === 'boolean') {
value = String(value).toLowerCase();
if (value !== 'true') {
value = 'false';
}
} else if (dtype === 'float') {
const match = String(value).match(/(-?\d+\.?\d*)/);
if (match) {
value = match[1];
} else {
value = 0.0;
}
} else if (dtype === 'integer') {
const match = String(value).match(/(-?\d+)/);
if (match) {
value = match[1];
} else {
value = 0;
}
}
// Create the appropriate RDF object
if (dtype === 'Individual') {
value = this.createIndividual(value);
objItem = DF.namedNode(value);
} else if (dtype === 'EnumeratedIndividual') {
objItem = DF.namedNode(value);
} else if (dtype === 'List') {
objItem = value;
} else {
// Get XSD datatype URI
let datatype = undefined;
if (dtype === 'float') datatype = DF.namedNode(NAMESPACES.xsd + 'float');
else if (dtype === 'integer') datatype = DF.namedNode(NAMESPACES.xsd + 'integer');
else if (dtype === 'boolean') datatype = DF.namedNode(NAMESPACES.xsd + 'boolean');
else if (dtype === 'date') datatype = DF.namedNode(NAMESPACES.xsd + 'date');
else if (dtype === 'dateTime') datatype = DF.namedNode(NAMESPACES.xsd + 'dateTime');
else if (dtype === 'string') datatype = DF.namedNode(NAMESPACES.xsd + 'string');
objItem = DF.literal(String(value), datatype);
}
// Don't add if property doesn't allow multiple values and a value already exists
if (!multiple) {
const existing = this.store.getQuads(
DF.namedNode(objId),
DF.namedNode(propId),
null,
null
);
if (existing.length > 0) {
return;
}
}
// Add the triple to the graph
this.store.addQuad(
DF.quad(
DF.namedNode(objId),
DF.namedNode(propId),
objItem,
DF.namedNode(this.graphUrl)
)
);
}
/**
* Create a full individual with all its properties
* @param obj Object to create
*/
private _createIndividualFull(obj: any): void {
const category = obj['@category'];
const extraCats = obj['@extracats'] || [];
const schemaName = obj['@schema'] || category;
const schema = this.schema[schemaName] || {};
const objId = obj['@id'];
if (!objId) {
return;
}
// Create category class
let categoryUri = null;
if (category) {
categoryUri = this.createClass(category);
}
// Create individual
const objUri = this.createIndividual(objId);
// Set individual classes
this.setIndividualClasses(objUri, categoryUri, extraCats);
// Set properties
for (const [key, value] of Object.entries(obj)) {
if (key[0] === '@') {
continue;
}
const details = this.getPropertyDetails(key, schema, value);
const prop = details.name;
const dtype = details.type;
const synonyms = details.synonyms || {};
let cat = details.category || null;
const sch = details.schema || null;
const fromJson = details.fromJson || null;
const multiple = details.multiple || false;
if (!prop) {
continue;
}
// Use schema if category is not set
if (sch && !cat) {
cat = sch;
}
// Create Property
const propDI = this.createProperty(prop, dtype, cat, multiple);
// Set property value
if (dtype === 'Individual') {
if (typeof value === 'string' && Object.keys(synonyms).length > 0) {
// If the value is a string and there are synonyms for this Individual
const lowerValue = value.toLowerCase();
if (synonyms[lowerValue]) {
// If we have a synonym-mapping for the value to an Individual
propDI[1] = 'EnumeratedIndividual'; // Rename property type to be an enumeration
let synId = synonyms[lowerValue].id;
if (!this.standardize) {
// If we don't want to standardize, then create a unique id for the individual
synId += '.' + uniqid();
}
this.setPropertyValue(objUri, propDI, synId);
// Only add object label in the current graph if set
if (this.addLabels) {
let label;
if (this.standardize) {
// Set the standard label for the individual
label = synonyms[lowerValue].label;
} else {
// Set the user label for the individual
label = value;
}
this.setObjectLabel(synId, label);
}
} else {
// We don't have a synonym-mapping for the value
// Create an individual and set its label to the value
propDI[1] = 'EnumeratedIndividual';
const synId = this.createIndividual(value) + '.' + uniqid();
this.setPropertyValue(objUri, propDI, synId);
this.setObjectLabel(synId, value);
}
} else {
// There are no synonyms, and value is not a string. Just use it directly
this.setPropertyValue(objUri, propDI, value);
}
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
this.setPropertyValue(objUri, propDI, value);
} else {
if (dtype === 'File') {
// File handling could be implemented here
// Similar to the Python code's commented section
} else {
this.setPropertyValue(objUri, propDI, value);
}
}
}
}
/**
* Parse persons string into array of person objects
* @param authorString String containing author names
* @param parent Optional parent object
* @returns Array of parsed author names
*/
private parsePersonsString(authorString: string, parent?: any): string[] {
// Check for semi-colon delimiter and split accordingly
if (authorString.includes(';')) {
const authorSplit = authorString.split(/\s*;\s*/);
// Further split the authors with commas if necessary
const authorList: string[] = [];
for (const author of authorSplit) {
if (author.includes(',')) {
const lastFirst = author.split(/\s*,\s*/);
authorList.push(`${lastFirst[1]} ${lastFirst[0]}`);
} else {
authorList.push(author);
}
}
return authorList;
} else {
// Split the author string with commas
const authorList: string[] = [];
const authorSplit = authorString.split(/\s*,\s*/);
if (authorSplit.length % 2 === 0) {
// Even number: last name first name
for (let i = 0; i < authorSplit.length; i += 2) {
authorList.push(`${authorSplit[i+1]} ${authorSplit[i]}`);
}
} else {
// Odd number: first name last name
for (const author of authorSplit) {
authorList.push(author);
}
}
return authorList;
}
}
/**
* Parse persons object into standardized format
* @param auths Author string or array of authors
* @param parent Optional parent object
* @returns Array of parsed person objects
*/
private parsePersons(auths: any, parent?: any): any[] {
const authors: string[] = [];
if (!Array.isArray(auths)) {
auths = [auths];
}
for (const authstr of auths) {
let authname = null;
if (typeof authstr === 'object' && authstr !== null) {
if ('name' in authstr) {
authname = authstr.name;
}
} else {
authname = authstr;
}
if (authname) {
const auth = this.parsePersonsString(authname, parent);
if (Array.isArray(auth)) {
authors.push(...auth);
} else {
authors.push(auth);
}
}
}
return authors.map(auth => ({ name: auth }));
}
/**
* Flatten array recursively (browser-compatible alternative to Array.flat())
* @param arr Array to flatten
* @returns Flattened array
*/
private _flattenArray(arr: any[]): any[] {
const result: any[] = [];
for (const item of arr) {
if (Array.isArray(item)) {
result.push(...this._flattenArray(item));
} else {
result.push(item);
}
}
return result;
}
/**
* Set column numbers for variables
* @param datatable Datatable object
* @param parent Parent object
* @returns Datatable object with ordered variables
*/
private setColumnNumbers(datatable: any, parent: any = null): any {
for (const [index, variable] of datatable.variables.entries()) {
variable.columnNumber = index + 1;
console.log("setColumnNumbers", variable);
}
return datatable;
}
/**
* Parse changeLog object into standardized format
* @param changes Change list
* @param parent Optional parent object
* @returns Array of parsed person objects
*/
private parseChanges(changes: any, parent?: any): any {
const newChanges: any = []
if (!Array.isArray(changes)) {
changes = [changes]
}
for (const change of changes) {
for (const name of Object.keys(change)) {
let notes = change[name] || []
// Convert notes to 1-dimensional array (browser-compatible alternative to flat())
notes = Array.isArray(notes) ? this._flattenArray(notes) : [notes];
const newChange = {
name: name,
notes: notes
}
newChanges.push(newChange);
}
}
return newChanges;
}
/**
* Parse location object
* @param geo Location object
* @param parent Parent object
* @returns Processed location object
*/
private parseLocation(geo: any, parent?: any): any {
const ngeo: Record<string, any> = {};
ngeo.locationType = geo.type || null;
if (parent && parent['@id']) {
ngeo.coordinatesFor = parent['@id'];
}
if (geo.geometry && geo.geometry.coordinates) {
const coords = geo.geometry.coordinates;
if (coords && coords.length > 0) {
ngeo.coordinates = `${coords[1]},${coords[0]}`;
ngeo['wgs84:lat'] = coords[1];
ngeo.hasLatitude = coords[1];
ngeo.latitude = coords[1];
ngeo['wgs84:long'] = coords[0];
ngeo.longitude = coords[0];
ngeo.hasLongitude = coords[0];
if (coords.length > 2) {
ngeo['wgs84:alt'] = coords[2];
ngeo.elevation = coords[2];
ngeo.hasElevation = coords[2];
}
}
}
if (geo.properties && typeof geo.properties === 'object') {
for (const [key, value] of Object.entries(geo.properties)) {
ngeo[key] = value;
}
} else if (typeof geo === 'object') {
for (const [key, value] of Object.entries(geo)) {
if (key !== 'geometry') {
// Do not add lat long if they are already added
if (!(`wgs84:${key}` in ngeo)) {
ngeo[key] = value;
}
}
}
}
return ngeo;
}
/**
* Process uncertainty values
* @param val Uncertainty value
* @param parent Parent object
* @returns Uncertainty object
*/
private getUncertainty(val: any, parent?: any): any {
const uncertainty: Record<string, any> = {};
uncertainty.hasValue = val;
uncertainty.analytical = val;
uncertainty.reproducibility = val;
return uncertainty;
}
/**
* Get Google Spreadsheet URL from key
* @param key Spreadsheet key
* @param parent Parent object
* @returns Spreadsheet URL
*/
private getGoogleSpreadsheetUrl(key: string, parent?: any): string {
return `https://docs.google.com/spreadsheets/d/${key}`;
}
/**
* Get a property from a parent object
* @param obj Object with parent reference
* @param prop Property to find
* @returns Property value or null
*/
private getParentProperty(obj: any, prop: string): any {
let parent = obj['@parent'];
while (parent) {
if (prop in parent) {
return parent[prop];
}
parent = parent['@parent'];
}
return null;
}
/**
* Get a parent with a specific property value
* @param obj Object with parent reference
* @param prop Property to check
* @param val Value to match
* @returns Parent object or null
*/
private getParentWithPropertyValue(obj: any, prop: string, val: any): any {
let parent = obj['@parent'];
while (parent) {
if (prop in parent && parent[prop] === val) {
return parent;
}
parent = parent['@parent'];
}
return null;
}
/**
* Set identifier properties for publications
* @param pub Publication object
* @param objHash Object hash
* @returns [modified publication, modified hash, added objects]
*/
private setIdentifierProperties(pub: any, objHash: Record<string, any>): [any, Record<string, any>, string[]] {
if ('identifier' in pub) {
for (const identifier of pub.identifier) {
if (identifier.type === 'doi') {
if (!('hasDOI' in pub)) {
pub.hasDOI = [];
}
pub.hasDOI.push(identifier.id);
} else if (identifier.type === 'issn') {
if (!('hasISSN' in pub)) {
pub.hasISSN = [];
}
pub.hasISSN.push(identifier.id);
} else if (identifier.type === 'isbn') {
if (!('hasISBN' in pub)) {
pub.hasISBN = [];
}
pub.hasISBN.push(identifier.id);
}
if ('url' in identifier) {
if (!('hasLink' in pub)) {
pub.hasLink = [];
}
pub.hasLink.push(identifier.url);
}
}
delete pub.identifier;
}
return [pub, objHash, []];
}
/**
* Convert values array to string
* @param obj Object with values
* @param objHash Object hash
* @returns [modified object, modified hash, added objects]
*/
private valuesToString(obj: any, objHash: Record<string, any>): [any, Record<string, any>, string[]] {
if ('values' in obj && Array.isArray(obj.values)) {
obj.values = obj.values.join(', ');
}
return [obj, objHash, []];
}
/**
* Guess sensor type based on archive, observation, and sensor
* @param archive Archive type
* @param observation Observation type
* @param sensor Sensor data
* @returns Guessed sensor type
*/
private guessSensorType(archive: string, observation: string, sensor: any): string {
if (('sensorGenus' in sensor) || ('sensorSpecies' in sensor)) {
if (archive === 'MarineSediment') {
return 'Foraminifera';
} else if (archive === 'Coral') {
return 'Polyp';
} else if (archive === 'Wood') {
return 'Vegetation';
} else if (archive === 'MolluskShell') {
return 'Bivalves';
} else if (archive === 'Sclerosponge') {
return 'Sponge';
}
return 'OrganicSensor';
} else {
if (archive === 'MarineSediment' && (observation === 'Uk37' || observation === 'Alkenone')) {
return 'Coccolithophores';
} else if (archive === 'MarineSediment' && observation === 'TEX86') {
return 'Archea';
} else if (archive === 'MarineSediment' && observation === 'D18O') {
return 'Foraminifera';
} else if (archive === 'MarineSediment' && observation === 'Mg/Ca') {
return 'Foraminifera';
} else if (archive === 'LakeSediment' && (observation === 'Uk37' || observation === 'Alkenone')) {
return 'Coccolithophores';
} else if (archive === 'LakeSediment' && observation === 'TEX86') {
return 'Archea';
} else if (archive === 'LakeSediment' && observation === 'Midge') {
return 'Chironomids';
} else if (archive === 'LakeSediment' && observation === 'BSi') {
return 'Diatoms';
} else if (archive === 'LakeSediment' && observation === 'Chironomid') {
return 'Chironomids';
} else if (archive === 'LakeSediment' && observation === 'Reflectance') {
return 'PhotosyntheticAlgae';
} else if (archive === 'LakeSediment' && observation === 'Pollen') {
return 'Watershed';
} else if (archive === 'Coral') {
return 'Polyp';
} else if (archive === 'Wood') {
return 'Vegetation';
} else if (archive === 'MolluskShell') {
return 'Bivalves';
} else if (archive === 'Sclerosponge') {
return 'Sponge';
} else if (archive === 'Speleothem') {
return 'Karst';
} else if (archive === 'GlacierIce') {
return 'Snow';
} else if (archive === 'LakeSediment' && observation === 'VarveThickness') {
return 'Catchment';
} else if (archive === 'GlacierIce' && observation === 'Melt') {
return 'IceSurface';
} else if (archive === 'Borehole') {
return 'Soil';
} else {
return 'InorganicSensor';
}
}
}
/**
* Standardize observation names
* @param observation Observation name
* @returns Standardized observation name
*/
private getObservation(observation: string | null | undefined): string | null {
if (observation === null || observation === undefined) {
return null;
}
if (observation.toLowerCase() === 'alkenone') {
return 'Uk37';
}
return camelCase(observation);
}
/**
* Generate a variable ID
* @param obj Variable object
* @param parentId Parent ID
* @returns Generated variable ID
*/
private getVariableId(obj: any, parentId: string): string {
const iobj: Record<string, any> = {};
// Convert keys to lowercase for case-insensitive matching
for (const [key, value] of Object.entries(obj)) {
iobj[key.toLowerCase()] = value;
}
if (!('tsid' in iobj)) {
iobj.tsid = uniqid();
}
let id = `${parentId}.${iobj.tsid}`;
id += `.${iobj.variablename || ''}`;
return id;
}
/**
* Wrap integration time data
* @param obj Object with integration time data
* @param objHash Object hash
* @returns [modified object, modified hash, added objects]
*/
private wrapIntegrationTime(obj: any, objHash: Record<string, any>): [any, Record<string, any>, string[]] {
const objId = obj['@id'];
const pvals: Record<string, any> = {};
// Process all integration time related properties
const keysToDelete: string[] = [];
for (const [key, value] of Object.entries(obj)) {
if (/^integrationTime$/i.test(key)) {
pvals.hasValue = value;
keysToDelete.push(key);
} else {
const match = key.match(/^integrationTime(.+)/);
if (match) {
const nkey = match[1];
const nkeyLcfirst = lcfirst(nkey);
pvals[nkeyLcfirst] = value;
keysToDelete.push(key);
}
}
}
// Delete the processed keys
for (const key of keysToDelete) {
delete obj[key];
}
// If we found any integration time values, create a new object
if (Object.keys(pvals).length > 0) {
const inTimeId = `${objId}.IntegrationTime`;
obj.integrationTime = inTimeId;
const inTime: Record<string, any> = {
'@id': inTimeId,
'@category': 'IntegrationTime',
'@schema': 'IntegrationTime'
};
// Add all properties
Object.assign(inTime, pvals);
// Add to object hash
objHash[inTimeId] = inTime;
return [obj, objHash, [inTimeId]];
}
return [obj, objHash, []];
}
/**
* Add interpretation rank
* @param obj Interpretation object
* @param objHash Object hash
* @returns [modified object, modified hash, added objects]
*/
private addInterpretationRank(obj: any, objHash: Record<string, any>): [any, Record<string, any>, string[]] {
if (!('rank' in obj) || typeof obj.rank !== 'number') {
const rank = obj['@index'] - 1;
obj.rank = rank;
}
return [obj, objHash, []];
}
/**
* Wrap uncertainty data
* @param obj Object with uncertainty data
* @param objHash Object hash
* @returns [modified object, modified hash, added objects]
*/
private wrapUncertainty(obj: any, objHash: Record<string, any>): [any, Record<string, any>, string[]] {
const objId = obj['@id'];
const pvals: Record<string, any> = {};
const keysToBeDeleted: string[] = [];
// Process all uncertainty related properties
for (const [key, value] of Object.entries(obj)) {
if (/^uncertainty$/i.test(key)) {
pvals.hasValue = value;
keysToBeDeleted.push(key);
} else if (/^uncertainty/i.test(key)) {
pvals[key] = value;
keysToBeDeleted.push(key);
}
}
// Delete the processed keys
for (const key of keysToBeDeleted) {
delete obj[key];
}
// If we found any uncertainty values, create a new object
if (Object.keys(pvals).length > 0) {
const uncId = `${objId}.Uncertainty`;
obj.hasUncertainty = uncId;
const uncertainty: Record<string, any> = {
'@id': uncId,
'@category': 'Uncertainty'
};
// Add all properties
for (const [prop, value] of Object.entries(pvals)) {
uncertainty[prop] = value;
}
// Add to object hash
objHash[uncId] = uncertainty;
return [obj, objHash, [uncId]];
}
return [obj, objHash, []];
}
/**
* Add found in table reference
* @param obj Object to modify
* @param objHash Object hash
* @returns [modified object, modified hash, added objects]
*/
private addFoundInTable(obj: any, objHash: Record<string, any>): [any, Record<string, any>, string[]] {
if (obj['@parent'] && obj['@parent']['@id']) {
obj.foundInTable = obj['@parent']['@id'];
}
return [obj, objHash, []];
}
/**
* Add found in dataset reference
* @param obj Object to modify
* @param objHash Object hash
* @returns [modified object, modified hash, added objects]
*/
private addFoundInDataset(obj: any, objHash: Record<string, any>): [any, Record<string, any>, string[]] {
let parent = obj['@parent'];
let top = parent;
while (parent) {
top = parent;
parent = parent['@parent'];
}
if (top && top['@id']) {
obj.foundInDataset = top['@id'];
}
return [obj, objHash, []];
}
/**
* Add variable values from CSV data
* @param obj Variable object
* @param objHash Object hash
* @returns [modified object, modified hash, added objects]
*/
private addVariableValues(obj: any, objHash: Record<string, any>): [any, Record<string, any>, string[]] {
if (!obj['@parent'] || !obj['@parent']['@id']) {
return [obj, objHash, []];
}
const csvName = `${obj['@parent']['@id']}.csv`;
if (!('number' in obj)) {
obj.number = obj['@index'];
}
if (typeof obj.number === 'string') {
obj.number = parseInt(obj.number, 10);
}
if (!Array.isArray(obj.number)) {
obj.number = [obj.number];
}
const indices = obj.number.map((col: any) => parseInt(col, 10) - 1);
// Enhanced CSV lookup - try multiple variants
let csvData: any[][] | null = null;
const lookupKeys = [
csvName, // e.g., "tableId.csv"
csvName.split('/').pop(), // basename only
];
// Also try all CSV filenames that end with the expected name
const availableCsvs = Object.keys(this.lipdCsvs);
for (const availableCsv of availableCsvs) {
if (availableCsv.endsWith(csvName)) {
lookupKeys.push(availableCsv);
}
}
logger.debug(`Looking for CSV with keys: ${lookupKeys.join(', ')}`);
logger.debug(`Available CSVs: ${availableCsvs.join(', ')}`);
for (const key of lookupKeys) {
if (key && key in this.lipdCsvs) {
csvData = this.lipdCsvs[key];
logger.debug(`Found CSV data with key: ${key}`);
break;
}
}
if (csvData) {
let values: any[] = [];
if (indices.length === 1) {
if (indices[0] >= 0 && csvData.length > 0 && indices[0] < csvData[0].length) {
// Extract column from all rows
values = csvData.map(row => row[indices[0]]);
}
} else {
// Handle multiple columns
values = indices.map((index: number) => {
return csvData!.map((row: any[]) => row[index]);
});
}
// Convert to JSON string
const valString = JSON.stringify(values);
obj.hasValues = valString;
logger.debug(`Added ${values.length} values for variable`);
return [obj, objHash, []];
} else {
logger.debug(`CSV '${csvName}' not found in zip — cannot fill hasValues. Available: ${availableCsvs.join(', ')}`);
}
return [obj, objHash, []];
}
/**
* Add standard variable reference
* @param obj Variable object
* @param objHash Object hash
* @returns [modified object, modified hash, added objects]
*/
private addStandardVariable(obj: any, objHash: Record<string, any>): [any, Record<string, any>, string[]] {
if ('variableName' in obj) {
const name = obj.variableName;
const synonyms = SYNONYMS.VARIABLES?.PaleoVariable;
if (typeof name === 'string' && synonyms && name.toLowerCase() in synonyms) {
obj.hasStandardVariable = synonyms[name.toLowerCase()].id;
// Only add object label in the current graph if set
if (this.addLabels) {
const label = synonyms[name.toLowerCase()].label;
this.setObjectLabel(obj.hasStandardVariable, label);
}
}
}
return [obj, objHash, []];
}
/**
* Stringify column numbers array
* @param obj Variable object
* @param objHash Object hash
* @returns [modified object, modified hash, added objects]
*/
private stringifyColumnNumbersArray(obj: any, objHash: Record<string, any>): [any, Record<string, any>, string[]] {
if ('number' in obj && Array.isArray(obj.number) && obj.number.length > 1) {
obj.hasColumnNumber = JSON.stringify(obj.number);
delete obj.number;
}
return [obj, objHash, []];
}
private async _convertBrowser(lipdPath: string): Promise<void> {
try {
// Fetch the LiPD file as ArrayBuffer
const response = await fetch(lipdPath);
if (!response.ok) {
throw new Error(`Failed to fetch LiPD file: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
// Load with JSZip
const zip = await JSZip.loadAsync(arrayBuffer);
// Separate CSV and JSON entries so we can ensure CSVs are cached first
const csvEntries: Array<[string, JSZip.JSZipObject]> = [];
const jsonEntries: JSZip.JSZipObject[] = [];
for (const fileName of Object.keys(zip.files)) {
const file = zip.files[fileName];
if (file.dir) continue;
if (fileName.endsWith('.csv')) {
csvEntries.push([fileName, file]);
} else if (fileName.endsWith('.jsonld')) {
jsonEntries.push(file);
}
}
// Pass 1: load all CSVs
for (const [fileName, file] of csvEntries) {
const csvContent = await file.async('string');
const parsedCsv = Papa.parse(csvContent, { header: false });
this.lipdCsvs[fileName] = parsedCsv.data as any[][];
const baseName = fileName.split('/').pop();
if (baseName) {
this.lipdCsvs[baseName] = parsedCsv.data as any[][];
}
logger.debug(`Loaded CSV '${fileName}' (${parsedCsv.data.length}×${((parsedCsv.data as any[])[0] as any[]).length || 0})`);
}
// Pass 2: process JSON after CSVs are ready
for (const file of jsonEntries) {
const jsonContent = await file.async('string');
this._loadLipdJsonString(jsonContent);
}
} catch (error) {
logger.error('Error converting LiPD in browser: %s', error instanceof Error ? error.message : String(error));
throw error;
}
}
/**
* Load LiPD JSON content that is already available as a string (used in browser flow)
* @param jsonContent Raw JSON-LD content
*/
private _loadLipdJsonString(jsonContent: string): void {
try {
const obj = JSON.parse(jsonContent);
// Set graph URL based on dataset name if available
if (obj.dataSetName) {
this.graphUrl = NSURL + "/" + sanitizeId(obj.dataSetName);
}
// Map LiPD data to RDF
const objHash: Record<string, any> = {};
this._mapLipdToJson(obj, null, null, 'Dataset', 'Dataset', objHash);
// Set hasUrl to the source if possible (not available from buffer)
if (obj['@id']) {
objHash[obj['@id']].hasUrl = DATAURL + "/" + obj['@id'] + '.lpd';
}
// Create individuals for all objects in the hash
for (const item of Object.values(objHash)) {
this._createIndividualFull(item);
}
} catch (error) {
logger.error('Error loading JSON content in browser: %s', error instanceof Error ? error.message : String(error));
throw error;
}
}
}