AutorÃa | Ultima modificación | Ver Log |
<?php
/**
* Class MatchingProcessor
*/
class MatchingProcessor extends TypeProcessor {
/**
* Pattern for separating between expressions.
*/
const EXPRESSION_SEPARATOR = '[,]';
/**
* Pattern for separating between matching elements
*/
const MATCH_SEPARATOR = '[.]';
/**
* Processes xAPI data and returns a human readable HTML report
*
* @inheritdoc
*/
function generateHTML($description, $crp, $response, $extras = NULL, $scoreSettings = NULL) {
// We need some style for our report
$this->setStyle('styles/matching.css');
$dropzones = $this->getDropzones($extras);
$draggables = $this->getDraggables($extras);
$mappedCRP = $this->mapPatternIDsToIndexes($crp[0],
$dropzones,
$draggables);
$mappedResponse = $this->mapPatternIDsToIndexes($response,
$dropzones,
$draggables);
if (empty($mappedCRP) && empty($mappedResponse)) {
return '';
}
$header = $this->generateHeader($description, $scoreSettings);
$tableHTML = $this->generateTable($mappedCRP,
$mappedResponse,
$dropzones,
$draggables
);
$container = '<div class="h5p-reporting-container h5p-matching-container">' .
$header . $tableHTML .
'</div>';
return $container;
}
/**
* Generate header element
*
* @param $description
* @param $scoreSettings
*
* @return string
*/
private function generateHeader($description, $scoreSettings) {
$descriptionHtml = $this->generateDescription($description);
$scoreHtml = $this->generateScoreHtml($scoreSettings);
return
"<div class='h5p-matching-header'>" .
$descriptionHtml . $scoreHtml .
"</div>";
}
/**
* Generate description element
*
* @param string $description
*
* @return string Description element as a string
*/
private function generateDescription($description) {
return
'<p class="h5p-reporting-description h5p-matching-task-description">' .
$description .
'</p>';
}
/**
* Create a map that links IDs from pattern to indexes in the droppable and
* draggable arrays.
*
* @param string $pattern
* @param array $dropzoneIds
* @param array $draggableIds
*
* @return array Pattern mapped to indexes instead of IDs
*/
function mapPatternIDsToIndexes($pattern, $dropzoneIds, $draggableIds) {
$mappedMatches = array();
if (empty($pattern)) {
return $mappedMatches;
}
$singlePatterns = explode(self::EXPRESSION_SEPARATOR, $pattern);
foreach($singlePatterns as $singlePattern) {
$matches = explode(self::MATCH_SEPARATOR, $singlePattern);
// ID does not necessarily map to index, so we must remap it
$dropzoneId = $this->findIndexOfItemWithId($dropzoneIds, $matches[0]);
$draggableId = $this->findIndexOfItemWithId($draggableIds, $matches[1]);
if (!isset($mappedMatches[$dropzoneId])) {
$mappedMatches[$dropzoneId] = array();
}
$mappedMatches[$dropzoneId][] = $draggableId;
}
return $mappedMatches;
}
/**
* Find id of an item with a given index inside given array
*
* @param array $haystack
* @param number $id
*
* @return number Id of mapped item
*/
function findIndexOfItemWithId($haystack, $id) {
return (isset($haystack[$id]) ? $haystack[$id]->id : NULL);
}
/**
* Generate table from user response, correct response pattern, dropzones and
* draggables
*
* @param array $mappedCRP
* @param array $mappedResponse
* @param array $dropzones
* @param array $draggables
*
* @return string Table element
*/
function generateTable($mappedCRP, $mappedResponse, $dropzones, $draggables) {
$header = $this->generateTableHeader();
$rows = $this->generateRows($mappedCRP, $mappedResponse, $dropzones,
$draggables);
return '<table class="h5p-matching-table">' . $header . $rows . '</table>';
}
/**
* Generate rows of table
*
* @param array $mappedCRP
* @param array $mappedResponse
* @param array $dropzones
* @param array $draggables
*
* @return string HTML for generated table rows
*/
function generateRows($mappedCRP, $mappedResponse, $dropzones, $draggables) {
$html = '';
foreach($dropzones as $index => $value) {
$html .= $this->generateDropzoneRows($value,
$draggables,
isset($mappedCRP[$index]) ? $mappedCRP[$index] : array(),
isset($mappedResponse[$index]) ? $mappedResponse[$index] : array()
);
}
return $html;
}
/**
* Sort handler for comparing result rows
*
* @param stdClass $a
* @param stdClass $b
* @return int
*/
private static function rowcmp($a, $b) {
if ($a->isCorrect && $b->isCorrect || !$a->isCorrect && !$b->isCorrect) {
return strcmp($a->response, $b->response);
}
if ($a->isCorrect && !$b->isCorrect) {
return -1;
}
if (!$a->isCorrect && $b->isCorrect) {
return 1;
}
}
/**
* Creates the inital set of rows needed when generating the table HTML
*
* @param array $draggables
* @param array $crp
* @param array $response
* @return array
*/
private static function createUserAnswerRows(&$draggables, &$crp, &$response) {
// Create list with all rows to display
$rows = array();
foreach ($response as $key => $answer) {
$row = (object) array(
'isCorrect' => in_array($answer, $crp),
'crp' => '',
);
// Locate response label
foreach ($draggables as $draggable) {
if ($draggable->id === $answer) {
$row->response = $draggable->value;
break;
}
}
$rows[] = $row;
}
// Sort rows
usort($rows, array('MatchingProcessor', 'rowcmp'));
return $rows;
}
/**
* Creates a list of soluton labels.
*
* @param array $draggables
* @param array $crp
* @return array
*/
private static function createSolutionRows(&$draggables, &$crp) {
// Create list of solution labels
$solutions = array();
foreach ($crp as $pattern) {
$solutions[] = $draggables[$pattern]->value;
}
sort($solutions);
return $solutions;
}
/**
* Puts the solutions labels into the approperiate rows.
*
* @param array $rows
* @param array $solutions
*/
private static function addSolutionsToRows(&$rows, &$solutions) {
// Add solution labels to rows
foreach ($rows as $key => &$row) {
if (empty($solutions)) {
break; // All solutions have been added
}
if ($row->isCorrect) {
// Add solution if hasn't been added yet
$index = array_search($row->response, $solutions);
if ($index !== FALSE) {
$row->crp = $solutions[$index];
unset($solutions[$index]); // Prevent adding multiple times
}
}
else {
// Add the next solution
$row->crp = array_shift($solutions);
}
}
// In case we still have some solutions left, add extra rows for them
foreach ($solutions as $solution) {
$rows[] = (object) array(
'isCorrect' => FALSE,
'crp' => $solution,
'response' => '',
);
}
}
/**
* Generate row for a single dropzone and populate it with correct answers and
* user answers
*
* @param object $dropzone
* @param array $draggables
* @param array $crp
* @param array $response
*
* @return string Drop zone rows element
*/
function generateDropzoneRows($dropzone, $draggables, $crp, $response) {
if (!count($response) && !count($crp)) {
return ''; // Skip if no correct or user answers
}
// Get rows needed to display user answers
$rows = self::createUserAnswerRows($draggables, $crp, $response);
// Get correct solutions labels for the task
$solutions = self::createSolutionRows($draggables, $crp);
// Merges the solutions into the correct rows
self::addSolutionsToRows($rows, $solutions);
// Ready to generate the HTML
$rowsHtml = '';
$lastCellInRow = 'h5p-matching-last-cell-in-row';
$numRows = count($rows);
foreach ($rows as $key => &$row) {
$rowHtml = '';
$tdClass = ($key >= $numRows - 1 ? $lastCellInRow : '');
if ($key === 0) {
// Print Drop Zone
$rowHtml .=
'<th class="' . 'h5p-matching-dropzone ' . $lastCellInRow . '"' .
' rowspan="' . $numRows . '"' .
'>' .
$dropzone->value .
'</th>';
}
// Add correct response pattern
$rowHtml .= '<td class="' . $tdClass . '">' .
$row->crp .
'</td>';
// Add user reponse
$correctClass = ($row->isCorrect ? 'h5p-matching-draggable-correct' : 'h5p-matching-draggable-wrong');
$classes = $tdClass . ($tdClass !== '' && $correctClass !== '' ? ' ' : '') . ($row->response !== '' ? $correctClass : '');
$rowHtml .= '<td class="' . $classes . '">' .
$row->response .
'</td>';
$rowsHtml .= '<tr>' . $rowHtml . '</tr>';
}
return $rowsHtml;
}
/**
* Generate table header
*
* @return string Table header element as a string
*/
function generateTableHeader() {
// Empty first item
$html = '<th class="h5p-matching-header-dropzone">Dropzone</th>' .
'<th class="h5p-matching-header-correct">Correct Answers</th>' .
'<th class="h5p-matching-header-user">Your answers</th>';
return '<tr class="h5p-matching-table-heading">' . $html . '</tr>';
}
/**
* Extract drop zones from extras parameters
*
* @param object $extras
*
* @return array Drop zones
*/
function getDropzones($extras) {
$dropzones = array();
foreach($extras->target as $value) {
$dropzones[] = (object) array(
'id' => $value->id,
'value' => $value->description->{'en-US'}
);
}
return $dropzones;
}
/**
* Extract draggables from extras parameters
*
* @param object $extras
*
* @return array Draggables
*/
function getDraggables($extras) {
$draggables = array();
foreach($extras->source as $value) {
$draggables[] = (object) array(
'id' => $value->id,
'value' => $value->description->{'en-US'}
);
}
return $draggables;
}
}