<!--
// tv_preview_engine.js
// TestVision standalone engine
// Versie 1.05
// Datum wijziging: 15082005
// - toevoeging fbsr-flag voor het wel of geen feedback / sturyref tonen tijdens toets
// - toevoeging toets-properties Cesuur, PartScore, ScoreRule, MarkRule
// - toevoeging scorings-functies
//   Letop: hotspotmatch en hotspotmultiple scoring nog niet correct geïmplementeerd (zie java-code)
//	 Letop: voor de multiple-vraagsoorten is nu de TV4 scoring actief
// Datum wijziging: 14022005
// - kleine letters voor bestandsnamen feedback (qfb i.p.v. QFB e.d.)
// Datum wijziging: 01052003
// - toevoeging vraagsoort fillinopen
// - toevoeging vraagsoorten hotspot

// Nog:
// - uitgebreide scoring
// - beveiligen vorige/volgende dmv replace ?
// - randomiseren vragen
// - randomiseren alt
// - trimmen fillin en fillinopen vraag (bij feedback-check en terugkijken vraag)

// ----------------------------------------------------------------------------------------------------------
// tvTestObject constructor functie
function tvTest(pID,pName,pTime,pReview,pFBSR,pCesuur,pPartScore,pScoreRule,pMarkRule) {
	// Algemene properties
	this.id     = pID;						
	this.name   = pName;

	if (pTime!='')     		this.time = pTime;						// Testtijd, standaard 'zero' (is onbeperkte tijd)
	else               		this.time = 'zero';			
	if (pReview!=null) 		this.review = pReview;				// Wel of niet terugkijken van een vraag, standaard true
	else               		this.review = true;			
	if (pFBSR!=null)   		this.fbsr = pFBSR;						// Wel of geen feedback / study reference tijdens toets, standaard true
	else               		this.fbsr = true;
	if (pCesuur!=null) 		this.cesuur = pCesuur;				// Cesuurscore, standaard 0 (iedereen geslaagd)
	else               		this.cesuur = 0;
	if (pPartScore!=null)	this.partscore = pPartScore;	// Gedeeltelijke scoring op toetsniveau, standaard true
	else               		this.partscore = true;	
	if (pScoreRule!=null) this.scorerule = pScoreRule;	// Scoringsregel, standaard -1 (geen scoringsregel)
	else               		this.scorerule = -1;
	if (pMarkRule!=null)  this.markrule = pMarkRule;		// Cijferregel, standaard -1 (geen cijferregel)
	else               		this.markrule = -1;	
	
	// Scoring properties
	this.points_good  = 1;
	this.points_wrong = 1;
	
	// Vraag properties
	this.nr_questions = 0;
	this.q_pointer    = 0;
	this.questions    = new Array();

	// Statusproperty
	this.results      = false;						// Wordt true als de toets beëindigd wordt
	
	// Methods
	this.addQuestion      		= meth_addQuestion;
	this.getQuestion      		= meth_getQuestion;
	this.getNrCorrect     		= meth_nrCorrect;
	this.getNrPartCorrect 		= meth_nrPartCorrect;	
	this.getNrNotCorrect  		= meth_nrNotCorrect;	
	this.getNrNotAnswered 		= meth_nrNotAnswered;	
	this.getNrNotCorrectedYet  = meth_nrNotCorrectedYet;	
	this.getMaxTestScore			= meth_getMaxTestScore;
	this.getTestScore				= meth_getTestScore;
	this.getPercTestScore		= meth_getPercTestScore;
	this.getPassFail				= meth_getPassFail;
   
   this.useScoringInPreview   = false;
   
}

// tvQuestionObject constructor functie
function tvQuestion(pID,pNr,pKind,pCase,pFeedback,pStudyref,pCAns,pWeight,pPartscore,pNr_alt,pNr_match) {
	// Algemene properties
	this.id          = pID;					// oorspronkelijke nummer
	this.nr          = pNr;					// volgnummer
	this.kind        = pKind;				// vraagsoort
	this.caseid      = pCase;				// bevat id van bijbehorende case (anders '')
	this.feedback    = pFeedback;		// bevat true of false
	this.studyref    = pStudyref;		// bevat true of false
	this.cans				 = pCAns;				// controle
	this.weight      = pWeight;			// weging
	this.partscore   = pPartscore;	// wel of geen gedeeltelijke scoring
	this.nr_alt      = pNr_alt;			// aantal alternatieven
	this.nr_match    = pNr_match;		// aantal matches

	// Gegeven antwoord is initieel 'dontknow'
	this.answer      = 'dontknow';	// gegeven antwoord

	// Methods	
	this.checkAnswer = meth_checkAnswer;
	this.getFeedback = meth_getFeedback;
}
// ----------------------------------------------------------------------------------------------------------
// Methods tvTestObject

