Location: PHPKode > projects > Comics chat > comicschat-0.2/comicschat.lib.php
<?php

/**
 *
 * The MIT License
 *
 * Copyright (c) 2008 hide@address.com
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * Project: Comics Chat
 * Version: 0.1
 *
 */

class PageNotExists extends Exception {
	public $pageNumber;
	function __construct ($pageNumber) { $this->pageNumber = $pageNumber; }
}

class HeroNotExists extends Exception {}

/**
 * ComicBook
 *
 * @uses IteratorAggregate
 * @package comicschat
 *
 */
class ComicBook implements IteratorAggregate {
	const COMPLETE = '0';
	const FIRST = '-1';
	const INNER = '1';
	const LAST = '2';

	/**
	 * Values for SQL filtering.
	 *
	 * @var array
	 * @access protected
	 */
	protected $filter;

	/**
	 * Cached values from functions.
	 *
	 * @var array
	 * @access protected
	 */
	protected $returnValueCache;

	/**
	 * DB connection.
	 *
	 * @var PDO object
	 * @access protected
	 */
	protected $db;

	/**
	 * Directory where images with heroes are located.
	 *
	 * @var string
	 * @access protected
	 */
	protected $heroesDirPath;


	/**
	 * This contains size, max string length and index of next possible panel.
	 *
	 * @var array
	 * @access protected
	 */
	protected $panelTypes = array (
		// +--
		// ---
		// ---
		0 => array ('lx' => 1, 'ly' => 1, 'width' => 1, 'height' => 1, 'maxBubbleTextLength' => 100, 'nextPossible' => array (1,5), 'first' => true),

		// -+-
		// ---
		// ---
		1 => array ('lx' => 2, 'ly' => 1, 'width' => 1, 'height' => 1, 'maxBubbleTextLength' => 100, 'nextPossible' => array (2)),

		// --+
		// ---
		// ---
		2 => array ('lx' => 3, 'ly' => 1, 'width' => 1, 'height' => 1, 'maxBubbleTextLength' => 100, 'nextPossible' => array (6,9,10,21)),

		// ++-
		// ---
		// ---
		3 => array ('lx' => 1, 'ly' => 1, 'width' => 2, 'height' => 1, 'maxBubbleTextLength' => 300, 'nextPossible' => array (2), 'first' => true),

		// +++
		// ---
		// ---
		4 => array ('lx' => 1, 'ly' => 1, 'width' => 3, 'height' => 1, 'maxBubbleTextLength' => 600, 'nextPossible' => array (6,9,10,21), 'first' => true),

		// -++
		// ---
		// ---
		5 => array ('lx' => 2, 'ly' => 1, 'width' => 2, 'height' => 1, 'maxBubbleTextLength' => 300, 'nextPossible' => array (6,9,10,21)),

		// ---
		// +--
		// ---
		6 => array ('lx' => 1, 'ly' => 2, 'width' => 1, 'height' => 1, 'maxBubbleTextLength' => 100, 'nextPossible' => array (7,11)),

		// ---
		// -+-
		// ---
		7 => array ('lx' => 2, 'ly' => 2, 'width' => 1, 'height' => 1, 'maxBubbleTextLength' => 100, 'nextPossible' => array (8)),

		// ---
		// --+
		// ---
		8 => array ('lx' => 3, 'ly' => 2, 'width' => 1, 'height' => 1, 'maxBubbleTextLength' => 100, 'nextPossible' => array (12,15,16)),

		// ---
		// ++-
		// ---
		9 => array ('lx' => 1, 'ly' => 2, 'width' => 2, 'height' => 1, 'maxBubbleTextLength' => 300, 'nextPossible' => array (8)),

		// ---
		// +++
		// ---
		10 => array ('lx' => 1, 'ly' => 2, 'width' => 3, 'height' => 1, 'maxBubbleTextLength' => 600, 'nextPossible' => array (12,15,16)),

		// ---
		// -++
		// ---
		11 => array ('lx' => 2, 'ly' => 2, 'width' => 2, 'height' => 1, 'maxBubbleTextLength' => 300, 'nextPossible' => array (12,15,16)),

		// ---
		// ---
		// +--
		12 => array ('lx' => 1, 'ly' => 3, 'width' => 1, 'height' => 1, 'maxBubbleTextLength' => 100, 'nextPossible' => array (13,17)),

		// ---
		// ---
		// -+-
		13 => array ('lx' => 2, 'ly' => 3, 'width' => 1, 'height' => 1, 'maxBubbleTextLength' => 100, 'nextPossible' => array (14)),

		// ---
		// ---
		// --+
		14 => array ('lx' => 3, 'ly' => 3, 'width' => 1, 'height' => 1, 'maxBubbleTextLength' => 100),

		// ---
		// ---
		// ++-
		15 => array ('lx' => 1, 'ly' => 3, 'width' => 2, 'height' => 1, 'maxBubbleTextLength' => 300, 'nextPossible' => array (18)),

		// ---
		// ---
		// +++
		16 => array ('lx' => 1, 'ly' => 3, 'width' => 3, 'height' => 1, 'maxBubbleTextLength' => 600),

		// ---
		// ---
		// -++
		17 => array ('lx' => 2, 'ly' => 3, 'width' => 2, 'height' => 1, 'maxBubbleTextLength' => 300),

		// ---
		// ---
		// --+
		18 => array ('lx' => 3, 'ly' => 3, 'width' => 1, 'height' => 1, 'maxBubbleTextLength' => 100),

		// +++
		// +++
		// ---
		19 => array ('lx' => 1, 'ly' => 1, 'width' => 3, 'height' => 2, 'maxBubbleTextLength' => 1600, 'nextPossible' => array (12,15,16), 'first' => true),

		// +++
		// +++
		// +++
		20 => array ('lx' => 1, 'ly' => 1, 'width' => 3, 'height' => 3, 'maxBubbleTextLength' => 3000, 'first' => true),

		// ---
		// +++
		// +++
		21 => array ('lx' => 1, 'ly' => 2, 'width' => 3, 'height' => 2, 'maxBubbleTextLength' => 1600),
	);

