Source: jsoclo/spectroscopy.js

/**
 * @class
 * 
 * @classdesc
 * <p>
 * specController() is called via jQueries document.ready function which appears in spectroscopy.html. It is called each time the
 * page is loaded or the browser is refreshed.  <p>specController is the javascript class responsible for running the 
 * interface for the spectroscopy JSOCLO. 
 * 
 * <p>
 * Category is not used in Spectroscopy so we must set the category cookie to the empty string when we initialize.
 * 
 * @see {@link searcher}
 * @tutorial ArchitectureOverview
 */
function specController() {
    /**
     * load the other javascripts and the CSS
     */    
    loadJS("../js/jquery/jquery.cookie.js");
    loadJS("../js/jsoclo/chemUtils.js"); 
    loadJS("../js/jsoclo/analyzerBase.js");
    loadJS("../js/jsoclo/spectroAnalyzer.js");
    loadJS("../js/jsparser/smidge.js");
    
    loadCSS("../css/spectroscopy.css");
  
    /** the number of attempts made on this quesiton */
   var  attempts = 0;
    /**
     * used to give unique div ID's to jsme instances in the response div. Used by createJsmeDiv()
     */
    var jsmeDivCount = 2;
    /**
     * The number of questions in the series. Set by getNumberOfQuestions() used by next and prev quesiton
     */
    var questionMax = 0;
    /**
     * after 3 attempts hints are allowed this controls showing them all at once.
     */
    var toggleHints = true; // default since hint button is hidden till needed

    /**
     * we keep track of all previous answers in submitAnswer()
     */
    var previousAnwers = new Array();
    /**
     * Keeps track of the the completed questions so that scoring works properly. Is reset when the category changes. The array is
     * stringified with Json and stored as a cookie.
     */
    var completedQuestions = new Array();

    try {

	//alert("hello");
	$.cookie("category", "", {
	    path : '/'
	});
	$.cookie("fClass", config.SPEC_FCLASS, {
	    path : '/'
	});

	if ($.cookie("question") == null) {
	    $.cookie("question", 1, {
		path : '/'
	    });
	}

	$.cookie("questionLock", "unlocked", {
	    path : '/'
	});
	$.cookie("completedQuestions", JSON.stringify(completedQuestions), {
	    path : '/'
	});
	$.cookie("numQuestionsTried", 0, {
	    path : '/'
	});
	$.cookie("score", 0, {
	    path : '/'
	});
	
	questionMax = getNumberOfQuestions();

	var analyzer = new spectroAnalyzer();
	
	// set the OCLO Functional Class and page titlE
	$("#title").html(config.SPEC_TITLE);
	$("#questionNum").html(config.SPEC_TITLE + ": " + config.SPEC_QNAME + $.cookie("question"));
	$("#difficultyImg").attr("src", analyzer.getDifficulty());

	// handlers for the buttons.

	$("#massButton").click(function() {
	    display(config.SPEC_MASS_IMG);
	});
	$("#irButton").click(function() {
	    display(config.SPEC_IR_IMG);
	});
	$("#cnmrButton").click(function() {
	    display(config.SPEC_CARBON_IMG);
	});
	$("#hnmrButton").click(function() {
	    display(config.SPEC_HYDROGEN_IMG);
	});
	$("#viewAllButton").click(function() {
	    display(config.SPEC_ALLSPEC_PAGE);
	});
	$("#submitAnswerButton").click(function() {
	    submitAnswer();
	});
	$("#nextButton").click(function() {
	    nextQuestion();
	});
	$("#prevButton").click(function() {
	    prevQuestion();
	});
	$("#hintButton").click(function() {
	    showHint();
	});
	$("#solutionButton").click(function() {
	    unlockQuestion();
	    display(config.SPEC_ANSWER_PAGE, false);
	});

	// prevent return key on the buttons
	$("#solutionButton").keydown(function(e) {
	    preventReturn(e);
	});
	$("#hintButton").keydown(function(e) {
	    preventReturn(e);
	});
	$("#nextButton").keydown(function(e) {
	    preventReturn(e);
	});
	$("#prevButton").keydown(function(e) {
	    preventReturn(e);
	});
	$("#massButton").keydown(function(e) {
	    preventReturn(e);
	});
	$("#irButton").keydown(function(e) {
	    preventReturn(e);
	});
	$("#hnmrButton").keydown(function(e) {
	    preventReturn(e);
	});
	$("#cnmrButton").keydown(function(e) {
	    preventReturn(e);
	});
	$("#viewAllButton").keydown(function(e) {
	    preventReturn(e);
	});

	// $("#helpButton").keydown(function(e) {preventReturn(e);});

	// hide the solution button
	$("#solutionButton").hide();
	$("#hintButton").hide();

	if (config.SPEC_UNLOCK_ALL_QUESTIONS) {
	    unlockQuestion();
	}

    } catch (e) {
	alert("initializeSpectroscopy: " + e);
    }


/**
 * callback handler for when a new question number has been selected from the next and previous buttons
 */
function loadNewQuestion(num) {

    var qnum = num;
    // set the questionNumber cookie for reload.
    $.cookie("question", qnum, {
	path : '/'
    });

    try {
	analyzer.loadQuestionFile(null, null, qnum);
	// set the OCLO Functional Class and page titlE
	$("#questionNum").html(config.SPEC_TITLE + ": " + config.SPEC_QNAME + $.cookie("question"));
	$("#difficultyImg").attr("src", analyzer.getDifficulty());

	jsmeApplet.reset();
	if (!config.SPEC_UNLOCK_ALL_QUESTIONS) {
	    $("#response").val("");
	    $("#solutionButton").hide();
	}

	attempts = 0;
	analyzer.questionScore = config.SPEC_MAX_QUESTION_SCORE;

	// if the question has been completed already
	// then unlock it.
	if (checkComplete()) {
	    unlockQuestion();
	}

    } catch (e) {
	// an unexpected error happened, probably loading or parsing the
	// question file
	// so we're going to reset the category and put them back on question
	// 1, or a complete reset back to functional groups if needed,

	alert(e + "  Question Number: " + $.cookie("question"));

	$.cookie("score", 0, {
	    path : '/'
	});
	$.cookie("question", 1, {
	    path : '/'
	});
	$.cookie("numQuestionsTried", 0, {
	    path : '/'
	});
	// unlock the question
	unlockQuestion();
	location.reload();
    }

    previousAnwers = new Array();

}


/**
 * Moves to the next question.
 * 
 * End of questions behaviour is configured using  CYCLE_NEXT_PREV value.
 * 
 */
function nextQuestion() {

    if (config.SPEC_UNLOCK_ALL_QUESTIONS) {
	unlockQuestion();
    }
    var locked = $.cookie("questionLock") == "locked";
    if (locked) {
	alert(config.NO_CATEGORY_CHANGE);
	return;
    }

    var nextQnum = new Number($.cookie("question")) + 1;

    if (nextQnum.valueOf() > questionMax) {
	if (config.CYCLE_NEXT_PREV) {
	    nextQnum = new Number(1);
	} else {
	    alert(config.NO_NEXT_QUESTION);
	    return;
	}
    }

    if (!config.SPEC_UNLOCK_ALL_QUESTIONS) {
	// hide the solution button
	$("#solutionButton").hide();
	$("#hintButton").hide();

    }

    $("#response").html("");
    loadNewQuestion(nextQnum);
    

}

/**
 * Moves to the previous question in the category.
 * 
 * Beginning of questions behaviour is configured using  config CYCLE_NEXT_PREV value.
 * 
 */
function prevQuestion() {

    if (config.SPEC_UNLOCK_ALL_QUESTIONS) {
	unlockQuestion();
    }
    var locked = $.cookie("questionLock") == "locked";
    if (locked) {
	alert(config.NO_CATEGORY_CHANGE);
	return;
    }

    var prevQnum = new Number($.cookie("question")) - 1;

    if (prevQnum.valueOf() == 0) {
	if (config.CYCLE_NEXT_PREV) {
	    prevQnum = new Number(questionMax);

	} else {
	    alert(config.NO_PREV_QUESTION);
	    return;
	}
    }

    if (!config.SPEC_UNLOCK_ALL_QUESTIONS) {
	// hide the solution button
	$("#solutionButton").hide();
	$("#hintButton").hide();
    }

    $("#response").html("");
    loadNewQuestion(prevQnum);

}

/**
 * Submits to the analyzer and updates the page attributes and values based on the analyzer responses.
 */

function submitAnswer() {

    var answer = jsmeApplet.smiles();
    var feedback = "";

    try {

	if (answer == "") {
	    throw config.NO_ANSWER;
	}
	
	// If this is the first non-empty submitted
	// answer we disable navigation
	if (previousAnwers.length == 0) {
	    // lock for page
	    $.cookie("questionLock", "locked", {
		path : '/'
	    });
	}
	// if the question has been completed already
	// then unlock it.
	if (checkComplete()) {
	    unlockQuestion();
	}
	if (checkForDuplicates(answer)) {
	    throw config.DUPLICATE_ANSWER;
	}

	attempts++;
	toggleHints = true;

	//
	// default a message if the analyzer produces no
	// feedback
	var tmpMsg = analyzer.getFeedback(answer);

	if (tmpMsg.match(/^\s*$/i)) {
	    feedback += config.UNRECOGNIZED_ANWER;
	} else {
	    feedback += tmpMsg;
	}

	if (attempts == config.MAXATTEMPTS && !analyzer.isCorrect(answer)) {
	    unlockQuestion();
	    if (config.ALERT_PERSIST_MSGS) {
		alert(config.getPersistMessage());
	    } else {
		feedback += "<b>" + config.getPersistMessage() + "</b><br>";
		feedback += "Solution button is available<br>";
	    }

	    attempts = 0;
	}

	if (config.ALLOW_HINTS_AT == attempts) {
	    if (config.ALERT_HINTS_MSGS) {
		alert(config.HINTS_ALERT_MSG);
	    } else {
		feedback += "<b>" + config.HINTS_ALERT_MSG + "</b><br>";
	    }
	    $("#hintButton").show();
	}

	// if this is the first time the answer is correct
	//
	if (!checkComplete() && analyzer.getCorrect(answer)) {
	    completedQuestions.push($.cookie("question"));
	    $.cookie("completedQuestions", JSON.stringify(completedQuestions), {
		path : '/'
	    });
	    $("#scoreDiv").html(createScoreString());
	    unlockQuestion();

	}

	// create the jsmeApplet to display the answer in,
	// first create the div string and a unique ID.
	var jsme = createJsmeDiv();
	// insert into the DOM
	var response = "Your Answer <br>" + jsme.div + "FeedBack:<br>" + feedback + "<br>-----------------<br>";
	var previousResponses = $("#response").html();
	$("#response").html(response + previousResponses);
	// create the applet inside the div and feed it the answer as a JME stirng.
	var myApplet = new JSApplet.JSME(jsme.id, "250px", "120px", {
	    "options" : "depict"
	});
	myApplet.readMolecule(jsmeApplet.jmeFile());

	/*
	 * $("#"+ jsme.id).css("margin-left", "300"); $("#"+ jsme.id).css("clear", "none");
	 */

    } catch (e) {
	alert(e);
    }

}

/**
 * Creates the string for a div to be populated with a JSME applet.
 * 
 * Returns an object with the div string and its unique id.
 * 
 */
function createJsmeDiv() {

    var id = "JSME" + jsmeDivCount++;

    var response = {
	"div" : String,
	"id" : String
    };

    response.div = '<div id="' + id + '"></div>';
    response.id = id;

    return response;

}

/**
 * Uses the values in the cookies to create a string showing the average (as a %) over x number of questions.
 * 
 * @returns {String}
 */
function createScoreString() {

    var me = "Score: " + Math.round(analyzer.getScore()) + "% on " + $.cookie("numQuestionsTried") + " question";

    var numcompletedQs = numberOfCompleteQuetions();

    // create the string
    if (numcompletedQs == 0) {
	return "";
    }

    if (numcompletedQs > 1) {
	me += "s";
    }

    return me;

}

/**
 * unlocks the question so that the solution can be viewed and the question or category changed. Also allows duplicate answers to be
 * submitted.
 * 
 */
function unlockQuestion() {

    // $("#numberList").removeAttr("disabled");
    $("#nextButton").removeAttr("disabled");
    $("#prevButton").removeAttr("disabled");
    $("#solutionButton").show();
    $("#hintButton").show();
    $.cookie("questionLock", "unlocked", {
	path : '/'
    });
    previousAnwers = new Array();
}

/**
 * Called from submitAnswer() to check if the answer was previously given.
 * 
 * @param answer
 *                The answer that was submitted.
 * @returns {Boolean} Can you guess what true means?
 */
function checkForDuplicates(answer) {
    if (config.SPEC_UNLOCK_ALL_QUESTIONS) {
	return false;
    }

    var regex = new RegExp(RegExp.escape(answer));
    // alert(previousAnwers);
    for (var int = 0; int < previousAnwers.length; int++) {
	var pastAnswer = previousAnwers[int];
	if (regex.test(pastAnswer) == true) {
	    if (pastAnswer == answer) {
		return true;
	    }

	}
    }

    previousAnwers.push(answer);
    return false;
}

/**
 * Alerts or displays a hint.  Handler for the hint button.  
 * Behaviour is configured via  config SPEC_UNLOCK_ALL_QUESTIONS and 
 * config ALERT_HINTS_MSGS
 */
function showHint() {

    if (config.SPEC_UNLOCK_ALL_QUESTIONS) {
	toggleHints = true;
    }

    if (config.ALERT_HINTS_MSGS) {

	if (toggleHints) {
	    alert(analyzer.getHint());
	} else {
	    alert(config.TRY_FIRST);

	}
    } else {

	if (toggleHints) {
	    $("#response").html("<b>" + analyzer.getHint() + "</b><br>" + $("#response").html());
	} else {
	    $("#response").html("<b>" + config.TRY_FIRST + "</b><br>" + $("#response").html());
	}
    }
    toggleHints = false; // show one hint at a time.
}

/**
 * 
 * @returns {Number} number of complete questions in the category since the category was selected.
 */
function numberOfCompleteQuetions() {

    var completedQs = JSON.parse($.cookie("completedQuestions"));
    return completedQs.length;

}

/**
 * checks to see if the question number in the cookie 'queston' has been completed.
 * 
 * @returns {Boolean} true if the question is complete, false otherwise.
 */
function checkComplete() {

    var completedQs = JSON.parse($.cookie("completedQuestions"));

    var questionNum = $.cookie("question");

    for (var int = 0; int < completedQs.length; int++) {
	var compl = completedQs[int];
	if (questionNum == compl) {
	    return true;
	}

    }
    return false;
}



/**
 * Displays a popup window or new tab
 * 
 * @param {String}
 *                what what is the rleative URL to display
 * @param {Boolean}
 *                popup true and the window will be a popup false and it will be in a new tab.
 */

function display(what, popup) {

    try {
	var newWindow = null;
	var specString = config.getBaseUrl() + analyzer.fclass + config.STN + config.QDIR_PREFIX
		+ $.cookie("question") + "/" + what;

	if (popup == true || popup == null) {
	    newWindow = window.open(specString, "Pup", "toolbar=no,menubar=no,status=yes,scrollbars=yes,width=750,height=460");
	    newWindow.focus();
	} else {
	    newWindow = window.open(specString, "Ptab");
	    newWindow.focus();
	}
	// alert(newWindow.document.title);
    } catch (e) {
	alert(e);
    }

}

/**
 * Uses Ajax (in a synchronous mode) to retrieve /json/spectroscopy.json and uses it determine the number 
 * of questions for spectroscopy. 
 * 
 * If you add a question you must update /json/spectroscopy.json accordingly 
 * 
 * @returns the number of questions
 */

function getNumberOfQuestions() {

    var maxQuestions = 0;
    // make it synch
    $.ajaxSetup({
	async : false
    });

    try {

	var url = config.getBaseUrl() + config.SPEC_CONF;

	$.get(url, function(data, status) {
	    maxQuestions = new Number(JSON.parse(JSON.stringify(data))).valueOf();
	    // alert(maxQuestions);
	});

    } catch (e) {

	alert("Fatal Error: Unable to load json" + e);
	throw e;
    }

    return maxQuestions;
}


/**
 * 
 * Used by initializeSpectroscopy to load javascript files it depends on. JSME and JQuery must already 
 * be loaded (from the html page). 
 * 
 * @param src
 */
function loadJS(src) {
    var jsLink = $("<script type='text/javascript' src='"+src+"'>");
    $("head").append(jsLink); 
} 

/**
 * Used by initializeSpectroscopy to load the CSS file. 
 * 
 * @param href
 */
function loadCSS(href) {
    var cssLink = $("<link rel='stylesheet' type='text/css' href='"+href+"'>");
    $("head").append(cssLink); 
}

}

/**
 * IMPORTANT: this is the function JSME calls when it initializes itself. This function and the document.ready function don't play
 * nice together.
 * 
 * This function should not be removed or called from the document ready function. It can be modified if needed. This function and
 * how it works is why we need to do Synchronous calls to the server for question files.
 * 
 */
function jsmeOnLoad() {

    try {

	 jsmeApplet = new JSApplet.JSME("appletContainer", "323px", "350px", {
	    "options" : "autoez,removehs"
	});

	// jsmeApplet has the same API as the original Java applet
	document.JME = jsmeApplet;
	jsmeApplet.showInfo(config.SPEC_TITLE + ": " + config.SPEC_QNAME + $.cookie("question"));
	adjustCSS();	

    } catch (e) {
	alert("error in jsmeOnLoad: " + e);

    }


}





/**
 * Adjusts the css on the page for the middle and right column. is called from jsmeOnload so the height includes the jsme App.
 * 
 */

function adjustCSS() {

    
    var colHeight = $("#leftColumn").height();

    $("#centerColumn").height(colHeight);
    $("#rightColumn").height(colHeight);

    var responseHeight = colHeight - $("#prevButton").height() - 60; // for padding

    // response div
    $("#response").height(responseHeight);

};