lipdjs
Version:
A JavaScript library for reading and writing LiPD (Linked Paleo Data) files
1,220 lines (1,078 loc) • 40.5 kB
text/typescript
/**
* 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;
}
}
}