diff --git a/app/javascript/projects/modelling/components/expression_component.ts b/app/javascript/projects/modelling/components/expression_component.ts index 2ef01181..9d3c5d34 100644 --- a/app/javascript/projects/modelling/components/expression_component.ts +++ b/app/javascript/projects/modelling/components/expression_component.ts @@ -9,6 +9,32 @@ import { PreviewControl } from "../controls/preview" import { isEqual } from "lodash" import { ProjectProperties } from "." import { createXYZ } from "ol/tilegrid" +import { getMedianCellSize } from "./cell_area_component" +import { NumericConstant } from "../numeric_constant" + +interface customFunction { + name: string + fn: (x: NumericTileGrid) => number +} + +// functions and constants that are already defined in mathjs therefore don't need to be added as inputs +const definedFnsCnsts: string[] = [ + 'exp', + 'log', + 'sin', + 'cos', + 'tan', + 'PI', + 'E', + 'sqrt' +] + +const customFns: customFunction[] = [ + { name: 'AREA_M2', fn: (x: NumericTileGrid) => getMedianCellSize(x).area }, + { name: 'AREA_KM2', fn: (x: NumericTileGrid) => getMedianCellSize(x).area / 1000000 }, + { name: 'LENGTH_KM', fn: (x: NumericTileGrid) => getMedianCellSize(x).length }, + { name: 'LENGTH_M', fn: (x: NumericTileGrid) => getMedianCellSize(x).length * 1000 }, +] interface Expression { @@ -18,16 +44,24 @@ interface Expression { const ExpressionList: Array = [ { id: 1, name: `height * scale + error` }, - { id: 2, name: `H^2 * scale + error` }, + //{ id: 2, name: `H^2 * scale + error` }, + { id: 3, name: `exp(A + (B * log(H * CD)) + error) / AREA_M2` }, + //{ id: 4, name: `log(0.016 + alpha) + (0.204^2/2)`}, + //{ id: 5, name: `2.013 + beta` }, + { id: 6, name: `sqrt(PI/4 * AREA_M2)` }, ] export class ExpressionComponent extends BaseComponent { projectProps: ProjectProperties + cache: Map + inputCache: Map constructor(ProjectProps: ProjectProperties) { super("Expression") this.category = "Arithmetic" this.projectProps = ProjectProps + this.cache = new Map() + this.inputCache = new Map() } async builder(node: Node) { @@ -48,11 +82,11 @@ export class ExpressionComponent extends BaseComponent { this.calculateVariables(node) - node.addOutput(new Output('out', 'Output', numericDataSocket)) + node.addOutput(new Output('out', 'Output', numericNumberDataSocket)) - node.addControl(new PreviewControl(() => - node.meta.output as any || new NumericTileGrid(0, 0, 0, 1, 1) - )) + //node.addControl(new PreviewControl(() => + // node.meta.output as any || new NumericTileGrid(0, 0, 0, 1, 1) + //)) } @@ -65,20 +99,28 @@ export class ExpressionComponent extends BaseComponent { return r?.name; } - - calculateVariables(node: Node): void { - const expression = this.getExpression(node.data.expressionId as number) as string - + retrieveVariables(node: Node, expression: string): Set { const uniqueSymbols = new Set( parse(expression) .filter(isSymbolNode) .map(n => (n as any).name) ) + return uniqueSymbols + } + + + calculateVariables(node: Node): void { + + const expression = this.getExpression(node.data.expressionId as number) as string + const uniqueSymbols = this.retrieveVariables(node, expression) + const symbolArray = Array.from(uniqueSymbols) for (let symbol of symbolArray) { - node.addInput(new Input(symbol as string, symbol as string, numericNumberDataSocket)) + if (!definedFnsCnsts.includes(symbol as string) && !customFns.map(f => f.name).includes(symbol as string)) { + node.addInput(new Input(symbol as string, symbol as string, numericNumberDataSocket)) + } } } @@ -105,25 +147,34 @@ export class ExpressionComponent extends BaseComponent { async worker(node: NodeData, inputs: WorkerInputs, outputs: WorkerOutputs, ...args: unknown[]) { + console.log("Expression worker") + const editorNode = this.editor?.nodes.find(n => n.id === node.id) if (editorNode === undefined) { return } const expression = this.getExpression(editorNode.data.expressionId as string) as string + console.log(expression) + let variables: string[] = [] let errorMsg: null | string = null + let numericConstantOutput = true + + for (const input in inputs) { inputs[input][0] === undefined ? errorMsg = "" : null + if(inputs[input][0] instanceof NumericTileGrid) numericConstantOutput = false variables.push(input) } if (errorMsg) { editorNode.meta.errorMessage = errorMsg - } else if (isEqual(editorNode.data.previousInputs, [inputs, expression])) { - const out = editorNode.meta.output = outputs['out'] = editorNode.data.previewsOutput + } else if (this.cache.has(expression) && isEqual(inputs, this.inputCache.get(expression))) { + console.log("using previous output") + const out = editorNode.meta.output = outputs['out'] = this.cache.get(expression) } else { @@ -131,13 +182,10 @@ export class ExpressionComponent extends BaseComponent { const p = parser() - const v = inputs[variables[0]][0] as NumericTileGrid - - const tileGrid = createXYZ() const outputTileRange = tileGrid.getTileRangeForExtentAndZ(this.projectProps.extent, this.projectProps.zoom) - const out = editorNode.meta.output = outputs['out'] = new NumericTileGrid( + const t = new NumericTileGrid( this.projectProps.zoom, outputTileRange.minX, outputTileRange.minY, @@ -145,26 +193,62 @@ export class ExpressionComponent extends BaseComponent { outputTileRange.getHeight() ) - out.iterate((x, y) => { + // check and calculate any constants or functions that are outside of MathJS + const customConsts = Array.from(this.retrieveVariables(editorNode, expression)).filter(symb => customFns.map(f => f.name).includes(symb as string)) + const ConstMap = new Map(customConsts.map(c => [c, customFns.find(f => f.name === c)?.fn(t)])) - variables.forEach((i) => { - const variableSource: any = inputs[i][0] - p.set(i, variableSource.get(x, y)) - }) + console.log(numericConstantOutput) + + if (numericConstantOutput) { + + console.log("numeric constant output") + + ConstMap.forEach((v, k) => p.set(k as string, v)) + variables.forEach(i => p.set(i, (inputs[i][0] as NumericConstant).value)) let r = p.evaluate(expression) - out.set(x, y, r); + const out = outputs['out'] = new NumericConstant(r, editorNode.data.name as string) + p.clear(); - }) - editorNode.data.previousInputs = [inputs, expression] - editorNode.data.previewsOutput = out + + this.cache.set(expression, out) + this.inputCache.set(expression, inputs) + + }else{ + + console.log("tile grid output") + + const out = outputs['out'] = t + + out.iterate((x, y) => { + + // apply any constants, ex: Area + ConstMap.forEach((v, k) => p.set(k as string, v)) + + // variables from input + variables.forEach((i) => { + const variableSource: any = inputs[i][0] + p.set(i, variableSource.get(x, y)) + }) + + // evaluate the expression + let r = p.evaluate(expression) + + out.set(x, y, r); + p.clear(); + + }) + + this.cache.set(expression, out) + this.inputCache.set(expression, inputs) + } } - const previewControl: any = editorNode.controls.get('Preview') - previewControl.update() + //const previewControl: any = editorNode.controls.get('Preview') + //previewControl.update() editorNode.update()