Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
   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  }