/**
* @module meteoJS/modelviewer/resources
*/
import addEventFunctions from '../Events.js';
import Image from './resource/Image.js';
import VariableCollection from './VariableCollection.js';
import Node from './Node.js';
/**
* Triggered on adding and removing Resource-Objects.
*
* @event module:meteoJS/modelviewer/resources#change:resources
* @type {Object}
* @property {module:meteoJS/modelviewer/resource.Resource} [addedResources] -
* Added resources.
* @property {module:meteoJS/modelviewer/resource.Resource} [removedResources] -
* Removed resources.
*/
/**
* Options for constructor.
*
* @typedef {Object} module:meteoJS/modelviewer/resources~options
* @param {module:meteoJS/modelviewer/node.Node} topNode - Top level node.
* @param {Set<module:meteoJS/modelviewer/variableCollection.VariableCollection>}
* timesVariableCollections - These collections group the top part of the
* hierarchy. For NWP, this contains typically the model and the run
* collection.
*/
/**
* @classdesc Linchpin of the modelviewer. In this class every available
* resource is registered. Additionally requests about data per Variable can
* be performed, like all available run times of a model or all available
* fields of model, etc. The hierarchy via
* {@link module:meteoJS/modelviewer/node.Node|Node}
* has to be defined before the construction of Resources.
*
* @fires module:meteoJS/modelviewer/resources#change:resources
*/
export class Resources {
constructor({ topNode,
timesVariableCollections = [] } = {}) {
/**
* @type module:meteoJS/modelviewer/variableCollection.VariableCollection
* @private
*/
this._topNode = topNode;
/**
* @type Map<module:meteoJS/modelviewer/node.Node,
* Set<module:meteoJS/modelviewer/variable.Variable>>
* @private
*/
this._availableVariablesMap = new Map();
/**
* @type Set<module:meteoJS/modelviewer/variableCollection.VariableCollection>
* @private
*/
this._timesVariableCollections = timesVariableCollections;
}
/**
* VariableCollectionNode that stand on the top of the hierarchy.
*
* @type module:meteoJS/modelviewer/variableCollection.VariableCollection
* @readonly
*/
get topNode() {
return this._topNode;
}
/**
* @type module:meteoJS/modelviewer/variableCollection.VariableCollection[]
* @readonly
*/
get variableCollections() {
let pushChildCollections;
pushChildCollections = node => {
node.children.forEach(n => {
result.push(n.variableCollection);
pushChildCollections(n);
});
};
let result = [this.topNode.variableCollection];
pushChildCollections(this.topNode);
return result;
}
/**
* Map of nodes and their variables (contained in the variableCollection of
* the node). For each variable exists at least one resource in this
* Resources-object that is defined by this variable.
*
* @type Map<module:meteoJS/modelviewer/node.Node,
* Set<module:meteoJS/modelviewer/variable.Variable>>
* @readonly
*/
get availableVariablesMap() {
return this._availableVariablesMap;
}
/**
* Append resources.
*
* @param {...module:meteoJS/modelviewer/resource.Resource} resources
* Available resources.
* @returns {module:meteoJS/modelviewer/resources.Resources} This.
* @fires module:meteoJS/modelviewer/resources#change:resources
*/
append(...resources) {
let addedResources = [];
resources.forEach(resource => {
let topNode = this._getTopNodeOfResourceDefinition(resource, this.topNode);
if (topNode !== undefined) {
let node = this._getTopMostChildWithAllVariables(new Set(resource.variables), topNode, true);
if (node !== undefined) {
let addedCount = node.append(resource);
if (addedCount > 0) {
addedResources.push(resource);
this._addAvailableVariablesMapByResource(resource);
}
}
}
});
if (addedResources.length > 0) {
// Debounce firing
if (INTERNAL_CHANGE_RESOURCES.timeoutId)
clearTimeout(INTERNAL_CHANGE_RESOURCES.timeoutId);
INTERNAL_CHANGE_RESOURCES.addedResources.push(...addedResources);
INTERNAL_CHANGE_RESOURCES.timeoutId = setTimeout(() => {
this.trigger('change:resources', {
addedResources: INTERNAL_CHANGE_RESOURCES.addedResources
});
INTERNAL_CHANGE_RESOURCES.addedResources = [];
}, 100);
}
return this;
}
/**
* Adds variables of a resource to _availableVariablesMap.
*
* @param {module:meteoJS/modelviewer/resource.Resource} resource - Resource.
* @private
*/
_addAvailableVariablesMapByResource(resource) {
resource.variables.forEach(variable => {
if (variable.variableCollection.node === undefined)
return;
if (!this._availableVariablesMap.has(variable.variableCollection.node))
this._availableVariablesMap.set(variable.variableCollection.node, new Set());
this._availableVariablesMap.get(variable.variableCollection.node).add(variable);
});
}
/**
* Removes resources.
*
* @param {...module:meteoJS/modelviewer/resource.Resource} resources
* Resources.
* @returns {module:meteoJS/modelviewer/resources.Resources} This.
* @fires module:meteoJS/modelviewer/resources#change:resources
*/
remove(...resources) {
let removedResources = [];
let removedNodeResourcesMap = new Map();
resources.forEach(resource => {
let topNode = this._getTopNodeOfResourceDefinition(resource, this.topNode);
if (topNode !== undefined) {
let node = this._getTopMostChildWithAllVariables(new Set(resource.variables), topNode, true);
if (node !== undefined) {
let removedCount = node.remove(resource);
if (removedCount > 0) {
removedResources.push(resource);
if (!removedNodeResourcesMap.has(node))
removedNodeResourcesMap.set(node, new Set());
removedNodeResourcesMap.get(node).add(resource);
}
}
}
});
if (removedNodeResourcesMap.size > 0)
this._removeAvailableVariablesMapByResources(removedNodeResourcesMap);
if (removedResources.length > 0)
this.trigger('change:resources', { removedResources });
return this;
}
/**
* Removes variables from _availableVariablesMap.
* Prerequisite: The resources have already to be removed of the nodes.
*
* @param {Map<module:meteoJS/modelviewer/node.Node,
* Set<module:meteoJS/modelviewer/resource.Resource>>}
* removedNodeResourcesMap - Map of Nodes with their removed Resources.
* @private
*/
_removeAvailableVariablesMapByResources(removedNodeResourcesMap) {
let fullCheckVariables = new Set();
for (let [node, resourcesSet] of removedNodeResourcesMap.entries()) {
let variables = new Set();
for (let resource of resourcesSet)
resource.variables.forEach(variable => variables.add(variable));
for (let variable of variables)
if (!node.hasResourcesByVariables(variable))
fullCheckVariables.add(variable);
}
for (let variable of fullCheckVariables) {
let node = this.getNodeByVariableCollection(variable.variableCollection);
if (!this._hasResourcesOfNodeChildren(node, [ variable ]))
if (this._availableVariablesMap.has(node))
this._availableVariablesMap.get(node).delete(variable);
}
}
/**
* Returns a node of the hierarchy, so that all parents and itself contain
* all the passed variables. The returned node is the most top in hierarchy
* as possible. If no node is found, an empty node object is returned.
*
* @param {...module:meteoJS/modelviewer/variable.Variable} variables
* Variables.
* @returns {module:meteoJS/modelviewer/node.Node} - Node.
*/
getTopMostNodeWithAllVariables(...variables) {
let result =
this._getTopMostChildWithAllVariables(new Set(variables), this.topNode, true);
return (result === undefined) ? new Node(new VariableCollection()) : result;
}
/**
* Returns first node in hierarchy that contains a VariableCollection which
* is part of the definition of the passed resource.
*
* @param {module:meteoJS/modelviewer/resource.Resource} resource
* Resource.
* @param {module:meteoJS/modelviewer/node.Node} node
* Search from 'node' and all the children.
* @returns {undefined|module:meteoJS/modelviewer/node.Node}
* Node or undefined if no node is found.
* @private
*/
_getTopNodeOfResourceDefinition(resource, node) {
if (resource.isDefinedByVariableOf(node.variableCollection))
return node;
let result = undefined;
node.children.forEach(childNode => {
if (result !== undefined)
result = this._getTopNodeOfResourceDefinition(resource, childNode);
});
return result;
}
/**
* Returns top most node for which on the way down (beginning from node)
* all variables are contained by the VariableCollections of the travelled
* nodes.
*
* @param {Set<module:meteoJS/modelviewer/variable.Variable>} variables
* Variables which have still to be found.
* @param {module:meteoJS/modelviewer/node.Node} node - Node.
* @param {boolean} bubbleDown - .
* @returns {undefined|module:meteoJS/modelviewer/node.Node} Child node.
*/
_getTopMostChildWithAllVariables(variables, node, bubbleDown) {
let isVariableContained = false;
node.variableCollection.variables.forEach(variable => {
if (variables.has(variable)) {
isVariableContained = true;
variables.delete(variable);
}
});
if (variables.size == 0)
return node;
else if (node.children.length == 0)
return undefined;
else if (!isVariableContained &&
!bubbleDown)
return undefined;
let result = undefined;
node.children.forEach(childNode => {
if (result === undefined)
result = this._getTopMostChildWithAllVariables(variables, childNode, bubbleDown);
});
return result;
}
/**
* Returns node which contains the passed variableCollection
*
* @param {module:meteoJS/modelviewer/variableCollection.VariableCollection}
* variableCollection
* VariableCollection.
* @returns {module:meteoJS/modelviewer/node.Node} Node.
*/
getNodeByVariableCollection(variableCollection) {
return (variableCollection.node === undefined)
? new Node(new VariableCollection())
: variableCollection.node;
}
/**
* Returns node which contains the variableCollection with the passed Id.
*
* @param {mixed} id - Id.
* @returns {module:meteoJS/modelviewer/node.Node} Node.
*/
getNodeByVariableCollectionId(id) {
let result = this._getNodeByVariableCollection(a => id == a.id);
return (result === undefined) ? new Node(new VariableCollection()) : result;
}
/**
* Returns node which contains the passed variableCollection.
*
* @param {Function} compareFunc - Argument is a VariableCollection-object.
* @returns {undefined|module:meteoJS/modelviewer/node.Node} Node.
* @private
*/
_getNodeByVariableCollection(compareFunc) {
return (compareFunc(this.topNode.variableCollection))
? this.topNode
: this._findChildNodeByVariableCollection(compareFunc, this.topNode);
}
/**
* Returns a VariableCollection with passed variableCollection of
* node's children.
*
* @param {Function} compareFunc - Argument is a VariableCollection-object.
* @param {module:meteoJS/modelviewer/node.Node} parentNode
* Search recursively in this node's children.
* @returns {undefined|module:meteoJS/modelviewer/node.Node} Node.
* @private
*/
_findChildNodeByVariableCollection(compareFunc, parentNode) {
let result;
parentNode.children.forEach(n => {
if (result === undefined &&
compareFunc(n.variableCollection)) {
result = n;
return;
}
if (result === undefined &&
n.children.length > 0)
result = this._findChildNodeByVariableCollection(compareFunc, n);
});
return result;
}
/**
* Appends an Image-resource. Alias for append(new Image(…)).
*
* @see module:meteoJS/modelviewer/resource/image.Image
* @returns {module:meteoJS/modelviewer/resources.Resources} This.
*/
appendImage({ variables, datetime, run, offset, url }) {
this.append(new Image({
variables,
datetime,
run,
offset,
url
}));
return this;
}
/**
* Returns the {@link module:meteoJS/modelviewer/variable.Variable|Variable}-Objects
* from the {@link module:meteoJS/modelviewer/variableCollection.VariableCollection|collection}
* with content. With this method
* you can deactive for example the other variables, so the user can't select
* a variable with no resource.
*
* This means the method returns a subset from the passed collection. For
* these Variable-Objects at least one resource is available (in the
* {@link meteoJS/modelviewer/variableCollection.VariableCollection#node|node}
* of the collection or one of its children). The resources are defined by
* on of these Variable-Objects. If you pass 'variables', you can
* additionally constrain the returned variables. E.g. you look for all
* run's with resources of a model, you pass the model's Variable-Object.
*
* @param {module:meteoJS/modelviewer/variableCollection.VariableCollection}
* variableCollection
* Return Variables of this VariableCollection.
* @param {Object} options - Options.
* @param {module:meteoJS/modelviewer/variable.Variable[]} [options.variables]
* Only
* @returns {Set<module:meteoJS/modelviewer/variable.Variable>}
* Available variables.
*/
getAvailableVariables(variableCollection, { variables = [] } = {}) {
const result = new Set();
const _checkVariableInNode = (variable, node) => {
if (node.resources.length > 0) {
for (const resource of node.resources) {
if (resource.isDefinedBy(false, variable, ...variables)) {
result.add(variable);
return true;
}
}
return false;
}
for (const n of node.children) {
if (_checkVariableInNode(variable, n))
return true;
}
return false;
};
Array.from(variableCollection).forEach(variable => {
_checkVariableInNode(variable, variableCollection.node);
});
return result;
}
/**
* Traverses all child nodes of the passed node and looks for a resource
* that is defined by all of the passed variables. If one child node contains
* such a resource, true is returned.
*
* @param {module:meteoJS/modelviewer/node.Node} node - Node.
* @param {module:meteoJS/modelviewer/variable.Variable[]} variables
* Look for resources defined by these variables.
* @param {Set} [traversedNode] - Internal Set.
* @returns {Boolean} A resource is contained in the child nodes.
* @private
*/
_hasResourcesOfNodeChildren(node, variables, traversedNode = new Set()) {
for (const n of node.children) {
if (traversedNode.has(n))
continue;
traversedNode.add(n);
if (n.hasResourcesByVariables(...variables))
return true;
if (this._hasResourcesOfNodeChildren(n, variables, traversedNode))
return true;
}
}
/**
* Returns all times with at least one resource. The resources are defined
* by the passed variable. If exact=true, then the resources are exactly
* defined by the variables.
* With NWP models, you could get all times from a model-run with at least
* one resource when you pass the model and run variable object.
* If you want to know all available times for a set of variables (e.g. all
* available image-plots for the EU-region, from the temperature in a specific
* level), then pass exact=true and all the variables.
*
* @param {Object} [options] - Options.
* @param {module:meteoJS/modelviewer/variable.Variable[]} [options.variables]
* Variables.
* @param {boolean} [options.exact=false] - When true, only resources which
* are exactly defined by the passed variables are taken into account.
* @returns {Date[]} - Sorted upwardly.
*/
getTimesByVariables({
variables = [],
exact = false
} = {}) {
const node = this._getTopMostChildWithAllVariables(
new Set(variables),
this.topNode,
true);
if (node === undefined)
return [];
const times = new Set();
const collectTimes = node => {
node.getResourcesByVariables(exact, ...variables).forEach(resource => {
if (resource.datetime !== undefined)
times.add(resource.datetime.valueOf());
});
node.children.forEach(n => collectTimes(n));
};
collectTimes(node);
return [...times].sort().map(t => new Date(t));
}
}
addEventFunctions(Resources.prototype);
export default Resources;
/**
* @private
*/
const INTERNAL_CHANGE_RESOURCES = {
timeoutId: undefined,
addedResources: []
};