dasf-web
Version:
Web frontend components for the data analytics software framework (DASF)
288 lines (233 loc) • 9.75 kB
text/typescript
import { DataArchive, QueryProperty, Dataset, EnsembleItem } from './DataArchive';
import axios from 'axios';
import * as xmljs from 'xml-js';
export class EsgfDataset extends EnsembleItem implements Dataset {
public static readonly DEFAULT_HIST_DATES: [Date, Date] = [new Date("1950-01-01"), new Date("2005-12-31")];
public static readonly DEFAULT_RCP_DATES: [Date, Date] = [new Date("2006-01-01"), new Date("2100-12-31")];
private title: string;
private url: string;
private startDate: Date;
private endDate: Date;
// "cordex.output.EUR-11.MPI-CSC.MPI-M-MPI-ESM-LR.historical.r1i1p1.REMO2009.v1.day.pr"
public readonly project: string;
public readonly product: string;
public readonly domain: string;
public readonly institute: string;
public readonly drivingModel: string;
public readonly experiment: string;
public readonly ensemble: string;
public readonly regionalModel: string;
public readonly downscaling: string;
public readonly timeFrequency: string;
public readonly variable: string;
constructor(title: string, url: string, start?: Date, end?: Date) {
super();
this.title = title;
this.url = url;
const titleSplit = title.split('.');
this.project = titleSplit[0];
this.product = titleSplit[1];
this.domain = titleSplit[2];
this.institute = titleSplit[3];
this.drivingModel = titleSplit[4];
this.experiment = titleSplit[5];
this.ensemble = titleSplit[6];
this.regionalModel = titleSplit[7];
this.downscaling = titleSplit[8];
this.timeFrequency = titleSplit[9];
this.variable = titleSplit[10];
if(start == undefined || start == null) {
this.startDate = this.experiment == 'historical' ? EsgfDataset.DEFAULT_HIST_DATES[0] : EsgfDataset.DEFAULT_RCP_DATES[0];
} else {
this.startDate = start;
}
if(end == undefined || end == null) {
this.endDate = this.experiment == 'historical' ? EsgfDataset.DEFAULT_HIST_DATES[1] : EsgfDataset.DEFAULT_RCP_DATES[1];
} else {
this.endDate = end;
}
}
public getName(): string {
return this.title;
}
public getUrl(): string {
return this.url;
}
public getStartDate(): Date {
return this.startDate;
}
public getEndDate(): Date {
return this.endDate;
}
public getLabel(): string {
return this.experiment;
}
public getTitle(): string {
return this.url;
}
}
export class EsgfDataArchive implements DataArchive {
private static readonly QUERRIED_FIELDS = ['id', 'title', 'type', 'datetime_start', 'datetime_stop'];
constructor(private dataNode: string, private propertyDefaults: Map<string, string[]> = new Map()) {
}
public async getQueryProperties(supportedProperties: string[] = ['*']): Promise<QueryProperty[]> {
let props: QueryProperty[] = [];
// fetch supported properties and their values via facet query
let facetQueryResponse = await axios.get('http://' + this.dataNode + '/esg-search/search?limit=0&facets=' + supportedProperties.join(','));
let facetQueryData = xmljs.xml2js(facetQueryResponse.data, { compact: true });
let response = facetQueryData['response'];
if (response) {
// get 'facet_count' attribute
let facets = this.getArrayAttribute(response, 'facet_counts');
// get 'facet_fields' attribute
facets = this.getArrayAttribute(facets, 'facet_fields');
// get facet field array
let facetFields: any[] = this.getLstArray(facets);
// iterate over all facets and values
if (facetFields) {
for (let facet of facetFields) {
props.push(this.compileFullQueryProperty(facet));
}
}
this.setDefaults(props);
}
return props;
}
public async queryDatasets(props: QueryProperty[]): Promise<Dataset[]> {
// esg-search/search?
let q = 'http://' + this.dataNode + '/esg-search/search?limit=1000&' + this.toQueryString(props) + "&fields=" + EsgfDataArchive.QUERRIED_FIELDS.join(',');
// console.log(q);
let datasetQueryResponse = await axios.get(q);
// console.log(datasetQueryResponse.data);
let dsQueryData = xmljs.xml2js(datasetQueryResponse.data, { compact: true });
let doc = dsQueryData['response']['result'];
let numResults = this.extractAttribute(doc, 'numFound');
if (numResults == 0) {
return [];
}
// there are results - continue
doc = doc['doc'];
this.checkFormat(doc, "Missing 'doc' attribute");
let datasets: Dataset[] = [];
for (let datasetEntry of doc) {
datasets.push(this.compileDataset(datasetEntry));
}
// console.log(doc);
return datasets;
}
private compileDataset(datasetEntry: any): EsgfDataset {
// extract name
let str = datasetEntry['str'];
this.checkFormat(str, 'Invalid dataset format');
if (!Array.isArray(str)) {
str = [str];
}
let id = 'Unknown';
let title = 'Unknown';
for (let strItem of str) {
let fieldName = this.extractAttribute(strItem, 'name');
switch (fieldName) {
case 'title':
title = strItem['_text'];
break;
case 'id':
id = strItem['_text']
break;
default:
console.warn('[EsgfDataArchive] ignored dataset string field: ' + fieldName);
}
}
this.checkFormat(id, 'invalid dataset format');
this.checkFormat(title, 'invalid dataset format');
// extracting the url from the dataset meta data gives us a thredds catalog url which is deprecated
// instead we build our own url based on the solr rest api that will give us the dataset file listing
const url = 'http://' + this.dataNode + '/esg-search/search?type=File&limit=1000&format=application%2Fsolr%2Bjson&dataset_id=' + id;
// e.g. https://esgf-data.dkrz.de/esg-search/search?type=File&dataset_id=cordex-reklies.output.EUR-11.GERICS.CCCma-CanESM2.historical.r1i1p1.REMO2015.v1.day.pr.v20170329|esgf1.dkrz.de&format=application%2Fsolr%2Bjson
// extract date ranges
let start: Date | undefined = undefined;
let end: Date | undefined = undefined;
const dates = datasetEntry['date'];
if(dates != undefined && dates != null && Array.isArray(dates)) {
for(let date of dates) {
let fieldName = this.extractAttribute(date, 'name');
switch(fieldName) {
case 'datetime_start':
start = new Date(date['_text']);
break;
case 'datetime_stop':
end = new Date(date['_text']);
break;
default:
console.warn('ignored date field: ' + fieldName);
}
}
}
console.log([title, url]);
// compile and return dataset object
return new EsgfDataset(title, url, start, end);
}
private compileFullQueryProperty(facet: any): QueryProperty {
let p = new QueryProperty(this.extractAttribute(facet, 'name'));
let values = facet['int'];
if (values) {
for (let value of values) {
p.addValue(this.extractAttribute(value, 'name'));
}
}
return p;
}
private extractAttribute(element: any, attributeName: string) {
let name = element['_attributes'][attributeName];
this.checkFormat(name, 'No ' + attributeName + ' attribute found');
return name;
}
private checkFormat(element: any, errmsg: string) {
if (!element) {
throw new Error('Unsupported ESGF facet or dataset query response received: ' + errmsg);
}
}
private getArrayAttribute(lst: any, name: string): any {
// check for lst array
if (!Array.isArray(lst)) {
lst = this.getLstArray(lst);
}
let element = this.getLstAttribute(lst, name);
this.checkFormat(element, name + " element not found");
return element;
}
private getLstArray(obj: any): any[] {
let array: any[] = obj['lst'];
this.checkFormat(array, 'lst element not found');
return array;
}
private getLstAttribute(lst: any, name: string): any {
if (lst) {
// find element with name attribute
for (let e of lst) {
if (this.extractAttribute(e, 'name') === name) {
return e;
}
}
}
}
private toQueryString(props: QueryProperty[]): string {
let facets: string[] = [];
for (let qp of props) {
if (qp.hasSelected()) {
for (let item of qp.selected) {
facets.push(qp.name + "=" + item);
}
}
}
return facets.join("&");
}
private setDefaults(props: QueryProperty[]): void {
for (let p of props) {
if (this.propertyDefaults.has(p.name)) {
for (let selected of this.propertyDefaults.get(p.name)) {
p.addSelected(selected);
}
}
}
}
}