Source: jsoclo/nomenclature.js

/**
 * @class
 * 
 * @classdesc
 * <p>
 * nomenclatureController() is called via jQueries document.ready function
 * which appears in StructureToName.html. It is called each time the page is
 * loaded or the browser is refreshed.
 * <p>
 * nomenclatureController  is the javascript class responsible for running the interface 
 * for the nomenclature JSOCLO.
 * 
 * <p>
 * Cookie values are used in this function and application.  cookies are used to determine if..
 * <ol>
 * <li>This is the first time the page is being loaded. (no Cookies present).
 * <li>This is a browser reload. (cookies present) and the question is not
 * locked.
 * <li>This is a browser reload. (cookies present) and the question is not
 * locked.
 * </ol>
 * 
 * In each case the action is slightly different. Locked means the question is
 * 'in progress'; is not yet completed and the max number of attempts has not
 * been reached.
 * 
 * <p>
 * Locked questions disable the users ability to change the question or
 * category. Reloading the page with the browser will not unlock a a locked
 * question.
 * 
 * <p>The category array contains the list of categories and the number of 
 * questions in each category.   It is defined in /json/orgnom.json and is 
 * loaded into the application via getCategoryArray(). 
 * 
 * <p>See the admin utilities  for functionality that can generate the JSON to use as the 
 * contents of /json/orgnom.json. 
 * 
 * 
 */
