Merge branch 'MDL-70248-310' of https://github.com/HuongNV13/moodle into MOODLE_310_S...
[moodle.git] / question / type / ddmarker / question.php
CommitLineData
b3878dbd
JP
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/**
2a71bf53 18 * Drag-and-drop markers question definition class.
b3878dbd 19 *
f8bb5fdc 20 * @package qtype_ddmarker
b409f199 21 * @copyright 2012 The Open University
b3878dbd
JP
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25
26defined('MOODLE_INTERNAL') || die();
27
dda954a2 28require_once($CFG->dirroot . '/question/type/ddimageortext/questionbase.php');
8aae74a4 29require_once($CFG->dirroot . '/question/type/ddmarker/shapes.php');
b3878dbd
JP
30
31
32/**
2a71bf53 33 * Represents a drag-and-drop markers question.
b3878dbd
JP
34 *
35 * @copyright 2009 The Open University
36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37 */
dda954a2 38class qtype_ddmarker_question extends qtype_ddtoimage_question_base {
43964253
JP
39
40 public $showmisplaced;
41
ddbea1ae
JP
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) {
64207dab 49 $question = $qa->get_question(false);
ddbea1ae
JP
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 }
a98db2a9 57 /**
8bc1d28b
EM
58 * Get a choice identifier
59 *
60 * @param int $choice stem number
a98db2a9
JP
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();
33164c04
JP
69 foreach ($this->choices[1] as $choice => $notused) {
70 $vars[$this->choice($choice)] = PARAM_NOTAGS;
a98db2a9
JP
71 }
72 return $vars;
73 }
33164c04 74 public function is_complete_response(array $response) {
6d6cda89
JP
75 foreach ($this->choices[1] as $choiceno => $notused) {
76 if (isset($response[$this->choice($choiceno)])
77 && '' != trim($response[$this->choice($choiceno)])) {
33164c04
JP
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);
cf25df7a 119 if (count($coords1) !== count($coords2)) {
33164c04
JP
120 return false;
121 } else if (count($coords1) === 0) {
122 return true;
123 } else {
317caf11 124 $valuesinbotharrays = $this->array_intersect_fixed($coords1, $coords2);
cf25df7a 125 return (count($valuesinbotharrays) == count($coords1));
33164c04
JP
126 }
127 }
317caf11
JP
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) {
5263d6d7
TH
141 if (($key = array_search($val, $array2, true)) !== false) {
142 $result[] = $val;
143 unset($array2[$key]);
144 }
317caf11
JP
145 }
146 return $result;
147 }
148
149
33164c04
JP
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 }
5263d6d7 156
33164c04
JP
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 }
5263d6d7 162
33164c04
JP
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);
2d391d1b
TH
217 $pointxy[0] = round($pointxy[0]);
218 $pointxy[1] = round($pointxy[1]);
33164c04
JP
219 if ($place->drop_hit($pointxy)) {
220 if (!isset($hits[$placeno])) {
221 $hits[$placeno] = array();
222 }
223 $hits[$placeno][$itemno] = $coord;
224 }
225 }
226 }
5263d6d7
TH
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).
33164c04
JP
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 }
bcbe321a 243 return null;
33164c04
JP
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;
7792acc0 253 $wrongtries = array();
33164c04
JP
254 foreach ($responses as $i => $response) {
255 $maxitemsdragged = max($maxitemsdragged,
256 $this->total_number_of_items_dragged($response));
257 $hits = $this->choose_hits($response);
33164c04 258 foreach ($hits as $place => $choiceitem) {
7792acc0
JP
259 if (!isset($wrongtries[$place])) {
260 $wrongtries[$place] = $i;
33164c04
JP
261 }
262 }
7792acc0 263 foreach ($wrongtries as $place => $notused) {
33164c04 264 if (!isset($hits[$place])) {
7792acc0 265 unset($wrongtries[$place]);
33164c04
JP
266 }
267 }
268 }
269 $numtries = count($responses);
7792acc0
JP
270 $numright = count($wrongtries);
271 $penalty = array_sum($wrongtries) * $this->penalty;
272 $grade = ($numright - $penalty) / (max($maxitemsdragged, count($this->places)));
33164c04
JP
273 return $grade;
274 }
b1d1ae50
JP
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 }
70c434ee
JP
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 != '') {
5263d6d7 298 $coordparts = explode(';', $coords);
70c434ee
JP
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
e47c7eae
JP
310 public function get_drop_zones_without_hit(array $response) {
311 $hits = $this->choose_hits($response);
33164c04 312
e47c7eae
JP
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();
1e83b54f 320 $nohit->markertext = $this->choices[1][$this->choiceorder[1][$choice]]->text;
e47c7eae
JP
321 $nohits[] = $nohit;
322 }
323 }
324 return $nohits;
325 }
e47c7eae 326
33164c04
JP
327 public function classify_response(array $response) {
328 $parts = array();
597a696a
JP
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);
14230fd6 333 $choice = $this->get_selected_choice(1, $shuffledchoiceno);
597a696a 334 $parts[$placeno] = new question_classified_response(
14230fd6
JP
335 $choice->no,
336 $choice->summarise(),
337 1 / count($this->places));
597a696a
JP
338 } else {
339 $parts[$placeno] = question_classified_response::no_response();
33164c04 340 }
33164c04
JP
341 }
342 return $parts;
343 }
bcbe321a
JP
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 }
597a696a
JP
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])) {
6d6cda89
JP
382 $shuffledchoiceno = $this->get_right_choice_for($placeno);
383 $choice = $this->get_selected_choice(1, $shuffledchoiceno);
597a696a
JP
384 $goodhits[] = "{".$place->summarise()." -> ". $choice->summarise(). "}";
385 }
386 }
bd3406ad 387 if (count($goodhits) == 0) {
597a696a
JP
388 return null;
389 }
390 return implode(', ', $goodhits);
391 }
0aa089c7
JP
392
393 public function get_random_guess_score() {
394 return null;
395 }
b3878dbd
JP
396}
397
b3878dbd 398/**
dda954a2 399 * Represents one of the choices (draggable markers).
b3878dbd
JP
400 *
401 * @copyright 2009 The Open University
402 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
403 */
404class qtype_ddmarker_drag_item {
8bc1d28b 405 /** @var string Label for the drag item */
b3878dbd 406 public $text;
8bc1d28b
EM
407
408 /** @var int Number of the item */
b3878dbd 409 public $no;
8bc1d28b
EM
410
411 /** @var int Group of the item */
ddbea1ae 412 public $infinite;
8bc1d28b
EM
413
414 /** @var int Number of drags */
e1f5f601 415 public $noofdrags;
b3878dbd 416
8bc1d28b
EM
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 */
e1f5f601 425 public function __construct($label, $no, $infinite, $noofdrags) {
dda954a2 426 $this->text = $label;
ddbea1ae 427 $this->infinite = $infinite;
b3878dbd 428 $this->no = $no;
e1f5f601 429 $this->noofdrags = $noofdrags;
b3878dbd 430 }
8bc1d28b
EM
431
432 /**
433 * Returns the group of this item.
434 *
435 * @return int
436 */
b3878dbd 437 public function choice_group() {
dda954a2 438 return 1;
b3878dbd
JP
439 }
440
8bc1d28b
EM
441 /**
442 * Creates summary text of for the drag item.
443 *
444 * @return string
445 */
b3878dbd 446 public function summarise() {
dda954a2 447 return $this->text;
b3878dbd
JP
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 */
456class qtype_ddmarker_drop_zone {
8bc1d28b 457 /** @var int Group of the item */
ddbea1ae 458 public $group = 1;
8bc1d28b
EM
459
460 /** @var int Number of the item */
b3878dbd 461 public $no;
8bc1d28b
EM
462
463 /** @var object Shape of the item */
dda954a2 464 public $shape;
8bc1d28b
EM
465
466 /** @var array Location of the item */
dda954a2 467 public $coords;
b3878dbd 468
8bc1d28b
EM
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 */
ddbea1ae 476 public function __construct($no, $shape, $coords) {
b3878dbd 477 $this->no = $no;
33164c04 478 $this->shape = qtype_ddmarker_shape::create($shape, $coords);
dda954a2 479 $this->coords = $coords;
b3878dbd
JP
480 }
481
8bc1d28b
EM
482 /**
483 * Creates summary text of for the drop zone
484 *
485 * @return string
486 */
b3878dbd 487 public function summarise() {
ddbea1ae 488 return get_string('summariseplaceno', 'qtype_ddmarker', $this->no);
b3878dbd 489 }
33164c04 490
8bc1d28b
EM
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 */
33164c04
JP
497 public function drop_hit($xy) {
498 return $this->shape->is_point_in_shape($xy);
499 }
bcbe321a 500
8bc1d28b
EM
501 /**
502 * Gets the center point of this zone
503 *
504 * @return array X and Y location
505 */
bcbe321a
JP
506 public function correct_coords() {
507 return $this->shape->center_point();
508 }
8aae74a4 509}