	/**
	 * Class constructor.
	 *
	 * @param array $config Configuration.
	 * @access protected
	 */
	function ComicBook ($config) {
		$this->heroesDirPath = !empty ($config ['heroesDirPath']) ? $config ['heroesDirPath'] : './';

		$this->db = new PDO($config ['dsn']);
		$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
		$s = $this->db->query('PRAGMA table_info(chat)');
		$info = $s->fetchAll(PDO::FETCH_ASSOC);
		if (count($info) == 0) {
			$this->db->exec('
			CREATE TABLE chat (
				id integer PRIMARY KEY,
				pageNumber INT,
				itemNumber INT,
				part INT,
				panelType INT,
				hero VARCHAR,
				inserted INT,
				bubbleText VARCHAR
			)
			');
			$this->db->exec('
			CREATE TABLE hero (
				id integer PRIMARY KEY,
				heroImage VARCHAR
			)
			');
		}
	}

	/**
	 * Returns index of another posible panel type according to length of input string.
	 *
	 * @param int $length length of input string
	 * @param int $lastPanel index of last used panel
	 * @access protected
	 * @return int
	 */
	protected function nextPanel ($length, $lastPanel=null) {
		$n = null;
		if (is_null ($lastPanel) || empty ($this->panelTypes [$lastPanel]['nextPossible']))
			$possiblePanels = array (0, 3, 4);
		else
			$possiblePanels = (array) $this->panelTypes [$lastPanel]['nextPossible'];

		foreach ($possiblePanels as $panelIndex) {
			$panel = $this->panelTypes [$panelIndex];
			$yes = false;

			if (!$n)
				$yes = true;
			else if ($panel ['maxBubbleTextLength'] >= $length) {
				if ($n ['maxBubbleTextLength'] < $length || $n ['maxBubbleTextLength'] > $panel ['maxBubbleTextLength'])
					$yes = true;
			}
			else if ($panel ['maxBubbleTextLength'] <= $length) {
				if ($n ['maxBubbleTextLength'] < $length && $n ['maxBubbleTextLength'] < $panel ['maxBubbleTextLength'])
					$yes = true;
			}

			if ($yes)
				$n = array ('panelIndex' => $panelIndex, 'maxBubbleTextLength' => $panel ['maxBubbleTextLength']);
		}

		return $n ['panelIndex'];
	}

	/**
	 * Returns panel info.
	 *
	 * @param int $typeid index of panel in panelTypes variable
	 * @access public
	 * @return int
	 */
	function getPanelInfo ($typeid) {
		if (!isset ($this->panelTypes [$typeid]))
			throw new Exception ('PANEL TYPE DOES NOT EXISTS');

		return $this->panelTypes [$typeid];
	}

	/**
	 * Returns number of pages in book.
	 *
	 * @access public
	 * @return int
	 */
	function countPages () {
		if (isset ($this->returnValueCache [__FUNCTION__]))
			return $this->returnValueCache [__FUNCTION__];
		$row = null;
		foreach ($this->db->query('SELECT pageNumber FROM chat ORDER BY pageNumber DESC LIMIT 1')->fetchAll(PDO::FETCH_ASSOC) as $row);

		return $this->returnValueCache [__FUNCTION__] = $row ? (int) reset ($row) : 0;
	}

	/**
	 * Returns absolute filesystem path to image of hero.
	 *
	 * @param string $file filename of image
	 * @access public
	 * @return string
	 */
	function getHeroImagePath ($file) {
		$n = realpath ($this->heroesDirPath . '/' . basename ($file));
		if (!$n)
			throw new HeroNotExists;
		else
			return $n;
	}

	/**
	 * Writes new message to book. It splits the text and creates panels.
	 *
	 * @param array $input
	 * @access public
	 * @return void
	 */
	function write ($input) {
		if (empty ($input ['text']))
			return null;
		else
			$text = $input ['text'];

		if (!empty ($input ['hero']))
			$hero = basename ($this->getHeroImagePath (basename ($input ['hero'])));
		else
			throw new HeroNotExists;

		$text = strip_tags (trim ($text));

		$length = strlen ($text);
		$data = array ();
		$result = null;
		foreach ($this->db->query ('SELECT panelType,pageNumber,itemNumber FROM chat ORDER BY id DESC LIMIT 1')->fetchAll(PDO::FETCH_ASSOC) as $result);
		$panelIndex = $result ? (int) $result ['panelType'] : null;
		$pageNumber = $result ? (int) $result ['pageNumber'] : 0;
		$itemNumber = $result ? (int) $result ['itemNumber'] : 0;
		++$itemNumber;
		$part = null;

		do {
			$panelIndex = $this->nextPanel ($length, $panelIndex);
			if (!empty ($this->panelTypes [$panelIndex]['first']))
				++$pageNumber;

			// avoid cutting text in middle of word.
			$cut = $this->panelTypes [$panelIndex]['maxBubbleTextLength'];
			while ($cut < $length && $text{$cut} != ' ')
				++$cut;

			// partial
			if ($cut >= $length) {
				if (is_null ($part))
					$part = ComicBook::COMPLETE;
				else
					ComicBook::LAST;
			}
			else if (is_null ($part))
				$part = ComicBook::FIRST;
			else
				$part = ComicBook::INNER;

			$data [] = array (
			'panelType' => $panelIndex,
				'itemNumber' => $itemNumber,
				'part' => $part,
				'bubbleText' => substr ($text, 0, $cut),
				'inserted' => mktime (),
				'pageNumber' => $pageNumber,
				'hero' => $hero,
			);
			$text = substr ($text, $cut);
			$length = strlen ($text);
		}
		while ($length > 0);

		$s = $this->db->prepare('INSERT INTO chat
			(panelType,itemNumber,part,bubbleText,inserted,pageNumber,hero)
			VALUES
			(:panelType,:itemNumber,:part,:bubbleText,:inserted,:pageNumber,:hero)');

		foreach ($data as $insert)
			$s->execute($insert);

		$this->returnValueCache = null;
	}

	/**
	 * Returns cached list of heroes.
	 *
	 * @access public
	 * @return array
	 */
	function listHeroes () {
		if (isset ($this->returnValueCache [__FUNCTION__]))
			return $this->returnValueCache [__FUNCTION__];

		$n = array ();

		$dir = dir ($this->heroesDirPath);
		while ($file = $dir->read ()) {
			if ($file{0} == '.') continue;
			$n [] = $file;
		}

		return $this->returnValueCache [__FUNCTION__] = $n;
	}

	/**
	 * Set the first page and limits number of pages. Throws PageNotExists exception if page doesn't exist.
	 *
	 * @param int $first
	 * @param int $count
	 * @access public
	 * @return void
	 */
	function selectPages ($first, $count=1) {
		settype ($first, 'integer');
		settype ($count, 'integer');

		$pageCount = $this->countPages ();
		if (!$pageCount || ($first < 1 || $first > $pageCount))
			throw new PageNotExists ($first);

		if ($count == 1)
			$this->filter ['pageNumber'] = 'pageNumber=' . $first;
		else
			$this->filter ['pageNumber'] = '(pageNumber BETWEEN ' . $first . ' AND ' . ($first + $count) . ')';
	}

	/**
	 * Returns all page numbers in book.
	 *
	 * @access public
	 * @return array
	 */
	function pageNumbers () {
		$n = array ();
		foreach ($this->db->query ('SELECT pageNumber FROM chat' . ($this->filter ? ' WHERE ' . implode (' AND ', $this->filter) : '') . ' GROUP BY pageNumber ORDER BY pageNumber') as $r)
			$n [] = (int) $r ['pageNumber'];

		return $n;
	}

	/**
	 * Returns interator according to filter and then resets filter.
	 *
	 * @access public
	 * @return Iterator
	 */
	function getIterator () {
		$list = $this->db->query ('SELECT * FROM chat' . ($this->filter ? ' WHERE ' . implode (' AND ', $this->filter) : '') . ' ORDER BY id')->fetchAll(PDO::FETCH_ASSOC);
		$this->filter = null;
		return new ArrayIterator($list);
	}
}
Return current item: Comics chat