import { Navigation , NavigationIndicator } from '@/services/data/tuples/navigation-types' ; import { isXaNumber } from '@/services/utils' ; import { useForceUpdate } from '@/widgets/basic/utils' ; import { Fragment , useEffect , useState } from 'react' ; import { useNavigationEditEventBus } from './navigation-edit-event-bus' ; import { NavigationEditEventTypes } from './navigation-edit-event-bus-types' ; import { CalculatedIndicatorValues , IndicatorValues } from './types' ; export const toNumber = ( x ) : number | '' => { if ( x == null || ! isXaNumber ( x ) ) { return '' ; } try { const v = Number ( x ) ; return Number . isNaN ( v ) ? '' : v ; } catch { return '' ; } } ; export const formatToNumber = ( x , fractionDigits = 2 ) => { const v = toNumber ( x ) ; return v === '' ? '' : v . toFixed ( fractionDigits ) ; } ; export const computeRatio = ( currentValue , previousValue ) : number => { const current = toNumber ( currentValue ) ; const previous = toNumber ( previousValue ) ; if ( current === '' ) { return 0 ; } else if ( previous === '' || previous === 0 ) { return 100 ; } else { return ( current - previous ) / previous * 100 ; } } ; export type ComputedScore = { ratio : number ; score ? : number ; useScore : boolean ; error ? : string ; } ; export const interpolation = ( value , min , minScore , max , maxScore ) => { const v = toNumber ( value ) ; if ( v === '' ) { return minScore ?? 0 ; } if ( v <= min ) { return minScore ?? 0 ; } else if ( v >= max ) { return maxScore ; } else { return ( minScore ?? 0 ) + Number ( ( ( maxScore - minScore ) * ( v - min ) / ( max - min ) ) . toFixed ( 1 ) ) ; } } ; const doComputeScore = ( options ) : { score ? : number ; error ? : string ; } => { const { script , current , previous , ratio } = options ; try { const mathFunctionNames = Object . getOwnPropertyNames ( Math ) ; const runScript = script . split ( '\n' ) . filter ( x => x != null && x . trim ( ) . length !== 0 ) . map ( ( line , index , lines ) => { return lines . length === index + 1 ? `return ${ line }` : line ; } ) . join ( '\n' ) ; const args = [ 'c' , 'p' , 'r' , ... mathFunctionNames , 'interpolation' , runScript ] ; const func = new Function ( ... args ) ; const params = [ current , previous , ratio / 100 , ... mathFunctionNames . map ( name => Math [ name ] ) , interpolation ] ; return { score : func ( ... params ) } ; } catch ( e ) { console . groupCollapsed ( 'Navigation Indicator Formula Script Error' ) ; console . error ( e ) ; console . groupEnd ( ) ; return { error : e . message || 'Navigation Indicator Formula Script Error.' } ; } } ; const shouldComputeScore = ( script ? ) => { return script != null && script . trim ( ) . length !== 0 ; } ; const computeScore = ( options ) : ComputedScore => { const { script , current , previous } = options ; const ratio = computeRatio ( current , previous ) ; const useScore = shouldComputeScore ( script ) ; if ( useScore ) { const score = doComputeScore ( { script : script ! , current , previous , ratio } ) ; return { ratio , useScore : true , ... score } ; } else { return { ratio , useScore : false } ; } } ; const asValuePair = ( value ? , fractionDigits = 2 ) : { value : number ; formatted : string ; } | undefined => { if ( value == null ) { return ( void 0 ) ; } return { value , formatted : formatToNumber ( value , fractionDigits ) } ; } ; const doCompute = ( navigationIndicator , values ) : CalculatedIndicatorValues => { if ( ! values . loaded ) { return { loaded : false , loadFailed : false , calculated : false , calculateFailed : false , shouldComputeScore : shouldComputeScore ( navigationIndicator . formula ) } ; } else if ( values . failed ) { return { loaded : true , loadFailed : true , calculated : false , calculateFailed : false , shouldComputeScore : shouldComputeScore ( navigationIndicator . formula ) } ; } else { const { ratio , useScore , score , error } = computeScore ( { script : navigationIndicator . formula , current : values . current , previous : values . previous } ) ; return { loaded : true , loadFailed : false , calculated : true , calculateFailed : error != null && error . trim ( ) . length !== 0 , calculateFailureReason : error , current : asValuePair ( values . current ) , previous : asValuePair ( values . previous ) , ratio : asValuePair ( ratio ) , score : asValuePair ( score , 1 ) , shouldComputeScore : useScore } ; } } ; /**
 * calculate  CalculatedIndicatorValues on
 * 1.  NavigationEditEventTypes ,
 * 2.  NavigationEditEventTypes .
 * only used on non-compute indicator
  export const IndicatorValuesCalculator = ( props ) => { const { navigation , navigationIndicator } = props ; const { on , off , fire } = useNavigationEditEventBus ( ) ; const [ calculatedValues , setCalculatedValues ] = useState < CalculatedIndicatorValues > ( { loaded : false , loadFailed : false , calculated : false , calculateFailed : false , shouldComputeScore : shouldComputeScore ( navigationIndicator . formula ) } ) ; const forceUpdate = useForceUpdate ( ) ; useEffect ( ( ) => { fire ( NavigationEditEventTypes . VALUES_CALCULATED , navigation , navigationIndicator , calculatedValues ) ; } , [ fire , navigation , navigationIndicator , calculatedValues ] ) ; useEffect ( ( ) => { const onValuesChanged = ( aNavigation , aNavigationIndicator , values ) => { if ( aNavigation !== navigation || aNavigationIndicator !== navigationIndicator ) { return ; } setCalculatedValues ( doCompute ( navigationIndicator , values ) ) ; } ; on ( NavigationEditEventTypes . VALUES_CHANGED , onValuesChanged ) ; return ( ) => { off ( NavigationEditEventTypes . VALUES_CHANGED , onValuesChanged ) ; } ; } , [ on , off , navigation , navigationIndicator ] ) ; useEffect ( ( ) => { const onFormulaChanged = ( aNavigation , aNavigationIndicator ) => { if ( aNavigation !== navigation || aNavigationIndicator !== navigationIndicator ) { return ; } if ( calculatedValues . loaded && ! calculatedValues . loadFailed ) { setCalculatedValues ( doCompute ( navigationIndicator , { loaded : true , failed : false , current : calculatedValues . current ?. value , previous : calculatedValues . previous ?. value } ) ) ; } } ; on ( NavigationEditEventTypes . INDICATOR_FORMULA_CHANGED , onFormulaChanged ) ; return ( ) => { off ( NavigationEditEventTypes . INDICATOR_FORMULA_CHANGED , onFormulaChanged ) ; } ; } , [ on , off , forceUpdate , navigation , navigationIndicator , calculatedValues ] ) ; useEffect ( ( ) => { const onAskCalculatedValues = ( aNavigation , aNavigationIndicator , onData ) => { if ( aNavigation !== navigation || aNavigationIndicator !== navigationIndicator ) { return ; } onData ( calculatedValues ) ; } ; on ( NavigationEditEventTypes . ASK_CALCULATED_VALUES , onAskCalculatedValues ) ; return ( ) => { off ( NavigationEditEventTypes . ASK_CALCULATED_VALUES , onAskCalculatedValues ) ; } ; } , [ on , off , navigation , navigationIndicator , calculatedValues ] ) ; return < Fragment />; } ; /**
 * handle my  NavigationEditEventTypes .
  export const useIndicatorValuesCalculator = ( navigation , navigationIndicator ) => { const { on , off } = useNavigationEditEventBus ( ) ; const [ calculatedValues , setCalculatedValues ] = useState < CalculatedIndicatorValues > ( { loaded : false , loadFailed : false , calculated : false , calculateFailed : false , shouldComputeScore : shouldComputeScore ( navigationIndicator . formula ) } ) ; useEffect ( ( ) => { const onValuesCalculated = ( aNavigation , aNavigationIndicator , values ) => { if ( navigation !== aNavigation || navigationIndicator !== aNavigationIndicator ) { return ; } setCalculatedValues ( values ) ; } ; on ( NavigationEditEventTypes . VALUES_CALCULATED , onValuesCalculated ) ; return ( ) => { off ( NavigationEditEventTypes . VALUES_CALCULATED , onValuesCalculated ) ; } ; } , [ on , off , navigation , navigationIndicator ] ) ; return calculatedValues ; } ;