function nomenclatureController() {

    	/**
    	 * load the other JS 
    	 * 
    	 */
  
    loadJS("../js/jquery/jquery.cookie.js");
    loadJS("../js/jsoclo/chemUtils.js"); 
    loadJS("../js/jsoclo/analyzerBase.js");
    loadJS("../js/jsoclo/orgnomAnalyzer.js");
    
	/** the number of attempts made on this question */
	var attempts = 0;

	/**
	 * maps category names to the number of questions
	 * in the category.  Is built in getCategoryArray()
	 */
	var categoryQuestionMap = [];
	
	/** built in getCategoryArray() */
	var categoryArray = new Array();
	
	/** used to populate the hidden div.
	 It is not currently used in this application. */
	var otherLink = "trouble.html";

	/**
	 * 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();

	// return button for scoreContent div
	var returnButton = "";

	try {
		

		// set the OCLO Functional Class and page title
		$.cookie("fClass", config.ORGNOM_FCLASS, {
			path : '/'
		});
		$("#title").html(config.ORGNOM_TITLE);

		// the default category and question number to load if
		// there is nothing set already
		if ($.cookie("category") == null) {
			$.cookie("category", "functional_groups", {path : '/'});
			$.cookie("score", 0, {path : '/'});
			$.cookie("numQuestionsTried", 0, {path : '/'});

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

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

		// loads based on above cookies
		analyzer = new OrgnomAnalyzer();
		var score = new Number($.cookie("score"))/ new Number($.cookie("numQuestionsTried"));
		analyzer.setScore(score);

		// populate the score string
		$("#scoreDiv").html(createScoreString());
		// create the menu
		$("#menu").html(createMenu());
		// based on the size the menu turned out to be we adjust the
		// content, score page and columns sizes
		adjustCSS();

		// callback handler for the the question
		// number select box.
		$("#numberList").change(function() {loadNewQuestion();});

		// set defaults has to be called
		// afteer createMenu() because
		// it relies on the categoryQuestionMap
		setDefaults();

		// hit enter while typing an answer submits
		$("#answerInput").keydown(function(e) {
			if (e.keyCode == 13) {
				submitAnswer();
			}
		});

		// callback handler for the menu
		// select. The menu is built as an <ul>
		$("li").click(function() {changeCategories($(this).html().split("<")[0]);});

		// button handlers
		$("#submitAnswerButton").click(function() {submitAnswer();});
		$("#solutionButton").click(function() {solution();});
		$("#scoreButton").click(function() {getOtherContent(otherLink);});
		$("#nextButton").click(function() {nextQuestion();});
		$("#prevButton").click(function() {prevQuestion();});
		$("#helpButton").click(function() {getHelp();});
		
		//prevent return key on buttons 
		$("#solutionButton").keydown(function(e) {preventReturn(e);});
		$("#scoreButton").keydown(function(e) {preventReturn(e);});
		$("#nextButton").keydown(function(e) {preventReturn(e);});
		$("#prevButton").keydown(function(e) {preventReturn(e);});
		$("#helpButton").keydown(function(e) {preventReturn(e);});

		// save the html for the return button so it can
		// be restored upon return.
		returnButton = $("#returnDiv").html();

		// what to show/hide on page load
		$("#backButton").hide();
		$("#scoreContent").hide();
		$("#content").show();
		$("#solutionButton").hide();

	} catch (e) {
		// an unexpected error occured. Probably in loading the question file
		// so we are going to try and reset.
		$.cookie("category", "functional_groups", {path : '/'});
		$.cookie("score", 0, {path : '/'});
		$.cookie("question", 1, {path : '/'});
		$.cookie("numQuestionsTried", 0, {path : '/'});
		location.reload();
	}



/**
 * Sets default values on page load based on the 'category' cookie value.
 * 
 */
function setDefaults() {

	var value = $.cookie("category");
	var numQuestions = categoryQuestionMap[value];

	$("#questionTitle").html(value.replace(/_/g, ' '));
	$("#questionQname").html(config.ORGNOM_QNAME + analyzer.questionNum);

	// empty the number list
	$("#numberList").find('option').remove();
	//re populate it based on this category. 
	for (var i = 0; i < numQuestions; i++) { // numbers for question dropdown
		$("#numberList").append(new Option(i + 1));
	}
	//select the question that the analyzer is set up for 
	$("#numberList").val(analyzer.questionNum);
	//clear the answer input
	$("#answerInput").val("");

	$("#typeMessage").html(analyzer.getType());
	$("#difficultyImg").attr("src", analyzer.getDifficulty());

	// if this reload happened while they were trying a
	// question then we lock the question again
	if ($.cookie("questionLock") == "locked") {
		$("#numberList").attr("disabled", "disabled");
	}

	// if this question has already been completed
	// then unclock the soltion

	if (checkComplete()) {
		unlockQuestion();

	}

	// for testing
	if (config.UNLOCK_QUESTION) {
		unlockQuestion();
	}
}

/**
 * Resets the page to the selected category and chooses question number 1. 
 * 
 * @param value
 *            the category chosen from the right-hand side menu.
 */
function changeCategories(value) {

	// place the _ back into the selected item.
	var actualValue = value.replace(/\s/g, '_');

	// if they choose the same category
	// then do nothing.

	if (actualValue == $.cookie("category")) {
		return;
	}

	if ($.cookie("questionLock") == "locked") {
		alert(config.NO_CATEGORY_CHANGE
				+ config.getPersistMessage());
		// they can't change categories
		return;

	}

	try {
		var numQuestions = categoryQuestionMap[actualValue];

		// reset the scoring
		$.cookie("score", 0, {path : '/'});
		$.cookie("numQuestionsTried", 0, {path : '/'});
		completedQuestions = [];
		$.cookie("completedQuestions", JSON.stringify(completedQuestions), {path : '/'});
		$.cookie("question", 1, {path : '/'});

		analyzer.loadQuestionFile(config.ORGNOM_FCLASS, actualValue, 1);
		analyzer.questionScore = config.ORGOM_MAX_QUESTION_SCORE;
		analyzer.setScore(0);
		// $("#averageScore").val(analyzer.getScore()+"%");
		$("#scoreDiv").html(createScoreString());
		// empty the number list then repopulate
		$("#numberList").find('option').remove();

		for (var i = 0; i < numQuestions; i++) { // numbers for question										
			$("#numberList").append(new Option(i + 1));
		}

		attempts = 0;
		$("#questionTitle").html(value.replace(/_/g, ' '));
		$("#response").val("");
		$("#answerInput").val("");
		$("#solutionButton").hide();
		$("#difficultyImg").attr("src", analyzer.getDifficulty());

		// load the first question
		$("#numberList").change();
	} catch (e) {
		alert("changeCategories error:  " + e);
	}

	previousAnwers = new Array();

	// we use the value here to test with
	if (config.UNLOCK_QUESTION) {
		unlockQuestion();
	}

}

/**
 * callback handler for when a new question number has been selected from the
 * drop-down select box or via the next and previous buttons
 */
function loadNewQuestion() {

	var qnum = $("#numberList option:selected").index() + 1;
	// set the questionNumber cookie for reload.
	$.cookie("question", qnum, {
		path : '/'
	});

	try {
		analyzer.loadQuestionFile(null, null, qnum);
		jsmeApplet.readMolecule(analyzer.getJme());
		// alert(analyzer.getJme());
		if (analyzer.getJme() == null) {
			jsmeApplet.reset();
		}
		$("#questionNumber").html(qnum);
		$("#typeMessage").html(analyzer.getType());
		$("#response").val("");
		$("#answerInput").val("");
		$("#solutionButton").hide();
		$("#difficultyImg").attr("src", analyzer.getDifficulty());
		$("#questionQname").html(config.ORGNOM_QNAME + analyzer.questionNum);

		attempts = 0;
		analyzer.questionScore = config.ORGOM_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 + " Category: " + $.cookie("category") + "  Question Number: "+ $.cookie("question"));

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

	previousAnwers = new Array();

}

/**
 * Opens a new window/tab using the link found 
 * in the @link section of the question file, or, 
 * alerts a no help message to the user if no link 
 * was in the file. 
 */
function getHelp() {

	if ("" == analyzer.getHelpURL()) {
		alert(config.NO_HELP_LINK);
		return;
	}
	window.open(analyzer.getHelpURL());
	
}

/**
 * Moves to the next question in the category. 
 * 
 * End of questions behaviour is configured using 
 * {@link jsocloconfig} CYCLE_NEXT_PREV value. 
 * 
 */
function nextQuestion() {

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

	if (nextQnum.valueOf() > categoryQuestionMap[$.cookie("category")]) {
		if(config.CYCLE_NEXT_PREV){		
			nextQnum = new Number(1); 
		}else{
			alert(config.NO_NEXT_QUESTION);
			return;
		}
	}

	// otherwise select the question in the 
	//drop down and call its handler
	$("#numberList").val(nextQnum.valueOf());
	$("#numberList").change();

}

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

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

	if (prevQnum.valueOf() == 0) {
		if(config.CYCLE_NEXT_PREV){
			prevQnum = new Number(categoryQuestionMap[$.cookie("category")]);
			
		}else{
			alert(config.NO_PREV_QUESTION);
			return;
		}
	}

	// otherwise select the question in the 
	//drop down and call its handler
	$("#numberList").val(prevQnum.valueOf());
	$("#numberList").change();

}

