AutorÃa | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** Drag-and-drop markers question definition class.** @package qtype_ddmarker* @copyright 2012 The Open University* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/defined('MOODLE_INTERNAL') || die();require_once($CFG->dirroot . '/question/type/ddimageortext/questionbase.php');require_once($CFG->dirroot . '/question/type/ddmarker/shapes.php');/*** Represents a drag-and-drop markers question.** @copyright 2009 The Open University* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class qtype_ddmarker_question extends qtype_ddtoimage_question_base {public $showmisplaced;public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {if ($filearea == 'bgimage') {$validfilearea = true;} else {$validfilearea = false;}if ($component == 'qtype_ddmarker' && $validfilearea) {$question = $qa->get_question(false);$itemid = reset($args);return $itemid == $question->id;} else {return parent::check_file_access($qa, $options, $component,$filearea, $args, $forcedownload);}}/*** Get a choice identifier** @param int $choice stem number* @return string the question-type variable name.*/public function choice($choice) {return 'c' . $choice;}public function get_expected_data() {$vars = array();foreach ($this->choices[1] as $choice => $notused) {$vars[$this->choice($choice)] = PARAM_NOTAGS;}return $vars;}public function is_complete_response(array $response) {foreach ($this->choices[1] as $choiceno => $notused) {if (isset($response[$this->choice($choiceno)])&& '' != trim($response[$this->choice($choiceno)])) {return true;}}return false;}public function is_gradable_response(array $response) {return $this->is_complete_response($response);}public function is_same_response(array $prevresponse, array $newresponse) {foreach ($this->choices[1] as $choice => $notused) {$fieldname = $this->choice($choice);if (!$this->arrays_same_at_key_integer($prevresponse, $newresponse, $fieldname)) {return false;}}return true;}/*** Tests to see whether two arrays have the same set of coords at a particular key. Coords* can be in any order.* @param array $array1 the first array.* @param array $array2 the second array.* @param string $key an array key.* @return bool whether the two arrays have the same set of coords (or lack of them)* for a given key.*/public function arrays_same_at_key_integer(array $array1, array $array2, $key) {if (array_key_exists($key, $array1)) {$value1 = $array1[$key];} else {$value1 = '';}if (array_key_exists($key, $array2)) {$value2 = $array2[$key];} else {$value2 = '';}$coords1 = explode(';', $value1);$coords2 = explode(';', $value2);if (count($coords1) !== count($coords2)) {return false;} else if (count($coords1) === 0) {return true;} else {$valuesinbotharrays = $this->array_intersect_fixed($coords1, $coords2);return (count($valuesinbotharrays) == count($coords1));}}/**** This function is a variation of array_intersect that checks for the existence of duplicate* array values too.* @author dml at nm dot ru (taken from comments on php manual)* @param array $array1* @param array $array2* @return bool whether array1 and array2 contain the same values including duplicate values*/protected function array_intersect_fixed($array1, $array2) {$result = array();foreach ($array1 as $val) {if (($key = array_search($val, $array2, true)) !== false) {$result[] = $val;unset($array2[$key]);}}return $result;}public function get_validation_error(array $response) {if ($this->is_complete_response($response)) {return '';}return get_string('pleasedragatleastonemarker', 'qtype_ddmarker');}public function get_num_parts_right(array $response) {$chosenhits = $this->choose_hits($response);$divisor = max(count($this->rightchoices), $this->total_number_of_items_dragged($response));return array(count($chosenhits), $divisor);}/*** Choose hits to maximize grade where drop targets may have more than one hit and drop targets* can overlap.* @param array $response* @return array chosen hits*/protected function choose_hits(array $response) {$allhits = $this->get_all_hits($response);$chosenhits = array();foreach ($allhits as $placeno => $hits) {foreach ($hits as $itemno => $hit) {$choice = $this->get_right_choice_for($placeno);$choiceitem = "$choice $itemno";if (!in_array($choiceitem, $chosenhits)) {$chosenhits[$placeno] = $choiceitem;break;}}}return $chosenhits;}public function total_number_of_items_dragged(array $response) {$total = 0;foreach ($this->choiceorder[1] as $choice) {$choicekey = $this->choice($choice);if (array_key_exists($choicekey, $response) && trim($response[$choicekey] !== '')) {$total += count(explode(';', $response[$choicekey]));}}return $total;}/*** Get's an array of all hits on drop targets. Needs further processing to find which hits* to select in the general case that drop targets may have more than one hit and drop targets* can overlap.* @param array $response* @return array all hits*/protected function get_all_hits(array $response) {$hits = array();foreach ($this->places as $placeno => $place) {$rightchoice = $this->get_right_choice_for($placeno);$rightchoicekey = $this->choice($rightchoice);if (!array_key_exists($rightchoicekey, $response)) {continue;}$choicecoords = $response[$rightchoicekey];$coords = explode(';', $choicecoords);foreach ($coords as $itemno => $coord) {if (trim($coord) === '') {continue;}$pointxy = explode(',', $coord);$pointxy[0] = round($pointxy[0]);$pointxy[1] = round($pointxy[1]);if ($place->drop_hit($pointxy)) {if (!isset($hits[$placeno])) {$hits[$placeno] = array();}$hits[$placeno][$itemno] = $coord;}}}// Reverse sort in order of number of hits per place (if two or more// hits per place then we want to make sure hits do not hit elsewhere).$sortcomparison = function ($a1, $a2){return (count($a1) - count($a2));};uasort($hits, $sortcomparison);return $hits;}public function get_right_choice_for($place) {$group = $this->places[$place]->group;foreach ($this->choiceorder[$group] as $choicekey => $choiceid) {if ($this->rightchoices[$place] == $choiceid) {return $choicekey;}}return null;}public function grade_response(array $response) {list($right, $total) = $this->get_num_parts_right($response);$fraction = $right / $total;return array($fraction, question_state::graded_state_for_fraction($fraction));}public function compute_final_grade($responses, $totaltries) {$maxitemsdragged = 0;$wrongtries = array();foreach ($responses as $i => $response) {$maxitemsdragged = max($maxitemsdragged,$this->total_number_of_items_dragged($response));$hits = $this->choose_hits($response);foreach ($hits as $place => $choiceitem) {if (!isset($wrongtries[$place])) {$wrongtries[$place] = $i;}}foreach ($wrongtries as $place => $notused) {if (!isset($hits[$place])) {unset($wrongtries[$place]);}}}$numtries = count($responses);$numright = count($wrongtries);$penalty = array_sum($wrongtries) * $this->penalty;$grade = ($numright - $penalty) / (max($maxitemsdragged, count($this->places)));return $grade;}public function clear_wrong_from_response(array $response) {$hits = $this->choose_hits($response);$cleanedresponse = array();foreach ($response as $choicekey => $coords) {$choice = (int)substr($choicekey, 1);$choiceresponse = array();$coordparts = explode(';', $coords);foreach ($coordparts as $itemno => $coord) {if (in_array("$choice $itemno", $hits)) {$choiceresponse[] = $coord;}}$cleanedresponse[$choicekey] = join(';', $choiceresponse);}return $cleanedresponse;}public function get_wrong_drags(array $response) {$hits = $this->choose_hits($response);$wrong = array();foreach ($response as $choicekey => $coords) {$choice = (int)substr($choicekey, 1);if ($coords != '') {$coordparts = explode(';', $coords);foreach ($coordparts as $itemno => $coord) {if (!in_array("$choice $itemno", $hits)) {$wrong[] = $this->get_selected_choice(1, $choice)->text;}}}}return $wrong;}public function get_drop_zones_without_hit(array $response) {$hits = $this->choose_hits($response);$nohits = array();foreach ($this->places as $placeno => $place) {$choice = $this->get_right_choice_for($placeno);if (!isset($hits[$placeno])) {$nohit = new stdClass();$nohit->coords = $place->coords;$nohit->shape = $place->shape->name();$nohit->markertext = $this->choices[1][$this->choiceorder[1][$choice]]->text;$nohits[] = $nohit;}}return $nohits;}public function classify_response(array $response) {$parts = array();$hits = $this->choose_hits($response);foreach ($this->places as $placeno => $place) {if (isset($hits[$placeno])) {$shuffledchoiceno = $this->get_right_choice_for($placeno);$choice = $this->get_selected_choice(1, $shuffledchoiceno);$parts[$placeno] = new question_classified_response($choice->no,$choice->summarise(),1 / count($this->places));} else {$parts[$placeno] = question_classified_response::no_response();}}return $parts;}public function get_correct_response() {$responsecoords = array();foreach ($this->places as $placeno => $place) {$rightchoice = $this->get_right_choice_for($placeno);if ($rightchoice !== null) {$rightchoicekey = $this->choice($rightchoice);$correctcoords = $place->correct_coords();if ($correctcoords !== null) {if (!isset($responsecoords[$rightchoicekey])) {$responsecoords[$rightchoicekey] = array();}$responsecoords[$rightchoicekey][] = join(',', $correctcoords);}}}$response = array();foreach ($responsecoords as $choicekey => $coords) {$response[$choicekey] = join(';', $coords);}return $response;}public function get_right_answer_summary() {$placesummaries = array();foreach ($this->places as $placeno => $place) {$shuffledchoiceno = $this->get_right_choice_for($placeno);$choice = $this->get_selected_choice(1, $shuffledchoiceno);$placesummaries[] = '{'.$place->summarise().' -> '.$choice->summarise().'}';}return join(', ', $placesummaries);}public function summarise_response(array $response) {$hits = $this->choose_hits($response);$goodhits = array();foreach ($this->places as $placeno => $place) {if (isset($hits[$placeno])) {$shuffledchoiceno = $this->get_right_choice_for($placeno);$choice = $this->get_selected_choice(1, $shuffledchoiceno);$goodhits[] = "{".$place->summarise()." -> ". $choice->summarise(). "}";}}if (count($goodhits) == 0) {return null;}return implode(', ', $goodhits);}public function get_random_guess_score() {return null;}}/*** Represents one of the choices (draggable markers).** @copyright 2009 The Open University* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class qtype_ddmarker_drag_item {/** @var string Label for the drag item */public $text;/** @var int Number of the item */public $no;/** @var int Group of the item */public $infinite;/** @var int Number of drags */public $noofdrags;/*** Drag item object setup.** @param string $label The label text of the drag item* @param int $no Which number drag item this is* @param bool $infinite True if the item can be used an unlimited number of times* @param int $noofdrags*/public function __construct($label, $no, $infinite, $noofdrags) {$this->text = $label;$this->infinite = $infinite;$this->no = $no;$this->noofdrags = $noofdrags;}/*** Returns the group of this item.** @return int*/public function choice_group() {return 1;}/*** Creates summary text of for the drag item.** @return string*/public function summarise() {return $this->text;}}/*** Represents one of the places (drop zones).** @copyright 2009 The Open University* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class qtype_ddmarker_drop_zone {/** @var int Group of the item */public $group = 1;/** @var int Number of the item */public $no;/** @var object Shape of the item */public $shape;/** @var array Location of the item */public $coords;/*** Setup a drop zone object.** @param int $no Which number drop zone this is* @param int $shape Shape of the drop zone* @param array $coords Coordinates of the zone*/public function __construct($no, $shape, $coords) {$this->no = $no;$this->shape = qtype_ddmarker_shape::create($shape, $coords);$this->coords = $coords;}/*** Creates summary text of for the drop zone** @return string*/public function summarise() {return get_string('summariseplaceno', 'qtype_ddmarker', $this->no);}/*** Indicates if the it coordinates are in this drop zone.** @param array $xy Array of X and Y location* @return bool*/public function drop_hit($xy) {return $this->shape->is_point_in_shape($xy);}/*** Gets the center point of this zone** @return array X and Y location*/public function correct_coords() {return $this->shape->center_point();}}