Merge branch 'MDL-70248-310' of https://github.com/HuongNV13/moodle into MOODLE_310_S...
[moodle.git] / question / type / ddmarker / shapes.php
CommitLineData
60c9d277
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 classes for dealing with shapes on the server side.
60c9d277 19 *
54b8ddda
TH
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 */
25
26
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
99c6577a
JP
32 */
33abstract class qtype_ddmarker_shape {
8bc1d28b 34 /** @var bool Indicates if there is an error */
6f789d3d
JP
35 protected $error = false;
36
8bc1d28b
EM
37 /** @var string The shape class prefix */
38 protected static $classnameprefix = 'qtype_ddmarker_shape_';
39
60c9d277
JP
40 public function __construct($coordsstring) {
41
42 }
43 public function inside_width_height($widthheight) {
44 foreach ($this->outlying_coords_to_test() as $coordsxy) {
00f09d8f
TH
45 if ($coordsxy[0] < 0 || $coordsxy[0] > $widthheight[0] ||
46 $coordsxy[1] < 0 || $coordsxy[1] > $widthheight[1]) {
60c9d277
JP
47 return false;
48 }
49 }
50 return true;
51 }
54b8ddda 52
60c9d277
JP
53 abstract protected function outlying_coords_to_test();
54
8bc1d28b
EM
55 /**
56 * Returns the center location of the shape.
57 *
58 * @return array X and Y location
59 */
bcbe321a
JP
60 abstract public function center_point();
61
8bc1d28b
EM
62 /**
63 * Test if all passed parameters consist of only numbers.
64 *
65 * @return bool True if only numbers
66 */
60c9d277
JP
67 protected function is_only_numbers() {
68 $args = func_get_args();
69 foreach ($args as $arg) {
54b8ddda 70 if (0 === preg_match('!^[0-9]+$!', $arg)) {
60c9d277
JP
71 return false;
72 }
73 }
74 return true;
75 }
76
8bc1d28b
EM
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 */
60c9d277 85 protected function is_point_in_bounding_box($pointxy, $xleftytop, $xrightybottom) {
0b4b0a7e 86 if ($pointxy[0] < $xleftytop[0]) {
60c9d277 87 return false;
0b4b0a7e 88 } else if ($pointxy[0] > $xrightybottom[0]) {
60c9d277 89 return false;
0b4b0a7e 90 } else if ($pointxy[1] < $xleftytop[1]) {
60c9d277 91 return false;
0b4b0a7e 92 } else if ($pointxy[1] > $xrightybottom[1]) {
60c9d277
JP
93 return false;
94 }
95 return true;
96 }
97
8bc1d28b
EM
98 /**
99 * Gets any coordinate error
100 *
101 * @return string|bool String of the error or false if there is no error
102 */
6f789d3d
JP
103 public function get_coords_interpreter_error() {
104 if ($this->error) {
105 $a = new stdClass();
6daf12fe 106 $a->shape = self::human_readable_name(true);
6f789d3d
JP
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 }
113
60c9d277 114 /**
8bc1d28b
EM
115 * Check if the location is within the shape.
116 *
60c9d277
JP
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);
64b3d6e8 121
8bc1d28b
EM
122 /**
123 * Returns the name of the shape.
124 *
125 * @return string
126 */
64b3d6e8 127 public static function name() {
6f789d3d 128 return substr(get_called_class(), strlen(self::$classnameprefix));
64b3d6e8
JP
129 }
130
8bc1d28b
EM
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 */
6daf12fe
JP
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');
64b3d6e8 143 }
6f789d3d
JP
144
145 public static function human_readable_coords_format() {
146 return get_string('shape_'.self::name().'_coords', 'qtype_ddmarker');
147 }
148
149
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 }
5d4b3421 157 $shapearray['0'] = '';
6f789d3d
JP
158 asort($shapearray);
159 return $shapearray;
160 }
8bc1d28b
EM
161
162 /**
163 * Checks if the passed shape exists.
164 *
165 * @param string $shape The shape name
166 * @return bool
167 */
6f789d3d
JP
168 public static function exists($shape) {
169 return class_exists((self::$classnameprefix).$shape);
170 }
8bc1d28b
EM
171
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 */
6f789d3d
JP
179 public static function create($shape, $coordsstring) {
180 $classname = (self::$classnameprefix).$shape;
181 return new $classname($coordsstring);
182 }
60c9d277 183}
54b8ddda
TH
184
185
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 */
60c9d277 192class qtype_ddmarker_shape_rectangle extends qtype_ddmarker_shape {
8bc1d28b 193 /** @var int Width of shape */
60c9d277 194 protected $width;
8bc1d28b
EM
195
196 /** @var int Height of shape */
60c9d277 197 protected $height;
8bc1d28b
EM
198
199 /** @var int Left location */
60c9d277 200 protected $xleft;
8bc1d28b
EM
201
202 /** @var int Top location */
60c9d277 203 protected $ytop;
54b8ddda 204
60c9d277 205 public function __construct($coordsstring) {
64b3d6e8
JP
206 $coordstring = preg_replace('!^\s*!', '', $coordsstring);
207 $coordstring = preg_replace('!\s*$!', '', $coordsstring);
6f789d3d 208 $coordsstringparts = preg_split('!;!', $coordsstring);
54b8ddda 209
60c9d277 210 if (count($coordsstringparts) > 2) {
6f789d3d 211 $this->error = 'toomanysemicolons';
54b8ddda 212
60c9d277 213 } else if (count($coordsstringparts) < 2) {
6f789d3d 214 $this->error = 'nosemicolons';
54b8ddda 215
60c9d277
JP
216 } else {
217 $xy = explode(',', $coordsstringparts[0]);
6f789d3d 218 $widthheightparts = explode(',', $coordsstringparts[1]);
60c9d277
JP
219 if (count($xy) !== 2) {
220 $this->error = 'unrecognisedxypart';
221 } else if (count($widthheightparts) !== 2) {
222 $this->error = 'unrecognisedwidthheightpart';
223 } else {
54b8ddda
TH
224 $this->width = trim($widthheightparts[0]);
225 $this->height = trim($widthheightparts[1]);
226 $this->xleft = trim($xy[0]);
227 $this->ytop = trim($xy[1]);
60c9d277
JP
228 }
229 if (!$this->is_only_numbers($this->width, $this->height, $this->ytop, $this->xleft)) {
230 $this->error = 'onlyusewholepositivenumbers';
231 }
54b8ddda 232 $this->width = (int) $this->width;
60c9d277 233 $this->height = (int) $this->height;
54b8ddda
TH
234 $this->xleft = (int) $this->xleft;
235 $this->ytop = (int) $this->ytop;
60c9d277
JP
236 }
237
238 }
239 protected function outlying_coords_to_test() {
00f09d8f 240 return [[$this->xleft, $this->ytop], [$this->xleft + $this->width, $this->ytop + $this->height]];
60c9d277
JP
241 }
242 public function is_point_in_shape($xy) {
99c6577a
JP
243 return $this->is_point_in_bounding_box($xy, array($this->xleft, $this->ytop),
244 array($this->xleft + $this->width, $this->ytop + $this->height));
60c9d277 245 }
bcbe321a
JP
246 public function center_point() {
247 return array($this->xleft + round($this->width / 2),
248 $this->ytop + round($this->height / 2));
249 }
60c9d277 250}
54b8ddda
TH
251
252
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 */
60c9d277 259class qtype_ddmarker_shape_circle extends qtype_ddmarker_shape {
8bc1d28b 260 /** @var int X center */
60c9d277 261 protected $xcentre;
8bc1d28b
EM
262
263 /** @var int Y center */
60c9d277 264 protected $ycentre;
8bc1d28b
EM
265
266 /** @var int Radius of circle */
60c9d277
JP
267 protected $radius;
268
269 public function __construct($coordsstring) {
270 $coordstring = preg_replace('!\s!', '', $coordsstring);
271 $coordsstringparts = explode(';', $coordsstring);
54b8ddda 272
60c9d277
JP
273 if (count($coordsstringparts) > 2) {
274 $this->error = 'toomanysemicolons';
54b8ddda 275
60c9d277
JP
276 } else if (count($coordsstringparts) < 2) {
277 $this->error = 'nosemicolons';
54b8ddda 278
60c9d277
JP
279 } else {
280 $xy = explode(',', $coordsstringparts[0]);
281 if (count($xy) !== 2) {
282 $this->error = 'unrecognisedxypart';
283 } else {
54b8ddda
TH
284 $this->radius = trim($coordsstringparts[1]);
285 $this->xcentre = trim($xy[0]);
286 $this->ycentre = trim($xy[1]);
60c9d277 287 }
54b8ddda 288
60c9d277
JP
289 if (!$this->is_only_numbers($this->xcentre, $this->ycentre, $this->radius)) {
290 $this->error = 'onlyusewholepositivenumbers';
291 }
54b8ddda 292
60c9d277
JP
293 $this->xcentre = (int) $this->xcentre;
294 $this->ycentre = (int) $this->ycentre;
54b8ddda 295 $this->radius = (int) $this->radius;
60c9d277
JP
296 }
297 }
54b8ddda 298
60c9d277 299 protected function outlying_coords_to_test() {
00f09d8f
TH
300 return [[$this->xcentre - $this->radius, $this->ycentre - $this->radius],
301 [$this->xcentre + $this->radius, $this->ycentre + $this->radius]];
60c9d277 302 }
54b8ddda 303
60c9d277 304 public function is_point_in_shape($xy) {
f8bb5fdc 305 $distancefromcentre = sqrt(pow(($xy[0] - $this->xcentre), 2) + pow(($xy[1] - $this->ycentre), 2));
0b4b0a7e 306 return $distancefromcentre <= $this->radius;
60c9d277 307 }
54b8ddda 308
bcbe321a
JP
309 public function center_point() {
310 return array($this->xcentre, $this->ycentre);
311 }
60c9d277 312}
54b8ddda
TH
313
314
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 */
60c9d277
JP
321class 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;
334
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 {
ceae322f 342 $lastxy = null;
60c9d277
JP
343 foreach ($coordsstringparts as $coordsstringpart) {
344 $xy = explode(',', $coordsstringpart);
345 if (count($xy) !== 2) {
346 $this->error = 'unrecognisedxypart';
347 }
54b8ddda 348 if (!$this->is_only_numbers(trim($xy[0]), trim($xy[1]))) {
60c9d277
JP
349 $this->error = 'onlyusewholepositivenumbers';
350 }
351 $xy[0] = (int) $xy[0];
352 $xy[1] = (int) $xy[1];
ceae322f
TH
353 if ($lastxy !== null && $lastxy[0] == $xy[0] && $lastxy[1] == $xy[1]) {
354 $this->error = 'repeatedpoint';
355 }
60c9d277 356 $this->coords[] = $xy;
ceae322f 357 $lastxy = $xy;
60c9d277
JP
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 }
54b8ddda 373 // Make sure polygon is not closed.
f8bb5fdc
TH
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]);
60c9d277
JP
377 }
378 }
379 }
54b8ddda 380
60c9d277
JP
381 protected function outlying_coords_to_test() {
382 return array($this->minxy, $this->maxxy);
383 }
54b8ddda 384
60c9d277 385 public function is_point_in_shape($xy) {
0b4b0a7e
TH
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:
389
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.
396
397 $point = new qtype_ddmarker_point($xy[0], $xy[1]);
60c9d277
JP
398 $windingnumber = 0;
399 foreach ($this->coords as $index => $coord) {
0b4b0a7e
TH
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;
60c9d277 403 } else {
0b4b0a7e 404 $endindex = 0;
60c9d277 405 }
0b4b0a7e
TH
406 $end = new qtype_ddmarker_point($this->coords[$endindex][0], $this->coords[$endindex][1]);
407
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 }
60c9d277 417 }
0b4b0a7e
TH
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 }
60c9d277 427 }
60c9d277
JP
428 }
429 }
0b4b0a7e 430 return $windingnumber != 0;
60c9d277 431 }
54b8ddda 432
60c9d277 433 /**
0b4b0a7e
TH
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.
60c9d277 442 */
0b4b0a7e
TH
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);
60c9d277 447 }
0b4b0a7e 448
bcbe321a 449 public function center_point() {
f8bb5fdc
TH
450 $center = array(round(($this->minxy[0] + $this->maxxy[0]) / 2),
451 round(($this->minxy[1] + $this->maxxy[1]) / 2));
bcbe321a
JP
452 if ($this->is_point_in_shape($center)) {
453 return $center;
454 } else {
455 return null;
456 }
bcbe321a 457 }
60c9d277 458}
54b8ddda
TH
459
460
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 */
60c9d277 467class qtype_ddmarker_point {
8bc1d28b 468 /** @var int X location */
60c9d277 469 public $x;
8bc1d28b
EM
470
471 /** @var int Y location */
60c9d277
JP
472 public $y;
473 public function __construct($x, $y) {
474 $this->x = $x;
475 $this->y = $y;
476 }
54b8ddda 477
60c9d277 478 /**
54b8ddda
TH
479 * Return the distance between this point and another
480 */
481 public function dist($other) {
60c9d277
JP
482 return sqrt(pow($this->x - $other->x, 2) + pow($this->y - $other->y, 2));
483 }
484}