import { Box, Autocomplete, TextField, Radio, Table, TableBody, TableCell, TableHead, TableRow } from '@mui/material'; import React, { useState } from 'react'; import ToolContent from '@components/ToolContent'; import { ToolComponentProps } from '@tools/defineTool'; import ToolTextResult from '@components/result/ToolTextResult'; import NumericInputWithUnit from '@components/input/NumericInputWithUnit'; import { UpdateField } from '@components/options/ToolOptions'; import { InitialValuesType } from './types'; import type { GenericCalcType } from './data/types'; import type { DataTable } from 'datatables'; import { getDataTable, dataTableLookup } from 'datatables'; import nerdamer from 'nerdamer'; import 'nerdamer/Algebra'; import 'nerdamer/Solve'; import 'nerdamer/Calculus'; export default async function makeTool( calcData: GenericCalcType ): Promise> { const initialValues: InitialValuesType = { outputVariable: '', vars: {}, presets: {} }; const dataTables: { [key: string]: DataTable } = {}; for (const selection of calcData.selections || []) { dataTables[selection.source] = await getDataTable(selection.source); } return function GenericCalc({ title }: ToolComponentProps) { const [result, setResult] = useState(''); const [shortResult, setShortResult] = useState(''); // For UX purposes we need to track what vars are const [valsBoundToPreset, setValsBoundToPreset] = useState<{ [key: string]: string; }>({}); const [extraOutputs, setExtraOutputs] = useState<{ [key: string]: number; }>({}); const updateVarField = ( name: string, value: number, unit: string, values: InitialValuesType, updateFieldFunc: UpdateField ) => { // Make copy const newVars = { ...values.vars }; newVars[name] = { value, unit: unit }; updateFieldFunc('vars', newVars); }; const handleSelectedTargetChange = ( varName: string, updateFieldFunc: UpdateField ) => { updateFieldFunc('outputVariable', varName); }; const handleSelectedPresetChange = ( selection: string, preset: string, currentValues: InitialValuesType, updateFieldFunc: UpdateField ) => { const newPresets = { ...currentValues.presets }; newPresets[selection] = preset; updateFieldFunc('presets', newPresets); // Clear old selection for (const key in valsBoundToPreset) { if (valsBoundToPreset[key] === selection) { delete valsBoundToPreset[key]; } } const selectionData = calcData.selections?.find( (sel) => sel.title === selection ); if (preset && preset != '') { if (selectionData) { for (const key in selectionData.bind) { valsBoundToPreset[key] = selection; if (currentValues.outputVariable === key) { handleSelectedTargetChange('', updateFieldFunc); } updateVarField( key, dataTableLookup(dataTables[selectionData.source], preset)[ selectionData.bind[key] ], dataTables[selectionData.source].columns[selectionData.bind[key]] ?.unit || '', currentValues, updateFieldFunc ); } } else { throw new Error( `Preset "${preset}" is not valid for selection "${selection}"` ); } } }; calcData.variables.forEach((variable) => { if (variable.default === undefined) { initialValues.vars[variable.name] = { value: NaN, unit: variable.unit }; initialValues.outputVariable = variable.name; } else { initialValues.vars[variable.name] = { value: variable.default || 0, unit: variable.unit }; } }); calcData.selections?.forEach((selection) => { initialValues.presets[selection.title] = selection.default; if (selection.default == '') return; for (const key in selection.bind) { initialValues.vars[key] = { value: dataTableLookup( dataTables[selection.source], selection.default )[selection.bind[key]], unit: dataTables[selection.source].columns[selection.bind[key]]?.unit || '' }; valsBoundToPreset[key] = selection.title; } }); return ( } initialValues={initialValues} toolInfo={{ title: 'Common Equations', description: 'Common mathematical equations that can be used in calculations.' }} getGroups={({ values, updateField }) => [ { title: 'Presets', component: ( Option Value {calcData.selections?.map((preset) => ( {preset.title} ', ...Object.keys( dataTables[preset.source].data ).sort() ]} sx={{ width: 300 }} onChange={(event, newValue) => { handleSelectedPresetChange( preset.title, newValue || '', values, updateField ); }} renderInput={(params) => ( )} > ))}
) }, { title: 'Variables', component: ( Variable Value Solve For {calcData.variables.map((variable) => ( {variable.title} updateVarField( variable.name, parseFloat(val.value), val.unit, values, updateField ) } type="number" /> handleSelectedTargetChange( variable.name, updateField ) } /> ))} {calcData.extraOutputs?.map((extraOutput) => ( {extraOutput.title} {extraOutput.unit} ))}
) } ]} compute={(values) => { if (values.outputVariable === '') { setResult('Please select a solve for variable'); return; } let expr = nerdamer(calcData.formula); Object.keys(values.vars).forEach((key) => { if (key === values.outputVariable) return; expr = expr.sub(key, values.vars[key].value.toString()); }); let result: nerdamer.Expression = expr.solveFor( values.outputVariable ); // Sometimes the result is an array if (result.toDecimal === undefined) { result = (result as unknown as nerdamer.Expression[])[0]; } setResult(result.toString()); if (result) { if (values.vars[values.outputVariable] != undefined) { values.vars[values.outputVariable].value = parseFloat( result.toDecimal() ); } setShortResult(parseFloat(result.toDecimal())); } else { setShortResult(NaN); } if (calcData.extraOutputs !== undefined) { for (let i = 0; i < calcData.extraOutputs.length; i++) { const extraOutput = calcData.extraOutputs[i]; let expr = nerdamer(extraOutput.formula); Object.keys(values.vars).forEach((key) => { if (key === values.outputVariable) return; expr = expr.sub(key, values.vars[key].value.toString()); }); const result: nerdamer.Expression = expr.evaluate(); if (result) { extraOutputs[extraOutput.title] = parseFloat( result.toDecimal() ); } } } }} /> ); }; }