// Copyright 2007 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview SvgGraphics sub class that uses SVG to draw the graphics. * @author arv@google.com (Erik Arvidsson) * @author yoah@google.com (Yoah Bar-David) */ goog.provide('goog.graphics.SvgGraphics'); goog.require('goog.Timer'); goog.require('goog.dom'); goog.require('goog.events.EventHandler'); goog.require('goog.events.EventType'); goog.require('goog.graphics.AbstractGraphics'); goog.require('goog.graphics.Font'); goog.require('goog.graphics.LinearGradient'); goog.require('goog.graphics.SolidFill'); goog.require('goog.graphics.Stroke'); goog.require('goog.graphics.SvgEllipseElement'); goog.require('goog.graphics.SvgGroupElement'); goog.require('goog.graphics.SvgImageElement'); goog.require('goog.graphics.SvgPathElement'); goog.require('goog.graphics.SvgRectElement'); goog.require('goog.graphics.SvgTextElement'); goog.require('goog.math.Size'); goog.require('goog.style'); goog.require('goog.userAgent'); /** * A Graphics implementation for drawing using SVG. * @param {string|number} width The width in pixels. Strings * expressing percentages of parent with (e.g. '80%') are also accepted. * @param {string|number} height The height in pixels. Strings * expressing percentages of parent with (e.g. '80%') are also accepted. * @param {?number=} opt_coordWidth The coordinate width - if * omitted or null, defaults to same as width. * @param {?number=} opt_coordHeight The coordinate height - if * omitted or null, defaults to same as height. * @param {goog.dom.DomHelper=} opt_domHelper The DOM helper object for the * document we want to render in. * @constructor * @extends {goog.graphics.AbstractGraphics} */ goog.graphics.SvgGraphics = function(width, height, opt_coordWidth, opt_coordHeight, opt_domHelper) { goog.graphics.AbstractGraphics.call(this, width, height, opt_coordWidth, opt_coordHeight, opt_domHelper); /** * Map from def key to id of def root element. * Defs are global "defines" of svg that are used to share common attributes, * for example gradients. * @type {Object} * @private */ this.defs_ = {}; /** * Whether to manually implement viewBox by using a coordinate transform. * As of 1/11/08 this is necessary for Safari 3 but not for the nightly * WebKit build. Apply to webkit versions < 526. 525 is the * last version used by Safari 3.1. * @type {boolean} * @private */ this.useManualViewbox_ = goog.userAgent.WEBKIT && !goog.userAgent.isVersion(526); /** * Event handler. * @type {goog.events.EventHandler} * @private */ this.handler_ = new goog.events.EventHandler(this); }; goog.inherits(goog.graphics.SvgGraphics, goog.graphics.AbstractGraphics); /** * The SVG namespace URN * @private * @type {string} */ goog.graphics.SvgGraphics.SVG_NS_ = 'http://www.w3.org/2000/svg'; /** * The name prefix for def entries * @private * @type {string} */ goog.graphics.SvgGraphics.DEF_ID_PREFIX_ = '_svgdef_'; /** * The next available unique identifier for a def entry. * This is a static variable, so that when multiple graphics are used in one * document, the same def id can not be re-defined by another SvgGraphics. * @type {number} * @private */ goog.graphics.SvgGraphics.nextDefId_ = 0; /** * Svg element for definitions for other elements, e.g. linear gradients. * @type {Element} * @private */ goog.graphics.SvgGraphics.prototype.defsElement_; /** * Creates an SVG element. Used internally and by different SVG classes. * @param {string} tagName The type of element to create. * @param {Object=} opt_attributes Map of name-value pairs for attributes. * @return {Element} The created element. * @private */ goog.graphics.SvgGraphics.prototype.createSvgElement_ = function(tagName, opt_attributes) { var element = this.dom_.getDocument().createElementNS( goog.graphics.SvgGraphics.SVG_NS_, tagName); if (opt_attributes) { this.setElementAttributes(element, opt_attributes); } return element; }; /** * Sets properties to an SVG element. Used internally and by different * SVG elements. * @param {Element} element The svg element. * @param {Object} attributes Map of name-value pairs for attributes. */ goog.graphics.SvgGraphics.prototype.setElementAttributes = function(element, attributes) { for (var key in attributes) { element.setAttribute(key, attributes[key]); } }; /** * Appends an element. * * @param {goog.graphics.Element} element The element wrapper. * @param {goog.graphics.SvgGroupElement=} opt_group The group wrapper element * to append to. If not specified, appends to the main canvas. * @private */ goog.graphics.SvgGraphics.prototype.append_ = function(element, opt_group) { var parent = opt_group || this.canvasElement; parent.getElement().appendChild(element.getElement()); }; /** * Sets the fill of the given element. * @param {goog.graphics.StrokeAndFillElement} element The element wrapper. * @param {goog.graphics.Fill?} fill The fill object. */ goog.graphics.SvgGraphics.prototype.setElementFill = function(element, fill) { var svgElement = element.getElement(); if (fill instanceof goog.graphics.SolidFill) { svgElement.setAttribute('fill', fill.getColor()); svgElement.setAttribute('fill-opacity', fill.getOpacity()); } else if (fill instanceof goog.graphics.LinearGradient) { // create a def key which is just a concat of all the relevant fields var defKey = 'lg-' + fill.getX1() + '-' + fill.getY1() + '-' + fill.getX2() + '-' + fill.getY2() + '-' + fill.getColor1() + '-' + fill.getColor2(); // It seems that the SVG version accepts opacity where the VML does not var id = this.getDef_(defKey); if (!id) { // No def for this yet, create it // Create the gradient def entry (only linear gradient are supported) var gradient = this.createSvgElement_('linearGradient', { 'x1': fill.getX1(), 'y1': fill.getY1(), 'x2': fill.getX2(), 'y2': fill.getY2(), 'gradientUnits': 'userSpaceOnUse' }); var gstyle = 'stop-color:' + fill.getColor1(); if (goog.isNumber(fill.getOpacity1())) { gstyle += ';stop-opacity:' + fill.getOpacity1(); } var stop1 = this.createSvgElement_( 'stop', {'offset': '0%', 'style': gstyle}); gradient.appendChild(stop1); // LinearGradients don't have opacity in VML so implement that before // enabling the following code. // if (fill.getOpacity() != null) { // gstyles += 'opacity:' + fill.getOpacity() + ';' // } gstyle = 'stop-color:' + fill.getColor2(); if (goog.isNumber(fill.getOpacity2())) { gstyle += ';stop-opacity:' + fill.getOpacity2(); } var stop2 = this.createSvgElement_( 'stop', {'offset': '100%', 'style': gstyle}); gradient.appendChild(stop2); // LinearGradients don't have opacity in VML so implement that before // enabling the following code. // if (fill.getOpacity() != null) { // gstyles += 'opacity:' + fill.getOpacity() + ';' // } id = this.addDef_(defKey, gradient); } // Link element to linearGradient definition svgElement.setAttribute('fill', 'url(#' + id + ')'); } else { svgElement.setAttribute('fill', 'none'); } }; /** * Sets the stroke of the given element. * @param {goog.graphics.StrokeAndFillElement} element The element wrapper. * @param {goog.graphics.Stroke?} stroke The stroke object. */ goog.graphics.SvgGraphics.prototype.setElementStroke = function(element, stroke) { var svgElement = element.getElement(); if (stroke) { svgElement.setAttribute('stroke', stroke.getColor()); var width = stroke.getWidth(); if (goog.isString(width) && width.indexOf('px') != -1) { svgElement.setAttribute('stroke-width', parseFloat(width) / this.getPixelScaleX()); } else { svgElement.setAttribute('stroke-width', width); } } else { svgElement.setAttribute('stroke', 'none'); } }; /** * Set the transformation of an element. * @param {goog.graphics.Element} element The element wrapper. * @param {number} x The x coordinate of the translation transform. * @param {number} y The y coordinate of the translation transform. * @param {number} angle The angle of the rotation transform. * @param {number} centerX The horizontal center of the rotation transform. * @param {number} centerY The vertical center of the rotation transform. */ goog.graphics.SvgGraphics.prototype.setElementTransform = function(element, x, y, angle, centerX, centerY) { element.getElement().setAttribute('transform', 'translate(' + x + ',' + y + ') rotate(' + angle + ' ' + centerX + ' ' + centerY + ')'); }; /** * Creates the DOM representation of the graphics area. */ goog.graphics.SvgGraphics.prototype.createDom = function() { // Set up the standard attributes. var attributes = { 'width': this.width, 'height': this.height, 'overflow': 'hidden' }; var svgElement = this.createSvgElement_('svg', attributes); var groupElement = this.createSvgElement_('g'); this.defsElement_ = this.createSvgElement_('defs'); this.canvasElement = new goog.graphics.SvgGroupElement(groupElement, this); svgElement.appendChild(this.defsElement_); svgElement.appendChild(groupElement); // Use the svgElement as the root element. this.setElementInternal(svgElement); // Set up the coordinate system. this.setViewBox_(); }; /** * Changes the coordinate system position. * @param {number} left The coordinate system left bound. * @param {number} top The coordinate system top bound. */ goog.graphics.SvgGraphics.prototype.setCoordOrigin = function(left, top) { this.coordLeft = left; this.coordTop = top; this.setViewBox_(); }; /** * Changes the coordinate size. * @param {number} coordWidth The coordinate width. * @param {number} coordHeight The coordinate height. */ goog.graphics.SvgGraphics.prototype.setCoordSize = function(coordWidth, coordHeight) { goog.graphics.SvgGraphics.superClass_.setCoordSize.apply( this, arguments); this.setViewBox_(); }; /** * @return {string} The view box string. * @private */ goog.graphics.SvgGraphics.prototype.getViewBox_ = function() { return this.coordLeft + ' ' + this.coordTop + ' ' + (this.coordWidth ? this.coordWidth + ' ' + this.coordHeight : ''); }; /** * Sets up the view box. * @private */ goog.graphics.SvgGraphics.prototype.setViewBox_ = function() { if (this.coordWidth || this.coordLeft || this.coordTop) { this.getElement().setAttribute('preserveAspectRatio', 'none'); if (this.useManualViewbox_) { this.updateManualViewBox_(); } else { this.getElement().setAttribute('viewBox', this.getViewBox_()); } } }; /** * Updates the transform of the root element to fake a viewBox. Should only * be called when useManualViewbox_ is set. * @private */ goog.graphics.SvgGraphics.prototype.updateManualViewBox_ = function() { if (!this.isInDocument() || !(this.coordWidth || this.coordLeft || !this.coordTop)) { return; } var size = this.getPixelSize(); if (size.width == 0) { // In Safari, invisible SVG is sometimes shown. Explicitly hide it. this.getElement().style.visibility = 'hidden'; return; } this.getElement().style.visibility = ''; var offsetX = - this.coordLeft; var offsetY = - this.coordTop; var scaleX = size.width / this.coordWidth; var scaleY = size.height / this.coordHeight; this.canvasElement.getElement().setAttribute('transform', 'scale(' + scaleX + ' ' + scaleY + ') ' + 'translate(' + offsetX + ' ' + offsetY + ')'); }; /** * Change the size of the canvas. * @param {number} pixelWidth The width in pixels. * @param {number} pixelHeight The height in pixels. */ goog.graphics.SvgGraphics.prototype.setSize = function(pixelWidth, pixelHeight) { goog.style.setSize(this.getElement(), pixelWidth, pixelHeight); }; /** @override */ goog.graphics.SvgGraphics.prototype.getPixelSize = function() { if (!goog.userAgent.GECKO) { return this.isInDocument() ? goog.style.getSize(this.getElement()) : goog.base(this, 'getPixelSize'); } // In Gecko, goog.style.getSize does not work for SVG elements. We have to // compute the size manually if it is percentage based. var width = this.width; var height = this.height; var computeWidth = goog.isString(width) && width.indexOf('%') != -1; var computeHeight = goog.isString(height) && height.indexOf('%') != -1; if (!this.isInDocument() && (computeWidth || computeHeight)) { return null; } var parent; var parentSize; if (computeWidth) { parent = /** @type {Element} */ (this.getElement().parentNode); parentSize = goog.style.getSize(parent); width = parseFloat(/** @type {string} */ (width)) * parentSize.width / 100; } if (computeHeight) { parent = parent || /** @type {Element} */ (this.getElement().parentNode); parentSize = parentSize || goog.style.getSize(parent); height = parseFloat(/** @type {string} */ (height)) * parentSize.height / 100; } return new goog.math.Size(/** @type {number} */ (width), /** @type {number} */ (height)); }; /** * Remove all drawing elements from the graphics. */ goog.graphics.SvgGraphics.prototype.clear = function() { this.canvasElement.clear(); goog.dom.removeChildren(this.defsElement_); this.defs_ = {}; }; /** * Draw an ellipse. * * @param {number} cx Center X coordinate. * @param {number} cy Center Y coordinate. * @param {number} rx Radius length for the x-axis. * @param {number} ry Radius length for the y-axis. * @param {goog.graphics.Stroke?} stroke Stroke object describing the * stroke. * @param {goog.graphics.Fill?} fill Fill object describing the fill. * @param {goog.graphics.SvgGroupElement=} opt_group The group wrapper element * to append to. If not specified, appends to the main canvas. * * @return {goog.graphics.EllipseElement} The newly created element. */ goog.graphics.SvgGraphics.prototype.drawEllipse = function( cx, cy, rx, ry, stroke, fill, opt_group) { var element = this.createSvgElement_('ellipse', {'cx': cx, 'cy': cy, 'rx': rx, 'ry': ry}); var wrapper = new goog.graphics.SvgEllipseElement(element, this, stroke, fill); this.append_(wrapper, opt_group); return wrapper; }; /** * Draw a rectangle. * * @param {number} x X coordinate (left). * @param {number} y Y coordinate (top). * @param {number} width Width of rectangle. * @param {number} height Height of rectangle. * @param {goog.graphics.Stroke?} stroke Stroke object describing the * stroke. * @param {goog.graphics.Fill?} fill Fill object describing the fill. * @param {goog.graphics.SvgGroupElement=} opt_group The group wrapper element * to append to. If not specified, appends to the main canvas. * * @return {goog.graphics.RectElement} The newly created element. */ goog.graphics.SvgGraphics.prototype.drawRect = function(x, y, width, height, stroke, fill, opt_group) { var element = this.createSvgElement_('rect', {'x': x, 'y': y, 'width': width, 'height': height}); var wrapper = new goog.graphics.SvgRectElement(element, this, stroke, fill); this.append_(wrapper, opt_group); return wrapper; }; /** * Draw an image. * * @param {number} x X coordinate (left). * @param {number} y Y coordinate (top). * @param {number} width Width of the image. * @param {number} height Height of the image. * @param {string} src The source fo the image. * @param {goog.graphics.SvgGroupElement=} opt_group The group wrapper element * to append to. If not specified, appends to the main canvas. * * @return {goog.graphics.ImageElement} The newly created image wrapped in a * rectangle element. */ goog.graphics.SvgGraphics.prototype.drawImage = function(x, y, width, height, src, opt_group) { var element = this.createSvgElement_('image', { 'x': x, 'y': y, 'width': width, 'height': height, 'image-rendering': 'optimizeQuality', 'preserveAspectRatio': 'none' }); element.setAttributeNS('http://www.w3.org/1999/xlink', 'href', src); var wrapper = new goog.graphics.SvgImageElement(element, this); this.append_(wrapper, opt_group); return wrapper; }; /** * Draw a text string vertically centered on a given line. * * @param {string} text The text to draw. * @param {number} x1 X coordinate of start of line. * @param {number} y1 Y coordinate of start of line. * @param {number} x2 X coordinate of end of line. * @param {number} y2 Y coordinate of end of line. * @param {string} align Horizontal alignment: left (default), center, right. * @param {goog.graphics.Font} font Font describing the font properties. * @param {goog.graphics.Stroke?} stroke Stroke object describing the * stroke. * @param {goog.graphics.Fill?} fill Fill object describing the fill. * @param {goog.graphics.SvgGroupElement=} opt_group The group wrapper element * to append to. If not specified, appends to the main canvas. * * @return {goog.graphics.TextElement} The newly created element. */ goog.graphics.SvgGraphics.prototype.drawTextOnLine = function( text, x1, y1, x2, y2, align, font, stroke, fill, opt_group) { var angle = Math.round(goog.math.angle(x1, y1, x2, y2)); var dx = x2 - x1; var dy = y2 - y1; var lineLength = Math.round(Math.sqrt(dx * dx + dy * dy)); // Length of line // SVG baseline is on the glyph's base line. We estimate it as 85% of the // font height. This is just a rough estimate, but do not have a better way. var fontSize = font.size; var attributes = {'font-family': font.family, 'font-size': fontSize}; var baseline = Math.round(fontSize * 0.85); var textY = Math.round(y1 - (fontSize / 2) + baseline); var textX = x1; if (align == 'center') { textX += Math.round(lineLength / 2); attributes['text-anchor'] = 'middle'; } else if (align == 'right') { textX += lineLength; attributes['text-anchor'] = 'end'; } attributes['x'] = textX; attributes['y'] = textY; if (font.bold) { attributes['font-weight'] = 'bold'; } if (font.italic) { attributes['font-style'] = 'italic'; } if (angle != 0) { attributes['transform'] = 'rotate(' + angle + ' ' + x1 + ' ' + y1 + ')'; } var element = this.createSvgElement_('text', attributes); element.appendChild(this.dom_.getDocument().createTextNode(text)); // Bypass a Firefox-Mac bug where text fill is ignored. If text has no stroke, // set a stroke, otherwise the text will not be visible. if (stroke == null && goog.userAgent.GECKO && goog.userAgent.MAC) { var color = 'black'; // For solid fills, use the fill color if (fill instanceof goog.graphics.SolidFill) { color = fill.getColor(); } stroke = new goog.graphics.Stroke(1, color); } var wrapper = new goog.graphics.SvgTextElement(element, this, stroke, fill); this.append_(wrapper, opt_group); return wrapper; }; /** * Draw a path. * * @param {goog.graphics.Path} path The path object to draw. * @param {goog.graphics.Stroke?} stroke Stroke object describing the * stroke. * @param {goog.graphics.Fill?} fill Fill object describing the fill. * @param {goog.graphics.SvgGroupElement=} opt_group The group wrapper element * to append to. If not specified, appends to the main canvas. * * @return {goog.graphics.PathElement} The newly created element. */ goog.graphics.SvgGraphics.prototype.drawPath = function( path, stroke, fill, opt_group) { var element = this.createSvgElement_('path', {'d': goog.graphics.SvgGraphics.getSvgPath(path)}); var wrapper = new goog.graphics.SvgPathElement(element, this, stroke, fill); this.append_(wrapper, opt_group); return wrapper; }; /** * Returns a string representation of a logical path suitable for use in * an SVG element. * * @param {goog.graphics.Path} path The logical path. * @return {string} The SVG path representation. */ goog.graphics.SvgGraphics.getSvgPath = function(path) { var list = []; path.forEachSegment(function(segment, args) { switch (segment) { case goog.graphics.Path.Segment.MOVETO: list.push('M'); Array.prototype.push.apply(list, args); break; case goog.graphics.Path.Segment.LINETO: list.push('L'); Array.prototype.push.apply(list, args); break; case goog.graphics.Path.Segment.CURVETO: list.push('C'); Array.prototype.push.apply(list, args); break; case goog.graphics.Path.Segment.ARCTO: var extent = args[3]; var toAngle = args[2] + extent; list.push('A', args[0], args[1], 0, Math.abs(extent) > 180 ? 1 : 0, extent > 0 ? 1 : 0, args[4], args[5]); break; case goog.graphics.Path.Segment.CLOSE: list.push('Z'); break; } }); return list.join(' '); }; /** * Create an empty group of drawing elements. * * @param {goog.graphics.SvgGroupElement=} opt_group The group wrapper element * to append to. If not specified, appends to the main canvas. * * @return {goog.graphics.GroupElement} The newly created group. */ goog.graphics.SvgGraphics.prototype.createGroup = function(opt_group) { var element = this.createSvgElement_('g'); var parent = opt_group || this.canvasElement; parent.getElement().appendChild(element); return new goog.graphics.SvgGroupElement(element, this); }; /** * Measure and return the width (in pixels) of a given text string. * Text measurement is needed to make sure a text can fit in the allocated area. * The way text length is measured is by writing it into a div that is after * the visible area, measure the div width, and immediatly erase the written * value. * * @param {string} text The text string to measure. * @param {goog.graphics.Font} font The font object describing the font style. */ goog.graphics.SvgGraphics.prototype.getTextWidth = function(text, font) { // TODO(user) Implement }; /** * Adds a defintion of an element to the global definitions. * @param {string} defKey This is a key that should be unique in a way that * if two definitions are equal the should have the same key. * @param {Element} defElement DOM element to add as a definition. It must * have an id attribute set. * @return {string} The assigned id of the defElement. * @private */ goog.graphics.SvgGraphics.prototype.addDef_ = function(defKey, defElement) { if (defKey in this.defs_) { return this.defs_[defKey]; } var id = goog.graphics.SvgGraphics.DEF_ID_PREFIX_ + goog.graphics.SvgGraphics.nextDefId_++; defElement.setAttribute('id', id); this.defs_[defKey] = id; // Add the def defElement of the defs list. var defs = this.defsElement_; defs.appendChild(defElement); return id; }; /** * Returns the id of a definition element. * @param {string} defKey This is a key that should be unique in a way that * if two definitions are equal the should have the same key. * @return {string} The id of the found definition element or null if * not found. * @private */ goog.graphics.SvgGraphics.prototype.getDef_ = function(defKey) { return defKey in this.defs_ ? this.defs_[defKey] : null; }; /** @override */ goog.graphics.SvgGraphics.prototype.enterDocument = function() { var oldPixelSize = this.getPixelSize(); goog.graphics.SvgGraphics.superClass_.enterDocument.call(this); // Dispatch a resize if this is the first time the size value is accurate. if (!oldPixelSize) { this.dispatchEvent(goog.events.EventType.RESIZE); } // For percentage based heights, listen for changes to size. if (this.useManualViewbox_) { var width = this.width; var height = this.height; if (typeof width == 'string' && width.indexOf('%') != -1 && typeof height == 'string' && height.indexOf('%') != -1) { // SVG elements don't behave well with respect to size events, so we // resort to polling. this.handler_.listen(goog.graphics.SvgGraphics.getResizeCheckTimer_(), goog.Timer.TICK, this.updateManualViewBox_); } this.updateManualViewBox_(); } }; /** @override */ goog.graphics.SvgGraphics.prototype.exitDocument = function() { goog.graphics.SvgGraphics.superClass_.exitDocument.call(this); // Stop polling. if (this.useManualViewbox_) { this.handler_.unlisten(goog.graphics.SvgGraphics.getResizeCheckTimer_(), goog.Timer.TICK, this.updateManualViewBox_); } }; /** * Disposes of the component by removing event handlers, detacing DOM nodes from * the document body, and removing references to them. * @override * @protected */ goog.graphics.SvgGraphics.prototype.disposeInternal = function() { delete this.defs_; delete this.defsElement_; delete this.canvasElement; goog.graphics.SvgGraphics.superClass_.disposeInternal.call(this); }; /** * The centralized resize checking timer. * @type {goog.Timer|undefined} * @private */ goog.graphics.SvgGraphics.resizeCheckTimer_; /** * @return {goog.Timer} The centralized timer object used for interval timing. * @private */ goog.graphics.SvgGraphics.getResizeCheckTimer_ = function() { if (!goog.graphics.SvgGraphics.resizeCheckTimer_) { goog.graphics.SvgGraphics.resizeCheckTimer_ = new goog.Timer(400); goog.graphics.SvgGraphics.resizeCheckTimer_.start(); } return /** @type {goog.Timer} */ ( goog.graphics.SvgGraphics.resizeCheckTimer_); }; /** @override */ goog.graphics.SvgGraphics.prototype.isDomClonable = function() { return true; };