/**
 * Adjusts the css on the page based on the size the Menu div turned out to be
 * after the menu was generated.
 */
function adjustCSS() {

	// make the main content area the same height as the
	// menu turned out to be.
	var contentHeight = $("#menu").height();
	$("#content").height(contentHeight);
	$("#scoreContent").height(contentHeight);

	// make the main content area the right width based on what the
	// menu turned out to be.
	var contentWidth = $("#container").width() - $("#menu").width() - 32;
	$("#content").width(contentWidth);
	$("#scoreContent").width(contentWidth);

	// adjust the columns
	var colHeight = contentHeight - $("#QuestionTitles").height()
			- $("#returnDiv").height() - $("#backButton").height()
			- $("#footer").height();
	$("#leftColumn").height(colHeight);
	$("#rightColumn").height(colHeight + 5); // make up for left top padding

	$("#deptBanner").width(contentWidth);
	$("#answerInput").width($("#appletContainer").width() - 17);

	$("#footer").css("padding-left", $("#menu").width() + 20);

	$("#noScript").css("width", 0);
	$("#noScript").css("height", 0);

}

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

function submitAnswer() {
	var answer = $("#answerInput").val();
	var msg = "Your Answer: " + answer + "\n\nFeedback: \n";

	// trim the answer
	answer = answer.replace(/^\s+|\s+$/g, '');
	try {

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

		//default a message if the analyzer produced no 
		//feedback
		var tmpMsg = analyzer.getFeedback(answer);	
		
		if (tmpMsg.match(/^\s*$/i)) {
			msg += config.UNRECOGNIZED_ANWER;
		} else {
			msg += tmpMsg;
		}

		//prepend the message to the responses
		$("#response").val(msg + "\n...........\n\n" + $("#response").val());

		// enable disabled buttons
		if (attempts == analyzer.getMaxAttempts() && !analyzer.isCorrect(answer)) {
			unlockQuestion();
			alert(config.getPersistMessage());
			attempts = 0;
		}

		attempts++;

		// ask the analyzer if the question is correct and is
		// not yet completed. enable the buttons and present
		// the score. getCorrect() deals with scoring
		if (!checkComplete() && analyzer.getCorrect(answer)) {
			completedQuestions.push($.cookie("question"));
			$.cookie("completedQuestions", JSON.stringify(completedQuestions),{path : '/'});
			$("#scoreDiv").html(createScoreString());
			unlockQuestion();

		}

	} catch (e) {
		alert("Submit Answer: " + e);
		return;
	}

	//for testing with
	if (config.UNLOCK_QUESTION) {
		unlockQuestion();
	}

}

/**
 * Uses the values in the cookies to crate a string showing 
 * the average (as a %) over x number of question in the category. 
 * 
 * @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";
	}
	
	/*
	 * Unused code that also shows the questions that were completed. 
	 * Dr. Hunt did not want them in. 
	 * if( numcompletedQs > 0){ me +=" ["; for (var int = 0; int <
	 * completedQs.length; int++) { var qnum = completedQs[int]; if(int ==
	 * completedQs.length -1){ me += qnum + "]"; return me; } me += qnum + ",";
	 *  } }
	 */

	return me;

}

