Merge branch 'MDL-70248-310' of https://github.com/HuongNV13/moodle into MOODLE_310_S...
[moodle.git] / question / type / ddmarker / shapes.php
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/>.
17 /**
18  * Drag-and-drop markers classes for dealing with shapes on the server side.
19  *
20  * @package   qtype_ddmarker
21  * @copyright 2012 The Open University
22  * @author    Jamie Pratt <me@jamiep.org>
23  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
27 /**
28  * Base class to represent a shape.
29  *
30  * @copyright 2012 The Open University
31  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32  */
33 abstract class qtype_ddmarker_shape {
34     /** @var bool Indicates if there is an error */
35     protected $error = false;
37     /** @var string The shape class prefix */
38     protected static $classnameprefix = 'qtype_ddmarker_shape_';
40     public function __construct($coordsstring) {
42     }
43     public function inside_width_height($widthheight) {
44         foreach ($this->outlying_coords_to_test() as $coordsxy) {
45             if ($coordsxy[0] < 0 || $coordsxy[0] > $widthheight[0] ||
46                     $coordsxy[1] < 0 || $coordsxy[1] > $widthheight[1]) {
47                 return false;
48             }
49         }
50         return true;
51     }
53     abstract protected function outlying_coords_to_test();
55     /**
56      * Returns the center location of the shape.
57      *
58      * @return array X and Y location
59      */
60     abstract public function center_point();
62     /**
63      * Test if all passed parameters consist of only numbers.
64      *
65      * @return bool True if only numbers
66      */
67     protected function is_only_numbers() {
68         $args = func_get_args();
69         foreach ($args as $arg) {
70             if (0 === preg_match('!^[0-9]+$!', $arg)) {
71                 return false;
72             }
73         }
74         return true;
75     }
77     /**
78      * Checks if the point is within the bounding box made by top left and bottom right
79      *
80      * @param array $pointxy Array of the point (x, y)
81      * @param array $xleftytop Top left point of bounding box
82      * @param array $xrightybottom Bottom left point of bounding box
83      * @return bool
84      */
85     protected function is_point_in_bounding_box($pointxy, $xleftytop, $xrightybottom) {
86         if ($pointxy[0] < $xleftytop[0]) {
87             return false;
88         } else if ($pointxy[0] > $xrightybottom[0]) {
89             return false;
90         } else if ($pointxy[1] < $xleftytop[1]) {
91             return false;
92         } else if ($pointxy[1] > $xrightybottom[1]) {
93             return false;
94         }
95         return true;
96     }
98     /**
99      * Gets any coordinate error
100      *
101      * @return string|bool String of the error or false if there is no error
102      */
103     public function get_coords_interpreter_error() {
104         if ($this->error) {
105             $a = new stdClass();
106             $a->shape = self::human_readable_name(true);
107             $a->coordsstring = self::human_readable_coords_format();
108             return get_string('formerror_'.$this->error, 'qtype_ddmarker', $a);
109         } else {
110             return false;
111         }
112     }
114     /**
115      * Check if the location is within the shape.
116      *
117      * @param array $xy $xy[0] is x, $xy[1] is y
118      * @return boolean is point inside shape
119      */
120     abstract public function is_point_in_shape($xy);
122     /**
123      * Returns the name of the shape.
124      *
125      * @return string
126      */
127     public static function name() {
128         return substr(get_called_class(), strlen(self::$classnameprefix));
129     }
131     /**
132      * Return a human readable name of the shape.
133      *
134      * @param bool $lowercase True if it should be lowercase.
135      * @return string
136      */
137     public static function human_readable_name($lowercase = false) {
138         $stringid = 'shape_'.self::name();
139         if ($lowercase) {
140             $stringid .= '_lowercase';
141         }
142         return get_string($stringid, 'qtype_ddmarker');
143     }
145     public static function human_readable_coords_format() {
146         return get_string('shape_'.self::name().'_coords', 'qtype_ddmarker');
147     }
150     public static function shape_options() {
151         $grepexpression = '!^'.preg_quote(self::$classnameprefix, '!').'!';
152         $shapes = preg_grep($grepexpression, get_declared_classes());
153         $shapearray = array();
154         foreach ($shapes as $shape) {
155             $shapearray[$shape::name()] = $shape::human_readable_name();
156         }
157         $shapearray['0'] = '';
158         asort($shapearray);
159         return $shapearray;
160     }
162     /**
163      * Checks if the passed shape exists.
164      *
165      * @param string $shape The shape name
166      * @return bool
167      */
168     public static function exists($shape) {
169         return class_exists((self::$classnameprefix).$shape);
170     }
172     /**
173      * Creates a new shape of the specified type.
174      *
175      * @param string $shape The shape to create
176      * @param string $coordsstring The string describing the coordinates
177      * @return object
178      */
179     public static function create($shape, $coordsstring) {
180         $classname = (self::$classnameprefix).$shape;
181         return new $classname($coordsstring);
182     }
186 /**
187  * Class to represent a rectangle.
188  *
189  * @copyright 2012 The Open University
190  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
191  */
192 class qtype_ddmarker_shape_rectangle extends qtype_ddmarker_shape {
193     /** @var int Width of shape */
194     protected $width;
196     /** @var int Height of shape */
197     protected $height;
199     /** @var int Left location */
200     protected $xleft;
202     /** @var int Top location */
203     protected $ytop;
205     public function __construct($coordsstring) {
206         $coordstring = preg_replace('!^\s*!', '', $coordsstring);
207         $coordstring = preg_replace('!\s*$!', '', $coordsstring);
208         $coordsstringparts = preg_split('!;!', $coordsstring);
210         if (count($coordsstringparts) > 2) {
211             $this->error = 'toomanysemicolons';
213         } else if (count($coordsstringparts) < 2) {
214             $this->error = 'nosemicolons';
216         } else {
217             $xy = explode(',', $coordsstringparts[0]);
218             $widthheightparts = explode(',', $coordsstringparts[1]);
219             if (count($xy) !== 2) {
220                 $this->error = 'unrecognisedxypart';
221             } else if (count($widthheightparts) !== 2) {
222                 $this->error = 'unrecognisedwidthheightpart';
223             } else {
224                 $this->width  = trim($widthheightparts[0]);
225                 $this->height = trim($widthheightparts[1]);
226                 $this->xleft  = trim($xy[0]);
227                 $this->ytop   = trim($xy[1]);
228             }
229             if (!$this->is_only_numbers($this->width, $this->height, $this->ytop, $this->xleft)) {
230                 $this->error = 'onlyusewholepositivenumbers';
231             }
232             $this->width  = (int) $this->width;
233             $this->height = (int) $this->height;
234             $this->xleft  = (int) $this->xleft;
235             $this->ytop   = (int) $this->ytop;
236         }
238     }
239     protected function outlying_coords_to_test() {
240         return [[$this->xleft, $this->ytop], [$this->xleft + $this->width, $this->ytop + $this->height]];
241     }
242     public function is_point_in_shape($xy) {
243         return $this->is_point_in_bounding_box($xy, array($this->xleft, $this->ytop),
244                                   array($this->xleft + $this->width, $this->ytop + $this->height));
245     }
246     public function center_point() {
247         return array($this->xleft + round($this->width / 2),
248                         $this->ytop + round($this->height / 2));
249     }
253 /**
254  * Class to represent a circle.
255  *
256  * @copyright 2012 The Open University
257  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
258  */
259 class qtype_ddmarker_shape_circle extends qtype_ddmarker_shape {
260     /** @var int X center */
261     protected $xcentre;
263     /** @var int Y center */
264     protected $ycentre;
266     /** @var int Radius of circle */
267     protected $radius;
269     public function __construct($coordsstring) {
270         $coordstring = preg_replace('!\s!', '', $coordsstring);
271         $coordsstringparts = explode(';', $coordsstring);
273         if (count($coordsstringparts) > 2) {
274             $this->error = 'toomanysemicolons';
276         } else if (count($coordsstringparts) < 2) {
277             $this->error = 'nosemicolons';
279         } else {
280             $xy = explode(',', $coordsstringparts[0]);
281             if (count($xy) !== 2) {
282                 $this->error = 'unrecognisedxypart';
283             } else {
284                 $this->radius = trim($coordsstringparts[1]);
285                 $this->xcentre = trim($xy[0]);
286                 $this->ycentre = trim($xy[1]);
287             }
289             if (!$this->is_only_numbers($this->xcentre, $this->ycentre, $this->radius)) {
290                 $this->error = 'onlyusewholepositivenumbers';
291             }
293             $this->xcentre = (int) $this->xcentre;
294             $this->ycentre = (int) $this->ycentre;
295             $this->radius  = (int) $this->radius;
296         }
297     }
299     protected function outlying_coords_to_test() {
300         return [[$this->xcentre - $this->radius, $this->ycentre - $this->radius],
301                 [$this->xcentre + $this->radius, $this->ycentre + $this->radius]];
302     }
304     public function is_point_in_shape($xy) {
305         $distancefromcentre = sqrt(pow(($xy[0] - $this->xcentre), 2) + pow(($xy[1] - $this->ycentre), 2));
306         return $distancefromcentre <= $this->radius;
307     }
309     public function center_point() {
310         return array($this->xcentre, $this->ycentre);
311     }
315 /**
316  * Class to represent a polygon.
317  *
318  * @copyright 2012 The Open University
319  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
320  */
321 class qtype_ddmarker_shape_polygon extends qtype_ddmarker_shape {
322     /**
323      * @var array Arrary of xy coords where xy coords are also in a two element array [x,y].
324      */
325     public $coords;
326     /**
327      * @var array min x and y coords in a two element array [x,y].
328      */
329     protected $minxy;
330     /**
331      * @var array max x and y coords in a two element array [x,y].
332      */
333     protected $maxxy;
335     public function __construct($coordsstring) {
336         $this->coords = array();
337         $coordstring = preg_replace('!\s!', '', $coordsstring);
338         $coordsstringparts = explode(';', $coordsstring);
339         if (count($coordsstringparts) < 3) {
340             $this->error = 'polygonmusthaveatleastthreepoints';
341         } else {
342             $lastxy = null;
343             foreach ($coordsstringparts as $coordsstringpart) {
344                 $xy = explode(',', $coordsstringpart);
345                 if (count($xy) !== 2) {
346                     $this->error = 'unrecognisedxypart';
347                 }
348                 if (!$this->is_only_numbers(trim($xy[0]), trim($xy[1]))) {
349                     $this->error = 'onlyusewholepositivenumbers';
350                 }
351                 $xy[0] = (int) $xy[0];
352                 $xy[1] = (int) $xy[1];
353                 if ($lastxy !== null && $lastxy[0] == $xy[0] && $lastxy[1] == $xy[1]) {
354                     $this->error = 'repeatedpoint';
355                 }
356                 $this->coords[] = $xy;
357                 $lastxy = $xy;
358                 if (isset($this->minxy)) {
359                     $this->minxy[0] = min($this->minxy[0], $xy[0]);
360                     $this->minxy[1] = min($this->minxy[1], $xy[1]);
361                 } else {
362                     $this->minxy[0] = $xy[0];
363                     $this->minxy[1] = $xy[1];
364                 }
365                 if (isset($this->maxxy)) {
366                     $this->maxxy[0] = max($this->maxxy[0], $xy[0]);
367                     $this->maxxy[1] = max($this->maxxy[1], $xy[1]);
368                 } else {
369                     $this->maxxy[0] = $xy[0];
370                     $this->maxxy[1] = $xy[1];
371                 }
372             }
373             // Make sure polygon is not closed.
374             if ($this->coords[count($this->coords) - 1][0] == $this->coords[0][0] &&
375                                 $this->coords[count($this->coords) - 1][1] == $this->coords[0][1]) {
376                 unset($this->coords[count($this->coords) - 1]);
377             }
378         }
379     }
381     protected function outlying_coords_to_test() {
382         return array($this->minxy, $this->maxxy);
383     }
385     public function is_point_in_shape($xy) {
386         // This code is based on the winding number algorithm from
387         // http://geomalgorithms.com/a03-_inclusion.html
388         // which comes with the following copyright notice:
390         // Copyright 2000 softSurfer, 2012 Dan Sunday
391         // This code may be freely used, distributed and modified for any purpose
392         // providing that this copyright notice is included with it.
393         // SoftSurfer makes no warranty for this code, and cannot be held
394         // liable for any real or imagined damage resulting from its use.
395         // Users of this code must verify correctness for their application.
397         $point = new qtype_ddmarker_point($xy[0], $xy[1]);
398         $windingnumber = 0;
399         foreach ($this->coords as $index => $coord) {
400             $start = new qtype_ddmarker_point($this->coords[$index][0], $this->coords[$index][1]);
401             if ($index < count($this->coords) - 1) {
402                 $endindex = $index + 1;
403             } else {
404                 $endindex = 0;
405             }
406             $end = new qtype_ddmarker_point($this->coords[$endindex][0], $this->coords[$endindex][1]);
408             if ($start->y <= $point->y) {
409                 if ($end->y >= $point->y) { // An upward crossing.
410                     $isleft = $this->is_left($start, $end, $point);
411                     if ($isleft == 0) {
412                         return true; // The point is on the line.
413                     } else if ($isleft > 0) {
414                         // A valid up intersect.
415                         $windingnumber += 1;
416                     }
417                 }
418             } else {
419                 if ($end->y <= $point->y) { // A downward crossing.
420                     $isleft = $this->is_left($start, $end, $point);
421                     if ($isleft == 0) {
422                         return true; // The point is on the line.
423                     } else if ($this->is_left($start, $end, $point) < 0) {
424                         // A valid down intersect.
425                         $windingnumber -= 1;
426                     }
427                 }
428             }
429         }
430         return $windingnumber != 0;
431     }
433     /**
434      * Tests if a point is left / on / right of an infinite line.
435      *
436      * @param qtype_ddmarker_point $start first of two points on the infinite line.
437      * @param qtype_ddmarker_point $end second of two points on the infinite line.
438      * @param qtype_ddmarker_point $point the oint to test.
439      * @return number > 0 if the point is left of the line.
440      *                 = 0 if the point is on the line.
441      *                 < 0 if the point is right of the line.
442      */
443     protected function is_left(qtype_ddmarker_point $start, qtype_ddmarker_point $end,
444             qtype_ddmarker_point $point) {
445         return ($end->x - $start->x) * ($point->y - $start->y)
446                 - ($point->x -  $start->x) * ($end->y - $start->y);
447     }
449     public function center_point() {
450         $center = array(round(($this->minxy[0] + $this->maxxy[0]) / 2),
451                         round(($this->minxy[1] + $this->maxxy[1]) / 2));
452         if ($this->is_point_in_shape($center)) {
453             return $center;
454         } else {
455             return null;
456         }
457     }
461 /**
462  * Class to represent a point.
463  *
464  * @copyright 2012 The Open University
465  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
466  */
467 class qtype_ddmarker_point {
468     /** @var int X location */
469     public $x;
471     /** @var int Y location */
472     public $y;
473     public function __construct($x, $y) {
474         $this->x = $x;
475         $this->y = $y;
476     }
478     /**
479      * Return the distance between this point and another
480      */
481     public function dist($other) {
482         return sqrt(pow($this->x - $other->x, 2) + pow($this->y - $other->y, 2));
483     }