1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Drag-and-drop markers question definition class. 19 * 20 * @package qtype_ddmarker 21 * @copyright 2012 The Open University 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 require_once($CFG->dirroot . '/question/type/ddimageortext/questionbase.php'); 29 require_once($CFG->dirroot . '/question/type/ddmarker/shapes.php'); 30 31 32 /** 33 * Represents a drag-and-drop markers question. 34 * 35 * @copyright 2009 The Open University 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 class qtype_ddmarker_question extends qtype_ddtoimage_question_base { 39 40 public $showmisplaced; 41 42 public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) { 43 if ($filearea == 'bgimage') { 44 $validfilearea = true; 45 } else { 46 $validfilearea = false; 47 } 48 if ($component == 'qtype_ddmarker' && $validfilearea) { 49 $question = $qa->get_question(false); 50 $itemid = reset($args); 51 return $itemid == $question->id; 52 } else { 53 return parent::check_file_access($qa, $options, $component, 54 $filearea, $args, $forcedownload); 55 } 56 } 57 /** 58 * Get a choice identifier 59 * 60 * @param int $choice stem number 61 * @return string the question-type variable name. 62 */ 63 public function choice($choice) { 64 return 'c' . $choice; 65 } 66 67 public function get_expected_data() { 68 $vars = array(); 69 foreach ($this->choices[1] as $choice => $notused) { 70 $vars[$this->choice($choice)] = PARAM_NOTAGS; 71 } 72 return $vars; 73 } 74 public function is_complete_response(array $response) { 75 foreach ($this->choices[1] as $choiceno => $notused) { 76 if (isset($response[$this->choice($choiceno)]) 77 && '' != trim($response[$this->choice($choiceno)])) { 78 return true; 79 } 80 } 81 return false; 82 } 83 public function is_gradable_response(array $response) { 84 return $this->is_complete_response($response); 85 } 86 public function is_same_response(array $prevresponse, array $newresponse) { 87 foreach ($this->choices[1] as $choice => $notused) { 88 $fieldname = $this->choice($choice); 89 if (!$this->arrays_same_at_key_integer( 90 $prevresponse, $newresponse, $fieldname)) { 91 return false; 92 } 93 } 94 return true; 95 } 96 /** 97 * Tests to see whether two arrays have the same set of coords at a particular key. Coords 98 * can be in any order. 99 * @param array $array1 the first array. 100 * @param array $array2 the second array. 101 * @param string $key an array key. 102 * @return bool whether the two arrays have the same set of coords (or lack of them) 103 * for a given key. 104 */ 105 public function arrays_same_at_key_integer( 106 array $array1, array $array2, $key) { 107 if (array_key_exists($key, $array1)) { 108 $value1 = $array1[$key]; 109 } else { 110 $value1 = ''; 111 } 112 if (array_key_exists($key, $array2)) { 113 $value2 = $array2[$key]; 114 } else { 115 $value2 = ''; 116 } 117 $coords1 = explode(';', $value1); 118 $coords2 = explode(';', $value2); 119 if (count($coords1) !== count($coords2)) { 120 return false; 121 } else if (count($coords1) === 0) { 122 return true; 123 } else { 124 $valuesinbotharrays = $this->array_intersect_fixed($coords1, $coords2); 125 return (count($valuesinbotharrays) == count($coords1)); 126 } 127 } 128 129 /** 130 * 131 * This function is a variation of array_intersect that checks for the existence of duplicate 132 * array values too. 133 * @author dml at nm dot ru (taken from comments on php manual) 134 * @param array $array1 135 * @param array $array2 136 * @return bool whether array1 and array2 contain the same values including duplicate values 137 */ 138 protected function array_intersect_fixed($array1, $array2) { 139 $result = array(); 140 foreach ($array1 as $val) { 141 if (($key = array_search($val, $array2, true)) !== false) { 142 $result[] = $val; 143 unset($array2[$key]); 144 } 145 } 146 return $result; 147 } 148 149 150 public function get_validation_error(array $response) { 151 if ($this->is_complete_response($response)) { 152 return ''; 153 } 154 return get_string('pleasedragatleastonemarker', 'qtype_ddmarker'); 155 } 156 157 public function get_num_parts_right(array $response) { 158 $chosenhits = $this->choose_hits($response); 159 $divisor = max(count($this->rightchoices), $this->total_number_of_items_dragged($response)); 160 return array(count($chosenhits), $divisor); 161 } 162 163 /** 164 * Choose hits to maximize grade where drop targets may have more than one hit and drop targets 165 * can overlap. 166 * @param array $response 167 * @return array chosen hits 168 */ 169 protected function choose_hits(array $response) { 170 $allhits = $this->get_all_hits($response); 171 $chosenhits = array(); 172 foreach ($allhits as $placeno => $hits) { 173 foreach ($hits as $itemno => $hit) { 174 $choice = $this->get_right_choice_for($placeno); 175 $choiceitem = "$choice $itemno"; 176 if (!in_array($choiceitem, $chosenhits)) { 177 $chosenhits[$placeno] = $choiceitem; 178 break; 179 } 180 } 181 } 182 return $chosenhits; 183 } 184 public function total_number_of_items_dragged(array $response) { 185 $total = 0; 186 foreach ($this->choiceorder[1] as $choice) { 187 $choicekey = $this->choice($choice); 188 if (array_key_exists($choicekey, $response) && trim($response[$choicekey] !== '')) { 189 $total += count(explode(';', $response[$choicekey])); 190 } 191 } 192 return $total; 193 } 194 195 /** 196 * Get's an array of all hits on drop targets. Needs further processing to find which hits 197 * to select in the general case that drop targets may have more than one hit and drop targets 198 * can overlap. 199 * @param array $response 200 * @return array all hits 201 */ 202 protected function get_all_hits(array $response) { 203 $hits = array(); 204 foreach ($this->places as $placeno => $place) { 205 $rightchoice = $this->get_right_choice_for($placeno); 206 $rightchoicekey = $this->choice($rightchoice); 207 if (!array_key_exists($rightchoicekey, $response)) { 208 continue; 209 } 210 $choicecoords = $response[$rightchoicekey]; 211 $coords = explode(';', $choicecoords); 212 foreach ($coords as $itemno => $coord) { 213 if (trim($coord) === '') { 214 continue; 215 } 216 $pointxy = explode(',', $coord); 217 $pointxy[0] = round($pointxy[0]); 218 $pointxy[1] = round($pointxy[1]); 219 if ($place->drop_hit($pointxy)) { 220 if (!isset($hits[$placeno])) { 221 $hits[$placeno] = array(); 222 } 223 $hits[$placeno][$itemno] = $coord; 224 } 225 } 226 } 227 // Reverse sort in order of number of hits per place (if two or more 228 // hits per place then we want to make sure hits do not hit elsewhere). 229 $sortcomparison = function ($a1, $a2){ 230 return (count($a1) - count($a2)); 231 }; 232 uasort($hits, $sortcomparison); 233 return $hits; 234 } 235 236 public function get_right_choice_for($place) { 237 $group = $this->places[$place]->group; 238 foreach ($this->choiceorder[$group] as $choicekey => $choiceid) { 239 if ($this->rightchoices[$place] == $choiceid) { 240 return $choicekey; 241 } 242 } 243 return null; 244 } 245 public function grade_response(array $response) { 246 list($right, $total) = $this->get_num_parts_right($response); 247 $fraction = $right / $total; 248 return array($fraction, question_state::graded_state_for_fraction($fraction)); 249 } 250 251 public function compute_final_grade($responses, $totaltries) { 252 $maxitemsdragged = 0; 253 $wrongtries = array(); 254 foreach ($responses as $i => $response) { 255 $maxitemsdragged = max($maxitemsdragged, 256 $this->total_number_of_items_dragged($response)); 257 $hits = $this->choose_hits($response); 258 foreach ($hits as $place => $choiceitem) { 259 if (!isset($wrongtries[$place])) { 260 $wrongtries[$place] = $i; 261 } 262 } 263 foreach ($wrongtries as $place => $notused) { 264 if (!isset($hits[$place])) { 265 unset($wrongtries[$place]); 266 } 267 } 268 } 269 $numtries = count($responses); 270 $numright = count($wrongtries); 271 $penalty = array_sum($wrongtries) * $this->penalty; 272 $grade = ($numright - $penalty) / (max($maxitemsdragged, count($this->places))); 273 return $grade; 274 } 275 public function clear_wrong_from_response(array $response) { 276 $hits = $this->choose_hits($response); 277 278 $cleanedresponse = array(); 279 foreach ($response as $choicekey => $coords) { 280 $choice = (int)substr($choicekey, 1); 281 $choiceresponse = array(); 282 $coordparts = explode(';', $coords); 283 foreach ($coordparts as $itemno => $coord) { 284 if (in_array("$choice $itemno", $hits)) { 285 $choiceresponse[] = $coord; 286 } 287 } 288 $cleanedresponse[$choicekey] = join(';', $choiceresponse); 289 } 290 return $cleanedresponse; 291 } 292 public function get_wrong_drags(array $response) { 293 $hits = $this->choose_hits($response); 294 $wrong = array(); 295 foreach ($response as $choicekey => $coords) { 296 $choice = (int)substr($choicekey, 1); 297 if ($coords != '') { 298 $coordparts = explode(';', $coords); 299 foreach ($coordparts as $itemno => $coord) { 300 if (!in_array("$choice $itemno", $hits)) { 301 $wrong[] = $this->get_selected_choice(1, $choice)->text; 302 } 303 } 304 } 305 } 306 return $wrong; 307 } 308 309 310 public function get_drop_zones_without_hit(array $response) { 311 $hits = $this->choose_hits($response); 312 313 $nohits = array(); 314 foreach ($this->places as $placeno => $place) { 315 $choice = $this->get_right_choice_for($placeno); 316 if (!isset($hits[$placeno])) { 317 $nohit = new stdClass(); 318 $nohit->coords = $place->coords; 319 $nohit->shape = $place->shape->name(); 320 $nohit->markertext = $this->choices[1][$this->choiceorder[1][$choice]]->text; 321 $nohits[] = $nohit; 322 } 323 } 324 return $nohits; 325 } 326 327 public function classify_response(array $response) { 328 $parts = array(); 329 $hits = $this->choose_hits($response); 330 foreach ($this->places as $placeno => $place) { 331 if (isset($hits[$placeno])) { 332 $shuffledchoiceno = $this->get_right_choice_for($placeno); 333 $choice = $this->get_selected_choice(1, $shuffledchoiceno); 334 $parts[$placeno] = new question_classified_response( 335 $choice->no, 336 $choice->summarise(), 337 1 / count($this->places)); 338 } else { 339 $parts[$placeno] = question_classified_response::no_response(); 340 } 341 } 342 return $parts; 343 } 344 345 public function get_correct_response() { 346 $responsecoords = array(); 347 foreach ($this->places as $placeno => $place) { 348 $rightchoice = $this->get_right_choice_for($placeno); 349 if ($rightchoice !== null) { 350 $rightchoicekey = $this->choice($rightchoice); 351 $correctcoords = $place->correct_coords(); 352 if ($correctcoords !== null) { 353 if (!isset($responsecoords[$rightchoicekey])) { 354 $responsecoords[$rightchoicekey] = array(); 355 } 356 $responsecoords[$rightchoicekey][] = join(',', $correctcoords); 357 } 358 } 359 } 360 $response = array(); 361 foreach ($responsecoords as $choicekey => $coords) { 362 $response[$choicekey] = join(';', $coords); 363 } 364 return $response; 365 } 366 367 public function get_right_answer_summary() { 368 $placesummaries = array(); 369 foreach ($this->places as $placeno => $place) { 370 $shuffledchoiceno = $this->get_right_choice_for($placeno); 371 $choice = $this->get_selected_choice(1, $shuffledchoiceno); 372 $placesummaries[] = '{'.$place->summarise().' -> '.$choice->summarise().'}'; 373 } 374 return join(', ', $placesummaries); 375 } 376 377 public function summarise_response(array $response) { 378 $hits = $this->choose_hits($response); 379 $goodhits = array(); 380 foreach ($this->places as $placeno => $place) { 381 if (isset($hits[$placeno])) { 382 $shuffledchoiceno = $this->get_right_choice_for($placeno); 383 $choice = $this->get_selected_choice(1, $shuffledchoiceno); 384 $goodhits[] = "{".$place->summarise()." -> ". $choice->summarise(). "}"; 385 } 386 } 387 if (count($goodhits) == 0) { 388 return null; 389 } 390 return implode(', ', $goodhits); 391 } 392 393 public function get_random_guess_score() { 394 return null; 395 } 396 } 397 398 /** 399 * Represents one of the choices (draggable markers). 400 * 401 * @copyright 2009 The Open University 402 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 403 */ 404 class qtype_ddmarker_drag_item { 405 /** @var string Label for the drag item */ 406 public $text; 407 408 /** @var int Number of the item */ 409 public $no; 410 411 /** @var int Group of the item */ 412 public $infinite; 413 414 /** @var int Number of drags */ 415 public $noofdrags; 416 417 /** 418 * Drag item object setup. 419 * 420 * @param string $label The label text of the drag item 421 * @param int $no Which number drag item this is 422 * @param bool $infinite True if the item can be used an unlimited number of times 423 * @param int $noofdrags 424 */ 425 public function __construct($label, $no, $infinite, $noofdrags) { 426 $this->text = $label; 427 $this->infinite = $infinite; 428 $this->no = $no; 429 $this->noofdrags = $noofdrags; 430 } 431 432 /** 433 * Returns the group of this item. 434 * 435 * @return int 436 */ 437 public function choice_group() { 438 return 1; 439 } 440 441 /** 442 * Creates summary text of for the drag item. 443 * 444 * @return string 445 */ 446 public function summarise() { 447 return $this->text; 448 } 449 } 450 /** 451 * Represents one of the places (drop zones). 452 * 453 * @copyright 2009 The Open University 454 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 455 */ 456 class qtype_ddmarker_drop_zone { 457 /** @var int Group of the item */ 458 public $group = 1; 459 460 /** @var int Number of the item */ 461 public $no; 462 463 /** @var object Shape of the item */ 464 public $shape; 465 466 /** @var array Location of the item */ 467 public $coords; 468 469 /** 470 * Setup a drop zone object. 471 * 472 * @param int $no Which number drop zone this is 473 * @param int $shape Shape of the drop zone 474 * @param array $coords Coordinates of the zone 475 */ 476 public function __construct($no, $shape, $coords) { 477 $this->no = $no; 478 $this->shape = qtype_ddmarker_shape::create($shape, $coords); 479 $this->coords = $coords; 480 } 481 482 /** 483 * Creates summary text of for the drop zone 484 * 485 * @return string 486 */ 487 public function summarise() { 488 return get_string('summariseplaceno', 'qtype_ddmarker', $this->no); 489 } 490 491 /** 492 * Indicates if the it coordinates are in this drop zone. 493 * 494 * @param array $xy Array of X and Y location 495 * @return bool 496 */ 497 public function drop_hit($xy) { 498 return $this->shape->is_point_in_shape($xy); 499 } 500 501 /** 502 * Gets the center point of this zone 503 * 504 * @return array X and Y location 505 */ 506 public function correct_coords() { 507 return $this->shape->center_point(); 508 } 509 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body