PageRenderTime 44ms CodeModel.GetById 13ms app.highlight 26ms RepoModel.GetById 1ms app.codeStats 0ms

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