// Toevoegen van vraagobject
function meth_addQuestion(pID,pNr,pKind,pCase,pFeedback,pStudyref,pCAns,pWeight,pPartscore,pNr_alt,pNr_match) {
	this.nr_questions++;
	this.questions[this.nr_questions-1] = new tvQuestion(pID,pNr,pKind,pCase,pFeedback,pStudyref,pCAns,pWeight,pPartscore,pNr_alt,pNr_match);
	this.q_pointer = 1;							// na toevoegen van een vraag kan de pointer naar het eerste volgnr
}
// Opvragen van vraagobject
function meth_getQuestion(pNr) {
	for (var i=0; i<this.questions.length; i++) {
		if (this.questions[i].nr==pNr) return this.questions[i];
	}
}
function meth_nrCorrect() {
	var n=0;
	for (var i=0; i<this.questions.length; i++) {
		if (getRightWrong(this.questions[i])==1) n++;
	}
	return n;
}
function meth_nrPartCorrect()	{
	var n=0;
	for (var i=0; i<this.questions.length; i++) {
		if (getRightWrong(this.questions[i])==2) n++;
	}
	return n;
}
function meth_nrNotCorrect(){
	var n=0;
	for (var i=0; i<this.questions.length; i++) {
		if (getRightWrong(this.questions[i])==-1) n++;
	}
	return n;
}
function meth_nrNotAnswered() {
	var n=0;
	for (var i=0; i<this.questions.length; i++) {
		if (getRightWrong(this.questions[i])==0) n++;
	}
	return n;
}
function meth_nrNotCorrectedYet() {
	var n=0;
	for (var i=0; i<this.questions.length; i++) {
		if (getRightWrong(this.questions[i])==-2) n++;
	}
	return n;
}
function meth_getMaxTestScore() {
	var n=0;
	for (var i=0; i<this.questions.length; i++) {
		if (this.questions[i].weight!=null) n+=this.questions[i].weight;
	}
	return n;
}
function meth_getTestScore() {
	var n=0;
	for (var i=0; i<this.questions.length; i++) {
		n+=getScore(this.questions[i],this);
	}
	return n;
}
function meth_getPercTestScore() {
	var perc = (this.getTestScore() / this.getMaxTestScore()) * 100; 
	if (perc>=this.cesuur-1 && perc < this.cesuur) return this.cesuur-1;
	return Math.round(perc);
}
function meth_getPassFail() {
	if (this.getPercTestScore() < this.cesuur) return '0' ;	// Niet geslaagd
	else if ((this.getPercTestScore() >= this.cesuur) && (this.getPercTestScore() < 101)) return '1';
	else return '-1';
}

// ----------------------------------------------------------------------------------------------------------
// Methods tvQuestionObject

