/**
* CALIBRATOR.JS
* Created. 2016
*
* Screen calibrator boilerplate for web based experiments.
*
* Authors. Albert Buchard, Amanda Yung and Augustin Joessel
*
* Requires: Underscore.js and jQuery
*
* LICENSE MIT
*/
/* =============== Set-up =============== */
/* === Get the absolute path of the library === */
var scripts = document.getElementsByTagName("script");
var calibratorFullpath = scripts[scripts.length - 1].src;
var delimiterIndices = findAllIndices("/", calibratorFullpath);
calibratorFullpath = calibratorFullpath.substr(0, delimiterIndices[delimiterIndices.length - 2]);
/* === Add the calibrator css once the page is loaded === */
document.addEventListener("DOMContentLoaded", function(event) {
var head = document.getElementsByTagName('head')[0];
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = calibratorFullpath + '/css/calibrator.css';
head.appendChild(link);
});
/* =============== Calibrator Class =============== */
/** A self contained object for easy web based screen calibration */
class Calibrator {
/**
* Setup of the calibrator object
* @param {function} callbackWhenClosed function to call when the calibrator is dismissed.
* An object containing relevant calibration information is passed as argument.
* @param {Boolean} showWhenReady If true, the calibrator is displayed after templates are loaded.
* @return {Calibrator}
*/
constructor(callbackWhenClosed = null, showWhenReady = true) {
/**
* Check if the full path is known
*/
if (typeof calibratorFullpath === "undefined") {
throw new Error("Calibrator.js: library path is unknown.");
} else {
this.calibratorFullpath = calibratorFullpath;
}
/**
* Check if underscore.js is loaded
*/
if (typeof _ !== "function") {
throw new Error("Calibrator.js: Underscore.js is needed for templating.");
}
/**
* Object containing the file path of all the views
* @type {Object}
* @const
* @private
*/
this.VIEWS_PATHS = {
container: this.calibratorFullpath + "/views/calibrator-container.template",
knownsize: this.calibratorFullpath + "/views/calibrator-s1-knownsize.template",
enterknownsize: this.calibratorFullpath + "/views/calibrator-s1-enterknownsize.template",
chooseobject: this.calibratorFullpath + "/views/calibrator-s1-chooseobject.template",
specifystandardsize: this.calibratorFullpath + "/views/calibrator-s1-specifystandardsize.template",
setbrightness: this.calibratorFullpath + "/views/calibrator-s2-content.template",
summary: this.calibratorFullpath + "/views/calibrator-s3-content.template"
};
var thisObject = this;
this.templateManager = new TemplateManager(this.VIEWS_PATHS, function() {
thisObject.templatesAreLoaded();
});
/**
* Image keys
* @const
* @private
*/
this.IMAGE_KEY_CREDITCARD = "creditCard";
this.IMAGE_KEY_CD = "cd";
/**
* Images object with [path, [width, height], real width in cm, maxScaling]
* @type {Object}
* @const
* @private
*/
this.IMAGES = {
[this.IMAGE_KEY_CREDITCARD]: [this.calibratorFullpath + "/img/card.png", [384, 242], 8.6, 1.2],
[this.IMAGE_KEY_CD]: [this.calibratorFullpath + "/img/cd.png", [596, 596], 12, 1.2]
}
/**
* Distance of the subject from the screen in cm, default to 50 cm (arm length)
* @type {Number}
* @const
* @public
*/
this.distanceFromScreen = 50;
/**
* Heigth of the canvas in pixels
* @type {Number}
* @private
*/
this.canvasHeight = 400;
/**
* Number of different gray shades for the brightness calibration
* @type {Number}
* @const
* @private
*/
this.BRIGHTNESS_NUMBER_OF_CONTRASTS = 12;
/**
* Object storing the cached image to draw on the canvas
* @type {Object}
* @private
*/
this.cachedImages = {};
/** Preload images */
this.preloadImages();
/**
* Steps constants
* @const
* @private
*/
this.STEP_TITLES = ["Step 1: Screen size calibration", "Step 2: Contrast and brightness", "Step 3: Summary"];
this.STEP_SCREENSIZE_ASK_IFKNOWS = 0;
this.STEP_SCREENSIZE_ENTER_KNOWNSIZE = 1;
this.STEP_SCREENSIZE_CHOOSE_OBJECT = 2;
this.STEP_SCREENSIZE_ENTER_OBJECTSIZE = 3;
this.STEP_BRIGHTNESS = 4;
this.STEP_SUMMARY = 5;
/**
* Buttons value attributes
* @const
* @private
*/
this.BUTTON_SIZEKNOWN = "s1:sizeKnown";
this.BUTTON_SIZEUNKNOWN = "s1:sizeUnknown";
this.BUTTON_CONFIRM_MANUALSIZE = "s1:confirmManualSize";
this.BUTTON_CHOOSE_CREDITCARD = "s1:chooseCreditCard";
this.BUTTON_CHOOSE_COMPACTDISK = "s1:chooseCompactDisk";
this.BUTTON_CONFIRM_OBJECTSIZE = "s1:confirmObjectSize";
this.BUTTON_CONFIRM_BRIGHTNESS = "s2:confirmBrightness";
this.BUTTON_FINAL_CONFIRM = "s3:finalConfirm";
this.BUTTON_BACK = "back";
this.currentStep = this.STEP_SCREENSIZE_ASK_IFKNOWS;
/**
* Desired precision of the text output
* @type {Number}
* @const
* @private
*/
this.FLOAT_PRECISION = 2;
/**
* Private variables
*/
/**
* Private variable holding screen real diagonal size in inches
* @type {Number}
* @private
*/
this._diagonalSize = null;
/**
* Private variable holding the current image key
* @type {string}
* @private
*/
this._currentImage = null;
/**
* Private variable holding the image scale ratio between 0 and 1.
* @type {Number}
* @private
*/
this._imageRatio = 0.5;
/**
* Determines if the calibrator automatically shows after loading of templates.
* @type {boolean}
* @private
*/
this._showWhenReady = (showWhenReady == true) ? true : false;
/** Setup callback */
/**
* Function called after the user closes the calibrator. Argument sent to the callback is an object with keys
* * status
* + 0 the calibrator did not finish normally
* + 1 calibrator finish normally
* * diagonalSize
* + diagonal size in inches
* * diagonalSizeInPx
* + diagonal size in inches
* * distanceFromScreenInCm
* + distance from the screen in cm (calibrator.distanceFromScreen)
* * pixelsPerInch
* + computed pixel density in pixels per inch
* * pixelsPerDegree
* + computed pixels per degree
*
* @type {function}
* @public
*/
this.callbackWhenClosed = null;
if (!callbackWhenClosed) {
console.log("Calibrator.js: no callback is set-up for the calibrator to call when finished!");
} else {
this.callbackWhenClosed = callbackWhenClosed;
}
/** Handle Resize */
$(window).resize(function() {
this.canvasResized();
});
}
/**
* Preloads images from path defined in this.IMAGES
* @return {undefined}
* @private
*/
preloadImages() {
for (var key in this.IMAGES) {
this.cachedImages[key] = new Image();
this.cachedImages[key].src = this.IMAGES[key][0];
//this.cachedImages[key].onload = function() {
// if (++loadedImages >= numImages) {
// callback(images);
// }
// };
}
}
/**
* Called after all templates are loaded and compiled.
* @return {undefined}
* @private
*/
templatesAreLoaded() {
console.log("Calibrator.js : All templates are loaded");
/** Add calibrator div to DOM */
this.addToDom();
/**
* Container element reference
* @type {object}
*/
this.container = $(".calibrator-container");
/** Toggle display depending on preset showWhenReady */
$(this.container).toggle(this._showWhenReady);
/** Setup events */
this.resetEvents();
}
/**
* Adds the calibrator container template to the DOM
* @private
*/
addToDom() {
this.templateManager.renderInTarget("container", {
title: this.currentTitle,
content: this.currentContent
}, "body");
}
/* ======== Appearence Methods ======== */
/**
* Shows the calibrator.
* @return {undefined}
* @public
*/
show() {
$(this.container).fadeIn(200);
}
/**
* Hides the calibrator.
* @return {undefined}
* @public
*/
hide() {
$(this.container).fadeOut(200);
}
/**
* Toggle the display of the calibrator.
* @return {undefined}
* @public
*/
toggle() {
$(this.container).toggle(200);
}
/**
* Toggle display of the information div.
* @return {undefined}
* @public
*/
toggleInfo() {
$(".calibrator-info-content").toggle(200);
}
/* ======== View Update Methods ======== */
/**
* Updates the view with the appropriate title and content for the current step.
* @return {undefined}
* @private
*/
updateView() {
/** keep reference to current object for the callbacks */
var thisObject = this;
/** Update top guide */
this.updateGuide();
/**
* Animate title switch
*/
$(".calibrator-title").animate({
opacity: 0
}, 300, function() {
$(".calibrator-title").html("<h3>" + thisObject.currentTitle + "</h3>");
$(".calibrator-title").animate({
opacity: 300
}, 100);
});
/**
* Setup animation of content switch
*/
$(".calibrator-content").animate({
opacity: 0
}, 300, function() {
/** Load content */
$(".calibrator-content").html(thisObject.currentContent);
/** Perform logic associated with this step */
thisObject.setStepLogic();
/** Add back button if necessary */
if (thisObject.currentStep != thisObject.STEP_SCREENSIZE_ASK_IFKNOWS) {
thisObject.addBackButton();
}
/** Show content div */
$(".calibrator-content").animate({
opacity: 300
}, 100, function() {
/** Reset events after animation is done and DOM is ready */
thisObject.resetEvents();
});
});
}
/**
* Function that update the classes of the top guide to show active step.
* @return {undefined}
* @private
*/
updateGuide() {
if ($(".calibrator-guide").length) {
_.each($(".calibrator-guide div"), function(element) {
$(element).removeClass("calibrator-guide-active");
});
switch (this.currentStep) {
case this.STEP_SCREENSIZE_ASK_IFKNOWS:
case this.STEP_SCREENSIZE_ENTER_KNOWNSIZE:
case this.STEP_SCREENSIZE_CHOOSE_OBJECT:
case this.STEP_SCREENSIZE_ENTER_OBJECTSIZE:
$("#calibrator-guide-step1").addClass("calibrator-guide-active");
break;
case this.STEP_BRIGHTNESS:
$("#calibrator-guide-step2").addClass("calibrator-guide-active");
break;
case this.STEP_SUMMARY:
$("#calibrator-guide-step3").addClass("calibrator-guide-active");
break;
}
} else {
console.log("Calibrator.js: calibrator-guide div not in the dom.");
}
}
/* ======== Event Handling Methods ======== */
/**
* Resets event listeners after DOM change
* @return {undefined}
* @private
*/
resetEvents() {
/** Remove current handlers */
$(".calibrator-info-icon").off();
$(".calibrator-dismiss-icon").off();
$(".calibrator-button").off();
$(".calibrator-size-range").off();
/**
* Hold the reference to the calibrator object for callbacks
* @type {Object}
*/
var thisObject = this;
$(".calibrator-info-icon").on("click", function(e) {
thisObject.toggleInfo();
});
$(".calibrator-dismiss-icon").on("click", function(e) {
thisObject.callbackNow(0);
thisObject.hide();
});
$(".calibrator-button").on("click", function(e) {
thisObject.buttonClicked(e);
});
$(".calibrator-size-range").on("change", function(e) {
thisObject.setRatioFromRange($(e.target));
thisObject.drawImage();
thisObject.updateSummaryInformation();
});
}
/* ======== Step Management Methods ======== */
/**
* Go to the specified step.
* @param {Number} step step index as defined by calibrate.STEP_XXX
* @return {undefined}
* @private
*/
goToStep(step) {
this.currentStep = step;
this.updateView();
}
/**
* After step content has been drawn, this function performs step specific logic.
* @private
*/
setStepLogic() {
switch (this.currentStep) {
case this.STEP_SCREENSIZE_ENTER_KNOWNSIZE:
/**
* If diagonalSize is valid - set the input to its value, else set _diagonalSize to null
*/
if ($.isNumeric(this.diagonalSize)) {
$("#calibrator-monitor-size")[0].value = this.diagonalSize.toFixed(this.FLOAT_PRECISION);
} else {
this.diagonalSize = null;
}
break;
case this.STEP_SCREENSIZE_CHOOSE_OBJECT:
// no logic
break;
case this.STEP_SCREENSIZE_ENTER_OBJECTSIZE:
this.setRangeFromRatio();
this.setDiagonalSizeFromRatio();
this.drawImage();
this.updateSummaryInformation();
break;
case this.STEP_BRIGHTNESS:
this.drawGrayScale();
console.log(this.pixelsPerDegree);
break;
case this.STEP_SUMMARY:
break;
}
}
/**
* Goes to previous step.
* @return {undefined}
* @private
*/
goToPreviousStep() {
switch (this.currentStep) {
case this.STEP_SCREENSIZE_ENTER_KNOWNSIZE:
this.goToStep(this.STEP_SCREENSIZE_ASK_IFKNOWS);
break;
case this.STEP_SCREENSIZE_CHOOSE_OBJECT:
this.goToStep(this.STEP_SCREENSIZE_ASK_IFKNOWS);
break;
case this.STEP_SCREENSIZE_ENTER_OBJECTSIZE:
this.goToStep(this.STEP_SCREENSIZE_CHOOSE_OBJECT);
break;
case this.STEP_BRIGHTNESS:
this.goToStep(this.STEP_SCREENSIZE_ASK_IFKNOWS);
break;
case this.STEP_SUMMARY:
this.goToStep(this.STEP_BRIGHTNESS);
break;
}
}
/**
* Add a back button to the content div.
* @private
*/
addBackButton() {
/** look for a .calibrator-backdiv placeholder in the document */
if ($(".calibrator-backdiv").length) {
var backButtonHtml = '<button class="btn calibrator-button calibrator-button-back" value="back">' +
'Back' +
'</button>';
$(".calibrator-backdiv").append(backButtonHtml);
} else {
var backButtonHtml = '<div class="col-xs-12 calibrator-spacing">' +
'</div>' +
'<div class="col-xs-12 ">' +
'<button class="btn calibrator-button calibrator-button-back" value="back">' +
'Back' +
'</button>' +
'</div>';
$(".calibrator-content").append(backButtonHtml);
}
}
/**
* handles all events related to the calibrator buttons being clicked
* @param {object} event event from the callback
* @private
*/
buttonClicked(event) {
var buttonValue = event.target.value;
switch (buttonValue) {
case this.BUTTON_SIZEKNOWN:
this.goToStep(this.STEP_SCREENSIZE_ENTER_KNOWNSIZE);
break;
case this.BUTTON_SIZEUNKNOWN:
this.goToStep(this.STEP_SCREENSIZE_CHOOSE_OBJECT);
break;
case this.BUTTON_CONFIRM_MANUALSIZE:
if ($.isNumeric($("#calibrator-monitor-size")[0].value)) {
this.diagonalSize = Number($("#calibrator-monitor-size")[0].value);
this.goToStep(this.STEP_BRIGHTNESS);
} else {
console.log("Calibrator.js: monitor size is invalid");
}
break;
case this.BUTTON_CHOOSE_CREDITCARD:
this._currentImage = this.IMAGE_KEY_CREDITCARD;
this.updateCanvasHeight();
this.goToStep(this.STEP_SCREENSIZE_ENTER_OBJECTSIZE);
break;
case this.BUTTON_CHOOSE_COMPACTDISK:
this._currentImage = this.IMAGE_KEY_CD;
this.updateCanvasHeight();
this.goToStep(this.STEP_SCREENSIZE_ENTER_OBJECTSIZE);
break;
case this.BUTTON_CONFIRM_OBJECTSIZE:
this.goToStep(this.STEP_BRIGHTNESS);
break;
case this.BUTTON_CONFIRM_BRIGHTNESS:
this.goToStep(this.STEP_SUMMARY);
break;
case this.BUTTON_FINAL_CONFIRM:
this.callbackNow(1);
this.hide();
break;
case this.BUTTON_BACK:
this.goToPreviousStep();
break;
}
}
/* ======== Summarize Informations ======== */
/**
* Function that looks for place holders in the current page for summary information and updates it using calibrator.FLOAT_PRECISION.
* @return {undefined}
* @private
*/
updateSummaryInformation() {
var thisObject = this;
if ($(".calibrator-diagonal-size-inches").length) {
_.each($(".calibrator-diagonal-size-inches"), function(element) {
$(element).html(thisObject.diagonalSize.toFixed(thisObject.FLOAT_PRECISION) + " inches");
})
}
}
/* ======== Callback Methods ======== */
/**
* Function called when calibrator is dismissed. Call the callbackWhenClosed function if it was provided, else just print the result of the calibrator in the console.
* @param {Number} status 0 for an early exit. 1 for a normal exit.
*/
callbackNow(status) {
var returnObject = {
status: 0,
diagonalSize: null,
diagonalSizeInPx: this.diagonalSizeInPx,
distanceFromScreenInCm: null,
pixelsPerInch: null,
pixelsPerDegree: null
}
if (this.diagonalSize) {
returnObject.status = status;
returnObject.diagonalSize = this.diagonalSize;
returnObject.distanceFromScreenInCm = this.distanceFromScreen;
returnObject.pixelsPerInch = this.pixelsPerInch;
returnObject.pixelsPerDegree = this.pixelsPerDegree;
}
if (this.callbackWhenClosed) {
this.callbackWhenClosed(returnObject);
} else {
console.log("Calibrator.js: Has been dismisse ")
console.log(returnObject);
}
}
/* ======== Object Methods ======== */
/**
* Update canvas height as a function of the selected image.
* @return {undefined}
* @private
*/
updateCanvasHeight() {
if (this.currentImage) {
var imageMaxHeight = this.IMAGES[this.currentImage][1][1] * this.IMAGES[this.currentImage][3];
this.canvasHeight = imageMaxHeight + 50;
}
}
/**
* Resizes the canvas to avoid unwanted scaling. Defines the canvas height as this.canvasHeight
* @return {undefined}
* @private
*/
fitCanvasToContainer() {
if ($(".calibrator-canvas").length) {
var canvas = $(".calibrator-canvas")[0];
/* Make it visually fill the positioned parent */
canvas.style.width = '100%';
canvas.style.height = this.canvasHeight + 'px';
/* then set the internal size to match */
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
}
}
/**
* Function handling canvas resize and redraw
*/
canvasResized() {
this.fitCanvasToContainer();
switch (this.currentStep) {
case this.STEP_SCREENSIZE_ENTER_OBJECTSIZE:
this.setDiagonalSizeFromRatio();
this.drawImage();
break;
case this.STEP_BRIGHTNESS:
this.drawGrayScale();
break;
}
}
drawImage() {
if (($(".calibrator-canvas").length) && (this.currentStep == this.STEP_SCREENSIZE_ENTER_OBJECTSIZE)) {
this.fitCanvasToContainer();
var canvas = $(".calibrator-canvas")[0];
var canvasContext = canvas.getContext("2d");
// var centerX = canvas.width / 2;
// var centerY = canvas.height / 2;
/** Clear for redraw */
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
/** Center the image */
var drawAtX = (canvas.width - this.currentImageScaledWidthInPx) / 2;
var drawAtY = (canvas.height - this.currentImageScaledHeightInPx) / 2;
canvasContext.drawImage(this.cachedImages[this._currentImage],
drawAtX, drawAtY, this.currentImageScaledWidthInPx, this.currentImageScaledHeightInPx);
}
}
setRatioFromRange(element = null) {
if (element == null) {
element = $(".calibrator-size-range")
}
if ($(element).length) {
var range = Number($(element).attr("max")) - Number($(element).attr("min"));
var value = Number($(element).val());
this.imageRatio = value / range;
}
}
setRangeFromRatio(element = null) {
if (element == null) {
element = $(".calibrator-size-range")
}
if ($(element).length) {
var range = Number($(element).attr("max")) - Number($(element).attr("min"));
$(element).val((this.imageRatio * range) + Number($(element).attr("min")));
}
}
/**
* Set the diagonal size in inches from the pixel per inches calculate fron the scaled currentImage drawn on the canvas.
* @private
*/
setDiagonalSizeFromRatio() {
if (this.currentImage) {
this.diagonalSize = this.diagonalSizeInPx / (this.currentImageScaledWidthInPx / this.currentImagePhysicalWidthInInches);
}
}
/* ======== Brightness ======== */
drawGrayScale() {
if (($(".calibrator-canvas").length) && (this.currentStep == this.STEP_BRIGHTNESS)) {
this.fitCanvasToContainer();
var canvas = $(".calibrator-canvas")[0];
var canvasContext = canvas.getContext("2d");
/** Clear for redraw */
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
// canvasContext.fillStyle = "white";
// canvasContext.fillRect(0, 0, canvas.width, canvas.height);
// var boxWidth = Math.round(1 * this.pixelsPerCm); //cm
// var boxHeight = Math.round(4 * this.pixelsPerCm); //cm
var boxWidth = 30;
var boxHeight = 150;
var centerX = Math.round(canvas.width / 2);
var centerY = Math.round(canvas.height / 2);
var topX = centerX - (boxWidth * this.BRIGHTNESS_NUMBER_OF_CONTRASTS / 2);
var topY = centerY - (boxHeight / 2);
for (var i = 0; i < this.BRIGHTNESS_NUMBER_OF_CONTRASTS; i++) {
var luminosity = Math.round(i * (255 / (this.BRIGHTNESS_NUMBER_OF_CONTRASTS - 1)));
canvasContext.fillStyle = "rgb(" + luminosity + "," + luminosity + "," + luminosity + ")";
canvasContext.fillRect((topX + i * boxWidth), topY, boxWidth, boxHeight);
}
} else {
throw new Error("Calibrator.js: the canvas element is not present, cannot drawImage().")
}
// pxpercm = Math.round(window.pxperinch / 2.54);
// //calculate pixels per degree
// var angle = Math.atan(screen.height / screen.width);
// var diagCM = (getSliderValue() / 10) * 2.54;
// var screenWidthCM = diagCM * Math.cos(angle);
// window.pxperdeg = Math.PI / 180 * screen.width * distance / screenWidthCM;
// window.monitorSize = parseFloat($("#screenInput").val());
}
/* =============== Getters and Setters =============== */
/* ======== Current Step Content ======== */
/**
* Returns the currentStep Title from calibrator.STEP_TITLES
* @return {String} Title
* @private
*/
get currentTitle() {
switch (this.currentStep) {
case this.STEP_SCREENSIZE_ASK_IFKNOWS:
return (this.STEP_TITLES[0]);
break;
case this.STEP_SCREENSIZE_ENTER_KNOWNSIZE:
return (this.STEP_TITLES[0]);
break;
case this.STEP_SCREENSIZE_CHOOSE_OBJECT:
return (this.STEP_TITLES[0]);
break;
case this.STEP_SCREENSIZE_ENTER_OBJECTSIZE:
return (this.STEP_TITLES[0]);
break;
case this.STEP_BRIGHTNESS:
return (this.STEP_TITLES[1]);
break;
case this.STEP_SUMMARY:
return (this.STEP_TITLES[2]);
break;
}
}
/**
* Returns the compiled currentStep content from the templateManager
* @return {String} HTML Content
* @private
*/
get currentContent() {
switch (this.currentStep) {
case this.STEP_SCREENSIZE_ASK_IFKNOWS:
return (this.templateManager.render("knownsize"));
break;
case this.STEP_SCREENSIZE_ENTER_KNOWNSIZE:
return (this.templateManager.render("enterknownsize"));
break;
case this.STEP_SCREENSIZE_CHOOSE_OBJECT:
return (this.templateManager.render("chooseobject"));
break;
case this.STEP_SCREENSIZE_ENTER_OBJECTSIZE:
return (this.templateManager.render("specifystandardsize"));
break;
case this.STEP_BRIGHTNESS:
return (this.templateManager.render("setbrightness"));
break;
case this.STEP_SUMMARY:
return (this.templateManager.render("summary", {
diagonalSize: this.diagonalSize.toFixed(this.FLOAT_PRECISION),
diagonalSizeInPx: Math.ceil(this.diagonalSizeInPx),
pixelsPerDegree: this.pixelsPerDegree.toFixed(this.FLOAT_PRECISION),
pixelsPerInch: this.pixelsPerInch.toFixed(this.FLOAT_PRECISION)
}));
break;
}
}
/* ======= Size processing ======= */
/* === Screen size === */
/**
* Sets physical screen diagonal in inches. Key variable of the object.
* @return {undefined}
* @private
*/
set diagonalSize(value) {
if (value == null) {
this._diagonalSize = null;
return;
}
value = Number(value);
if ((value > 0) && (value < 60)) {
this._diagonalSize = value;
} else {
console.log("Calibrator.js: Invalid diagonal size");
}
}
/**
* Screen physical diagonal size in inches
* @return {Number} Number of inches on the screen's diagonal
*/
get diagonalSize() {
if (this._diagonalSize) {
return (this._diagonalSize);
} else {
//console.log("Calibrator.js: diagonalSize is not set.");
return (null);
}
}
/**
* Physical diagonal size of the screen in cm.
* @return {Number} Number of cm on the screen's diagonal
*/
get diagonalSizeInCm() {
if (this.diagonalSize) {
return (this.diagonalSize * 2.54);
} else {
return (null);
}
}
/**
* Returns Diagonal of the screen in pixels. Depends on the resolution of the screen. We are always able to compute it.
* @return {Number} Number of pixels on the screen diagonal
*/
get diagonalSizeInPx() {
return (Math.sqrt(Math.pow(screen.availWidth, 2) + Math.pow(screen.availHeight, 2)));
}
/**
* Return the screen pixel per inches
* @return {Number} Pixel per inches
*/
get pixelsPerInch() {
if (this.diagonalSize) {
return (this.diagonalSizeInPx / this.diagonalSize);
} else {
return (null);
}
}
/**
* Return the screen pixel per cm
* @return {Number} Pixel per cm
*/
get pixelsPerCm() {
if (this.diagonalSize) {
return (this.pixelsPerInch / 2.54);
} else {
return (null);
}
}
/**
* Returns the pixels per degree as a function of pixel density of the screen and subject's distance from the screen
* @return {Number} Pixel per degree
*/
get pixelsPerDegree() {
if (this.diagonalSize) {
var visualAngleInRadian = 2 * Math.atan((screen.availWidth / this.pixelsPerCm) / (2 * this.distanceFromScreen));
var degreePerRadian = (180 / Math.PI);
return (screen.availWidth / (degreePerRadian * visualAngleInRadian));
} else {
return (null);
}
}
/* === Image size === */
/**
* Sets the image scale ratio
* @param {Number} ratio Real number between 0 and 1
* @private
*/
set imageRatio(ratio) {
if ((ratio >= 0) && (ratio <= 1)) {
this._imageRatio = ratio;
this.setDiagonalSizeFromRatio();
this.drawImage();
}
}
/**
* Current image scale ratio
* @return {Number} Ratio between 0 and 1 (this ratio will then be multiplied by the maximum scaling factor for each image to produce the observed size)
*/
get imageRatio() {
return (this._imageRatio);
}
/**
* Set the current selected image to the specified key and redraw.
* @param {string} imageKey image key as stored in calibrator.IMAGES
* @private
*/
set currentImage(imageKey) {
if (_.contains(this.IMAGES, imageKey)) {
this._currentImage = imageKey;
this.updateCanvasHeight();
this.drawImage();
}
}
/**
* Get the current image key
* @return {string} image key as stored in calibrator.IMAGES
*/
get currentImage() {
if (this._currentImage) {
return (this._currentImage);
} else {
console.log("Calibrator.js: currentImage is not set.")
return (null);
}
}
/**
* Get physical width of the object represented by the current image.
* @return {Number} Size in cm
* @private
*/
get currentImagePhysicalWidthInCm() {
if (this.currentImage) {
return (this.IMAGES[this.currentImage][2]);
} else {
return (null);
}
}
/**
* Get physical width of the object represented by the current image.
* @return {Number} Size in inches
* @private
*/
get currentImagePhysicalWidthInInches() {
if (this.currentImage) {
return (this.IMAGES[this.currentImage][2] / 2.54);
} else {
return (null);
}
}
/**
* Returns the scaled height depending on the selected scale factor (imageRatio) and maximum scaling of the image.
* @return {Number} Scaled height in pixel
* @private
*/
get currentImageScaledHeightInPx() {
if (this.currentImage) {
/**
* The scaled pixel size is the base pixel size * maximum scaling factor * ratio as determined by the slider/range position (imageRatio)
*/
return (this.IMAGES[this.currentImage][1][1] * this.IMAGES[this.currentImage][3] * this.imageRatio);
} else {
return (null);
}
}
/**
* Returns the scaled width depending on the selected scale factor (imageRatio) and maximum scaling of the image.
* @return {Number} Scaled width in pixel
*/
get currentImageScaledWidthInPx() {
if (this.currentImage) {
/**
* The scaled pixel size is the base pixel size * maximum scaling factor * ratio as determined by the slider/range position (imageRatio)
*/
return (this.IMAGES[this.currentImage][1][0] * this.IMAGES[this.currentImage][3] * this.imageRatio);
} else {
return (null);
}
}
}
/* =============== TemplateManager Class =============== */
/**
* Class to manage the loading of templates from external files using underscore.js simple templating capabilities and JQuery.
*/
class TemplateManager {
/**
* Constructor function for the templateManager
* @param {object} viewPaths list of template URLs. Object keys will be used as the template name.
* {templateName1: templateUrl1, templateName2: templateUrl2, ...}
* @param {function} callbackWhenLoaded Callback function to call when templates are loaded.
* @public
*/
constructor(viewPaths = mandatory(), callbackWhenLoaded = null) {
/* Allow double curly bracket syntax in the template html: {{variable}} */
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
};
/**
* Contains all templates urls
* @type {object}
*/
this.viewPaths = viewPaths;
/**
* Contains cached template in underscore template format
* @type {Object}
*/
this.cached = {};
/** setup callback when all templates are loaded */
if (callbackWhenLoaded) {
this.callbackWhenLoaded = callbackWhenLoaded;
} else {
this.callbackWhenLoaded = function() {
console.log("TemplateManager.js: all templates loaded.");
}
}
/* Keeps reference to the current object */
var thisObject = this;
/* Caches every templates asynchronously */
_.each(this.viewPaths, function(value, key, list) {
$.get(thisObject.viewPaths[key], function(raw) {
/** store after loading */
thisObject.store(key, raw);
/** checks if all template are loaded */
if (_.every(_.allKeys(thisObject.viewPaths), function(key) {
return (_.contains(_.allKeys(thisObject.cached), key));
})) {
/** All templates loaded, call the supplied callback. */
thisObject.callbackWhenLoaded();
}
});
});
}
/**
* Render the HTML of a template based on its name.
* @param {string} name template name
* @param {Object} variables Object holding the variable values to replace in the template before rendering.
*/
render(name, variables = {}) {
var thisObject = this;
if (this.isCached(name)) {
return (this.cached[name](variables));
} else {
$.get(this.urlFor(name), function(raw) {
thisObject.store(name, raw);
thisObject.render(name, variables);
});
}
}
/**
* Render the HTML of a template based on its name into a DOM target.
* @param {string} name template name
* @param {Object} variables Object holding the variable values to replace in the template before rendering.
* @param {Object} target DOM element to render the HTML into
*/
renderInTarget(name, variables, target) {
var thisObject = this;
if (this.isCached(name)) {
$(target).append(this.cached[name](variables));
} else {
$.get(this.urlFor(name), function(raw) {
thisObject.store(name, raw);
thisObject.renderInTarget(name, variables, target);
});
}
}
/**
* Synchronous fetching and rendering using ajax synchronous file fetching.
* @param {string} name template name
*/
renderSync(name) {
if (!this.isCached(name)) {
this.fetch(name);
}
this.render(name);
}
/**
* Preloads and cache the template as underscore templates.
* @param {string} name template name
*/
prefetch(name) {
var thisObject = this;
$.get(this.urlFor(name), function(raw) {
thisObject.store(name, raw);
});
}
/**
* Synchronously fetch a template.
* @param {string} name template name
*/
fetch(name) {
// synchronous, for those times when you need it.
if (!this.isCached(name)) {
var raw = $.ajax({
'url': this.urlFor(name),
'async': false
}).responseText;
this.store(name, raw);
}
}
/**
* Checks if a specified template is already cached
* @param {string} name template name
* @return {Boolean}
*/
isCached(name) {
return !!this.cached[name];
}
/**
* Stores a template from raw html as a underscore template.
* @param {string} name template name
* @param {string} raw template html
*/
store(name, raw) {
this.cached[name] = _.template(raw);
}
/**
* Return the path of the specified template
* @param {string} name template name
* @return {string} template url
*/
urlFor(name) {
return (this.viewPaths[name]);
}
}
/* =============== Utility Functions =============== */
/**
* Called when mandatory argument is not set
* @param {String} param Optional name of the missing argument
*/
function mandatory(param = "") {
throw new Error('Missing parameter ' + param);
}
/**
* Find all the positions of a needle in a haystack string
* @param {string} needle string to find
* @param {string} haystack string to scan
* @return {Array} Either -1 if no match is found or an array containing the indicies
*/
function findAllIndices(needle = mandatory(), haystack = mandatory()) {
var indices = [];
for (i = 0; i < haystack.length; i++) {
if ((haystack.substr(i, needle.length)) === needle) {
indices.push(i);
}
}
if (indices.length) {
return (indices);
} else {
return (-1)
}
}