<?php
/**
* Author: Marc Ermshaus <marc@ermshaus.org>
* License: Free to use (at own risk).
*/
/**
*
*/
interface MatrixInterface
{
public function get($row, $column);
public function set($row, $column, $value);
public function getColumnCount();
public function getRowCount();
public function multiply(MatrixInterface $matrix);
}
/**
*
*/
class MatrixFactory
{
/**
*
* @param float $dx
* @param float $dy
* @return Matrix
*/
public function createTranslationMatrix($dx, $dy)
{
$m = new Matrix(3, 3);
$m->set(0, 0, 1); $m->set(0, 1, 0); $m->set(0, 2, $dx);
$m->set(1, 0, 0); $m->set(1, 1, 1); $m->set(1, 2, $dy);
$m->set(2, 0, 0); $m->set(2, 1, 0); $m->set(2, 2, 1);
return $m;
}
/**
*
* @param float $radiant
* @param Vector2D $rotationCenter
* @return Matrix
*/
public function createRotationMatrix($radiant, Vector2D $rotationCenter)
{
$m = $this->createTranslationMatrix($rotationCenter->getX(), $rotationCenter->getY());
$n = new Matrix(3, 3);
$n->set(0, 0, cos($radiant));
$n->set(0, 1, -sin($radiant));
$n->set(0, 2, 0);
$n->set(1, 0, sin($radiant));
$n->set(1, 1, cos($radiant));
$n->set(1, 2, 0);
$n->set(2, 0, 0);
$n->set(2, 1, 0);
$n->set(2, 2, 1);
$o = $this->createTranslationMatrix(-$rotationCenter->getX(), -$rotationCenter->getY());
return $m->multiply($n)->multiply($o);
}
}
/**
*
*/
class Matrix implements MatrixInterface
{
protected $rows;
protected $columns;
protected $data;
public function __construct($rows, $columns)
{
$this->rows = $rows;
$this->columns = $columns;
$this->data = array_fill(0, $rows, array_fill(0, $columns, 0));
}
public function set($row, $column, $value)
{
$this->data[$row][$column] = $value;
}
public function get($row, $column)
{
if (!isset($this->data[$row][$column])) {
throw new Exception();
}
return $this->data[$row][$column];
}
public function getRowCount()
{
return $this->rows;
}
public function getColumnCount()
{
return $this->columns;
}
public function multiply(MatrixInterface $matrix)
{
if ($this->columns !== $matrix->getRowCount()) {
throw new Exception();
}
$rows = $matrix->getRowCount();
$columns = $matrix->getColumnCount();
$resultMatrix = new Matrix($rows, $columns);
for ($row = 0; $row < $rows; $row++) {
for ($column = 0; $column < $columns; $column++) {
$sum = 0;
for ($k = 0; $k < $rows; $k++) {
$sum += $this->get($row, $k) * $matrix->get($k, $column);
}
$resultMatrix->set($row, $column, $sum);
}
}
return $resultMatrix;
}
}
/**
*
*/
class Vector2D
{
protected $x;
protected $y;
public function __construct($x, $y)
{
$this->x = $x;
$this->y = $y;
}
public function getX()
{
return $this->x;
}
public function setX($x)
{
$this->x = $x;
}
public function getY()
{
return $this->y;
}
public function setY($y)
{
$this->y = $y;
}
public function transform(MatrixInterface $matrix)
{
$res = $matrix->multiply($this->convertToMatrix());
$this->x = $res->get(0, 0);
$this->y = $res->get(1, 0);
}
public function convertToMatrix()
{
$matrix = new Matrix(3, 1);
$matrix->set(0, 0, $this->x);
$matrix->set(1, 0, $this->y);
$matrix->set(2, 0, 1);
return $matrix;
}
}
/**
*
*/
class TextDrawer
{
/**
*
* @var MatrixFactory
*/
protected $matrixFactory;
/**
*
* @param MatrixFactory $matrixFactory
*/
public function __construct(MatrixFactory $matrixFactory, Imagick $img)
{
$this->matrixFactory = $matrixFactory;
$this->img = $img;
}
/**
* Crossing number test for a point in a polygon
*
* This code is patterned after Franklin
* <http://www.ecse.rpi.edu/Homepages/wrf/research/geom/pnpoly.html>.
*
* Copyright 2000 softSurfer, 2012 Dan Sunday
*
* This code may be freely used 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.
*
* Source: <http://geomalgorithms.com/a03-_inclusion.html>
* (Ported to PHP by Marc Ermshaus.)
*
*
* @param Vector2D $P A point
* @param array $V Vertex points of a polygon V[n+1] with V[n]=V[0]
* @return int 0 = outside, 1 = inside
*/
protected function cn_PnPoly(Vector2D $P, array $V)
{
$n = count($V) - 1;
$cn = 0; // the crossing number counter
// loop through all edges of the polygon
for ($i=0; $i<$n; $i++) { // edge from V[i] to V[i+1]
if ((($V[$i]->getY() <= $P->getY()) && ($V[$i+1]->getY() > $P->getY())) // an upward crossing
|| (($V[$i]->getY() > $P->getY()) && ($V[$i+1]->getY() <= $P->getY()))) { // a downward crossing
// compute the actual edge-ray intersect x-coordinate
$vt = (float)($P->getY() - $V[$i]->getY()) / ($V[$i+1]->getY() - $V[$i]->getY());
if ($P->getX() < $V[$i]->getX() + $vt * ($V[$i+1]->getX() - $V[$i]->getX())) // P->getX() < intersect
++$cn; // a valid crossing of y=P->getY() right of P->getX()
}
}
return ($cn&1); // 0 if even (out), and 1 if odd (in)
}
/**
*
*
* Source: <http://stackoverflow.com/questions/1119451/how-to-tell-if-a-line-intersects-a-polygon-in-c/1120126#1120126>
*
* @param Vector2D $start1
* @param Vector2D $end1
* @param Vector2D $start2
* @param Vector2D $end2
* @return null|Vector2D
*/
protected function findLineIntersection(Vector2D $start1, Vector2D $end1, Vector2D $start2, Vector2D $end2)
{
$denom = (($end1->getX() - $start1->getX()) * ($end2->getY() - $start2->getY())) - (($end1->getY() - $start1->getY()) * ($end2->getX() - $start2->getX()));
// AB & CD are parallel
if ($denom == 0) {
return null;
}
$numer = (($start1->getY() - $start2->getY()) * ($end2->getX() - $start2->getX())) - (($start1->getX() - $start2->getX()) * ($end2->getY() - $start2->getY()));
$r = $numer / $denom;
$numer2 = (($start1->getY() - $start2->getY()) * ($end1->getX() - $start1->getX())) - (($start1->getX() - $start2->getX()) * ($end1->getY() - $start1->getY()));
$s = $numer2 / $denom;
if (($r < 0 || $r > 1) || ($s < 0 || $s > 1)) {
return null;
}
// Find intersection point
$result = new Vector2D(0, 0);
$result->setX($start1->getX() + ($r * ($end1->getX() - $start1->getX())));
$result->setY($start1->getY() + ($r * ($end1->getY() - $start1->getY())));
return $result;
}
/**
*
* @param array $points Array of Vector2D
* @return array
*/
protected function bigBoundingBox(array $points = array())
{
$x_min = null;
$y_min = null;
$x_max = null;
$y_max = null;
foreach ($points as $point) {
$x = $point->getX();
$y = $point->getY();
if ($x_min === null || $x < $x_min) {
$x_min = $x;
}
if ($x_max === null || $x > $x_max) {
$x_max = $x;
}
if ($y_min === null || $y < $y_min) {
$y_min = $y;
}
if ($y_max === null || $y > $y_max) {
$y_max = $y;
}
}
return array($x_min, $y_min, $x_max, $y_max);
}
/**
*
* @param Imagick $img
* @param ImagickDraw $draw
* @param int $width
* @param int $height
* @param string $text
* @return array
*/
private function macgyverSomeStuff(ImagickDraw $draw, $text, $angleRange, $fontRange)
{
$fontSize = rand($fontRange[0], $fontRange[1]);
$x = rand(0, $this->img->getImageWidth());
$y = rand(0, $this->img->getImageHeight());
$rotation = mt_rand($angleRange[0], $angleRange[1]);
$draw->setFontSize($fontSize);
$fontMetrics = $this->img->queryFontMetrics($draw, $text);
// Rotation um ($x, $y)
$m = $this->matrixFactory->createRotationMatrix(deg2rad($rotation), new Vector2D($x, $y));
$a = new Vector2D($x, $y);
$b = new Vector2D($x + $fontMetrics['textWidth'], $y);
$c = new Vector2D($x + $fontMetrics['textWidth'], $y + $fontMetrics['textHeight']);
$d = new Vector2D($x, $y + $fontMetrics['textHeight']);
$a->transform($m);
$b->transform($m);
$c->transform($m);
$d->transform($m);
// $x und $y so anpassen, dass Ausgabe komplett im sichtbaren Bereich
// liegt
list($x0, $y0, $x1, $y1) = $this->bigBoundingBox(array($a, $b, $c, $d));
$dx = 0.0;
$dy = 0.0;
if ($x0 < 0) {
$dx = -$x0;
}
if ($y0 < 0) {
$dy = -$y0;
}
if ($x1 > $this->img->getImageWidth()) {
$dx = $this->img->getImageWidth() - $x1;
}
if ($y1 > $this->img->getImageHeight()) {
$dy = $this->img->getImageHeight() - $y1;
}
$n = $this->matrixFactory->createTranslationMatrix($dx, $dy);
$a->transform($n);
$b->transform($n);
$c->transform($n);
$d->transform($n);
$x += $dx;
$y += $dy;
return array($x, $y, $fontSize, $rotation, array($a, $b, $c, $d, $a));
}
/**
*
* @param type $polygons
* @param type $test
* @return boolean
*/
protected function isOverlap($polygons, $test)
{
// Testen, ob ein Punkt von $poly1 in $poly2 liegt:
foreach ($polygons as $poly) {
foreach ($test as $point) {
if ($this->cn_PnPoly($point, $poly)) {
return true;
}
}
// Umgekehrt
foreach ($poly as $point) {
if ($this->cn_PnPoly($point, $test)) {
return true;
}
}
}
foreach ($polygons as $polygon) {
$c_poly = count($polygon);
$c_test = count($test);
// Für jede Kante aus $polygon...
for ($i = 0; $i < $c_poly - 1; $i++) {
// ...mit jeder Kante aus $test...
for ($j = 0; $j < $c_test - 1; $j++) {
$res = $this->findLineIntersection($polygon[$i], $polygon[$i+1], $test[$j], $test[$j+1]);
if ($res !== null) {
return true;
}
}
}
}
return false;
}
/**
*
* @param array $data
*/
public function positionTexts($data, array $angleRange, array $fontRange)
{
$draw = new ImagickDraw();
$draw->setGravity(Imagick::GRAVITY_NORTHWEST);
$polygons = array();
$polygonCounter = 0;
foreach ($data as $entry) {
do {
list($x, $y, $fontSize, $rotation, $polygon)
= $this->macgyverSomeStuff($draw, $entry, $angleRange, $fontRange);
$polygonCounter++;
} while ($this->isOverlap($polygons, $polygon));
// ---------------------------------------------------------------------
$draw->setStrokeColor(new ImagickPixel('green'));
$draw->setFillOpacity(0.0);
// Draw bounding box for texts
$c = count($polygon);
for ($i = 0; $i < $c - 1; $i++) {
$draw->line(
$polygon[$i]->getX(), $polygon[$i]->getY(),
$polygon[$i + 1]->getX(), $polygon[$i + 1]->getY()
);
}
// ---
$polygons[] = $polygon;
$draw->setStrokeColor('black');
$draw->setFontSize($fontSize);
$draw->setFillColor('black');
$draw->setFillOpacity(1.0);
$draw->setStrokeOpacity(1.0);
$this->img->annotateImage($draw, $x, $y, $rotation, $entry);
}
$draw->setFontSize(10);
$draw->setStrokeOpacity(0.0);
$draw->annotation(0, 0, 'Verworfene Polygone: ' . ($polygonCounter - count($data)));
$this->img->setImageFormat('jpg');
$this->img->drawimage($draw);
}
}
error_reporting(-1);
ini_set('display_errors', 1);
$texts = array(
'Borussia Dortmund',
'FC Bayern München',
'Hamburger SV',
'FC Schalke 04',
'FC Augsburg',
'VfB Stuttgart',
'Werder Bremen',
'Hannover 96',
'Bayer Leverkusen'
);
$img = new Imagick();
$img->newImage(640, 480, new ImagickPixel('white'));
$textDrawer = new TextDrawer(new MatrixFactory(), $img);
$textDrawer->positionTexts($texts, array(-50, 50), array(30, 60));
header('Content-Type: image/jpeg');
echo $img;