// Vraag controleren
function meth_checkAnswer() {
  var antwoord = getRightWrong(this);
	var strResult = '---';

	if (parent.interface_language=='l2') {	// Engels
		switch ( antwoord ) {
			case -2: // Niet gescoord (open vraag)
				strResult = 'not yet corrected (open question)';
				break;
			case -1: // Fout
				strResult = 'wrong answered';
				break;
			case  0: // Onbeantwoord
				strResult = 'not answered';
				break;
			case  1: // Goed
				strResult = 'right answered';
				break;
			case  2: // Gedeeltelijk goed
				strResult = 'partially right answered';
				break;
		 }
	}
	else {
		switch ( antwoord ) {
			case -2: // Niet gescoord (open vraag)
				strResult = 'nog niet beoordeeld (open vraag)';
				break;
			case -1: // Fout
				strResult = 'fout beantwoord';
				break;
			case  0: // Onbeantwoord
				strResult = 'niet beantwoord';
				break;
			case  1: // Goed
				strResult = 'goed beantwoord';
				break;
			case  2: // Gedeeltelijk goed
				strResult = 'gedeeltelijk goed beantwoord';
				break;
		 }
	}

	return strResult;    
}
// Opvragen juiste feedback
function meth_getFeedback() {
  var antwoord = getRightWrong(this);
	var strFeedback = 'qfb_none';
	
	// Voor feedback op alternatiefniveau (elk alternatief een andere feedback, alleen bij mpc)
	// Vooralsnog niet geïmplementeerd, aangezien de filecheck niet kan in javascript

   switch ( antwoord ) {
        case -1: // Fout
           strFeedback = 'qfbw_' + this.id;
           break;
        case  0: // Onbeantwoord
           strFeedback = 'qfb_noanswer';
           break;
        case  1: // Goed
           strFeedback = 'qfbr_' + this.id;
           break;
        case  2: // Gedeeltelijk goed
           strFeedback = 'qfbp_' + this.id;
           break;
   }

	return strFeedback;    
}
// --------------------------------------------------------------------------------------------
// Ondersteunende routines voor tonen feedback en scoring

