/dozer/media/javascript/canviz.js

https://bitbucket.org/bbangert/dozer/ · JavaScript · 522 lines · 491 code · 2 blank · 29 comment · 87 complexity · aa61dc00c5c4222c0be94511be6b06b4 MD5 · raw file

  1. // $Id: canviz.js 390 2007-03-29 10:45:46Z rschmidt $
  2. var Tokenizer = Class.create();
  3. Tokenizer.prototype = {
  4. initialize: function(str) {
  5. this.str = str;
  6. },
  7. takeChars: function(num) {
  8. if (!num) {
  9. num = 1;
  10. }
  11. var tokens = new Array();
  12. while (num--) {
  13. var matches = this.str.match(/^(\S+)\s*/);
  14. if (matches) {
  15. this.str = this.str.substr(matches[0].length);
  16. tokens.push(matches[1]);
  17. } else {
  18. tokens.push(false);
  19. }
  20. }
  21. if (1 == tokens.length) {
  22. return tokens[0];
  23. } else {
  24. return tokens;
  25. }
  26. },
  27. takeNumber: function(num) {
  28. if (!num) {
  29. num = 1;
  30. }
  31. if (1 == num) {
  32. return Number(this.takeChars())
  33. } else {
  34. var tokens = this.takeChars(num);
  35. while (num--) {
  36. tokens[num] = Number(tokens[num]);
  37. }
  38. return tokens;
  39. }
  40. },
  41. takeString: function() {
  42. var chars = Number(this.takeChars());
  43. if ('-' != this.str.charAt(0)) {
  44. return false;
  45. }
  46. var str = this.str.substr(1, chars);
  47. this.str = this.str.substr(1 + chars).replace(/^\s+/, '');
  48. return str;
  49. }
  50. }
  51. var Graph = Class.create();
  52. Graph.prototype = {
  53. initialize: function(ctx, file, engine) {
  54. this.maxXdotVersion = 1.2;
  55. this.systemScale = 4/3;
  56. this.scale = 1;
  57. this.padding = 8;
  58. this.ctx = ctx;
  59. this.images = new Hash();
  60. this.numImages = 0;
  61. this.numImagesFinished = 0;
  62. if (file) {
  63. this.load(file, engine);
  64. }
  65. },
  66. setImagePath: function(imagePath) {
  67. this.imagePath = imagePath;
  68. },
  69. load: function(file, engine) {
  70. $('debug_output').innerHTML = '';
  71. var url = 'graph.php';
  72. var params = 'file=' + file;
  73. if (engine) {
  74. params += '&engine=' + engine;
  75. }
  76. new Ajax.Request(url, {
  77. method: 'get',
  78. parameters: params,
  79. onComplete: this.parse.bind(this)
  80. });
  81. },
  82. parse: function(request) {
  83. this.xdotversion = false;
  84. this.commands = new Array();
  85. this.width = 0;
  86. this.height = 0;
  87. this.maxWidth = false;
  88. this.maxHeight = false;
  89. this.bbEnlarge = false;
  90. this.bbScale = 1;
  91. this.orientation = 'portrait';
  92. this.bgcolor = '#ffffff';
  93. this.dashLength = 6;
  94. this.dotSpacing = 4;
  95. this.fontName = 'Times New Roman';
  96. this.fontSize = 14;
  97. var graph_src = request.responseText;
  98. var lines = graph_src.split('\n');
  99. var i = 0;
  100. var line, lastchar, matches, is_graph, entity, params, param_name, param_value;
  101. var container_stack = new Array();
  102. while (i < lines.length) {
  103. line = lines[i++].replace(/^\s+/, '');
  104. if ('' != line && '#' != line.substr(0, 1)) {
  105. while (i < lines.length && ';' != (lastchar = line.substr(line.length - 1, line.length)) && '{' != lastchar && '}' != lastchar) {
  106. if ('\\' == lastchar) {
  107. line = line.substr(0, line.length - 1);
  108. }
  109. line += lines[i++];
  110. }
  111. // debug(line);
  112. matches = line.match(/^(.*?)\s*{$/);
  113. if (matches) {
  114. container_stack.push(matches[1]);
  115. // debug('begin container ' + container_stack.last());
  116. } else if ('}' == line) {
  117. // debug('end container ' + container_stack.last());
  118. container_stack.pop();
  119. } else {
  120. // matches = line.match(/^(".*?[^\\]"|\S+?)\s+\[(.+)\];$/);
  121. matches = line.match(/^(.*?)\s+\[(.+)\];$/);
  122. if (matches) {
  123. is_graph = ('graph' == matches[1]);
  124. // entity = this.unescape(matches[1]);
  125. entity = matches[1];
  126. params = matches[2];
  127. do {
  128. matches = params.match(/^(\S+?)=(""|".*?[^\\]"|<(<[^>]+>|[^<>]+?)+>|\S+?)(?:,\s*|$)/);
  129. if (matches) {
  130. params = params.substr(matches[0].length);
  131. param_name = matches[1];
  132. param_value = this.unescape(matches[2]);
  133. // debug(param_name + ' ' + param_value);
  134. if (is_graph && 1 == container_stack.length) {
  135. switch (param_name) {
  136. case 'bb':
  137. var bb = param_value.split(/,/);
  138. this.width = Number(bb[2]);
  139. this.height = Number(bb[3]);
  140. break;
  141. case 'bgcolor':
  142. this.bgcolor = this.parseColor(param_value);
  143. break;
  144. case 'size':
  145. var size = param_value.match(/^(\d+|\d*(?:\.\d+)),\s*(\d+|\d*(?:\.\d+))(!?)$/);
  146. if (size) {
  147. this.maxWidth = 72 * Number(size[1]);
  148. this.maxHeight = 72 * Number(size[2]);
  149. this.bbEnlarge = ('!' == size[3]);
  150. } else {
  151. debug('can\'t parse size');
  152. }
  153. break;
  154. case 'orientation':
  155. if (param_value.match(/^l/i)) {
  156. this.orientation = 'landscape';
  157. }
  158. break;
  159. case 'rotate':
  160. if (90 == param_value) {
  161. this.orientation = 'landscape';
  162. }
  163. break;
  164. case 'xdotversion':
  165. this.xdotversion = parseFloat(param_value);
  166. if (this.maxXdotVersion < this.xdotversion) {
  167. debug('unsupported xdotversion ' + this.xdotversion + '; this script currently supports up to xdotversion ' + this.maxXdotVersion);
  168. }
  169. break;
  170. }
  171. }
  172. switch (param_name) {
  173. case '_draw_':
  174. case '_ldraw_':
  175. case '_hdraw_':
  176. case '_tdraw_':
  177. case '_hldraw_':
  178. case '_tldraw_':
  179. // debug(entity + ': ' + param_value);
  180. this.commands.push(param_value);
  181. break;
  182. }
  183. }
  184. } while (matches);
  185. }
  186. }
  187. }
  188. }
  189. if (!this.xdotversion) {
  190. this.xdotversion = 1.0;
  191. }
  192. /*
  193. if (this.maxWidth && this.maxHeight) {
  194. if (this.width > this.maxWidth || this.height > this.maxHeight || this.bbEnlarge) {
  195. this.bbScale = Math.min(this.maxWidth / this.width, this.maxHeight / this.height);
  196. this.width = Math.round(this.width * this.bbScale);
  197. this.height = Math.round(this.height * this.bbScale);
  198. }
  199. if ('landscape' == this.orientation) {
  200. var temp = this.width;
  201. this.width = this.height;
  202. this.height = temp;
  203. }
  204. }
  205. */
  206. // debug('done');
  207. this.draw();
  208. },
  209. draw: function(redraw_canvas) {
  210. if (!redraw_canvas) redraw_canvas = false;
  211. var width = Math.round(this.scale * this.systemScale * this.width + 2 * this.padding);
  212. var height = Math.round(this.scale * this.systemScale * this.height + 2 * this.padding);
  213. if (!redraw_canvas) {
  214. canvas.width = width;
  215. canvas.height = height;
  216. Element.setStyle(canvas, {
  217. width: width + 'px',
  218. height: height + 'px'
  219. });
  220. Element.setStyle('graph_container', {
  221. width: width + 'px'
  222. });
  223. $('graph_texts').innerHTML = '';
  224. }
  225. this.ctx.save();
  226. this.ctx.lineCap = 'round';
  227. this.ctx.fillStyle = this.bgcolor;
  228. this.ctx.fillRect(0, 0, width, height);
  229. this.ctx.translate(this.padding, this.padding);
  230. this.ctx.scale(this.scale * this.systemScale, this.scale * this.systemScale);
  231. this.ctx.lineWidth = 1 / this.systemScale;
  232. var i, tokens;
  233. var entity_id = 0;
  234. var text_divs = '';
  235. for (var command_index = 0; command_index < this.commands.length; command_index++) {
  236. var command = this.commands[command_index];
  237. // debug(command);
  238. var tokenizer = new Tokenizer(command);
  239. var token = tokenizer.takeChars();
  240. if (token) {
  241. ++entity_id;
  242. var entity_text_divs = '';
  243. this.dashStyle = 'solid';
  244. this.ctx.save();
  245. while (token) {
  246. // debug('processing token ' + token);
  247. switch (token) {
  248. case 'E': // filled ellipse
  249. case 'e': // unfilled ellipse
  250. var filled = ('E' == token);
  251. var cx = tokenizer.takeNumber();
  252. var cy = this.height - tokenizer.takeNumber();
  253. var rx = tokenizer.takeNumber();
  254. var ry = tokenizer.takeNumber();
  255. this.render(new Ellipse(cx, cy, rx, ry), filled);
  256. break;
  257. case 'P': // filled polygon
  258. case 'p': // unfilled polygon
  259. case 'L': // polyline
  260. var filled = ('P' == token);
  261. var closed = ('L' != token);
  262. var num_points = tokenizer.takeNumber();
  263. tokens = tokenizer.takeNumber(2 * num_points); // points
  264. var path = new Path();
  265. for (i = 2; i < 2 * num_points; i += 2) {
  266. path.addBezier([
  267. new Point(tokens[i - 2], this.height - tokens[i - 1]),
  268. new Point(tokens[i], this.height - tokens[i + 1])
  269. ]);
  270. }
  271. if (closed) {
  272. path.addBezier([
  273. new Point(tokens[2 * num_points - 2], this.height - tokens[2 * num_points - 1]),
  274. new Point(tokens[0], this.height - tokens[1])
  275. ]);
  276. }
  277. this.render(path, filled);
  278. break;
  279. case 'B': // unfilled b-spline
  280. case 'b': // filled b-spline
  281. var filled = ('b' == token);
  282. var num_points = tokenizer.takeNumber();
  283. tokens = tokenizer.takeNumber(2 * num_points); // points
  284. var path = new Path();
  285. for (i = 2; i < 2 * num_points; i += 6) {
  286. path.addBezier([
  287. new Point(tokens[i - 2], this.height - tokens[i - 1]),
  288. new Point(tokens[i], this.height - tokens[i + 1]),
  289. new Point(tokens[i + 2], this.height - tokens[i + 3]),
  290. new Point(tokens[i + 4], this.height - tokens[i + 5])
  291. ]);
  292. }
  293. this.render(path, filled);
  294. break;
  295. case 'I': // image
  296. var x = tokenizer.takeNumber();
  297. var y = this.height - tokenizer.takeNumber();
  298. var w = tokenizer.takeNumber();
  299. var h = tokenizer.takeNumber();
  300. var src = tokenizer.takeString();
  301. if (!this.images[src]) {
  302. y -= h;
  303. this.images[src] = new GraphImage(this, src, x, y, w, h);
  304. }
  305. this.images[src].draw();
  306. break;
  307. case 'T': // text
  308. var x = Math.round(this.scale * this.systemScale * tokenizer.takeNumber() + this.padding);
  309. var y = Math.round(height - (this.scale * this.systemScale * (tokenizer.takeNumber() + this.bbScale * this.fontSize) + this.padding));
  310. var text_align = tokenizer.takeNumber();
  311. var text_width = Math.round(this.scale * this.systemScale * tokenizer.takeNumber());
  312. var str = tokenizer.takeString();
  313. if (!redraw_canvas && !str.match(/^\s*$/)) {
  314. // debug('draw text ' + str + ' ' + x + ' ' + y + ' ' + text_align + ' ' + text_width);
  315. str = str.escapeHTML();
  316. do {
  317. matches = str.match(/ ( +)/);
  318. if (matches) {
  319. var spaces = ' ';
  320. matches[1].length.times(function() {
  321. spaces += '&nbsp;';
  322. });
  323. str = str.replace(/ +/, spaces);
  324. }
  325. } while (matches);
  326. entity_text_divs += '<div style="font:' + Math.round(this.fontSize * this.scale * this.systemScale * this.bbScale) + 'px \'' + this.fontName +'\';color:' + this.ctx.strokeStyle + ';';
  327. switch (text_align) {
  328. case -1: //left
  329. entity_text_divs += 'left:' + x + 'px;';
  330. break;
  331. case 1: // right
  332. entity_text_divs += 'text-align:right;right:' + x + 'px;';
  333. break;
  334. case 0: // center
  335. default:
  336. entity_text_divs += 'text-align:center;left:' + (x - text_width) + 'px;';
  337. break;
  338. }
  339. entity_text_divs += 'top:' + y + 'px;width:' + (2 * text_width) + 'px">' + str + '</div>';
  340. }
  341. break;
  342. case 'C': // set fill color
  343. case 'c': // set pen color
  344. var fill = ('C' == token);
  345. var color = this.parseColor(tokenizer.takeString());
  346. if (fill) {
  347. this.ctx.fillStyle = color;
  348. } else {
  349. this.ctx.strokeStyle = color;
  350. }
  351. break;
  352. case 'F': // set font
  353. this.fontSize = tokenizer.takeNumber();
  354. this.fontName = tokenizer.takeString();
  355. switch (this.fontName) {
  356. case 'Times-Roman':
  357. this.fontName = 'Times New Roman';
  358. break;
  359. case 'Courier':
  360. this.fontName = 'Courier New';
  361. break;
  362. case 'Helvetica':
  363. this.fontName = 'Arial';
  364. break;
  365. default:
  366. // nothing
  367. }
  368. // debug('set font ' + this.fontSize + 'pt ' + this.fontName);
  369. break;
  370. case 'S': // set style
  371. var style = tokenizer.takeString();
  372. switch (style) {
  373. case 'solid':
  374. case 'filled':
  375. // nothing
  376. break;
  377. case 'dashed':
  378. case 'dotted':
  379. this.dashStyle = style;
  380. break;
  381. case 'bold':
  382. this.ctx.lineWidth = 2 / this.systemScale;
  383. break;
  384. default:
  385. matches = style.match(/^setlinewidth\((.*)\)$/);
  386. if (matches) {
  387. this.ctx.lineWidth = Number(matches[1]) / this.systemScale;
  388. } else {
  389. debug('unknown style ' + style);
  390. }
  391. }
  392. break;
  393. default:
  394. debug('unknown token ' + token);
  395. return;
  396. }
  397. token = tokenizer.takeChars();
  398. }
  399. this.ctx.restore();
  400. if (entity_text_divs) {
  401. text_divs += '<div id="entity' + entity_id + '">' + entity_text_divs + '</div>';
  402. }
  403. }
  404. };
  405. this.ctx.restore();
  406. if (!redraw_canvas) $('graph_texts').innerHTML = text_divs;
  407. },
  408. render: function(path, filled) {
  409. if (filled) {
  410. this.ctx.beginPath();
  411. path.draw(this.ctx);
  412. this.ctx.fill();
  413. }
  414. if (this.ctx.fillStyle != this.ctx.strokeStyle || !filled) {
  415. switch (this.dashStyle) {
  416. case 'dashed':
  417. this.ctx.beginPath();
  418. path.drawDashed(this.ctx, this.dashLength);
  419. break;
  420. case 'dotted':
  421. var oldLineWidth = this.ctx.lineWidth;
  422. this.ctx.lineWidth *= 2;
  423. this.ctx.beginPath();
  424. path.drawDotted(this.ctx, this.dotSpacing);
  425. break;
  426. case 'solid':
  427. default:
  428. if (!filled) {
  429. this.ctx.beginPath();
  430. path.draw(this.ctx);
  431. }
  432. }
  433. this.ctx.stroke();
  434. if (oldLineWidth) this.ctx.lineWidth = oldLineWidth;
  435. }
  436. },
  437. unescape: function(str) {
  438. var matches = str.match(/^"(.*)"$/);
  439. if (matches) {
  440. return matches[1].replace(/\\"/g, '"');
  441. } else {
  442. return str;
  443. }
  444. },
  445. parseColor: function(color) {
  446. if (gvcolors[color]) { // named color
  447. return 'rgb(' + gvcolors[color][0] + ',' + gvcolors[color][1] + ',' + gvcolors[color][2] + ')';
  448. } else {
  449. var matches = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
  450. if (matches) { // rgba
  451. return 'rgba(' + parseInt(matches[1], 16) + ',' + parseInt(matches[2], 16) + ',' + parseInt(matches[3], 16) + ',' + (parseInt(matches[4], 16) / 255) + ')';
  452. } else {
  453. matches = color.match(/(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)/);
  454. if (matches) { // hsv
  455. return this.hsvToRgbColor(matches[1], matches[2], matches[3]);
  456. } else if (color.match(/^#[0-9a-f]{6}$/i)) {
  457. return color;
  458. }
  459. }
  460. }
  461. debug('unknown color ' + color);
  462. return '#000000';
  463. },
  464. hsvToRgbColor: function(h, s, v) {
  465. var i, f, p, q, t, r, g, b;
  466. h *= 360;
  467. i = Math.floor(h / 60) % 6;
  468. f = h / 60 - i;
  469. p = v * (1 - s);
  470. q = v * (1 - f * s);
  471. t = v * (1 - (1 - f) * s)
  472. switch (i) {
  473. case 0: r = v; g = t; b = p; break;
  474. case 1: r = q; g = v; b = p; break;
  475. case 2: r = p; g = v; b = t; break;
  476. case 3: r = p; g = q; b = v; break;
  477. case 4: r = t; g = p; b = v; break;
  478. case 5: r = v; g = p; b = q; break;
  479. }
  480. return 'rgb(' + Math.round(255 * r) + ',' + Math.round(255 * g) + ',' + Math.round(255 * b) + ')';
  481. }
  482. }
  483. var GraphImage = Class.create();
  484. GraphImage.prototype = {
  485. initialize: function(graph, src, x, y, w, h) {
  486. this.graph = graph;
  487. ++this.graph.numImages;
  488. this.src = this.graph.imagePath + '/' + src;
  489. this.x = x;
  490. this.y = y;
  491. this.w = w;
  492. this.h = h;
  493. this.loaded = false;
  494. this.img = new Image();
  495. this.img.onload = this.succeeded.bind(this);
  496. this.img.onerror = this.finished.bind(this);
  497. this.img.onabort = this.finished.bind(this);
  498. this.img.src = this.src;
  499. },
  500. succeeded: function() {
  501. this.loaded = true;
  502. this.finished();
  503. },
  504. finished: function() {
  505. ++this.graph.numImagesFinished;
  506. if (this.graph.numImages == this.graph.numImagesFinished) {
  507. this.graph.draw(true);
  508. }
  509. },
  510. draw: function() {
  511. if (this.loaded) {
  512. this.graph.ctx.drawImage(this.img, this.x, this.y, this.w, this.h);
  513. }
  514. }
  515. }
  516. function debug(str) {
  517. $('debug_output').innerHTML += '&raquo;' + String(str).escapeHTML() + '&laquo;<br />';
  518. }