/**
 * unlocks the question so that the solution can be viewed and the question
 * or category changed.
 */
function unlockQuestion() {

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

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

	var regex = new RegExp(RegExp.escape(answer));

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

/**
 * Not currently used in this application
 * 
 * @param page page is the URL to load into the 
 * hidden div.  
 */
function getOtherContent(page) {

	$.get(page, function(data, status) {
		fillContent(data, status);
	});

}

/**
 * AJAX callback handler for the SJAX request made in getOtherContent()
 * 
 * @param data
 * @param status
 */
function fillContent(data, status) {
	try {

		$("#content").hide();
		$("#scoreContent").html(data + returnButton);
		$("#scoreContent").show();

		$("#backButton").show();

		$("#backButton").click(function() {
			$("#scoreContent").html("" + returnButton);
			$("#backButton").hide();
			$("#scoreContent").hide();
			$("#content").show();
		});

		// $("#scoreContent").attr("width", "500");

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

}

/**
 * If the question has been completed then it alerts the answer. If the question
 * has not been completed then it asks for confirmation, warning of question
 * forfeit before alerting the answer and giving zero for the question
 * 
 */
function solution() {
	var answer = new String(analyzer.getCorrectAnswer());
	answer = answer.replace(/\|/g, " or ");

	// if the question has already been completed then just
	// show the answer
	if (checkComplete()) {
		alert(answer + config.ALREADY_CORRECT);
		return;
	}

	if (confirm(config.FORFEIT_CONFIRMATION)) {
		// forfeit the question
		analyzer.forfeitQuestion();
		// display the new score string
		$("#scoreDiv").html(createScoreString());
		// add the question to the completed list
		completedQuestions.push($.cookie("question"));
		// reset the cookie array
		$.cookie("completedQuestions", JSON.stringify(completedQuestions), {
			path : '/'
		});
		// updte the score string
		$("#scoreDiv").html(createScoreString());
		// display the anwer
		alert(answer + config.FORFEIT);
	}

}

/**
 * 
 * @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 since the time we changed categories.
 * 
 * @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;
}

/**
 * Creates and uses the Cateogry Array to set the menu up. The menu is created as an
 * unordered list and uses CSS and Javascript to work as links
 */
function createMenu() {

	// setup for building the menu dynamically
	// categoryQuestionMap is built in getCategoryArray()
	categoryArray = getcategoryArray();

	var response = "<ul id='mainMenu'>\n";

	for (var int = 0; int < categoryArray.length; int++) {
		response += "<li id='" + categoryArray[int][0] + "' class='fakeLink'>"
				+ categoryArray[int][0].replace(/_/g, ' ') + "</li>\n";
	}

	response += "</ul>\n";
	return response;

}




/**
 * Uses Ajax (in a synchronous mode) to retrieve 
 * /json/orgnom.json and uses it to instantiate the category array. 
 * 
 * side effect, builds question map which maps the 
 * category name to the number of questions in the 
 * category.
 * 
 * @returns the category Array 
 */

function getcategoryArray() {
	
		var categoryArray = null; 	
		//make it synch
		$.ajaxSetup({
			async : false
		});
			
		try{
			
			var url = config.getBaseUrl() + config.ORGNOM_CONF;
			
			$.get(url, function(data, status) {
				categoryArray = JSON.parse(JSON.stringify(data));
			});
			
		}catch(e){
			
			alert("Fatal Error: Unable to load orgnom.json"+ e);	
			return null;
		}
	
	// map the number of questions in the category to the category name
	for (var int = 0; int < categoryArray.length; int++) {

		categoryQuestionMap[categoryArray[int][0]] = categoryArray[int][1];

	}

	return categoryArray;
}


function loadJS(src) {
    var jsLink = $("<script type='text/javascript' src='"+src+"'>");
    $("head").append(jsLink); 
} 

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", "335px", "230px", {
			"options" : "depict"
		});

		// jsmeApplet has the same API as the original Java applet
		document.JME = jsmeApplet;
		// display the SMILE
		jsmeApplet.readMolecule(analyzer.getJme());

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

	}
	

	// Opera patch: if some applet elements are not displayed, force repaint
	// jsmeApplet.deferredRepaint(); //the applet will be repainted after the
	// browser event loop returns
	// it is recommended to use it if the JSME is created outside this
	// jsmeOnLoad() function

}