//----------------------------------------------------------------------
// returnwaarden:
//  -2 : De vraag kan niet worden gescoord (open vraag)
//  -1 : De vraag is volkomen fout beantwoord
//   0 : De vraag is niet beantwoord
//  +1 : De vraag is helemaal goed beantwoord
//  +2 : De vraag is gedeeltelijk goed beantwoord
//----------------------------------------------------------------------
function getRightWrong(cv)	{
 	var blnEmpty = false;
 	var intResult=0;
 	var Antwoord = cv.answer;
 	var cAntwoord = cv.cans;
 	var vraagsoort = cv.kind;
 	var lngAlternatieven = cv.nr_alt;
 	var v = new Array();
	var vAntwoord = new Array();

  if ( Antwoord.length == 0 ) blnEmpty = true;
  if ( Antwoord=='dontknow' ) return 0;
  if ( Antwoord=='empty' ) blnEmpty = true;
	
	if (vraagsoort!='fillin' && vraagsoort!='fillinopen') {
	  v = Antwoord.split(',');
    vAntwoord = cAntwoord.split(',');
  }
	else { 
      if (vraagsoort=='fillin') {
         //alert('correct antwoord is ' + cAntwoord);
         vAntwoord = cAntwoord.split(',');
         //ATE 11.09.2007 Dit gaat natuurlijk mis als een van de correcte antwoordalternatieven een comma bevat
      }
      else { 
   	  v[0] = Antwoord;
        vAntwoord[0] = cAntwoord;
      }
	}
   
	switch ( vraagsoort )
	{
		case 'multiplechoice': 
		case 'truefalse':
		case 'hotspot':
			 if ( blnEmpty ) {
					intResult = 0;
			 }
			 else {
					var strAltNr = v[0];
					if ( ArrayContains( vAntwoord, strAltNr ) ) intResult = 1;
					else intResult = -1;
			 }
			 break;
	
		case 'hotspotmultiple':
		case 'multiplechoicemore':
		case 'truefalsemultiple':
		case 'matchmatrix':
			 var resultaatAlternatief;
			 resultaatAlternatief=bepaalAntwoordAlternatief( v, vAntwoord, blnEmpty, lngAlternatieven);
			 var aantalAltGoed= resultaatAlternatief[0];
			 var aantalAltFout= resultaatAlternatief[1];
			 
			 if ( aantalAltGoed > 0 ) {
					if ( aantalAltFout > 0 ) intResult = 2;
					else intResult = 1;
			 }
			 else if ( aantalAltFout > 0 ) {
					intResult = -1;
			 }
			 break;
	
		case 'hotspotmatch':
		case 'matchone':
		case 'matchmany':
			 var resultaatMatch;
			 resultaatMatch = bepaalAntwoordMatch( v, vAntwoord);
	
			 var aantalMatchGoed = resultaatMatch[0];
			 var aantalMatchFout = resultaatMatch[1];
			 if ( aantalMatchGoed > 0 ) {
					if ( aantalMatchGoed == lngAlternatieven ) intResult = 1;
					else intResult = 2;
			 }
			 else if ( aantalMatchFout > 0 ) {
					intResult = -1;
			 }
			 break;
			 
		case 'classification':
			 if (!blnEmpty) {
					var resultaatOrder;
					resultaatOrder = bepaalAntwoordOrder( v, vAntwoord, lngAlternatieven );
	
					var intPuntenGoed = resultaatOrder[0];
					var intPuntenFout = resultaatOrder[1];
	
					// Het maximaal te behalen aantalpunten is het aantal alternatieven * 2
					if ( intPuntenGoed > 0 ) {
						 if ( intPuntenFout != 0 || intPuntenGoed != lngAlternatieven * 2 )
								intResult = 2;
						 else
								intResult = 1;
					}
					else if ( intPuntenFout > 0 ) {
						 intResult = -1;
					}
			 }
			 break;
			 
		case 'fillinopen':
				intResult = -2;  // open vraag kan niet worden gescoord
				break;
	
		case 'fillin':
			 var strAntwoord;
			 strAntwoord = spaceTrimVoor(Antwoord);
			 strAntwoord = spaceTrimNa(strAntwoord);
	
			 intResult = -1;
			 var i;
			 for ( i=0; i<vAntwoord.length; i++ ) {
					if ( compareFillInAnswer( strAntwoord, vAntwoord[i] ) ) {
						 intResult = 1;
						 break;
					}
			 }
			 break;
			 
		default:
			 alert( 'Unknown type in question.getRightWrong!' );
			 break;
	}

   // Als er niet gedeeltelijk mag worden gescoord is een gedeeltelijk goed antwoord helemaal fout
   if ( !cv.partscore && intResult == 2 ) intResult = -1;
   return intResult;
}
function bepaalAntwoordOrder( v, correct, lngAlternatieven )
{
   var resultaat = new Array(2);

   resultaat[0] = 0;
   resultaat[1] = lngAlternatieven * 2;
   if ( v.length != correct.length ) {
      alert( "Number of alternatives not the same for Classification question." );
      return;
   }

   var antwoord = new Array(correct.length);
   for (var i=0; i<correct.length; i++) {
      antwoord[ i ] = v[i];
   }

   var intPuntenGoed = 0;
   var intPuntenFout = 0;
   for (var i=0; i < correct.length; i++) {
      var intAltNr = correct[i];

      var intCorrectVoor=-1;
      for (var j=0; j<correct.length; j++) {
         var intCorrect = correct[j];
         if (intCorrect == parseInt(intAltNr)-1)
            intCorrectVoor = j;
      }

      var intCorrectNa=correct.length;
      for (var j=0; j<correct.length; j++) {
         var intCorrect = correct[j];
         if (intCorrect == parseInt(intAltNr)+1)
            intCorrectNa = j;
      }

      var intRangNr = antwoord[i];
      if (intRangNr > 0) { 
         var intPosVoor = -2; // Ongeldige waarde
         var intPosNa = -2; // Ongeldige waarde
         if (intRangNr == 1) intPosVoor = -1; // Eerste in de rij
         if (intRangNr == correct.length) intPosNa = correct.length; // Laatste in de rij

         for (var j=0; j<correct.length; j++) {
            var intTmpRangNr = antwoord[j];

            if (intTmpRangNr > 0 && intTmpRangNr <= correct.length) {
               if (intTmpRangNr == parseInt(intRangNr) - 1) intPosVoor = j;
               if (intTmpRangNr == parseInt(intRangNr) + 1) intPosNa = j;
            }
         }

         if (intPosVoor == intCorrectVoor) intPuntenGoed++;
         else intPuntenFout++;

         if (intPosNa == intCorrectNa) intPuntenGoed++;
         else intPuntenFout++;
      }
	 }

   resultaat[0] = intPuntenGoed;
   resultaat[1] = intPuntenFout;
   return resultaat;
}

