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

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


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


                 return units;
    var VALID_TYPES = [
        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: {
 
            toString: function( units ) {
        return null;
                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;


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


                 return units;
            for( var calculatorId in calculatorData ) {
                 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', '°' );


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


    mw.calculators.addUnits( {
            // Store the calculators
        bpm: {
            for( var calculatorId in calculators ) {
             baseName: 'bpm'
                mw.calculators.calculators[ moduleId ][ calculatorId ] = calculators[ calculatorId ];
 
                mw.calculators.calculators[ moduleId ][ calculatorId ].render();
             }
         },
         },
         pcthct: {
         addUnitsBases: function( unitsBaseData ) {
             baseName: 'hgb'
             var unitsBases = mw.calculators.createCalculatorObjects( 'UnitsBase', unitsBaseData );
 
            for( var unitsBaseId in unitsBases ) {
                mw.calculators.unitsBases[ unitsBaseId ] = unitsBases[ unitsBaseId ];
            }
         },
         },
         pcto2: {
         addUnits: function( unitsData ) {
             baseName: 'o2'
             var units = mw.calculators.createCalculatorObjects( 'Units', unitsData );
 
            for( var unitsId in units ) {
                if( mw.calculators.units.hasOwnProperty( unitsId ) ) {
                    continue;
                }
 
                try {
                    var unitData = {
                        aliases: units[ unitsId ].aliases,
                        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 ];
            }
         },
         },
         ghgbperdL: {
         addVariables: function( variableData ) {
             baseName: 'hgb',
             var variables = mw.calculators.createCalculatorObjects( 'Variable', variableData );
             prefixes: 'short',
 
            definition: '3 pcthct'
             for( var variableId in variables ) {
        }
                mw.calculators.variables[ variableId ] = variables[ variableId ];
    } );
 
                var cookieValue = mw.calculators.getCookieValue( variableId );


    mw.calculators.addVariables( {
                if( cookieValue ) {
        caseDuration: {
                    // Try to set the variable value from the cookie value
            name: 'Case duration',
                    if( !mw.calculators.variables[ variableId ].setValue( cookieValue ) ) {
            type: 'number',
                        // Unset the cookie value since for whatever reason it's no longer valid.
            abbreviation: 'Duration',
                        mw.calculators.setCookieValue( variableId, null );
            minValue: '0 hr',
                    }
            maxLength: 3,
                 }
            units: [
             }
                 'hr',
                'min'
             ]
         },
         },
         hct: {
         createCalculatorObjects: function( className, objectData ) {
             name: 'Current hematocrit',
             if( !mw.calculators.objectClasses.hasOwnProperty( className ) ) {
            type: 'number',
                throw new Error( 'Invalid class name "' + className + '"' );
             abbreviation: 'Current hct',
             }
            minValue: '10 pcthct',
 
             maxValue: '75 pcthct',
             var objects = {};
            defaultValue: '45 pcthct',
 
             maxLength: 4,
             for( var objectId in objectData ) {
            units: [
                var propertyValues = objectData[ objectId ];
                 'pcthct',
 
                 'ghgbperdL'
                 // Id can either be specified using the 'id' property, or as the property name in objectData
             ]
                 if( propertyValues.hasOwnProperty( 'id' ) ) {
                    objectId = propertyValues.id;
                }
                else {
                    propertyValues.id = objectId;
                }
 
                objects[ objectId ] = new mw.calculators.objectClasses[ className ]( propertyValues );
             }
 
            return objects;
         },
         },
         heartRate: {
         createInputGroup: function( variableIds, global ) {
             name: 'Heart rate',
             var $form = $( '<form>', {
             type: 'number',
                novalidate: true
            abbreviation: 'HR',
            } );
             defaultValue: '60 bpm',
 
             maxLength: 4,
             var $formRow = $( '<div>', {
             units: [
                class: 'form-row'
                 'bpm'
             } ).css( 'flex-wrap', 'nowrap' );
             ]
 
             var inputOptions = {
                global: !!global
            };
 
             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 );
         },
         },
         hgb: {
         getCookieKey: function( variableId ) {
             name: 'Hemoglobin',
             return 'calculators-var-' + variableId;
            type: 'number',
            abbreviation: 'HgB',
            minValue: '3 ghgbperdL',
            maxValue: '25 ghgbperdL',
            defaultValue: '13 ghgbperdL',
            maxLength: 4,
            units: [
                'pcthct',
                'ghgbperdL'
            ]
         },
         },
         minHct: {
         getCookieValue: function( varId ) {
             name: 'Minimum hematocrit',
             var cookieValue = mw.cookie.get( mw.calculators.getCookieKey( varId ) );
            type: 'number',
 
             abbreviation: 'Min hct',
             if( !cookieValue ) {
            minValue: '10 pcthct',
                return null;
             maxValue: '45 pcthct',
             }
            defaultValue: '21 pcthct',
 
            maxLength: 4,
             return cookieValue;
            units: [
                'pcthct',
                'ghgbperdL'
             ]
         },
         },
         paCO2: {
         getCalculation: function( calculationId ) {
             name: 'PaCO<sub>2</sub>',
             if( mw.calculators.calculations.hasOwnProperty( calculationId ) ) {
            type: 'number',
                return mw.calculators.calculations[ calculationId ];
             abbreviation: 'PaCO<sub>2</sub>',
             } else {
            minValue: '20 mmHg',
                 return null;
            defaultValue: '40 mmHg',
             }
            maxLength: 3,
            units: [
                 'mmHg'
             ]
         },
         },
         saO2: {
         getCalculator: function( moduleId, calculatorId ) {
             name: 'SaO<sub>2</sub>',
             if( mw.calculators.calculators.hasOwnProperty( moduleId ) &&
            type: 'number',
                mw.calculators.calculators[ moduleId ].hasOwnProperty( calculatorId ) ) {
            abbreviation: 'SaO<sub>2</sub>',
                return mw.calculators.calculators[ moduleId ][ calculatorId ];
             minValue: '25 pcto2',
             } else {
            maxValue: '100 pcto2',
                 return null;
            defaultValue: '100 pcto2',
             }
            maxLength: 3,
            units: [
                 'pcto2'
             ]
         },
         },
         smvO2: {
         getUnitsByBase: function( value ) {
             name: 'SmvO<sub>2</sub>',
             if( typeof value !== 'object' || !value.hasOwnProperty( 'units' ) ) {
            type: 'number',
                return null;
             abbreviation: 'SmvO<sub>2</sub>',
             }
            minValue: '25 pcto2',
 
             maxValue: '100 pcto2',
             var unitsByBase = {};
            defaultValue: '75 pcto2',
 
             maxLength: 3,
             for( var iUnits in value.units ) {
            units: [
                var units = value.units[ iUnits ];
                 'pcto2'
 
             ]
                 unitsByBase[ units.unit.base.key.toLowerCase() ] = units.prefix.name + units.unit.name;
            }
 
             return unitsByBase;
         },
         },
         npoTime: {
         getUnitsString: function( value ) {
             name: 'Time spent NPO',
             if( typeof value !== 'object' ) {
             type: 'number',
                return null;
             abbreviation: 'NPO time',
            }
             minValue: '0 hr',
 
             defaultValue: '8 hr',
            var unitsString = value.formatUnits();
            maxLength: 2,
 
             units: [
            var reDenominator = /\/\s?\((.*)\)/;
                'hr'
            var denominatorMatches = unitsString.match( reDenominator );
             ]
 
             if( denominatorMatches ) {
                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 ) {
                unitsBase = unitsBase.toLowerCase();
 
                if( mw.calculators.unitsBases.hasOwnProperty( unitsBase ) &&
                    typeof mw.calculators.unitsBases[ unitsBase ].toString === 'function' ) {
                    unitsString = mw.calculators.unitsBases[ unitsBase ].toString( unitsString );
                }
             } else {
                // TODO nasty hack to fix weight units in compound units which have no base
                unitsString = unitsString.replace( 'kgwt', 'kg' );
                unitsString = unitsString.replace( 'ug', 'mcg' );
             }
 
             return unitsString;
         },
         },
         surgicalTrauma: {
         getValueDecimals: function( value ) {
             name: 'Severity of surgical trauma',
             // Supports either numeric values or math objects
             type: 'string',
            if( mw.calculators.isValueMathObject( value ) ) {
             abbreviation: 'Surgical trauma',
                value = mw.calculators.getValueNumber( value );
             defaultValue: 'Minimal',
            }
             options: [
 
                'Minimal',
            if( typeof value !== 'number' ) {
                'Moderate',
                return null;
                'Severe'
            }
             ]
 
             // Convert the number to a string, reverse, and count the number of characters up to the period.
             var decimals = value.toString().split('').reverse().join('').indexOf( '.' );
 
             // If no decimal is present, will be set to -1 by indexOf. If so, set to 0.
             decimals = decimals > 0 ? decimals : 0;
 
             return decimals;
         },
         },
         temperature: {
         getValueNumber: function( value, decimals ) {
             name: 'Temperature',
             if( typeof value !== 'object' ) {
            type: 'number',
                 return null;
            abbreviation: 'Temp',
             }
            minValue: '20 degC',
            maxValue: '44 degC',
            defaultValue: '37 degC',
            maxLength: 5,
            units: [
                 'degC',
                'degF'
             ]
        }
    } );


            // Remove floating point errors
            var number = math.round( value.toNumber(), 10 );


            var absNumber = math.abs( number );


    mw.calculators.addCalculations( {
            if( absNumber >= 10 ) {
        bmi: {
                 decimals = 0;
            name: 'Body mass index',
             } else {
            abbreviation: 'BMI',
                 decimals = -math.floor( math.log10( absNumber ) ) + 1;
            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 );
             }
             }
            return math.round( number, decimals );
         },
         },
         bsa: {
         getValueString: function( value, decimals ) {
            name: 'Body surface area',
             if( !mw.calculators.isValueMathObject( value ) ) {
            abbreviation: 'BSA',
                 return null;
            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: {
             var valueNumber = mw.calculators.getValueNumber( value, decimals );
             name: 'Estimated blood volume',
             var valueUnits = mw.calculators.getUnitsString( value );
             abbreviation: 'EBV',
 
             data: {
             if( math.abs( math.log10( valueNumber ) ) > 3 ) {
                 variables: {
                 var valueUnitsByBase = mw.calculators.getUnitsByBase( value );
                     required: [ 'weight', 'age' ]
 
                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' );


                 var ebvPerKg;
                 if( oldSIUnit ) {
                    // 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.
                    var newSIUnit = newSIValue.formatUnits();
 
                    if( newSIUnit !== oldSIUnit ) {
                        var newValue = math.unit( newSIValue.toNumber() + ' ' + value.formatUnits().replace( oldSIUnit, newSIUnit ) );


                if( age >= 1 ) {
                        valueNumber = mw.calculators.getValueNumber( newValue, decimals );
                    if( data.gender === 'F' ) {
                         valueUnits = mw.calculators.getUnitsString( newValue );
                        ebvPerKg = 65;
                    } else {
                         ebvPerKg = 75;
                     }
                     }
                } else if( age >= 1/12 ) {
                    ebvPerKg = 80;
                } else if( age >= 0 ) {
                    ebvPerKg = 85;
                } else {
                    ebvPerKg = 95;
                 }
                 }
            }


                 return weight * ebvPerKg;
            var valueString = String( valueNumber );
 
            if( valueUnits ) {
                 valueString += ' ' + valueUnits;
             }
             }
            return valueString;
         },
         },
         fluidMaintenanceRate: {
         getVariable: function( variableId ) {
             name: 'Fluid maintenance rate',
             if( mw.calculators.variables.hasOwnProperty( variableId ) ) {
             abbreviation: 'Fluid maint.',
                return mw.calculators.variables[ variableId ];
            data: {
            } else {
                 variables: {
                return null;
                     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>',
        },
            digits: 0,
        isMobile: function() {
             units: 'mL/hr',
             return window.matchMedia( 'only screen and (max-width: 760px)' ).matches;
            formula: '',
        },
             references: [
        isValueMathObject: function( value ) {
                'Miller\'s Anesthesia 7e, section IV, pg. 1728'
             return value && value.hasOwnProperty( 'value' );
             ],
        },
             calculate: function( data ) {
        setCookieValue: function( variableId, value ) {
                 var weight = data.weight.toNumber( 'kgwt' );
             mw.cookie.set( mw.calculators.getCookieKey( variableId ), value, {
                expires: COOKIE_EXPIRATION
            } );
        },
        setValue: function( variableId, value ) {
            if( !mw.calculators.variables.hasOwnProperty( variableId ) ) {
                return false;
            }
 
             if( !mw.calculators.variables[ variableId ].setValue( value ) ) {
                return false;
            }
 
            mw.calculators.setCookieValue( variableId, value );
 
             return true;
        },
        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;
                    }


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


                if( weight > 10 ) {
                     delete propertyValues[ requiredProperty ];
                     maintenanceRate += 2 * Math.min( weight - 10, 10 );
                 }
                 }
            }


                 if( weight > 20) {
            if( properties.hasOwnProperty( 'optional' ) ) {
                     maintenanceRate += weight - 20;
                 for( var iOptionalProperty in properties.optional ) {
                    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 );


                 return maintenanceRate;
            if( invalidProperties.length ) {
                 console.warn( 'Unsupported properties defined for ' + typeof this + ' with id "' + this.id + '": ' + invalidProperties.join( ', ' ) );
             }
             }
         },
         }
         intraopFluids: {
    };
             name: 'Intraoperative fluid dosing',
 
             abbreviation: 'Intraop fluids',
    mw.calculators.objectClasses.CalculatorObject.prototype.getProperties = function() {
             data: {
         return {
                calculations: {
             required: [],
                    required: [ 'fluidMaintenanceRate' ]
             optional: []
                },
        };
                variables: {
    };
                    required: [ 'weight', 'npoTime', 'surgicalTrauma' ]
 
                }
    mw.calculators.objectClasses.CalculatorObject.prototype.mergeProperties = function( inheritedProperties, properties ) {
             },
        var uniqueValues = function( value, index, self ) {
             type: 'string',
             return self.indexOf( value ) === index;
             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: [
                'toString'
             ]
        };
 
        mw.calculators.objectClasses.CalculatorObject.call( this, properties, propertyValues );
    };
 
    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'
             ],
             ],
             calculate: function( data ) {
             optional: [
                var weight = data.weight.toNumber( 'kgwt' );
                'aliases',
                var maintenanceRate = data.fluidMaintenanceRate.toNumber( 'mL/hr' );
                'baseName',
                var npoTime = data.npoTime.toNumber( 'hr' );
                'definition',
                 var surgicalTrauma = data.surgicalTrauma;
                'offset',
                'prefixes'
            ]
        };
 
        mw.calculators.objectClasses.CalculatorObject.call( this, properties, propertyValues );
    };
 
    mw.calculators.objectClasses.Units.prototype = Object.create( mw.calculators.objectClasses.CalculatorObject.prototype );
 
 
 
 
    /**
    * Class Variable
    * @param {Object} propertyValues
    * @returns {mw.calculators.objectClasses.Variable}
    * @constructor
    */
    mw.calculators.objectClasses.Variable = function( propertyValues ) {
        mw.calculators.objectClasses.CalculatorObject.call( this, this.getProperties(), propertyValues );
 
        if( VALID_TYPES.indexOf( this.type ) === -1 ) {
            throw new Error( 'Invalid type "' + this.type + '" for variable "' + this.id + '"' );
        }
 
        // Accept options as either an array of strings, or an object with ids as keys and display text as values
        if( Array.isArray( this.options ) ) {
            var options = {};
 
            for( var iOption in this.options ) {
                 var option = this.options[ iOption ];
 
                options[ option ] = option;
            }


                var output = '';
            this.options = options;
        }


                var npoDeficit = npoTime * maintenanceRate;
        this.calculations = [];


                var surgicalLossMin, surgicalLossMax;
        if( this.defaultValue ) {
            this.defaultValue = this.prepareValue( this.defaultValue );
        }


                if( surgicalTrauma === 'Minimal' ) {
        if( this.minValue ) {
                    surgicalLossMin = 2 * weight;
            this.minValue = this.prepareValue( this.minValue );
                    surgicalLossMax = 4 * weight;
        }
                } else if( surgicalTrauma === 'Moderate' ) {
 
                    surgicalLossMin = 4 * weight;
        if( this.maxValue ) {
                    surgicalLossMax = 6 * weight;
            this.maxValue = this.prepareValue( this.maxValue );
                } else {
        }
                    surgicalLossMin = 6 * weight;
 
                    surgicalLossMax = 8 * weight;
        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 firstHour = Math.round( npoDeficit / 2 ) + maintenanceRate;
        var variableId = this.id;
                var nextHoursMin = Math.round( npoDeficit / 4 ) + maintenanceRate + surgicalLossMin;
        var inputId = 'calculator-input-' + variableId;
                var nextHoursMax = Math.round( npoDeficit / 4 ) + maintenanceRate + surgicalLossMax;
                var remainingHoursMin = maintenanceRate + surgicalLossMin;
                var remainingHoursMax = maintenanceRate + surgicalLossMax;


                output += 'NPO deficit: ' + Math.round( npoDeficit ) + ' mL<br/>';
        // If not creating a global input, assign an iterated id
                output += 'Surgical losses: ' + surgicalLossMin + '-' + surgicalLossMax + ' mL/hr<br/>';
        if( !inputOptions.global ) {
                output += '1st hour: ' + firstHour + ' mL<br/>';
            var inputIdCount = 0;
                output += '2nd hour: ' + nextHoursMin + '-' + nextHoursMax + ' mL<br/>';
                output += '3rd hour: ' + nextHoursMin + '-' + nextHoursMax + ' mL<br/>';
                output += '4+ hours: ' + remainingHoursMin + '-' + remainingHoursMax + ' mL<br/>';


                 return output;
            while( $( '#' + inputId + '-' + inputIdCount ).length ) {
                 inputIdCount++;
             }
             }
         },
 
         maxAbl: {
            inputId += '-' + inputIdCount;
             name: 'Maximum allowable blood loss',
         }
            abbreviation: 'Max ABL',
 
             data: {
         var inputContainerTag = inputOptions.inline ? '<span>' : '<div>';
                calculations: {
 
                    required: [ 'ebv' ]
        var inputContainerAttributes = {
                },
             class: 'form-group mb-0 calculator-container-input'
                variables: {
        };
                    required: [ 'weight', 'age', 'hct', 'minHct' ]
 
        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 {
            digits: 0,
                 inputValue = value;
            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 ) {
            // Initialize input options
                    return '-';
            var inputAttributes = {
                 }
                 id: inputId,
                class: 'form-control form-control-sm calculator-input calculator-input-text',
                type: 'text',
                autocomplete: 'off',
                inputmode: 'decimal',
                 value: inputValue
            };


                 return data.ebv.toNumber( 'mL' ) * ( currentHct - minHct ) / currentHct;
            // Configure additional options
            if( this.maxLength ) {
                 inputAttributes.maxlength = this.maxLength;
             }
             }
        },
        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;
            // 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 ];


                if( age > 1 ) {
                    $unitsContainer
                    minUop = 0.5 * weight;
                        .css( 'padding', '0 0.5em' )
                        .append( mw.calculators.getUnitsString( math.unit( '0 ' + this.units[ 0 ] ) ) )
                        .append( $( '<input>', unitsInputAttributes ) );
                 } else {
                 } else {
                     minUop = 1 * weight;
                     // Initialize the units input options
                    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' );
                    } else {
                        $( this ).parent().removeClass( 'is-invalid' );
                    }
                } );


                 if( caseDuration ) {
            // Create the input group
                     minUop = minUop * caseDuration + ' mL';
            var $inputGroup = $( '<div>', {
                 class: 'input-group'
            } ).append( $input );
 
            if( $unitsContainer ) {
                $inputGroup.append( $unitsContainer );
            }
 
            $inputContainer.append( $inputGroup );
        } else if( this.type === TYPE_STRING ) {
            if( this.hasOptions() ) {
                var optionKeys = Object.keys( this.options );
 
                if( optionKeys.length === 1 ) {
                     $inputContainer.append( this.options[ optionKeys[ 0 ] ] );
                 } else {
                 } else {
                     minUop = minUop + ' mL/hr';
                     var selectAttributes = {
                        id: inputId,
                        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 ) {
            $inputContainer.append( $( '<div>', {
                class: 'invalid-feedback'
            } ) );
        }


                return minUop;
        return $inputContainer;
            }
    };
        },
 
         systolicBloodPressure: {
    mw.calculators.objectClasses.Variable.prototype.getLabelString = function() {
            name: 'Systolic blood pressure',
         return mw.calculators.isMobile() && this.abbreviation ? this.abbreviation : this.name;
            abbreviation: 'SBP',
    };
            data: {
 
                variables: {
    mw.calculators.objectClasses.Variable.prototype.getProperties = function() {
                    required: [ 'age' ]
        return {
                 }
            required: [
            },
                'id',
            type: 'string',
                 'name',
            references: [
                 'type'
                 'Baby Miller 6e, ch. 16, pg. 550'
             ],
             ],
             calculate: function( data ) {
             optional: [
                 var age = data.age.toNumber( 'yo' );
                 'abbreviation',
                'defaultValue',
                'maxLength',
                'maxValue',
                'minValue',
                'options',
                'units'
            ]
        };
    };


                var systolicMin, systolicMax, diastolicMin, diastolicMax, meanMin, meanMax;
    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;
        }
    };


                if( age >= 16 ) {
    mw.calculators.objectClasses.Variable.prototype.getValueString = function() {
                    systolicMin = 100;
        return String( this.getValue() );
                    systolicMax = 125;
    };
                } else if( age >= 13 ) {
 
                    systolicMin = 95;
    mw.calculators.objectClasses.Variable.prototype.hasOptions = function() {
                    systolicMax = 120;
        return this.options !== null;
                } else if( age >= 9 ) {
    };
                    systolicMin = 90;
 
                    systolicMax = 115;
    mw.calculators.objectClasses.Variable.prototype.hasUnits = function() {
                } else if( age >= 6 ) {
        return this.units !== null;
                    systolicMin = 85;
    };
                    systolicMax = 105;
 
                } else if( age >= 3 ) {
    mw.calculators.objectClasses.Variable.prototype.hasValue = function() {
                    systolicMin = 80;
        var value = this.getValue();
                    systolicMax = 100;
 
                } else if( age >= 1 ) {
        if( value === null ||
                    systolicMin = 75;
            ( mw.calculators.isValueMathObject( value ) && !value.toNumber() ) ) {
                    systolicMax = 95;
            return false;
                } else if( age >= 6 / 12 ) {
        }
                    systolicMin = 70;
 
                    systolicMax = 90;
        return true;
                } else if( age >= 1 / 12 ) {
    };
                    systolicMin = 65;
 
                    systolicMax = 85;
    mw.calculators.objectClasses.Variable.prototype.isValueMathObject = function() {
                 } else {
        return mw.calculators.isValueMathObject( this.value );
                     systolicMin = 60;
    };
                    systolicMax = 75;
 
    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 );
                 }
                 }
             }
             }
         }
         }
    } );


     // Cardiovascular
        return value;
     mw.calculators.addCalculations( {
    };
         vO2: {
 
             name: 'Rate of oxygen consumption (VO<sub>2</sub>)',
     mw.calculators.objectClasses.Variable.prototype.setValue = function( value ) {
            abbreviation: 'VO<sub>2</sub>',
        var validateResult = this.validateValue( value );
             data: {
 
                 calculations: {
        this.valid = !!validateResult.valid;
                     required: [ 'bsa' ]
        this.message = validateResult.message;
                 },
 
                variables: {
        if( !this.valid ) {
                     optional: [ 'age' ]
            this.value = null;
            this.valueUpdated();
 
            return false;
        }
 
        this.value = this.prepareValue( value );
 
        this.valueUpdated();
 
        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;
 
             if( this.hasUnits() ) {
                 valueUnits = value.formatUnits();
 
                if( !valueUnits ) {
                     // 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: 'mL/min',
            references: [],
            calculate: function( data ) {
                var bsa = data.bsa.toNumber();
                var age = data.age ? data.age.toNumber( 'yr' ) : null;


                if( age >= 70 ) {
            if( this.minValue && math.smaller( value, this.minValue ) ) {
                     return 110 * bsa;
                var minValueString = valueUnits ? mw.calculators.getValueString( this.minValue.to( valueUnits ) ) :
                 } else {
                     mw.calculators.getValueString( this.minValue );
                     return 125 * bsa;
 
                 }
                result.message = String( this ) + ' must be at least ' + minValueString;
                 result.valid = false;
            } 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;
                result.valid = false;
             }
             }
         },
         } else if( this.hasOptions() ) {
        cardiacOutputFick: {
             if( !this.options.hasOwnProperty( value ) ) {
            name: 'Cardiac output (Fick)',
                 // Unlikely to be a user error, so don't set message
            abbreviation: 'CO (Fick)',
                 result.valid = false;
            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 );
                 console.warn( consoleWarnPrefix + 'Value must be one of: ' + Object.keys( this.options ).join( ', ' ) );
             }
             }
         },
         }
         cardiacIndex: {
 
            name: 'Cardiac index',
         return result;
            abbreviation: 'CI',
    };
            data: {
 
                calculations: {
    mw.calculators.objectClasses.Variable.prototype.valueUpdated = function() {
                    required: [ 'bsa', 'cardiacOutputFick' ]
        for( var iCalculation in this.calculations ) {
                }
            var calculation = mw.calculators.getCalculation( this.calculations[ iCalculation ] );
            },
            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;
            if( calculation ) {
                 calculation.render();
             }
             }
         },
         }
         strokeVolume: {
    };
             name: 'Stroke volume',
 
            abbreviation: 'SV',
 
            data: {
 
                variables: {
    /**
                    required: [ 'heartRate' ]
    * Class AbstractCalculation
                },
    * @param {Object} propertyValues
                calculations: {
    * @returns {mw.calculators.objectClasses.AbstractCalculation}
                    required: [ 'cardiacOutputFick' ]
    * @constructor
                }
    */
            },
    mw.calculators.objectClasses.AbstractCalculation = function( propertyValues ) {
            units: 'mL',
        mw.calculators.objectClasses.CalculatorObject.call( this, this.getProperties(), propertyValues );
             formula: '<math>\\mathrm{SV}=\\frac{\\mathrm{CO}}{\\mathrm{HR}}</math>',
 
            link: false,
         this.initialize();
             references: [],
    };
             calculate: function( data ) {
 
                 var cardiacOutput = data.cardiacOutputFick.toNumber( 'mL/min' );
    mw.calculators.objectClasses.AbstractCalculation.prototype = Object.create( mw.calculators.objectClasses.CalculatorObject.prototype );
                var heartRate = data.heartRate.toNumber();
 
    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>' );


                 return cardiacOutput / heartRate;
            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;
         }
         }
    } );


    // Neuro
        return $( '<span>', {
    mw.calculators.addCalculations( {
            class: this.getElementClasses( 'infoButton' )
         brainMass: {
         } )
             name: 'Brain mass',
             .append( $( '<a>', {
            data: {
                'data-toggle': 'collapse',
                 variables: {
                 href: '#' + infoContainerId,
                    optional: [ 'age', 'gender' ]
                role: 'button',
                 }
                'aria-expanded': 'false',
             },
                 'aria-controls': infoContainerId
            digits: 0,
             } )
            units: 'gwt',
                .append( $( '<i>', {
            references: [
                    class: 'far fa-question-circle'
                '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.'
                } ) ) );
    };
 
    mw.calculators.objectClasses.AbstractCalculation.prototype.getProperties = function() {
        return {
            required: [
                'id',
                'calculate'
             ],
             ],
             calculate: function( data ) {
             optional: [
                 var age = data.age ? data.age.toNumber( 'yr' ) : null;
                'data',
                var gender = data.gender ? data.gender : null;
                'description',
                'formula',
                'onRender',
                'onRendered',
                'references',
                 'type'
            ]
        };
    };


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


                if( age !== null ) {
    mw.calculators.objectClasses.AbstractCalculation.prototype.getTitleHtml = function() {
                    if( age <= 10 / 365 ) {
        return this.getTitleString();
                        // <=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 );
    mw.calculators.objectClasses.AbstractCalculation.prototype.getTitleString = function() {
                        brainMassMale = 380 + ageFactor * ( 640 - 380 );
        return this.id;
                    } else if( age <= 8 * 30 / 365 ) {
    };
                        // <=8 months
 
                        brainMassFemale = 580;
    mw.calculators.objectClasses.AbstractCalculation.prototype.getValue = function() {
                        brainMassMale = 640;
        // For now, we always need to recalculate, since the calculation may not be rendered but still required by
                    } else if( age <= 18 * 30 / 365 ) {
        // other calculations (i.e. drug dosages using lean body weight).
                        // <=18 months
        this.recalculate();
                        brainMassFemale = 940;
 
                        brainMassMale = 970;
        return this.value;
                    } else if( age <= 30 * 30 / 365 ) {
    };
                        // <=30 months
 
                        brainMassFemale = 1040;
    mw.calculators.objectClasses.AbstractCalculation.prototype.hasInfo = function() {
                        brainMassMale = 1120;
        return this.description || this.formula || this.references.length;
                    } else if( age <= 43 * 30 / 365 ) {
    };
                        // <=43 months
 
                        brainMassFemale = 1090;
    mw.calculators.objectClasses.AbstractCalculation.prototype.hasValue = function() {
                        brainMassMale = 1270;
        if( this.value === null ||
                    } else if( age <= 5 ) {
            ( this.isValueMathObject() && !this.value.toNumber() ) ) {
                        brainMassFemale = 1150;
            return false;
                        brainMassMale = 1300;
        }
                    } else if( age <= 7 ) {
 
                        brainMassFemale = 1210;
        return true;
                        brainMassMale = 1330;
    };
                    } else if( age <= 9 ) {
 
                        brainMassFemale = 1180;
    mw.calculators.objectClasses.AbstractCalculation.prototype.getCalculationData = function() {
                        brainMassMale = 1370;
        return this.data;
                    } else if( age <= 12 ) {
    };
                        brainMassFemale = 1260;
 
                        brainMassMale = 1440;
    mw.calculators.objectClasses.AbstractCalculation.prototype.getCalculationDataValues = function() {
                    } else if( age <= 15 ) {
        var calculationData = this.getCalculationData();
                        brainMassFemale = 1280;
 
                        brainMassMale = 1410;
        var data = {};
                    } else if( age <= 18 ) {
        var missingRequiredData = '';
                        brainMassFemale = 1340;
        var calculationId, calculation, variableId, variable;
                        brainMassMale = 1440;
 
                    } else if( age <= 21 ) {
        for( var iRequiredCalculation in calculationData.calculations.required ) {
                        brainMassFemale = 1310;
            calculationId = calculationData.calculations.required[ iRequiredCalculation ];
                        brainMassMale = 1450;
            calculation = mw.calculators.getCalculation( calculationId );
                    } else if( age <= 30 ) {
 
                        brainMassFemale = 1300;
            if( !calculation ) {
                        brainMassMale = 1440;
                throw new Error( 'Invalid required calculation "' + calculationId + '" for calculation "' + this.id + '"' );
                    } else if( age <= 40 ) {
            } else if( !calculation.hasValue() ) {
                        brainMassFemale = 1290;
                if( missingRequiredData ) {
                        brainMassMale = 1440;
                     missingRequiredData = missingRequiredData + ', ';
                    } 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' ) {
                 missingRequiredData = missingRequiredData + String( calculation );
                    return brainMassFemale;
            } else {
                } else if( gender === 'M' ) {
                data[ calculationId ] = calculation.value;
                    return brainMassMale;
            }
                 } else {
        }
                     return ( brainMassFemale + brainMassMale ) / 2;
 
        for( var iRequiredVariable in calculationData.variables.required ) {
            variableId = calculationData.variables.required[ iRequiredVariable ];
            variable = mw.calculators.getVariable( variableId );
 
            if( !variable ) {
                throw new Error( 'Invalid required variable "' + variableId + '" for calculation "' + this.id + '"' );
            } else if( !variable.hasValue() ) {
                 if( missingRequiredData ) {
                     missingRequiredData = missingRequiredData + ', ';
                 }
                 }
                missingRequiredData = missingRequiredData + String( variable );
            } else {
                data[ variableId ] = variable.getValue();
            }
        }
        if( missingRequiredData ) {
            this.message = missingRequiredData + ' required';
            return false;
        }
        for( var iOptionalCalculation in calculationData.calculations.optional ) {
            calculationId = calculationData.calculations.optional[ iOptionalCalculation ];
            calculation = mw.calculators.getCalculation( calculationId );
            if( !calculation ) {
                throw new Error( 'Invalid optional calculation "' + calculationId + '" for calculation "' + this.id + '"' );
             }
             }
        },
        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;
            data[ calculationId ] = calculation.hasValue() ? calculation.value : null;
        }
 
        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 + '"' );
             }
             }
        },
        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;
            data[ variableId ] = variable.hasValue() ? variable.getValue() : null;
        }
 
        return data;
    };
 
    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( temperature ) {
            if( this.type === TYPE_NUMBER && !isNaN( value ) ) {
                     cerebralMetabolicRateFactor += 0.07 * ( temperature - 37 );
                 if( this.units ) {
                     value = value + ' ' + this.units;
                 }
                 }


                 return cerebralMetabolicRateFactor;
                 this.value = math.unit( value );
            } else {
                this.value = value;
             }
             }
         },
         } catch( e ) {
        cerebralMetabolicRateO2: {
             console.warn( e.message );
             name: 'Cerebral metabolic rate (O<sub>2</sub>)',
 
             abbreviation: 'CMRO<sub>2</sub>',
             this.message = e.message;
             data: {
             this.value = null;
                calculations: {
        } finally {
                    required: [ 'brainMass', 'cerebralMetabolicRateFactor' ]
            this.valueUpdated();
                },
        }
                variables: {
 
                    optional: [ 'temperature' ]
        return true;
                }
    };
            },
 
            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: [
    mw.calculators.objectClasses.AbstractCalculation.prototype.render = function() {
                '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'
        this.recalculate();
             ],
 
            calculate: function( data ) {
        if( typeof this.onRender === 'function' ) {
                // Temperature is included as an optional variable to generate the input.
             this.onRender();
                // It is used by cerebralMetabolicRateFactor, which is an internal calculation not typically shown.
        }
                var brainMass = data.brainMass.toNumber( 'gwt' );
 
                var cerebralMetabolicRateFactor = data.cerebralMetabolicRateFactor.toNumber();
        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 ];


                 return cerebralMetabolicRateFactor * 3 * brainMass / 100;
            if( !mw.calculators.calculations.hasOwnProperty( calculationId ) ) {
                 throw new Error('Calculation "' + calculationId + '" does not exist for calculation "' + this.id + '"');
             }
             }
        },
        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;
            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 ) ) {
                 throw new Error('Variable "' + variableId + '" does not exist for calculation "' + this.id + '"');
             }
             }
        },
        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;
            mw.calculators.variables[ variableId ].addCalculation( this.id );
        }


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


                    cerebralBloodFlow += 1.5 * brainMass / 100 * ( paCO2 - 40 );
    mw.calculators.objectClasses.AbstractCalculation.prototype.toString = function() {
        return this.getTitleString();
    };


                    cerebralBloodFlow = math.max( cerebralBloodFlow, minCerebralBloodFlow );
    mw.calculators.objectClasses.AbstractCalculation.prototype.valueUpdated = function() {
                }
        for( var iCalculation in this.calculations ) {
            calculation = mw.calculators.getCalculation( this.calculations[ iCalculation ] );


                 return cerebralBloodFlow;
            if( calculation ) {
                 calculation.render();
             }
             }
         }
         }
     } );
     };






     // Pulmonary
     /**
     mw.calculators.addCalculations( {
    * Class CalculationData
         aaGradientO2Expected: {
    * @param {Object} propertyValues
             name: 'A-a O<sub>2</sub> gradient (expected)',
    * @returns {mw.calculators.objectClasses.CalculationData}
             abbreviation: 'A-a O<sub>2</sub> ex.',
    * @constructor
            data: {
    */
                variables: {
     mw.calculators.objectClasses.CalculationData = function( propertyValues ) {
                     required: [ 'age' ]
         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 ] = [];
                    }
                 }
                 }
             },
             }
             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.'
 
            ],
    mw.calculators.objectClasses.CalculationData.prototype = Object.create( mw.calculators.objectClasses.CalculatorObject.prototype );
             calculate: function( data ) {
 
                 var age = data.age.toNumber( 'yr' );
    mw.calculators.objectClasses.CalculationData.prototype.getDataRequirements = function() {
        return [
            '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 ) );
 
        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 );


                 return ( age + 10 ) / 4;
                 mergedData[ dataType ].optional = mergedData[ dataType ].optional
                    .concat( data[ iData ][ dataType ].optional )
                    .filter( mw.calculators.uniqueValues );
             }
             }
         },
         }
         lowTidalVolume: {
 
            name: 'Low tidal volume',
         return mergedData;
            abbreviation: 'LTV',
    };
            data: {
 
                calculations: {
 
                    required: [ 'ibw' ]
 
                }
 
             },
 
            type: 'string',
    /**
            description: '6-8 mL/kg',
    * Class SimpleCalculation
            references: [
    * @param {Object} propertyValues
                '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.'
    * @returns {mw.calculators.objectClasses.SimpleCalculation}
            ],
    * @constructor
            calculate: function( data ) {
    */
                var ibw = data.ibw.toNumber( 'kgwt' );
    mw.calculators.objectClasses.SimpleCalculation = function( propertyValues ) {
        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 ];


                return math.round( 6 * ibw ) + '-' + math.round( 8 * ibw ) + ' mL';
            if( !$( '#calculator-input-' + variableId ).length ) {
                missingVariableInputs.push( variableId );
             }
             }
         }
         }
    } );


    var tableMaxWidth = 600;
        // Out of 12, uses Bootstrap col- classes in a container
        var titleColumns = '7';
        var valueColumns = '5';


    mw.calculators.addCalculators( moduleId, {
        // Store this object in a local variable since .each() will reassign this to the DOM object of each
         anatomy: {
        // calculation container.
             name: 'Patient statistics',
        var calculation = this;
             calculations: [
        var calculationCount = 0;
                 'bmi',
 
                 'bsa',
        // Eventually may implement different rendering, so we should regenerate
                 'ibw',
        // all elements with each iteration of the loop.
                 'lbw'
        // I.e. might show result in table and inline in 2 different places of article.
             ],
         $calculationContainer.each( function() {
            css: {
             // Initalize the variables for all the elements of the calculation. These need to be in order of placement
                 'max-width': tableMaxWidth
            // 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
            elements.title.$container = $( '<div>', {
                id: elements.title.id
            } );
 
            elements.title.$container.append( calculation.getTitleHtml() );
 
            if( calculation.hasInfo() ) {
                elements.title.$container.append( calculation.getInfoButton( calculationCount ) );
 
                // Id of the info container should already be set by getInfo()
                elements.info.$container = calculation.getInfo();
             }
             }
        },
 
        fluidManagement: {
            // Create the value element
            name: 'Fluid management',
            elements.value.$container = $( '<div>', {
             calculations: [
                class: 'col-' + valueColumns + ' ' + calculation.getElementClasses( 'value' )
                 'ebv',
            } ).append( valueString );
                 'fluidMaintenanceRate',
 
                'intraopFluids',
            if( !missingVariableInputs.length ) {
                 'maxAbl',
                // If we have no variable inputs to show, we can put the title and value in one row of the table
                 'minUop'
                elements.title.$container.addClass( 'col-' + titleColumns + ' border-right' );
            ],
 
            css: {
                // Add the id attribute to the value container
                'max-width': tableMaxWidth
                elements.value.$container.attr( 'id', elements.value.id );
             } 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
                );
             }
             }
        },
 
        cardiovascular: {
            // Add the title classes after the layout classes
             name: 'Cardiovascular',
             elements.title.$container.addClass( calculation.getElementClasses( 'title' ) );
             calculations: [
 
                 'vO2',
            // Iterate over elementTypes since it is in order of rendering
                 'cardiacOutputFick',
             for( var iElementType in elementTypes ) {
                'cardiacIndex',
                var elementType = elementTypes[ iElementType ];
                'strokeVolume'
 
            ],
                 var $existingContainer = $( '#' + elements[ elementType ].id );
            css: {
 
                 'max-width': tableMaxWidth
                 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 );
                 }
             }
             }
         },
 
        neuro: {
            calculationCount++;
            name: 'Neuro',
         } );
            calculations: [
    };
                'brainMass',
 
                'cerebralBloodVolume',
    mw.calculators.objectClasses.SimpleCalculation.prototype.getClassName = function() {
                'cerebralMetabolicRateO2',
        return 'SimpleCalculation';
                'cerebralMetabolicRateGlucose',
    };
                 'cerebralBloodFlow'
 
    mw.calculators.objectClasses.SimpleCalculation.prototype.getProperties = function() {
        var inheritedProperties = mw.calculators.objectClasses.AbstractCalculation.prototype.getProperties();
 
        return this.mergeProperties( inheritedProperties, {
            required: [
                 'name'
             ],
             ],
             css: {
             optional: [
                 'max-width': tableMaxWidth
                'abbreviation',
                '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 ] );
             }
             }
         },
 
         pulmonary: {
            titleHtml = $( '<a>', {
             name: 'Pulmonary',
                href: href,
             calculations: [
                text: titleHtml
                 'aaGradientO2Expected',
            } )[ 0 ].outerHTML;
                 'lowTidalVolume'
        }
 
        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'
             ],
             ],
             css: {
             optional: [
                'max-width': tableMaxWidth
                '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:08, 27 August 2021

/**
 * @author Chris Rishel
 */
( function() {
    var COOKIE_EXPIRATION = 12 * 60 * 60;

    var TYPE_NUMBER = 'number';
    var TYPE_STRING = 'string';

    var VALID_TYPES = [
        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;
            }
        }

        return null;
    };


    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();
            }
        },
        addCalculators: function( moduleId, calculatorData, className ) {
            className = className ? className : DEFAULT_CALCULATOR_CLASS;

            for( var calculatorId in calculatorData ) {
                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' );
                    }
                }
            }

            var calculators = mw.calculators.createCalculatorObjects( className, calculatorData );

            // Initalize the calculators property for the module
            if( !mw.calculators.calculators.hasOwnProperty( moduleId ) ) {
                mw.calculators.calculators[ moduleId ] = {};
            }

            // Store the calculators
            for( var calculatorId in calculators ) {
                mw.calculators.calculators[ moduleId ][ calculatorId ] = calculators[ calculatorId ];

                mw.calculators.calculators[ moduleId ][ calculatorId ].render();
            }
        },
        addUnitsBases: function( unitsBaseData ) {
            var unitsBases = mw.calculators.createCalculatorObjects( 'UnitsBase', unitsBaseData );

            for( var unitsBaseId in unitsBases ) {
                mw.calculators.unitsBases[ unitsBaseId ] = unitsBases[ unitsBaseId ];
            }
        },
        addUnits: function( unitsData ) {
            var units = mw.calculators.createCalculatorObjects( 'Units', unitsData );

            for( var unitsId in units ) {
                if( mw.calculators.units.hasOwnProperty( unitsId ) ) {
                    continue;
                }

                try {
                    var unitData = {
                        aliases: units[ unitsId ].aliases,
                        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 ) {
            var variables = mw.calculators.createCalculatorObjects( 'Variable', variableData );

            for( var variableId in variables ) {
                mw.calculators.variables[ variableId ] = variables[ variableId ];

                var cookieValue = mw.calculators.getCookieValue( variableId );

                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 ) {
            if( !mw.calculators.objectClasses.hasOwnProperty( className ) ) {
                throw new Error( 'Invalid class name "' + className + '"' );
            }

            var objects = {};

            for( var objectId in objectData ) {
                var propertyValues = objectData[ objectId ];

                // Id can either be specified using the 'id' property, or as the property name in objectData
                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 ) {
            var $form = $( '<form>', {
                novalidate: true
            } );

            var $formRow = $( '<div>', {
                class: 'form-row'
            } ).css( 'flex-wrap', 'nowrap' );

            var inputOptions = {
                global: !!global
            };

            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 ) {
            return 'calculators-var-' + variableId;
        },
        getCookieValue: function( varId ) {
            var cookieValue = mw.cookie.get( mw.calculators.getCookieKey( varId ) );

            if( !cookieValue ) {
                return null;
            }

            return cookieValue;
        },
        getCalculation: function( calculationId ) {
            if( mw.calculators.calculations.hasOwnProperty( calculationId ) ) {
                return mw.calculators.calculations[ calculationId ];
            } else {
                return null;
            }
        },
        getCalculator: function( moduleId, calculatorId ) {
            if( mw.calculators.calculators.hasOwnProperty( moduleId ) &&
                mw.calculators.calculators[ moduleId ].hasOwnProperty( calculatorId ) ) {
                return mw.calculators.calculators[ moduleId ][ calculatorId ];
            } else {
                return null;
            }
        },
        getUnitsByBase: function( value ) {
            if( typeof value !== 'object' || !value.hasOwnProperty( 'units' ) ) {
                return null;
            }

            var unitsByBase = {};

            for( var iUnits in value.units ) {
                var units = value.units[ iUnits ];

                unitsByBase[ units.unit.base.key.toLowerCase() ] = units.prefix.name + units.unit.name;
            }

            return unitsByBase;
        },
        getUnitsString: function( value ) {
            if( typeof value !== 'object' ) {
                return null;
            }

            var unitsString = value.formatUnits();

            var reDenominator = /\/\s?\((.*)\)/;
            var denominatorMatches = unitsString.match( reDenominator );

            if( denominatorMatches ) {
                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 ) {
                unitsBase = unitsBase.toLowerCase();

                if( mw.calculators.unitsBases.hasOwnProperty( unitsBase ) &&
                    typeof mw.calculators.unitsBases[ unitsBase ].toString === 'function' ) {
                    unitsString = mw.calculators.unitsBases[ unitsBase ].toString( unitsString );
                }
            } else {
                // TODO nasty hack to fix weight units in compound units which have no base
                unitsString = unitsString.replace( 'kgwt', 'kg' );
                unitsString = unitsString.replace( 'ug', 'mcg' );
            }

            return unitsString;
        },
        getValueDecimals: function( value ) {
            // Supports either numeric values or math objects
            if( mw.calculators.isValueMathObject( value ) ) {
                value = mw.calculators.getValueNumber( value );
            }

            if( typeof value !== 'number' ) {
                return null;
            }

            // Convert the number to a string, reverse, and count the number of characters up to the period.
            var decimals = value.toString().split('').reverse().join('').indexOf( '.' );

            // If no decimal is present, will be set to -1 by indexOf. If so, set to 0.
            decimals = decimals > 0 ? decimals : 0;

            return decimals;
        },
        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 ) {
            if( !mw.calculators.isValueMathObject( value ) ) {
                return null;
            }

            var valueNumber = mw.calculators.getValueNumber( value, decimals );
            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;
                }

                if( oldSIUnit ) {
                    // 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.
                    var newSIUnit = newSIValue.formatUnits();

                    if( newSIUnit !== oldSIUnit ) {
                        var newValue = math.unit( newSIValue.toNumber() + ' ' + value.formatUnits().replace( oldSIUnit, newSIUnit ) );

                        valueNumber = mw.calculators.getValueNumber( newValue, decimals );
                        valueUnits = mw.calculators.getUnitsString( newValue );
                    }
                }
            }

            var valueString = String( valueNumber );

            if( valueUnits ) {
                valueString += ' ' + valueUnits;
            }

            return valueString;
        },
        getVariable: function( variableId ) {
            if( mw.calculators.variables.hasOwnProperty( variableId ) ) {
                return mw.calculators.variables[ variableId ];
            } else {
                return null;
            }
        },
        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 );
                }
            } );
        },
        isMobile: function() {
            return window.matchMedia( 'only screen and (max-width: 760px)' ).matches;
        },
        isValueMathObject: function( value ) {
            return value && value.hasOwnProperty( 'value' );
        },
        setCookieValue: function( variableId, value ) {
            mw.cookie.set( mw.calculators.getCookieKey( variableId ), value, {
                expires: COOKIE_EXPIRATION
            } );
        },
        setValue: function( variableId, value ) {
            if( !mw.calculators.variables.hasOwnProperty( variableId ) ) {
                return false;
            }

            if( !mw.calculators.variables[ variableId ].setValue( value ) ) {
                return false;
            }

            mw.calculators.setCookieValue( variableId, value );

            return true;
        },
        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' ) ) {
                for( var iOptionalProperty in properties.optional ) {
                    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 ) {
                console.warn( 'Unsupported properties defined for ' + typeof this + ' with id "' + this.id + '": ' + invalidProperties.join( ', ' ) );
            }
        }
    };

    mw.calculators.objectClasses.CalculatorObject.prototype.getProperties = function() {
        return {
            required: [],
            optional: []
        };
    };

    mw.calculators.objectClasses.CalculatorObject.prototype.mergeProperties = function( inheritedProperties, properties ) {
        var uniqueValues = function( value, index, self ) {
            return self.indexOf( value ) === index;
        };

        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: [
                'toString'
            ]
        };

        mw.calculators.objectClasses.CalculatorObject.call( this, properties, propertyValues );
    };

    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 );




    /**
     * Class Variable
     * @param {Object} propertyValues
     * @returns {mw.calculators.objectClasses.Variable}
     * @constructor
     */
    mw.calculators.objectClasses.Variable = function( propertyValues ) {
        mw.calculators.objectClasses.CalculatorObject.call( this, this.getProperties(), propertyValues );

        if( VALID_TYPES.indexOf( this.type ) === -1 ) {
            throw new Error( 'Invalid type "' + this.type + '" for variable "' + this.id + '"' );
        }

        // Accept options as either an array of strings, or an object with ids as keys and display text as values
        if( Array.isArray( this.options ) ) {
            var options = {};

            for( var iOption in this.options ) {
                var option = this.options[ iOption ];

                options[ option ] = option;
            }

            this.options = options;
        }

        this.calculations = [];

        if( this.defaultValue ) {
            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++;
            }

            inputId += '-' + inputIdCount;
        }

        var inputContainerTag = inputOptions.inline ? '<span>' : '<div>';

        var inputContainerAttributes = {
            class: 'form-group mb-0 calculator-container-input'
        };

        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
                        .css( 'padding', '0 0.5em' )
                        .append( mw.calculators.getUnitsString( math.unit( '0 ' + this.units[ 0 ] ) ) )
                        .append( $( '<input>', unitsInputAttributes ) );
                } else {
                    // Initialize the units input options
                    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' );
                    } else {
                        $( this ).parent().removeClass( 'is-invalid' );
                    }
                } );

            // Create the input group
            var $inputGroup = $( '<div>', {
                class: 'input-group'
            } ).append( $input );

            if( $unitsContainer ) {
                $inputGroup.append( $unitsContainer );
            }

            $inputContainer.append( $inputGroup );
        } else if( this.type === TYPE_STRING ) {
            if( this.hasOptions() ) {
                var optionKeys = Object.keys( this.options );

                if( optionKeys.length === 1 ) {
                    $inputContainer.append( this.options[ optionKeys[ 0 ] ] );
                } else {
                    var selectAttributes = {
                        id: inputId,
                        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 ) {
            $inputContainer.append( $( '<div>', {
                class: 'invalid-feedback'
            } ) );
        }

        return $inputContainer;
    };

    mw.calculators.objectClasses.Variable.prototype.getLabelString = function() {
        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 );
                }
            }
        }

        return value;
    };

    mw.calculators.objectClasses.Variable.prototype.setValue = function( value ) {
        var validateResult = this.validateValue( value );

        this.valid = !!validateResult.valid;
        this.message = validateResult.message;

        if( !this.valid ) {
            this.value = null;
            this.valueUpdated();

            return false;
        }

        this.value = this.prepareValue( value );

        this.valueUpdated();

        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;

            if( this.hasUnits() ) {
                valueUnits = value.formatUnits();

                if( !valueUnits ) {
                    // 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' );
                }
            }

            if( this.minValue && math.smaller( value, this.minValue ) ) {
                var minValueString = valueUnits ? mw.calculators.getValueString( this.minValue.to( valueUnits ) ) :
                    mw.calculators.getValueString( this.minValue );

                result.message = String( this ) + ' must be at least ' + minValueString;
                result.valid = false;
            } 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;
                result.valid = false;
            }
        } else if( this.hasOptions() ) {
            if( !this.options.hasOwnProperty( value ) ) {
                // Unlikely to be a user error, so don't set message
                result.valid = false;

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

        return result;
    };

    mw.calculators.objectClasses.Variable.prototype.valueUpdated = function() {
        for( var iCalculation in this.calculations ) {
            var calculation = mw.calculators.getCalculation( this.calculations[ iCalculation ] );

            if( calculation ) {
                calculation.render();
            }
        }
    };



    /**
     * 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: [
                'data',
                'description',
                'formula',
                'onRender',
                'onRendered',
                'references',
                'type'
            ]
        };
    };

    mw.calculators.objectClasses.AbstractCalculation.prototype.getReferences = function() {
        return this.references;
    };

    mw.calculators.objectClasses.AbstractCalculation.prototype.getTitleHtml = function() {
        return this.getTitleString();
    };

    mw.calculators.objectClasses.AbstractCalculation.prototype.getTitleString = function() {
        return this.id;
    };

    mw.calculators.objectClasses.AbstractCalculation.prototype.getValue = function() {
        // For now, we always need to recalculate, since the calculation may not be rendered but still required by
        // other calculations (i.e. drug dosages using lean body weight).
        this.recalculate();

        return this.value;
    };

    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;
            }
        }

        for( var iRequiredVariable in calculationData.variables.required ) {
            variableId = calculationData.variables.required[ iRequiredVariable ];
            variable = mw.calculators.getVariable( variableId );

            if( !variable ) {
                throw new Error( 'Invalid required variable "' + variableId + '" for calculation "' + this.id + '"' );
            } else if( !variable.hasValue() ) {
                if( missingRequiredData ) {
                    missingRequiredData = missingRequiredData + ', ';
                }

                missingRequiredData = missingRequiredData + String( variable );
            } else {
                data[ variableId ] = variable.getValue();
            }
        }

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

            return false;
        }

        for( var iOptionalCalculation in calculationData.calculations.optional ) {
            calculationId = calculationData.calculations.optional[ iOptionalCalculation ];
            calculation = mw.calculators.getCalculation( calculationId );

            if( !calculation ) {
                throw new Error( 'Invalid optional calculation "' + calculationId + '" for calculation "' + this.id + '"' );
            }

            data[ calculationId ] = calculation.hasValue() ? calculation.value : null;
        }

        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;
        }

        return data;
    };

    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;
                }

                this.value = math.unit( value );
            } else {
                this.value = value;
            }
        } catch( e ) {
            console.warn( e.message );

            this.message = e.message;
            this.value = null;
        } finally {
            this.valueUpdated();
        }

        return true;
    };



    mw.calculators.objectClasses.AbstractCalculation.prototype.render = function() {
        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 ) ) {
                throw new Error('Variable "' + variableId + '" does not exist for calculation "' + this.id + '"');
            }

            mw.calculators.variables[ variableId ].addCalculation( this.id );
        }

        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();
            }
        }
    };



    /**
     * Class CalculationData
     * @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 ] = [];
                    }
                }
            }
        }
    };

    mw.calculators.objectClasses.CalculationData.prototype = Object.create( mw.calculators.objectClasses.CalculatorObject.prototype );

    mw.calculators.objectClasses.CalculationData.prototype.getDataRequirements = function() {
        return [
            '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 ) );

        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;
    };





    /**
     * Class SimpleCalculation
     * @param {Object} propertyValues
     * @returns {mw.calculators.objectClasses.SimpleCalculation}
     * @constructor
     */
    mw.calculators.objectClasses.SimpleCalculation = function( propertyValues ) {
        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 );
            }
        }

        // Out of 12, uses Bootstrap col- classes in a container
        var titleColumns = '7';
        var valueColumns = '5';

        // Store this object in a local variable since .each() will reassign this to the DOM object of each
        // calculation container.
        var calculation = this;
        var calculationCount = 0;

        // Eventually may implement different rendering, so we should regenerate
        // 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
            elements.title.$container = $( '<div>', {
                id: elements.title.id
            } );

            elements.title.$container.append( calculation.getTitleHtml() );

            if( calculation.hasInfo() ) {
                elements.title.$container.append( calculation.getInfoButton( calculationCount ) );

                // Id of the info container should already be set by getInfo()
                elements.info.$container = calculation.getInfo();
            }

            // Create the value element
            elements.value.$container = $( '<div>', {
                class: 'col-' + valueColumns + ' ' + calculation.getElementClasses( 'value' )
            } ).append( valueString );

            if( !missingVariableInputs.length ) {
                // If we have no variable inputs to show, we can put the title and value in one row of the table
                elements.title.$container.addClass( 'col-' + titleColumns + ' border-right' );

                // Add the id attribute to the value container
                elements.value.$container.attr( 'id', elements.value.id );
            } 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
            elements.title.$container.addClass( calculation.getElementClasses( 'title' ) );

            // Iterate over elementTypes since it is in order of rendering
            for( var iElementType in elementTypes ) {
                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: [
                'abbreviation',
                '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();

}() );