PageRenderTime 42ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 0ms

/inc/lib/a2s/a2s52.php

https://bitbucket.org/yoander/mtrack
PHP | 2383 lines | 2002 code | 155 blank | 226 comment | 68 complexity | 88810926266d1413c60b68dc0167c4e3 MD5 | raw file
Possible License(s): BSD-3-Clause, Apache-2.0
  1. <?php # DO NOT EDIT: generated from ASCIIToSVG.php by mk52.pl
  2. /*
  3. * A2S_ASCIIToSVG.php: ASCII diagram -> SVG art generator.
  4. * Copyright Š 2012 Devon H. O'Dell <devon.odell@gmail.com>
  5. * All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions
  9. * are met:
  10. *
  11. * o Redistributions of source code must retain the above copyright notice,
  12. * this list of conditions and the following disclaimer.
  13. * o Redistributions in binary form must reproduce the above copyright notice,
  14. * this list of conditions and the following disclaimer in the documentation
  15. * and/or other materials provided with the distribution.
  16. *
  17. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  18. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  19. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  20. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
  21. * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  22. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  23. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  24. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  25. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  26. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  27. * POSSIBILITY OF SUCH DAMAGE.
  28. *
  29. */
  30. #namespace org\dh0\a2s;
  31. include 'svg-path.lex.php';
  32. include 'colors.php';
  33. /*
  34. * A2S_Scale is a singleton class that is instantiated to apply scale
  35. * transformations on the text -> canvas grid geometry. We could probably use
  36. * SVG's native scaling for this, but I'm not sure how yet.
  37. */
  38. class A2S_Scale {
  39. private static $instance = null;
  40. public $xScale;
  41. public $yScale;
  42. private function __construct() {}
  43. private function __clone() {}
  44. public static function getInstance() {
  45. if (self::$instance == null) {
  46. self::$instance = new A2S_Scale();
  47. }
  48. return self::$instance;
  49. }
  50. public function setScale($x, $y) {
  51. $o = self::getInstance();
  52. $o->xScale = $x;
  53. $o->yScale = $y;
  54. }
  55. }
  56. /*
  57. * A2S_CustomObjects allows users to create their own custom SVG paths and use
  58. * them as box types with a2s:type references.
  59. *
  60. * Paths must have width and height set, and must not span multiple lines.
  61. * Multiple paths can be specified, one path per line. All objects must
  62. * reside in the same directory.
  63. *
  64. * File operations are horribly slow, so we make a best effort to avoid
  65. * as many as possible:
  66. *
  67. * * If the directory mtime hasn't changed, we attempt to load our
  68. * objects from a cache file.
  69. *
  70. * * If this file doesn't exist, can't be read, or the mtime has
  71. * changed, we scan the directory and update files that have changed
  72. * based on their mtime.
  73. *
  74. * * We attempt to save our cache in a temporary directory. It's volatile
  75. * but also requires no configuration.
  76. *
  77. * We could do a bit better by utilizing APC's shared memory storage, which
  78. * would help greatly when running on a server.
  79. *
  80. * Note that the path parser isn't foolproof, mostly because PHP isn't the
  81. * greatest language ever for implementing a parser.
  82. */
  83. class A2S_CustomObjects {
  84. public static $objects = array();
  85. /*
  86. * Closures / callable function names / whatever for integrating non-default
  87. * loading and storage functionality.
  88. */
  89. public static $loadCacheFn = null;
  90. public static $storCacheFn = null;
  91. public static $loadObjsFn = null;
  92. public static function loadObjects() {
  93. $cacheFile = sys_get_temp_dir() . "/.a2s.objcache";
  94. $dir = './objects';
  95. if (is_callable(self::$loadCacheFn)) {
  96. /*
  97. * Should return exactly what was given to the $storCacheFn when it was
  98. * last called, or null if nothing can be loaded.
  99. */
  100. $fn = self::$loadCacheFn;
  101. self::$objects = $fn();
  102. return;
  103. } else {
  104. if (is_readable($cacheFile)) {
  105. $cacheTime = filemtime($cacheFile);
  106. if (filemtime($dir) <= filemtime($cacheFile)) {
  107. self::$objects = unserialize(file_get_contents($cacheFile));
  108. return;
  109. }
  110. } else {
  111. $cacheTime = 0;
  112. }
  113. }
  114. if (is_callable(self::$loadObjsFn)) {
  115. /*
  116. * Returns an array of arrays of path information. The innermost arrays
  117. * (containing the path information) contain the path name, the width of
  118. * the bounding box, the height of the bounding box, and the path
  119. * command. This interface does *not* want the path's XML tag. An array
  120. * returned from here containing two objects that each have 1 line would
  121. * look like:
  122. *
  123. * array (
  124. * array (
  125. * name => 'pathA',
  126. * paths => array (
  127. * array ('width' => 10, 'height' => 10, 'path' => 'M 0 0 L 10 10'),
  128. * array ('width' => 10, 'height' => 10, 'path' => 'M 0 10 L 10 0'),
  129. * ),
  130. * ),
  131. * array (
  132. * name => 'pathB',
  133. * paths => array (
  134. * array ('width' => 10, 'height' => 10, 'path' => 'M 0 5 L 5 10'),
  135. * array ('width' => 10, 'height' => 10, 'path' => 'M 5 10 L 10 5'),
  136. * ),
  137. * ),
  138. * );
  139. */
  140. $fn = self::$loadObjsFn;
  141. $objs = $fn();
  142. $i = 0;
  143. foreach ($objs as $obj) {
  144. foreach ($obj['paths'] as $path) {
  145. self::$objects[$obj['name']][$i]['width'] = $path['width'];
  146. self::$objects[$obj['name']][$i]['height'] = $path['height'];
  147. self::$objects[$obj['name']][$i++]['path'] =
  148. self::parsePath($path['path']);
  149. }
  150. }
  151. } else {
  152. $ents = scandir($dir);
  153. foreach ($ents as $ent) {
  154. $file = "{$dir}/{$ent}";
  155. $base = substr($ent, 0, -5);
  156. if (substr($ent, -5) == '.path' && is_readable($file)) {
  157. if (isset(self::$objects[$base]) &&
  158. filemtime($file) <= self::$cacheTime) {
  159. continue;
  160. }
  161. $lines = file($file);
  162. $i = 0;
  163. foreach ($lines as $line) {
  164. preg_match('/width="(\d+)/', $line, $m);
  165. $width = $m[1];
  166. preg_match('/height="(\d+)/', $line, $m);
  167. $height = $m[1];
  168. preg_match('/d="([^"]+)"/', $line, $m);
  169. $path = $m[1];
  170. self::$objects[$base][$i]['width'] = $width;
  171. self::$objects[$base][$i]['height'] = $height;
  172. self::$objects[$base][$i++]['path'] = self::parsePath($path);
  173. }
  174. }
  175. }
  176. }
  177. if (is_callable(self::$storCacheFn)) {
  178. $fn = self::$storCacheFn;
  179. $fn(self::$objects);
  180. } else {
  181. file_put_contents($cacheFile, serialize(self::$objects));
  182. }
  183. }
  184. private static function parsePath($path) {
  185. $stream = fopen("data://text/plain,{$path}", 'r');
  186. $P = new A2S_SVGPathParser();
  187. $S = new A2S_Yylex($stream);
  188. while ($t = $S->nextToken()) {
  189. $P->A2S_SVGPath($t->type, $t);
  190. }
  191. /* Force shift/reduce of last token. */
  192. $P->A2S_SVGPath(0);
  193. fclose($stream);
  194. $cmdArr = array();
  195. $i = 0;
  196. foreach ($P->commands as $cmd) {
  197. foreach ($cmd as $arg) {
  198. $arg = (array)$arg;
  199. $cmdArr[$i][] = $arg['value'];
  200. }
  201. $i++;
  202. }
  203. return $cmdArr;
  204. }
  205. }
  206. /*
  207. * All lines and polygons are represented as a series of point coordinates
  208. * along a path. Points can have different properties; markers appear on
  209. * edges of lines and control points denote that a bezier curve should be
  210. * calculated for the corner represented by this point.
  211. */
  212. class A2S_Point {
  213. public $gridX;
  214. public $gridY;
  215. public $x;
  216. public $y;
  217. public $flags;
  218. const POINT = 0x1;
  219. const CONTROL = 0x2;
  220. const SMARKER = 0x4;
  221. const IMARKER = 0x8;
  222. public function __construct($x, $y) {
  223. $this->flags = 0;
  224. $s = A2S_Scale::getInstance();
  225. $this->x = ($x * $s->xScale) + ($s->xScale / 2);
  226. $this->y = ($y * $s->yScale) + ($s->yScale / 2);
  227. $this->gridX = $x;
  228. $this->gridY = $y;
  229. }
  230. }
  231. /*
  232. * Groups objects together and sets common properties for the objects in the
  233. * group.
  234. */
  235. class A2S_SVGGroup {
  236. private $groups;
  237. private $curGroup;
  238. private $groupStack;
  239. private $options;
  240. public function __construct() {
  241. $this->groups = array();
  242. $this->groupStack = array();
  243. $this->options = array();
  244. }
  245. public function getGroup($groupName) {
  246. return $this->groups[$groupName];
  247. }
  248. public function pushGroup($groupName) {
  249. if (!isset($this->groups[$groupName])) {
  250. $this->groups[$groupName] = array();
  251. $this->options[$groupName] = array();
  252. }
  253. $this->groupStack[] = $groupName;
  254. $this->curGroup = $groupName;
  255. }
  256. public function popGroup() {
  257. /*
  258. * Remove the last group and fetch the current one. array_pop will return
  259. * NULL for an empty array, so this is safe to do when only one element
  260. * is left.
  261. */
  262. array_pop($this->groupStack);
  263. $this->curGroup = array_pop($this->groupStack);
  264. }
  265. public function addObject($o) {
  266. $this->groups[$this->curGroup][] = $o;
  267. }
  268. public function setOption($opt, $val) {
  269. $this->options[$this->curGroup][$opt] = $val;
  270. }
  271. public function render() {
  272. $out = '';
  273. foreach($this->groups as $groupName => $objects) {
  274. $out .= "<g id=\"{$groupName}\" ";
  275. foreach ($this->options[$groupName] as $opt => $val) {
  276. if (strpos($opt, 'a2s:', 0) === 0) {
  277. continue;
  278. }
  279. $out .= "$opt=\"$val\" ";
  280. }
  281. $out .= ">\n";
  282. foreach($objects as $obj) {
  283. $out .= $obj->render();
  284. }
  285. $out .= "</g>\n";
  286. }
  287. return $out;
  288. }
  289. }
  290. /*
  291. * The Path class represents lines and polygons.
  292. */
  293. class A2S_SVGPath {
  294. private $options;
  295. private $points;
  296. private $flags;
  297. private $text;
  298. private $name;
  299. private static $id = 0;
  300. const CLOSED = 0x1;
  301. public function __construct() {
  302. $this->options = array();
  303. $this->points = array();
  304. $this->text = array();
  305. $this->flags = 0;
  306. $this->name = self::$id++;
  307. }
  308. /*
  309. * Making sure that we always started at the top left coordinate
  310. * makes so many things so much easier. First, find the lowest Y
  311. * position. Then, of all matching Y positions, find the lowest X
  312. * position. This is the top left.
  313. *
  314. * As far as the points are considered, they're definitely on the
  315. * top somewhere, but not necessarily the most left. This could
  316. * happen if there was a corner connector in the top edge (perhaps
  317. * for a line to connect to). Since we couldn't turn right there,
  318. * we have to try now.
  319. *
  320. * This should only be called when we close a polygon.
  321. */
  322. public function orderPoints() {
  323. $pPoints = count($this->points);
  324. $minY = $this->points[0]->y;
  325. $minX = $this->points[0]->x;
  326. $minIdx = 0;
  327. for ($i = 1; $i < $pPoints; $i++) {
  328. if ($this->points[$i]->y <= $minY) {
  329. $minY = $this->points[$i]->y;
  330. if ($this->points[$i]->x < $minX) {
  331. $minX = $this->points[$i]->x;
  332. $minIdx = $i;
  333. }
  334. }
  335. }
  336. /*
  337. * If our top left isn't at the 0th index, it is at the end. If
  338. * there are bits after it, we need to cut those and put them at
  339. * the front.
  340. */
  341. if ($minIdx != 0) {
  342. $startPoints = array_splice($this->points, $minIdx);
  343. $this->points = array_merge($startPoints, $this->points);
  344. }
  345. }
  346. /*
  347. * Useful for recursive walkers when speculatively trying a direction.
  348. */
  349. public function popPoint() {
  350. array_pop($this->points);
  351. }
  352. public function addPoint($x, $y, $flags = A2S_Point::POINT) {
  353. $p = new A2S_Point($x, $y);
  354. /*
  355. * If we attempt to add our original point back to the path, the polygon
  356. * must be closed.
  357. */
  358. if (count($this->points) > 0) {
  359. if ($this->points[0]->x == $p->x && $this->points[0]->y == $p->y) {
  360. $this->flags |= self::CLOSED;
  361. return true;
  362. }
  363. /*
  364. * For the purposes of this library, paths should never intersect each
  365. * other. Even in the case of closing the polygon, we do not store the
  366. * final coordinate twice.
  367. */
  368. foreach ($this->points as $point) {
  369. if ($point->x == $p->x && $point->y == $p->y) {
  370. return true;
  371. }
  372. }
  373. }
  374. $p->flags |= $flags;
  375. $this->points[] = $p;
  376. return false;
  377. }
  378. /*
  379. * It's useful to be able to know the points in a shape.
  380. */
  381. public function getPoints() {
  382. return $this->points;
  383. }
  384. /*
  385. * Add a marker to a line. The third argument specifies which marker to use,
  386. * and this depends on the orientation of the line. Due to the way the line
  387. * parser works, we may have to use an inverted representation.
  388. */
  389. public function addMarker($x, $y, $t) {
  390. $p = new A2S_Point($x, $y);
  391. $p->flags |= $t;
  392. $this->points[] = $p;
  393. }
  394. /*
  395. * Is this path closed?
  396. */
  397. public function isClosed() {
  398. return ($this->flags & self::CLOSED);
  399. }
  400. public function addText($t) {
  401. $this->text[] = $t;
  402. }
  403. public function getText() {
  404. return $this->text;
  405. }
  406. public function setID($id) {
  407. $this->name = str_replace(' ', '_', str_replace('"', '_', $id));
  408. }
  409. public function getID() {
  410. return $this->name;
  411. }
  412. /*
  413. * Set options as a JSON string. Specified as a merge operation so that it
  414. * can be called after an individual setOption call.
  415. */
  416. public function setOptions($opt) {
  417. $this->options = array_merge($this->options, $opt);
  418. }
  419. public function setOption($opt, $val) {
  420. $this->options[$opt] = $val;
  421. }
  422. public function getOption($opt) {
  423. if (isset($this->options[$opt])) {
  424. return $this->options[$opt];
  425. }
  426. return null;
  427. }
  428. /*
  429. * Does the given point exist within this polygon? Since we can
  430. * theoretically have some complex concave and convex polygon edges in the
  431. * same shape, we need to do a full point-in-polygon test. This algorithm
  432. * seems like the standard one. See: http://alienryderflex.com/polygon/
  433. */
  434. public function hasPoint($x, $y) {
  435. if ($this->isClosed() == false) {
  436. return false;
  437. }
  438. $oddNodes = false;
  439. $bound = count($this->points);
  440. for ($i = 0, $j = count($this->points) - 1; $i < $bound; $i++) {
  441. if (($this->points[$i]->gridY < $y && $this->points[$j]->gridY >= $y ||
  442. $this->points[$j]->gridY < $y && $this->points[$i]->gridY >= $y) &&
  443. ($this->points[$i]->gridX <= $x || $this->points[$j]->gridX <= $x)) {
  444. if ($this->points[$i]->gridX + ($y - $this->points[$i]->gridY) /
  445. ($this->points[$j]->gridY - $this->points[$i]->gridY) *
  446. ($this->points[$j]->gridX - $this->points[$i]->gridX) < $x) {
  447. $oddNodes = !$oddNodes;
  448. }
  449. }
  450. $j = $i;
  451. }
  452. return $oddNodes;
  453. }
  454. /*
  455. * Apply a matrix transformation to the coordinates ($x, $y). The
  456. * multiplication is implemented on the matrices:
  457. *
  458. * | a b c | | x |
  459. * | d e f | * | y |
  460. * | 0 0 1 | | 1 |
  461. *
  462. * Additional information on the transformations and what each R,C in the
  463. * transformation matrix represents, see:
  464. *
  465. * http://www.w3.org/TR/SVG/coords.html#TransformMatrixDefined
  466. */
  467. private function matrixTransform($matrix, $x, $y) {
  468. $xyMat = array(array($x), array($y), array(1));
  469. $newXY = array(array());
  470. for ($i = 0; $i < 3; $i++) {
  471. for ($j = 0; $j < 1; $j++) {
  472. $sum = 0;
  473. for ($k = 0; $k < 3; $k++) {
  474. $sum += $matrix[$i][$k] * $xyMat[$k][$j];
  475. }
  476. $newXY[$i][$j] = $sum;
  477. }
  478. }
  479. /* Return the coordinates as a vector */
  480. return array($newXY[0][0], $newXY[1][0], $newXY[2][0]);
  481. }
  482. /*
  483. * Translate the X and Y coordinates. tX and tY specify the distance to
  484. * transform.
  485. */
  486. private function translateTransform($tX, $tY, $x, $y) {
  487. $matrix = array(array(1, 0, $tX), array(0, 1, $tY), array(0, 0, 1));
  488. return $this->matrixTransform($matrix, $x, $y);
  489. }
  490. /*
  491. * A2S_Scale transformations are implemented by applying the scale to the X and
  492. * Y coordinates. One unit in the new coordinate system equals $s[XY] units
  493. * in the old system. Thus, if you want to double the size of an object on
  494. * both axes, you sould call scaleTransform(0.5, 0.5, $x, $y)
  495. */
  496. private function scaleTransform($sX, $sY, $x, $y) {
  497. $matrix = array(array($sX, 0, 0), array(0, $sY, 0), array(0, 0, 1));
  498. return $this->matrixTransform($matrix, $x, $y);
  499. }
  500. /*
  501. * Rotate the coordinates around the center point cX and cY. If these
  502. * are not specified, the coordinate is rotated around 0,0. The angle
  503. * is specified in degrees.
  504. */
  505. private function rotateTransform($angle, $x, $y, $cX = 0, $cY = 0) {
  506. $angle = $angle * (pi() / 180);
  507. if ($cX != 0 || $cY != 0) {
  508. list ($x, $y) = $this->translateTransform($cX, $cY, $x, $y);
  509. }
  510. $matrix = array(array(cos($angle), -sin($angle), 0),
  511. array(sin($angle), cos($angle), 0),
  512. array(0, 0, 1));
  513. $ret = $this->matrixTransform($matrix, $x, $y);
  514. if ($cX != 0 || $cY != 0) {
  515. list ($x, $y) = $this->translateTransform(-$cX, -$cY, $ret[0], $ret[1]);
  516. $ret[0] = $x;
  517. $ret[1] = $y;
  518. }
  519. return $ret;
  520. }
  521. /*
  522. * Skews along the X axis at specified angle. The angle is specified in
  523. * degrees.
  524. */
  525. private function skewXTransform($angle, $x, $y) {
  526. $angle = $angle * (pi() / 180);
  527. $matrix = array(array(1, tan($angle), 0), array(0, 1, 0), array(0, 0, 1));
  528. return $this->matrixTransform($matrix, $x, $y);
  529. }
  530. /*
  531. * Skews along the Y axis at specified angle. The angle is specified in
  532. * degrees.
  533. */
  534. private function skewYTransform($angle, $x, $y) {
  535. $angle = $angle * (pi() / 180);
  536. $matrix = array(array(1, 0, 0), array(tan($angle), 1, 0), array(0, 0, 1));
  537. return $this->matrixTransform($matrix, $x, $y);
  538. }
  539. /*
  540. * Apply a transformation to a point $p.
  541. */
  542. private function applyTransformToPoint($txf, $p, $args) {
  543. switch ($txf) {
  544. case 'translate':
  545. return $this->translateTransform($args[0], $args[1], $p->x, $p->y);
  546. case 'scale':
  547. return $this->scaleTransform($args[0], $args[1], $p->x, $p->y);
  548. case 'rotate':
  549. if (count($args) > 1) {
  550. return $this->rotateTransform($args[0], $p->x, $p->y, $args[1], $args[2]);
  551. } else {
  552. return $this->rotateTransform($args[0], $p->x, $p->y);
  553. }
  554. case 'skewX':
  555. return $this->skewXTransform($args[0], $p->x, $p->y);
  556. case 'skewY':
  557. return $this->skewYTransform($args[0], $p->x, $p->y);
  558. }
  559. }
  560. /*
  561. * Apply the transformation function $txf to all coordinates on path $p
  562. * providing $args as arguments to the transformation function.
  563. */
  564. private function applyTransformToPath($txf, &$p, $args) {
  565. $pathCmds = count($p['path']);
  566. $curPoint = new A2S_Point(0, 0);
  567. $prevType = null;
  568. $curType = null;
  569. for ($i = 0; $i < $pathCmds; $i++) {
  570. $cmd = &$p['path'][$i];
  571. $prevType = $curType;
  572. $curType = $cmd[0];
  573. switch ($curType) {
  574. case 'z':
  575. case 'Z':
  576. /* Can't transform this */
  577. break;
  578. case 'm':
  579. if ($prevType != null) {
  580. $curPoint->x += $cmd[1];
  581. $curPoint->y += $cmd[2];
  582. list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args);
  583. $curPoint->x = $x;
  584. $curPoint->y = $y;
  585. $cmd[1] = $x;
  586. $cmd[2] = $y;
  587. } else {
  588. $curPoint->x = $cmd[1];
  589. $curPoint->y = $cmd[2];
  590. list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args);
  591. $curPoint->x = $x;
  592. $curPoint->y = $y;
  593. $cmd[1] = $x;
  594. $cmd[2] = $y;
  595. $curType = 'l';
  596. }
  597. break;
  598. case 'M':
  599. $curPoint->x = $cmd[1];
  600. $curPoint->y = $cmd[2];
  601. list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args);
  602. $curPoint->x = $x;
  603. $curPoint->y = $y;
  604. $cmd[1] = $x;
  605. $cmd[2] = $y;
  606. if ($prevType == null) {
  607. $curType = 'L';
  608. }
  609. break;
  610. case 'l':
  611. $curPoint->x += $cmd[1];
  612. $curPoint->y += $cmd[2];
  613. list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args);
  614. $curPoint->x = $x;
  615. $curPoint->y = $y;
  616. $cmd[1] = $x;
  617. $cmd[2] = $y;
  618. break;
  619. case 'L':
  620. $curPoint->x = $cmd[1];
  621. $curPoint->y = $cmd[2];
  622. list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args);
  623. $curPoint->x = $x;
  624. $curPoint->y = $y;
  625. $cmd[1] = $x;
  626. $cmd[2] = $y;
  627. break;
  628. case 'v':
  629. $curPoint->y += $cmd[1];
  630. $curPoint->x += 0;
  631. list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args);
  632. $curPoint->x = $x;
  633. $curPoint->y = $y;
  634. $cmd[1] = $y;
  635. break;
  636. case 'V':
  637. $curPoint->y = $cmd[1];
  638. list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args);
  639. $curPoint->x = $x;
  640. $curPoint->y = $y;
  641. $cmd[1] = $y;
  642. break;
  643. case 'h':
  644. $curPoint->x += $cmd[1];
  645. list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args);
  646. $curPoint->x = $x;
  647. $curPoint->y = $y;
  648. $cmd[1] = $x;
  649. break;
  650. case 'H':
  651. $curPoint->x = $cmd[1];
  652. list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args);
  653. $curPoint->x = $x;
  654. $curPoint->y = $y;
  655. $cmd[1] = $x;
  656. break;
  657. case 'c':
  658. $tP = new A2S_Point(0, 0);
  659. $tP->x = $curPoint->x + $cmd[1]; $tP->y = $curPoint->y + $cmd[2];
  660. list ($x, $y) = $this->applyTransformToPoint($txf, $tP, $args);
  661. $cmd[1] = $x;
  662. $cmd[2] = $y;
  663. $tP->x = $curPoint->x + $cmd[3]; $tP->y = $curPoint->y + $cmd[4];
  664. list ($x, $y) = $this->applyTransformToPoint($txf, $tP, $args);
  665. $cmd[3] = $x;
  666. $cmd[4] = $y;
  667. $curPoint->x += $cmd[5];
  668. $curPoint->y += $cmd[6];
  669. list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args);
  670. $curPoint->x = $x;
  671. $curPoint->y = $y;
  672. $cmd[5] = $x;
  673. $cmd[6] = $y;
  674. break;
  675. case 'C':
  676. $curPoint->x = $cmd[1];
  677. $curPoint->y = $cmd[2];
  678. list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args);
  679. $cmd[1] = $x;
  680. $cmd[2] = $y;
  681. $curPoint->x = $cmd[3];
  682. $curPoint->y = $cmd[4];
  683. list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args);
  684. $cmd[3] = $x;
  685. $cmd[4] = $y;
  686. $curPoint->x = $cmd[5];
  687. $curPoint->y = $cmd[6];
  688. list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args);
  689. $curPoint->x = $x;
  690. $curPoint->y = $y;
  691. $cmd[5] = $x;
  692. $cmd[6] = $y;
  693. break;
  694. case 's':
  695. case 'S':
  696. case 'q':
  697. case 'Q':
  698. case 't':
  699. case 'T':
  700. case 'a':
  701. break;
  702. case 'A':
  703. /*
  704. * This radius is relative to the start and end points, so it makes
  705. * sense to scale, rotate, or skew it, but not translate it.
  706. */
  707. if ($txf != 'translate') {
  708. $curPoint->x = $cmd[1];
  709. $curPoint->y = $cmd[2];
  710. list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args);
  711. $cmd[1] = $x;
  712. $cmd[2] = $y;
  713. }
  714. $curPoint->x = $cmd[6];
  715. $curPoint->y = $cmd[7];
  716. list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args);
  717. $curPoint->x = $x;
  718. $curPoint->y = $y;
  719. $cmd[6] = $x;
  720. $cmd[7] = $y;
  721. break;
  722. }
  723. }
  724. }
  725. public function render() {
  726. $startPoint = array_shift($this->points);
  727. $endPoint = $this->points[count($this->points) - 1];
  728. $out = "<g id=\"group{$this->name}\">\n";
  729. /*
  730. * If someone has specified one of our special object types, we are going
  731. * to want to completely override any of the pathing that we would have
  732. * done otherwise, but we defer until here to do anything about it because
  733. * we need information about the object we're replacing.
  734. */
  735. if (isset($this->options['a2s:type']) &&
  736. isset(A2S_CustomObjects::$objects[$this->options['a2s:type']])) {
  737. $object = A2S_CustomObjects::$objects[$this->options['a2s:type']];
  738. /* Again, if no fill was specified, specify one. */
  739. if (!isset($this->options['fill'])) {
  740. $this->options['fill'] = '#fff';
  741. }
  742. /*
  743. * We don't care so much about the area, but we do care about the width
  744. * and height of the object. All of our "custom" objects are implemented
  745. * in 100x100 space, which makes the transformation marginally easier.
  746. */
  747. $minX = $startPoint->x; $maxX = $minX;
  748. $minY = $startPoint->y; $maxY = $minY;
  749. foreach ($this->points as $p) {
  750. if ($p->x < $minX) {
  751. $minX = $p->x;
  752. } elseif ($p->x > $maxX) {
  753. $maxX = $p->x;
  754. }
  755. if ($p->y < $minY) {
  756. $minY = $p->y;
  757. } elseif ($p->y > $maxY) {
  758. $maxY = $p->y;
  759. }
  760. }
  761. $objW = $maxX - $minX;
  762. $objH = $maxY - $minY;
  763. $i = 0;
  764. foreach ($object as $o) {
  765. $id = self::$id++;
  766. $out .= "\t<path id=\"path{$this->name}\" d=\"";
  767. $oW = $o['width'];
  768. $oH = $o['height'];
  769. $this->applyTransformToPath('scale', $o, array($objW/$oW, $objH/$oH));
  770. $this->applyTransformToPath('translate', $o, array($minX, $minY));
  771. foreach ($o['path'] as $cmd) {
  772. $out .= join(' ', $cmd) . ' ';
  773. }
  774. $out .= '" ';
  775. /* Don't add options to sub-paths */
  776. if ($i++ < 1) {
  777. foreach ($this->options as $opt => $val) {
  778. if (strpos($opt, 'a2s:', 0) === 0) {
  779. continue;
  780. }
  781. $out .= "$opt=\"$val\" ";
  782. }
  783. }
  784. $out .= " />\n";
  785. }
  786. if (count($this->text) > 0) {
  787. foreach ($this->text as $text) {
  788. $out .= "\t" . $text->render() . "\n";
  789. }
  790. }
  791. $out .= "</g>\n";
  792. /* Bazinga. */
  793. return $out;
  794. }
  795. /*
  796. * Nothing fancy here -- this is just rendering for our standard
  797. * polygons.
  798. *
  799. * Our start point is represented by a single moveto command (unless the
  800. * start point is curved) as the shape will be closed with the Z command
  801. * automatically if it is a closed shape. If we have a control point, we
  802. * have to go ahead and draw the curve.
  803. */
  804. if (($startPoint->flags & A2S_Point::CONTROL)) {
  805. $cX = $startPoint->x;
  806. $cY = $startPoint->y;
  807. $sX = $startPoint->x;
  808. $sY = $startPoint->y + 10;
  809. $eX = $startPoint->x + 10;
  810. $eY = $startPoint->y;
  811. $path = "M {$sX} {$sY} Q {$cX} {$cY} {$eX} {$eY} ";
  812. } else {
  813. $path = "M {$startPoint->x} {$startPoint->y} ";
  814. }
  815. $prevP = $startPoint;
  816. $bound = count($this->points);
  817. for ($i = 0; $i < $bound; $i++) {
  818. $p = $this->points[$i];
  819. /*
  820. * Handle quadratic Bezier curves. NOTE: This algorithm for drawing
  821. * the curves only works if the shapes are drawn in a clockwise
  822. * manner.
  823. */
  824. if (($p->flags & A2S_Point::CONTROL)) {
  825. /* Our control point is always the original corner */
  826. $cX = $p->x;
  827. $cY = $p->y;
  828. /* Need next point to determine which way to turn */
  829. if ($i == count($this->points) - 1) {
  830. $nP = $startPoint;
  831. } else {
  832. $nP = $this->points[$i + 1];
  833. }
  834. if ($prevP->x == $p->x) {
  835. /*
  836. * If we are on the same vertical axis, our starting X coordinate
  837. * is the same as the control point coordinate.
  838. */
  839. $sX = $p->x;
  840. /* Offset start point from control point in the proper direction */
  841. if ($prevP->y < $p->y) {
  842. $sY = $p->y - 10;
  843. } else {
  844. $sY = $p->y + 10;
  845. }
  846. $eY = $p->y;
  847. /* Offset end point from control point in the proper direction */
  848. if ($nP->x < $p->x) {
  849. $eX = $p->x - 10;
  850. } else {
  851. $eX = $p->x + 10;
  852. }
  853. } elseif ($prevP->y == $p->y) {
  854. /* Horizontal decisions mirror vertical's above */
  855. $sY = $p->y;
  856. if ($prevP->x < $p->x) {
  857. $sX = $p->x - 10;
  858. } else {
  859. $sX = $p->x + 10;
  860. }
  861. $eX = $p->x;
  862. if ($nP->y <= $p->y) {
  863. $eY = $p->y - 10;
  864. } else {
  865. $eY = $p->y + 10;
  866. }
  867. }
  868. $path .= "L {$sX} {$sY} Q {$cX} {$cY} {$eX} {$eY} ";
  869. } else {
  870. /* The excruciating difficulty of drawing a straight line */
  871. $path .= "L {$p->x} {$p->y} ";
  872. }
  873. $prevP = $p;
  874. }
  875. if ($this->isClosed()) {
  876. $path .= 'Z';
  877. }
  878. $id = self::$id++;
  879. /* Add markers if necessary. */
  880. if ($startPoint->flags & A2S_Point::SMARKER) {
  881. $this->options["marker-start"] = "url(#Pointer)";
  882. } elseif ($startPoint->flags & A2S_Point::IMARKER) {
  883. $this->options["marker-start"] = "url(#iPointer)";
  884. }
  885. if ($endPoint->flags & A2S_Point::SMARKER) {
  886. $this->options["marker-end"] = "url(#Pointer)";
  887. } elseif ($endPoint->flags & A2S_Point::IMARKER) {
  888. $this->options["marker-end"] = "url(#iPointer)";
  889. }
  890. /*
  891. * SVG objects without a fill will be transparent, and this looks so
  892. * terrible with the drop-shadow effect. Any objects that aren't filled
  893. * automatically get a white fill.
  894. */
  895. if ($this->isClosed() && !isset($this->options['fill'])) {
  896. $this->options['fill'] = '#fff';
  897. }
  898. $out .= "\t<path id=\"path{$this->name}\" ";
  899. foreach ($this->options as $opt => $val) {
  900. if (strpos($opt, 'a2s:', 0) === 0) {
  901. continue;
  902. }
  903. $out .= "$opt=\"$val\" ";
  904. }
  905. $out .= "d=\"{$path}\" />\n";
  906. if (count($this->text) > 0) {
  907. foreach ($this->text as $text) {
  908. $text->setID($this->name);
  909. $out .= "\t" . $text->render() . "\n";
  910. }
  911. }
  912. $out .= "</g>\n";
  913. return $out;
  914. }
  915. }
  916. /*
  917. * Nothing really special here. Container for representing text bits.
  918. */
  919. class A2S_SVGText {
  920. private $options;
  921. private $string;
  922. private $point;
  923. private $name;
  924. private static $id = 0;
  925. public function __construct($x, $y) {
  926. $this->point = new A2S_Point($x, $y);
  927. $this->name = self::$id++;
  928. $this->options = array();
  929. }
  930. public function setOption($opt, $val) {
  931. $this->options[$opt] = $val;
  932. }
  933. public function setID($id) {
  934. $this->name = str_replace(' ', '_', str_replace('"', '_', $id));
  935. }
  936. public function getID() {
  937. return $this->name;
  938. }
  939. public function getPoint() {
  940. return $this->point;
  941. }
  942. public function setString($string) {
  943. $this->string = $string;
  944. }
  945. public function render() {
  946. $out = "<text x=\"{$this->point->x}\" y=\"{$this->point->y}\" id=\"text{$this->name}\" ";
  947. foreach ($this->options as $opt => $val) {
  948. if (strpos($opt, 'a2s:', 0) === 0) {
  949. continue;
  950. }
  951. $out .= "$opt=\"$val\" ";
  952. }
  953. $out .= ">";
  954. $out .= htmlentities($this->string);
  955. $out .= "</text>\n";
  956. return $out;
  957. }
  958. }
  959. /*
  960. * Main class for parsing ASCII and constructing the SVG output based on the
  961. * above classes.
  962. */
  963. class A2S_ASCIIToSVG {
  964. private $rawData;
  965. private $grid;
  966. private $svgObjects;
  967. private $clearCorners;
  968. /* Directions for traversing lines in our grid */
  969. const DIR_UP = 0x1;
  970. const DIR_DOWN = 0x2;
  971. const DIR_LEFT = 0x4;
  972. const DIR_RIGHT = 0x8;
  973. const DIR_NE = 0x10;
  974. const DIR_SE = 0x20;
  975. public function __construct($data) {
  976. /* For debugging purposes */
  977. $this->rawData = $data;
  978. A2S_CustomObjects::loadObjects();
  979. $this->clearCorners = array();
  980. /*
  981. * Parse out any command references. These need to be at the bottom of the
  982. * diagram due to the way they're removed. Format is:
  983. * [identifier] optional-colon optional-spaces ({json-blob})\n
  984. *
  985. * The JSON blob may not contain objects as values or the regex will break.
  986. */
  987. $this->commands = array();
  988. preg_match_all('/^\[([^\]]+)\]:?\s+({[^}]+?})/ims', $data, $matches);
  989. $bound = count($matches[1]);
  990. for ($i = 0; $i < $bound; $i++) {
  991. $this->commands[$matches[1][$i]] = json_decode($matches[2][$i], true);
  992. }
  993. $data = preg_replace('/^\[([^\]]+)\](:?)\s+.*/ims', '', $data);
  994. /*
  995. * Treat our ASCII field as a grid and store each character as a point in
  996. * that grid. The (0, 0) coordinate on this grid is top-left, just as it
  997. * is in images.
  998. */
  999. $this->grid = explode("\n", $data);
  1000. foreach ($this->grid as $k => $line) {
  1001. $this->grid[$k] = str_split($line);
  1002. }
  1003. $this->svgObjects = new A2S_SVGGroup();
  1004. }
  1005. /*
  1006. * This is kind of a stupid and hacky way to do this, but this allows setting
  1007. * the default scale of one grid space on the X and Y axes.
  1008. */
  1009. public function setDimensionScale($x, $y) {
  1010. $o = A2S_Scale::getInstance();
  1011. $o->setScale($x, $y);
  1012. }
  1013. public function dump() {
  1014. var_export($this);
  1015. }
  1016. /* Render out what we've done! */
  1017. public function render() {
  1018. $o = A2S_Scale::getInstance();
  1019. /* Figure out how wide we need to make the canvas */
  1020. $canvasWidth = 0;
  1021. foreach($this->grid as $line) {
  1022. if (count($line) > $canvasWidth) {
  1023. $canvasWidth = count($line);
  1024. }
  1025. }
  1026. /* Add a fudge factor for drop-shadow and gaussian blur */
  1027. $canvasWidth = $canvasWidth * $o->xScale + 30;
  1028. $canvasHeight = count($this->grid) * $o->yScale + 30;
  1029. /*
  1030. * Boilerplate header with definitions that we might be using for markers
  1031. * and drop shadows.
  1032. */
  1033. $out = <<<SVG
  1034. <?xml version="1.0" standalone="no"?>
  1035. <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
  1036. "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
  1037. <!-- Created with A2S_ASCIIToSVG (http://9vx.org/~dho/a2s/) -->
  1038. <svg width="{$canvasWidth}px" height="{$canvasHeight}px" version="1.1"
  1039. xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  1040. <defs>
  1041. <filter id="dsFilter" width="150%" height="150%">
  1042. <feOffset result="offOut" in="SourceGraphic" dx="3" dy="3"/>
  1043. <feColorMatrix result="matrixOut" in="offOut" type="matrix" values="0.2 0 0 0 0 0 0.2 0 0 0 0 0 0.2 0 0 0 0 0 1 0"/>
  1044. <feGaussianBlur result="blurOut" in="matrixOut" stdDeviation="3"/>
  1045. <feBlend in="SourceGraphic" in2="blurOut" mode="normal"/>
  1046. </filter>
  1047. <marker id="iPointer"
  1048. viewBox="0 0 10 10" refX="5" refY="5"
  1049. markerUnits="strokeWidth"
  1050. markerWidth="8" markerHeight="7"
  1051. orient="auto">
  1052. <path d="M 10 0 L 10 10 L 0 5 z" />
  1053. </marker>
  1054. <marker id="Pointer"
  1055. viewBox="0 0 10 10" refX="5" refY="5"
  1056. markerUnits="strokeWidth"
  1057. markerWidth="8" markerHeight="7"
  1058. orient="auto">
  1059. <path d="M 0 0 L 10 5 L 0 10 z" />
  1060. </marker>
  1061. </defs>
  1062. SVG;
  1063. /* Render the group, everything lives in there */
  1064. $out .= $this->svgObjects->render();
  1065. $out .= "</svg>\n";
  1066. return $out;
  1067. }
  1068. /*
  1069. * Parsing the grid is a multi-step process. We parse out boxes first, as
  1070. * this makes it easier to then parse lines. By parse out, I do mean we
  1071. * parse them and then remove them. This does mean that a complete line
  1072. * will not travel along the edge of a box, but you probably won't notice
  1073. * unless the box is curved anyway. While edges are removed, points are
  1074. * not. This means that you can cleanly allow lines to intersect boxes
  1075. * (as long as they do not bisect!
  1076. *
  1077. * After parsing boxes and lines, we remove the corners from the grid. At
  1078. * this point, all we have left should be text, which we can pick up and
  1079. * place.
  1080. */
  1081. public function parseGrid() {
  1082. $this->parseBoxes();
  1083. $this->parseLines();
  1084. foreach ($this->clearCorners as $corner) {
  1085. $this->grid[$corner[0]][$corner[1]] = ' ';
  1086. }
  1087. $this->parseText();
  1088. $this->injectCommands();
  1089. }
  1090. /*
  1091. * Ahh, good ol' box parsing. We do this by scanning each row for points and
  1092. * attempting to close the shape. Since the approach is first horizontal,
  1093. * then vertical, we complete the shape in a clockwise order (which is
  1094. * important for the Bezier curve generation.
  1095. */
  1096. private function parseBoxes() {
  1097. /* Set up our box group */
  1098. $this->svgObjects->pushGroup('boxes');
  1099. $this->svgObjects->setOption('stroke', 'black');
  1100. $this->svgObjects->setOption('stroke-width', '2');
  1101. $this->svgObjects->setOption('fill', 'none');
  1102. /* Scan the grid for corners */
  1103. foreach ($this->grid as $row => $line) {
  1104. foreach ($line as $col => $char) {
  1105. if ($this->isCorner($char)) {
  1106. $path = new A2S_SVGPath();
  1107. if ($char == '.' || $char == "'") {
  1108. $path->addPoint($col, $row, A2S_Point::CONTROL);
  1109. } else {
  1110. $path->addPoint($col, $row);
  1111. }
  1112. /*
  1113. * The wall follower is a left-turning, marking follower. See that
  1114. * function for more information on how it works.
  1115. */
  1116. $this->wallFollow($path, $row, $col+1, self::DIR_RIGHT);
  1117. /* We only care about closed polygons */
  1118. if ($path->isClosed()) {
  1119. $path->orderPoints();
  1120. $skip = false;
  1121. /*
  1122. * The walking code can find the same box from a different edge:
  1123. *
  1124. * +---+ +---+
  1125. * | | | |
  1126. * | +---+ |
  1127. * +-----------+
  1128. *
  1129. * so ignore adding a box that we've already added.
  1130. */
  1131. foreach($this->svgObjects->getGroup('boxes') as $box) {
  1132. $bP = $box->getPoints();
  1133. $pP = $path->getPoints();
  1134. $pPoints = count($pP);
  1135. $shared = 0;
  1136. /*
  1137. * If the boxes don't have the same number of edges, they
  1138. * obviously cannot be the same box.
  1139. */
  1140. if (count($bP) != $pPoints) {
  1141. continue;
  1142. }
  1143. /* Traverse the vertices of this new box... */
  1144. for ($i = 0; $i < $pPoints; $i++) {
  1145. /* ...and find them in this existing box. */
  1146. for ($j = 0; $j < $pPoints; $j++) {
  1147. if ($pP[$i]->x == $bP[$j]->x && $pP[$i]->y == $bP[$j]->y) {
  1148. $shared++;
  1149. }
  1150. }
  1151. }
  1152. /* If all the edges are in common, it's the same shape. */
  1153. if ($shared == count($bP)) {
  1154. $skip = true;
  1155. break;
  1156. }
  1157. }
  1158. if ($skip == false) {
  1159. /* Search for any references for styling this polygon; add it */
  1160. $path->setOption('filter', 'url(#dsFilter)');
  1161. $name = $this->findCommands($path);
  1162. if ($name != '') {
  1163. $path->setID($name);
  1164. }
  1165. $this->svgObjects->addObject($path);
  1166. }
  1167. }
  1168. }
  1169. }
  1170. }
  1171. /*
  1172. * Once we've found all the boxes, we want to remove them from the grid so
  1173. * that they don't confuse the line parser. However, we don't remove any
  1174. * corner characters because these might be shared by lines.
  1175. */
  1176. foreach ($this->svgObjects->getGroup('boxes') as $box) {
  1177. $this->clearObject($box);
  1178. }
  1179. /* Anything after this is not a subgroup */
  1180. $this->svgObjects->popGroup();
  1181. }
  1182. /*
  1183. * Our line parser operates differently than the polygon parser. This is
  1184. * because lines are not intrinsically marked with starting points (markers
  1185. * are optional) -- they just sort of begin. Additionally, so that markers
  1186. * will work, we can't just construct a line from some random point: we need
  1187. * to start at the correct edge.
  1188. *
  1189. * Thus, the line parser traverses vertically first, then horizontally. Once
  1190. * a line is found, it is cleared immediately (but leaving any control points
  1191. * in case there were any intersections.
  1192. */
  1193. private function parseLines() {
  1194. /* Set standard line options */
  1195. $this->svgObjects->pushGroup('lines');
  1196. $this->svgObjects->setOption('stroke', 'black');
  1197. $this->svgObjects->setOption('stroke-width', '2');
  1198. $this->svgObjects->setOption('fill', 'none');
  1199. /* The grid is not uniform, so we need to determine the longest row. */
  1200. $maxCols = 0;
  1201. $bound = count($this->grid);
  1202. for ($r = 0; $r < $bound; $r++) {
  1203. $maxCols = max($maxCols, count($this->grid[$r]));
  1204. }
  1205. for ($c = 0; $c < $maxCols; $c++) {
  1206. for ($r = 0; $r < $bound; $r++) {
  1207. /* This gets set if we find a line-start here. */
  1208. $dir = false;
  1209. $line = new A2S_SVGPath();
  1210. /*
  1211. * Since the column count isn't uniform, don't attempt to handle any
  1212. * rows that don't extend out this far.
  1213. */
  1214. if (!isset($this->grid[$r][$c])) {
  1215. continue;
  1216. }
  1217. $char = $this->getChar($r, $c);
  1218. switch ($char) {
  1219. /*
  1220. * Do marker characters first. These are the easiest because they are
  1221. * basically guaranteed to represent the start of the line.
  1222. */
  1223. case '<':
  1224. $e = $this->getChar($r, $c + 1);
  1225. if ($this->isEdge($e, self::DIR_RIGHT) || $this->isCorner($e)) {
  1226. $line->addMarker($c, $r, A2S_Point::IMARKER);
  1227. $dir = self::DIR_RIGHT;
  1228. } else {
  1229. $se = $this->getChar($r + 1, $c + 1);
  1230. $ne = $this->getChar($r - 1, $c + 1);
  1231. if ($se == "\\") {
  1232. $line->addMarker($c, $r, A2S_Point::IMARKER);
  1233. $dir = self::DIR_SE;
  1234. } elseif ($ne == '/') {
  1235. $line->addMarker($c, $r, A2S_Point::IMARKER);
  1236. $dir = self::DIR_NE;
  1237. }
  1238. }
  1239. break;
  1240. case '^':
  1241. $s = $this->getChar($r + 1, $c);
  1242. if ($this->isEdge($s, self::DIR_DOWN) || $this->isCorner($s)) {
  1243. $line->addMarker($c, $r, A2S_Point::IMARKER);
  1244. $dir = self::DIR_DOWN;
  1245. } elseif ($this->getChar($r + 1, $c + 1) == "\\") {
  1246. /* Don't need to check west for diagonals. */
  1247. $line->addMarker($c, $r, A2S_Point::IMARKER);
  1248. $dir = self::DIR_SE;
  1249. }
  1250. break;
  1251. case '>':
  1252. $w = $this->getChar($r, $c - 1);
  1253. if ($this->isEdge($w, self::DIR_LEFT) || $this->isCorner($w)) {
  1254. $line->addMarker($c, $r, A2S_Point::IMARKER);
  1255. $dir = self::DIR_LEFT;
  1256. }
  1257. /* All diagonals come from west, so we don't need to check */
  1258. break;
  1259. case 'v':
  1260. $n = $this->getChar($r - 1, $c);
  1261. if ($this->isEdge($n, self::DIR_UP) || $this->isCorner($n)) {
  1262. $line->addMarker($c, $r, A2S_Point::IMARKER);
  1263. $dir = self::DIR_UP;
  1264. } elseif ($this->getChar($r - 1, $c + 1) == '/') {
  1265. $line->addMarker($c, $r, A2S_Point::IMARKER);
  1266. $dir = self::DIR_NE;
  1267. }
  1268. break;
  1269. /*
  1270. * Edges are handled specially. We have to look at the context of the
  1271. * edge to determine whether it's the start of a line. A vertical edge
  1272. * can appear as the start of a line in the following circumstances:
  1273. *
  1274. * +------------- +-------------- +---- | (s)
  1275. * | | | |
  1276. * | | (s) +-------+ |(s) |
  1277. * +------+ | (s)
  1278. *
  1279. * From this we can extrapolate that we are a starting edge if our
  1280. * southern neighbor is a vertical edge or corner, but we have no line
  1281. * material to our north (and vice versa). This logic does allow for
  1282. * the southern / northern neighbor to be part of a separate
  1283. * horizontal line.
  1284. */
  1285. case ':':
  1286. $line->setOption('stroke-dasharray', '5 5');
  1287. /* FALLTHROUGH */
  1288. case '|':
  1289. $n = $this->getChar($r-1, $c);
  1290. $s = $this->getChar($r+1, $c);
  1291. if (($s == '|' || $s == ':' || $this->isCorner($s)) &&
  1292. $n != '|' && $n != ':' && !$this->isCorner($n) &&
  1293. $n != '^') {
  1294. $dir = self::DIR_DOWN;
  1295. } elseif (($n == '|' || $n == ':' || $this->isCorner($n)) &&
  1296. $s != '|' && $s != ':' && !$this->isCorner($s) &&
  1297. $s != 'v') {
  1298. $dir = self::DIR_UP;
  1299. }
  1300. break;
  1301. /*
  1302. * Horizontal edges have the same properties for search as vertical
  1303. * edges, except we need to look east / west. The diagrams for the
  1304. * vertical case are still accurate to visualize this case; just
  1305. * mentally turn them 90 degrees clockwise.
  1306. */
  1307. case '=':
  1308. $line->setOption('stroke-dasharray', '5 5');
  1309. /* FALLTHROUGH */
  1310. case '-':
  1311. $w = $this->getChar($r, $c-1);
  1312. $e = $this->getChar($r, $c+1);
  1313. if (($w == '-' || $w == '=' || $this->isCorner($w)) &&
  1314. $e != '=' && $e != '-' && !$this->isCorner($e) &&
  1315. $e != '>') {
  1316. $dir = self::DIR_LEFT;
  1317. } elseif (($e == '-' || $e == '=' || $this->isCorner($e)) &&
  1318. $w != '=' && $w != '-' && !$this->isCorner($w) &&
  1319. $w != '<') {
  1320. $dir = self::DIR_RIGHT;
  1321. }
  1322. break;
  1323. /*
  1324. * We can only find diagonals going north or south and east. This is
  1325. * simplified due to the fact that they have no corners. We are
  1326. * guaranteed to run into their westernmost point or their relevant
  1327. * marker.
  1328. */
  1329. case '/':
  1330. $ne = $this->getChar($r-1, $c+1);
  1331. if ($ne == '/' || $ne == '^' || $ne == '>') {
  1332. $dir = self::DIR_NE;
  1333. }
  1334. break;
  1335. case "\\":
  1336. $se = $this->getChar($r+1, $c+1);
  1337. if ($se == "\\" || $se == "v" || $se == '>') {
  1338. $dir = self::DIR_SE;
  1339. }
  1340. break;
  1341. /*
  1342. * The corner case must consider all four directions. Though a
  1343. * reasonable person wouldn't use slant corners for this, they are
  1344. * considered corners, so it kind of makes sense to handle them the
  1345. * same way. For this case, envision the starting point being a corner
  1346. * character in both the horizontal and vertical case. And then
  1347. * mentally overlay them and consider that :).
  1348. */
  1349. case '+':
  1350. case '#':
  1351. $ne = $this->getChar($r-1, $c+1);
  1352. $se = $this->getChar($r+1, $c+1);
  1353. if ($ne == '/' || $ne == '^' || $ne == '>') {
  1354. $dir = self::DIR_NE;
  1355. } elseif ($se == "\\" || $se == "v" || $se == '>') {
  1356. $dir = self::DIR_SE;
  1357. }
  1358. /* FALLTHROUGH */
  1359. case '.':
  1360. case "'":
  1361. $n = $this->getChar($r-1, $c);
  1362. $w = $this->getChar($r, $c-1);
  1363. $s = $this->getChar($r+1, $c);
  1364. $e = $this->getChar($r, $c+1);
  1365. if (($w == '=' || $w == '-') && $n != '|' && $n != ':' && $w != '-' &&
  1366. $e != '=' && $e != '|' && $s != ':') {
  1367. $dir = self::DIR_LEFT;
  1368. } elseif (($e == '=' || $e == '-') && $n != '|' && $n != ':' &&
  1369. $w != '-' && $w != '=' && $s != '|' && $s != ':') {
  1370. $dir = self::DIR_RIGHT;
  1371. } elseif (($s == '|' || $s == ':') && $n != '|' && $n != ':' &&
  1372. $w != '-' && $w != '=' && $e != '-' && $e != '=' &&
  1373. (($char != '.' && $char != "'") ||
  1374. ($char == '.' && $s != '.') ||
  1375. ($char == "'" && $s != "'"))) {
  1376. $dir = self::DIR_DOWN;
  1377. } elseif (($n == '|' || $n == ':') && $s != '|' && $s != ':' &&
  1378. $w != '-' && $w != '=' && $e != '-' && $e != '=' &&
  1379. (($char != '.' && $char != "'") ||
  1380. ($char == '.' && $s != '.') ||
  1381. ($char == "'" && $s != "'"))) {
  1382. $dir = self::DIR_UP;
  1383. }
  1384. break;
  1385. }
  1386. /* It does actually save lines! */
  1387. if ($dir !== false) {
  1388. $rInc = 0; $cInc = 0;
  1389. if (!$this->isMarker($char)) {
  1390. $line->addPoint($c, $r);
  1391. }
  1392. /*
  1393. * The walk routine may attempt to add the point again, so skip it.
  1394. * If we don't, we can miss the line or end up with just a point.
  1395. */
  1396. if ($dir == self::DIR_UP) {
  1397. $rInc = -1; $cInc = 0;
  1398. } elseif ($dir == self::DIR_DOWN) {
  1399. $rInc = 1; $cInc = 0;
  1400. } elseif ($dir == self::DIR_RIGHT) {
  1401. $rInc = 0; $cInc = 1;
  1402. } elseif ($dir == self::DIR_LEFT) {
  1403. $rInc = 0; $cInc = -1;
  1404. } elseif ($dir == self::DIR_NE) {
  1405. $rInc = -1; $cInc = 1;
  1406. } elseif ($dir == self::DIR_SE) {
  1407. $rInc = 1; $cInc = 1;
  1408. }
  1409. /*
  1410. * Walk the points of this line. Note we don't use wallFollow; we are
  1411. * operating under the assumption that lines do not meander. (And, in
  1412. * any event, that algorithm is intended to find a closed object.)
  1413. */
  1414. $this->walk($line, $r+$rInc, $c+$cInc, $dir);
  1415. /*
  1416. * Remove it so that we don't confuse any other lines. This leaves
  1417. * corners in tact, still.
  1418. */
  1419. $this->clearObject($line);
  1420. $this->svgObjects->addObject($line);
  1421. }
  1422. }
  1423. }
  1424. $this->svgObjects->popGroup();
  1425. }
  1426. /*
  1427. * Look for text in a file. If the text appears in a box that has a dark
  1428. * fill, we want to give it a light fill (and vice versa). This means we
  1429. * have to figure out what box it lives in (if any) and do all sorts of
  1430. * color calculation magic.
  1431. */
  1432. private function parseText() {
  1433. $o = A2S_Scale::getInstance();
  1434. /*
  1435. * The style options deserve some comments. The monospace and font-size
  1436. * choices are not accidental. This gives the best sort of estimation
  1437. * for font size to scale that I could come up with empirically.
  1438. *
  1439. * N.B. This might change with different scales. I kind of feel like this
  1440. * is a bug waiting to be filed, but whatever.
  1441. */
  1442. $fSize = 0.95*$o->yScale;
  1443. $this->svgObjects->pushGroup('text');
  1444. $this->svgObjects->setOption('fill', 'black');
  1445. $this->svgObjects->setOption('style',
  1446. "font-family:Consolas,Monaco,Anonymous Pro,Anonymous,Bitstream Sans Mono,monospace;font-size:{$fSize}px");
  1447. /*
  1448. * Text gets the same scanning treatment as boxes. We do left-to-right
  1449. * scanning, which should probably be configurable in case someone wants
  1450. * to use this with e.g. Arabic or some other right-to-left language.
  1451. * Either way, this isn't UTF-8 safe (thanks, PHP!!!), so that'll require
  1452. * thought regardless.
  1453. */
  1454. $boxes = $this->svgObjects->getGroup('boxes');
  1455. $bound = count($boxes);
  1456. foreach ($this->grid as $row => $line) {
  1457. $cols = count($line);
  1458. for ($i = 0; $i < $cols; $i++) {
  1459. if ($this->getChar($row, $i) != ' ') {
  1460. /* More magic numbers that probably need research. */
  1461. $t = new A2S_SVGText($i - .6, $row + 0.3);
  1462. /* Time to figure out which (if any) box we live inside */
  1463. $tP = $t->getPoint();
  1464. $maxPoint = new A2S_Point(-1, -1);
  1465. $boxQueue = array();
  1466. for ($j = 0; $j < $bound; $j++) {
  1467. if ($boxes[$j]->hasPoint($tP->gridX, $tP->gridY)) {
  1468. $boxPoints = $boxes[$j]->getPoints();
  1469. $boxTL = $boxPoints[0];
  1470. /*
  1471. * This text is in this box, but it may still be in a more
  1472. * specific nested box. Find the box with the highest top
  1473. * left X,Y coordinate. Keep a queue of boxes in case the top
  1474. * most box doesn't have a fill.
  1475. */
  1476. if ($boxTL->y > $maxPoint->y && $boxTL->x > $maxPoint->x) {
  1477. $maxPoint->x = $boxTL->x;
  1478. $maxPoint->y = $boxTL->y;
  1479. $boxQueue[] = $boxes[$j];
  1480. }
  1481. }
  1482. }
  1483. if (count($boxQueue) > 0) {
  1484. /*
  1485. * Work backwards through the boxes to find the box with the most
  1486. * specific fill.
  1487. */
  1488. for ($j = count($boxQueue) - 1; $j >= 0; $j--) {
  1489. $fill = $boxQueue[$j]->getOption('fill');
  1490. if ($fill == 'none' || $fill == null) {
  1491. continue;
  1492. }
  1493. if (substr($fill, 0, 1) != '#') {
  1494. if (!isset($GLOBALS['A2S_colors'][strtolower($fill)])) {
  1495. continue;
  1496. } else {
  1497. $fill = $GLOBALS['A2S_colors'][strtolower($fill)];
  1498. }
  1499. } else {
  1500. if (strlen($fill) != 4 && strlen($fill) != 7) {
  1501. continue;
  1502. }
  1503. }
  1504. if ($fill) {
  1505. /* Attempt to parse the fill color */
  1506. if (strlen($fill) == 4) {
  1507. $cR = hexdec(str_repeat($fill[1], 2));
  1508. $cG = hexdec(str_repeat($fill[2], 2));
  1509. $cB = hexdec(str_repeat($fill[3], 2));
  1510. } elseif (strlen($fill) == 7) {
  1511. $cR = hexdec(substr($fill, 1, 2));
  1512. $cG = hexdec(substr($fill, 3, 2));
  1513. $cB = hexdec(substr($fill, 5, 2));
  1514. }
  1515. /*
  1516. * This magic is gleaned from the working group paper on
  1517. * accessibility at http://www.w3.org/TR/AERT. The recommended
  1518. * contrast is a brightness difference of at least 125 and a
  1519. * color difference of at least 500. Since our default color
  1520. * is black, that makes the color difference easier.
  1521. */
  1522. $bFill = (($cR * 299) + ($cG * 587) + ($cB * 114)) / 1000;
  1523. $bDiff = $cR + $cG + $cB;
  1524. $bText = 0;
  1525. if ($bFill - $bText < 125 || $bDiff < 500) {
  1526. /* If black is too dark, white will work */
  1527. $t->setOption('fill', '#fff');
  1528. } else {
  1529. $t->setOption('fill', '#000');
  1530. }
  1531. break;
  1532. }
  1533. }
  1534. if ($j < 0) {
  1535. $t->setOption('fill', '#000');
  1536. }
  1537. } else {
  1538. /* This text isn't inside a box; make it black */
  1539. $t->setOption('fill', '#000');
  1540. }
  1541. /* We found a stringy character, eat it and the rest. */
  1542. $str = $this->getChar($row, $i++);
  1543. while ($i < count($line) && $this->getChar($row, $i) != ' ') {
  1544. $str .= $this->getChar($row, $i++);
  1545. /* Eat up to 1 space */
  1546. if ($this->getChar($row, $i) == ' ') {
  1547. $str .= ' ';
  1548. $i++;
  1549. }
  1550. }
  1551. if ($str == '') {
  1552. continue;
  1553. }
  1554. $t->setString($str);
  1555. /*
  1556. * If we were in a box, group with the box. Otherwise it gets its
  1557. * own group.
  1558. */
  1559. if (count($boxQueue) > 0) {
  1560. $t->setOption('stroke', 'none');
  1561. $t->setOption('style',
  1562. "font-family:Consolas,Monaco,Anonymous Pro,Anonymous,Bitstream Sans Mono,monospace;font-size:{$fSize}px");
  1563. $boxQueue[count($boxQueue) - 1]->addText($t);
  1564. } else {
  1565. $this->svgObjects->addObject($t);
  1566. }
  1567. }
  1568. }
  1569. }
  1570. }
  1571. /*
  1572. * Allow specifying references that target an object starting at grid point
  1573. * (ROW,COL). This allows styling of lines, boxes, or any text object.
  1574. */
  1575. private function injectCommands() {
  1576. $boxes = $this->svgObjects->getGroup('boxes');
  1577. $lines = $this->svgObjects->getGroup('lines');
  1578. $text = $this->svgObjects->getGroup('text');
  1579. foreach ($boxes as $obj) {
  1580. $objPoints = $obj->getPoints();
  1581. $pointCmd = "{$objPoints[0]->gridY},{$objPoints[0]->gridX}";
  1582. if (isset($this->commands[$pointCmd])) {
  1583. $obj->setOptions($this->commands[$pointCmd]);
  1584. }
  1585. foreach ($obj->getText() as $text) {
  1586. $textPoint = $text->getPoint();
  1587. $pointCmd = "{$textPoint->gridY},{$textPoint->gridX}";
  1588. if (isset($this->commands[$pointCmd])) {
  1589. $text->setOptions($this->commands[$pointCmd]);
  1590. }
  1591. }
  1592. }
  1593. foreach ($lines as $obj) {
  1594. $objPoints = $obj->getPoints();
  1595. $pointCmd = "{$objPoints[0]->gridY},{$objPoints[0]->gridX}";
  1596. if (isset($this->commands[$pointCmd])) {
  1597. $obj->setOptions($this->commands[$pointCmd]);
  1598. }
  1599. }
  1600. foreach ($text as $obj) {
  1601. $objPoint = $obj->getPoint();
  1602. $pointCmd = "{$objPoint->gridY},{$objPoint->gridX}";
  1603. if (isset($this->commands[$pointCmd])) {
  1604. $obj->setOptions($this->commands[$pointCmd]);
  1605. }
  1606. }
  1607. }
  1608. /*
  1609. * A generic, recursive line walker. This walker makes the assumption that
  1610. * lines want to go in the direction that they are already heading. I'm
  1611. * sure that there are ways to formulate lines to screw this walker up,
  1612. * but it does a good enough job right now.
  1613. */
  1614. private function walk($path, $row, $col, $dir, $d = 0) {
  1615. $d++;
  1616. $r = $row;
  1617. $c = $col;
  1618. if ($dir == self::DIR_RIGHT || $dir == self::DIR_LEFT) {
  1619. $cInc = ($dir == self::DIR_RIGHT) ? 1 : -1;
  1620. $rInc = 0;
  1621. } elseif ($dir == self::DIR_DOWN || $dir == self::DIR_UP) {
  1622. $cInc = 0;
  1623. $rInc = ($dir == self::DIR_DOWN) ? 1 : -1;
  1624. } elseif ($dir == self::DIR_SE || $dir == self::DIR_NE) {
  1625. $cInc = 1;
  1626. $rInc = ($dir == self::DIR_SE) ? 1 : -1;
  1627. }
  1628. /* Follow the edge for as long as we can */
  1629. $cur = $this->getChar($r, $c);
  1630. while ($this->isEdge($cur, $dir)) {
  1631. if ($cur == ':' || $cur == '=') {
  1632. $path->setOption('stroke-dasharray', '5 5');
  1633. }
  1634. $c += $cInc;
  1635. $r += $rInc;
  1636. $cur = $this->getChar($r, $c);
  1637. }
  1638. if ($this->isCorner($cur)) {
  1639. if ($cur == '.' || $cur == "'") {
  1640. $path->addPoint($c, $r, A2S_Point::CONTROL);
  1641. } else {
  1642. $path->addPoint($c, $r);
  1643. }
  1644. if ($path->isClosed()) {
  1645. $path->popPoint();
  1646. return;
  1647. }
  1648. /*
  1649. * Attempt first to continue in the current direction. If we can't,
  1650. * try to go in any direction other than the one opposite of where
  1651. * we just came from -- no backtracking.
  1652. */
  1653. $n = $this->getChar($r - 1, $c);
  1654. $s = $this->getChar($r + 1, $c);
  1655. $e = $this->getChar($r, $c + 1);
  1656. $w = $this->getChar($r, $c - 1);
  1657. $next = $this->getChar($r + $rInc, $c + $cInc);
  1658. $se = $this->getChar($r + 1, $c + 1);
  1659. $ne = $this->getChar($r - 1, $c + 1);
  1660. if ($this->isCorner($next) || $this->isEdge($next, $dir)) {
  1661. return $this->walk($path, $r + $rInc, $c + $cInc, $dir, $d);
  1662. } elseif ($dir != self::DIR_DOWN &&
  1663. ($this->isCorner($n) || $this->isEdge($n, self::DIR_UP))) {
  1664. /* Can't turn up into bottom corner */
  1665. if (($cur != '.' && $cur != "'") || ($cur == '.' && $n != '.') ||
  1666. ($cur == "'" && $n != "'")) {
  1667. return $this->walk($path, $r - 1, $c, self::DIR_UP, $d);
  1668. }
  1669. } elseif ($dir != self::DIR_UP &&
  1670. ($this->isCorner($s) || $this->isEdge($s, self::DIR_DOWN))) {
  1671. /* Can't turn down into top corner */
  1672. if (($cur != '.' && $cur != "'") || ($cur == '.' && $s != '.') ||
  1673. ($cur == "'" && $s != "'")) {
  1674. return $this->walk($path, $r + 1, $c, self::DIR_DOWN, $d);
  1675. }
  1676. } elseif ($dir != self::DIR_LEFT &&
  1677. ($this->isCorner($e) || $this->isEdge($e, self::DIR_RIGHT))) {
  1678. return $this->walk($path, $r, $c + 1, self::DIR_RIGHT, $d);
  1679. } elseif ($dir != self::DIR_RIGHT &&
  1680. ($this->isCorner($w) || $this->isEdge($w, self::DIR_LEFT))) {
  1681. return $this->walk($path, $r, $c - 1, self::DIR_LEFT, $d);
  1682. } elseif ($dir == self::DIR_SE &&
  1683. ($this->isCorner($ne) || $this->isEdge($ne, self::DIR_NE))) {
  1684. return $this->walk($path, $r - 1, $c + 1, self::DIR_NE, $d);
  1685. } elseif ($dir == self::DIR_NE &&
  1686. ($this->isCorner($se) || $this->isEdge($se, self::DIR_SE))) {
  1687. return $this->walk($path, $r + 1, $c + 1, self::DIR_SE, $d);
  1688. }
  1689. } elseif ($this->isMarker($cur)) {
  1690. /* We found a marker! Add it. */
  1691. $path->addMarker($c, $r, A2S_Point::SMARKER);
  1692. return;
  1693. } else {
  1694. /*
  1695. * Not a corner, not a marker, and we already ate edges. Whatever this
  1696. * is, it is not part of the line.
  1697. */
  1698. $path->addPoint($c, $r);
  1699. return;
  1700. }
  1701. }
  1702. /*
  1703. * This function attempts to follow a line and complete it into a closed
  1704. * polygon. It assumes that we have been called from a top point, and in any
  1705. * case that the polygon can be found by moving clockwise along its edges.
  1706. * Any time this algorithm finds a corner, it attempts to turn right. If it
  1707. * cannot turn right, it goes in any direction other than the one it came
  1708. * from. If it cannot complete the polygon by continuing in any direction
  1709. * from a point, that point is removed from the path, and we continue on
  1710. * from the previous point (since this is a recursive function).
  1711. *
  1712. * Because the function assumes that it is starting from the top left,
  1713. * if its first turn cannot be a right turn to moving down, the object
  1714. * cannot be a valid polygon. It also maintains an internal list of points
  1715. * it has already visited, and refuses to visit any point twice.
  1716. */
  1717. private function wallFollow($path, $r, $c, $dir, $bucket = array(), $d = 0) {
  1718. $d++;
  1719. if ($dir == self::DIR_RIGHT || $dir == self::DIR_LEFT) {
  1720. $cInc = ($dir == self::DIR_RIGHT) ? 1 : -1;
  1721. $rInc = 0;
  1722. } elseif ($dir == self::DIR_DOWN || $dir == self::DIR_UP) {
  1723. $cInc = 0;
  1724. $rInc = ($dir == self::DIR_DOWN) ? 1 : -1;
  1725. }
  1726. /* Traverse the edge in whatever direction we are going. */
  1727. $cur = $this->getChar($r, $c);
  1728. while ($this->isBoxEdge($cur, $dir)) {
  1729. $r += $rInc;
  1730. $c += $cInc;
  1731. $cur = $this->getChar($r, $c);
  1732. }
  1733. /* We 'key' our location by catting r and c together */
  1734. $key = "{$r}{$c}";
  1735. if (isset($bucket[$key])) {
  1736. return;
  1737. }
  1738. /*
  1739. * When we run into a corner, we have to make a somewhat complicated
  1740. * decision about which direction to turn.
  1741. */
  1742. if ($this->isBoxCorner($cur)) {
  1743. if (!isset($bucket[$key])) {
  1744. $bucket[$key] = 0;
  1745. }
  1746. switch ($cur) {
  1747. case '.':
  1748. case "'":
  1749. $pointExists = $path->addPoint($c, $r, A2S_Point::CONTROL);
  1750. break;
  1751. case '#':
  1752. $pointExists = $path->addPoint($c, $r);
  1753. break;
  1754. }
  1755. if ($path->isClosed() || $pointExists) {
  1756. return;
  1757. }
  1758. /*
  1759. * Special case: if we're looking for our first turn and we can't make it
  1760. * due to incompatible corners, keep looking, but don't adjust our call
  1761. * depth so that we can continue to make progress.
  1762. */
  1763. if ($d == 1 && $cur == '.' && $this->getChar($r + 1, $c) == '.') {
  1764. return $this->wallFollow($path, $r, $c + 1, $dir, $bucket, 0);
  1765. }
  1766. /*
  1767. * We need to make a decision here on where to turn. We may have multiple
  1768. * directions we can choose, and all of them might generate a closed
  1769. * object. Always try turning right first.
  1770. */
  1771. $newDir = false;
  1772. $n = $this->getChar($r - 1, $c);
  1773. $s = $this->getChar($r + 1, $c);
  1774. $e = $this->getChar($r, $c + 1);
  1775. $w = $this->getChar($r, $c - 1);
  1776. if ($dir == self::DIR_RIGHT) {
  1777. if (!($bucket[$key] & self::DIR_DOWN) &&
  1778. ($this->isBoxEdge($s, self::DIR_DOWN) || $this->isBoxCorner($s))) {
  1779. /* We can't turn into another top edge. */
  1780. if (($cur != '.' && $cur != "'") || ($cur == '.' && $s != '.') ||
  1781. ($cur == "'" && $s != "'")) {
  1782. $newDir = self::DIR_DOWN;
  1783. }
  1784. } else {
  1785. /* There is no right hand turn for us; this isn't a valid start */
  1786. if ($d == 1) {
  1787. return;
  1788. }
  1789. }
  1790. } elseif ($dir == self::DIR_DOWN) {
  1791. if (!($bucket[$key] & self::DIR_LEFT) &&
  1792. ($this->isBoxEdge($w, self::DIR_LEFT) || $this->isBoxCorner($w))) {
  1793. $newDir == self::DIR_LEFT;
  1794. }
  1795. } elseif ($dir == self::DIR_LEFT) {
  1796. if (!($bucket[$key] & self::DIR_UP) &&
  1797. ($this->isBoxEdge($n, self::DIR_UP) || $this->isBoxCorner($n))) {
  1798. /* We can't turn into another bottom edge. */
  1799. if (($cur != '.' && $cur != "'") || ($cur == '.' && $n != '.') ||
  1800. ($cur == "'" && $n != "'")) {
  1801. $newDir = self::DIR_UP;
  1802. }
  1803. }
  1804. } elseif ($dir == self::DIR_UP) {
  1805. if (!($bucket[$key] & self::DIR_RIGHT) &&
  1806. ($this->isBoxEdge($e, self::DIR_RIGHT) || $this->isBoxCorner($e))) {
  1807. $newDir = self::DIR_RIGHT;
  1808. }
  1809. }
  1810. if ($newDir != false) {
  1811. if ($newDir == self::DIR_RIGHT || $newDir == self::DIR_LEFT) {
  1812. $cMod = ($newDir == self::DIR_RIGHT) ? 1 : -1;
  1813. $rMod = 0;
  1814. } elseif ($newDir == self::DIR_DOWN || $newDir == self::DIR_UP) {
  1815. $cMod = 0;
  1816. $rMod = ($newDir == self::DIR_DOWN) ? 1 : -1;
  1817. }
  1818. $bucket[$key] |= $newDir;
  1819. $this->wallFollow($path, $r+$rMod, $c+$cMod, $newDir, $bucket, $d);
  1820. if ($path->isClosed()) {
  1821. return;
  1822. }
  1823. }
  1824. /*
  1825. * Unfortunately, we couldn't complete the search by turning right,
  1826. * so we need to pick a different direction. Note that this will also
  1827. * eventually cause us to continue in the direction we were already
  1828. * going. We make sure that we don't go in the direction opposite of
  1829. * the one in which we're already headed, or an any direction we've
  1830. * already travelled for this point (we may have hit it from an
  1831. * earlier branch). We accept the first closing polygon as the
  1832. * "correct" one for this object.
  1833. */
  1834. if ($dir != self::DIR_RIGHT && !($bucket[$key] & self::DIR_LEFT) &&
  1835. ($this->isBoxEdge($w, self::DIR_LEFT) || $this->isBoxCorner($w))) {
  1836. $bucket[$key] |= self::DIR_LEFT;
  1837. $this->wallFollow($path, $r, $c - 1, self::DIR_LEFT, $bucket, $d);
  1838. if ($path->isClosed()) {
  1839. return;
  1840. }
  1841. }
  1842. if ($dir != self::DIR_LEFT && !($bucket[$key] & self::DIR_RIGHT) &&
  1843. ($this->isBoxEdge($e, self::DIR_RIGHT) || $this->isBoxCorner($e))) {
  1844. $bucket[$key] |= self::DIR_RIGHT;
  1845. $this->wallFollow($path, $r, $c + 1, self::DIR_RIGHT, $bucket, $d);
  1846. if ($path->isClosed()) {
  1847. return;
  1848. }
  1849. }
  1850. if ($dir != self::DIR_DOWN && !($bucket[$key] & self::DIR_UP) &&
  1851. ($this->isBoxEdge($n, self::DIR_UP) || $this->isBoxCorner($n))) {
  1852. if (($cur != '.' && $cur != "'") || ($cur == '.' && $n != '.') ||
  1853. ($cur == "'" && $n != "'")) {
  1854. /* We can't turn into another bottom edge. */
  1855. $bucket[$key] |= self::DIR_UP;
  1856. $this->wallFollow($path, $r - 1, $c, self::DIR_UP, $bucket, $d);
  1857. if ($path->isClosed()) {
  1858. return;
  1859. }
  1860. }
  1861. }
  1862. if ($dir != self::DIR_UP && !($bucket[$key] & self::DIR_DOWN) &&
  1863. ($this->isBoxEdge($s, self::DIR_DOWN) || $this->isBoxCorner($s))) {
  1864. if (($cur != '.' && $cur != "'") || ($cur == '.' && $s != '.') ||
  1865. ($cur == "'" && $s != "'")) {
  1866. /* We can't turn into another top edge. */
  1867. $bucket[$key] |= self::DIR_DOWN;
  1868. $this->wallFollow($path, $r + 1, $c, self::DIR_DOWN, $bucket, $d);
  1869. if ($path->isClosed()) {
  1870. return;
  1871. }
  1872. }
  1873. }
  1874. /*
  1875. * If we get here, the path doesn't close in any direction from this
  1876. * point (it's probably a line extension). Get rid of the point from our
  1877. * path and go back to the last one.
  1878. */
  1879. $path->popPoint();
  1880. return;
  1881. } elseif ($this->isMarker($this->getChar($r, $c))) {
  1882. /* Marker is part of a line, not a wall to close. */
  1883. return;
  1884. } else {
  1885. /* We landed on some whitespace or something; this isn't a closed path */
  1886. return;
  1887. }
  1888. }
  1889. /*
  1890. * Clears an object from the grid, erasing all edge and marker points. This
  1891. * function retains corners in "clearCorners" to be cleaned up before we do
  1892. * text parsing.
  1893. */
  1894. private function clearObject($obj) {
  1895. $points = $obj->getPoints();
  1896. $closed = $obj->isClosed();
  1897. $bound = count($points);
  1898. for ($i = 0; $i < $bound; $i++) {
  1899. $p = $points[$i];
  1900. if ($i == count($points) - 1) {
  1901. /* This keeps us from handling end of line to start of line */
  1902. if ($closed) {
  1903. $nP = $points[0];
  1904. } else {
  1905. $nP = null;
  1906. }
  1907. } else {
  1908. $nP = $points[$i+1];
  1909. }
  1910. /* If we're on the same vertical axis as our next point... */
  1911. if ($nP != null && $p->gridX == $nP->gridX) {
  1912. /* ...traverse the vertical line from the minimum to maximum points */
  1913. $maxY = max($p->gridY, $nP->gridY);
  1914. for ($j = min($p->gridY, $nP->gridY); $j <= $maxY; $j++) {
  1915. $char = $this->getChar($j, $p->gridX);
  1916. if ($this->isEdge($char) || $this->isMarker($char)) {
  1917. $this->grid[$j][$p->gridX] = ' ';
  1918. } elseif($this->isCorner($char)) {
  1919. $this->clearCorners[] = array($j, $p->gridX);
  1920. }
  1921. }
  1922. } elseif ($nP != null && $p->gridY == $nP->gridY) {
  1923. /* Same horizontal plane; traverse from min to max point */
  1924. $maxX = max($p->gridX, $nP->gridX);
  1925. for ($j = min($p->gridX, $nP->gridX); $j <= $maxX; $j++) {
  1926. $char = $this->getChar($p->gridY, $j);
  1927. if ($this->isEdge($char) || $this->isMarker($char)) {
  1928. $this->grid[$p->gridY][$j] = ' ';
  1929. } elseif($this->isCorner($char)) {
  1930. $this->clearCorners[] = array($p->gridY, $j);
  1931. }
  1932. }
  1933. } elseif ($nP != null && $closed == false && $p->gridX != $nP->gridX &&
  1934. $p->gridY != $nP->gridY) {
  1935. /*
  1936. * This is a diagonal line starting from the westernmost point. It
  1937. * must contain max(p->gridY, nP->gridY) - min(p->gridY, nP->gridY)
  1938. * segments, and we can tell whether to go north or south depending
  1939. * on which side of zero p->gridY - nP->gridY lies. There are no
  1940. * corners in diagonals, so we don't have to keep those around.
  1941. */
  1942. $c = $p->gridX;
  1943. $r = $p->gridY;
  1944. $rInc = ($p->gridY > $nP->gridY) ? -1 : 1;
  1945. $bound = max($p->gridY, $nP->gridY) - min($p->gridY, $nP->gridY);
  1946. /*
  1947. * This looks like an off-by-one, but it is not. This clears the
  1948. * corner, if one exists.
  1949. */
  1950. for ($j = 0; $j <= $bound; $j++) {
  1951. $char = $this->getChar($r, $c);
  1952. if ($char == '/' || $char == "\\" || $this->isMarker($char)) {
  1953. $this->grid[$r][$c++] = ' ';
  1954. } elseif ($this->isCorner($char)) {
  1955. $this->clearCorners[] = array($r, $c++);
  1956. }
  1957. $r += $rInc;
  1958. }
  1959. $this->grid[$p->gridY][$p->gridX] = ' ';
  1960. break;
  1961. }
  1962. }
  1963. }
  1964. /*
  1965. * Find style information for this polygon. This information is required to
  1966. * exist on the first line after the top, touching the left wall. It's kind
  1967. * of a pain requirement, but there's not a much better way to do it:
  1968. * ditaa's handling requires too much text flung everywhere and this way
  1969. * gives you a good method for specifying *tons* of information about the
  1970. * object.
  1971. */
  1972. private function findCommands($box) {
  1973. $points = $box->getPoints();
  1974. $sX = $points[0]->gridX + 1;
  1975. $sY = $points[0]->gridY + 1;
  1976. $ref = '';
  1977. if ($this->getChar($sY, $sX++) == '[') {
  1978. $char = $this->getChar($sY, $sX++);
  1979. while ($char != ']') {
  1980. $ref .= $char;
  1981. $char = $this->getChar($sY, $sX++);
  1982. }
  1983. if ($char == ']') {
  1984. $sX = $points[0]->gridX + 1;
  1985. $sY = $points[0]->gridY + 1;
  1986. if (!isset($this->commands[$ref]['a2s:delref']) &&
  1987. !isset($this->commands[$ref]['a2s:label'])) {
  1988. $this->grid[$sY][$sX] = ' ';
  1989. $this->grid[$sY][$sX + strlen($ref) + 1] = ' ';
  1990. } else {
  1991. if (isset($this->commands[$ref]['a2s:label'])) {
  1992. $label = $this->commands[$ref]['a2s:label'];
  1993. } else {
  1994. $label = null;
  1995. }
  1996. $len = strlen($ref) + 2;
  1997. for ($i = 0; $i < $len; $i++) {
  1998. if (strlen($label) > $i) {
  1999. $this->grid[$sY][$sX + $i] = substr($label, $i, 1);
  2000. } else {
  2001. $this->grid[$sY][$sX + $i] = ' ';
  2002. }
  2003. }
  2004. }
  2005. if (isset($this->commands[$ref])) {
  2006. $box->setOptions($this->commands[$ref]);
  2007. }
  2008. }
  2009. }
  2010. return $ref;
  2011. }
  2012. /*
  2013. * Extremely useful debugging information to figure out what has been
  2014. * parsed, especially when used in conjunction with clearObject.
  2015. */
  2016. private function dumpGrid() {
  2017. foreach($this->grid as $lines) {
  2018. echo implode('', $lines) . "\n";
  2019. }
  2020. }
  2021. private function getChar($row, $col) {
  2022. if (isset($this->grid[$row][$col])) {
  2023. return $this->grid[$row][$col];
  2024. }
  2025. return null;
  2026. }
  2027. private function isBoxEdge($char, $dir = null) {
  2028. if ($dir == null) {
  2029. return $char == '-' || $char == '|' || char == ':' || $char == '=' || $char == '*' || $char == '+';
  2030. } elseif ($dir == self::DIR_UP || $dir == self::DIR_DOWN) {
  2031. return $char == '|' || $char == ':' || $char == '*' || $char == '+';
  2032. } elseif ($dir == self::DIR_LEFT || $dir == self::DIR_RIGHT) {
  2033. return $char == '-' || $char == '=' || $char == '*' || $char == '+';
  2034. }
  2035. }
  2036. private function isEdge($char, $dir = null) {
  2037. if ($dir == null) {
  2038. return $char == '-' || $char == '|' || $char == ':' || $char == '=' || $char == '*' || $char == '/' || $char == "\\";
  2039. } elseif ($dir == self::DIR_UP || $dir == self::DIR_DOWN) {
  2040. return $char == '|' || $char == ':' || $char == '*';
  2041. } elseif ($dir == self::DIR_LEFT || $dir == self::DIR_RIGHT) {
  2042. return $char == '-' || $char == '=' || $char == '*';
  2043. } elseif ($dir == self::DIR_NE) {
  2044. return $char == '/';
  2045. } elseif ($dir == self::DIR_SE) {
  2046. return $char == "\\";
  2047. }
  2048. }
  2049. private function isBoxCorner($char) {
  2050. return $char == '.' || $char == "'" || $char == '#';
  2051. }
  2052. private function isCorner($char) {
  2053. return $char == '.' || $char == "'" || $char == '#' || $char == '+';
  2054. }
  2055. private function isMarker($char) {
  2056. return $char == 'v' || $char == '^' || $char == '<' || $char == '>';
  2057. }
  2058. }
  2059. /* vim:ts=2:sw=2:et:
  2060. * * */