function bepaalAntwoordMatch( v, vAntwoord)
{
   var aantalMatchGoed=0;
   var aantalMatchFout=0;
   var resultaat = new Array(2);
   var i
   for (i=0; i<v.length; i++) {
      var strAltNr = v[i];

      if ( strAltNr > 0 ) {
         if ( i < vAntwoord.length ) {
            var lngMatchNr = vAntwoord[i];
            if ( Math.abs(lngMatchNr) == Math.abs(strAltNr) ) aantalMatchGoed++;
            else aantalMatchFout++;
         }
         else aantalMatchFout++;
      }
   }
 
   resultaat[0] = aantalMatchGoed;
   resultaat[1] = aantalMatchFout;
   return resultaat;
}
function bepaalAntwoordAlternatief( v, vAntwoord, blnEmpty, lngAlternatieven)
{
   var resultaat;
   resultaat = new Array(2);
   var aantalAltGoed=0;
   var aantalAltFout=0;
   for ( var i=0; i<v.length && !blnEmpty; i++ ) {
      var strAltNr = v[i];
      if (ArrayContains(vAntwoord, strAltNr)) aantalAltGoed++;
      else aantalAltFout++;
   }

   if ( aantalAltGoed + aantalAltFout != lngAlternatieven ) {
      aantalAltFout += vAntwoord.length - aantalAltGoed;
      aantalAltGoed = lngAlternatieven - aantalAltFout;
   }

   resultaat[0] = aantalAltGoed;
   resultaat[1] = aantalAltFout;
   return resultaat;
}
function compareFillInAnswer( Antwoord, CorrectAntwoord )
{
   if (Antwoord.toUpperCase() == CorrectAntwoord.toUpperCase()) return true;
   return false;
}
function ArrayContains( v, s )
{
   var i;

   for(i=0;i<v.length;i++) {
      if (v[i]==s) return true;
   }
   return false;
}
function spaceTrimVoor(pStr) {
	if (pStr.charAt(0)==' ') {
		pStr=spaceTrimVoor(pStr.substring(1,pStr.length));
	}
	return pStr;
} 
function spaceTrimNa(pStr) {
	if (pStr.charAt(pStr.length-1)==' ') {
		pStr=spaceTrimNa(pStr.substring(0,pStr.length-1));
	}
	return pStr;
}

