import { Autocomplete, Box, MenuItem, Radio, Select, Stack, TextField, useTheme } from '@mui/material'; import React, { useContext, useState } from 'react'; import ToolContent from '@components/ToolContent'; import { ToolComponentProps } from '@tools/defineTool'; import NumericInputWithUnit from '@components/input/NumericInputWithUnit'; import { UpdateField } from '@components/options/ToolOptions'; import { InitialValuesType } from './types'; import type { AlternativeVarInfo, GenericCalcType } from './data/types'; import { dataTableLookup } from 'datatables'; import nerdamer from 'nerdamer-prime'; import 'nerdamer-prime/Algebra'; import 'nerdamer-prime/Solve'; import 'nerdamer-prime/Calculus'; import Qty from 'js-quantities'; import { CustomSnackBarContext } from 'contexts/CustomSnackBarContext'; import Typography from '@mui/material/Typography'; import Grid from '@mui/material/Grid'; import useMediaQuery from '@mui/material/useMediaQuery'; import { useTranslation } from 'react-i18next'; import { validNamespaces } from '../../../../i18n'; function numericSolveEquationFor( equation: string, varName: string, variables: { [key: string]: number } ) { let expr = nerdamer(equation); for (const key in variables) { expr = expr.sub(key, variables[key].toString()); } let result: nerdamer.Expression | nerdamer.Expression[] = expr.solveFor(varName); // Sometimes the result is an array, check for it while keeping linter happy if ((result as unknown as nerdamer.Expression).toDecimal === undefined) { result = (result as unknown as nerdamer.Expression[])[0]; } return parseFloat( (result as unknown as nerdamer.Expression).evaluate().toDecimal() ); } export default async function makeTool( calcData: GenericCalcType ): Promise> { const initialValues: InitialValuesType = { outputVariable: '', vars: {}, presets: {} }; return function GenericCalc({ title }: ToolComponentProps) { const { showSnackBar } = useContext(CustomSnackBarContext); const { t } = useTranslation(validNamespaces); const theme = useTheme(); const lessThanSmall = useMediaQuery(theme.breakpoints.down('sm')); // 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 using setState callback pattern setValsBoundToPreset((prevState) => { const newState = { ...prevState }; // Remove all keys bound to this selection Object.keys(newState).forEach((key) => { if (newState[key] === selection) { delete newState[key]; } }); return newState; }); const selectionData = calcData.presets?.find( (sel) => sel.title === selection ); if (preset && preset != '') { if (selectionData) { // Create an object with the new bindings const newBindings: { [key: string]: string } = {}; for (const key in selectionData.bind) { // Add to newBindings for later state update newBindings[key] = selection; if (currentValues.outputVariable === key) { handleSelectedTargetChange('', updateFieldFunc); } updateVarField( key, dataTableLookup(selectionData.source, preset)[ selectionData.bind[key] ], selectionData.source.columns[selectionData.bind[key]]?.unit || '', currentValues, updateFieldFunc ); } // Update state with new bindings setValsBoundToPreset((prevState) => ({ ...prevState, ...newBindings })); } else { throw new Error( `Preset "${preset}" is not valid for selection "${selection}"` ); } } }; calcData.variables.forEach((variable) => { if (variable.solvable === undefined) { variable.solvable = true; } 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.presets?.forEach((selection) => { initialValues.presets[selection.title] = selection.default; if (selection.default == '') return; for (const key in selection.bind) { initialValues.vars[key] = { value: dataTableLookup(selection.source, selection.default)[ selection.bind[key] ], unit: selection.source.columns[selection.bind[key]]?.unit || '' }; // We'll set this in useEffect instead of directly modifying state } }); function getAlternate( alternateInfo: AlternativeVarInfo, mainInfo: GenericCalcType['variables'][number], mainValue: { value: number; unit: string; } ) { if (isNaN(mainValue.value)) return NaN; const canonicalValue = Qty(mainValue.value, mainValue.unit).to( mainInfo.unit ).scalar; return numericSolveEquationFor(alternateInfo.formula, 'x', { v: canonicalValue }); } function getMainFromAlternate( alternateInfo: AlternativeVarInfo, mainInfo: GenericCalcType['variables'][number], alternateValue: { value: number; unit: string; } ) { if (isNaN(alternateValue.value)) return NaN; const canonicalValue = Qty(alternateValue.value, alternateValue.unit).to( alternateInfo.unit ).scalar; return numericSolveEquationFor(alternateInfo.formula, 'v', { x: canonicalValue }); } return ( [ ...(calcData.presets?.length ? [ { title: 'Presets', component: ( {calcData.presets?.map((preset) => ( {preset.title} ', ...Object.keys(preset.source.data).sort() ]} sx={{ width: '80%' }} onChange={(event, newValue) => { handleSelectedPresetChange( preset.title, newValue || '', values, updateField ); }} renderInput={(params) => ( )} /> ))} ) } ] : []), { title: 'Variables', component: ( {lessThanSmall ? ( Solve for ) : ( Solve For )} {calcData.variables.map((variable) => ( {variable.title} updateVarField( variable.name, val.value, val.unit, values, updateField ) } /> {variable.alternates?.map((alt) => ( {alt.title} updateVarField( variable.name, getMainFromAlternate( alt, variable, val ), variable.unit, values, updateField ) } /> ))} {!lessThanSmall && ( handleSelectedTargetChange( variable.name, updateField ) } /> )} ))} ) }, ...(calcData.extraOutputs ? [ { title: 'Extra outputs', component: ( {calcData.extraOutputs?.map((extraOutput) => ( {extraOutput.title} ))} ) } ] : []) ]} compute={(values) => { if (values.outputVariable === '') { showSnackBar('Please select a solve for variable', 'error'); return; } let expr: nerdamer.Expression | null = null; for (const variable of calcData.variables) { if (variable.name === values.outputVariable) { if (variable.formula !== undefined) { expr = nerdamer(variable.formula); } } } if (expr == null) { expr = nerdamer(calcData.formula); } if (expr == null) { throw new Error('No formula found'); } Object.keys(values.vars).forEach((key) => { if (key === values.outputVariable) return; if (expr === null) { throw new Error('Math fail'); } expr = expr.sub(key, values.vars[key].value.toString()); }); let result: nerdamer.Expression | nerdamer.Expression[] = expr.solveFor(values.outputVariable); // Sometimes the result is an array if ( (result as unknown as nerdamer.Expression).toDecimal === undefined ) { if ((result as unknown as nerdamer.Expression[])?.length < 1) { values.vars[values.outputVariable].value = NaN; if (calcData.extraOutputs !== undefined) { // Update extraOutputs using setState setExtraOutputs((prevState) => { const newState = { ...prevState }; for (let i = 0; i < calcData.extraOutputs!.length; i++) { const extraOutput = calcData.extraOutputs![i]; newState[extraOutput.title] = NaN; } return newState; }); } throw new Error('No solution found for this input'); } result = (result as unknown as nerdamer.Expression[])[0]; } if (result) { if (values.vars[values.outputVariable] != undefined) { values.vars[values.outputVariable].value = parseFloat( (result as unknown as nerdamer.Expression) .evaluate() .toDecimal() ); } } else { values.vars[values.outputVariable].value = 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) => { expr = expr.sub(key, values.vars[key].value.toString()); }); // todo could this have multiple solutions too? const result: nerdamer.Expression = expr.evaluate(); if (result) { // Update extraOutputs state properly setExtraOutputs((prevState) => ({ ...prevState, [extraOutput.title]: parseFloat(result.toDecimal()) })); } } } }} /> ); }; }