Source: render/2d/kekule.render.raphaelRenderer.js

/**
 * @fileoverview
 * 2D renderer using Raphael.js.
 * @author Partridge Jiang
 */

/*
 * requires /lan/classes.js
 * requires /core/kekule.structures.js
 * requires /render/kekule.render.base.js
 * requires /render/kekule.render.baseTextRender.js
 * requires /render/2d/kekule.render.def2DRenderer.js
 */

// Some helper methods of Raphael
if (this.Raphael)
{
	// draw a simple line
	/** @ignore */
	Raphael.fn.line = function(x1, y1, x2, y2)
	{
		return this.path('M' + x1 + ' ' + y1 + ' L' + x2 + ' ' + y2);
	};
	/** @ignore */
	Raphael.fn.arrowLine = function (x1, y1, x2, y2, arrowParams)
	{
		// TODO: arrow still has bug in drawing in IE/VML
		if (!arrowParams)
			return this.line(x1, y1, x2, y2);
		var result = this.set();
		/*
    var angle = Math.atan2(x1-x2,y2-y1);
    angle = (angle / (2 * Math.PI)) * 360;
		var width = (arrowParams.width || 6) / 2;
		var length = arrowParams.length || 3;
		result.push(
			// arrow path
			this.path('M' + x2 + ' ' + y2 + ' L' + (x2 - length) + ' ' + (y2 - width) +
				' M' + x2 + ' ' + y2 +
				' L' + (x2 - length) + ' ' + (y2 + width)).rotate((90+angle),x2,y2),
			// line path
    	this.path('M' + x1 + ' ' + y1 + ' L' + x2 + ' ' + y2)
		);
    //return [linePath,arrowPath];
		*/
		var dx = x2 - x1;
		var dy = y2 - y1;
		var alpha = Math.atan(dy / dx);

		var width = (arrowParams.width || 6) / 2;
		var length = arrowParams.length || 3;
		var beta = Math.atan(width / length);
		var l = Math.sqrt(Math.sqr(width) + Math.sqr(length));

		result.push(
			// arrow path
			this.path('M' + x2 + ' ' + y2
				+ ' L' + (x2 - l * Math.cos(alpha - beta)) + ' ' + (y2 - l * Math.sin(alpha - beta))
				+ ' M' + x2 + ' ' + y2
				+ ' L' + (x2 - l * Math.cos(alpha + beta)) + ' ' + (y2 - l * Math.sin(alpha + beta))),
			// line path
    	this.path('M' + x1 + ' ' + y1 + ' L' + x2 + ' ' + y2)
		);
		return result;
	};
	/** @ignore */
	Raphael.fn.triangle = function(x1, y1, x2, y2, x3, y3)
	{
		return this.path('M' + x1 + ' ' + y1 + ' L' + x2 + ' ' + y2 + ' L'
			+ x3 + ' ' + y3 + ' L' + x1 + ' ' + y1);
	};
	/** @ignore */
	Raphael.fn.polygon = function(coords)
	{
		var s = 'M' + coords[0].x + ' ' + coords[0].y;
		for (var i = 1, l = coords.length; i < l; ++i)
		{
			s += ' L' + coords[i].x + ' ' + coords[i].y;
		}
		s += 'L' + coords[0].x + ' ' + coords[0].y;
		return this.path(s);
	};
	// translate coordinate of a element with delta
	/** @ignore */
	Raphael.el.translateCoord = function(delta)
	{
		if (this.forEach)  // is set
			this.forEach(function(e) { e.translateCoord(delta); });
		else
		{
			var coord = this.attr(['x', 'y']);
			if ((typeof(coord.x) != 'undefined') && (typeof(coord.y) != 'undefined'))
				this.attr({'x': coord.x + delta.x, 'y': coord.y + delta.y});
		}
		//this.transform('t' + delta.x + ',' + delta.y);
		return this;
	};
	/** @ignore */
	Raphael.st.translateCoord = function(delta)
	{
		if (this.forEach)  // is set
			this.forEach(function(e) { e.translateCoord(delta); });
		//this.transform('t' + delta.x + ',' + delta.y);
		return this;
	};
	/** @ignore */
	Raphael.st.remove = function(delta)
	{
		// remove all children first
		this.forEach(function(elem)
			{
				elem.remove();
			}, this);
		this.clear();
	};
}

