const TapDigit = require("./tapdigit");
const Parameter = require("./parameter");
const BuilderNode = require('./node');
/**
* This converts a mathematical formula into a openEO process for you.
*
* Operators: - (subtract), + (add), / (divide), * (multiply), ^ (power)
*
* It supports all mathematical functions (i.e. expects a number and returns a number) the back-end implements, e.g. `sqrt(x)`.
* For namespaced processes, use for example `process@namespace(x)` - EXPERIMENTAL!
*
* Only available if a builder is specified in the constructor:
* You can refer to output from processes with a leading `#`, e.g. `#loadco1` if the node to refer to has the key `loadco1`.
*
* Only available if a parent node is set via `setNode()`:
* Parameters can be accessed simply by name.
* If the first parameter is a (labeled) array, the value for a specific index or label can be accessed by typing the numeric index or textual label with a `$` in front, for example `$B1` for the label `B1` or `$0` for the first element in the array. Numeric labels are not supported.
* You can access subsequent parameters by adding additional `$` at the beginning, e.g. `$$0` to access the first element of an array in the second parameter, `$$$0` for the same in the third parameter etc.
*
* An example that computes an EVI (assuming the labels for the bands are `NIR`, `RED` and `BLUE`): `2.5 * ($NIR - $RED) / (1 + $NIR + 6 * $RED + (-7.5 * $BLUE))`
*/
class Formula {
/**
* Creates a math formula object.
*
* @param {string} formula - A mathematical formula to parse.y
*/
constructor(formula) {
let parser = new TapDigit.Parser();
/**
* @type {object.<string, *>}
*/
this.tree = parser.parse(formula);
/**
* @type {Builder | null}
*/
this.builder = null;
}
/**
* The builder instance to use.
*
* @param {Builder} builder - The builder instance to add the formula to.
*/
setBuilder(builder) {
this.builder = builder;
}
/**
* Generates the processes for the formula specified in the constructor.
*
* Returns the last node that computes the result.
*
* @param {boolean} setResultNode - Set the `result` flag to `true`.
* @returns {BuilderNode}
* @throws {Error}
*/
generate(setResultNode = true) {
let finalNode = this.parseTree(this.tree);
if (!(finalNode instanceof BuilderNode)) {
throw new Error('Invalid formula specified.');
}
// Set result node
if (setResultNode) {
finalNode.result = true;
}
return finalNode;
}
/**
* Walks through the tree generated by the TapDigit parser and generates process nodes.
*
* @protected
* @param {object.<string, *>} tree
* @returns {object.<string, *>}
* @throws {Error}
*/
parseTree(tree) {
let key = Object.keys(tree)[0]; // There's never more than one property so no loop required
switch(key) {
case 'Number':
return parseFloat(tree.Number);
case 'Identifier':
return this.getRef(tree.Identifier);
case 'Expression':
return this.parseTree(tree.Expression);
case 'FunctionCall': {
let args = [];
for(let i in tree.FunctionCall.args) {
args.push(this.parseTree(tree.FunctionCall.args[i]));
}
return this.builder.process(tree.FunctionCall.name, args);
}
case 'Binary':
return this.addOperatorProcess(
tree.Binary.operator,
this.parseTree(tree.Binary.left),
this.parseTree(tree.Binary.right)
);
case 'Unary': {
let val = this.parseTree(tree.Unary.expression);
if (tree.Unary.operator === '-') {
if (typeof val === 'number') {
return -val;
}
else {
return this.addOperatorProcess('*', -1, val);
}
}
else {
return val;
}
}
default:
throw new Error('Operation ' + key + ' not supported.');
}
}
/**
* Gets the reference for a value, e.g. from_node or from_parameter.
*
* @protected
* @param {*} value
* @returns {*}
*/
getRef(value) {
// Convert native data types
if (value === 'true') {
return true;
}
else if (value === 'false') {
return false;
}
else if (value === 'null') {
return null;
}
// Output of a process
if (typeof value === 'string' && value.startsWith('#')) {
let nodeId = value.substring(1);
if (nodeId in this.builder.nodes) {
return { from_node: nodeId };
}
}
let callbackParams = this.builder.getParentCallbackParameters();
// Array labels / indices
if (typeof value === 'string' && callbackParams.length > 0) {
let prefix = value.match(/^\$+/);
let count = prefix ? prefix[0].length : 0;
if (count > 0 && callbackParams.length >= count) {
let ref = value.substring(count);
return callbackParams[count-1][ref];
}
}
// Everything else is a parameter
let parameter = new Parameter(value);
// Add new parameter if it doesn't exist
this.builder.addParameter(parameter);
return parameter;
}
/**
* Adds a process node for an operator like +, -, *, / etc.
*
* @param {string} operator - The operator.
* @param {number|object.<string, *>} left - The left part for the operator.
* @param {number|object.<string, *>} right - The right part for the operator.
* @returns {BuilderNode}
* @throws {Error}
*/
addOperatorProcess(operator, left, right) {
let processName = Formula.operatorMapping[operator];
let process = this.builder.spec(processName);
if (processName && process) {
let args = {};
if (!Array.isArray(process.parameters) || process.parameters.length < 2) {
throw new Error("Process for operator " + operator + " must have at least two parameters");
}
args[process.parameters[0].name || 'x'] = left;
args[process.parameters[1].name || 'y'] = right;
return this.builder.process(processName, args);
}
else {
throw new Error('Operator ' + operator + ' not supported');
}
}
}
/**
* List of supported operators.
*
* All operators must have the parameters be name x and y.
*
* The key is the mathematical operator, the value is the process identifier.
*
* @type {object.<string, string>}
*/
Formula.operatorMapping = {
"-": "subtract",
"+": "add",
"/": "divide",
"*": "multiply",
"^": "power"
};
module.exports = Formula;