[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/question/type/ddmarker/ -> shapes.php (source)

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1