// Het this-object is het toets-object
// Het cv-object is het vraag-object
function getScore(cv,toetsObj)	{
	var Antwoord = cv.answer;
	var blnGedScoren = toetsObj.partscore; // controle toetsobject verwijzing
	var puntenGoed = toetsObj.points_good;
	var puntenFout = toetsObj.points_wrong;
	var bVersion5 = true // TV5 flag altijd true

	if (toetsObj.scorerule!=-1) {
		alert('Scorerule '+toetsObj.scorerule+' nog niet geïmplementeerd');
		var blnGedwongenRaden = true ; //  this.scorerule = -1 dan deze true
		var blnEigen = false //  this.scorerule = -1 dan deze false
	}
	else {
		var blnGedwongenRaden = true ; //  this.scorerule = -1 dan deze true
		var blnEigen = false //  this.scorerule = -1 dan deze false
	}

	// Deze basisklasse kan de score berekenen als niet gedeeltelijk hoeft
	// te worden gescoord.
	if (blnGedScoren == false || !cv.partscore || blnEigen) {
		 if ( !blnEigen ) {
				puntenGoed = 1;
				puntenFout = 1;
		 }
		 var rslt = getRightWrong(cv);
		 // Antwoord is goed
		 if (rslt > 0) return cv.weight * puntenGoed;
		 // Geen antwoord gegeven of gedwongen scoringsregel (zonder aftrek)
		 if (rslt == 0 || blnGedwongenRaden) return 0;
		 if (blnEigen) return -(cv.weight * puntenFout);
		 // Fout antwoord gegeven en correctie voor raden regel
		 return -getCorrectieAftrek( Antwoord );
	}
	return getGedeeltelijkeScore(cv, blnGedwongenRaden) * cv.weight;
}
// Deze routine wordt aangeroepen als er niet gedeeltelijk mag worden
// gescoord, het antwoord fout (of gedeeltelijk fout) is en er aftrek moet
// worden bepaald.
function getCorrectieAftrek( Antwoord ) {
/*
private double getCorrectieAftrek( String Antwoord, boolean bVersion5 ) throws Exception
   {
      double dblKansGoed = getKansGoed(false, bVersion5);

      if (dblKansGoed > 0 && dblKansGoed <= 0.99)
         return ( dblKansGoed / (1 - dblKansGoed) ) * (double)getWeging();
      else if (dblKansGoed > 0.99)
         return (double)getWeging();
      else
         return 0.0;
   }
*/
	alert('getCorrectieAftrek() nog niet beschikbaar');
	return Antwoord;
}
// bepaal de gedeeltelijke score van een gegeven antwoord.
// hierbij wordt geen rekening gehouden met een eventuele weging.
function getGedeeltelijkeScore(cv, blnGedwongenRaden) {
	var blnEmpty = false;
 	var dblScore = 0.0;
 	var Antwoord = cv.answer;
 	var cAntwoord = cv.cans;
 	var vraagsoort = cv.kind;
 	var lngAlternatieven = cv.nr_alt;
	var lngMatches = cv.nr_match;
 	var v = new Array();
	var vAntwoord = new Array();

  if ( Antwoord.length == 0 ) blnEmpty = true;
  if ( Antwoord=='dontknow' ) return 0.0;
  if ( Antwoord=='empty' ) blnEmpty = true;
	
	if (vraagsoort!='fillin' && vraagsoort!='fillinopen') {
	  v = Antwoord.split(',');
    vAntwoord = cAntwoord.split(',');
  }
	else { 
	  v[0] = Antwoord;
    vAntwoord[0] = cAntwoord;
	}

	switch (vraagsoort) {
		case 'multiplechoice': 
		case 'truefalse':
		case 'fillin':
		case 'hotspot':
      alert('Question type '+vraagsoort+' can not be partially scored!');
      dblScore = 0.0;
			break;
		case 'fillinopen':
      dblScore = 0.0;
			break;
		case 'hotspotmultiple':  // Letop in TV-scoreberekening hier een eigen afhandeling - nog checken
			alert('hotspotmultiple wordt gescoord als multiplechoice more - nog geen eigen scoringsafhandeling')
		case 'multiplechoicemore':
		case 'truefalsemultiple':
		case 'matchmatrix':
			var resultaatAlternatief;
			resultaatAlternatief=bepaalAntwoordAlternatief( v, vAntwoord, blnEmpty, lngAlternatieven);
			var aantalAltGoed= resultaatAlternatief[0];
			var aantalAltFout= resultaatAlternatief[1];
			var aantalCorrectAlt = vAntwoord.length;
        // TV 4.0 scoreberekening
				if (blnGedwongenRaden) {
					dblScore = (aantalAltGoed/lngAlternatieven);
				}
			 	else { // Correctie voor raden
					dblScore = ( (aantalAltGoed - aantalAltFout) / lngAlternatieven );
			 	}

				// TV 5.0 scoreberekening
			  // Score = aantalGoed / aantalGoedeAlternatieven - aantalFout/AantalAfleiders
			  // Bij gedwongen raden: Als de score negatief dan nul anders de berekende score.
			  /*
				dblScore = (aantalAltGoed/aantalCorrectAlt) - (aantalAltFout/(lngAlternatieven-aantalCorrectAlt));
			  if (blnGedwongenRaden) {
				 	if (dblScore < 0) dblScore = 0.0;
			  }
				*/
			 break;
		case 'hotspotmatch': // Letop in TV-scoreberekening hier een eigen afhandeling - nog checken
			alert('hotspotmatch wordt gescoord als matchone - nog geen eigen scoringsafhandeling')
		case 'matchone':
		case 'matchmany':
			var resultaatMatch;
			resultaatMatch = bepaalAntwoordMatch( v, vAntwoord);
	
			var aantalMatchGoed = resultaatMatch[0];
			var aantalMatchFout = resultaatMatch[1];
			if (blnGedwongenRaden) {
				dblScore = aantalMatchGoed/lngAlternatieven;
			}
			else { // Correctie voor raden
			 	dblScore = aantalMatchGoed/lngAlternatieven - aantalMatchFout/(lngAlternatieven*(lngMatches-1));
			}
			break;
		case 'classification':
			if (!blnEmpty) {
				var resultaatOrder;
				resultaatOrder = bepaalAntwoordOrder( v, vAntwoord, lngAlternatieven );
				var intPuntenGoed = resultaatOrder[0];
				var intPuntenFout = resultaatOrder[1];
				var dblMaxPunten = lngAlternatieven * 2.0;

			 	if (blnGedwongenRaden) {
					dblScore = intPuntenGoed/dblMaxPunten;
				}
				else {
					dblScore = intPuntenGoed/dblMaxPunten - intPuntenFout/(dblMaxPunten*(lngAlternatieven-1));
				}
			}
			break;
	}
  return dblScore;
}
//-->


