MediaWiki:Gadget-calculator-anatomyPhysiology.js
From WikiAnesthesia
Revision as of 04:03, 21 August 2021 by Chris Rishel (talk | contribs)
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
/**
* @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 {
// isValueValid will throw an error if invalid, so the catch clause is our else condition
if( mw.calculators.variables[ variableId ].isValueValid( cookieValue ) ) {
mw.calculators.variables[ variableId ].setValue( cookieValue );
}
} catch( e ) {
// 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 ) {
var $form = $( '<form>', {
} );
var $formRow = $( '<div>', {
class: 'form-row'
} ).css( 'flex-wrap', 'nowrap' );
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() );
}
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 ) ) {
mw.calculators.setCookieValue( variableId, value );
return true;
}
return false;
},
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 );
}
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.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;
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 += ': ';
labelCss[ 'margin-bottom' ] = 0;
}
// Create the input container
var $inputContainer = $( inputContainerTag, inputContainerAttributes ).css( inputContainerCss );
var $label = $( '<label>', labelAttributes ).css( labelCss );
$inputContainer.append( $label );
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 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 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;
mw.calculators.setValue( variableId, newValue );
} );
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();
}
mw.calculators.setValue( variableId, newValue );
} );
// 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 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() {
mw.calculators.setValue( variableId, $( this ).val() );
} );
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 );
}
}
}
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.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.isValueValid = function( value ) {
if( value === null ) {
return true;
}
if( this.type === TYPE_NUMBER ) {
if( typeof value !== 'object' ) {
value = math.unit( value );
}
if( this.hasUnits() ) {
var valueUnits = value.formatUnits();
if( !valueUnits ) {
throw new Error( 'Could not set value for "' + this.id + '": Value must define units' );
} else if( this.units.indexOf( valueUnits ) === -1 ) {
throw new Error( 'Could not set value for "' + this.id + '": Units "' + valueUnits + '" are not valid for this variable' );
}
}
} else if( this.hasOptions() ) {
if( !this.options.hasOwnProperty( value ) ) {
throw new Error( 'Could not set value "' + value + '" for "' + this.id + '": Value must define be one of: ' + Object.keys( this.options ).join( ', ' ) );
}
}
return true;
};
mw.calculators.objectClasses.Variable.prototype.prepareValue = function( value ) {
if( !this.isValueValid( value ) ) {
// isValueValid will throw a meaningful error to the console
return null;
}
if( value !== null ) {
if( this.type === TYPE_NUMBER ) {
if( typeof value !== 'object' ) {
value = math.unit( value );
}
}
}
return value;
};
mw.calculators.objectClasses.Variable.prototype.setValue = function( value ) {
this.value = this.prepareValue( value );
this.valueUpdated();
return true;
};
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() {};
mw.calculators.objectClasses.AbstractCalculation.prototype.getContainerClass = function() {
return 'calculator-calculation-' + this.id;
};
mw.calculators.objectClasses.AbstractCalculation.prototype.getLabelString = function() {
return this.id;
};
mw.calculators.objectClasses.AbstractCalculation.prototype.getProperties = function() {
return {
required: [
'id',
'calculate'
],
optional: [
'data',
'description',
'onRender',
'onRendered',
'references',
'type'
]
};
};
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 false;
};
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 + calculation.getLabelString();
} 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 + variable.getLabelString();
} 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.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.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();
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.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.hasInfo = function() {
return this.description || this.formula || this.references.length;
};
mw.calculators.objectClasses.SimpleCalculation.prototype.getLabelHtml = function() {
var labelHtml = this.getLabelString();
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 ] );
}
labelHtml = $( '<a>', {
href: href,
text: labelHtml
} )[ 0 ].outerHTML;
}
return labelHtml;
};
mw.calculators.objectClasses.SimpleCalculation.prototype.getLabelString = function() {
return mw.calculators.isMobile() && this.abbreviation ? this.abbreviation : this.name;
};
mw.calculators.objectClasses.SimpleCalculation.prototype.getProperties = function() {
var inheritedProperties = mw.calculators.objectClasses.AbstractCalculation.prototype.getProperties();
return this.mergeProperties( inheritedProperties, {
required: [
'name'
],
optional: [
'abbreviation',
'digits',
'formula',
'link',
'units'
]
} );
};
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 );
}
};
mw.calculators.objectClasses.SimpleCalculation.prototype.doRender = function() {
var $calculationContainer = $( '.' + this.getContainerClass() );
if( !$calculationContainer.length ) {
return;
}
var valueString = this.getValueString();
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 );
}
}
var calculation = this;
$calculationContainer.each( function() {
$( this ).empty();
var isTable = this.tagName.toLowerCase() === 'tr';
var $infoButton = null;
if( calculation.hasInfo() ) {
$infoButton = $( '<a>', {
'data-toggle': 'collapse',
href: '#' + calculation.getContainerClass() + '-info',
role: 'button',
'aria-expanded': 'false',
'aria-controls': calculation.getContainerClass() + '-info'
} )
.append( $( '<i>', {
class: 'far fa-question-circle'
} ) );
}
var labelHtml = calculation.getLabelHtml();
if( isTable ) {
if( calculation.hasInfo() ) {
labelHtml += $( '<span>', {
class: 'calculator-SimpleCalculator-info'
} ).append( $infoButton )[ 0 ].outerHTML;
}
$( this )
.append( $( '<th>', {
class: 'calculator-SimpleCalculator-calculation-cell',
html: labelHtml
} ) )
.append( $( '<td>', {
class: 'calculator-SimpleCalculator-value-cell',
html: valueString
} ) );
} else {
$( this )
.append( labelHtml + $infoButton[ 0 ].outerHTML + ': ' + valueString );
}
if( calculation.hasInfo() ) {
var infoHtml = '';
if( calculation.description ) {
infoHtml += $( '<p>', {
html: calculation.description
} )[ 0 ].outerHTML;
}
if( calculation.formula ) {
infoHtml += $( '<span>', {
class: calculation.getContainerClass() + '-formula'
} )[ 0 ].outerHTML;
var api = new mw.Api();
api.parse( calculation.formula ).then( function( result ) {
$( '.' + calculation.getContainerClass() + '-formula' ).html( result );
} );
}
if( calculation.references.length ) {
var $references = $( '<ol>' );
for( var iReference in calculation.references ) {
$references.append( $( '<li>', {
text: calculation.references[ iReference ]
} ) );
}
infoHtml += $references[ 0 ].outerHTML;
}
var infoContainerId = calculation.getContainerClass() + '-info';
var $infoContainer = $( '#' + infoContainerId );
if( $infoContainer.length ) {
$infoContainer.empty();
}
if( isTable ) {
$infoContainer = $( '<tr>', {
id: infoContainerId,
class: 'collapse'
} )
.append( $( '<td>', {
colspan: 2
} ).append( infoHtml ) );
} else {
$infoContainer = $( '<div>', {
id: infoContainerId,
class: 'collapse'
} ).append( infoHtml );
}
$( this ).after( $infoContainer );
}
if( missingVariableInputs.length ) {
var variablesContainerClass = 'calculator-SimpleCalculator-variables ' + calculation.getContainerClass() + '-variables';
var inputGroup = mw.calculators.createInputGroup( missingVariableInputs );
if( isTable ) {
$variablesContainer = $( '<tr>' )
.append( $( '<td>', {
class: variablesContainerClass,
colspan: 2
} ).append( inputGroup ) );
} else {
$variablesContainer = $( '<div>', {
class: variablesContainerClass
} ).append( inputGroup );
}
$( this ).after( $variablesContainer );
missingVariableInputs = [];
}
} );
};
/**
* 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.getCalculatorClass = function() {
return '';
};
mw.calculators.objectClasses.AbstractCalculator.prototype.getContainerClass = function() {
return 'calculator-' + this.module + '-' + this.id;
};
mw.calculators.objectClasses.AbstractCalculator.prototype.getProperties = function() {
return {
required: [
'id',
'module',
'name',
'calculations'
],
optional: [
'onRender',
'onRendered'
]
};
};
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.doRender = function() {};
/**
* 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.doRender = function() {
var $calculatorContainer = $( '.' + this.getContainerClass() );
if( !$calculatorContainer.length ) {
return;
}
$calculatorContainer.addClass( this.getCalculatorClass() );
if( this.css ) {
$calculatorContainer.css( this.css );
}
$calculatorContainer.empty();
$calculatorContainer.append( $( '<h4>', {
text: this.name
} ) );
var $calculationsContainer;
if( this.table ) {
$calculationsContainer = $( '<table>', {
class: 'wikitable'
} ).append( '<tbody>' );
$calculationsContainer
.append( $( '<tr>' )
.append(
$( '<th>', {
class: this.getCalculatorClass() + '-calculation-header'
} ).text( 'Calculation' ),
$( '<th>', {
class: this.getCalculatorClass() + '-value-header'
} ).text( 'Value' )
)
);
} else {
$calculationsContainer = $( '<div>' );
}
$calculatorContainer.append( $calculationsContainer );
for( var iCalculationId in this.calculations ) {
var calculation = mw.calculators.getCalculation( this.calculations[ iCalculationId ] );
var calculationContainerClass = calculation.getContainerClass();
var $calculationContainer = $( '.' + calculationContainerClass );
// If a container doesn't exist yet, add it
if( !$calculationContainer.length ) {
if( this.table ) {
$calculationContainer = $( '<tr>', {
class: calculationContainerClass
} );
} else {
$calculationContainer = $( '<div>', {
class: calculationContainerClass
} );
}
$calculationsContainer.append( $calculationContainer );
}
calculation.render();
}
};
mw.calculators.objectClasses.SimpleCalculator.prototype.getCalculatorClass = function() {
return 'calculator-SimpleCalculator';
};
mw.calculators.objectClasses.SimpleCalculator.prototype.getProperties = function() {
var inheritedProperties = mw.calculators.objectClasses.AbstractCalculator.prototype.getProperties();
return this.mergeProperties( inheritedProperties, {
required: [],
optional: [
'css',
'table'
]
} );
};
mw.calculators.initialize();
}() );