AutorÃa | Ultima modificación | Ver Log |
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Drag-and-drop markers classes for dealing with shapes on the server side.
*
* @package qtype_ddmarker
* @copyright 2012 The Open University
* @author Jamie Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Base class to represent a shape.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class qtype_ddmarker_shape {
/** @var bool Indicates if there is an error */
protected $error = false;
/** @var string The shape class prefix */
protected static $classnameprefix = 'qtype_ddmarker_shape_';
public function __construct($coordsstring) {
}
public function inside_width_height($widthheight) {
foreach ($this->outlying_coords_to_test() as $coordsxy) {
if ($coordsxy[0] < 0 || $coordsxy[0] > $widthheight[0] ||
$coordsxy[1] < 0 || $coordsxy[1] > $widthheight[1]) {
return false;
}
}
return true;
}
abstract protected function outlying_coords_to_test();
/**
* Returns the center location of the shape.
*
* @return array X and Y location
*/
abstract public function center_point();
/**
* Test if all passed parameters consist of only numbers.
*
* @return bool True if only numbers
*/
protected function is_only_numbers() {
$args = func_get_args();
foreach ($args as $arg) {
if (0 === preg_match('!^[0-9]+$!', $arg)) {
return false;
}
}
return true;
}
/**
* Checks if the point is within the bounding box made by top left and bottom right
*
* @param array $pointxy Array of the point (x, y)
* @param array $xleftytop Top left point of bounding box
* @param array $xrightybottom Bottom left point of bounding box
* @return bool
*/
protected function is_point_in_bounding_box($pointxy, $xleftytop, $xrightybottom) {
if ($pointxy[0] < $xleftytop[0]) {
return false;
} else if ($pointxy[0] > $xrightybottom[0]) {
return false;
} else if ($pointxy[1] < $xleftytop[1]) {
return false;
} else if ($pointxy[1] > $xrightybottom[1]) {
return false;
}
return true;
}
/**
* Gets any coordinate error
*
* @return string|bool String of the error or false if there is no error
*/
public function get_coords_interpreter_error() {
if ($this->error) {
$a = new stdClass();
$a->shape = self::human_readable_name(true);
$a->coordsstring = self::human_readable_coords_format();
return get_string('formerror_'.$this->error, 'qtype_ddmarker', $a);
} else {
return false;
}
}
/**
* Check if the location is within the shape.
*
* @param array $xy $xy[0] is x, $xy[1] is y
* @return boolean is point inside shape
*/
abstract public function is_point_in_shape($xy);
/**
* Returns the name of the shape.
*
* @return string
*/
public static function name() {
return substr(get_called_class(), strlen(self::$classnameprefix));
}
/**
* Return a human readable name of the shape.
*
* @param bool $lowercase True if it should be lowercase.
* @return string
*/
public static function human_readable_name($lowercase = false) {
$stringid = 'shape_'.self::name();
if ($lowercase) {
$stringid .= '_lowercase';
}
return get_string($stringid, 'qtype_ddmarker');
}
public static function human_readable_coords_format() {
return get_string('shape_'.self::name().'_coords', 'qtype_ddmarker');
}
public static function shape_options() {
$grepexpression = '!^'.preg_quote(self::$classnameprefix, '!').'!';
$shapes = preg_grep($grepexpression, get_declared_classes());
$shapearray = array();
foreach ($shapes as $shape) {
$shapearray[$shape::name()] = $shape::human_readable_name();
}
$shapearray['0'] = '';
asort($shapearray);
return $shapearray;
}
/**
* Checks if the passed shape exists.
*
* @param string $shape The shape name
* @return bool
*/
public static function exists($shape) {
return class_exists((self::$classnameprefix).$shape);
}
/**
* Creates a new shape of the specified type.
*
* @param string $shape The shape to create
* @param string $coordsstring The string describing the coordinates
* @return object
*/
public static function create($shape, $coordsstring) {
$classname = (self::$classnameprefix).$shape;
return new $classname($coordsstring);
}
}
/**
* Class to represent a rectangle.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddmarker_shape_rectangle extends qtype_ddmarker_shape {
/** @var int Width of shape */
protected $width;
/** @var int Height of shape */
protected $height;
/** @var int Left location */
protected $xleft;
/** @var int Top location */
protected $ytop;
public function __construct($coordsstring) {
$coordstring = preg_replace('!^\s*!', '', $coordsstring);
$coordstring = preg_replace('!\s*$!', '', $coordsstring);
$coordsstringparts = preg_split('!;!', $coordsstring);
if (count($coordsstringparts) > 2) {
$this->error = 'toomanysemicolons';
} else if (count($coordsstringparts) < 2) {
$this->error = 'nosemicolons';
} else {
$xy = explode(',', $coordsstringparts[0]);
$widthheightparts = explode(',', $coordsstringparts[1]);
if (count($xy) !== 2) {
$this->error = 'unrecognisedxypart';
} else if (count($widthheightparts) !== 2) {
$this->error = 'unrecognisedwidthheightpart';
} else {
$this->width = trim($widthheightparts[0]);
$this->height = trim($widthheightparts[1]);
$this->xleft = trim($xy[0]);
$this->ytop = trim($xy[1]);
}
if (!$this->is_only_numbers($this->width, $this->height, $this->ytop, $this->xleft)) {
$this->error = 'onlyusewholepositivenumbers';
}
$this->width = (int) $this->width;
$this->height = (int) $this->height;
$this->xleft = (int) $this->xleft;
$this->ytop = (int) $this->ytop;
}
}
protected function outlying_coords_to_test() {
return [[$this->xleft, $this->ytop], [$this->xleft + $this->width, $this->ytop + $this->height]];
}
public function is_point_in_shape($xy) {
return $this->is_point_in_bounding_box($xy, array($this->xleft, $this->ytop),
array($this->xleft + $this->width, $this->ytop + $this->height));
}
public function center_point() {
return array($this->xleft + round($this->width / 2),
$this->ytop + round($this->height / 2));
}
}
/**
* Class to represent a circle.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddmarker_shape_circle extends qtype_ddmarker_shape {
/** @var int X center */
protected $xcentre;
/** @var int Y center */
protected $ycentre;
/** @var int Radius of circle */
protected $radius;
public function __construct($coordsstring) {
$coordstring = preg_replace('!\s!', '', $coordsstring);
$coordsstringparts = explode(';', $coordsstring);
if (count($coordsstringparts) > 2) {
$this->error = 'toomanysemicolons';
} else if (count($coordsstringparts) < 2) {
$this->error = 'nosemicolons';
} else {
$xy = explode(',', $coordsstringparts[0]);
if (count($xy) !== 2) {
$this->error = 'unrecognisedxypart';
} else {
$this->radius = trim($coordsstringparts[1]);
$this->xcentre = trim($xy[0]);
$this->ycentre = trim($xy[1]);
}
if (!$this->is_only_numbers($this->xcentre, $this->ycentre, $this->radius)) {
$this->error = 'onlyusewholepositivenumbers';
}
$this->xcentre = (int) $this->xcentre;
$this->ycentre = (int) $this->ycentre;
$this->radius = (int) $this->radius;
}
}
protected function outlying_coords_to_test() {
return [[$this->xcentre - $this->radius, $this->ycentre - $this->radius],
[$this->xcentre + $this->radius, $this->ycentre + $this->radius]];
}
public function is_point_in_shape($xy) {
$distancefromcentre = sqrt(pow(($xy[0] - $this->xcentre), 2) + pow(($xy[1] - $this->ycentre), 2));
return $distancefromcentre <= $this->radius;
}
public function center_point() {
return array($this->xcentre, $this->ycentre);
}
}
/**
* Class to represent a polygon.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddmarker_shape_polygon extends qtype_ddmarker_shape {
/**
* @var array Arrary of xy coords where xy coords are also in a two element array [x,y].
*/
public $coords;
/**
* @var array min x and y coords in a two element array [x,y].
*/
protected $minxy;
/**
* @var array max x and y coords in a two element array [x,y].
*/
protected $maxxy;
public function __construct($coordsstring) {
$this->coords = array();
$coordstring = preg_replace('!\s!', '', $coordsstring);
$coordsstringparts = explode(';', $coordsstring);
if (count($coordsstringparts) < 3) {
$this->error = 'polygonmusthaveatleastthreepoints';
} else {
$lastxy = null;
foreach ($coordsstringparts as $coordsstringpart) {
$xy = explode(',', $coordsstringpart);
if (count($xy) !== 2) {
$this->error = 'unrecognisedxypart';
}
if (!$this->is_only_numbers(trim($xy[0]), trim($xy[1]))) {
$this->error = 'onlyusewholepositivenumbers';
}
$xy[0] = (int) $xy[0];
$xy[1] = (int) $xy[1];
if ($lastxy !== null && $lastxy[0] == $xy[0] && $lastxy[1] == $xy[1]) {
$this->error = 'repeatedpoint';
}
$this->coords[] = $xy;
$lastxy = $xy;
if (isset($this->minxy)) {
$this->minxy[0] = min($this->minxy[0], $xy[0]);
$this->minxy[1] = min($this->minxy[1], $xy[1]);
} else {
$this->minxy[0] = $xy[0];
$this->minxy[1] = $xy[1];
}
if (isset($this->maxxy)) {
$this->maxxy[0] = max($this->maxxy[0], $xy[0]);
$this->maxxy[1] = max($this->maxxy[1], $xy[1]);
} else {
$this->maxxy[0] = $xy[0];
$this->maxxy[1] = $xy[1];
}
}
// Make sure polygon is not closed.
if ($this->coords[count($this->coords) - 1][0] == $this->coords[0][0] &&
$this->coords[count($this->coords) - 1][1] == $this->coords[0][1]) {
unset($this->coords[count($this->coords) - 1]);
}
}
}
protected function outlying_coords_to_test() {
return array($this->minxy, $this->maxxy);
}
public function is_point_in_shape($xy) {
// This code is based on the winding number algorithm from
// http://geomalgorithms.com/a03-_inclusion.html
// which comes with the following copyright notice:
// Copyright 2000 softSurfer, 2012 Dan Sunday
// This code may be freely used, distributed and modified for any purpose
// providing that this copyright notice is included with it.
// SoftSurfer makes no warranty for this code, and cannot be held
// liable for any real or imagined damage resulting from its use.
// Users of this code must verify correctness for their application.
$point = new qtype_ddmarker_point($xy[0], $xy[1]);
$windingnumber = 0;
foreach ($this->coords as $index => $coord) {
$start = new qtype_ddmarker_point($this->coords[$index][0], $this->coords[$index][1]);
if ($index < count($this->coords) - 1) {
$endindex = $index + 1;
} else {
$endindex = 0;
}
$end = new qtype_ddmarker_point($this->coords[$endindex][0], $this->coords[$endindex][1]);
if ($start->y <= $point->y) {
if ($end->y >= $point->y) { // An upward crossing.
$isleft = $this->is_left($start, $end, $point);
if ($isleft == 0) {
return true; // The point is on the line.
} else if ($isleft > 0) {
// A valid up intersect.
$windingnumber += 1;
}
}
} else {
if ($end->y <= $point->y) { // A downward crossing.
$isleft = $this->is_left($start, $end, $point);
if ($isleft == 0) {
return true; // The point is on the line.
} else if ($this->is_left($start, $end, $point) < 0) {
// A valid down intersect.
$windingnumber -= 1;
}
}
}
}
return $windingnumber != 0;
}
/**
* Tests if a point is left / on / right of an infinite line.
*
* @param qtype_ddmarker_point $start first of two points on the infinite line.
* @param qtype_ddmarker_point $end second of two points on the infinite line.
* @param qtype_ddmarker_point $point the oint to test.
* @return number > 0 if the point is left of the line.
* = 0 if the point is on the line.
* < 0 if the point is right of the line.
*/
protected function is_left(qtype_ddmarker_point $start, qtype_ddmarker_point $end,
qtype_ddmarker_point $point) {
return ($end->x - $start->x) * ($point->y - $start->y)
- ($point->x - $start->x) * ($end->y - $start->y);
}
public function center_point() {
$center = array(round(($this->minxy[0] + $this->maxxy[0]) / 2),
round(($this->minxy[1] + $this->maxxy[1]) / 2));
if ($this->is_point_in_shape($center)) {
return $center;
} else {
return null;
}
}
}
/**
* Class to represent a point.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddmarker_point {
/** @var int X location */
public $x;
/** @var int Y location */
public $y;
public function __construct($x, $y) {
$this->x = $x;
$this->y = $y;
}
/**
* Return the distance between this point and another
*/
public function dist($other) {
return sqrt(pow($this->x - $other->x, 2) + pow($this->y - $other->y, 2));
}
}