/**
* @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
}