UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

651 lines (604 loc) 18.6 kB
/** * @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.render2D.js */ (function(){ "use strict"; var RAPHAEL_MODULE_NAME = 'Raphael.js'; var Raphael; /** * Render bridge class of Raphael. * @class */ Kekule.Render.RaphaelRendererBridge = Class.create(Kekule.Render.Abstract2DDrawBridge, /** @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. * @param {Hash} params Additional params to create context. * @returns {Object} Context used for drawing. */ createContext: function(parentElem, width, height, params) { 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; }, setFilter: function(context, filter) { var elem = this.getContextElem(context); if (elem) { elem.style.filter = filter; } }, clearFilter: function(context) { this.setFilter(context, 'none'); }, /** * 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); if ((options.transforms && options.transforms.length) || options.transform) { this.setTransformSeq(elem, options.transforms || [options.transform]); } }, /** @private */ setTransformSeq: function(elem, transformSeq) { for (var i = 0, l = transformSeq.length; i < l; ++i) { var transform = Object.extend({'rotateAngle': transformSeq[i].rotate}, transformSeq[i]); this.setTransform(elem, transform); } }, /** @private */ setTransform: function(elem, transform) { var center = transform.center || {'x': 0, 'y': 0}; // rotate if (transform.rotateAngle) { elem.rotate(transform.rotateAngle / Math.PI * 180, center.x, center.y); } // scale var scaleX = Kekule.oneOf(transform.scaleX, transform.scale, 1); var scaleY = Kekule.oneOf(transform.scaleY, transform.scale, 1); if (scaleX !== 1 || scaleY !== 1) { elem.scale(scaleX, scaleY, center.x, center.y); } // translate if (transform.translateX || transform.translateY) { elem.translate(transform.translateX || 0, transform.translateY || 0); } }, /** @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 = DataType.XmlUtility.serializeNode(context.canvas); return 'data:image/svg+xml;base64,' + btoa(svg); } } }); /** * Check if current environment supports Raphael (SVG or VML). * @returns {Bool} * @deprecated */ Kekule.Render.RaphaelRendererBridge.isSupported = function() { var result = false; if (Kekule.$jsRoot.Raphael) { result = !!(Raphael.svg || Raphael.vml); } return result; }; /** * Returns the availability information of Raphael renderer. * @returns {Hash} */ Kekule.Render.RaphaelRendererBridge.getAvailabilityInformation = function() { var raphaelAvailable = !!Kekule.$jsRoot.Raphael; var raphaelRenderingAvailable = !!(raphaelAvailable && (Kekule.$jsRoot.Raphael.svg || Kekule.$jsRoot.Raphael.vml)); return { 'available': !!raphaelRenderingAvailable, 'message': !raphaelAvailable? Kekule.$L('ErrorMsg.RAPHAEL_LIB_NOT_UNAVAILABLE'): !raphaelRenderingAvailable? Kekule.$L('ErrorMsg.RAPHAEL_SVG_VML_UNAVAILABLE'): null } }; Kekule.Render.DrawBridge2DMananger.register(Kekule.Render.RaphaelRendererBridge, 10); //Kekule.ClassUtils.makeSingleton(Kekule.Render.RaphaelRendererBridge); // Some helper methods of Raphael var _raphaelRegistered = function() { Raphael = Kekule.externalResourceManager.getResource(RAPHAEL_MODULE_NAME); // set the global variable used by classes if (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(); }; } Kekule.Render.DrawBridge2DMananger.register(Kekule.Render.RaphaelRendererBridge, 10); }; var _raphaelUnregistered = function() { // Kekule.Render.DrawBridge2DMananger.unregister(Kekule.Render.RaphaelRendererBridge); }; var _registerRaphael = function(raphaelRoot) { Kekule.Render.registerExternalModule(RAPHAEL_MODULE_NAME, raphaelRoot); }; var _tryRegisterRaphael = function() { if (!_tryRegisterRaphael.registered) { if (Kekule.$jsRoot.Raphael && Kekule.$jsRoot.Raphael.el) // Raphael.js loaded { _registerRaphael(Kekule.$jsRoot.Raphael); _tryRegisterRaphael.registered = true; } } }; Kekule.externalResourceManager.on(RAPHAEL_MODULE_NAME + 'Registered', _raphaelRegistered); Kekule.externalResourceManager.on(RAPHAEL_MODULE_NAME + 'Unregistered', _raphaelUnregistered); // try register Three.js on execute and on DOM load _tryRegisterRaphael(); Kekule.X.domReady(_tryRegisterRaphael); })();