Created
June 10, 2019 22:38
-
-
Save dbcook/598e0a0c00df1c83ab728fba10f21b2a to your computer and use it in GitHub Desktop.
embeddable scale modeling dimension calculator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* <h1>Scale model calculator gadget</h1> | |
* This is a well-featured page-embeddable gadget to compute dimensions | |
* for scale models given the real-world prototype dimensions. | |
* | |
* <h2>Features</h2> | |
* <ul> | |
* <li>Selectable categories of predefined scales, e.g. model RR, plastic model, etc. | |
* <li>Supports arbitrary user-defined scale ratios. | |
* <li>Selectable sets of units for prototype and scale dimensions. | |
* <li>Reverse lookup of predefined scales when user types a user-defined scale factor. | |
* <li>Writes output history with computed results. | |
* <li>Selectable output history formats | |
* <ul> | |
* <li>Text with formatted tabular scale factors</li> | |
* <li>CSV for spreadsheet import</li> | |
* </ul> | |
* <li>Cookie persistence of preferences and recent output history. | |
* <li>Internationalization support. | |
* <li>Full CSS style and layout control. | |
* <li>Designed with HTML/CSS layout and Javascript UI widgets that bind to the HTML. | |
* <li>JavaScript code Passes jslint. | |
* <li>Full JavaScript code documentation with YUI Doc tags. | |
* </ul> | |
* | |
* <h2>Todo list</h2> | |
* <ul> | |
* <li>Button to emit a CSV header line to the output (pretty easy).</li> | |
* <li>Button to copy the calc history to the clipboard. Easy on IE, harder on FFox | |
* due to higher security levels. | |
* <li>Upload a file of dimensions and process it (requires server-side code) | |
* <li>Link to get calc tape output as XML file (requires server-side code) | |
* </ul> | |
* | |
* <h2>Configuration and Installation</h2> | |
* <h3>Required Tools</h3> | |
* For various reasons it's easier to install and run all the JavaScript tools on Linux. It can be done on Windows | |
* but that requires installing additional layers of tools just to have a Make utility and Bash shell. | |
* | |
* <p>To build the minified JavaScript, verify jslint compliance, and generate the code documentation, you need | |
* <ul> | |
* <li>Gnu Make. On Linux this is almost always present. On Windows you can use a Cygwin shell with Make installed.</li> | |
* <li><a href="http://developer.yahoo.com/yui/compressor/">YUI Compressor</a>. The fully-commented scalecalc package | |
* will shrink dramatically when minified, so it's highly recommended that you do so. You could also use | |
* the Google Closure compiler which is even more aggressive.</li> | |
* <li><a href="http://developer.yahoo.com/yui/yuidoc/">YUI Doc</a>. YUI Doc is much more robust than 'jsdoc'. | |
* The latter blows up on large source files and doesn't handle all forms of class definitions wrapped in | |
* anonymous functions.</li> | |
* <li><a href="http://www.jslint.com/">jslint</a>. See also the very latest jslint source at | |
* <a href="https://github.com/douglascrockford/JSLint">Crockford's GitHub repository</a>. There is no | |
* longer a Rhino version on jslint.com but a major update to jslint was posted 6 Jan 2011 so it may | |
* just be a matter of a little time. The new fulljslint.js provides a JSLint function that returns | |
* an object with the errors as a member, so a small wrapper will be needed for use with Rhino.</li> | |
* <li><a href="http://www.mozilla.org/rhino/">Rhino</a> (needed for standalone command-line execution of jslint under Java)</li> | |
* </ul> | |
* </p> | |
* <h3>Makefile Adjustments</h3> | |
* You will need to configure the Makefile to set the installed locations of these tools: | |
* <ul> | |
* <li>YUI Compressor</li> | |
* <li>YUI Doc</li> | |
* <li>Rhino</li> | |
* <li>JSLint</li> | |
* </ul> | |
* There is a set of Make variables defined at the top of the Makefile whose meanings should be self-evident. | |
* | |
* @module scalecalc | |
* @author Copyright 2009,2010,2011 by Dave Cook dcook@caveduck.com | |
* @version 1.0.1 | |
*/ | |
/*jslint browser: true */ | |
/*global $, Dom, Utils, window, DomElement, Div, Table, InputButton, InputText, Selector, Option, TextArea */ | |
(function () { | |
var U = Utils; | |
/** | |
* Constructs a new ScaleCalc scale model calculator gadget. | |
* @class ScaleCalc | |
*/ | |
/*global ScaleCalc:true*/ | |
ScaleCalc = function () { | |
var T = this, SC = ScaleCalc, CU = Const.Units, i, pl, sl, tmp, sselopts, prefs, unitsopt; | |
T.id = U.uniqueID("ScaleCalc"); | |
SC.loadPrefs(); | |
prefs = SC.prefs; | |
T.precision = 5; | |
T.ratioText = new InputText(".scaleFactorScaleRatioText", prefs.userDefScale, 8); | |
Dom.setTitle(T.ratioText, SC.i18n("RatioText_hover")); | |
Dom.addHandler(T.ratioText, "onchange", T.ratioTextChange, T); | |
// Set language-specific content of headings and labels | |
T.setLocaleStrings(); | |
// Scale domain selector | |
sselopts = [ | |
{key: "All_opt", val: "all"}, | |
{key: "Model_RR_opt", val: "rr"}, | |
{key: "Plastic_Models_opt", val: "pm"}, | |
{key: "Slot_Cars_opt", val: "slot"}, | |
{key: "Dollhouse_opt", val: "doll"} | |
]; | |
T.scaleSetSel = new Selector(".scaleFactorDomainSel", SC.i18n("Modeling_Domain_hover"), null, sselopts, SC.TT); | |
tmp = T.scaleSetSel; | |
tmp.setSelectedValue(prefs.scaleSet); | |
Dom.addHandler(tmp, "onchange", T.scaleSetSelOnchange, T); | |
// Scale selector | |
T.scSel = new Selector(".scaleFactorScaleName", SC.i18n("Scale_Name_hover")); | |
Dom.setSize(T.scSel, "10em"); | |
Dom.addHandler(T.scSel, "onchange", T.scSelOnchange, T); | |
// Units list with i18n keyword and selector val | |
unitsopt = [ | |
{key: "inches", val: "in"}, | |
{key: "feet", val: "ft"}, | |
{key: "millimeters", val: "mm"}, | |
{key: "centimeters", val: "cm"}, | |
{key: "meters", val: "m"} | |
]; | |
// prototype units selector | |
T.protoUnitsSel = new Selector(".protoUnitsSel", SC.i18n("PROTOTYPE_UNITS_hover"), null, unitsopt, SC.TT); | |
tmp = T.protoUnitsSel; | |
tmp.setMultiSelect(true); | |
tmp.setNumVis(5); | |
tmp.setOptionSelect(0, false); | |
pl = prefs.protoUnitsList.split(","); | |
for (i = 0; i < pl.length; i += 1) { | |
tmp.setOptionSelect(pl[i], true); | |
} | |
Dom.addHandler(tmp, "onchange", T.protoUnitsSelOnchange, T); | |
// scale units selector | |
T.scaleUnitsSel = new Selector(".modelUnitsSel", SC.i18n("SCALE_UNITS_hover"), null, unitsopt, SC.TT); | |
tmp = T.scaleUnitsSel; | |
tmp.setMultiSelect(true); | |
tmp.setNumVis(5); | |
tmp.setOptionSelect(0, false); | |
sl = prefs.scaleUnitsList.split(","); | |
for (i = 0; i < sl.length; i += 1) { | |
tmp.setOptionSelect(sl[i], true); | |
} | |
Dom.addHandler(tmp, "onchange", T.scaleUnitsSelOnchange, T); | |
// Load scale selector values and make scSel and ratioText consistent | |
T.scaleSetSelOnchange(); | |
// static numeric fields that get assigned to dynamic field lists | |
T.fld_R_in = T.createScaleField(8, true, CU.IN_TO_M, SC.i18n("UNITS_IN")); | |
T.fld_R_ft = T.createScaleField(8, true, CU.FT_TO_M, SC.i18n("UNITS_FT")); | |
T.fld_R_mm = T.createScaleField(8, true, CU.MM_TO_M, SC.i18n("UNITS_MM")); | |
T.fld_R_cm = T.createScaleField(8, true, CU.CM_TO_M, SC.i18n("UNITS_CM")); | |
T.fld_R_m = T.createScaleField(8, true, 1, SC.i18n("UNITS_M")); | |
T.fld_S_in = T.createScaleField(8, false, CU.IN_TO_M, SC.i18n("UNITS_IN")); | |
T.fld_S_ft = T.createScaleField(8, false, CU.FT_TO_M, SC.i18n("UNITS_FT")); | |
T.fld_S_mm = T.createScaleField(8, false, CU.MM_TO_M, SC.i18n("UNITS_MM")); | |
T.fld_S_cm = T.createScaleField(8, false, CU.CM_TO_M, SC.i18n("UNITS_CM")); | |
T.fld_S_m = T.createScaleField(8, false, 1, SC.i18n("UNITS_M")); | |
// prototype and scale dimensions tables | |
T.subtab1 = new Table(null, 1, 12); | |
T.subtab2 = new Table(null, 1, 12); | |
T.subtab1.setCell(0, 0, {content: SC.i18n("REAL"), sclass: "bold dimLabel"}); | |
T.subtab2.setCell(0, 0, {content: SC.i18n("MODEL"), sclass: "bold dimLabel"}); | |
T.protoDimSpan = new DomElement(".prototypeDimensionSpan", 'span', T.subtab1); | |
T.protoDimSpan.jq.addClass('scRealRow'); | |
T.modelDimSpan = new DomElement(".modelDimensionSpan", 'span', T.subtab2); | |
T.modelDimSpan.jq.addClass('scScaleRow'); | |
// Run proto & scale units selector change handlers to generate arrays of field refs | |
// and assign the active fields to display table cells | |
T.R_fields = []; | |
T.S_fields = []; | |
T.fields = []; | |
T.protoUnitsSelOnchange(); | |
T.scaleUnitsSelOnchange(); | |
// Item description label and field | |
T.descripText = new InputText(".itemDescription", "", 50); | |
Dom.setTitle(T.descripText, SC.i18n("Description_hover")); | |
// record button | |
T.recordButton = new InputButton('.recordButton', false, SC.i18n("Record")); | |
Dom.setTitle(T.recordButton, SC.i18n("Record_hover")); | |
Dom.addHandler(T.recordButton, "onclick", T.recordButtonOnclick, T); | |
// clear button | |
T.clearButton = new InputButton('.clearButton', false, SC.i18n("Clear")); | |
Dom.setTitle(T.clearButton, SC.i18n("Clear_hover")); | |
Dom.addHandler(T.clearButton, "onclick", T.clearButtonOnclick, T); | |
// output mode selector | |
T.outModeSel = new Selector(".outputFmtSel", SC.i18n("Output_Format_hover")); | |
tmp = T.outModeSel; | |
tmp.addOption(new Option(SC.i18n("Text"), "text")); | |
tmp.addOption(new Option(SC.i18n("CSV"), "csv")); | |
tmp.setSelectedValue(prefs.outputMode); | |
Dom.addHandler(tmp, "onchange", T.outModeSelOnchange, T); | |
// textarea for "calculator tape" history | |
T.calcTape = new TextArea(".outputTextarea"); | |
tmp = T.calcTape; | |
tmp.setAreaSize(15, 90); | |
tmp.setValue(prefs.history); | |
// Set up onchange handlers for the dimension fields | |
for (i = 0; i < T.fields.length; i += 1) { | |
Dom.addHandler(T.fields[i], "onchange", T.scFldChange, T.fields[i]); | |
} | |
console.log("ScaleCalc init OK\n"); | |
}; | |
var SC = ScaleCalc; | |
SC.fn = SC.prototype; | |
/** | |
* Preferences object, loaded from a cookie. | |
* @static | |
* @type Object | |
*/ | |
SC.prefs = { | |
protoUnitsList: "ft,m", | |
scaleUnitsList: "in,cm", | |
userDefScale: "48", | |
scaleSet: "all", | |
history: "", | |
outputMode: "text" | |
}; | |
/** | |
* Limit on length of calc tape history stored in prefs. | |
* @type number | |
* @static | |
*/ | |
SC.maxCalcHistoryLength = 1024; | |
/** | |
* Name of the gadget's cookie | |
* @static | |
* @type string | |
*/ | |
SC.cookieName = "scaleCalc"; | |
/** | |
* Loads preferences from the gadget's cookie, using a non-recursive merge. | |
* @static | |
*/ | |
SC.loadPrefs = function () { | |
var ob, str, ck; | |
ck = new U.Cookie(SC.cookieName); | |
ob = ck.getObj(); | |
if (ob) { | |
$.extend(SC.prefs, ob); | |
} | |
}; | |
/** | |
* Saves preferences to the gadget's cookie | |
* @static | |
*/ | |
SC.savePrefs = function () { | |
var ck = new U.Cookie(SC.cookieName); | |
ck.set(SC.prefs, 60); | |
}; | |
/** | |
* Wrapper for Utils.i18n to eliminate first arg | |
* @static | |
*/ | |
SC.i18n = function (k1, k2) { | |
return U.i18n(SC.TT, k1, k2); | |
}; | |
/** | |
* Sets language-specific content of headings and labels. Elements are selected by | |
* their unique classname in the layout file. | |
*/ | |
SC.fn.setLocaleStrings = function () { | |
$('.scaleCalcH1-title').html(SC.i18n("Scale_Model_Calculator")); | |
$('.legend_scaleFactor').html(SC.i18n("Scale_Factor")); | |
$('.label_modelingDomain').html(SC.i18n("Modeling_Domain")); | |
$('.label_scaleName').html(SC.i18n("Scale_Name")); | |
$('.label_scaleRatio').html(SC.i18n("Scale_Ratio")); | |
$('.legend_unitsToDisplay').html(SC.i18n("Units_to_Display")); | |
$('.label_prototypeUnits').html(SC.i18n("Prototype_Units")); | |
$('.label_modelUnits').html(SC.i18n("Model_Units")); | |
$('.legend_item').html(SC.i18n("Item")); | |
$('.label_description').html(SC.i18n("Description")); | |
$('.legend_listing').html(SC.i18n("Listing")); | |
$('.label_outputFormat').html(SC.i18n("Output_Format")); | |
}; | |
/** | |
* Returns the top level DOM element for this gadget. Required API for Dom utilities. | |
*/ | |
SC.fn.getElmt = function () { | |
return this.tab.getElmt(); | |
}; | |
/** | |
* Appends current calculated values to the calculator tape TextArea | |
*/ | |
SC.fn.recordButtonOnclick = function () { | |
var T = this, i, txt, omode, ct, dt, pad = " ", ntxt = "", hv, mlen, | |
R_fields = T.R_fields, S_fields = T.S_fields; | |
ct = T.calcTape; | |
txt = ct.getValue(); | |
omode = T.outModeSel.getSelectedValue(); | |
// emit CSV header as first line when output history is blank | |
if ((txt.length === 0) && (omode === "csv")) { | |
ntxt += "Description,Ratio,"; | |
for (i = 0; i < R_fields.length; i += 1) { | |
ntxt += "Proto_" + R_fields[i].units + ","; | |
} | |
for (i = 0; i < S_fields.length; i += 1) { | |
ntxt += "Scale_" + S_fields[i].units + ","; | |
} | |
ntxt += "\n"; | |
} | |
// Output the current values to the history textarea | |
if (omode === "text") { | |
ntxt += T.descripText.getValue() + " " + SC.i18n("SCALE_TAPE") + | |
T.ratioText.getValue() + ")\n"; | |
pad = " "; | |
ntxt += SC.i18n("REAL_TAPEHDR"); | |
for (i = 0; i < R_fields.length; i += 1) { | |
ntxt += R_fields[i].getValue() + " " + R_fields[i].units + pad; | |
} | |
ntxt += "\n"; | |
ntxt += SC.i18n("MODEL_TAPEHDR"); | |
for (i = 0; i < S_fields.length; i += 1) { | |
ntxt += S_fields[i].getValue() + " " + S_fields[i].units + pad; | |
} | |
ntxt += "\n"; | |
} else if (omode === "csv") { | |
// :TODO: number formatting on numeric fields to avoid locale probs with vals like 3,000 | |
// surround description with double quotes in case it contains commas | |
dt = "\"" + T.descripText.getValue() + "\""; | |
ntxt += dt + "," + T.ratioText.getValue() + ","; | |
for (i = 0; i < R_fields.length; i += 1) { | |
ntxt += R_fields[i].getValue() + ","; | |
} | |
for (i = 0; i < S_fields.length; i += 1) { | |
ntxt += S_fields[i].getValue() + ","; | |
} | |
ntxt += "\n"; | |
} | |
ct.setValue(txt + ntxt); | |
ct.scrollToBottom(); | |
// We limit the amount of history saved due to cookie size limit | |
hv = ct.getValue(); | |
mlen = SC.maxCalcHistoryLength; | |
SC.prefs.history = hv.length > mlen ? hv.substr(hv.length - mlen) : hv; | |
SC.savePrefs(); | |
}; | |
/** | |
* Handler to clear the history TextArea | |
*/ | |
SC.fn.clearButtonOnclick = function () { | |
this.calcTape.setValue(""); | |
SC.prefs.history = this.calcTape.getValue(); | |
SC.savePrefs(); | |
}; | |
/** | |
* Updates numeric and units display cells from the field arrays | |
*/ | |
SC.fn.updateDisplayCells = function () { | |
var T = this, i, j, st1 = T.subtab1, st2 = T.subtab2; | |
// Nuke existing contents of display subtabs and remove width style | |
for (i = 1; i < st1.getNumCols(); i += 1) { | |
st1.setCell(0, i, {content: "", width: 0}); | |
} | |
for (i = 1; i < st2.getNumCols(); i += 1) { | |
st2.setCell(0, i, {content: "", width: 0}); | |
} | |
// Poke in new contents | |
for (i = 0, j = 1; i < T.R_fields.length; i += 1, j += 2) { | |
st1.setCell(0, j, {content: T.R_fields[i], align: "center"}); | |
st1.setCell(0, j + 1, {content: T.R_fields[i].units, align: "left", sclass: "bold", width: "3em"}); | |
} | |
for (i = 0, j = 1; i < T.S_fields.length; i += 1, j += 2) { | |
st2.setCell(0, j, {content: T.S_fields[i], align: "center"}); | |
st2.setCell(0, j + 1, {content: T.S_fields[i].units, align: "left", sclass: "bold", width: "3em"}); | |
} | |
}; | |
/** | |
* Creates an InputText wrapper with some extra properties for scale conversion machinery: | |
* <ul> | |
* <li>Numeric value for the computed size. | |
* <li>Conversion factor that converts the field's numeric value to meters. | |
* <li>Flag specifying whether this is an on-model or real-world field. | |
* <li>A back-link to the parent object for use in event handlers. | |
* </ul> | |
* @param {number} siz Size of the InputText field in chars. | |
* @param {boolean} bReal True if this a "real-world" measurement, false for scale measurement. | |
* @param {number} toMeters Conversion factor that carries the field's value to meters. | |
* @param {number} units Units of measure string for the field. | |
* @param {number} val Initial value for the field. Can be omitted for all but one of the fields. | |
* @return {InputText} Returns an augmented InputText widget. | |
*/ | |
SC.fn.createScaleField = function (siz, bReal, toMeters, units, val) { | |
var fld = new InputText(null, "", siz); | |
fld.bReal = bReal; | |
fld.toMeters = toMeters; | |
fld.units = units; | |
fld.parent = this; | |
fld.val = (U.isnum(val) ? val : 0); | |
fld.setValue(fld.val.toPrecision(this.precision)); | |
return fld; | |
}; | |
/** | |
* Rebuilds the composite fields array by concatenating all the members of R_fields and S_fields. | |
* Also forces the first field -- which is always an R_field -- to have a nonzero value so | |
* that recomputeFields will work correctly during initial setup. | |
*/ | |
SC.fn.rebuildFieldsArr = function () { | |
var T = this, i = 0, j; | |
T.fields = []; | |
for (j = 0; j < T.R_fields.length; j += 1) { | |
T.fields[i++] = T.R_fields[j]; | |
} | |
for (j = 0; j < T.S_fields.length; j += 1) { | |
T.fields[i++] = T.S_fields[j]; | |
} | |
// Make sure the first field has a nonzero numeric value so recomputeFields will always work | |
if (T.fields && (T.fields.length > 0)) { | |
if ((typeof T.fields[0].val !== "number") || (T.fields[0].val === 0)) { | |
T.fields[0].val = 1.0; | |
T.fields[0].setValue(T.fields[0].val.toPrecision(T.precision)); | |
} | |
} | |
}; | |
/** | |
* Onchange handler for the prototype units selector. Rebuilds the R_fields array | |
* to contain just the current selection set. | |
*/ | |
SC.fn.protoUnitsSelOnchange = function () { | |
var T = this, i, ls, nref, vals = T.protoUnitsSel.getAllSelectedValues(); | |
// Rebuid R_fields array with just the selected ones | |
T.R_fields = []; | |
ls = ""; | |
for (i = 0; i < vals.length; i += 1) { | |
if (i !== 0) { | |
ls += ","; | |
} | |
ls += vals[i]; | |
nref = null; | |
switch (vals[i]) { | |
case "in": | |
nref = T.fld_R_in; | |
break; | |
case "ft": | |
nref = T.fld_R_ft; | |
break; | |
case "mm": | |
nref = T.fld_R_mm; | |
break; | |
case "cm": | |
nref = T.fld_R_cm; | |
break; | |
case "m": | |
nref = T.fld_R_m; | |
break; | |
} | |
if (nref) { | |
T.R_fields[i] = nref; | |
} | |
} | |
T.rebuildFieldsArr(); | |
T.updateDisplayCells(); | |
T.recomputeFields(T.fields[0]); | |
SC.prefs.protoUnitsList = ls; | |
SC.savePrefs(); | |
}; | |
/** | |
* Onchange handler for the scale units selector. Rebuilds the S_fields array | |
* to contain just the current selection set. | |
*/ | |
SC.fn.scaleUnitsSelOnchange = function () { | |
// get array of selected Option value strings | |
var T = this, i, nref, ls, vals = T.scaleUnitsSel.getAllSelectedValues(); | |
// Rebuid S_fields array with just the selected ones | |
T.S_fields = []; | |
ls = ""; | |
for (i = 0; i < vals.length; i += 1) { | |
if (i !== 0) { | |
ls += ","; | |
} | |
ls += vals[i]; | |
nref = null; | |
switch (vals[i]) { | |
case "in": | |
nref = T.fld_S_in; | |
break; | |
case "ft": | |
nref = T.fld_S_ft; | |
break; | |
case "mm": | |
nref = T.fld_S_mm; | |
break; | |
case "cm": | |
nref = T.fld_S_cm; | |
break; | |
case "m": | |
nref = T.fld_S_m; | |
break; | |
} | |
if (nref) { | |
T.S_fields[i] = nref; | |
} | |
} | |
T.rebuildFieldsArr(); | |
T.updateDisplayCells(); | |
T.recomputeFields(T.fields[0]); | |
SC.prefs.scaleUnitsList = ls; | |
SC.savePrefs(); | |
}; | |
/** | |
* Option key/value array for model railroad scales. | |
* @static | |
* @type array of anonymous objects | |
*/ | |
SC.opts_modelRR = [ | |
{'key': 'GScale_opt', 'val': '22.5'}, | |
{'key': 'Gauge1_opt', 'val': '32'}, | |
{'key': 'OScale_opt', 'val': '48'}, | |
{'key': 'SGauge_opt', 'val': '64'}, | |
{'key': 'HOScale_opt', 'val': '87'}, | |
{'key': 'TTScale_opt', 'val': '120'}, | |
{'key': 'NScale_opt', 'val': '160'}, | |
{'key': 'ZScale_opt', 'val': '220'} | |
]; | |
/** | |
* Option key/value array for common plastic model scales | |
* @static | |
* @type array of anonymous objects | |
*/ | |
SC.opts_PM_common = [ | |
{'key': '1/12', 'val': '12'}, | |
{'key': '1/24', 'val': '24'}, | |
{'key': '1/32', 'val': '32'}, | |
{'key': '1/35', 'val': '35'}, | |
{'key': '1/48', 'val': '48'}, | |
{'key': '1/72', 'val': '72'}, | |
{'key': '1/144', 'val': '144'}, | |
{'key': '1/350', 'val': '350'}, | |
{'key': '1/700', 'val': '700'} | |
]; | |
/** | |
* Option key/value array for dollhouse scales | |
* @static | |
* @type array of anonymous objects | |
*/ | |
SC.opts_dollhouse = [ | |
{'key': 'DollhouseScale_opt', 'val': '12'}, | |
{'key': 'HalfDollhouseScale_opt', 'val': '24'} | |
]; | |
/** | |
* Option key/value array for slot car scales | |
* @static | |
* @type array of anonymous objects | |
*/ | |
SC.opts_slotcar = [ | |
{'key': '1/24', 'val': '24'}, | |
{'key': '1/32', 'val': '32'}, | |
{'key': '1/43', 'val': '43'}, | |
{'key': '1/64', 'val': '64'}, | |
{'key': 'HOScale_opt', 'val': '87'} | |
]; | |
/** | |
* Option key/value array for unique slot car scales that don't appear in the PM or MRR scale lists | |
* @static | |
* @type array of anonymous objects | |
*/ | |
SC.opts_slotcar_unique = [ | |
{'key': '1/43', 'val': '43'}, | |
{'key': '1/64', 'val': '64'} // snaps to S Guage MRR scale | |
]; | |
/** | |
* Onchange handler for the scale set selector. Rebuilds the scSel Selector | |
* with scales from the newly selected set, and re-scans for the selected | |
* scale within the set based on the ratio stored in the prefs. | |
*/ | |
SC.fn.scaleSetSelOnchange = function () { | |
var T = this, scaleset = T.scaleSetSel.getSelectedValue(); | |
T.scSel.removeAll(); | |
// userdef Option must come first in all sets | |
T.scSel.addOption(new Option(SC.i18n("UserDefined_opt"), "")); | |
switch (scaleset) { | |
case "rr": | |
T.scSel.addOptions(SC.opts_modelRR, SC.TT); | |
break; | |
case "all": | |
// option groups for composite menu | |
T.scSel.addOptionGroup(new OptionGroup(SC.opts_modelRR, SC.TT, SC.i18n("Model_RR_opt"))); | |
T.scSel.addOptionGroup(new OptionGroup(SC.opts_PM_common, SC.TT, SC.i18n("Plastic_Models_opt"))); | |
T.scSel.addOptionGroup(new OptionGroup(SC.opts_slotcar, SC.TT, SC.i18n("Slot_Cars_opt"))); | |
T.scSel.addOptionGroup(new OptionGroup(SC.opts_dollhouse, SC.TT, SC.i18n("Dollhouse_opt"))); | |
break; | |
case "pm": | |
T.scSel.addOptions(SC.opts_PM_common, SC.TT); | |
break; | |
case "doll": | |
T.scSel.addOptions(SC.opts_dollhouse, SC.TT); | |
break; | |
case "slot": | |
T.scSel.addOptions(SC.opts_slotcar, SC.TT); | |
break; | |
} | |
// update ratio text and set selected scSel item from prefs scale factor | |
T.updateScaleFromPrefs(); | |
SC.prefs.scaleSet = scaleset; | |
SC.savePrefs(); | |
}; | |
/** | |
* Updates the scale selector and ratio text from the prefs ratio field. | |
*/ | |
SC.fn.updateScaleFromPrefs = function () { | |
var T = this, udscale, selIndx; | |
udscale = SC.prefs.userDefScale; | |
T.ratioText.setValue(udscale); | |
// Look up prefs scale against the scSel values and set the selector index | |
// If not found, change scSel to user-defined | |
selIndx = T.scSel.getOptionIndexByValue(udscale); | |
if (selIndx === null) { | |
T.scSel.setSelectedIndex(0); | |
} else { | |
T.scSel.setSelectedIndex(selIndx); | |
} | |
}; | |
/** | |
* Onchange handler for the scale ratio Selector. Recomputes all of the field | |
* values in the newly selected scale. We really only need to update the on-model | |
* fields but it's easier to just launch a full recompute. | |
*/ | |
SC.fn.scSelOnchange = function () { | |
var T = this, curIndx = T.scSel.getSelectedIndex(); | |
if (curIndx !== 0) { | |
T.ratioText.setValue(T.scSel.getSelectedValue()); | |
// base the recompute on any real-world field since changing the ratio only | |
// affects the on-model values | |
T.recomputeFields(T.fields[0]); | |
SC.prefs.userDefScale = T.ratioText.getValue(); | |
SC.savePrefs(); | |
} | |
// Disallow meaningless manual change of the selector to user-defined | |
// Re-scan for the item matching the ratio and put the selector back | |
T.updateScaleFromPrefs(); | |
}; | |
/** | |
* Onchange handler for all the InputText scale fields. Recomputes all the field | |
* values based on the new value of the changed field. | |
*/ | |
SC.fn.scFldChange = function () { | |
// 'this' must be the InputText wrapper | |
this.parent.recomputeFields(this); | |
}; | |
/** | |
* Onchange handler for the user-defined scale ratio InputText field. | |
* Changes the Selector value to "user-defined" and recomputes the scale field values. | |
*/ | |
SC.fn.ratioTextChange = function () { | |
var T = this, selIndx = T.scSel.getOptionIndexByValue(T.ratioText.getValue()); | |
T.scSel.setSelectedIndex(selIndx ? selIndx : 0); | |
T.recomputeFields(T.fields[0]); | |
SC.prefs.userDefScale = T.ratioText.getValue(); | |
SC.savePrefs(); | |
}; | |
/** | |
* Recalcs the numeric dimension fields. All of the fields are recomputed to | |
* match the (changed) value in <i>srcFld</i> via the conversion-to-meters factor | |
* stored in each field object and the scale ratio. | |
* @param srcFld Changed field used as the basis to compute the others. | |
*/ | |
SC.fn.recomputeFields = function (srcFld) { | |
var T = this, i, ratio, srcm, dstToMeters; | |
ratio = parseFloat(T.ratioText.getValue()); | |
// Convert text to numeric value in edited field | |
srcFld.val = parseFloat(srcFld.getValue()); | |
srcFld.setValue(srcFld.val.toPrecision(T.precision)); | |
srcm = srcFld.val * srcFld.toMeters; | |
for (i = 0; i < T.fields.length; i += 1) { | |
if (T.fields[i].id !== srcFld.id) { | |
dstToMeters = T.fields[i].toMeters; | |
// convert src value to this field per scale factors | |
if (srcFld.bReal && T.fields[i].bReal) { | |
// real-to-real | |
T.fields[i].val = srcm / dstToMeters; | |
} else if (!srcFld.bReal && !T.fields[i].bReal) { | |
// model-to-model | |
T.fields[i].val = srcm / dstToMeters; | |
} else if (srcFld.bReal) { | |
// real-to-model | |
T.fields[i].val = srcm / ratio / dstToMeters; | |
} else { | |
// model-to-real | |
T.fields[i].val = srcm * ratio / dstToMeters; | |
} | |
T.fields[i].setValue(T.fields[i].val.toPrecision(T.precision)); | |
} | |
} | |
}; | |
/** | |
* Onchange handler for history output mode selector. Saves the new value to the prefs. | |
*/ | |
SC.fn.outModeSelOnchange = function () { | |
SC.prefs.outputMode = this.outModeSel.getSelectedValue(); | |
SC.savePrefs(); | |
}; | |
/** | |
* Language table - English | |
* @static | |
*/ | |
SC.TT_en = { | |
'Scale_Model_Calculator': "Scale Model Dimension Calculator", | |
'Scale_Factor': "Scale Factor", | |
'Modeling_Domain': "Modeling Domain", | |
'Scale_Name': "Scale Name", | |
'Scale_Ratio': "Scale Ratio", | |
'Units_to_Display': "Units to Display", | |
'Prototype_Units': "Prototype Units", | |
'Model_Units': "Model Units", | |
'Item': "Item", | |
'Description': "Description", | |
'Listing': "Listing", | |
'Output_Format': "Output Format", | |
REAL: "PROTOTYPE", | |
MODEL: "SCALE", | |
REAL_TAPEHDR: " PROTOTYPE: ", | |
MODEL_TAPEHDR: " SCALE : ", | |
SCALE_TAPE: "(Scale 1:", | |
'Modeling_Domain_hover': "Select modeling domain to see typical scales in that domain", | |
'Scale_Name_hover': "Select a modeling scale by name", | |
PROTOTYPE_UNITS_hover: "Select one or more units for prototype dimensions", | |
SCALE_UNITS_hover: "Select one or more units for scale dimensions", | |
'Output_Format_hover': "Select format for text listing", | |
Record: "Record", | |
Record_hover: "Click to write current values to the history", | |
Clear: "Clear", | |
Clear_hover: "Click to clear the history", | |
Description_hover: "Enter description for calculator tape output", | |
RatioText_hover: "Enter custom scale factor as a number, e.g. 32, 48, 72.5, etc", | |
// scale domain sets | |
All_opt: "All Scales", | |
Model_RR_opt: "Model Railroad", | |
Plastic_Models_opt: "Plastic Model", | |
Slot_Cars_opt: "Slot Car", | |
Dollhouse_opt: "Dollhouse", | |
// named scales | |
UserDefined_opt: "User-defined", | |
GScale_opt: "G Scale", | |
Gauge1_opt: "1 Gauge", | |
OScale_opt: "O Scale", | |
SGauge_opt: "S Gauge", | |
HOScale_opt: "HO Scale", | |
TTScale_opt: "TT Scale", | |
NScale_opt: "N Scale", | |
ZScale_opt: "Z Scale", | |
DollhouseScale_opt: "Dollhouse Scale", | |
HalfDollhouseScale_opt: "Half Dollhouse Scale", | |
// Units lowercase abbrevs | |
UNITS_IN: "in", | |
UNITS_FT: "ft", | |
UNITS_M: "m", | |
UNITS_CM: "cm", | |
UNITS_MM: "mm", | |
// Units spelled out in lowercase | |
inches: "inches", | |
feet: "feet", | |
millimeters: "millimeters", | |
centimeters: "centimeters", | |
meters: "meters", | |
// Output modes | |
Text: "Text", | |
CSV: "CSV" | |
}; | |
/** | |
* Language table selector | |
* @static | |
*/ | |
SC.TT = SC.TT_en; | |
/** | |
* Instantiates a calculator widget and binds to document elements | |
* @static | |
*/ | |
SC.main = function () { | |
Utils.setupFirebugAPI(); | |
var scalc = new SC(); | |
}; | |
contentLoaded(window, SC.main); | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment