PageRenderTime 62ms CodeModel.GetById 23ms 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

Large files files are truncated, but you can click here to view the full file

  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. }

Large files files are truncated, but you can click here to view the full file