Difference between revisions of "MediaWiki:Gadget-calculator-core.js"

From WikiAnesthesia
Line 1: Line 1:
/**
* @author Chris Rishel
*/
( function() {
( function() {
     var COOKIE_EXPIRATION = 12 * 60 * 60;
     var moduleId = 'anatomyPhysiology';


     var TYPE_NUMBER = 'number';
     mw.calculators.addUnitsBases( {
    var TYPE_STRING = 'string';
        bpm: {
            toString: function( units ) {
                units = units.replace( 'bpm', 'beats/min' );


    var VALID_TYPES = [
                 return units;
        TYPE_NUMBER,
        TYPE_STRING
    ];
 
    var DEFAULT_CALCULATION_CLASS = 'SimpleCalculation';
    var DEFAULT_CALCULATOR_CLASS = 'SimpleCalculator';
 
    // Polyfill to fetch unit's base. This may become unnecessary in a future version of math.js
    math.Unit.prototype.getBase = function() {
        for( var iBase in math.Unit.BASE_UNITS ) {
            if( this.equalBase( math.Unit.BASE_UNITS[ iBase ] ) ) {
                 return iBase;
             }
             }
         }
         },
 
         hgb: {
        return null;
            toString: function( units ) {
    };
                units = units.replace( 'hgbperdL', '/dL' );
 
                 units = units.replace( 'pcthct', '%' );
 
    mw.calculators = {
        calculators: {},
         calculations: {},
        objectClasses: {},
        units: {},
        unitsBases: {},
        variables: {},
        addCalculations: function( calculationData, className ) {
            className = className ? className : DEFAULT_CALCULATION_CLASS;
 
            var calculations = mw.calculators.createCalculatorObjects( className, calculationData );
 
            for( var calculationId in calculations ) {
                 var calculation = calculations[ calculationId ];
 
                mw.calculators.calculations[ calculationId ] = calculation;


                 mw.calculators.calculations[ calculationId ].setDependencies();
                 return units;
             }
             }
         },
         },
         addCalculators: function( moduleId, calculatorData, className ) {
         o2: {
            className = className ? className : DEFAULT_CALCULATOR_CLASS;
            toString: function( units ) {
                units = units.replace( 'pcto2', '%' );


            for( var calculatorId in calculatorData ) {
                 return units;
                 calculatorData[ calculatorId ].module = moduleId;
 
                // Make sure the calculations have been defined
                for( var iCalculation in calculatorData[ calculatorId ].calculations ) {
                    var calculationId = calculatorData[ calculatorId ].calculations[ iCalculation ];
 
                    if( !mw.calculators.getCalculation( calculationId ) ) {
                        throw new Error( 'Calculator "' + calculatorId + '" references calculation "' + calculationId + '" which is not defined' );
                    }
                }
             }
             }
        },
        temperature: {
            toString: function( units ) {
                units = units.replace( 'deg', '°' );


            var calculators = mw.calculators.createCalculatorObjects( className, calculatorData );
                 return units;
 
            // Initalize the calculators property for the module
            if( !mw.calculators.calculators.hasOwnProperty( moduleId ) ) {
                 mw.calculators.calculators[ moduleId ] = {};
             }
             }
        }
    } );


            // Store the calculators
    mw.calculators.addUnits( {
             for( var calculatorId in calculators ) {
        bpm: {
                mw.calculators.calculators[ moduleId ][ calculatorId ] = calculators[ calculatorId ];
             baseName: 'bpm'
 
        },
                mw.calculators.calculators[ moduleId ][ calculatorId ].render();
        pcthct: {
             }
            baseName: 'hgb'
        },
        pcto2: {
             baseName: 'o2'
         },
         },
         addUnitsBases: function( unitsBaseData ) {
         ghgbperdL: {
             var unitsBases = mw.calculators.createCalculatorObjects( 'UnitsBase', unitsBaseData );
             baseName: 'hgb',
            prefixes: 'short',
            definition: '3 pcthct'
        }
    } );


            for( var unitsBaseId in unitsBases ) {
    mw.calculators.addVariables( {
                mw.calculators.unitsBases[ unitsBaseId ] = unitsBases[ unitsBaseId ];
        caseDuration: {
             }
            name: 'Case duration',
            type: 'number',
            abbreviation: 'Duration',
            minValue: '0 hr',
            maxLength: 3,
            units: [
                'hr',
                'min'
             ]
         },
         },
         addUnits: function( unitsData ) {
         hct: {
             var units = mw.calculators.createCalculatorObjects( 'Units', unitsData );
             name: 'Current hematocrit',
 
             type: 'number',
             for( var unitsId in units ) {
            abbreviation: 'Current hct',
                if( mw.calculators.units.hasOwnProperty( unitsId ) ) {
            minValue: '10 pcthct',
                    continue;
            maxValue: '75 pcthct',
                }
            defaultValue: '45 pcthct',
 
            maxLength: 4,
                try {
            units: [
                    var unitData = {
                 'pcthct',
                        aliases: units[ unitsId ].aliases,
                 'ghgbperdL'
                        baseName: units[ unitsId ].baseName ? units[ unitsId ].baseName.toUpperCase() : units[ unitsId ].baseName,
             ]
                        definition: units[ unitsId ].definition,
                        prefixes: units[ unitsId ].prefixes,
                        offset: units[ unitsId ].offset,
                    };
 
                    math.createUnit( unitsId, unitData );
                } catch( e ) {
                    console.warn( e.message );
                 }
 
                 mw.calculators.units[ units ] = units[ unitsId ];
             }
         },
         },
         addVariables: function( variableData ) {
         heartRate: {
             var variables = mw.calculators.createCalculatorObjects( 'Variable', variableData );
             name: 'Heart rate',
 
            type: 'number',
             for( var variableId in variables ) {
             abbreviation: 'HR',
                mw.calculators.variables[ variableId ] = variables[ variableId ];
            defaultValue: '60 bpm',
 
            maxLength: 4,
                var cookieValue = mw.calculators.getCookieValue( variableId );
            units: [
 
                 'bpm'
                if( cookieValue ) {
             ]
                    // Try to set the variable value from the cookie value
                    if( !mw.calculators.variables[ variableId ].setValue( cookieValue ) ) {
                        // Unset the cookie value since for whatever reason it's no longer valid.
                        mw.calculators.setCookieValue( variableId, null );
                    }
                 }
             }
         },
         },
         createCalculatorObjects: function( className, objectData ) {
         hgb: {
             if( !mw.calculators.objectClasses.hasOwnProperty( className ) ) {
             name: 'Hemoglobin',
                throw new Error( 'Invalid class name "' + className + '"' );
            type: 'number',
             }
            abbreviation: 'HgB',
 
             minValue: '3 ghgbperdL',
             var objects = {};
            maxValue: '25 ghgbperdL',
 
             defaultValue: '13 ghgbperdL',
             for( var objectId in objectData ) {
            maxLength: 4,
                var propertyValues = objectData[ objectId ];
             units: [
 
                 'pcthct',
                 // Id can either be specified using the 'id' property, or as the property name in objectData
                 'ghgbperdL'
                 if( propertyValues.hasOwnProperty( 'id' ) ) {
            ]
                    objectId = propertyValues.id;
                }
                else {
                    propertyValues.id = objectId;
                }
 
                objects[ objectId ] = new mw.calculators.objectClasses[ className ]( propertyValues );
            }
 
            return objects;
         },
         },
         createInputGroup: function( variableIds, global ) {
         minHct: {
             var $form = $( '<form>', {
             name: 'Minimum hematocrit',
                novalidate: true
            type: 'number',
             } );
             abbreviation: 'Min hct',
 
             minValue: '10 pcthct',
             var $formRow = $( '<div>', {
            maxValue: '45 pcthct',
                class: 'form-row'
             defaultValue: '21 pcthct',
             } ).css( 'flex-wrap', 'nowrap' );
             maxLength: 4,
 
             units: [
             var inputOptions = {
                 'pcthct',
                global: !!global
                'ghgbperdL'
             };
            ]
 
            for( var iVariableId in variableIds ) {
                var variableId = variableIds[ iVariableId ];
 
                 if( !mw.calculators.variables.hasOwnProperty( variableId ) ) {
                    throw new Error( 'Invalid variable name "' + variableId + '"' );
                }
 
                $formRow.append( mw.calculators.variables[ variableId ].createInput( inputOptions ) );
            }
 
            return $form.append( $formRow );
         },
         },
         getCookieKey: function( variableId ) {
         paCO2: {
             return 'calculators-var-' + variableId;
             name: 'PaCO<sub>2</sub>',
            type: 'number',
            abbreviation: 'PaCO<sub>2</sub>',
            minValue: '20 mmHg',
            defaultValue: '40 mmHg',
            maxLength: 3,
            units: [
                'mmHg'
            ]
         },
         },
         getCookieValue: function( varId ) {
         saO2: {
             var cookieValue = mw.cookie.get( mw.calculators.getCookieKey( varId ) );
             name: 'SaO<sub>2</sub>',
 
            type: 'number',
             if( !cookieValue ) {
            abbreviation: 'SaO<sub>2</sub>',
                return null;
            minValue: '25 pcto2',
             }
             maxValue: '100 pcto2',
 
            defaultValue: '100 pcto2',
             return cookieValue;
            maxLength: 3,
             units: [
                'pcto2'
             ]
         },
         },
         getCalculation: function( calculationId ) {
         smvO2: {
             if( mw.calculators.calculations.hasOwnProperty( calculationId ) ) {
             name: 'SmvO<sub>2</sub>',
                return mw.calculators.calculations[ calculationId ];
            type: 'number',
             } else {
            abbreviation: 'SmvO<sub>2</sub>',
                 return null;
            minValue: '25 pcto2',
             }
            maxValue: '100 pcto2',
            defaultValue: '75 pcto2',
            maxLength: 3,
             units: [
                 'pcto2'
             ]
         },
         },
         getCalculator: function( moduleId, calculatorId ) {
         npoTime: {
             if( mw.calculators.calculators.hasOwnProperty( moduleId ) &&
            name: 'Time spent NPO',
                mw.calculators.calculators[ moduleId ].hasOwnProperty( calculatorId ) ) {
            type: 'number',
                return mw.calculators.calculators[ moduleId ][ calculatorId ];
            abbreviation: 'NPO time',
             } else {
             minValue: '0 hr',
                 return null;
            defaultValue: '8 hr',
             }
            maxLength: 2,
             units: [
                 'hr'
             ]
         },
         },
         getUnitsByBase: function( value ) {
         surgicalTrauma: {
             if( typeof value !== 'object' || !value.hasOwnProperty( 'units' ) ) {
             name: 'Severity of surgical trauma',
                return null;
            type: 'string',
             }
             abbreviation: 'Surgical trauma',
 
             defaultValue: 'Minimal',
             var unitsByBase = {};
             options: [
 
                 'Minimal',
             for( var iUnits in value.units ) {
                'Moderate',
                 var units = value.units[ iUnits ];
                 'Severe'
 
             ]
                 unitsByBase[ units.unit.base.key.toLowerCase() ] = units.prefix.name + units.unit.name;
             }
 
            return unitsByBase;
         },
         },
         getUnitsString: function( value ) {
         temperature: {
             if( typeof value !== 'object' ) {
             name: 'Temperature',
                return null;
            type: 'number',
             }
             abbreviation: 'Temp',
 
            minValue: '20 degC',
             var unitsString = value.formatUnits();
             maxValue: '44 degC',
 
             defaultValue: '37 degC',
             var reDenominator = /\/\s?\((.*)\)/;
             maxLength: 5,
             var denominatorMatches = unitsString.match( reDenominator );
             units: [
 
                 'degC',
             if( denominatorMatches ) {
                'degF'
                var denominatorUnits = denominatorMatches[ 1 ];
             ]
 
        }
                 unitsString = unitsString.replace( reDenominator, '/' + denominatorUnits.replace( ' ', '/' ) );
    } );
             }


            unitsString = unitsString
                .replace( /\s/g, '' )
                .replace( /(\^(\d+))/g, '<sup>$2</sup>' );


            var unitsBase = value.getBase();


            if( unitsBase ) {
    mw.calculators.addCalculations( {
                unitsBase = unitsBase.toLowerCase();
        bmi: {
 
            name: 'Body mass index',
                if( mw.calculators.unitsBases.hasOwnProperty( unitsBase ) &&
            abbreviation: 'BMI',
                    typeof mw.calculators.unitsBases[ unitsBase ].toString === 'function' ) {
            data: {
                     unitsString = mw.calculators.unitsBases[ unitsBase ].toString( unitsString );
                variables: {
                     required: [ 'weight', 'height' ]
                 }
                 }
             } else {
             },
                // TODO nasty hack to fix weight units in compound units which have no base
            digits: 0,
                unitsString = unitsString.replace( 'kgwt', 'kg' );
            units: 'kg/m^2',
                 unitsString = unitsString.replace( 'ug', 'mcg' );
            formula: '<math>\\mathrm{BMI} = \\frac{\\mathrm{mass_{kg}}}{{(\\mathrm{height_{m}}})^2}</math>',
            link: '[[Body mass index]]',
            references: [],
            calculate: function( data ) {
                 return data.weight.toNumber( 'kgwt' ) / Math.pow( data.height.toNumber( 'm' ), 2 );
             }
             }
            return unitsString;
         },
         },
         getValueDecimals: function( value ) {
         bsa: {
             // Supports either numeric values or math objects
             name: 'Body surface area',
             if( mw.calculators.isValueMathObject( value ) ) {
            abbreviation: 'BSA',
                 value = mw.calculators.getValueNumber( value );
             data: {
             }
                variables: {
 
                    required: [ 'weight', 'height' ]
             if( typeof value !== 'number' ) {
                 }
                return null;
             },
             }
            digits: 2,
 
             units: 'm^2',
             // Convert the number to a string, reverse, and count the number of characters up to the period.
             formula: '<math>\\mathrm{BSA} = \\sqrt{\\frac{\\mathrm{weight_{kg}}*\\mathrm{height_{cm}}}{3600}}</math>',
             var decimals = value.toString().split('').reverse().join('').indexOf( '.' );
             link: false,
 
             references: [
             // If no decimal is present, will be set to -1 by indexOf. If so, set to 0.
                'Mosteller RD. Simplified calculation of body-surface area. N Engl J Med. 1987 Oct 22;317(17):1098. doi: 10.1056/NEJM198710223171717. PMID: 3657876.'
            decimals = decimals > 0 ? decimals : 0;
             ],
 
             calculate: function( data ) {
             return decimals;
                return Math.sqrt( data.height.toNumber( 'cm' ) * data.weight.toNumber( 'kgwt' ) / 3600 );
        },
        getValueNumber: function( value, decimals ) {
            if( typeof value !== 'object' ) {
                return null;
             }
             }
            // Remove floating point errors
            var number = math.round( value.toNumber(), 10 );
            var absNumber = math.abs( number );
            if( absNumber >= 10 ) {
                decimals = 0;
            } else {
                decimals = -math.floor( math.log10( absNumber ) ) + 1;
            }
            return math.round( number, decimals );
         },
         },
         getValueString: function( value, decimals ) {
         ebv: {
             if( !mw.calculators.isValueMathObject( value ) ) {
             name: 'Estimated blood volume',
                return null;
             abbreviation: 'EBV',
             }
             data: {
 
                 variables: {
            var valueNumber = mw.calculators.getValueNumber( value, decimals );
                    required: [ 'weight', 'age' ]
             var valueUnits = mw.calculators.getUnitsString( value );
 
            if( math.abs( math.log10( valueNumber ) ) > 3 ) {
                 var valueUnitsByBase = mw.calculators.getUnitsByBase( value );
 
                var oldSIUnit;
 
                if( valueUnitsByBase.hasOwnProperty( 'mass' ) ) {
                    oldSIUnit = valueUnitsByBase.mass;
                } else if( valueUnitsByBase.hasOwnProperty( 'volume' ) ) {
                    oldSIUnit = valueUnitsByBase.volume;
                 }
                 }
            },
            digits: 0,
            units: 'mL',
            formula: '',
            references: [
                'Morgan & Mikhail\'s Clinical Anesthesiology. 5e. p1168'
            ],
            calculate: function( data ) {
                var weight = data.weight.toNumber( 'kgwt' );
                var age = data.age.toNumber( 'yo' );


                 if( oldSIUnit ) {
                 var ebvPerKg;
                    // This new value should simplify to the optimal SI prefix.
                    // We need to create a completely new unit from the formatted (i.e. simplified) value
                    var newSIValue = math.unit( math.unit( valueNumber + ' ' + oldSIUnit ).format() );


                    // There is a bug in mathjs where formatUnits() won't simplify the units, only format() will.
                if( age >= 1 ) {
                    var newSIUnit = newSIValue.formatUnits();
                     if( data.gender === 'F' ) {
 
                         ebvPerKg = 65;
                     if( newSIUnit !== oldSIUnit ) {
                    } else {
                        var newValue = math.unit( newSIValue.toNumber() + ' ' + value.formatUnits().replace( oldSIUnit, newSIUnit ) );
                         ebvPerKg = 75;
 
                         valueNumber = mw.calculators.getValueNumber( newValue, decimals );
                         valueUnits = mw.calculators.getUnitsString( newValue );
                     }
                     }
                } else if( age >= 1/12 ) {
                    ebvPerKg = 80;
                } else if( age >= 0 ) {
                    ebvPerKg = 85;
                } else {
                    ebvPerKg = 95;
                 }
                 }
            }


            var valueString = String( valueNumber );
                 return weight * ebvPerKg;
 
            if( valueUnits ) {
                 valueString += ' ' + valueUnits;
             }
             }
            return valueString;
         },
         },
         getVariable: function( variableId ) {
         fluidMaintenanceRate: {
             if( mw.calculators.variables.hasOwnProperty( variableId ) ) {
             name: 'Fluid maintenance rate',
                return mw.calculators.variables[ variableId ];
             abbreviation: 'Fluid maint.',
            } else {
             data: {
                return null;
                 variables: {
            }
                    required: [ 'weight' ]
        },
        hasData: function( dataType, dataId ) {
             if( mw.calculators.hasOwnProperty( dataType ) &&
                mw.calculators[ dataType ].hasOwnProperty( dataId ) ) {
                return true;
             } else {
                 return false;
            }
        },
        initialize: function() {
            $( '.calculator' ).each( function() {
                var gadgetModule = 'ext.gadget.calculator-' + $( this ).attr( 'data-module' );
 
                if( gadgetModule && mw.loader.getState( gadgetModule ) === 'registered' ) {
                    mw.loader.load( gadgetModule );
                 }
                 }
             } );
             },
        },
            description: 'Uses 4-2-1 rule:<ul><li>4 mL/kg for the first 10 kg</li><li>2 mL/kg for the next 10 kg</li><li>1 mL/kg for the remaining weight</li></ul>',
        isMobile: function() {
            digits: 0,
            return window.matchMedia( 'only screen and (max-width: 760px)' ).matches;
             units: 'mL/hr',
        },
            formula: '',
        isValueMathObject: function( value ) {
             references: [
             return value && value.hasOwnProperty( 'value' );
                'Miller\'s Anesthesia 7e, section IV, pg. 1728'
        },
             ],
        setCookieValue: function( variableId, value ) {
            calculate: function( data ) {
             mw.cookie.set( mw.calculators.getCookieKey( variableId ), value, {
                var weight = data.weight.toNumber( 'kgwt' );
                expires: COOKIE_EXPIRATION
             } );
        },
        setValue: function( variableId, value ) {
            if( !mw.calculators.variables.hasOwnProperty( variableId ) ) {
                return false;
            }


            if( !mw.calculators.variables[ variableId ].setValue( value ) ) {
                // Uses 4-2-1 rule
                 return false;
                 var maintenanceRate = 4 * Math.min( weight, 10 );
            }
 
            mw.calculators.setCookieValue( variableId, value );


            return true;
                if( weight > 10 ) {
        },
                    maintenanceRate += 2 * Math.min( weight - 10, 10 );
        uniqueValues: function( value, index, self ) {
            return self.indexOf( value ) === index;
        }
    };
 
    /**
    * Class CalculatorObject
    *
    * @param {Object} properties
    * @param {Object} propertyValues
    * @returns {mw.calculators.objectClasses.CalculatorObject}
    * @constructor
    */
    mw.calculators.objectClasses.CalculatorObject = function( properties, propertyValues ) {
        propertyValues = propertyValues ? propertyValues : {};
 
        if( properties ) {
            if( properties.hasOwnProperty( 'required' ) ) {
                for( var iRequiredProperty in properties.required ) {
                    var requiredProperty = properties.required[ iRequiredProperty ];
 
                    if( !propertyValues || !propertyValues.hasOwnProperty( requiredProperty ) ) {
                        console.error( 'Missing required property "' + requiredProperty + '"' );
                        console.log( propertyValues );
 
                        return null;
                    }
 
                    this[ requiredProperty ] = propertyValues[ requiredProperty ];
 
                    delete propertyValues[ requiredProperty ];
                 }
                 }
            }


            if( properties.hasOwnProperty( 'optional' ) ) {
                 if( weight > 20) {
                 for( var iOptionalProperty in properties.optional ) {
                     maintenanceRate += weight - 20;
                    var optionalProperty = properties.optional[ iOptionalProperty ];
 
                    if( propertyValues && propertyValues.hasOwnProperty( optionalProperty ) ) {
                        this[ optionalProperty ] = propertyValues[ optionalProperty ];
 
                        delete propertyValues[ optionalProperty ];
                     } else if( typeof this[ optionalProperty ] === 'undefined' ) {
                        this[ optionalProperty ] = null;
                    }
                 }
                 }
            }
            var invalidProperties = Object.keys( propertyValues );


            if( invalidProperties.length ) {
                 return maintenanceRate;
                 console.warn( 'Unsupported properties defined for ' + typeof this + ' with id "' + this.id + '": ' + invalidProperties.join( ', ' ) );
             }
             }
         }
         },
    };
        intraopFluids: {
 
            name: 'Intraoperative fluid dosing',
    mw.calculators.objectClasses.CalculatorObject.prototype.getProperties = function() {
            abbreviation: 'Intraop fluids',
        return {
            data: {
            required: [],
                calculations: {
            optional: []
                    required: [ 'fluidMaintenanceRate' ]
        };
                },
    };
                variables: {
 
                    required: [ 'weight', 'npoTime', 'surgicalTrauma' ]
    mw.calculators.objectClasses.CalculatorObject.prototype.mergeProperties = function( inheritedProperties, properties ) {
                }
        var uniqueValues = function( value, index, self ) {
            },
             return self.indexOf( value ) === index;
            type: 'string',
        };
             references: [
 
                'Corcoran T, Rhodes JE, Clarke S, Myles PS, Ho KM. Perioperative fluid management strategies in major surgery: a stratified meta-analysis. Anesth Analg. 2012 Mar;114(3):640-51. doi: 10.1213/ANE.0b013e318240d6eb. Epub 2012 Jan 16. PMID: 22253274.'
        properties.required = inheritedProperties.required.concat( properties.required ).filter( uniqueValues );
        properties.optional = inheritedProperties.optional.concat( properties.optional ).filter( uniqueValues );
 
        return properties;
    };
 
 
 
 
    /**
    * Class UnitsBase
    * @param {Object} propertyValues
    * @returns {mw.calculators.objectClasses.UnitsBase}
    * @constructor
    */
    mw.calculators.objectClasses.UnitsBase = function( propertyValues ) {
        var properties = {
            required: [
                'id'
             ],
             ],
             optional: [
             calculate: function( data ) {
                 'toString'
                 var weight = data.weight.toNumber( 'kgwt' );
            ]
                var maintenanceRate = data.fluidMaintenanceRate.toNumber( 'mL/hr' );
        };
                var npoTime = data.npoTime.toNumber( 'hr' );
                var surgicalTrauma = data.surgicalTrauma;


        mw.calculators.objectClasses.CalculatorObject.call( this, properties, propertyValues );
                var output = '';
    };
 
    mw.calculators.objectClasses.UnitsBase.prototype = Object.create( mw.calculators.objectClasses.CalculatorObject.prototype );
 
 
 
 
    /**
    * Class Units
    * @param {Object} propertyValues
    * @returns {mw.calculators.objectClasses.Units}
    * @constructor
    */
    mw.calculators.objectClasses.Units = function( propertyValues ) {
        var properties = {
            required: [
                'id'
            ],
            optional: [
                'aliases',
                'baseName',
                'definition',
                'offset',
                'prefixes'
            ]
        };
 
        mw.calculators.objectClasses.CalculatorObject.call( this, properties, propertyValues );
    };
 
    mw.calculators.objectClasses.Units.prototype = Object.create( mw.calculators.objectClasses.CalculatorObject.prototype );


                var npoDeficit = npoTime * maintenanceRate;


                var surgicalLossMin, surgicalLossMax;


                if( surgicalTrauma === 'Minimal' ) {
                    surgicalLossMin = 2 * weight;
                    surgicalLossMax = 4 * weight;
                } else if( surgicalTrauma === 'Moderate' ) {
                    surgicalLossMin = 4 * weight;
                    surgicalLossMax = 6 * weight;
                } else {
                    surgicalLossMin = 6 * weight;
                    surgicalLossMax = 8 * weight;
                }


    /**
                var firstHour = Math.round( npoDeficit / 2 ) + maintenanceRate;
    * Class Variable
                var nextHoursMin = Math.round( npoDeficit / 4 ) + maintenanceRate + surgicalLossMin;
    * @param {Object} propertyValues
                var nextHoursMax = Math.round( npoDeficit / 4 ) + maintenanceRate + surgicalLossMax;
    * @returns {mw.calculators.objectClasses.Variable}
                var remainingHoursMin = maintenanceRate + surgicalLossMin;
    * @constructor
                var remainingHoursMax = maintenanceRate + surgicalLossMax;
    */
    mw.calculators.objectClasses.Variable = function( propertyValues ) {
        mw.calculators.objectClasses.CalculatorObject.call( this, this.getProperties(), propertyValues );


        if( VALID_TYPES.indexOf( this.type ) === -1 ) {
                output += 'NPO deficit: ' + Math.round( npoDeficit ) + ' mL<br/>';
            throw new Error( 'Invalid type "' + this.type + '" for variable "' + this.id + '"' );
                output += 'Surgical losses: ' + surgicalLossMin + '-' + surgicalLossMax + ' mL/hr<br/>';
        }
                output += '1st hour: ' + firstHour + ' mL<br/>';
                output += '2nd hour: ' + nextHoursMin + '-' + nextHoursMax + ' mL<br/>';
                output += '3rd hour: ' + nextHoursMin + '-' + nextHoursMax + ' mL<br/>';
                output += '4+ hours: ' + remainingHoursMin + '-' + remainingHoursMax + ' mL<br/>';


        // Accept options as either an array of strings, or an object with ids as keys and display text as values
                 return output;
        if( Array.isArray( this.options ) ) {
            var options = {};
 
            for( var iOption in this.options ) {
                 var option = this.options[ iOption ];
 
                options[ option ] = option;
             }
             }
        },
        maxAbl: {
            name: 'Maximum allowable blood loss',
            abbreviation: 'Max ABL',
            data: {
                calculations: {
                    required: [ 'ebv' ]
                },
                variables: {
                    required: [ 'weight', 'age', 'hct', 'minHct' ]
                }
            },
            digits: 0,
            units: 'mL',
            formula: '',
            references: [
                'Morgan & Mikhail\'s Clinical Anesthesiology. 5e. p1168'
            ],
            calculate: function( data ) {
                var currentHct = data.hct.toNumber( 'pcthct' );
                var minHct = data.minHct.toNumber( 'pcthct' );


            this.options = options;
                if( currentHct < minHct ) {
        }
                    return '-';
 
                }
        this.calculations = [];


        if( this.defaultValue ) {
                return data.ebv.toNumber( 'mL' ) * ( currentHct - minHct ) / currentHct;
            this.defaultValue = this.prepareValue( this.defaultValue );
        }
 
        if( this.minValue ) {
            this.minValue = this.prepareValue( this.minValue );
        }
 
        if( this.maxValue ) {
            this.maxValue = this.prepareValue( this.maxValue );
        }
 
        this.message = null;
        this.valid = true;
 
        this.value = null;
    };
 
    mw.calculators.objectClasses.Variable.prototype = Object.create( mw.calculators.objectClasses.CalculatorObject.prototype );
 
    mw.calculators.objectClasses.Variable.prototype.addCalculation = function( calculationId ) {
        if( this.calculations.indexOf( calculationId ) !== -1 ) {
            return;
        }
 
        this.calculations.push( calculationId );
    };
 
    mw.calculators.objectClasses.Variable.prototype.createInput = function( inputOptions ) {
        if( !inputOptions ) {
            inputOptions = {};
        }
 
        inputOptions.class = inputOptions.hasOwnProperty( 'class' ) ? inputOptions.class : '';
        inputOptions.global = inputOptions.hasOwnProperty( 'class' ) ? inputOptions.global : false;
        inputOptions.hideLabel = inputOptions.hasOwnProperty( 'hideLabel' ) ? inputOptions.hideLabel : false;
        inputOptions.hideLabelMobile = inputOptions.hasOwnProperty( 'hideLabelMobile' ) ? inputOptions.hideLabelMobile : false;
        inputOptions.inline = inputOptions.hasOwnProperty( 'inline' ) ? inputOptions.inline : false;
        inputOptions.inputClass = inputOptions.hasOwnProperty( 'inputClass' ) ? inputOptions.inputClass : '';
 
        var variableId = this.id;
        var inputId = 'calculator-input-' + variableId;
 
        // If not creating a global input, assign an iterated id
        if( !inputOptions.global ) {
            var inputIdCount = 0;
 
            while( $( '#' + inputId + '-' + inputIdCount ).length ) {
                inputIdCount++;
             }
             }
        },
        minUop: {
            name: 'Minimum urine output',
            abbreviation: 'Min UOP',
            data: {
                variables: {
                    required: [ 'weight', 'age' ],
                    optional: [ 'caseDuration' ]
                }
            },
            type: 'string',
            formula: '',
            references: [
                'Klahr S, Miller SB. Acute oliguria. N Engl J Med. 1998 Mar 5;338(10):671-5. doi: 10.1056/NEJM199803053381007. PMID: 9486997.',
                'Arant BS Jr. Postnatal development of renal function during the first year of life. Pediatr Nephrol. 1987 Jul;1(3):308-13. doi: 10.1007/BF00849229. PMID: 3153294.'
            ],
            calculate: function( data ) {
                var weight = data.weight.toNumber( 'kgwt' );
                var age = data.age.toNumber( 'yo' );
                var caseDuration = data.caseDuration ? data.caseDuration.toNumber( 'hr' ) : null;


            inputId += '-' + inputIdCount;
                var minUop;
        }


        var inputContainerTag = inputOptions.inline ? '<span>' : '<div>';
                if( age > 1 ) {
 
                    minUop = 0.5 * weight;
        var inputContainerAttributes = {
                 } else {
            class: 'form-group mb-0 calculator-container-input'
                     minUop = 1 * weight;
        };
 
        inputContainerAttributes.class += inputOptions.class ? ' ' + inputOptions.class : '';
        inputContainerAttributes.class += ' calculator-container-input-' + variableId;
 
        var inputContainerCss = {};
 
        // Initialize label attributes
        var labelAttributes = {
            for: inputId,
            html: this.getLabelString()
        };
 
        if( inputOptions.hideLabel || ( inputOptions.hideLabelMobile && mw.calculators.isMobile() ) ) {
            labelAttributes.class = 'sr-only';
        }
 
        var labelCss = {};
 
        if( inputOptions.inline ) {
            inputContainerTag = '<span>';
 
            inputContainerCss[ 'align-items' ] = 'center';
            inputContainerCss[ 'display' ] = 'flex';
            //inputContainerCss[ 'height' ] = 'calc(1.5em + 0.75rem + 2px)';
 
            labelAttributes.html += ':&nbsp;';
            labelCss[ 'margin-bottom' ] = 0;
        }
 
        // Create the input container
        var $inputContainer = $( inputContainerTag, inputContainerAttributes ).css( inputContainerCss );
 
        var $label = $( '<label>', labelAttributes ).css( labelCss );
 
        $inputContainer.append( $label );
 
        // 'this' will be redefined for event handlers
        var variable = this;
        var value = this.getValue();
 
        if( this.type === TYPE_NUMBER ) {
            // Initialize the primary units variables (needed for handlers, even if doesn't have units)
            var unitsId = null;
            var $unitsContainer = null;
 
            var inputValue = '';
 
            if( mw.calculators.isValueMathObject( value ) ) {
                 var number = value.toNumber();
 
                if( number ) {
                     inputValue = number;
                 }
                 }
            } else {
                inputValue = value;
            }
            // Initialize input options
            var inputAttributes = {
                id: inputId,
                class: 'form-control form-control-sm calculator-input calculator-input-text',
                type: 'text',
                autocomplete: 'off',
                inputmode: 'decimal',
                value: inputValue
            };
            // Configure additional options
            if( this.maxLength ) {
                inputAttributes.maxlength = this.maxLength;
            }
            // Add any additional classes to the input
            inputAttributes.class += inputOptions.inputClass ? ' ' + inputOptions.inputClass : '';
            // Add the input id to the list of classes
            inputAttributes.class += ' ' + inputId;
            // If the variable has units, create the units input
            if( this.hasUnits() ) {
                // Set the units id
                unitsId = inputId + '-units';
                var unitsValue = mw.calculators.isValueMathObject( value ) ? value.formatUnits() : null;
                var unitsInputAttributes = {
                    id: unitsId
                };
                // Create the units container
                $unitsContainer = $( '<div>', {
                    class: 'input-group-append'
                } ).css( 'align-items', 'center' );
                if( this.units.length === 1 ) {
                    unitsInputAttributes.type = 'hidden';
                    unitsInputAttributes.value = this.units[ 0 ];


                    $unitsContainer
                if( caseDuration ) {
                        .css( 'padding', '0 0.5em' )
                    minUop = minUop * caseDuration + ' mL';
                        .append( mw.calculators.getUnitsString( math.unit( '0 ' + this.units[ 0 ] ) ) )
                        .append( $( '<input>', unitsInputAttributes ) );
                 } else {
                 } else {
                     // Initialize the units input options
                     minUop = minUop + ' mL/hr';
                    unitsInputAttributes.class = 'custom-select custom-select-sm calculator-input-select';
 
                    // Add any additional classes to the input
                    unitsInputAttributes.class += inputOptions.inputClass ? ' ' + inputOptions.inputClass : '';
 
                    unitsInputAttributes.class = unitsInputAttributes.class + ' ' + unitsId;
 
                    var $unitsInput = $( '<select>', unitsInputAttributes )
                        .on( 'change', function() {
                            var numberValue = $( '#' + inputId ).val();
 
                            var newValue = numberValue ? numberValue + ' ' + $( this ).val() : null;
 
                            if( !mw.calculators.setValue( variableId, newValue ) ) {
                                if( variable.message ) {
                                    $( this ).parent().parent().parent().find( '.invalid-feedback' ).html( variable.message );
                                }
 
                                $( this ).parent().parent().addClass( 'is-invalid' );
                            } else {
                                $( this ).parent().parent().removeClass( 'is-invalid' );
                            }
                        } );
 
                    for( var iUnits in this.units ) {
                        var units = this.units[ iUnits ];
 
                        var unitsOptionAttributes = {
                            html: mw.calculators.getUnitsString( math.unit( '0 ' + units ) ),
                            value: units
                        };
 
                        if( units === unitsValue ) {
                            unitsOptionAttributes.selected = true;
                        }
 
                        $unitsInput.append( $( '<option>', unitsOptionAttributes ) );
                    }
 
                    $unitsContainer.append( $unitsInput );
                 }
                 }
            }
            // Create the input and add handlers
            var $input = $( '<input>', inputAttributes )
                .on( 'input', function() {
                    var numberValue = $( this ).val();
                    var newValue = numberValue ? numberValue : null;
                    if( newValue && unitsId ) {
                        newValue = newValue + ' ' + $( '#' + unitsId ).val();
                    }
                    if( !mw.calculators.setValue( variableId, newValue ) ) {
                        if( variable.message ) {
                            $( this ).parent().parent().find( '.invalid-feedback' ).html( variable.message );
                        }


                        $( this ).parent().addClass( 'is-invalid' );
                 return minUop;
                    } else {
                        $( this ).parent().removeClass( 'is-invalid' );
                    }
                 } );
 
            // Create the input group
            var $inputGroup = $( '<div>', {
                class: 'input-group'
            } ).append( $input );
 
            if( $unitsContainer ) {
                $inputGroup.append( $unitsContainer );
             }
             }
        },
        systolicBloodPressure: {
            name: 'Systolic blood pressure',
            abbreviation: 'SBP',
            data: {
                variables: {
                    required: [ 'age' ]
                }
            },
            type: 'string',
            references: [
                'Baby Miller 6e, ch. 16, pg. 550'
            ],
            calculate: function( data ) {
                var age = data.age.toNumber( 'yo' );


            $inputContainer.append( $inputGroup );
                 var systolicMin, systolicMax, diastolicMin, diastolicMax, meanMin, meanMax;
        } else if( this.type === TYPE_STRING ) {
            if( this.hasOptions() ) {
                 var optionKeys = Object.keys( this.options );


                 if( optionKeys.length === 1 ) {
                 if( age >= 16 ) {
                     $inputContainer.append( this.options[ optionKeys[ 0 ] ] );
                    systolicMin = 100;
                    systolicMax = 125;
                } else if( age >= 13 ) {
                    systolicMin = 95;
                    systolicMax = 120;
                } else if( age >= 9 ) {
                    systolicMin = 90;
                    systolicMax = 115;
                } else if( age >= 6 ) {
                    systolicMin = 85;
                    systolicMax = 105;
                } else if( age >= 3 ) {
                    systolicMin = 80;
                    systolicMax = 100;
                } else if( age >= 1 ) {
                     systolicMin = 75;
                    systolicMax = 95;
                } else if( age >= 6 / 12 ) {
                    systolicMin = 70;
                    systolicMax = 90;
                } else if( age >= 1 / 12 ) {
                    systolicMin = 65;
                    systolicMax = 85;
                 } else {
                 } else {
                     var selectAttributes = {
                     systolicMin = 60;
                        id: inputId,
                     systolicMax = 75;
                        class: 'custom-select custom-select-sm calculator-input calculator-input-select'
                    };
 
                    // Add any additional classes to the input
                    selectAttributes.class += inputOptions.inputClass ? ' ' + inputOptions.inputClass : '';
 
                    var $select = $( '<select>', selectAttributes )
                        .on( 'change', function() {
                            if( !mw.calculators.setValue( variableId, newValue ) ) {
                                if( variable.message ) {
                                    $( this ).parent().parent().find( '.invalid-feedback' ).html( variable.message );
                                }
 
                                $( this ).parent().addClass( 'is-invalid' );
                            } else {
                                $( this ).parent().removeClass( 'is-invalid' );
                            }
 
                        } );
 
                     for( var optionId in this.options ) {
                        var displayText = this.options[ optionId ];
 
                        var optionAttributes = {
                            value: optionId,
                            text: displayText
                        };
 
                        if( optionId === value ) {
                            optionAttributes.selected = true;
                        }
 
                        $select.append( $( '<option>', optionAttributes ) );
                    }
 
                    $inputContainer.append( $select );
                 }
                 }
             }
             }
         }
         }
    } );


         if( $inputContainer.length ) {
    // Cardiovascular
             $inputContainer.append( $( '<div>', {
    mw.calculators.addCalculations( {
                 class: 'invalid-feedback'
         vO2: {
             } ) );
            name: 'Rate of oxygen consumption (VO<sub>2</sub>)',
        }
             abbreviation: 'VO<sub>2</sub>',
            data: {
                calculations: {
                    required: [ 'bsa' ]
                },
                 variables: {
                    optional: [ 'age' ]
                }
             },
            units: 'mL/min',
            references: [],
            calculate: function( data ) {
                var bsa = data.bsa.toNumber();
                var age = data.age ? data.age.toNumber( 'yr' ) : null;


        return $inputContainer;
                 if( age >= 70 ) {
    };
                    return 110 * bsa;
 
                } else {
    mw.calculators.objectClasses.Variable.prototype.getLabelString = function() {
                    return 125 * bsa;
        return mw.calculators.isMobile() && this.abbreviation ? this.abbreviation : this.name;
    };
 
    mw.calculators.objectClasses.Variable.prototype.getProperties = function() {
        return {
            required: [
                'id',
                'name',
                'type'
            ],
            optional: [
                'abbreviation',
                'defaultValue',
                'maxLength',
                'maxValue',
                'minValue',
                'options',
                 'units'
            ]
        };
    };
 
    mw.calculators.objectClasses.Variable.prototype.getValue = function() {
        if( !this.valid ) {
            return null;
        } else if( this.value !== null ) {
            return this.value;
        } else if( this.defaultValue !== null ) {
            return this.defaultValue;
        } else {
            return null;
        }
    };
 
    mw.calculators.objectClasses.Variable.prototype.getValueString = function() {
        return String( this.getValue() );
    };
 
    mw.calculators.objectClasses.Variable.prototype.hasOptions = function() {
        return this.options !== null;
    };
 
    mw.calculators.objectClasses.Variable.prototype.hasUnits = function() {
        return this.units !== null;
    };
 
    mw.calculators.objectClasses.Variable.prototype.hasValue = function() {
        var value = this.getValue();
 
        if( value === null ||
            ( mw.calculators.isValueMathObject( value ) && !value.toNumber() ) ) {
            return false;
        }
 
        return true;
    };
 
    mw.calculators.objectClasses.Variable.prototype.isValueMathObject = function() {
        return mw.calculators.isValueMathObject( this.value );
    };
 
    mw.calculators.objectClasses.Variable.prototype.prepareValue = function( value ) {
        if( value !== null ) {
            if( this.type === TYPE_NUMBER ) {
                if( !mw.calculators.isValueMathObject( value ) ) {
                    value = math.unit( value );
                 }
                 }
             }
             }
         }
         },
 
        cardiacOutputFick: {
        return value;
            name: 'Cardiac output (Fick)',
    };
            abbreviation: 'CO (Fick)',
 
            data: {
    mw.calculators.objectClasses.Variable.prototype.setValue = function( value ) {
                variables: {
        var validateResult = this.validateValue( value );
                    required: [ 'saO2', 'smvO2', 'hgb' ]
 
                },
        this.valid = !!validateResult.valid;
                calculations: {
        this.message = validateResult.message;
                    required: [ 'vO2' ]
 
                }
        if( !this.valid ) {
            },
            this.value = null;
            units: 'L/min',
            this.valueUpdated();
            formula: '<math>\\mathrm{CO_{Fick}}=\\frac{VO_2}{(S_aO_2 - S_{mv}O_2) * H_b * 13.4}</math>',
 
            link: false,
            return false;
            references: [],
        }
            calculate: function( data ) {
 
                var vO2 = data.vO2.toNumber( 'mL/min' );
        this.value = this.prepareValue( value );
                var saO2 = data.saO2.toNumber() / 100;
                var smvO2 = data.smvO2.toNumber() / 100;
                var hgb = data.hgb.toNumber( 'ghgbperdL' );


        this.valueUpdated();
                return vO2 / ( ( saO2 - smvO2 ) * hgb * 13.4 );
 
        return true;
    };
 
    mw.calculators.objectClasses.Variable.prototype.toString = function() {
        return this.getLabelString();
    };
 
    mw.calculators.objectClasses.Variable.prototype.validateValue = function( value ) {
        // Initialize valid flag to true. Will be set false if an error is found.
        result = {
            message: null,
            valid: true
        };
 
        // (At least for now) unsetting a variable is always valid
        if( value === null ) {
            return result;
        }
 
        // Some errors which are plausibly from normal user input we will show as feedback on the input (e.g.
        // a numeric value that is below the minimum value. Errors which are unlikely to be from user input
        // and instead relate to developer issues (e.g. incorrect units in select boxes), only show on the console.
        var consoleWarnPrefix = 'Could not set value "' + value + '" for "' + this.id + '":';
 
        if( this.type === TYPE_NUMBER ) {
            if( !mw.calculators.isValueMathObject( value ) ) {
                value = math.unit( value );
             }
             }
 
        },
             var valueUnits;
        cardiacIndex: {
 
             name: 'Cardiac index',
             if( this.hasUnits() ) {
            abbreviation: 'CI',
                 valueUnits = value.formatUnits();
             data: {
 
                 calculations: {
                if( !valueUnits ) {
                     required: [ 'bsa', 'cardiacOutputFick' ]
                     // Unlikely to be a user error, so don't set message.
                    result.valid = false;
 
                    console.warn( consoleWarnPrefix + 'Value must define units' );
                } else if( this.units.indexOf( valueUnits ) === -1 ) {
                    // Unlikely to be a user error, so don't set message.
                    result.valid = false;
 
                    console.warn( consoleWarnPrefix + 'Units "' + valueUnits + '" are not valid for this variable' );
                 }
                 }
             }
             },
 
            units: 'L/min/m^2',
             if( this.minValue && math.smaller( value, this.minValue ) ) {
             formula: '<math>\\mathrm{CI}=\\frac{\\mathrm{CO}}{\\mathrm{BSA}}</math>',
                var minValueString = valueUnits ? mw.calculators.getValueString( this.minValue.to( valueUnits ) ) :
            link: false,
                    mw.calculators.getValueString( this.minValue );
            references: [],
 
             calculate: function( data ) {
                result.message = String( this ) + ' must be at least ' + minValueString;
                 var cardiacOutput = data.cardiacOutputFick.toNumber( 'L/min' );
                result.valid = false;
                var bsa = data.bsa.toNumber( 'm^2' );
             } else if( this.maxValue && math.larger( value, this.maxValue ) ) {
                 var maxValueString = valueUnits ? mw.calculators.getValueString( this.maxValue.to( valueUnits ) ) :
                    mw.calculators.getValueString( this.maxValue );


                 result.message = String( this ) + ' must be less than ' + maxValueString;
                 return cardiacOutput / bsa;
                result.valid = false;
             }
             }
         } else if( this.hasOptions() ) {
         },
             if( !this.options.hasOwnProperty( value ) ) {
        strokeVolume: {
                 // Unlikely to be a user error, so don't set message
            name: 'Stroke volume',
                 result.valid = false;
            abbreviation: 'SV',
            data: {
                variables: {
                    required: [ 'heartRate' ]
                },
                calculations: {
                    required: [ 'cardiacOutputFick' ]
                }
            },
            units: 'mL',
            formula: '<math>\\mathrm{SV}=\\frac{\\mathrm{CO}}{\\mathrm{HR}}</math>',
             link: false,
            references: [],
            calculate: function( data ) {
                 var cardiacOutput = data.cardiacOutputFick.toNumber( 'mL/min' );
                 var heartRate = data.heartRate.toNumber();


                 console.warn( consoleWarnPrefix + 'Value must be one of: ' + Object.keys( this.options ).join( ', ' ) );
                 return cardiacOutput / heartRate;
             }
             }
         }
         }
    } );


        return result;
     // Neuro
    };
     mw.calculators.addCalculations( {
 
         brainMass: {
    mw.calculators.objectClasses.Variable.prototype.valueUpdated = function() {
             name: 'Brain mass',
        for( var iCalculation in this.calculations ) {
             data: {
            var calculation = mw.calculators.getCalculation( this.calculations[ iCalculation ] );
                 variables: {
 
                     optional: [ 'age', 'gender' ]
            if( calculation ) {
                }
                calculation.render();
             },
            }
             digits: 0,
        }
             units: 'gwt',
    };
             references: [
 
                'Dekaban AS. Changes in brain weights during the span of human life: relation of brain weights to body heights and body weights. Ann Neurol. 1978 Oct;4(4):345-56. doi: 10.1002/ana.410040410. PMID: 727739.'
 
 
     /**
    * Class AbstractCalculation
    * @param {Object} propertyValues
    * @returns {mw.calculators.objectClasses.AbstractCalculation}
    * @constructor
    */
     mw.calculators.objectClasses.AbstractCalculation = function( propertyValues ) {
         mw.calculators.objectClasses.CalculatorObject.call( this, this.getProperties(), propertyValues );
 
        this.initialize();
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype = Object.create( mw.calculators.objectClasses.CalculatorObject.prototype );
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.addCalculation = function( calculationId ) {
        if( this.calculations.indexOf( calculationId ) !== -1 ) {
             return;
        }
 
        this.calculations.push( calculationId );
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.doRender = function() {
        throw new Error( 'AbstractCalculation child class "' + this.getClassName() + '" must implement doRender()' );
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.getClassName = function() {
        throw new Error( 'AbstractCalculation child class must implement getClassName()' );
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.getContainerClasses = function() {
        return this.getElementClasses();
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.getContainerId = function() {
        return this.getElementPrefix() + '-' + this.id;
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.getDescription = function() {
        return this.description;
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.getElementPrefix = function( useClassName ) {
        var elementPrefix = 'calculator-';
 
        elementPrefix += useClassName ? this.getClassName() : 'calculation';
 
        return elementPrefix;
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.getElementClasses = function( elementId ) {
        elementId = elementId ? '-' + elementId : '';
 
        return this.getElementPrefix() + elementId + ' ' +
             this.getElementPrefix( true ) + elementId + ' ' +
            this.getContainerId() + elementId;
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.getFormula = function() {
        return this.formula;
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.getInfo = function( infoCount ) {
        var infoHtml = '';
 
        var description = this.getDescription();
 
        if( description ) {
            infoHtml += $( '<p>', {
                html: description
            } )[ 0 ].outerHTML;
        }
 
        var formula = this.getFormula();
 
        if( formula ) {
            infoHtml += $( '<div>', {
                 class: this.getElementClasses( 'formula' )
            } )[ 0 ].outerHTML;
        }
 
        var references = this.getReferences();
 
        if( references.length ) {
            var $references = $( '<ol>' );
 
            for( var iReference in references ) {
                $references.append( $( '<li>', {
                     text: references[ iReference ]
                } ) );
            }
 
            infoHtml += $( '<div>', {
                class: this.getElementClasses( 'references' )
            } ).append( $references )[ 0 ].outerHTML;
        }
 
        var infoContainerId = this.getContainerId() + '-info';
 
        if( infoCount ) {
             infoContainerId += '-' + infoCount;
        }
 
        $infoContainer = $( '<div>', {
             id: infoContainerId,
             class: 'collapse row no-gutters border-top ' + this.getElementClasses( 'info' )
        } ).append( infoHtml );
 
        return $infoContainer;
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.getInfoButton = function( infoCount ) {
        var infoContainerId = this.getContainerId() + '-info';
 
        if( infoCount ) {
             infoContainerId += '-' + infoCount;
        }
 
        return $( '<span>', {
            class: this.getElementClasses( 'infoButton' )
        } )
            .append( $( '<a>', {
                'data-toggle': 'collapse',
                href: '#' + infoContainerId,
                role: 'button',
                'aria-expanded': 'false',
                'aria-controls': infoContainerId
            } )
                .append( $( '<i>', {
                    class: 'far fa-question-circle'
                } ) ) );
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.getProperties = function() {
        return {
            required: [
                'id',
                'calculate'
             ],
             ],
             optional: [
             calculate: function( data ) {
                 'data',
                 var age = data.age ? data.age.toNumber( 'yr' ) : null;
                'description',
                 var gender = data.gender ? data.gender : null;
                 'formula',
                'onRender',
                'onRendered',
                'references',
                'type'
            ]
        };
    };


    mw.calculators.objectClasses.AbstractCalculation.prototype.getReferences = function() {
                var brainMassFemale = 1290;
        return this.references;
                var brainMassMale = 1450;
    };


    mw.calculators.objectClasses.AbstractCalculation.prototype.getTitleHtml = function() {
                if( age !== null ) {
        return this.getTitleString();
                    if( age <= 10 / 365 ) {
    };
                        // <=10 days
                        brainMassFemale = 360;
                        brainMassMale = 380;
                    } else if( age <= 4 * 30 / 365 ) {
                        // Less than 4 months. This is a gap in the reported data of the paper, so linearly interpolate?
                        var ageFactor = 1 - ( 4 * 30 / 365 - age ) / ( 4 * 30 / 365 - 10 / 365 );


    mw.calculators.objectClasses.AbstractCalculation.prototype.getTitleString = function() {
                        brainMassFemale = 360 + ageFactor * ( 580 - 360 );
        return this.id;
                        brainMassMale = 380 + ageFactor * ( 640 - 380 );
    };
                    } else if( age <= 8 * 30 / 365 ) {
                        // <=8 months
                        brainMassFemale = 580;
                        brainMassMale = 640;
                    } else if( age <= 18 * 30 / 365 ) {
                        // <=18 months
                        brainMassFemale = 940;
                        brainMassMale = 970;
                    } else if( age <= 30 * 30 / 365 ) {
                        // <=30 months
                        brainMassFemale = 1040;
                        brainMassMale = 1120;
                    } else if( age <= 43 * 30 / 365 ) {
                        // <=43 months
                        brainMassFemale = 1090;
                        brainMassMale = 1270;
                    } else if( age <= 5 ) {
                        brainMassFemale = 1150;
                        brainMassMale = 1300;
                    } else if( age <= 7 ) {
                        brainMassFemale = 1210;
                        brainMassMale = 1330;
                    } else if( age <= 9 ) {
                        brainMassFemale = 1180;
                        brainMassMale = 1370;
                    } else if( age <= 12 ) {
                        brainMassFemale = 1260;
                        brainMassMale = 1440;
                    } else if( age <= 15 ) {
                        brainMassFemale = 1280;
                        brainMassMale = 1410;
                    } else if( age <= 18 ) {
                        brainMassFemale = 1340;
                        brainMassMale = 1440;
                    } else if( age <= 21 ) {
                        brainMassFemale = 1310;
                        brainMassMale = 1450;
                    } else if( age <= 30 ) {
                        brainMassFemale = 1300;
                        brainMassMale = 1440;
                    } else if( age <= 40 ) {
                        brainMassFemale = 1290;
                        brainMassMale = 1440;
                    } else if( age <= 50 ) {
                        brainMassFemale = 1290;
                        brainMassMale = 1430;
                    } else if( age <= 55 ) {
                        brainMassFemale = 1280;
                        brainMassMale = 1410;
                    } else if( age <= 60 ) {
                        brainMassFemale = 1250;
                        brainMassMale = 1370;
                    } else if( age <= 65 ) {
                        brainMassFemale = 1240;
                        brainMassMale = 1370;
                    } else if( age <= 70 ) {
                        brainMassFemale = 1240;
                        brainMassMale = 1360;
                    } else if( age <= 75 ) {
                        brainMassFemale = 1230;
                        brainMassMale = 1350;
                    } else if( age <= 80 ) {
                        brainMassFemale = 1190;
                        brainMassMale = 1330;
                    } else if( age <= 85 ) {
                        brainMassFemale = 1170;
                        brainMassMale = 1310;
                    } else {
                        brainMassFemale = 1140;
                        brainMassMale = 1290;
                    }
                }


    mw.calculators.objectClasses.AbstractCalculation.prototype.getValue = function() {
                if( gender === 'F' ) {
        // For now, we always need to recalculate, since the calculation may not be rendered but still required by
                    return brainMassFemale;
        // other calculations (i.e. drug dosages using lean body weight).
                } else if( gender === 'M' ) {
        this.recalculate();
                    return brainMassMale;
 
                 } else {
        return this.value;
                    return ( brainMassFemale + brainMassMale ) / 2;
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.hasInfo = function() {
        return this.description || this.formula || this.references.length;
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.hasValue = function() {
        if( this.value === null ||
            ( this.isValueMathObject() && !this.value.toNumber() ) ) {
            return false;
        }
 
        return true;
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.getCalculationData = function() {
        return this.data;
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.getCalculationDataValues = function() {
        var calculationData = this.getCalculationData();
 
        var data = {};
        var missingRequiredData = '';
        var calculationId, calculation, variableId, variable;
 
        for( var iRequiredCalculation in calculationData.calculations.required ) {
            calculationId = calculationData.calculations.required[ iRequiredCalculation ];
            calculation = mw.calculators.getCalculation( calculationId );
 
            if( !calculation ) {
                 throw new Error( 'Invalid required calculation "' + calculationId + '" for calculation "' + this.id + '"' );
            } else if( !calculation.hasValue() ) {
                if( missingRequiredData ) {
                    missingRequiredData = missingRequiredData + ', ';
                 }
                 }
                missingRequiredData = missingRequiredData + String( calculation );
            } else {
                data[ calculationId ] = calculation.value;
             }
             }
         }
         },
 
         cerebralBloodVolume: {
         for( var iRequiredVariable in calculationData.variables.required ) {
             name: 'Cerebral blood volume',
             variableId = calculationData.variables.required[ iRequiredVariable ];
             abbreviation: 'CBV',
            variable = mw.calculators.getVariable( variableId );
             data: {
 
                 calculations: {
             if( !variable ) {
                     required: [ 'brainMass' ]
                throw new Error( 'Invalid required variable "' + variableId + '" for calculation "' + this.id + '"' );
             } else if( !variable.hasValue() ) {
                 if( missingRequiredData ) {
                     missingRequiredData = missingRequiredData + ', ';
                 }
                 }
            },
            units: 'mL',
            description: '4 mL per 100g of brain mass',
            references: [
                'Tameem A, Krovvidi H, Cerebral physiology, Continuing Education in Anaesthesia Critical Care & Pain, Volume 13, Issue 4, August 2013, Pages 113–118, https://doi.org/10.1093/bjaceaccp/mkt001'
            ],
            calculate: function( data ) {
                var brainMass = data.brainMass.toNumber( 'gwt' );


                 missingRequiredData = missingRequiredData + String( variable );
                 return 4 * brainMass / 100;
            } else {
                data[ variableId ] = variable.getValue();
             }
             }
         }
         },
        cerebralMetabolicRateFactor: {
            name: 'Cerebral metabolic rate factor',
            abbreviation: '%CMR',
            data: {
                variables: {
                    optional: [ 'temperature' ]
                }
            },
            description: '7% change in CMR for every 1 &deg;C change in temperature',
            references: [
                'Tameem A, Krovvidi H, Cerebral physiology, Continuing Education in Anaesthesia Critical Care & Pain, Volume 13, Issue 4, August 2013, Pages 113–118, https://doi.org/10.1093/bjaceaccp/mkt001'
            ],
            calculate: function( data ) {
                var temperature = data.temperature ? data.temperature.toNumber( 'degC' ) : null;


        if( missingRequiredData ) {
                var cerebralMetabolicRateFactor = 1;
            this.message = missingRequiredData + ' required';


            return false;
                if( temperature ) {
        }
                    cerebralMetabolicRateFactor += 0.07 * ( temperature - 37 );
                }


        for( var iOptionalCalculation in calculationData.calculations.optional ) {
                 return cerebralMetabolicRateFactor;
            calculationId = calculationData.calculations.optional[ iOptionalCalculation ];
            calculation = mw.calculators.getCalculation( calculationId );
 
            if( !calculation ) {
                 throw new Error( 'Invalid optional calculation "' + calculationId + '" for calculation "' + this.id + '"' );
             }
             }
        },
        cerebralMetabolicRateO2: {
            name: 'Cerebral metabolic rate (O<sub>2</sub>)',
            abbreviation: 'CMRO<sub>2</sub>',
            data: {
                calculations: {
                    required: [ 'brainMass', 'cerebralMetabolicRateFactor' ]
                },
                variables: {
                    optional: [ 'temperature' ]
                }
            },
            units: 'mL/min',
            description: '3 mL O<sub>2</sub>/min per 100g of brain mass. 7% change in CMR for every 1 &deg;C change in temperature.',
            references: [
                'Tameem A, Krovvidi H, Cerebral physiology, Continuing Education in Anaesthesia Critical Care & Pain, Volume 13, Issue 4, August 2013, Pages 113–118, https://doi.org/10.1093/bjaceaccp/mkt001'
            ],
            calculate: function( data ) {
                // Temperature is included as an optional variable to generate the input.
                // It is used by cerebralMetabolicRateFactor, which is an internal calculation not typically shown.
                var brainMass = data.brainMass.toNumber( 'gwt' );
                var cerebralMetabolicRateFactor = data.cerebralMetabolicRateFactor.toNumber();


            data[ calculationId ] = calculation.hasValue() ? calculation.value : null;
                 return cerebralMetabolicRateFactor * 3 * brainMass / 100;
        }
 
        for( var iOptionalVariable in calculationData.variables.optional ) {
            variableId = calculationData.variables.optional[ iOptionalVariable ];
            variable = mw.calculators.getVariable( variableId );
 
            if( !variable ) {
                 throw new Error( 'Invalid optional variable "' + variableId + '" for calculation "' + this.id + '"' );
             }
             }
 
         },
            data[ variableId ] = variable.hasValue() ? variable.getValue() : null;
         cerebralMetabolicRateGlucose: {
        }
             name: 'Cerebral metabolic rate (Glucose)',
 
            abbreviation: 'CMR<sub>glu</sub>',
         return data;
             data: {
    };
                 calculations: {
 
                     required: [ 'brainMass', 'cerebralMetabolicRateFactor' ]
    mw.calculators.objectClasses.AbstractCalculation.prototype.initialize = function() {
         if( typeof this.calculate !== 'function' ) {
             throw new Error( 'calculate() must be a function for Calculation "' + this.id + '"' );
        }
 
        // Initialize array to store calculation ids which depend on this calculation's value
        this.calculations = [];
 
        this.data = new mw.calculators.objectClasses.CalculationData( this.getCalculationData() );
 
        this.references = this.references ? this.references : [];
        this.type = this.type ? this.type : TYPE_NUMBER;
 
        this.message = null;
        this.value = null;
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.isValueMathObject = function() {
        return mw.calculators.isValueMathObject( this.value );
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.parseFormula = function() {
        var formula = this.getFormula();
 
        if( !formula ) {
            return;
        }
 
        var api = new mw.Api();
 
        var containerId = this.getContainerId() + '-formula';
 
        api.parse( formula ).then( function( result ) {
             $( '.' + containerId ).html( result );
        } );
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.recalculate = function() {
        this.message = '';
        this.value = null;
 
        var data = this.getCalculationDataValues();
 
        if( data === false ) {
            this.valueUpdated();
 
            return false;
        }
 
        try {
            var value = this.calculate( data );
 
            if( this.type === TYPE_NUMBER && !isNaN( value ) ) {
                 if( this.units ) {
                     value = value + ' ' + this.units;
                 }
                 }
            },
            units: 'mg/min',
            description: '5 mg glucose/min per 100g of brain mass. 7% change in CMR for every 1 &deg;C change in temperature',
            references: [
                'Tameem A, Krovvidi H, Cerebral physiology, Continuing Education in Anaesthesia Critical Care & Pain, Volume 13, Issue 4, August 2013, Pages 113–118, https://doi.org/10.1093/bjaceaccp/mkt001'
            ],
            calculate: function( data ) {
                var brainMass = data.brainMass.toNumber( 'gwt' );
                var cerebralMetabolicRateFactor = data.cerebralMetabolicRateFactor.toNumber();


                 this.value = math.unit( value );
                 return cerebralMetabolicRateFactor * 5 * brainMass / 100;
            } else {
                this.value = value;
             }
             }
         } catch( e ) {
         },
            console.warn( e.message );
        cerebralBloodFlow: {
            name: 'Cerebral blood flow',
            abbreviation: 'CBF',
            data: {
                calculations: {
                    required: [ 'brainMass', 'cerebralMetabolicRateFactor' ]
                },
                variables: {
                    optional: [ 'paCO2' ]
                }
            },
            units: 'mL/min',
            description: '<ul><li>50 mL/min per 100g of brain mass.</li><li>Every mmHg in PaCO2 changes CBF by 1.5 mL/min per 100g of brain mass.</li><li>Cerebral blood flow and cerebral metabolic rate are coupled. Factors that alter CMR (e.g. temperature) will proportionally alter CBF.</li>',
            references: [
                'Tameem A, Krovvidi H, Cerebral physiology, Continuing Education in Anaesthesia Critical Care & Pain, Volume 13, Issue 4, August 2013, Pages 113–118, https://doi.org/10.1093/bjaceaccp/mkt001',
                'Brian JE Jr. Carbon dioxide and the cerebral circulation. Anesthesiology. 1998 May;88(5):1365-86. doi: 10.1097/00000542-199805000-00029. PMID: 9605698.'
            ],
            calculate: function( data ) {
                var brainMass = data.brainMass.toNumber( 'gwt' );
                var cerebralMetabolicRateFactor = data.cerebralMetabolicRateFactor.toNumber();
                var paCO2 = data.paCO2.toNumber( 'mmHg' );


            this.message = e.message;
                var cerebralBloodFlow = cerebralMetabolicRateFactor * 50 * brainMass / 100;
            this.value = null;
        } finally {
            this.valueUpdated();
        }


        return true;
                if( paCO2 ) {
    };
                    // CO2 reductions don't reduce CBF more than 50%
                    var minCerebralBloodFlow = cerebralBloodFlow / 2;


                    cerebralBloodFlow += 1.5 * brainMass / 100 * ( paCO2 - 40 );


                    cerebralBloodFlow = math.max( cerebralBloodFlow, minCerebralBloodFlow );
                }


    mw.calculators.objectClasses.AbstractCalculation.prototype.render = function() {
                 return cerebralBloodFlow;
        this.recalculate();
 
        if( typeof this.onRender === 'function' ) {
            this.onRender();
        }
 
        this.doRender();
 
        this.parseFormula();
 
        if( typeof this.onRendered === 'function' ) {
            this.onRendered();
        }
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.setDependencies = function() {
        this.data = this.getCalculationData();
 
        var calculationIds = this.data.calculations.required.concat( this.data.calculations.optional );
 
        for( var iCalculationId in calculationIds ) {
            var calculationId = calculationIds[ iCalculationId ];
 
            if( !mw.calculators.calculations.hasOwnProperty( calculationId ) ) {
                 throw new Error('Calculation "' + calculationId + '" does not exist for calculation "' + this.id + '"');
             }
             }
            mw.calculators.calculations[ calculationId ].addCalculation( this.id );
         }
         }
    } );


        var variableIds = this.data.variables.required.concat( this.data.variables.optional );


        for( var iVariableId in variableIds ) {
            var variableId = variableIds[ iVariableId ];


            if( !mw.calculators.variables.hasOwnProperty( variableId ) ) {
    // Pulmonary
                 throw new Error('Variable "' + variableId + '" does not exist for calculation "' + this.id + '"');
    mw.calculators.addCalculations( {
            }
        aaGradientO2Expected: {
            name: 'A-a O<sub>2</sub> gradient (expected)',
            abbreviation: 'A-a O<sub>2</sub> ex.',
            data: {
                variables: {
                    required: [ 'age' ]
                 }
            },
            units: 'mmHg',
            references: [
                'Hantzidiamantis PJ, Amaro E. Physiology, Alveolar to Arterial Oxygen Gradient. 2021 Feb 22. In: StatPearls [Internet]. Treasure Island (FL): StatPearls Publishing; 2021 Jan–. PMID: 31424737.'
            ],
            calculate: function( data ) {
                var age = data.age.toNumber( 'yr' );


            mw.calculators.variables[ variableId ].addCalculation( this.id );
                return ( age + 10 ) / 4;
        }
 
        this.recalculate();
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.toString = function() {
        return this.getTitleString();
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.valueUpdated = function() {
        for( var iCalculation in this.calculations ) {
            calculation = mw.calculators.getCalculation( this.calculations[ iCalculation ] );
 
            if( calculation ) {
                calculation.render();
             }
             }
         }
         },
    };
         lowTidalVolume: {
 
             name: 'Low tidal volume',
 
             abbreviation: 'LTV',
 
            data: {
    /**
                 calculations: {
    * Class CalculationData
                     required: [ 'ibw' ]
    * @param {Object} propertyValues
    * @returns {mw.calculators.objectClasses.CalculationData}
    * @constructor
    */
    mw.calculators.objectClasses.CalculationData = function( propertyValues ) {
        mw.calculators.objectClasses.CalculatorObject.call( this, this.getProperties(), propertyValues );
 
         var dataTypes = this.getDataTypes();
        var dataRequirements = this.getDataRequirements();
 
        // Iterate through the supported data types (e.g. calculation, variable) to initialize the structure
        for( var iDataType in dataTypes ) {
             var dataType = dataTypes[ iDataType ];
 
             if( !this[ dataType ] ) {
                this[ dataType ] = {
                    optional: [],
                    required: []
                };
            } else {
                 // Iterate through the requirement levels (i.e. optional, required) to initialize the structure
                for( var iDataRequirement in dataRequirements ) {
                     var dataRequirement = dataRequirements[ iDataRequirement ];
 
                    if( this[ dataType ].hasOwnProperty( dataRequirement ) ) {
                        for( var iDataId in this[ dataType ][ dataRequirement ] ) {
                            var dataId = this[ dataType ][ dataRequirement ][ iDataId ];
                        }
                    } else {
                        this[ dataType ][ dataRequirement ] = [];
                    }
                 }
                 }
             }
             },
        }
             type: 'string',
    };
             description: '6-8 mL/kg',
 
            references: [
    mw.calculators.objectClasses.CalculationData.prototype = Object.create( mw.calculators.objectClasses.CalculatorObject.prototype );
                'Acute Respiratory Distress Syndrome Network, Brower RG, Matthay MA, Morris A, Schoenfeld D, Thompson BT, Wheeler A. Ventilation with lower tidal volumes as compared with traditional tidal volumes for acute lung injury and the acute respiratory distress syndrome. N Engl J Med. 2000 May 4;342(18):1301-8. doi: 10.1056/NEJM200005043421801. PMID: 10793162.'
 
             ],
    mw.calculators.objectClasses.CalculationData.prototype.getDataRequirements = function() {
             calculate: function( data ) {
        return [
                var ibw = data.ibw.toNumber( 'kgwt' );
             'optional',
             'required'
        ];
    };
 
    mw.calculators.objectClasses.CalculationData.prototype.getDataTypes = function() {
        return [
            'calculations',
            'variables'
        ];
    };
 
    mw.calculators.objectClasses.CalculationData.prototype.getProperties = function() {
        return {
             required: [],
             optional: [
                'calculations',
                'variables'
            ]
        };
    };
 
 
 
    mw.calculators.objectClasses.CalculationData.prototype.merge = function() {
        var mergedData = new mw.calculators.objectClasses.CalculationData();


        var data = [ this ].concat( Array.prototype.slice.call( arguments ) );
                 return math.round( 6 * ibw ) + '-' + math.round( 8 * ibw ) + ' mL';
 
        var dataTypes = this.getDataTypes();
 
        for( var iData in data ) {
            for( var iDataType in dataTypes ) {
                var dataType = dataTypes[ iDataType ];
 
                 mergedData[ dataType ].required = mergedData[ dataType ].required
                    .concat( data[ iData ][ dataType ].required )
                    .filter( mw.calculators.uniqueValues );
 
                mergedData[ dataType ].optional = mergedData[ dataType ].optional
                    .concat( data[ iData ][ dataType ].optional )
                    .filter( mw.calculators.uniqueValues );
             }
             }
         }
         }
    } );


        return mergedData;
     var tableMaxWidth = 600;
     };


 
     mw.calculators.addCalculators( moduleId, {
 
         anatomy: {
 
            name: 'Patient statistics',
 
             calculations: [
     /**
                'bmi',
    * Class SimpleCalculation
                'bsa',
    * @param {Object} propertyValues
                'ibw',
    * @returns {mw.calculators.objectClasses.SimpleCalculation}
                'lbw'
    * @constructor
            ],
    */
            css: {
    mw.calculators.objectClasses.SimpleCalculation = function( propertyValues ) {
                'max-width': tableMaxWidth
         mw.calculators.objectClasses.CalculatorObject.call( this, this.getProperties(), propertyValues );
 
        this.initialize();
    };
 
    mw.calculators.objectClasses.SimpleCalculation.prototype = Object.create( mw.calculators.objectClasses.AbstractCalculation.prototype );
 
    mw.calculators.objectClasses.SimpleCalculation.prototype.doRender = function() {
        var $calculationContainer = $( '.' + this.getContainerId() );
 
        if( !$calculationContainer.length ) {
             return;
        }
 
        // Add all required classes
        $calculationContainer.addClass( 'border ' + this.getContainerClasses() );
 
        // Get a string version of the calculation's value
        var valueString = this.getValueString();
 
        // We will need to show variable inputs for non-global variable inputs.
        // Global inputs (i.e. those in the header) will claim the DOM id for that variable.
        // Non-global inputs (i.e. specific to a calculation) will only set a class but not the id,
        // and thus will get added to each calculation even if a duplicate.
        // E.g. 2 calculation might use the current hematocrit, but we should show them for both calculations since
        // it wouldn't be obvious the input that only showed the first time would apply to both calculations.
        var inputVariableIds = this.data.variables.required.concat( this.data.variables.optional );
        var missingVariableInputs = [];
 
        for( var iInputVariableId in inputVariableIds ) {
            var variableId = inputVariableIds[ iInputVariableId ];
 
            if( !$( '#calculator-input-' + variableId ).length ) {
                missingVariableInputs.push( variableId );
             }
             }
         }
         },
 
         fluidManagement: {
        // Out of 12, uses Bootstrap col- classes in a container
            name: 'Fluid management',
         var titleColumns = '7';
             calculations: [
        var valueColumns = '5';
                 'ebv',
 
                 'fluidMaintenanceRate',
        // Store this object in a local variable since .each() will reassign this to the DOM object of each
                 'intraopFluids',
        // calculation container.
                 'maxAbl',
        var calculation = this;
                'minUop'
        var calculationCount = 0;
             ],
 
            css: {
        // Eventually may implement different rendering, so we should regenerate
                 'max-width': tableMaxWidth
        // all elements with each iteration of the loop.
        // I.e. might show result in table and inline in 2 different places of article.
        $calculationContainer.each( function() {
             // Initalize the variables for all the elements of the calculation. These need to be in order of placement
            // in the calculation container
            var elementTypes = [
                 'title',
                 'variables',
                 'value',
                 'info'
            ];
 
            var elements = {};
 
             for( var iElementType in elementTypes ) {
                var elementType = elementTypes[ iElementType ];
 
                elements[ elementType ] = {
                    $container: null,
                    id: calculation.getContainerId() + '-' + elementType
                };
 
                 if( calculationCount ) {
                    elements[ elementType ].id += '-' + calculationCount;
                }
             }
             }
 
        },
             // Create title element and append to container
        cardiovascular: {
             elements.title.$container = $( '<div>', {
             name: 'Cardiovascular',
                 id: elements.title.id
             calculations: [
            } );
                'vO2',
 
                 'cardiacOutputFick',
             elements.title.$container.append( calculation.getTitleHtml() );
                'cardiacIndex',
 
                'strokeVolume'
             if( calculation.hasInfo() ) {
             ],
                 elements.title.$container.append( calculation.getInfoButton( calculationCount ) );
             css: {
 
                 'max-width': tableMaxWidth
                // Id of the info container should already be set by getInfo()
                elements.info.$container = calculation.getInfo();
             }
             }
 
        },
            // Create the value element
        neuro: {
            elements.value.$container = $( '<div>', {
             name: 'Neuro',
                class: 'col-' + valueColumns + ' ' + calculation.getElementClasses( 'value' )
             calculations: [
             } ).append( valueString );
                 'brainMass',
 
                 'cerebralBloodVolume',
            if( !missingVariableInputs.length ) {
                'cerebralMetabolicRateO2',
                // If we have no variable inputs to show, we can put the title and value in one row of the table
                 'cerebralMetabolicRateGlucose',
                elements.title.$container.addClass( 'col-' + titleColumns + ' border-right' );
                 'cerebralBloodFlow'
 
            ],
                // Add the id attribute to the value container
            css: {
                elements.value.$container.attr( 'id', elements.value.id );
                 'max-width': tableMaxWidth
             } else {
                 // If we need to show variable inputs, make the title span the full width of the container,
                // put the variable inputs on a new row, and show the result on a row below that.
                elements.title.$container.addClass( 'col-12 border-bottom' );
 
                // Create a new row for the variable inputs
                 elements.variables.$container = $( '<div>', {
                    class: 'row no-gutters border-bottom ' + calculation.getElementClasses( 'variables' ),
                    id: elements.variables.id
                 } )
                    .append( $( '<div>', {
                        class: 'col-12'
                    } )
                        .append( mw.calculators.createInputGroup( missingVariableInputs ) ) );
 
                 elements.value.$container = $( '<div>', {
                    class: 'row no-gutters',
                    id: elements.value.id
                 } )
                    .append(
                        $( '<div>', {
                            class: 'col-' + titleColumns,
                            html: '&nbsp;'
                        } ),
                        elements.value.$container
                );
             }
             }
 
        },
            // Add the title classes after the layout classes
        pulmonary: {
             elements.title.$container.addClass( calculation.getElementClasses( 'title' ) );
             name: 'Pulmonary',
 
             calculations: [
             // Iterate over elementTypes since it is in order of rendering
                 'aaGradientO2Expected',
            for( var iElementType in elementTypes ) {
                 'lowTidalVolume'
                var elementType = elementTypes[ iElementType ];
 
                var $existingContainer = $( '#' + elements[ elementType ].id );
 
                if( $existingContainer.length ) {
                    // If an input within this container has focus (i.e. the user changed a variable input which
                    // triggered this rerender), don't rerender the element as this would destroy the focus on
                    // the input.
                    if( !$.contains( $existingContainer[ 0 ], $( ':focus' )[ 0 ] ) ) {
                        $existingContainer.replaceWith( elements[ elementType ].$container );
                    }
                } else {
                    $( this ).append( elements[ elementType ].$container );
                 }
            }
 
            calculationCount++;
        } );
    };
 
    mw.calculators.objectClasses.SimpleCalculation.prototype.getClassName = function() {
        return 'SimpleCalculation';
    };
 
    mw.calculators.objectClasses.SimpleCalculation.prototype.getProperties = function() {
        var inheritedProperties = mw.calculators.objectClasses.AbstractCalculation.prototype.getProperties();
 
        return this.mergeProperties( inheritedProperties, {
            required: [
                 'name'
             ],
             ],
             optional: [
             css: {
                 'abbreviation',
                 'max-width': tableMaxWidth
                'digits',
                'link',
                'units'
            ]
        } );
    };
 
    mw.calculators.objectClasses.SimpleCalculation.prototype.getTitleHtml = function() {
        var titleHtml = this.getTitleString();
 
        if( this.link ) {
            var href = this.link;
 
            // Detect internal links (this isn't great)
            var matches = href.match( /\[\[(.*?)\]\]/ );
 
            if( matches ) {
                href = mw.util.getUrl( matches[ 1 ] );
             }
             }
            titleHtml = $( '<a>', {
                href: href,
                text: titleHtml
            } )[ 0 ].outerHTML;
        }
        return titleHtml;
    };
    mw.calculators.objectClasses.SimpleCalculation.prototype.getTitleString = function() {
        return mw.calculators.isMobile() && this.abbreviation ? this.abbreviation : this.name;
    };
    mw.calculators.objectClasses.SimpleCalculation.prototype.getValueString = function() {
        if( this.message ) {
            return this.message;
        } else if( typeof this.value === 'object' && this.value.hasOwnProperty( 'value' ) ) {
            return mw.calculators.getValueString( this.value );
        } else {
            return String( this.value );
        }
    };
    /**
    * Class AbstractCalculator
    * @param {Object} propertyValues
    * @returns {mw.calculators.objectClasses.AbstractCalculator}
    * @constructor
    */
    mw.calculators.objectClasses.AbstractCalculator = function( propertyValues ) {
        mw.calculators.objectClasses.CalculatorObject.call( this, this.getProperties(), propertyValues );
    };
    mw.calculators.objectClasses.AbstractCalculator.prototype = Object.create( mw.calculators.objectClasses.CalculatorObject.prototype );
    mw.calculators.objectClasses.AbstractCalculator.prototype.doRender = function() {
        var $calculatorContainer = $( '.' + this.getContainerId() );
        if( !$calculatorContainer.length ) {
            return;
         }
         }
 
     } );
        $calculatorContainer.addClass( this.getContainerClasses() );
 
        if( this.css ) {
            $calculatorContainer.css( this.css );
        }
 
        $calculatorContainer.empty();
 
        $calculatorContainer.append( $( '<h4>', {
            text: this.name
        } ) );
 
        var $calculationsContainer = $( '<div>' );
 
        $calculatorContainer.append( $calculationsContainer );
 
        for( var iCalculationId in this.calculations ) {
            var calculation = mw.calculators.getCalculation( this.calculations[ iCalculationId ] );
            var calculationContainerClass = 'row no-gutters ' + calculation.getContainerId();
 
            var $calculationContainer = $( '<div>', {
                class: calculationContainerClass
            } );
 
            $calculationsContainer.append( $calculationContainer );
 
            calculation.render();
        }
    };
 
    mw.calculators.objectClasses.AbstractCalculator.prototype.getClassName = function() {
        throw new Error( 'AbstractCalculator child class must implement getClassName()' );
    };
 
    mw.calculators.objectClasses.AbstractCalculator.prototype.getContainerClasses = function() {
        return this.getElementClasses();
    };
 
    mw.calculators.objectClasses.AbstractCalculator.prototype.getContainerId = function() {
        return 'calculator-' + this.module + '-' + this.id;
    };
 
    mw.calculators.objectClasses.AbstractCalculator.prototype.getElementPrefix = function( useClassName ) {
        var elementPrefix = 'calculator-';
 
        elementPrefix += useClassName ? this.getClassName() : 'calculator';
 
        return elementPrefix;
     };
 
    mw.calculators.objectClasses.AbstractCalculator.prototype.getElementClasses = function( elementId ) {
        elementId = elementId ? '-' + elementId : '';
 
        return this.getElementPrefix() + elementId + ' ' +
            this.getElementPrefix( true ) + elementId
            this.getContainerId() + elementId;
    };
 
    mw.calculators.objectClasses.AbstractCalculator.prototype.getProperties = function() {
        return {
            required: [
                'id',
                'module',
                'name',
                'calculations'
            ],
            optional: [
                'css',
                'onRender',
                'onRendered'
            ]
        };
    };
 
    mw.calculators.objectClasses.AbstractCalculator.prototype.getTitleString = function() {
        return this.name;
    };
 
    mw.calculators.objectClasses.AbstractCalculator.prototype.render = function() {
        if( typeof this.onRender === 'function' ) {
            this.onRender();
        }
 
        this.doRender();
 
        if( typeof this.onRendered === 'function' ) {
            this.onRendered();
        }
    };
 
    mw.calculators.objectClasses.AbstractCalculator.prototype.toString = function() {
        return this.getTitleString();
    };
 
 
 
 
 
    /**
    * Class SimpleCalculator
    * @param {Object} propertyValues
    * @returns {mw.calculators.objectClasses.SimpleCalculator}
    * @constructor
    */
    mw.calculators.objectClasses.SimpleCalculator = function( propertyValues ) {
        mw.calculators.objectClasses.CalculatorObject.call( this, this.getProperties(), propertyValues );
    };
 
    mw.calculators.objectClasses.SimpleCalculator.prototype = Object.create( mw.calculators.objectClasses.AbstractCalculator.prototype );
 
    mw.calculators.objectClasses.SimpleCalculator.prototype.getClassName = function() {
        return 'SimpleCalculator';
    };
 
    mw.calculators.initialize();
 
}() );
}() );

Revision as of 19:07, 27 August 2021

( function() {
    var moduleId = 'anatomyPhysiology';

    mw.calculators.addUnitsBases( {
        bpm: {
            toString: function( units ) {
                units = units.replace( 'bpm', 'beats/min' );

                return units;
            }
        },
        hgb: {
            toString: function( units ) {
                units = units.replace( 'hgbperdL', '/dL' );
                units = units.replace( 'pcthct', '%' );

                return units;
            }
        },
        o2: {
            toString: function( units ) {
                units = units.replace( 'pcto2', '%' );

                return units;
            }
        },
        temperature: {
            toString: function( units ) {
                units = units.replace( 'deg', '&deg;' );

                return units;
            }
        }
    } );

    mw.calculators.addUnits( {
        bpm: {
            baseName: 'bpm'
        },
        pcthct: {
            baseName: 'hgb'
        },
        pcto2: {
            baseName: 'o2'
        },
        ghgbperdL: {
            baseName: 'hgb',
            prefixes: 'short',
            definition: '3 pcthct'
        }
    } );

    mw.calculators.addVariables( {
        caseDuration: {
            name: 'Case duration',
            type: 'number',
            abbreviation: 'Duration',
            minValue: '0 hr',
            maxLength: 3,
            units: [
                'hr',
                'min'
            ]
        },
        hct: {
            name: 'Current hematocrit',
            type: 'number',
            abbreviation: 'Current hct',
            minValue: '10 pcthct',
            maxValue: '75 pcthct',
            defaultValue: '45 pcthct',
            maxLength: 4,
            units: [
                'pcthct',
                'ghgbperdL'
            ]
        },
        heartRate: {
            name: 'Heart rate',
            type: 'number',
            abbreviation: 'HR',
            defaultValue: '60 bpm',
            maxLength: 4,
            units: [
                'bpm'
            ]
        },
        hgb: {
            name: 'Hemoglobin',
            type: 'number',
            abbreviation: 'HgB',
            minValue: '3 ghgbperdL',
            maxValue: '25 ghgbperdL',
            defaultValue: '13 ghgbperdL',
            maxLength: 4,
            units: [
                'pcthct',
                'ghgbperdL'
            ]
        },
        minHct: {
            name: 'Minimum hematocrit',
            type: 'number',
            abbreviation: 'Min hct',
            minValue: '10 pcthct',
            maxValue: '45 pcthct',
            defaultValue: '21 pcthct',
            maxLength: 4,
            units: [
                'pcthct',
                'ghgbperdL'
            ]
        },
        paCO2: {
            name: 'PaCO<sub>2</sub>',
            type: 'number',
            abbreviation: 'PaCO<sub>2</sub>',
            minValue: '20 mmHg',
            defaultValue: '40 mmHg',
            maxLength: 3,
            units: [
                'mmHg'
            ]
        },
        saO2: {
            name: 'SaO<sub>2</sub>',
            type: 'number',
            abbreviation: 'SaO<sub>2</sub>',
            minValue: '25 pcto2',
            maxValue: '100 pcto2',
            defaultValue: '100 pcto2',
            maxLength: 3,
            units: [
                'pcto2'
            ]
        },
        smvO2: {
            name: 'SmvO<sub>2</sub>',
            type: 'number',
            abbreviation: 'SmvO<sub>2</sub>',
            minValue: '25 pcto2',
            maxValue: '100 pcto2',
            defaultValue: '75 pcto2',
            maxLength: 3,
            units: [
                'pcto2'
            ]
        },
        npoTime: {
            name: 'Time spent NPO',
            type: 'number',
            abbreviation: 'NPO time',
            minValue: '0 hr',
            defaultValue: '8 hr',
            maxLength: 2,
            units: [
                'hr'
            ]
        },
        surgicalTrauma: {
            name: 'Severity of surgical trauma',
            type: 'string',
            abbreviation: 'Surgical trauma',
            defaultValue: 'Minimal',
            options: [
                'Minimal',
                'Moderate',
                'Severe'
            ]
        },
        temperature: {
            name: 'Temperature',
            type: 'number',
            abbreviation: 'Temp',
            minValue: '20 degC',
            maxValue: '44 degC',
            defaultValue: '37 degC',
            maxLength: 5,
            units: [
                'degC',
                'degF'
            ]
        }
    } );



    mw.calculators.addCalculations( {
        bmi: {
            name: 'Body mass index',
            abbreviation: 'BMI',
            data: {
                variables: {
                    required: [ 'weight', 'height' ]
                }
            },
            digits: 0,
            units: 'kg/m^2',
            formula: '<math>\\mathrm{BMI} = \\frac{\\mathrm{mass_{kg}}}{{(\\mathrm{height_{m}}})^2}</math>',
            link: '[[Body mass index]]',
            references: [],
            calculate: function( data ) {
                return data.weight.toNumber( 'kgwt' ) / Math.pow( data.height.toNumber( 'm' ), 2 );
            }
        },
        bsa: {
            name: 'Body surface area',
            abbreviation: 'BSA',
            data: {
                variables: {
                    required: [ 'weight', 'height' ]
                }
            },
            digits: 2,
            units: 'm^2',
            formula: '<math>\\mathrm{BSA} = \\sqrt{\\frac{\\mathrm{weight_{kg}}*\\mathrm{height_{cm}}}{3600}}</math>',
            link: false,
            references: [
                'Mosteller RD. Simplified calculation of body-surface area. N Engl J Med. 1987 Oct 22;317(17):1098. doi: 10.1056/NEJM198710223171717. PMID: 3657876.'
            ],
            calculate: function( data ) {
                return Math.sqrt( data.height.toNumber( 'cm' ) * data.weight.toNumber( 'kgwt' ) / 3600 );
            }
        },
        ebv: {
            name: 'Estimated blood volume',
            abbreviation: 'EBV',
            data: {
                variables: {
                    required: [ 'weight', 'age' ]
                }
            },
            digits: 0,
            units: 'mL',
            formula: '',
            references: [
                'Morgan & Mikhail\'s Clinical Anesthesiology. 5e. p1168'
            ],
            calculate: function( data ) {
                var weight = data.weight.toNumber( 'kgwt' );
                var age = data.age.toNumber( 'yo' );

                var ebvPerKg;

                if( age >= 1 ) {
                    if( data.gender === 'F' ) {
                        ebvPerKg = 65;
                    } else {
                        ebvPerKg = 75;
                    }
                } else if( age >= 1/12 ) {
                    ebvPerKg = 80;
                } else if( age >= 0 ) {
                    ebvPerKg = 85;
                } else {
                    ebvPerKg = 95;
                }

                return weight * ebvPerKg;
            }
        },
        fluidMaintenanceRate: {
            name: 'Fluid maintenance rate',
            abbreviation: 'Fluid maint.',
            data: {
                variables: {
                    required: [ 'weight' ]
                }
            },
            description: 'Uses 4-2-1 rule:<ul><li>4 mL/kg for the first 10 kg</li><li>2 mL/kg for the next 10 kg</li><li>1 mL/kg for the remaining weight</li></ul>',
            digits: 0,
            units: 'mL/hr',
            formula: '',
            references: [
                'Miller\'s Anesthesia 7e, section IV, pg. 1728'
            ],
            calculate: function( data ) {
                var weight = data.weight.toNumber( 'kgwt' );

                // Uses 4-2-1 rule
                var maintenanceRate = 4 * Math.min( weight, 10 );

                if( weight > 10 ) {
                    maintenanceRate += 2 * Math.min( weight - 10, 10 );
                }

                if( weight > 20) {
                    maintenanceRate += weight - 20;
                }

                return maintenanceRate;
            }
        },
        intraopFluids: {
            name: 'Intraoperative fluid dosing',
            abbreviation: 'Intraop fluids',
            data: {
                calculations: {
                    required: [ 'fluidMaintenanceRate' ]
                },
                variables: {
                    required: [ 'weight', 'npoTime', 'surgicalTrauma' ]
                }
            },
            type: 'string',
            references: [
                'Corcoran T, Rhodes JE, Clarke S, Myles PS, Ho KM. Perioperative fluid management strategies in major surgery: a stratified meta-analysis. Anesth Analg. 2012 Mar;114(3):640-51. doi: 10.1213/ANE.0b013e318240d6eb. Epub 2012 Jan 16. PMID: 22253274.'
            ],
            calculate: function( data ) {
                var weight = data.weight.toNumber( 'kgwt' );
                var maintenanceRate = data.fluidMaintenanceRate.toNumber( 'mL/hr' );
                var npoTime = data.npoTime.toNumber( 'hr' );
                var surgicalTrauma = data.surgicalTrauma;

                var output = '';

                var npoDeficit = npoTime * maintenanceRate;

                var surgicalLossMin, surgicalLossMax;

                if( surgicalTrauma === 'Minimal' ) {
                    surgicalLossMin = 2 * weight;
                    surgicalLossMax = 4 * weight;
                } else if( surgicalTrauma === 'Moderate' ) {
                    surgicalLossMin = 4 * weight;
                    surgicalLossMax = 6 * weight;
                } else {
                    surgicalLossMin = 6 * weight;
                    surgicalLossMax = 8 * weight;
                }

                var firstHour = Math.round( npoDeficit / 2 ) + maintenanceRate;
                var nextHoursMin = Math.round( npoDeficit / 4 ) + maintenanceRate + surgicalLossMin;
                var nextHoursMax = Math.round( npoDeficit / 4 ) + maintenanceRate + surgicalLossMax;
                var remainingHoursMin = maintenanceRate + surgicalLossMin;
                var remainingHoursMax = maintenanceRate + surgicalLossMax;

                output += 'NPO deficit: ' + Math.round( npoDeficit ) + ' mL<br/>';
                output += 'Surgical losses: ' + surgicalLossMin + '-' + surgicalLossMax + ' mL/hr<br/>';
                output += '1st hour: ' + firstHour + ' mL<br/>';
                output += '2nd hour: ' + nextHoursMin + '-' + nextHoursMax + ' mL<br/>';
                output += '3rd hour: ' + nextHoursMin + '-' + nextHoursMax + ' mL<br/>';
                output += '4+ hours: ' + remainingHoursMin + '-' + remainingHoursMax + ' mL<br/>';

                return output;
            }
        },
        maxAbl: {
            name: 'Maximum allowable blood loss',
            abbreviation: 'Max ABL',
            data: {
                calculations: {
                    required: [ 'ebv' ]
                },
                variables: {
                    required: [ 'weight', 'age', 'hct', 'minHct' ]
                }
            },
            digits: 0,
            units: 'mL',
            formula: '',
            references: [
                'Morgan & Mikhail\'s Clinical Anesthesiology. 5e. p1168'
            ],
            calculate: function( data ) {
                var currentHct = data.hct.toNumber( 'pcthct' );
                var minHct = data.minHct.toNumber( 'pcthct' );

                if( currentHct < minHct ) {
                    return '-';
                }

                return data.ebv.toNumber( 'mL' ) * ( currentHct - minHct ) / currentHct;
            }
        },
        minUop: {
            name: 'Minimum urine output',
            abbreviation: 'Min UOP',
            data: {
                variables: {
                    required: [ 'weight', 'age' ],
                    optional: [ 'caseDuration' ]
                }
            },
            type: 'string',
            formula: '',
            references: [
                'Klahr S, Miller SB. Acute oliguria. N Engl J Med. 1998 Mar 5;338(10):671-5. doi: 10.1056/NEJM199803053381007. PMID: 9486997.',
                'Arant BS Jr. Postnatal development of renal function during the first year of life. Pediatr Nephrol. 1987 Jul;1(3):308-13. doi: 10.1007/BF00849229. PMID: 3153294.'
            ],
            calculate: function( data ) {
                var weight = data.weight.toNumber( 'kgwt' );
                var age = data.age.toNumber( 'yo' );
                var caseDuration = data.caseDuration ? data.caseDuration.toNumber( 'hr' ) : null;

                var minUop;

                if( age > 1 ) {
                    minUop = 0.5 * weight;
                } else {
                    minUop = 1 * weight;
                }

                if( caseDuration ) {
                    minUop = minUop * caseDuration + ' mL';
                } else {
                    minUop = minUop + ' mL/hr';
                }

                return minUop;
            }
        },
        systolicBloodPressure: {
            name: 'Systolic blood pressure',
            abbreviation: 'SBP',
            data: {
                variables: {
                    required: [ 'age' ]
                }
            },
            type: 'string',
            references: [
                'Baby Miller 6e, ch. 16, pg. 550'
            ],
            calculate: function( data ) {
                var age = data.age.toNumber( 'yo' );

                var systolicMin, systolicMax, diastolicMin, diastolicMax, meanMin, meanMax;

                if( age >= 16 ) {
                    systolicMin = 100;
                    systolicMax = 125;
                } else if( age >= 13 ) {
                    systolicMin = 95;
                    systolicMax = 120;
                } else if( age >= 9 ) {
                    systolicMin = 90;
                    systolicMax = 115;
                } else if( age >= 6 ) {
                    systolicMin = 85;
                    systolicMax = 105;
                } else if( age >= 3 ) {
                    systolicMin = 80;
                    systolicMax = 100;
                } else if( age >= 1 ) {
                    systolicMin = 75;
                    systolicMax = 95;
                } else if( age >= 6 / 12 ) {
                    systolicMin = 70;
                    systolicMax = 90;
                } else if( age >= 1 / 12 ) {
                    systolicMin = 65;
                    systolicMax = 85;
                } else {
                    systolicMin = 60;
                    systolicMax = 75;
                }
            }
        }
    } );

    // Cardiovascular
    mw.calculators.addCalculations( {
        vO2: {
            name: 'Rate of oxygen consumption (VO<sub>2</sub>)',
            abbreviation: 'VO<sub>2</sub>',
            data: {
                calculations: {
                    required: [ 'bsa' ]
                },
                variables: {
                    optional: [ 'age' ]
                }
            },
            units: 'mL/min',
            references: [],
            calculate: function( data ) {
                var bsa = data.bsa.toNumber();
                var age = data.age ? data.age.toNumber( 'yr' ) : null;

                if( age >= 70 ) {
                    return 110 * bsa;
                } else {
                    return 125 * bsa;
                }
            }
        },
        cardiacOutputFick: {
            name: 'Cardiac output (Fick)',
            abbreviation: 'CO (Fick)',
            data: {
                variables: {
                    required: [ 'saO2', 'smvO2', 'hgb' ]
                },
                calculations: {
                    required: [ 'vO2' ]
                }
            },
            units: 'L/min',
            formula: '<math>\\mathrm{CO_{Fick}}=\\frac{VO_2}{(S_aO_2 - S_{mv}O_2) * H_b * 13.4}</math>',
            link: false,
            references: [],
            calculate: function( data ) {
                var vO2 = data.vO2.toNumber( 'mL/min' );
                var saO2 = data.saO2.toNumber() / 100;
                var smvO2 = data.smvO2.toNumber() / 100;
                var hgb = data.hgb.toNumber( 'ghgbperdL' );

                return vO2 / ( ( saO2 - smvO2 ) * hgb * 13.4 );
            }
        },
        cardiacIndex: {
            name: 'Cardiac index',
            abbreviation: 'CI',
            data: {
                calculations: {
                    required: [ 'bsa', 'cardiacOutputFick' ]
                }
            },
            units: 'L/min/m^2',
            formula: '<math>\\mathrm{CI}=\\frac{\\mathrm{CO}}{\\mathrm{BSA}}</math>',
            link: false,
            references: [],
            calculate: function( data ) {
                var cardiacOutput = data.cardiacOutputFick.toNumber( 'L/min' );
                var bsa = data.bsa.toNumber( 'm^2' );

                return cardiacOutput / bsa;
            }
        },
        strokeVolume: {
            name: 'Stroke volume',
            abbreviation: 'SV',
            data: {
                variables: {
                    required: [ 'heartRate' ]
                },
                calculations: {
                    required: [ 'cardiacOutputFick' ]
                }
            },
            units: 'mL',
            formula: '<math>\\mathrm{SV}=\\frac{\\mathrm{CO}}{\\mathrm{HR}}</math>',
            link: false,
            references: [],
            calculate: function( data ) {
                var cardiacOutput = data.cardiacOutputFick.toNumber( 'mL/min' );
                var heartRate = data.heartRate.toNumber();

                return cardiacOutput / heartRate;
            }
        }
    } );

    // Neuro
    mw.calculators.addCalculations( {
        brainMass: {
            name: 'Brain mass',
            data: {
                variables: {
                    optional: [ 'age', 'gender' ]
                }
            },
            digits: 0,
            units: 'gwt',
            references: [
                'Dekaban AS. Changes in brain weights during the span of human life: relation of brain weights to body heights and body weights. Ann Neurol. 1978 Oct;4(4):345-56. doi: 10.1002/ana.410040410. PMID: 727739.'
            ],
            calculate: function( data ) {
                var age = data.age ? data.age.toNumber( 'yr' ) : null;
                var gender = data.gender ? data.gender : null;

                var brainMassFemale = 1290;
                var brainMassMale = 1450;

                if( age !== null ) {
                    if( age <= 10 / 365 ) {
                        // <=10 days
                        brainMassFemale = 360;
                        brainMassMale = 380;
                    } else if( age <= 4 * 30 / 365 ) {
                        // Less than 4 months. This is a gap in the reported data of the paper, so linearly interpolate?
                        var ageFactor = 1 - ( 4 * 30 / 365 - age ) / ( 4 * 30 / 365 - 10 / 365 );

                        brainMassFemale = 360 + ageFactor * ( 580 - 360 );
                        brainMassMale = 380 + ageFactor * ( 640 - 380 );
                    } else if( age <= 8 * 30 / 365 ) {
                        // <=8 months
                        brainMassFemale = 580;
                        brainMassMale = 640;
                    } else if( age <= 18 * 30 / 365 ) {
                        // <=18 months
                        brainMassFemale = 940;
                        brainMassMale = 970;
                    } else if( age <= 30 * 30 / 365 ) {
                        // <=30 months
                        brainMassFemale = 1040;
                        brainMassMale = 1120;
                    } else if( age <= 43 * 30 / 365 ) {
                        // <=43 months
                        brainMassFemale = 1090;
                        brainMassMale = 1270;
                    } else if( age <= 5 ) {
                        brainMassFemale = 1150;
                        brainMassMale = 1300;
                    } else if( age <= 7 ) {
                        brainMassFemale = 1210;
                        brainMassMale = 1330;
                    } else if( age <= 9 ) {
                        brainMassFemale = 1180;
                        brainMassMale = 1370;
                    } else if( age <= 12 ) {
                        brainMassFemale = 1260;
                        brainMassMale = 1440;
                    } else if( age <= 15 ) {
                        brainMassFemale = 1280;
                        brainMassMale = 1410;
                    } else if( age <= 18 ) {
                        brainMassFemale = 1340;
                        brainMassMale = 1440;
                    } else if( age <= 21 ) {
                        brainMassFemale = 1310;
                        brainMassMale = 1450;
                    } else if( age <= 30 ) {
                        brainMassFemale = 1300;
                        brainMassMale = 1440;
                    } else if( age <= 40 ) {
                        brainMassFemale = 1290;
                        brainMassMale = 1440;
                    } else if( age <= 50 ) {
                        brainMassFemale = 1290;
                        brainMassMale = 1430;
                    } else if( age <= 55 ) {
                        brainMassFemale = 1280;
                        brainMassMale = 1410;
                    } else if( age <= 60 ) {
                        brainMassFemale = 1250;
                        brainMassMale = 1370;
                    } else if( age <= 65 ) {
                        brainMassFemale = 1240;
                        brainMassMale = 1370;
                    } else if( age <= 70 ) {
                        brainMassFemale = 1240;
                        brainMassMale = 1360;
                    } else if( age <= 75 ) {
                        brainMassFemale = 1230;
                        brainMassMale = 1350;
                    } else if( age <= 80 ) {
                        brainMassFemale = 1190;
                        brainMassMale = 1330;
                    } else if( age <= 85 ) {
                        brainMassFemale = 1170;
                        brainMassMale = 1310;
                    } else {
                        brainMassFemale = 1140;
                        brainMassMale = 1290;
                    }
                }

                if( gender === 'F' ) {
                    return brainMassFemale;
                } else if( gender === 'M' ) {
                    return brainMassMale;
                } else {
                    return ( brainMassFemale + brainMassMale ) / 2;
                }
            }
        },
        cerebralBloodVolume: {
            name: 'Cerebral blood volume',
            abbreviation: 'CBV',
            data: {
                calculations: {
                    required: [ 'brainMass' ]
                }
            },
            units: 'mL',
            description: '4 mL per 100g of brain mass',
            references: [
                'Tameem A, Krovvidi H, Cerebral physiology, Continuing Education in Anaesthesia Critical Care & Pain, Volume 13, Issue 4, August 2013, Pages 113–118, https://doi.org/10.1093/bjaceaccp/mkt001'
            ],
            calculate: function( data ) {
                var brainMass = data.brainMass.toNumber( 'gwt' );

                return 4 * brainMass / 100;
            }
        },
        cerebralMetabolicRateFactor: {
            name: 'Cerebral metabolic rate factor',
            abbreviation: '%CMR',
            data: {
                variables: {
                    optional: [ 'temperature' ]
                }
            },
            description: '7% change in CMR for every 1 &deg;C change in temperature',
            references: [
                'Tameem A, Krovvidi H, Cerebral physiology, Continuing Education in Anaesthesia Critical Care & Pain, Volume 13, Issue 4, August 2013, Pages 113–118, https://doi.org/10.1093/bjaceaccp/mkt001'
            ],
            calculate: function( data ) {
                var temperature = data.temperature ? data.temperature.toNumber( 'degC' ) : null;

                var cerebralMetabolicRateFactor = 1;

                if( temperature ) {
                    cerebralMetabolicRateFactor += 0.07 * ( temperature - 37 );
                }

                return cerebralMetabolicRateFactor;
            }
        },
        cerebralMetabolicRateO2: {
            name: 'Cerebral metabolic rate (O<sub>2</sub>)',
            abbreviation: 'CMRO<sub>2</sub>',
            data: {
                calculations: {
                    required: [ 'brainMass', 'cerebralMetabolicRateFactor' ]
                },
                variables: {
                    optional: [ 'temperature' ]
                }
            },
            units: 'mL/min',
            description: '3 mL O<sub>2</sub>/min per 100g of brain mass. 7% change in CMR for every 1 &deg;C change in temperature.',
            references: [
                'Tameem A, Krovvidi H, Cerebral physiology, Continuing Education in Anaesthesia Critical Care & Pain, Volume 13, Issue 4, August 2013, Pages 113–118, https://doi.org/10.1093/bjaceaccp/mkt001'
            ],
            calculate: function( data ) {
                // Temperature is included as an optional variable to generate the input.
                // It is used by cerebralMetabolicRateFactor, which is an internal calculation not typically shown.
                var brainMass = data.brainMass.toNumber( 'gwt' );
                var cerebralMetabolicRateFactor = data.cerebralMetabolicRateFactor.toNumber();

                return cerebralMetabolicRateFactor * 3 * brainMass / 100;
            }
        },
        cerebralMetabolicRateGlucose: {
            name: 'Cerebral metabolic rate (Glucose)',
            abbreviation: 'CMR<sub>glu</sub>',
            data: {
                calculations: {
                    required: [ 'brainMass', 'cerebralMetabolicRateFactor' ]
                }
            },
            units: 'mg/min',
            description: '5 mg glucose/min per 100g of brain mass. 7% change in CMR for every 1 &deg;C change in temperature',
            references: [
                'Tameem A, Krovvidi H, Cerebral physiology, Continuing Education in Anaesthesia Critical Care & Pain, Volume 13, Issue 4, August 2013, Pages 113–118, https://doi.org/10.1093/bjaceaccp/mkt001'
            ],
            calculate: function( data ) {
                var brainMass = data.brainMass.toNumber( 'gwt' );
                var cerebralMetabolicRateFactor = data.cerebralMetabolicRateFactor.toNumber();

                return cerebralMetabolicRateFactor * 5 * brainMass / 100;
            }
        },
        cerebralBloodFlow: {
            name: 'Cerebral blood flow',
            abbreviation: 'CBF',
            data: {
                calculations: {
                    required: [ 'brainMass', 'cerebralMetabolicRateFactor' ]
                },
                variables: {
                    optional: [ 'paCO2' ]
                }
            },
            units: 'mL/min',
            description: '<ul><li>50 mL/min per 100g of brain mass.</li><li>Every mmHg in PaCO2 changes CBF by 1.5 mL/min per 100g of brain mass.</li><li>Cerebral blood flow and cerebral metabolic rate are coupled. Factors that alter CMR (e.g. temperature) will proportionally alter CBF.</li>',
            references: [
                'Tameem A, Krovvidi H, Cerebral physiology, Continuing Education in Anaesthesia Critical Care & Pain, Volume 13, Issue 4, August 2013, Pages 113–118, https://doi.org/10.1093/bjaceaccp/mkt001',
                'Brian JE Jr. Carbon dioxide and the cerebral circulation. Anesthesiology. 1998 May;88(5):1365-86. doi: 10.1097/00000542-199805000-00029. PMID: 9605698.'
            ],
            calculate: function( data ) {
                var brainMass = data.brainMass.toNumber( 'gwt' );
                var cerebralMetabolicRateFactor = data.cerebralMetabolicRateFactor.toNumber();
                var paCO2 = data.paCO2.toNumber( 'mmHg' );

                var cerebralBloodFlow = cerebralMetabolicRateFactor * 50 * brainMass / 100;

                if( paCO2 ) {
                    // CO2 reductions don't reduce CBF more than 50%
                    var minCerebralBloodFlow = cerebralBloodFlow / 2;

                    cerebralBloodFlow += 1.5 * brainMass / 100 * ( paCO2 - 40 );

                    cerebralBloodFlow = math.max( cerebralBloodFlow, minCerebralBloodFlow );
                }

                return cerebralBloodFlow;
            }
        }
    } );



    // Pulmonary
    mw.calculators.addCalculations( {
        aaGradientO2Expected: {
            name: 'A-a O<sub>2</sub> gradient (expected)',
            abbreviation: 'A-a O<sub>2</sub> ex.',
            data: {
                variables: {
                    required: [ 'age' ]
                }
            },
            units: 'mmHg',
            references: [
                'Hantzidiamantis PJ, Amaro E. Physiology, Alveolar to Arterial Oxygen Gradient. 2021 Feb 22. In: StatPearls [Internet]. Treasure Island (FL): StatPearls Publishing; 2021 Jan–. PMID: 31424737.'
            ],
            calculate: function( data ) {
                var age = data.age.toNumber( 'yr' );

                return ( age + 10 ) / 4;
            }
        },
        lowTidalVolume: {
            name: 'Low tidal volume',
            abbreviation: 'LTV',
            data: {
                calculations: {
                    required: [ 'ibw' ]
                }
            },
            type: 'string',
            description: '6-8 mL/kg',
            references: [
                'Acute Respiratory Distress Syndrome Network, Brower RG, Matthay MA, Morris A, Schoenfeld D, Thompson BT, Wheeler A. Ventilation with lower tidal volumes as compared with traditional tidal volumes for acute lung injury and the acute respiratory distress syndrome. N Engl J Med. 2000 May 4;342(18):1301-8. doi: 10.1056/NEJM200005043421801. PMID: 10793162.'
            ],
            calculate: function( data ) {
                var ibw = data.ibw.toNumber( 'kgwt' );

                return math.round( 6 * ibw ) + '-' + math.round( 8 * ibw ) + ' mL';
            }
        }
    } );

    var tableMaxWidth = 600;

    mw.calculators.addCalculators( moduleId, {
        anatomy: {
            name: 'Patient statistics',
            calculations: [
                'bmi',
                'bsa',
                'ibw',
                'lbw'
            ],
            css: {
                'max-width': tableMaxWidth
            }
        },
        fluidManagement: {
            name: 'Fluid management',
            calculations: [
                'ebv',
                'fluidMaintenanceRate',
                'intraopFluids',
                'maxAbl',
                'minUop'
            ],
            css: {
                'max-width': tableMaxWidth
            }
        },
        cardiovascular: {
            name: 'Cardiovascular',
            calculations: [
                'vO2',
                'cardiacOutputFick',
                'cardiacIndex',
                'strokeVolume'
            ],
            css: {
                'max-width': tableMaxWidth
            }
        },
        neuro: {
            name: 'Neuro',
            calculations: [
                'brainMass',
                'cerebralBloodVolume',
                'cerebralMetabolicRateO2',
                'cerebralMetabolicRateGlucose',
                'cerebralBloodFlow'
            ],
            css: {
                'max-width': tableMaxWidth
            }
        },
        pulmonary: {
            name: 'Pulmonary',
            calculations: [
                'aaGradientO2Expected',
                'lowTidalVolume'
            ],
            css: {
                'max-width': tableMaxWidth
            }
        }
    } );
}() );