UNPKG

lipdjs

Version:

A JavaScript library for reading and writing LiPD (Linked Paleo Data) files

1,220 lines (1,078 loc) 40.5 kB
/** * The RDFToLiPD class helps in converting an RDF Graph to a LiPD file. * It uses the SCHEMA dictionary (from globals/schema.ts) to do the conversion */ import { Quad, Store } from 'n3'; import { DataFactory, NamedNode, Literal } from 'n3'; import { Logger } from '../utils/logger'; import * as fs from 'fs'; import * as path from 'path'; import AdmZip from 'adm-zip'; import { NSURL } from '../globals/urls'; import { SCHEMA } from '../globals/schema'; import { REVERSE_BLACKLIST } from '../globals/blacklist'; import { lcfirst, parseVariableValues, ucfirst } from './utils'; import { RSYNONYMS } from '../globals/synonyms'; import { Change } from '../classes/change'; import { createBagitFiles } from './bagit'; import { DataTable } from '../classes/datatable'; import { Variable } from '../classes/variable'; import pako from 'pako'; const logger = Logger.getInstance(); const DF = DataFactory; interface TableColumn { name: string; values: any[]; number?: number; [key: string]: any; } interface Table { name: string; filename?: string; columns: TableColumn[]; [key: string]: any; } interface LiPDData { paleoData?: Array<{ measurementTable?: Table[]; model?: Array<{ ensembleTable?: Table[]; summaryTable?: Table[]; }>; }>; chronData?: Array<{ measurementTable?: Table[]; model?: Array<{ ensembleTable?: Table[]; summaryTable?: Table[]; }>; }>; [key: string]: any; } interface PropertyDetails { name: string; multiple?: boolean; [key: string]: any; } interface RDFValue { '@type': 'uri' | 'literal'; '@id'?: string; '@value'?: any; '@datatype'?: string; } interface Facts { [key: string]: RDFValue[]; } interface GeoJSONGeometry { type: string; coordinates: number[]; } interface GeoJSONFeature { type: string; geometry: GeoJSONGeometry; properties: { [key: string]: any }; } export class RDFToLiPD { private store: Store; private lipdCsvs: { [key: string]: any[][] } = {}; private graphurl: string; private namespace: string; private schema: any; private rschema: any; private allfacts: { [key: string]: Facts } = {}; /** * Constructor for RDFToLiPD class * @param store The RDF graph to convert */ constructor(store: Store) { this.store = store; this.graphurl = NSURL; this.namespace = NSURL + '/'; this.schema = { ...SCHEMA }; this.rschema = this.getSchemaReverseMap(); logger.debug('RDFToLiPD instance created'); } /** * Convert RDF graph to a LiPD file * @param dsname Dataset name * @param lipdfile Output LiPD file path * @returns The converted LiPD data */ public async convert(dsname: string, lipdfile: string): Promise<any> { const lipd = this.convertToJson(dsname); const tempDir = fs.mkdtempSync('rdf_to_lipd_'); const dsDir = path.join(tempDir, dsname); const dataDir = path.join(dsDir, 'data'); try { // Create data directory fs.mkdirSync(dataDir, { recursive: true }); // Create CSV files and metadata this.createCsvs(lipd, dataDir); fs.writeFileSync( path.join(dataDir, 'metadata.jsonld'), JSON.stringify(lipd, null, 4) ); // Create bagit files await this.createBagitFiles(dsDir); // Zip the directory await this.zipDirectory(dsDir, lipdfile); logger.debug('Successfully converted RDF to LiPD file: %s', lipdfile); return lipd; } catch (error) { logger.error('Error converting RDF to LiPD: %s', error instanceof Error ? error.message : String(error)); throw error; } finally { // Clean up temp directory fs.rmSync(tempDir, { recursive: true, force: true }); } } /** * Convert RDF graph to LiPD JSON format * @param dsname Dataset name * @returns The converted LiPD JSON data */ public convertToJson(dsname: string): any { this.schema = { ...SCHEMA }; this.rschema = this.getSchemaReverseMap(); this.allfacts = {}; this.indexFacts(this.namespace + dsname); const lipd = this._convertToLipd(this.namespace + dsname, "Dataset", "Dataset", {}); return this.postProcessing(lipd); } /** * Post-process the converted LiPD object * @param obj Object to process * @param parent Parent object * @returns Processed object */ private postProcessing(obj: any, parent: any = null): any { if (!obj || typeof obj !== 'object') { return obj; } if (!('@schema' in obj)) { return obj; } // Get schema for this object const schemaname = obj['@schema']; const tschema = this.schema[schemaname] || null; // Apply pre-processing functions if any if (tschema && '@toJson_pre' in tschema) { for (const func of tschema['@toJson_pre']) { const fn = (this as any)[func]; if (fn) { obj = fn.call(this, obj, parent); } } } // Process all properties recursively for (const [key, value] of Object.entries(obj)) { if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) { obj[key][i] = this.postProcessing(value[i], obj); } } else { obj[key] = this.postProcessing(value, obj); } } // Apply post-processing functions if any if (tschema && '@toJson' in tschema) { for (const func of tschema['@toJson']) { const fn = (this as any)[func]; if (fn) { obj = fn.call(this, obj, parent); } } } // Handle hasValues conversion to values if ('hasValues' in obj) { const valuestr = obj['hasValues']; obj['values'] = parseVariableValues(valuestr); delete obj['hasValues']; } // Clean up metadata fields delete obj['@id']; delete obj['@schema']; delete obj['@category']; if ('type' in obj) { delete obj['type']; } return obj; } /** * Get property details from schema * @param pname Property name * @param schema Schema object * @returns Property details */ private getPropertyDetails(pname: string, schema: any): PropertyDetails { const details: PropertyDetails = { name: pname }; if (schema && pname in schema) { for (const [key, value] of Object.entries(schema[pname])) { details[key] = value; } } return details; } /** * Get RDF property details from schema * @param pname Property name * @param fullkey Full property key * @param schema Schema object * @returns Property details */ private getRdfPropertyDetails(pname: string, fullkey: string, schema: any): PropertyDetails { const key = pname; pname = lcfirst(pname); const details: PropertyDetails = { name: pname }; // Check for full key in schema if (schema && fullkey in schema) { for (const [key, value] of Object.entries(schema[fullkey])) { details[key] = value; } } return details; } /** * Get schema reverse map * @returns Reverse schema map */ private getSchemaReverseMap(): any { const newschema: any = {}; for (const [schid, sch] of Object.entries(this.schema)) { const newsch: any = {}; for (const [prop, details] of Object.entries(sch as any)) { if (prop[0] === "@") { continue; } if ("skip_auto_convert_to_json" in (details as any)) { continue; } const pdetails = this.getPropertyDetails(prop, sch as any); const pname = pdetails.name; pdetails.name = prop; newsch[pname] = pdetails; if ("category" in pdetails) { const catpname = pname + "." + ucfirst(pdetails.category as string); newsch[catpname] = pdetails; } if ("schema" in pdetails) { const schpname = pname + "." + ucfirst(pdetails.schema as string); newsch[schpname] = pdetails; } } newschema[schid] = newsch; } return newschema; } /** * Extract local name from URL * @param url URL to extract from * @returns Local name */ private localName(url: string): string { return url.replace(/^.*[#/]/, ''); } /** * Convert RDF to LiPD format * @param id ID to convert * @param category Category of the item * @param schemaname Schema name * @param pagesdone Map of processed pages * @returns Converted LiPD object */ private convertToLipd(id: string, category: string, schemaname: string, pagesdone: Map<string, boolean>): any { if (pagesdone.has(id)) return null; pagesdone.set(id, true); const facts = this.allfacts[id]; if (!facts) return null; const obj: any = {}; const schema = this.schema[schemaname]; // Process each property in the facts for (const [pname, values] of Object.entries(facts)) { // Skip type property if (pname === 'type') continue; // Get property details const details = this.getRdfPropertyDetails(pname, pname, schema); const propname = details.name; // Skip blacklisted properties if (Array.isArray(REVERSE_BLACKLIST) && REVERSE_BLACKLIST.includes(propname)) continue; // Convert values const converted = []; for (const value of values) { if (value['@type'] === 'uri' && value['@id']) { const subid = value['@id']; const subobj = this.convertToLipd(subid, category, schemaname, pagesdone); if (subobj) converted.push(subobj); } else if (value['@value'] !== undefined) { converted.push(value['@value']); } } // Add to object if we have values if (converted.length > 0) { if (details.multiple) { obj[propname] = converted; } else { obj[propname] = converted[0]; } } } return obj; } /** * Order variables in a datatable * @param datatable Datatable object * @param parent Parent object * @returns Datatable object with ordered variables */ private orderVariables(datatable: DataTable, parent: any = null): any { datatable.variables = datatable.variables.sort((a: Variable, b: Variable) => (a.columnNumber ?? 0) - (b.columnNumber ?? 0)); console.log("orderVariables",datatable.variables); return datatable; } private changesToJson(change: Change, parent: any = null): any { let newChange: any = {} if (change.name) { newChange[change.name] = change.notes || [] return newChange; } return null; } /** * Convert location to GeoJSON format * @param geo Location object * @param parent Parent object * @returns GeoJSON object */ private locationToJson(geo: any, parent: any = null): GeoJSONFeature { const geojson: GeoJSONFeature = { type: 'Feature', geometry: { type: 'Point', coordinates: [0, 0, 0] }, properties: {} }; if ('coordinates' in geo) { const latlong = geo['coordinates'].split(','); geojson.geometry.coordinates = [ parseFloat(latlong[1]), parseFloat(latlong[0]), latlong.length > 2 ? parseFloat(latlong[2]) : 0 ]; } if ('long' in geo) { geojson.geometry.coordinates[0] = parseFloat(geo['long']); } if ('longitude' in geo) { geojson.geometry.coordinates[0] = parseFloat(geo['longitude']); } if ('lat' in geo) { geojson.geometry.coordinates[1] = parseFloat(geo['lat']); } if ('latitude' in geo) { geojson.geometry.coordinates[1] = parseFloat(geo['latitude']); } if ('alt' in geo && geo['alt'] !== 'NA') { geojson.geometry.coordinates[2] = parseFloat(geo['alt']); } if ('elevation' in geo && geo['elevation'] !== 'NA') { geojson.geometry.coordinates[2] = parseFloat(geo['elevation']); } for (const [prop, value] of Object.entries(geo)) { if (prop.startsWith('@')) continue; if (prop === 'locationType') { geojson.type = geo['locationType']; } else if (prop !== 'coordinates' && prop !== 'coordinatesFor') { if (!prop.match(/^(geo|wgs84):/)) { if (!['long', 'lat', 'alt'].includes(prop)) { geojson.properties[prop] = value; } } } } return geojson; } /** * Unarray column number * @param variable Variable object * @param parent Parent object * @returns Variable object with unarrayed number */ private unarrayColumnNumber(variable: any, parent: any = null): any { if (!variable) return variable; if ('number' in variable) { if (Array.isArray(variable['number']) && variable['number'].length === 1) { variable['number'] = variable['number'][0]; } if (typeof variable['number'] === 'string') { variable['number'] = JSON.parse(variable['number']); } } return variable; } /** * Extract table data from columns * @param table The table containing columns with data * @returns Array of row data */ private getTableData(table: Table): any[][] { const data: any[][] = []; if (!table.columns) return data; // Not including header row in csv // const headerRow = table.columns.map(col => col.variableName); // data.push(headerRow); // Get the maximum length of values const maxLength = Math.max(...table.columns.map(col => col.values?.length || 0)); // Add data rows for (let i = 0; i < maxLength; i++) { const row = table.columns.map(col => col.values[i] ?? null); data.push(row); } return data; } /** * Create CSV files from table data * @param lipd The LiPD data containing tables * @param dataDir Directory to write CSV files */ private createCsvs(lipd: LiPDData, dataDir: string): void { const csvs: { [key: string]: any[][] } = {}; const datakeys = ['paleoData', 'chronData']; for (const datakey of datakeys) { const data = lipd[datakey]; if (!data) continue; for (const item of data) { // Handle measurement tables if (item.measurementTable) { for (const table of item.measurementTable) { csvs[table.filename] = this.getTableData(table); } } // Handle model tables if (item.model) { for (const model of item.model) { // Handle ensemble tables if (model.ensembleTable) { for (const table of model.ensembleTable) { csvs[table.filename] = this.getTableData(table); } } // Handle summary tables if (model.summaryTable) { for (const table of model.summaryTable) { csvs[table.filename] = this.getTableData(table); } } // Handle distribution tables if (model.distributionTable) { for (const table of model.distributionTable) { csvs[table.filename] = this.getTableData(table); } } } } } } // Write CSV files for (const [csvname, csvdata] of Object.entries(csvs)) { const csvContent = csvdata.map(row => row.join(',')).join('\n'); fs.writeFileSync(path.join(dataDir, csvname), csvContent); } } /** * Create bagit files in the data directory * @param dataDir Directory to create bagit files in * @returns Promise that resolves when bagit files are created */ private createBagitFiles(dataDir: string): Promise<void> { const bagInfo = { 'Bag-Software-Agent': 'lipdjs', 'Bagging-Date': new Date().toISOString() }; return createBagitFiles(dataDir, bagInfo); } /** * Zip a directory into a LiPD file * @param dataDir Directory to zip * @param lipdfile Output LiPD file path * @returns Promise that resolves when the zip file is created */ private zipDirectory(dataDir: string, lipdfile: string): Promise<void> { return new Promise((resolve, reject) => { const zip = new AdmZip(); const addFilesToZip = (currentPath: string, relativePath: string = '') => { const files = fs.readdirSync(currentPath); for (const file of files) { const filePath = path.join(currentPath, file); const zipPath = path.join(relativePath, file); if (fs.statSync(filePath).isDirectory()) { addFilesToZip(filePath, zipPath); } else { zip.addLocalFile(filePath, path.dirname(zipPath)); } } }; addFilesToZip(dataDir); // Use the callback version of writeZip zip.writeZip(lipdfile, (error) => { if (error) { reject(error); } else { resolve(); } }); }); } /** * Get property values from query result * @param qres Query result containing predicate and object * @returns Object with property names and their values */ private _getPropValuesFromQueryResultPO(qres: Quad[]): Record<string, any[]> { const result: Record<string, any[]> = {}; for (const row of qres) { const pname = this.localName(row.predicate.id); if (!(pname in result)) { result[pname] = []; } const value: any = {}; if (row.object.termType === 'NamedNode') { value["@type"] = "uri"; value["@id"] = row.object.id; } else if (row.object.termType === 'Literal') { value["@type"] = "literal"; value["@value"] = row.object.value; value["@datatype"] = null; // FIXME: Add datatype } result[pname].push(value); } return result; } /** * Get facts for a specific ID * @param id The ID to query facts for * @returns Object containing all properties and values for the ID */ private _getFacts(id: string): Record<string, any[]> { const qres = this.store.getQuads(DF.namedNode(id), null, null, null); return this._getPropValuesFromQueryResultPO(qres); } /** * Get and index facts for an ID and all related resources * @param id The ID to index facts for */ private indexFacts(id: string): void { if (id in this.allfacts) { return; } const facts = this._getFacts(id); this.allfacts[id] = facts; for (const [pname, pfacts] of Object.entries(facts)) { for (const pfact of pfacts) { if (pfact["@type"] === "uri") { if (pname !== "type") { this.indexFacts(pfact["@id"]); } } } } } private _convertToLipd(id: string, category?: string, schemaname?: string, pagesdone: Record<string, any> = {}): any { if (id in this.allfacts) { const facts = this.allfacts[id]; if (id in pagesdone) { return pagesdone[id]; } const schema = schemaname && this.rschema[schemaname] ? this.rschema[schemaname] : null; if (schemaname && !category) { category = schemaname; } if ("type" in facts) { const cats = facts["type"]; for (const cat of cats) { if (cat["@type"] === "uri") { category = this.localName(cat["@id"] as any); break; } } } const obj: any = { "@id": id, "@category": category, "@schema": schemaname }; pagesdone[id] = obj; for (const [pname, pfacts] of Object.entries(facts)) { if (pname in REVERSE_BLACKLIST) { continue; } let prop = pname; prop = prop.replace(/\s/g, "_"); // Get a sample value page category, and use to make a property key let propkey = prop; for (const value of pfacts) { if (value["@type"] === "uri") { if (value["@id"] && value["@id"] in this.allfacts) { const pfact = this.allfacts[value["@id"]]; if ("type" in pfact) { const valcats = pfact["type"]; for (const valcat of valcats) { if (valcat["@type"] === "uri") { const valcatname = this.localName(valcat["@id"] as any); propkey = prop + "." + valcatname; break; } } } } } } const details = this.getRdfPropertyDetails(prop, propkey, schema); const name = details.name; const ptype = details.type || null; let cat = details.category || null; let sch = details.schema || null; if (cat && !sch) { sch = cat; } const toJson = details.toJson || null; let multiple = details.multiple || false; if (pfacts.length > 0) { if (multiple) { obj[name] = []; } for (const pfact of pfacts) { let val; if (pfact["@type"] === "uri") { val = this._convertToLipd(pfact["@id"] as any, cat, sch, pagesdone); } else { val = pfact["@value"]; } if (toJson) { val = (this as any)[toJson](val); } // If there is already a value present // - Then this need to be marked as "multiple" if (!multiple && name in obj && !Array.isArray(obj[name])) { multiple = true; obj[name] = [obj[name]]; } if (multiple) { obj[name].push(val); } else { obj[name] = val; } } } } return obj; } else { return id.replace(/_/g, " "); } } /** * Convert location to GeoJSON format * @param geo Location object * @param parent Parent object * @returns GeoJSON object */ private _location_to_json(geo: any, parent: any = null): GeoJSONFeature { const geojson: GeoJSONFeature = { type: "Feature", geometry: { type: "Point", coordinates: [0, 0, 0] }, properties: {} }; if ("coordinates" in geo) { const latlong = geo["coordinates"].split(","); geojson.geometry.coordinates = [ parseFloat(latlong[1]), parseFloat(latlong[0]), latlong.length > 2 ? parseFloat(latlong[2]) : 0 ]; } if ("long" in geo) { geojson.geometry.coordinates[0] = parseFloat(geo["long"]); } if ("longitude" in geo) { geojson.geometry.coordinates[0] = parseFloat(geo["longitude"]); } if ("lat" in geo) { geojson.geometry.coordinates[1] = parseFloat(geo["lat"]); } if ("latitude" in geo) { geojson.geometry.coordinates[1] = parseFloat(geo["latitude"]); } if ("alt" in geo && geo["alt"] !== "NA") { geojson.geometry.coordinates[2] = parseFloat(geo["alt"]); } if ("elevation" in geo && geo["elevation"] !== "NA") { geojson.geometry.coordinates[2] = parseFloat(geo["elevation"]); } for (const [prop, value] of Object.entries(geo)) { if (prop[0] === "@") { continue; } if (prop === "locationType") { geojson.type = geo["locationType"]; } else { if (prop === "coordinates" || prop === "coordinatesFor") { // Ignore } else if (/^(geo|wgs84):/.test(prop)) { // Ignore } else if (["long", "lat", "alt"].includes(prop)) { // Ignore } else { geojson.properties[prop] = value; } } } return geojson; } /** * Extract Google Spreadsheet key from URL * @param url Google Spreadsheet URL * @param parent Parent object * @returns Google Spreadsheet key */ private getGoogleSpreadsheetKey(url: string, parent: any = null): string { return url.replace("https://docs.google.com/spreadsheets/d/", ""); } /** * Remove foundInTable property * @param variable Variable object * @param parent Parent object * @returns Variable object without foundInTable */ private removeFoundInTable(variable: any, parent: any = null): any { if ("foundInTable" in variable) { delete variable["foundInTable"]; } return variable; } /** * Remove foundInDataset property * @param variable Variable object * @param parent Parent object * @returns Variable object without foundInDataset */ private removeFoundInDataset(variable: any, parent: any = null): any { if ("foundInDataset" in variable) { delete variable["foundInDataset"]; } return variable; } /** * Unwrap uncertainty values * @param variable Variable object * @param parent Parent object * @returns Variable object with unwrapped uncertainty */ private unwrapUncertainty(variable: any, parent: any = null): any { if ("hasUncertainty" in variable) { const unc = variable["hasUncertainty"]; if ("hasValue" in unc) { variable["uncertainty"] = parseFloat(unc["hasValue"]); delete unc["hasValue"]; } for (const [key, value] of Object.entries(unc)) { if (key[0] !== "@") { variable[key] = value; } } delete variable["hasUncertainty"]; } return variable; } /** * Unwrap integration time * @param interp Interpretation object * @param parent Parent object * @returns Interpretation object with unwrapped integration time */ private unwrapIntegrationTime(interp: any, parent: any = null): any { if ("integrationTime" in interp) { const intime = interp["integrationTime"]; if ("hasValue" in intime) { interp["integrationTime"] = parseFloat(intime["hasValue"]); delete intime["hasValue"]; } for (const [key, value] of Object.entries(intime)) { if (key[0] !== "@") { interp["integrationTime" + ucfirst(key)] = value; } } delete interp["hasIntegrationTime"]; } return interp; } /** * Collect variables by ID * @param item Item to process * @param arr Array of collected variables * @returns Updated array of collected variables */ private collectVariablesById(item: any, arr: { [key: string]: any }): { [key: string]: any } { if (typeof item !== 'object' || item === null) { return arr; } if ("@category" in item && "@id" in item && /Variable$/.test(item["@category"])) { arr[item["@id"]] = item; } else { for (const [key, value] of Object.entries(item)) { if (key[0] !== "@") { arr = this.collectVariablesById(item[key], arr); } } } return arr; } /** * Set archive type label * @param ds Dataset object * @param parent Parent object * @returns Dataset object with archive type label */ private setArchiveTypeLabel(ds: any, parent: any = null): any { if ("hasArchiveType" in ds) { if ("@id" in ds["hasArchiveType"]) { const id = ds["hasArchiveType"]["@id"]; if (RSYNONYMS && id in RSYNONYMS) { ds["archiveType"] = RSYNONYMS[id]; } else { ds["archiveType"] = ds["hasArchiveType"]["label"]; } } delete ds["hasArchiveType"]; } return ds; } /** * Set variable name from standard variable label * @param variable Variable object * @param parent Parent object * @returns Variable object with variable name */ private setVariableNameFromStandardVariableLabel(variable: any, parent: any = null): any { if ("hasStandardVariable" in variable) { if ("@id" in variable["hasStandardVariable"]) { const id = variable["hasStandardVariable"]["@id"]; if (RSYNONYMS && id in RSYNONYMS) { variable["variableName"] = RSYNONYMS[id]; } else { variable["variableName"] = variable["hasStandardVariable"]["label"]; } } delete variable["hasStandardVariable"]; } return variable; } /** * Set units label * @param variable Variable object * @param parent Parent object * @returns Variable object with units label */ private setUnitsLabel(variable: any, parent: any = null): any { if ("hasUnits" in variable) { if ("@id" in variable["hasUnits"]) { const id = variable["hasUnits"]["@id"]; if (RSYNONYMS && id in RSYNONYMS) { variable["units"] = RSYNONYMS[id]; } else { variable["units"] = variable["hasUnits"]["label"]; } } delete variable["hasUnits"]; } return variable; } /** * Set proxy label * @param variable Variable object * @param parent Parent object * @returns Variable object with proxy label */ private setProxyLabel(variable: any, parent: any = null): any { if ("hasProxy" in variable) { if ("@id" in variable["hasProxy"]) { const id = variable["hasProxy"]["@id"]; if (RSYNONYMS && id in RSYNONYMS) { variable["proxy"] = RSYNONYMS[id]; } else { variable["proxy"] = variable["hasProxy"]["label"]; } } delete variable["hasProxy"]; } return variable; } /** * Set proxy general label * @param variable Variable object * @param parent Parent object * @returns Variable object with proxy general label */ private setProxyGeneralLabel(variable: any, parent: any = null): any { if ("hasProxyGeneral" in variable) { if ("@id" in variable["hasProxyGeneral"]) { const id = variable["hasProxyGeneral"]["@id"]; if (RSYNONYMS && id in RSYNONYMS) { variable["proxyGeneral"] = RSYNONYMS[id]; } else { variable["proxyGeneral"] = variable["hasProxyGeneral"]["label"]; } } delete variable["hasProxyGeneral"]; } return variable; } /** * Set interpretation variable label * @param interp Interpretation object * @param parent Parent object * @returns Interpretation object with variable label */ private setInterpretationVariableLabel(interp: any, parent: any = null): any { if ("hasVariable" in interp) { if ("@id" in interp["hasVariable"]) { const id = interp["hasVariable"]["@id"]; if (RSYNONYMS && id in RSYNONYMS) { interp["variable"] = RSYNONYMS[id]; } else { interp["variable"] = interp["hasVariable"]["label"]; } } delete interp["hasVariable"]; } return interp; } /** * Set seasonality labels * @param interp Interpretation object * @param parent Parent object * @returns Interpretation object with seasonality labels */ private setSeasonalityLabels(interp: any, parent: any = null): any { const convs: { [key: string]: string } = { "hasSeasonality": "seasonality", "hasSeasonalityGeneral": "seasonalityGeneral", "hasSeasonalityOriginal": "seasonalityOriginal" }; for (const [pid, nid] of Object.entries(convs)) { if (pid in interp) { if ("@id" in interp[pid]) { const id = interp[pid]["@id"]; if (RSYNONYMS && id in RSYNONYMS) { interp[nid] = RSYNONYMS[id]; } else { interp[nid] = interp[pid]["label"]; } } delete interp[pid]; } } return interp; } /** * Create publication identifier * @param pub Publication object * @param parent Parent object * @returns Publication object with identifier */ private createPublicationIdentifier(pub: any, parent: any = null): any { const identifiers = []; if ("hasDOI" in pub) { const identifier: { [key: string]: string } = { "type": "doi", "id": pub["hasDOI"] }; if ("link" in pub) { for (const link of Object.values(pub["link"])) { if (typeof link === 'string' && /dx\.doi\.org/.test(link)) { identifier["url"] = link; } } delete pub["link"]; } delete pub["hasDOI"]; identifiers.push(identifier); } pub["identifier"] = identifiers; return pub; } /** * Convert values to array * @param resolution Resolution object * @param parent Parent object * @returns Array of values */ private valuesToArray(resolution: any, parent: any = null): any { if ("values" in resolution) { return resolution["values"].split(","); } return resolution; } /** * Unarray column number * @param variable Variable object * @param parent Parent object * @returns Variable object with unarrayed number */ private unArrayColumnNumber(variable: any, parent: any = null): any { if (!variable) return variable; if ("number" in variable) { if (Array.isArray(variable["number"]) && variable["number"].length === 1) { variable["number"] = variable["number"][0]; } if (typeof variable["number"] === 'string') { variable["number"] = JSON.parse(variable["number"]); } } return variable; } /** * Extract variable values * @param variable Variable object * @param parent Parent object * @returns Variable object with extracted values */ private extractVariableValues(variable: any, parent: any = null): any { if ("hasValues" in variable) { const valuestr = variable["hasValues"]; const values = parseVariableValues(valuestr); if (typeof values === 'object' && values !== null && "base64_zlib" in values) { variable["hasValues"] = this.unzipString(values["base64_zlib"]); } else { variable["hasValues"] = values; } } return variable; } /** * Unzip a base64 encoded and zlib compressed string * @param str The base64 encoded and zlib compressed string * @returns The uncompressed string */ private unzipString(str: string): string { try { let binary: Uint8Array; // Prefer Buffer in Node, fall back to atob in browsers if (typeof Buffer !== 'undefined' && Buffer.from) { binary = Uint8Array.from(Buffer.from(str, 'base64')); } else { // @ts-ignore atob may be available only in browser const decoded = atob(str); binary = Uint8Array.from(decoded, c => c.charCodeAt(0)); } const text = new TextDecoder().decode(pako.inflate(binary)); return text; } catch (e) { logger.error('Could not decode/unzip the contents', e); throw e; } } }