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 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 */ 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 32 */ 33 abstract class qtype_ddmarker_shape { 34 /** @var bool Indicates if there is an error */ 35 protected $error = false; 36 37 /** @var string The shape class prefix */ 38 protected static $classnameprefix = 'qtype_ddmarker_shape_'; 39 40 public function __construct($coordsstring) { 41 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 } 52 53 abstract protected function outlying_coords_to_test(); 54 55 /** 56 * Returns the center location of the shape. 57 * 58 * @return array X and Y location 59 */ 60 abstract public function center_point(); 61 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 } 76 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 } 97 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 } 113 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); 121 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 } 130 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 } 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 } 157 $shapearray['0'] = ''; 158 asort($shapearray); 159 return $shapearray; 160 } 161 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 } 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 */ 179 public static function create($shape, $coordsstring) { 180 $classname = (self::$classnameprefix).$shape; 181 return new $classname($coordsstring); 182 } 183 } 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 */ 192 class qtype_ddmarker_shape_rectangle extends qtype_ddmarker_shape { 193 /** @var int Width of shape */ 194 protected $width; 195 196 /** @var int Height of shape */ 197 protected $height; 198 199 /** @var int Left location */ 200 protected $xleft; 201 202 /** @var int Top location */ 203 protected $ytop; 204 205 public function __construct($coordsstring) { 206 $coordstring = preg_replace('!^\s*!', '', $coordsstring); 207 $coordstring = preg_replace('!\s*$!', '', $coordsstring); 208 $coordsstringparts = preg_split('!;!', $coordsstring); 209 210 if (count($coordsstringparts) > 2) { 211 $this->error = 'toomanysemicolons'; 212 213 } else if (count($coordsstringparts) < 2) { 214 $this->error = 'nosemicolons'; 215 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 } 237 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 } 250 } 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 */ 259 class qtype_ddmarker_shape_circle extends qtype_ddmarker_shape { 260 /** @var int X center */ 261 protected $xcentre; 262 263 /** @var int Y center */ 264 protected $ycentre; 265 266 /** @var int Radius of circle */ 267 protected $radius; 268 269 public function __construct($coordsstring) { 270 $coordstring = preg_replace('!\s!', '', $coordsstring); 271 $coordsstringparts = explode(';', $coordsstring); 272 273 if (count($coordsstringparts) > 2) { 274 $this->error = 'toomanysemicolons'; 275 276 } else if (count($coordsstringparts) < 2) { 277 $this->error = 'nosemicolons'; 278 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 } 288 289 if (!$this->is_only_numbers($this->xcentre, $this->ycentre, $this->radius)) { 290 $this->error = 'onlyusewholepositivenumbers'; 291 } 292 293 $this->xcentre = (int) $this->xcentre; 294 $this->ycentre = (int) $this->ycentre; 295 $this->radius = (int) $this->radius; 296 } 297 } 298 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 } 303 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 } 308 309 public function center_point() { 310 return array($this->xcentre, $this->ycentre); 311 } 312 } 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 */ 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; 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 { 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 } 380 381 protected function outlying_coords_to_test() { 382 return array($this->minxy, $this->maxxy); 383 } 384 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: 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]); 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]); 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 } 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 } 432 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 } 448 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 } 458 } 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 */ 467 class qtype_ddmarker_point { 468 /** @var int X location */ 469 public $x; 470 471 /** @var int Y location */ 472 public $y; 473 public function __construct($x, $y) { 474 $this->x = $x; 475 $this->y = $y; 476 } 477 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 } 484 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body