/**
 * Render bridge class of Raphael.
 * @class
 */
Kekule.Render.RaphaelRendererBridge = Class.create(
/** @lends Kekule.Render.RaphaelRendererBridge# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Render.RaphaelRendererBridge',

	/**
	 * Create a context element for drawing.
	 * @param {Element} parentElem
	 * @param {Int} width Width of context, in px.
	 * @param {Int} height Height of context, in px.
	 * @returns {Object} Context used for drawing.
	 */
	createContext: function(parentElem, width, height)
	{
		return Raphael(parentElem, width, height);
	},
	/**
	 * Destroy context created.
	 * @param {Object} context
	 */
	releaseContext: function(context)
	{
		context.remove();
	},

	/**
	 * Get width and height of context.
	 * @param {Object} context
	 * @returns {Hash} {width, height}
	 */
	getContextDimension: function(context)
	{
		return {'width': context.width, 'height': context.height};
	},

	/**
	 * Set new width and height of context.
	 * Note in canvas, the content should be redrawn after resizing.
	 * @param {Object} context
	 * @param {Int} width
	 * @param {Int} height
	 */
	setContextDimension: function(context, width, height)
	{
		context.setSize(width, height);
		this.clearContext(context);
	},

	/**
	 * Clear the whole context.
	 * @param {Object} context
	 */
	clearContext: function(context)
	{
		context.clear();
		var clearColor = context.__$clearColor__;
		if (clearColor)
		{
			var dim = this.getContextDimension(context);
			this.drawRect(context, {x: 0, y: 0}, {x: dim.width, y: dim.height}, {'fillColor': clearColor, 'strokeColor': clearColor});
		}
	},

	setClearColor: function(context, color)
	{
		if (context)
			context.__$clearColor__ = color;
	},

	/**
	 * Get context related element.
	 * @param {Object} context
	 */
	getContextElem: function(context)
	{
		return context? context.canvas: null;
	},

	/**
	 * Transform a context based coord to screen based one (usually in pixel).
	 * @param {Object} context
	 * @param {Hash} coord
	 * @return {Hash}
	 */
	transformContextCoordToScreen: function(context, coord)
	{
		return coord;
	},
	/**
	 * Transform a screen based coord to context based one.
	 * @param {Object} context
	 * @param {Hash} coord
	 * @return {Hash}
	 */
	transformScreenCoordToContext: function(context, coord)
	{
		return coord;
	},

	/**
	 * Indicate whether this bridge and context can change glyph content or position after drawing it.
	 * Raphael is a typical environment of this type while canvas should returns false.
	 * @param {Object} context
	 * @returns {Bool}
	 */
	canModifyGraphic: function(context)
	{
		return true;
	},
	drawPath: function(context, path, options)
	{
		var sPath = '';
		for (var i = 0, l = path.length; i < l; ++i)
		{
			var item = path[i];
			sPath += item.method;
			if (item.params && item.params.length)
			{
				for (var j = 0, k = item.params.length; j < k; ++j)
				{
					sPath += (j === 0) ? '' : ',';
					if (DataType.isArrayValue(item.params[j]))
						sPath += item.params[j].join(',');
					else
						sPath += item.params[j];
				}
			}
		}
		var result = context.path(sPath);
		this.setBasicElemAttribs(result, options);
		return result;
	},
	drawLine: function(context, coord1, coord2, options)
	{
		var result = context.line(coord1.x, coord1.y, coord2.x, coord2.y);
		this.setBasicElemAttribs(result, options);
		return result;
	},
	drawTriangle: function(context, coord1, coord2, coord3, options)
	{
		var result = context.triangle(coord1.x, coord1.y, coord2.x, coord2.y, coord3.x, coord3.y);
		this.setBasicElemAttribs(result, options);
		return result;
	},
	drawRect: function(context, coord1, coord2, options)
	{
		var result = context.rect(coord1.x, coord1.y, coord2.x - coord1.x, coord2.y - coord1.y);
		this.setBasicElemAttribs(result, options);
		return result;
	},
	drawRoundRect: function(context, coord1, coord2, cornerRadius, options)
	{
		var result = context.rect(coord1.x, coord1.y, coord2.x - coord1.x, coord2.y - coord1.y, cornerRadius);
		this.setBasicElemAttribs(result, options);
		return result;
	},
	drawCircle: function(context, baseCoord, radius, options)
	{
		var result = context.circle(baseCoord.x, baseCoord.y, radius);
		this.setBasicElemAttribs(result, options);
		return result;
	},
	drawArc: function(context, centerCoord, radius, startAngle, endAngle, anticlockwise, options)
	{
		var pToC = Kekule.GeometryUtils.polarToCartesian;
		var stAngle = Kekule.GeometryUtils.standardizeAngle;
		var CU = Kekule.CoordUtils;
		var start = pToC(centerCoord.x, centerCoord.y, radius, endAngle);
		var end = pToC(centerCoord.x, centerCoord.y, radius, startAngle);

		var aStart = stAngle(startAngle, 0);
		var aEnd = stAngle(endAngle, 0);

		var aDelta = aEnd - aStart;
		var largeArc = ((stAngle(aDelta) <= Math.PI) && !anticlockwise)
				|| ((stAngle(aDelta) > Math.PI) && anticlockwise);

		var largeArcFlag = largeArc ? '0' : '1';
		var sweepFlag = anticlockwise? '1': '0';

		var d = [
			'M', start.x, start.y,
			'A', radius, radius, 0, largeArcFlag, sweepFlag, end.x, end.y
		].join(' ');

		//console.log(d);

		var result = context.path(d);
		this.setBasicElemAttribs(result, options);
		return result;
	},
	drawImage: function(context, src, baseCoord, size, options, callback)
	{
		try
		{
			var result = context.image(src, baseCoord.x, baseCoord.y, size.x, size.y);
			this.setBasicElemAttribs(result, options);
			if (callback)
				callback(true);
		}
		catch(e)
		{
			if (callback)
				callback(false);
			throw e;
		}
		return result;
	},
	drawImageElem: function(context, imgElem, baseCoord, size, options)
	{
		return this.drawImage(context, imgElem.src, baseCoord, size, options);
	},

	/** @private */
	setBasicElemAttribs: function(elem, options)
	{
		if (Kekule.ObjUtils.notUnset(options.strokeWidth))
			elem.attr('stroke-width', options.strokeWidth);
		if (options.strokeColor)
			elem.attr('stroke', options.strokeColor);
		if (options.strokeDash)
		{
			// TODO: currently ignore all complex dash styles
			var dashStyle = '- ';  //(options.strokeDash === true)? '- ': options.strokeDash;
			elem.attr('stroke-dasharray', dashStyle);
		}
		if (options.fillColor)
			elem.attr('fill', options.fillColor);
		if (options.opacity)
		{
			elem.attr({
				'stroke-opacity': options.opacity,
				'fill-opacity': options.opacity
			});
		}
		if (options.lineCap)
			elem.attr('stroke-linecap', options.linecap);
		if (options.lineJoin)
			elem.attr('stroke-linejoin', options.linejoin);
	},

	/** @private */
	getRaphaelFontStyle: function(drawOptions)
	{
		// NOTE: in VML, can not set style "font: 14px" (without font family)
		// so we set attribs separately
		var result = {};
		if (drawOptions.fontStyle)
			result['font-style'] = drawOptions.fontStyle;
		if (drawOptions.fontWeight)
			result['font-weight'] = drawOptions.fontWeight;
		if (drawOptions.fontSize)
			result['font-size'] = drawOptions.fontSize + 'px';
		if (drawOptions.fontFamily)
			result['font-family'] = drawOptions.fontFamily;
		/*
		if (drawOptions.color)
		{
			result['fill'] = drawOptions.color;
			result['stroke'] = drawOptions.color;
		}
		*/

		return result;
	},

	/**
	 * Draw a plain text on context.
	 * @param {Object} context
	 * @param {Object} coord The top left coord to draw text.
	 * @param {Object} text
	 * @param {Object} options Draw options, may contain the following fields:
	 * 	 {fontSize, fontFamily}
	 * @returns {Object} Null or element drawn on context.
	 */
	drawText: function(context, coord, text, options)
	{
		// hack, otherwise the leading and tailing space may not be displayed in SVG
		if (text.endsWith(' '))
			text = text.substring(0, text.length - 1) + '\u00A0';
		if (text.startsWith(' '))
			text = '\u00A0' + text.substr(1);


		var fontStyle = this.getRaphaelFontStyle(options);
		//console.log(text, options);

		var elem = context.text(-10000, -10000, text); // Raphael set text center to coord, so need to adjust after draw
		elem.attr(/*'font', */fontStyle);
		if (options.color)
			elem.attr({'stroke': options.color, 'fill': options.color});
		this.modifyDrawnTextCoord(context, elem, coord);
		this.setBasicElemAttribs(elem, options);
		//console.log('RAPHAEL DRAW TEXT', text, coord);
		return elem;
	},

	/**
	 * Indicate whether this bridge and context can measure text dimension before drawing it.
	 * HTML Canvas is a typical environment of this type.
	 * @param {Object} context
	 * @returns {Bool}
	 */
	canMeasureText: function(context)
	{
		return false;
	},

	/**
	 * Indicate whether this bridge and context can measure text dimension before drawing it.
	 * Raphael is a typical environment of this type.
	 * Such a bridge must also has the ability to modify text pos after drawn.
	 * @param {Object} context
	 * @returns {Bool}
	 */
	canMeasureDrawnText: function(context)
	{
		return true;
	},

	/**
	 * Indicate whether this bridge and context can change text content or position after drawing it.
	 * Raphael is a typical environment of this type.
	 * @param {Object} context
	 * @returns {Bool}
	 */
	canModifyText: function(context)
	{
		return true;
	},

	/**
	 * Mearsure the width and height of text on context after drawing it.
	 * @param {Object} context
	 * @param {Object} textElem Drawn text element on context.
	 * @param {Object} options
	 * @returns {Hash} An object with width and height fields, top and left is optional.
	 */
	measureDrawnText: function(context, textElem, options)
	{
		var oneOf = Kekule.oneOf;
		var box = textElem.getBBox();
		var result = {};
		result.width = oneOf(box.width, (box.x2 - box.x1));
		result.height = oneOf(box.height, (box.y2 - box.y1));
		//console.log('measure box', box, result, textElem);
		return result;
	},
	/**
	 * Change text drawn on context to a new coord. Not all context can apply this action.
	 * @param {Object} context
	 * @param {Object} textElem
	 * @param {Hash} newCoord The top left coord of text box.
	 */
	modifyDrawnTextCoord: function(context, textElem, newCoord)
	{
		// Raphael set text center to coord, so need to adjust to suitable position
		var dimension = this.measureDrawnText(context, textElem, null);
		textElem.attr({'x': newCoord.x + dimension.width / 2, 'y': newCoord.y + dimension.height / 2});
	},

	/** @ignore */
	createGroup: function(context)
	{
		return context.set();
	},
	/** @ignore */
	addToGroup: function(elem, group)
	{
		return group.push(elem);
	},
	/** @ignore */
	removeFromGroup: function(elem, group)
	{
		if (elem !== group)
			return group.exclude(elem);
		else
			return false;
	},

	/**
	 * Remove an element in context.
	 * @param {Object} context
	 * @param {Object} elem
	 */
	removeDrawnElem: function(context, elem)
	{
		elem.remove();  // remove element or clear set
	},

	// export
	/** @ignore */
	exportToDataUri: function(context, dataType, options)
	{
		if (context.toSVG)  // if Rapheal.Export lib used
		{
			var svg = context.toSVG();
			return 'data:image/svg+xml;base64,' + btoa(svg);
		}
		else if (context.canvas && (context.canvas.tagName.toLowerCase() === 'svg'))
		{
			var svg = XmlUtility.serializeNode(context.canvas);
			return 'data:image/svg+xml;base64,' + btoa(svg);
		}
	}
});

/**
 * Check if current environment supports Raphael (SVG or VML).
 * @returns {Bool}
 */
Kekule.Render.RaphaelRendererBridge.isSupported = function()
{
	var result = false;
	if (Kekule.$jsRoot.Raphael)
	{
		result = !!(Raphael.svg || Raphael.vml);
	}
	return result;
};

//Kekule.ClassUtils.makeSingleton(Kekule.Render.RaphaelRendererBridge);

Kekule.Render.DrawBridge2DMananger.register(Kekule.Render.RaphaelRendererBridge, 10);